440 lines
22 KiB
TypeScript
440 lines
22 KiB
TypeScript
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>
|
||
);
|
||
}
|