156 lines
5.8 KiB
TypeScript
156 lines
5.8 KiB
TypeScript
import type { CallableQAItem } from "./types";
|
|
|
|
export type ChatRole = "assistant" | "user";
|
|
|
|
export type ChatMessage = {
|
|
id: string;
|
|
role: ChatRole;
|
|
content: string;
|
|
source?: string;
|
|
boundary?: string;
|
|
feedback?: "liked" | "disliked";
|
|
createdAt: string;
|
|
};
|
|
|
|
export type ChatSession = {
|
|
id: string;
|
|
title: string;
|
|
preview: string;
|
|
messages: ChatMessage[];
|
|
updatedAt: string;
|
|
questionCount: number;
|
|
};
|
|
|
|
export type SuggestedQuestion = {
|
|
id: string;
|
|
text: string;
|
|
tags: string[];
|
|
};
|
|
|
|
export const suggestedQuestions: SuggestedQuestion[] = [
|
|
{
|
|
id: "parent-child-emotion",
|
|
text: "孩子一写作业就发脾气,我怎么接住他的情绪?",
|
|
tags: ["亲子关系", "情绪"]
|
|
},
|
|
{
|
|
id: "self-growth",
|
|
text: "我知道要行动,但总是拖延,应该先从哪里开始?",
|
|
tags: ["个人成长", "行动力"]
|
|
},
|
|
{
|
|
id: "marriage-talk",
|
|
text: "伴侣不愿意沟通,我怎么表达才不容易吵起来?",
|
|
tags: ["婚姻关系", "沟通"]
|
|
}
|
|
];
|
|
|
|
export const initialMessages: ChatMessage[] = [
|
|
{
|
|
id: "welcome",
|
|
role: "assistant",
|
|
content:
|
|
"你好,我是千问千答助手。你可以把最近遇到的亲子、关系、情绪或行动问题发给我,我会优先基于已审核的答疑资产给你一个可落地的回应。",
|
|
boundary: "当前为 H5 体验版,登录与个性化记录暂用 mock 数据。",
|
|
createdAt: "09:30"
|
|
}
|
|
];
|
|
|
|
export function createEmptySession(): ChatSession {
|
|
return {
|
|
id: `session-${Date.now()}`,
|
|
title: "新的问答",
|
|
preview: "还没有开始提问",
|
|
messages: initialMessages,
|
|
updatedAt: new Date().toISOString(),
|
|
questionCount: 0
|
|
};
|
|
}
|
|
|
|
export function summarizeSession(sessionId: string, messages: ChatMessage[]): ChatSession {
|
|
const userMessages = messages.filter((message) => message.role === "user");
|
|
const lastMessage = [...messages].reverse().find((message) => message.role === "assistant" || message.role === "user");
|
|
const firstQuestion = userMessages[0]?.content;
|
|
|
|
return {
|
|
id: sessionId,
|
|
title: firstQuestion ? firstQuestion.slice(0, 24) : "新的问答",
|
|
preview: lastMessage?.content.slice(0, 42) ?? "还没有开始提问",
|
|
messages,
|
|
updatedAt: new Date().toISOString(),
|
|
questionCount: userMessages.length
|
|
};
|
|
}
|
|
|
|
export const mockCallableQa: CallableQAItem[] = [
|
|
{
|
|
id: 9001,
|
|
standard_code: "MOCK-001",
|
|
standard_question: "孩子一写作业就发脾气,家长应该怎么办?",
|
|
standard_answer:
|
|
"先不要急着纠正行为,建议先区分两件事:孩子是在逃避任务,还是被情绪淹没了。可以先用一句很短的话接住情绪,比如“我看到你现在很烦,先停两分钟”。等情绪下降后,再把任务拆小,只约定下一步,而不是一次性要求全部完成。",
|
|
similar_questions: ["孩子写作业崩溃怎么办", "孩子学习时情绪大怎么办"],
|
|
primary_topic: "亲子关系",
|
|
problem_tags: ["情绪接纳", "作业冲突", "边界"],
|
|
course_stage: "大本营综合",
|
|
audience_tags: ["家长"],
|
|
answer_boundary: "不替代心理诊断;如果孩子长期极端失控,建议寻求专业支持。",
|
|
forbidden_expressions: ["保证有效", "一定改变"],
|
|
source_raw_qa_id: 9001,
|
|
session_id: 9001
|
|
},
|
|
{
|
|
id: 9002,
|
|
standard_code: "MOCK-002",
|
|
standard_question: "知道要行动但总是拖延,应该怎么开始?",
|
|
standard_answer:
|
|
"拖延时不要先给自己贴标签,可以先把目标缩小到“今天能完成的最小动作”。比如不是要求自己完整学习一小时,而是先打开资料、看 5 分钟、记一句话。行动力常常不是靠情绪准备好了才开始,而是靠一个足够小的开始把状态带起来。",
|
|
similar_questions: ["总是拖延怎么办", "行动力差怎么调整"],
|
|
primary_topic: "个人成长",
|
|
problem_tags: ["行动力", "拖延", "自我要求"],
|
|
course_stage: "觉知",
|
|
audience_tags: ["学员"],
|
|
answer_boundary: "只提供学习和自我观察建议,不承诺课程效果。",
|
|
forbidden_expressions: ["一定改变", "保证有效"],
|
|
source_raw_qa_id: 9002,
|
|
session_id: 9002
|
|
},
|
|
{
|
|
id: 9003,
|
|
standard_code: "MOCK-003",
|
|
standard_question: "伴侣不愿意沟通时,如何降低冲突?",
|
|
standard_answer:
|
|
"可以先把“让对方马上理解我”调整成“先让对话能继续”。表达时尽量少用评判句,多用事实加感受,比如“刚才你直接走开,我有点失落,也有点不知道怎么继续”。如果对方暂时不想谈,可以约一个明确时间,而不是追着当场解决。",
|
|
similar_questions: ["伴侣拒绝沟通怎么办", "夫妻沟通容易吵架怎么办"],
|
|
primary_topic: "婚姻关系",
|
|
problem_tags: ["沟通", "冲突降温", "关系边界"],
|
|
course_stage: "原生",
|
|
audience_tags: ["伴侣关系"],
|
|
answer_boundary: "不做法律判断;涉及暴力或安全风险时应优先保护自身安全。",
|
|
forbidden_expressions: ["法律上必然", "一定改变"],
|
|
source_raw_qa_id: 9003,
|
|
session_id: 9003
|
|
}
|
|
];
|
|
|
|
function scoreQuestion(question: string, qa: CallableQAItem): number {
|
|
const haystack = [
|
|
qa.standard_question,
|
|
qa.standard_answer,
|
|
qa.primary_topic,
|
|
qa.course_stage,
|
|
...qa.problem_tags,
|
|
...qa.similar_questions
|
|
].join(" ");
|
|
return Array.from(new Set(question.trim().split(""))).reduce((score, char) => {
|
|
if (!char.trim()) return score;
|
|
return haystack.includes(char) ? score + 1 : score;
|
|
}, 0);
|
|
}
|
|
|
|
export function findQaAnswer(question: string, qaItems: CallableQAItem[]) {
|
|
const candidates = qaItems.length > 0 ? qaItems : mockCallableQa;
|
|
const [best] = [...candidates].sort((left, right) => scoreQuestion(question, right) - scoreQuestion(question, left));
|
|
return best ?? mockCallableQa[0];
|
|
}
|