2026-04-17 11:32:17 +08:00

440 lines
22 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Link from "next/link";
import { redirect } from "next/navigation";
import { BindDeviceForm, SendMessageForm } from "@/components/dashboard-actions";
import { getCaregiverDashboard } from "@/lib/monitor-data";
import {
buildCaregiverSessionBootstrapPath,
getCaregiverSession,
} from "@/lib/session";
export const dynamic = "force-dynamic";
function formatTime(date: Date | null | undefined) {
if (!date) {
return "暂时还没有记录";
}
return new Intl.DateTimeFormat("zh-CN", {
month: "numeric",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(date);
}
function getImportanceLabel(level: string) {
switch (level) {
case "LOW":
return "温和提醒";
case "HIGH":
return "希望尽快看到";
case "URGENT":
return "需要尽快联系";
default:
return "日常问候";
}
}
function getUsageLabel(eventType: string) {
switch (eventType) {
case "APP_OPEN":
return "打开了老人端应用";
case "SETTINGS_OPENED":
return "打开了家人连接页";
case "AI_SESSION_STARTED":
return "开始了一次陪伴对话";
case "AI_SESSION_ENDED":
return "结束了一次陪伴对话";
case "CAMERA_ENABLED":
return "打开了镜头模式";
case "CAMERA_DISABLED":
return "关闭了镜头模式";
case "TOOL_CALLED":
return "模型调用了一次桥接工具";
default:
return eventType;
}
}
export default async function Home() {
const caregiver = await getCaregiverSession();
if (!caregiver) {
redirect(buildCaregiverSessionBootstrapPath("/"));
}
const dashboard = await getCaregiverDashboard(caregiver.id);
const totalUnreadForElder = dashboard.devices.reduce(
(total, device) => total + device.familyUnreadCount,
0,
);
const totalUnreadForCaregiver = dashboard.devices.reduce(
(total, device) => total + device.elderUnreadCount,
0,
);
const totalConversations = dashboard.devices.reduce(
(total, device) => total + device.elderDevice._count.conversationTurns,
0,
);
return (
<main className="relative overflow-hidden px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
<div className="pointer-events-none absolute inset-x-0 top-0 h-64 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.78),transparent_68%)]" />
<div className="mx-auto max-w-7xl space-y-6">
<section className="paper-panel relative overflow-hidden rounded-[36px] border border-white/70 px-6 py-8 shadow-[0_35px_90px_rgba(105,68,41,0.13)] sm:px-8 lg:px-10 lg:py-10">
<div className="absolute -right-10 top-[-52px] h-40 w-40 rounded-full bg-[rgba(200,103,51,0.10)] blur-2xl" />
<div className="absolute left-10 top-8 h-20 w-20 rounded-full border border-[rgba(118,132,94,0.18)]" />
<div className="grid gap-8 lg:grid-cols-[1.2fr_0.8fr] lg:items-end">
<div>
<p className="text-sm font-semibold tracking-[0.24em] text-[var(--copper)] uppercase">
Digital Human Bridge
</p>
<h1 className="mt-4 max-w-3xl font-[family-name:var(--font-display)] text-4xl leading-[1.15] text-[var(--ink)] sm:text-5xl">
</h1>
<p className="mt-5 max-w-2xl text-base leading-8 text-[var(--muted)] sm:text-lg">
使
</p>
<div className="mt-6 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
<span className="rounded-full border border-[var(--line)] bg-white/70 px-4 py-2">
{dashboard.caregiver.sessionToken.slice(0, 8).toUpperCase()}
</span>
<span className="rounded-full border border-[var(--line)] bg-white/70 px-4 py-2">
{dashboard.devices.length}
</span>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-3 lg:grid-cols-1">
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{dashboard.devices.length}
</p>
</div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{totalUnreadForElder}
</p>
</div>
<div className="rounded-[28px] border border-white/80 bg-white/88 p-5">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-3 font-[family-name:var(--font-display)] text-4xl text-[var(--ink)]">
{totalConversations}
</p>
</div>
</div>
</div>
</section>
<BindDeviceForm />
<section className="grid gap-6 lg:grid-cols-[0.85fr_1.15fr]">
<div className="rounded-[32px] border border-white/70 bg-white/78 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<div className="mt-4 space-y-4">
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-2 text-3xl font-semibold text-[var(--ink)]">
{totalUnreadForCaregiver}
</p>
</div>
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-2 text-3xl font-semibold text-[var(--ink)]">
{totalUnreadForElder}
</p>
</div>
<div className="rounded-[24px] bg-[var(--paper-soft)] px-5 py-4">
<p className="text-sm text-[var(--muted)]"></p>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
</p>
<Link
href="/scan"
className="mt-4 inline-flex h-11 items-center justify-center rounded-full bg-[var(--olive)] px-4 text-sm font-semibold text-white transition hover:bg-[var(--olive-deep)]"
>
</Link>
</div>
</div>
</div>
<div className="rounded-[32px] border border-white/70 bg-white/78 p-6 shadow-[0_26px_60px_rgba(115,76,42,0.12)] backdrop-blur">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<div className="mt-4 grid gap-4 sm:grid-cols-3">
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
function calling
</p>
</div>
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
使便
</p>
</div>
<div className="rounded-[24px] bg-[var(--paper-soft)] p-5">
<h3 className="font-[family-name:var(--font-display)] text-xl text-[var(--ink)]">
</h3>
<p className="mt-2 text-sm leading-7 text-[var(--muted)]">
function calling
</p>
</div>
</div>
</div>
</section>
{dashboard.devices.length === 0 ? (
<section className="rounded-[32px] border border-dashed border-[var(--line)] bg-white/70 px-6 py-10 text-center shadow-[0_20px_50px_rgba(115,76,42,0.10)]">
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
</h2>
<p className="mx-auto mt-4 max-w-2xl text-base leading-8 text-[var(--muted)]">
</p>
</section>
) : (
<section className="space-y-6">
{dashboard.devices.map((binding) => {
const familyMessages = binding.elderDevice.messages.filter(
(message) => message.direction === "FAMILY_TO_ELDER",
);
const elderMessages = binding.elderDevice.messages.filter(
(message) => message.direction === "ELDER_TO_FAMILY",
);
return (
<article
key={binding.id}
className="rounded-[36px] border border-white/75 bg-white/80 p-6 shadow-[0_28px_70px_rgba(108,73,41,0.12)] backdrop-blur lg:p-7"
>
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
<div>
<p className="text-sm font-medium tracking-[0.18em] text-[var(--copper)] uppercase">
</p>
<h2 className="mt-2 font-[family-name:var(--font-display)] text-3xl text-[var(--ink)]">
{binding.elderDevice.displayName || "未命名陪伴设备"}
</h2>
<div className="mt-3 flex flex-wrap gap-3 text-sm text-[var(--muted)]">
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
UUID{binding.elderDevice.deviceUuid}
</span>
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
线{formatTime(binding.elderDevice.lastSeenAt)}
</span>
<span className="rounded-full border border-[var(--line)] bg-[var(--paper-soft)] px-4 py-2">
{binding.bindUrl}
</span>
</div>
</div>
<div className="grid gap-3 sm:grid-cols-4">
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderUnreadCount}
</p>
</div>
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.familyUnreadCount}
</p>
</div>
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderDevice._count.conversationTurns}
</p>
</div>
<div className="rounded-[22px] bg-[var(--paper-soft)] px-4 py-4">
<p className="text-xs text-[var(--muted)]"></p>
<p className="mt-2 text-2xl font-semibold text-[var(--ink)]">
{binding.elderDevice._count.toolCalls}
</p>
</div>
</div>
</div>
<div className="mt-6 grid gap-6 xl:grid-cols-[0.95fr_1.05fr]">
<div className="space-y-6">
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
</div>
<span className="rounded-full bg-white px-4 py-2 text-sm text-[var(--ink)]">
{binding.elderUnreadCount}
</span>
</div>
<div className="mt-4 space-y-3">
{elderMessages.length === 0 ? (
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
</p>
) : (
elderMessages.map((message) => (
<div key={message.id} className="rounded-[22px] bg-white/88 px-4 py-4">
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
{getImportanceLabel(message.importance)}
</span>
{message.recipientRelation ? (
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
{message.recipientRelation}
</span>
) : null}
<span>{formatTime(message.createdAt)}</span>
</div>
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
{message.content}
</p>
</div>
))
)}
</div>
</section>
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
<div className="mt-4 space-y-3">
{binding.elderDevice.usageEvents.length === 0 ? (
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
使
</p>
) : (
binding.elderDevice.usageEvents.map((event) => (
<div key={event.id} className="rounded-[22px] bg-white/88 px-4 py-4">
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
<span>{getUsageLabel(event.eventType)}</span>
<span>{formatTime(event.createdAt)}</span>
</div>
</div>
))
)}
</div>
</section>
</div>
<div className="space-y-6">
<SendMessageForm deviceUuid={binding.elderDevice.deviceUuid} />
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
<div className="flex items-center justify-between gap-3">
<div>
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
</div>
<span className="rounded-full bg-white px-4 py-2 text-sm text-[var(--ink)]">
{binding.familyUnreadCount}
</span>
</div>
<div className="mt-4 space-y-3">
{familyMessages.length === 0 ? (
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
</p>
) : (
familyMessages.map((message) => (
<div key={message.id} className="rounded-[22px] bg-white/88 px-4 py-4">
<div className="flex flex-wrap items-center gap-2 text-xs text-[var(--muted)]">
<span className="rounded-full bg-[var(--paper-soft)] px-3 py-1">
{getImportanceLabel(message.importance)}
</span>
<span>{formatTime(message.createdAt)}</span>
<span>
{message.readAt ? "老人已看过" : "等待老人查看"}
</span>
</div>
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
{message.content}
</p>
</div>
))
)}
</div>
</section>
<section className="rounded-[28px] bg-[var(--paper-soft)] p-5">
<p className="text-sm font-medium tracking-[0.16em] text-[var(--copper)] uppercase">
</p>
<h3 className="mt-2 font-[family-name:var(--font-display)] text-2xl text-[var(--ink)]">
</h3>
<div className="mt-4 space-y-3">
{binding.elderDevice.conversationTurns.length === 0 ? (
<p className="rounded-[22px] bg-white/80 px-4 py-4 text-sm leading-7 text-[var(--muted)]">
</p>
) : (
binding.elderDevice.conversationTurns.map((turn) => (
<div key={turn.id} className="rounded-[22px] bg-white/88 px-4 py-4">
<div className="flex items-center justify-between gap-3 text-xs text-[var(--muted)]">
<span>
{turn.role === "USER"
? "老人说"
: turn.role === "ASSISTANT"
? "数字人回复"
: "工具结果"}
</span>
<span>{formatTime(turn.createdAt)}</span>
</div>
<p className="mt-3 text-sm leading-7 text-[var(--ink)]">
{turn.content}
</p>
</div>
))
)}
</div>
</section>
</div>
</div>
</article>
);
})}
</section>
)}
</div>
</main>
);
}