feat: improve elder companion conversations

This commit is contained in:
Codex Local
2026-08-29 22:06:30 +08:00
parent e652516147
commit f0849119f7
11 changed files with 176 additions and 22 deletions
+55 -5
View File
@@ -76,6 +76,15 @@ type LoadState = "idle" | "loading" | "ready" | "error";
type MediaLoadState = "idle" | "loading" | "ready" | "restricted" | "error";
type EventStreamState = "connecting" | "connected" | "reconnecting";
type ConversationPace = "slow" | "normal" | "lively";
type DialoguePreset = {
key: "daily" | "memory" | "interest";
label: string;
description: string;
goal: string;
pace: ConversationPace;
preferredTopics: string[];
avoidedTopics: string[];
};
type RecordingSegment = ConversationMedia["segments"][number];
type RecordingGrantInput = { grantee_user_id: string; permission: "PLAY" | "DOWNLOAD"; purpose: string; expires_at: string };
type SharingGrantInput = { resource_type: "STORY" | "TRANSCRIPT" | "RECORDING"; resource_id: string; grantee_user_id: string; permission: "VIEW_TEXT" | "LISTEN" | "DOWNLOAD" | "EDIT"; purpose: string; expires_at: string; login_required: boolean };
@@ -116,6 +125,24 @@ type FamilyControlDraft =
| { kind: "sharing_grant_create"; subjectID: string; subjectName: string; granteeName: string; resourceLabel: string; input: SharingGrantInput; acknowledged: boolean; confirmation: string }
| { kind: "sharing_revoke"; item: SharingGrant; acknowledged: boolean; confirmation: string };
const dialoguePresets: DialoguePreset[] = [
{
key: "daily", label: "日常陪伴", description: "从当天生活和轻松小事自然开场",
goal: "轻松聊聊当天的生活,让长者感到有人陪伴", pace: "slow",
preferredTopics: ["天气", "饮食", "睡眠", "花草", "电视节目"], avoidedTopics: ["疾病", "家庭矛盾", "收入"]
},
{
key: "memory", label: "回忆往事", description: "温和引导年轻时的经历和重要人物",
goal: "温和引导长者回忆年轻时的工作和生活经历", pace: "slow",
preferredTopics: ["家乡", "童年", "工作经历", "老朋友", "节日"], avoidedTopics: ["遗憾经历", "家庭矛盾", "经济压力"]
},
{
key: "interest", label: "兴趣聊天", description: "围绕熟悉爱好展开低压力交流",
goal: "围绕长者喜欢的兴趣进行轻松交流", pace: "normal",
preferredTopics: ["戏曲", "养花", "做饭", "散步", "孙辈"], avoidedTopics: ["争议新闻", "疾病", "家庭矛盾"]
}
];
export function SaaSWorkspace({ accessToken, accounts, section = "home", onSectionChange, showNavigation = true, enableFirmware = false }: {
accessToken: string;
accounts: CurrentUser["accounts"];
@@ -734,6 +761,13 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
});
}
function applyNewSubjectDialoguePreset(preset: DialoguePreset) {
setNewSubjectGoal(preset.goal);
setNewSubjectPace(preset.pace);
setNewSubjectTopics(preset.preferredTopics.join("、"));
setNewSubjectAvoidedTopics(preset.avoidedTopics.join("、"));
}
function updateSubjectProfile(subject: Subject, input: UpdateSubjectRequest) {
openFamilyControl({ kind: "subject_update", item: subject, input, acknowledged: false, confirmation: "" });
}
@@ -1261,12 +1295,16 @@ export function SaaSWorkspace({ accessToken, accounts, section = "home", onSecti
<div className="subject-create-modal__body">
<div className="subject-avatar-upload" aria-hidden="true"><UserAddOutlined /><span>长者头像</span></div>
<label className="subject-field subject-field--wide"><span>称呼</span><input value={newSubjectName} maxLength={120} onChange={(event) => setNewSubjectName(event.target.value)} placeholder="请输入长者的称呼,如:爷爷、张奶奶" required /></label>
<div className="dialogue-presets subject-field--wide"><div><strong>陪伴方式模板</strong><small>模板只填充建议值,保存前仍可逐项修改</small></div><div>{dialoguePresets.map((preset) => <button type="button" key={preset.key} onClick={() => applyNewSubjectDialoguePreset(preset)}><strong>{preset.label}</strong><span>{preset.description}</span></button>)}</div></div>
<div className="subject-create-modal__row">
<label className="subject-field"><span>语言</span><select value="zh-CN" disabled><option value="zh-CN">普通话</option></select></label>
<label className="subject-field"><span>交流节奏</span><select value={newSubjectPace} onChange={(event) => setNewSubjectPace(event.target.value as ConversationPace)}><option value="slow">较慢,需要更多耐心</option><option value="normal">自然日常节奏</option><option value="lively">轻快交流</option></select></label>
</div>
<label className="subject-field subject-field--wide"><span>背景资料</span><textarea value={newSubjectBackground} maxLength={500} onChange={(event) => setNewSubjectBackground(event.target.value)} placeholder="记录长者的兴趣爱好、过往经历或需要特别注意的事项" required /></label>
<label className="subject-field subject-field--wide"><span>本次交流目标</span><input value={newSubjectGoal} maxLength={200} onChange={(event) => setNewSubjectGoal(event.target.value)} placeholder="例如:聊聊第一次参加工作的经历" required /></label>
<label className="subject-field subject-field--wide"><span>背景资料</span><textarea value={newSubjectBackground} maxLength={500} onChange={(event) => setNewSubjectBackground(event.target.value)} placeholder="例如:退休前是教师,年轻时在天津生活过" required /><small>只填写已经核实、允许用于陪伴交流的事实,不要填写账号、证件或联系方式。</small></label>
<label className="subject-field subject-field--wide"><span>兴趣标签</span><input value={newSubjectInterests} maxLength={400} onChange={(event) => setNewSubjectInterests(event.target.value)} placeholder="例如:京剧、养花、做饭(用逗号分隔,最多8项)" /><small>用于寻找低压力的开场话题。</small></label>
<label className="subject-field subject-field--wide"><span>本次交流目标</span><input value={newSubjectGoal} maxLength={200} onChange={(event) => setNewSubjectGoal(event.target.value)} placeholder="例如:多聊年轻时的工作经历,让长者心情放松" required /><small>描述希望智能体怎样陪伴,而不是要求长者必须回答什么。</small></label>
<label className="subject-field subject-field--wide"><span>优先话题</span><input value={newSubjectTopics} maxLength={400} onChange={(event) => setNewSubjectTopics(event.target.value)} placeholder="例如:家乡、京剧、孙辈(用逗号分隔,最多8项)" /><small>智能体会优先从这些话题中选择自然的切入点。</small></label>
<label className="subject-field subject-field--wide"><span>避免主动提及</span><input value={newSubjectAvoidedTopics} maxLength={400} onChange={(event) => setNewSubjectAvoidedTopics(event.target.value)} placeholder="例如:收入、家庭矛盾(用逗号分隔,最多8项)" /><small>智能体不会主动提起;长者主动提及时会先共情,再谨慎回应。</small></label>
</div>
<footer><button type="button" className="subject-modal-cancel" onClick={() => setSubjectCreateOpen(false)}>取消</button><button type="submit" disabled={actionBusy || !newSubjectName.trim() || !newSubjectBackground.trim() || !newSubjectGoal.trim()}>立即创建</button></footer>
</form>
@@ -1594,7 +1632,18 @@ function SubjectProfileEditor({ subject, canWrite, actionBusy, onSave }: {
conversation_preferences: { ...subject.conversation_preferences, conversation_goal: goal.trim(), pace, preferred_topics: topics, avoided_topics: avoidedTopics }
});
}
function applyDialoguePreset(preset: DialoguePreset) {
if (!canWrite) return;
setGoal(preset.goal);
setPace(preset.pace);
setTopics([...preset.preferredTopics]);
setAvoidedTopics([...preset.avoidedTopics]);
}
return <form className="profile-editor subject-profile-editor" onSubmit={(event) => void submit(event)}>
<section className="subject-editor-card subject-editor-card--presets">
<div className="subject-section-heading"><span className="subject-section-icon subject-section-icon--topic"><MessageOutlined /></span><div><h4>陪伴方式模板</h4><p>快速设置交流目标、节奏和话题边界,应用后仍需核对并保存</p></div></div>
<div className="dialogue-presets dialogue-presets--editor">{dialoguePresets.map((preset) => <button type="button" key={preset.key} disabled={!canWrite} onClick={() => applyDialoguePreset(preset)}><strong>{preset.label}</strong><span>{preset.description}</span></button>)}</div>
</section>
<section className="subject-editor-card">
<div className="subject-section-heading"><span className="subject-section-icon subject-section-icon--interest"><HeartOutlined /></span><div><h4>兴趣标签</h4><p>记录长者日常喜欢的活动或事物</p></div></div>
<SubjectTagEditor items={interests} onChange={setInterests} disabled={!canWrite} placeholder="添加新标签(按回车确认)" emptyText="尚未添加兴趣标签" removeLabel="删除兴趣标签" />
@@ -1602,6 +1651,7 @@ function SubjectProfileEditor({ subject, canWrite, actionBusy, onSave }: {
<section className="subject-editor-card subject-editor-card--topics">
<div className="subject-section-heading"><span className="subject-section-icon subject-section-icon--topic"><MessageOutlined /></span><div><h4>推荐话题</h4><p>沟通时容易引起共鸣的积极话题</p></div></div>
<SubjectTagEditor items={topics} onChange={setTopics} disabled={!canWrite} placeholder="添加推荐话题(按回车确认)" emptyText="尚未添加推荐话题" removeLabel="删除推荐话题" tone="topic" />
<small className="subject-tag-editor__note">优先填写家乡、兴趣、工作经历等容易自然展开的话题。</small>
</section>
<section className="subject-editor-card subject-editor-card--avoid">
<div className="subject-section-heading"><span className="subject-section-icon subject-section-icon--avoid"><StopOutlined /></span><div><h4>避讳话题</h4><p>避免引起长者不悦或情绪波动,不影响紧急安全响应</p></div></div>
@@ -1613,8 +1663,8 @@ function SubjectProfileEditor({ subject, canWrite, actionBusy, onSave }: {
<div className="subject-basics-grid">
<label className="subject-field"><span>称呼</span><input disabled={!canWrite} value={name} maxLength={120} onChange={(event) => setName(event.target.value)} required /></label>
<label className="subject-field"><span>交流节奏</span><select disabled={!canWrite} value={pace} onChange={(event) => setPace(event.target.value as ConversationPace)}><option value="slow">放慢语速</option><option value="normal">自然语速</option><option value="lively">轻快交流</option></select></label>
<label className="subject-field subject-field--wide"><span>背景资料</span><textarea disabled={!canWrite} value={background} maxLength={500} onChange={(event) => setBackground(event.target.value)} required /></label>
<label className="subject-field subject-field--wide"><span>交流目标</span><input disabled={!canWrite} value={goal} maxLength={200} onChange={(event) => setGoal(event.target.value)} required /></label>
<label className="subject-field subject-field--wide"><span>背景资料</span><textarea disabled={!canWrite} value={background} maxLength={500} onChange={(event) => setBackground(event.target.value)} required /><small>只填写已经核实并允许用于陪伴交流的事实。</small></label>
<label className="subject-field subject-field--wide"><span>交流目标</span><input disabled={!canWrite} value={goal} maxLength={200} onChange={(event) => setGoal(event.target.value)} required /><small>例如:多聊年轻时的工作经历,让长者心情放松。</small></label>
</div>
</section>
{canWrite ? <footer className="subject-editor-actions"><span>{profileChanged ? "已有尚未保存的资料变更" : "资料已是最新版本"}</span><button type="submit" disabled={actionBusy || !profileChanged || !name.trim() || !background.trim() || !goal.trim()}>核对并保存新版本</button></footer> : null}
@@ -2316,7 +2366,7 @@ function exportTaskCategories(task: ExportTask) { const value = task.scope.categ
function profileString(value: Record<string, unknown>, key: string) { return typeof value[key] === "string" ? value[key] as string : ""; }
function profilePace(value: Record<string, unknown>): ConversationPace { const pace = profileString(value, "pace"); return pace === "normal" || pace === "lively" ? pace : "slow"; }
function profileList(value: Record<string, unknown>, key: string) { return Array.isArray(value[key]) ? (value[key] as unknown[]).filter((item): item is string => typeof item === "string") : []; }
function splitContextList(value: string) { return value.split(/[,,]/).map((item) => item.trim()).filter(Boolean).slice(0, 8).map((item) => item.slice(0, 50)); }
function splitContextList(value: string) { return value.split(/[、,,]/).map((item) => item.trim()).filter(Boolean).slice(0, 8).map((item) => item.slice(0, 50)); }
function contextItemsLabel(value: unknown) { return Array.isArray(value) && value.length ? value.filter((item): item is string => typeof item === "string").join("、") || "未填写" : "未填写"; }
function conversationPaceLabel(value: unknown) { return ({ slow: "放慢语速", normal: "自然语速", lively: "轻快交流" } as Record<string, string>)[typeof value === "string" ? value : ""] ?? "未填写"; }
function subjectProfileChangeSummary(subject: Subject, input: UpdateSubjectRequest) {