fix: make reminder actions use real resources
This commit is contained in:
Vendored
+2
@@ -4202,6 +4202,8 @@ export interface components {
|
||||
id: number;
|
||||
/** @enum {string} */
|
||||
type: "device.presence.changed" | "conversation.started" | "conversation.completed" | "conversation.failed" | "story.created" | "story.revised" | "media.upload.completed" | "export.ready" | "privacy_request.updated" | "device.command.updated";
|
||||
/** Format: uuid */
|
||||
household_id: string;
|
||||
resource_type: string;
|
||||
/** Format: uuid */
|
||||
resource_id: string;
|
||||
|
||||
@@ -673,15 +673,17 @@ input[type="checkbox"], input[type="radio"], input[type="range"] { accent-color:
|
||||
.reminder-card > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 19px 20px 14px; border-bottom: 1px solid var(--v074-line); }
|
||||
.reminder-card h3 { margin: 0; font-size: 20px; }
|
||||
.reminder-card header p { margin: 6px 0 0; color: var(--v074-muted); font-size: 12px; }
|
||||
.reminder-row { display: grid; grid-template-columns: 78px 14px minmax(0,1fr) auto; align-items: center; gap: 10px; min-height: 66px; padding: 12px 20px; border-bottom: 1px solid var(--v074-line); }
|
||||
.reminder-row { display: grid; grid-template-columns: 78px 14px minmax(0,1fr) auto; align-items: center; gap: 10px; width: 100%; min-height: 66px; padding: 12px 20px; border: 0; border-bottom: 1px solid var(--v074-line); border-radius: 0; color: var(--v074-ink); background: transparent; text-align: left; box-shadow: none; }
|
||||
.reminder-row:last-child { border-bottom: 0; }
|
||||
.reminder-row:hover,.reminder-row:focus-visible { background: #f8fafb; }
|
||||
.reminder-row time,.reminder-row small { color: var(--v074-muted); font-size: 12px; }
|
||||
.reminder-row__dot { width: 10px; height: 10px; border-radius: 50%; background: var(--v074-blue); }
|
||||
.reminder-row p,.reminder-todo p { margin: 4px 0 0; color: var(--v074-muted); font-size: 12px; }
|
||||
.reminder-count { display: grid; place-items: center; min-width: 28px; height: 28px; border-radius: 999px; color: var(--v074-coral); background: var(--v074-coral-soft); font-size: 12px; }
|
||||
.reminder-todos { display: grid; }
|
||||
.reminder-todo { display: grid; grid-template-columns: 10px minmax(0,1fr) auto; align-items: center; gap: 12px; min-height: 72px; padding: 13px 18px; border-bottom: 1px solid var(--v074-line); }
|
||||
.reminder-todo { display: grid; grid-template-columns: 10px minmax(0,1fr) auto; align-items: center; gap: 12px; width: 100%; min-height: 72px; padding: 13px 18px; border: 0; border-bottom: 1px solid var(--v074-line); border-radius: 0; color: var(--v074-ink); background: transparent; text-align: left; box-shadow: none; }
|
||||
.reminder-todo:last-child { border-bottom: 0; }
|
||||
.reminder-todo:hover,.reminder-todo:focus-visible { background: #faf8f5; }
|
||||
.reminder-todo > span { width: 8px; height: 8px; border-radius: 50%; background: var(--v074-blue); }
|
||||
.reminder-todo--warning > span { background: var(--v074-coral); }
|
||||
.reminder-todo small { color: var(--v074-blue); white-space: nowrap; }
|
||||
@@ -689,8 +691,9 @@ input[type="checkbox"], input[type="radio"], input[type="range"] { accent-color:
|
||||
.reminder-empty .anticon { color: #9aa9b6; font-size: 30px; }
|
||||
.reminder-empty strong { color: var(--v074-ink); }
|
||||
.reminder-history__summary { display: grid; grid-template-columns: repeat(4,1fr); }
|
||||
.reminder-history__summary > div { padding: 18px 20px; border-right: 1px solid var(--v074-line); }
|
||||
.reminder-history__summary > div:last-child { border-right: 0; }
|
||||
.reminder-history__summary > div,.reminder-history__summary > button { padding: 18px 20px; border: 0; border-right: 1px solid var(--v074-line); border-radius: 0; color: var(--v074-ink); background: transparent; text-align: left; box-shadow: none; }
|
||||
.reminder-history__summary > button:hover,.reminder-history__summary > button:focus-visible { background: #f8fafb; }
|
||||
.reminder-history__summary > :last-child { border-right: 0; }
|
||||
.reminder-history__summary strong { display: block; font-size: 24px; }
|
||||
.reminder-history__summary span { color: var(--v074-muted); font-size: 12px; }
|
||||
|
||||
@@ -747,6 +750,6 @@ input[type="checkbox"], input[type="radio"], input[type="range"] { accent-color:
|
||||
.reminder-row { grid-template-columns: 62px 12px minmax(0,1fr); }
|
||||
.reminder-row small { display: none; }
|
||||
.reminder-history__summary { grid-template-columns: repeat(2,1fr); }
|
||||
.reminder-history__summary > div:nth-child(2) { border-right: 0; }
|
||||
.reminder-history__summary > div:nth-child(-n+2) { border-bottom: 1px solid var(--v074-line); }
|
||||
.reminder-history__summary > :nth-child(2) { border-right: 0; }
|
||||
.reminder-history__summary > :nth-child(-n+2) { border-bottom: 1px solid var(--v074-line); }
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ const sectionMeta: Record<PortalSection, { title: string; description: string; b
|
||||
sharing: { title: "内容分享", description: "向指定家人分享故事、文字或录音,并随时撤销权限。", breadcrumb: "隐私与授权 / 内容分享" },
|
||||
privacy: { title: "数据与授权", description: "按项目查看授权,并申请访问、更正、撤回、删除或私有导出。", breadcrumb: "隐私与授权 / 数据与授权" },
|
||||
billing: { title: "服务用量", description: "围绕长者聊天查看时长、活跃日和最近通话。", breadcrumb: "服务用量" },
|
||||
messages: { title: "日程与提醒", description: "查看陪伴动态、设备状态和需要处理的家庭提醒。", breadcrumb: "日程与提醒" },
|
||||
messages: { title: "提醒与待办", description: "查看服务端记录的陪伴动态,并处理故事、设备和隐私事项。", breadcrumb: "提醒与待办" },
|
||||
models: { title: "大模型管理", description: "配置、测试并激活用于聊天总结、关键词和故事候选生成的大模型。", breadcrumb: "设置与权限 / 大模型管理" },
|
||||
profile: { title: "设置与权限", description: "查看登录身份、家庭角色和全部权限范围。", breadcrumb: "设置与权限" }
|
||||
};
|
||||
@@ -85,7 +85,7 @@ export function CustomerPortal() {
|
||||
{ key: "conversations", icon: <CommentOutlined />, label: "聊天记录" },
|
||||
{ key: "stories", icon: <BookOutlined />, label: "故事与记忆" },
|
||||
{ key: "subjects", icon: <HeartOutlined />, label: "长者资料" },
|
||||
{ key: "messages", icon: <MessageOutlined />, label: "日程与提醒" },
|
||||
{ key: "messages", icon: <MessageOutlined />, label: "提醒与待办" },
|
||||
{ key: "members", icon: <TeamOutlined />, label: "家庭管理" },
|
||||
{ type: "divider" },
|
||||
{ key: "devices", icon: <LaptopOutlined />, label: "陪伴设备" },
|
||||
@@ -145,7 +145,7 @@ export function CustomerPortal() {
|
||||
</div>
|
||||
<div className="customer-header__actions">
|
||||
<Input.Search value={searchValue} onChange={(event) => setSearchValue(event.target.value)} onSearch={search} prefix={<SearchOutlined />} placeholder="搜索长者、设备、通话" allowClear className="customer-search" />
|
||||
<Tooltip title="消息中心"><Badge dot><Button type="text" icon={<BellOutlined />} aria-label="打开消息中心" onClick={() => navigate("messages")} /></Badge></Tooltip>
|
||||
<Tooltip title="提醒与待办"><Badge dot><Button type="text" icon={<BellOutlined />} aria-label="打开提醒与待办" onClick={() => navigate("messages")} /></Badge></Tooltip>
|
||||
{auth.currentUser || designPreview ? <Dropdown menu={{ items: accountMenu }} placement="bottomRight">
|
||||
<Button type="text" className="customer-account-button">
|
||||
<Avatar size={36} src={designPreview ? "/assets/family-chen.png" : undefined} icon={<UserOutlined />} />
|
||||
|
||||
@@ -82,6 +82,7 @@ type Tab = CustomerSection | "firmware";
|
||||
type LoadState = "idle" | "loading" | "ready" | "error";
|
||||
type MediaLoadState = "idle" | "loading" | "ready" | "restricted" | "error";
|
||||
type EventStreamState = "connecting" | "connected" | "reconnecting";
|
||||
type WebEventNotice = { id: string; type: string; householdID: string; resourceType: string; resourceID: string; subjectID?: string; status: string; occurredAt: string };
|
||||
type ConversationPace = "slow" | "normal" | "lively";
|
||||
type DialoguePreset = {
|
||||
key: "daily" | "memory" | "interest";
|
||||
@@ -219,6 +220,7 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
|
||||
const [firmwareReleaseImpact, setFirmwareReleaseImpact] = useState<FirmwareReleaseImpact>();
|
||||
const [firmwareImpactLoading, setFirmwareImpactLoading] = useState(false);
|
||||
const [selectedStory, setSelectedStory] = useState<Story>();
|
||||
const [storyReviewRequestID, setStoryReviewRequestID] = useState<string>();
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
const [refreshRevision, setRefreshRevision] = useState(0);
|
||||
const [actionBusy, setActionBusy] = useState(false);
|
||||
@@ -269,7 +271,7 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
|
||||
const [rollbackReleaseID, setRollbackReleaseID] = useState("");
|
||||
const [eventStreamState, setEventStreamState] = useState<EventStreamState>("connecting");
|
||||
const [lastEventAt, setLastEventAt] = useState<string>();
|
||||
const [eventNotices, setEventNotices] = useState<Array<{ id: string; type: string; resourceType: string; status: string; occurredAt: string }>>([]);
|
||||
const [eventNotices, setEventNotices] = useState<WebEventNotice[]>([]);
|
||||
const eventRefreshTimer = useRef<number | undefined>(undefined);
|
||||
const lastEventRefreshAt = useRef(0);
|
||||
const conversationDetailController = useRef<AbortController | undefined>(undefined);
|
||||
@@ -278,6 +280,7 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
|
||||
useEffect(() => setTab(section), [section]);
|
||||
|
||||
function selectSection(next: CustomerSection) {
|
||||
if (next !== "stories") setStoryReviewRequestID(undefined);
|
||||
setTab(next);
|
||||
onSectionChange?.(next);
|
||||
}
|
||||
@@ -380,11 +383,14 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
|
||||
if (data) {
|
||||
setLastEventAt(new Date().toISOString());
|
||||
try {
|
||||
const item = JSON.parse(data) as { id?: number; type?: string; resource_type?: string; status?: string; occurred_at?: string };
|
||||
const item = JSON.parse(data) as { id?: number; type?: string; household_id?: string; resource_type?: string; resource_id?: string; subject_id?: string; status?: string; occurred_at?: string };
|
||||
setEventNotices((current) => [{
|
||||
id: String(item.id ?? id),
|
||||
type: item.type ?? "resource.updated",
|
||||
householdID: item.household_id ?? "",
|
||||
resourceType: item.resource_type ?? "resource",
|
||||
resourceID: item.resource_id ?? "",
|
||||
subjectID: item.subject_id,
|
||||
status: item.status ?? "UPDATED",
|
||||
occurredAt: item.occurred_at ?? new Date().toISOString()
|
||||
}, ...current.filter((notice) => notice.id !== String(item.id ?? id))].slice(0, 50));
|
||||
@@ -1005,6 +1011,41 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
|
||||
}
|
||||
}
|
||||
|
||||
async function openStoryReminder(story: Story) {
|
||||
setSelectedSubjectID(story.subject_id);
|
||||
setStoryReviewRequestID(story.id);
|
||||
selectSection("stories");
|
||||
await openStory(story.id);
|
||||
}
|
||||
|
||||
async function openEventNotice(notice: WebEventNotice) {
|
||||
if (notice.subjectID) setSelectedSubjectID(notice.subjectID);
|
||||
switch (notice.resourceType) {
|
||||
case "story":
|
||||
if (notice.resourceID) setStoryReviewRequestID(notice.resourceID);
|
||||
selectSection("stories");
|
||||
if (notice.resourceID) await openStory(notice.resourceID);
|
||||
return;
|
||||
case "conversation":
|
||||
selectSection("conversations");
|
||||
if (notice.resourceID) await openConversation(notice.resourceID);
|
||||
return;
|
||||
case "upload_batch":
|
||||
selectSection("conversations");
|
||||
return;
|
||||
case "privacy_request":
|
||||
case "export_task":
|
||||
selectSection("privacy");
|
||||
return;
|
||||
case "device":
|
||||
case "device_command":
|
||||
selectSection("devices");
|
||||
return;
|
||||
default:
|
||||
selectSection("home");
|
||||
}
|
||||
}
|
||||
|
||||
async function reviseStory(story: Story, content: string, reason: string, errorType?: ContentErrorType) {
|
||||
await runAction("故事已保存为新的人工修订版本。", async () => {
|
||||
const response = await platformApi.reviseStory(story, content, reason, accessToken, errorType);
|
||||
@@ -1470,7 +1511,7 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
|
||||
) : null}
|
||||
{tab === "models" ? <ContentModelPanel models={contentModels} loading={modelLoading} error={modelError} canManage={canManageModels} actionBusy={actionBusy} onCreate={createContentModel} onTest={testContentModel} onActivate={activateContentModel} onRevoke={revokeContentModel} /> : null}
|
||||
{tab === "stories" ? <StoryPanel selectedID={selectedSubjectID}
|
||||
stories={stories} memories={memories} selected={selectedStory} loading={subjectLoading} actionBusy={actionBusy} canReview={canReviewStory}
|
||||
stories={stories} memories={memories} selected={selectedStory} openRequestID={storyReviewRequestID} loading={subjectLoading} actionBusy={actionBusy} canReview={canReviewStory}
|
||||
onOpen={openStory} onOpenConversation={openStoryConversation} onRevise={reviseStory} onConfirm={confirmStory} onReject={rejectStory} onUpdateFact={updateFactCandidate} onUpdateMemory={updateMemory} onDeleteMemory={deleteMemory} /> : null}
|
||||
{tab === "sharing" && canManageSharing ? <SharingPanel subjects={subjects} selectedID={selectedSubjectID} onSelect={setSelectedSubjectID}
|
||||
members={members} stories={stories} conversations={conversations} media={conversationMedia} grants={sharingGrants}
|
||||
@@ -1482,7 +1523,7 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
|
||||
{tab === "privacy" && !canReadPrivacy ? <StatePanel title="当前角色不能查看隐私请求" detail="家庭查看者没有隐私请求读取权限,请联系家庭所有者或管理员。" /> : null}
|
||||
{tab === "sharing" && !canManageSharing ? <StatePanel title="当前角色不能管理资源分享" detail="仅家庭所有者或管理员可以创建、撤销故事、文字和录音分享。" /> : null}
|
||||
{tab === "billing" ? <FamilyUsageOverview accessToken={accessToken} household={selectedHousehold} devices={devices.filter((item) => item.household_id === selectedHousehold.id)} /> : null}
|
||||
{tab === "messages" ? <CustomerMessagePanel notices={eventNotices} devices={devices.filter((item) => item.household_id === selectedHousehold.id)} presence={presence} stories={stories} privacyRequests={privacyRequests} eventState={eventStreamState} /> : null}
|
||||
{tab === "messages" ? <CustomerMessagePanel notices={eventNotices.filter((item) => item.householdID === selectedHousehold.id)} devices={devices.filter((item) => item.household_id === selectedHousehold.id)} presence={presence} stories={stories} privacyRequests={privacyRequests} eventState={eventStreamState} onNavigate={selectSection} onReviewStory={openStoryReminder} onOpenNotice={openEventNotice} /> : null}
|
||||
</>
|
||||
) : null}
|
||||
</section>
|
||||
@@ -1561,36 +1602,41 @@ function CustomerHomePanel({ subject, subjects, devices, presence, conversations
|
||||
</div>;
|
||||
}
|
||||
|
||||
function CustomerMessagePanel({ notices, devices, presence, stories, privacyRequests, eventState }: {
|
||||
notices: Array<{ id: string; type: string; resourceType: string; status: string; occurredAt: string }>;
|
||||
function CustomerMessagePanel({ notices, devices, presence, stories, privacyRequests, eventState, onNavigate, onReviewStory, onOpenNotice }: {
|
||||
notices: WebEventNotice[];
|
||||
devices: ManagedDevice[];
|
||||
presence: Record<string, DevicePresence>;
|
||||
stories: Story[];
|
||||
privacyRequests: PrivacyRequest[];
|
||||
eventState: EventStreamState;
|
||||
onNavigate: (section: CustomerSection) => void;
|
||||
onReviewStory: (story: Story) => Promise<void>;
|
||||
onOpenNotice: (notice: WebEventNotice) => Promise<void>;
|
||||
}) {
|
||||
const offline = devices.filter((item) => presence[item.id] && !presence[item.id].online);
|
||||
const pendingStories = stories.filter((item) => item.status === "CANDIDATE" || item.status === "IN_REVIEW");
|
||||
const activePrivacy = privacyRequests.filter((item) => !["COMPLETED", "REJECTED", "FAILED"].includes(item.status));
|
||||
const derived = [
|
||||
...pendingStories.map((item) => ({ id: `story-${item.id}`, title: "故事等待家庭确认", detail: item.title, tone: "info", action: "去审核" })),
|
||||
...offline.map((item) => ({ id: `offline-${item.id}`, title: "设备当前离线", detail: `${item.subject_name} · ${item.model}`, tone: "warning", action: "查看设备" })),
|
||||
...activePrivacy.map((item) => ({ id: `privacy-${item.id}`, title: "数据请求处理中", detail: `${privacyRequestTypeLabel(item.request_type)} · ${privacyStatusLabel(item.status)}`, tone: "info", action: "查看进度" }))
|
||||
...pendingStories.map((item) => ({ id: `story-${item.id}`, title: "故事等待家庭确认", detail: item.title, tone: "info", action: "去审核", run: () => onReviewStory(item) })),
|
||||
...offline.map((item) => ({ id: `offline-${item.id}`, title: "设备当前离线", detail: `${item.subject_name} · ${item.model}`, tone: "warning", action: "查看设备", run: () => onNavigate("devices") })),
|
||||
...activePrivacy.map((item) => ({ id: `privacy-${item.id}`, title: "数据请求处理中", detail: `${privacyRequestTypeLabel(item.request_type)} · ${privacyStatusLabel(item.status)}`, tone: "info", action: "查看进度", run: () => onNavigate("privacy") }))
|
||||
];
|
||||
const today = new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", weekday: "short" }).format(new Date());
|
||||
const now = new Date();
|
||||
const today = new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", weekday: "short" }).format(now);
|
||||
const todayNotices = notices.filter((notice) => sameLocalDate(new Date(notice.occurredAt), now));
|
||||
return <section className="customer-message-panel reminder-center">
|
||||
<div className="reminder-center__grid">
|
||||
<section className="reminder-card reminder-card--today"><header><div><h3>今天的陪伴动态</h3><p>{today} · 聊天、故事和设备状态会自动汇总</p></div><span className={`event-stream-status event-stream-status--${eventState}`}>{eventState === "connected" ? "实时连接" : eventState === "reconnecting" ? "重新连接" : "正在连接"}</span></header>
|
||||
{notices.length ? notices.slice(0, 5).map((notice) => <article className="reminder-row" key={notice.id}><time>{formatDate(notice.occurredAt)}</time><span className="reminder-row__dot" aria-hidden="true" /><div><strong>{eventTypeLabel(notice.type)}</strong><p>{resourceTypeLabel(notice.resourceType)} · {notice.status}</p></div><small>已记录</small></article>) : <div className="reminder-empty"><ClockCircleOutlined /><strong>今天还没有新的实时动态</strong><span>新聊天、故事整理和设备状态变化会显示在这里。</span></div>}
|
||||
<section className="reminder-card reminder-card--today"><header><div><h3>今天的系统动态</h3><p>{today} · 仅显示服务端实际产生的状态事件</p></div><span className={`event-stream-status event-stream-status--${eventState}`}>{eventState === "connected" ? "实时连接" : eventState === "reconnecting" ? "重新连接" : "正在连接"}</span></header>
|
||||
{todayNotices.length ? todayNotices.slice(0, 8).map((notice) => <button type="button" className="reminder-row" key={notice.id} onClick={() => void onOpenNotice(notice)}><time>{formatDate(notice.occurredAt)}</time><span className="reminder-row__dot" aria-hidden="true" /><div><strong>{eventTypeLabel(notice.type)}</strong><p>{resourceTypeLabel(notice.resourceType)} · {eventStatusLabel(notice.status)}</p></div><small>查看详情 ›</small></button>) : <div className="reminder-empty"><ClockCircleOutlined /><strong>今天还没有新的系统动态</strong><span>完成通话、产生故事、设备变化或隐私请求更新后才会出现。</span></div>}
|
||||
</section>
|
||||
<aside className="reminder-card"><header><div><h3>需要家人处理</h3><p>不展示转写、录音正文或隐私原因</p></div><span className="reminder-count">{derived.length}</span></header>{derived.length ? <div className="reminder-todos">{derived.map((item) => <article key={item.id} className={`reminder-todo reminder-todo--${item.tone}`}><span aria-hidden="true" /><div><strong>{item.title}</strong><p>{item.detail}</p></div><small>{item.action}</small></article>)}</div> : <div className="reminder-empty"><CheckCircleOutlined /><strong>当前没有待处理提醒</strong><span>所有家庭事项都已处理。</span></div>}</aside>
|
||||
<aside className="reminder-card"><header><div><h3>需要家人处理</h3><p>全部来自当前故事、设备和隐私请求状态</p></div><span className="reminder-count">{derived.length}</span></header>{derived.length ? <div className="reminder-todos">{derived.map((item) => <button type="button" key={item.id} className={`reminder-todo reminder-todo--${item.tone}`} onClick={() => void item.run()}><span aria-hidden="true" /><div><strong>{item.title}</strong><p>{item.detail}</p></div><small>{item.action} ›</small></button>)}</div> : <div className="reminder-empty"><CheckCircleOutlined /><strong>当前没有待处理提醒</strong><span>故事、设备和隐私接口均未返回待处理事项。</span></div>}</aside>
|
||||
</div>
|
||||
<section className="reminder-card reminder-history"><header><div><h3>近期记录</h3><p>按发生时间集中查看家庭陪伴状态</p></div></header><div className="reminder-history__summary"><div><strong>{pendingStories.length}</strong><span>待确认故事</span></div><div><strong>{offline.length}</strong><span>离线设备</span></div><div><strong>{activePrivacy.length}</strong><span>处理中数据请求</span></div><div><strong>{notices.length}</strong><span>本次实时事件</span></div></div></section>
|
||||
<section className="reminder-card reminder-history"><header><div><h3>当前业务状态</h3><p>数量直接来自业务接口,不使用演示日程或前端占位数据</p></div></header><div className="reminder-history__summary"><button type="button" onClick={() => onNavigate("stories")}><strong>{pendingStories.length}</strong><span>待确认故事</span></button><button type="button" onClick={() => onNavigate("devices")}><strong>{offline.length}</strong><span>离线设备</span></button><button type="button" onClick={() => onNavigate("privacy")}><strong>{activePrivacy.length}</strong><span>处理中数据请求</span></button><div><strong>{todayNotices.length}</strong><span>今天实时事件</span></div></div></section>
|
||||
</section>;
|
||||
}
|
||||
|
||||
function customerSectionTitle(section: Tab) {
|
||||
return ({ home: "陪伴周记", members: "家庭管理", subjects: "长者资料", devices: "陪伴设备", firmware: "固件发布", conversations: "回忆时间轴", stories: "故事与记忆", sharing: "内容分享", privacy: "数据与授权", billing: "服务用量", messages: "日程与提醒", models: "大模型管理" } as Record<Tab, string>)[section];
|
||||
return ({ home: "陪伴周记", members: "家庭管理", subjects: "长者资料", devices: "陪伴设备", firmware: "固件发布", conversations: "回忆时间轴", stories: "故事与记忆", sharing: "内容分享", privacy: "数据与授权", billing: "服务用量", messages: "提醒与待办", models: "大模型管理" } as Record<Tab, string>)[section];
|
||||
}
|
||||
|
||||
function customerSectionDescription(section: Tab) {
|
||||
@@ -1605,7 +1651,7 @@ function customerSectionDescription(section: Tab) {
|
||||
sharing: "按资源和权限范围向指定家庭成员分享内容。",
|
||||
privacy: "提交访问、导出或删除申请并跟踪处理进度。",
|
||||
billing: "汇总本家庭及设备的通话时长,并在核价可用时展示费用。",
|
||||
messages: "集中查看陪伴动态、设备、故事和隐私请求的状态提醒。",
|
||||
messages: "集中查看服务端记录的陪伴动态,并处理设备、故事和隐私请求待办。",
|
||||
models: "配置、测试并激活用于聊天摘要、关键词和故事候选生成的大模型。"
|
||||
} as Record<Tab, string>)[section];
|
||||
}
|
||||
@@ -1618,6 +1664,18 @@ function resourceTypeLabel(type: string) {
|
||||
return ({ device: "设备", conversation: "通话", story: "故事", upload_batch: "媒体", export_task: "导出任务", privacy_request: "隐私请求", device_command: "设备指令" } as Record<string, string>)[type] ?? "业务资源";
|
||||
}
|
||||
|
||||
function eventStatusLabel(status: string) {
|
||||
return ({
|
||||
ACTIVE: "进行中", COMPLETED: "已完成", FAILED: "失败", CANDIDATE: "待审核", IN_REVIEW: "审核中", CONFIRMED: "已确认", REJECTED: "已拒绝",
|
||||
ONLINE: "在线", OFFLINE: "离线", READY: "已就绪", PENDING: "待处理", PROCESSING: "处理中", SUCCEEDED: "已成功", CANCELLED: "已取消",
|
||||
CREATED: "已创建", UPDATED: "已更新", UPLOADED: "已上传"
|
||||
} as Record<string, string>)[status.toUpperCase()] ?? status;
|
||||
}
|
||||
|
||||
function sameLocalDate(left: Date, right: Date) {
|
||||
return !Number.isNaN(left.getTime()) && left.getFullYear() === right.getFullYear() && left.getMonth() === right.getMonth() && left.getDate() === right.getDate();
|
||||
}
|
||||
|
||||
function MemberPanel({ household, members, invitations, canManage, canTransfer, actionBusy, inviteeHint, onInviteeHint, inviteRole, onInviteRole, issuedToken, onDismissToken, onCreateInvitation, onUpdateRole, onRemove, onRevoke, onTransfer, onAddHousehold, onAcceptInvitation, onRenameHousehold }: {
|
||||
household: Household; households: Household[]; members: HouseholdMember[]; invitations: MemberInvitation[]; canManage: boolean; canTransfer: boolean; actionBusy: boolean;
|
||||
inviteeHint: string; onInviteeHint: (value: string) => void; inviteRole: "admin" | "editor" | "viewer"; onInviteRole: (value: "admin" | "editor" | "viewer") => void;
|
||||
@@ -2228,9 +2286,9 @@ function RecordingMediaBlock({ retentionMode, media, state, error, grants, membe
|
||||
</section>;
|
||||
}
|
||||
|
||||
function StoryPanel({ selectedID, stories, memories, selected, loading, actionBusy, canReview, onOpen, onOpenConversation, onRevise, onConfirm, onReject, onUpdateFact, onUpdateMemory, onDeleteMemory }: {
|
||||
function StoryPanel({ selectedID, stories, memories, selected, openRequestID, loading, actionBusy, canReview, onOpen, onOpenConversation, onRevise, onConfirm, onReject, onUpdateFact, onUpdateMemory, onDeleteMemory }: {
|
||||
selectedID?: string; stories: Story[]; memories: CuratedMemory[];
|
||||
selected?: Story; loading: boolean; actionBusy: boolean; canReview: boolean; onOpen: (id: string) => Promise<void>;
|
||||
selected?: Story; openRequestID?: string; loading: boolean; actionBusy: boolean; canReview: boolean; onOpen: (id: string) => Promise<void>;
|
||||
onOpenConversation: (story: Story) => Promise<void>;
|
||||
onRevise: (story: Story, content: string, reason: string, errorType?: ContentErrorType) => Promise<void>;
|
||||
onConfirm: (story: Story, reason: string) => Promise<void>; onReject: (story: Story, reason: string, errorType?: ContentErrorType) => Promise<void>;
|
||||
@@ -2245,6 +2303,12 @@ function StoryPanel({ selectedID, stories, memories, selected, loading, actionBu
|
||||
const [selectedMemoryID, setSelectedMemoryID] = useState<string>();
|
||||
const [detailOpen, setDetailOpen] = useState(false);
|
||||
useEffect(() => { setContent(selected?.content ?? ""); setReason(""); setErrorType(""); }, [selected]);
|
||||
useEffect(() => {
|
||||
if (openRequestID && selected?.id === openRequestID) {
|
||||
setSelectedMemoryID(undefined);
|
||||
setDetailOpen(true);
|
||||
}
|
||||
}, [openRequestID, selected?.id]);
|
||||
const reviewable = canReview && (selected?.status === "CANDIDATE" || selected?.status === "IN_REVIEW");
|
||||
const visibleMemories = memories.filter((memory) => memory.content.toLocaleLowerCase().includes(memoryQuery.trim().toLocaleLowerCase()));
|
||||
const selectedMemory = memories.find((memory) => memory.id === selectedMemoryID);
|
||||
|
||||
Reference in New Issue
Block a user