Use backend callable QA for H5 answers
This commit is contained in:
@@ -20,7 +20,6 @@ import {
|
||||
createEmptySession,
|
||||
findQaAnswer,
|
||||
initialMessages,
|
||||
mockCallableQa,
|
||||
suggestedQuestions,
|
||||
summarizeSession,
|
||||
type ChatSession,
|
||||
@@ -51,6 +50,22 @@ function buildAnswerMessage(question: string, qa: CallableQAItem): ChatMessage {
|
||||
};
|
||||
}
|
||||
|
||||
function buildUnavailableMessage(reason: "loading" | "empty" | "error"): ChatMessage {
|
||||
const content = {
|
||||
loading: "我还在连接可调用问答库,请稍等几秒后再试。",
|
||||
empty: "当前可调用问答库还没有可用内容。请先到后台标准问答库中,把已审核、低/中风险、已脱敏或无需脱敏的问答标记为“可调用”。",
|
||||
error: "暂时连接不到后端可调用库,请检查后端服务或网络连接后再试。"
|
||||
}[reason];
|
||||
|
||||
return {
|
||||
id: `assistant-unavailable-${Date.now()}`,
|
||||
role: "assistant",
|
||||
content,
|
||||
boundary: "H5 端不会使用未审核或不可调用的内容生成回答。",
|
||||
createdAt: nowText()
|
||||
};
|
||||
}
|
||||
|
||||
function formatSessionTime(value: string) {
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
month: "2-digit",
|
||||
@@ -70,11 +85,12 @@ export default function H5QAExperience() {
|
||||
const [currentSessionId, setCurrentSessionId] = useState("");
|
||||
const [callableQa, setCallableQa] = useState<CallableQAItem[]>([]);
|
||||
const [loadingQa, setLoadingQa] = useState(true);
|
||||
const [qaLoadError, setQaLoadError] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<MockUser | null>(null);
|
||||
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
|
||||
const bottomRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const qaSource = useMemo(() => (callableQa.length > 0 ? callableQa : mockCallableQa), [callableQa]);
|
||||
const qaStatusText = loadingQa ? "正在连接可调用库" : qaLoadError ? "后端连接异常" : callableQa.length > 0 ? "已连接可调用库" : "可调用库为空";
|
||||
|
||||
useEffect(() => {
|
||||
const fallback = createEmptySession();
|
||||
@@ -101,8 +117,14 @@ export default function H5QAExperience() {
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<CallableQAItem[]>("/standard-qa/callable")
|
||||
.then(setCallableQa)
|
||||
.catch(() => setCallableQa([]))
|
||||
.then((items) => {
|
||||
setCallableQa(items);
|
||||
setQaLoadError(null);
|
||||
})
|
||||
.catch((error) => {
|
||||
setCallableQa([]);
|
||||
setQaLoadError(error instanceof Error ? error.message : "可调用库接口请求失败");
|
||||
})
|
||||
.finally(() => setLoadingQa(false));
|
||||
}, []);
|
||||
|
||||
@@ -173,8 +195,15 @@ export default function H5QAExperience() {
|
||||
const trimmed = question.trim();
|
||||
if (!trimmed) return;
|
||||
|
||||
const matchedQa = findQaAnswer(trimmed, qaSource);
|
||||
const matchedQa = findQaAnswer(trimmed, callableQa);
|
||||
const timestamp = Date.now();
|
||||
const assistantMessage = loadingQa
|
||||
? buildUnavailableMessage("loading")
|
||||
: qaLoadError
|
||||
? buildUnavailableMessage("error")
|
||||
: matchedQa
|
||||
? { ...buildAnswerMessage(trimmed, matchedQa), id: `assistant-${timestamp}` }
|
||||
: buildUnavailableMessage("empty");
|
||||
const nextMessages: ChatMessage[] = [
|
||||
...messages,
|
||||
{
|
||||
@@ -183,7 +212,7 @@ export default function H5QAExperience() {
|
||||
content: trimmed,
|
||||
createdAt: nowText()
|
||||
},
|
||||
{ ...buildAnswerMessage(trimmed, matchedQa), id: `assistant-${timestamp}` }
|
||||
assistantMessage
|
||||
];
|
||||
persistMessages(nextMessages);
|
||||
setInput("");
|
||||
@@ -207,7 +236,7 @@ export default function H5QAExperience() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm leading-6 text-slate-600">
|
||||
<p>当前页面只做前端加法:登录注册为 mock 状态,问答优先读取已审核可调用库,没有数据时使用本地演示问答。</p>
|
||||
<p>当前页面只做前端加法:登录注册为 mock 状态,问答只读取后端已审核可调用库。</p>
|
||||
<p>后续封装微信小程序时,主交互可以保留:顶部身份区、聊天流、快捷问题、底部导航和账号面板。</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -224,7 +253,7 @@ export default function H5QAExperience() {
|
||||
<h1 className="text-lg font-semibold leading-6">千问千答助手</h1>
|
||||
<div className="mt-0.5 flex items-center gap-1 text-xs text-slate-500">
|
||||
<ShieldCheck className="h-3.5 w-3.5 text-jade" aria-hidden />
|
||||
{loadingQa ? "正在检查可调用库" : callableQa.length > 0 ? "已连接可调用库" : "演示模式"}
|
||||
{qaStatusText}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -254,6 +283,11 @@ export default function H5QAExperience() {
|
||||
<div className="mb-4 rounded-md border border-teal-100 bg-teal-50 px-3 py-2 text-xs leading-5 text-teal-800">
|
||||
回答会优先遵守审核边界;涉及医疗、法律、极端风险或个人隐私时,请以人工复核为准。
|
||||
</div>
|
||||
{!loadingQa && (qaLoadError || callableQa.length === 0) ? (
|
||||
<div className="mb-4 rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-800">
|
||||
{qaLoadError ? `可调用库接口异常:${qaLoadError}` : "当前后端可调用库为空,H5 不会用本地 mock 内容代替真实答案。"}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-4">
|
||||
{messages.map((message) => (
|
||||
<ChatBubble key={message.id} message={message} onCopy={copyMessage} onFeedback={setFeedback} />
|
||||
@@ -359,7 +393,7 @@ export default function H5QAExperience() {
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>数据模式</span>
|
||||
<span className="font-medium text-ink">{callableQa.length > 0 ? "接口数据" : "mock 演示"}</span>
|
||||
<span className="font-medium text-ink">{qaLoadError ? "接口异常" : callableQa.length > 0 ? "接口数据" : "等待入库"}</span>
|
||||
</div>
|
||||
</section>
|
||||
<button
|
||||
|
||||
@@ -51,7 +51,7 @@ export const initialMessages: ChatMessage[] = [
|
||||
role: "assistant",
|
||||
content:
|
||||
"你好,我是千问千答助手。你可以把最近遇到的亲子、关系、情绪或行动问题发给我,我会优先基于已审核的答疑资产给你一个可落地的回应。",
|
||||
boundary: "当前为 H5 体验版,登录与个性化记录暂用 mock 数据。",
|
||||
boundary: "当前为 H5 体验版,登录暂用 mock 占位;问答答案只读取后端可调用库。",
|
||||
createdAt: "09:30"
|
||||
}
|
||||
];
|
||||
@@ -82,57 +82,6 @@ export function summarizeSession(sessionId: string, messages: ChatMessage[]): Ch
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -149,7 +98,9 @@ function scoreQuestion(question: string, qa: CallableQAItem): number {
|
||||
}
|
||||
|
||||
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];
|
||||
if (qaItems.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const [best] = [...qaItems].sort((left, right) => scoreQuestion(question, right) - scoreQuestion(question, left));
|
||||
return best ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user