"use client"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; import { BookOpenCheck, Clock3, Home, MessageCircle, Plus, RotateCcw, SendHorizonal, Settings, ShieldCheck, Trash2, UserRound } from "lucide-react"; import { api } from "@/lib/api"; import type { CallableQAItem } from "@/lib/types"; import { createEmptySession, findQaAnswer, initialMessages, suggestedQuestions, summarizeSession, type ChatSession, type ChatMessage } from "@/lib/h5MockQa"; import ChatBubble from "./ChatBubble"; import MockAuthPanel, { type MockUser } from "./MockAuthPanel"; type MobileTab = "qa" | "history" | "me"; const STORAGE_KEY = "hy-qa-h5-sessions"; function nowText() { return new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date()); } function buildAnswerMessage(question: string, qa: CallableQAItem): ChatMessage { return { id: `assistant-${Date.now()}`, role: "assistant", content: qa.standard_answer, source: `${qa.standard_code} / ${qa.primary_topic} / ${qa.course_stage}`, boundary: qa.answer_boundary ?? "答复仅基于已审核答疑资产生成,不能替代专业诊断、法律判断或医疗建议。", createdAt: nowText() }; } 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", day: "2-digit", hour: "2-digit", minute: "2-digit", hour12: false }).format(new Date(value)); } export default function H5QAExperience() { const [activeTab, setActiveTab] = useState("qa"); const [authOpen, setAuthOpen] = useState(false); const [input, setInput] = useState(""); const [messages, setMessages] = useState(initialMessages); const [sessions, setSessions] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(""); const [callableQa, setCallableQa] = useState([]); const [loadingQa, setLoadingQa] = useState(true); const [qaLoadError, setQaLoadError] = useState(null); const [user, setUser] = useState(null); const [copiedMessageId, setCopiedMessageId] = useState(null); const bottomRef = useRef(null); const qaStatusText = loadingQa ? "正在连接可调用库" : qaLoadError ? "后端连接异常" : callableQa.length > 0 ? "已连接可调用库" : "可调用库为空"; useEffect(() => { const fallback = createEmptySession(); try { const saved = window.localStorage.getItem(STORAGE_KEY); const parsed = saved ? (JSON.parse(saved) as ChatSession[]) : []; const validSessions = parsed.length > 0 ? parsed : [fallback]; setSessions(validSessions); setCurrentSessionId(validSessions[0].id); setMessages(validSessions[0].messages); } catch { setSessions([fallback]); setCurrentSessionId(fallback.id); setMessages(fallback.messages); } }, []); useEffect(() => { if (sessions.length > 0) { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions)); } }, [sessions]); useEffect(() => { api .get("/standard-qa/callable") .then((items) => { setCallableQa(items); setQaLoadError(null); }) .catch((error) => { setCallableQa([]); setQaLoadError(error instanceof Error ? error.message : "可调用库接口请求失败"); }) .finally(() => setLoadingQa(false)); }, []); useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" }); }, [messages, activeTab]); function mockLogin(mode: MockUser["mode"]) { setUser({ mode, name: mode === "login" ? "大本营学员" : "新学员", phone: "138****2026" }); setAuthOpen(false); } function persistMessages(nextMessages: ChatMessage[]) { setMessages(nextMessages); setSessions((current) => { const sessionId = currentSessionId || createEmptySession().id; const nextSession = summarizeSession(sessionId, nextMessages); const withoutCurrent = current.filter((session) => session.id !== sessionId); return [nextSession, ...withoutCurrent].slice(0, 20); }); } function startNewSession() { const nextSession = createEmptySession(); setCurrentSessionId(nextSession.id); setMessages(nextSession.messages); setSessions((current) => [nextSession, ...current].slice(0, 20)); setActiveTab("qa"); } function openSession(session: ChatSession) { setCurrentSessionId(session.id); setMessages(session.messages); setActiveTab("qa"); } function clearHistory() { const nextSession = createEmptySession(); setCurrentSessionId(nextSession.id); setMessages(nextSession.messages); setSessions([nextSession]); setActiveTab("qa"); } async function copyMessage(message: ChatMessage) { try { await navigator.clipboard.writeText(message.content); } catch { // Some embedded webviews/headless browsers block clipboard writes. // Keep the UI feedback so the user knows the copy action was triggered. } setCopiedMessageId(message.id); window.setTimeout(() => setCopiedMessageId(null), 1400); } function setFeedback(messageId: string, feedback: "liked" | "disliked") { const nextMessages = messages.map((message) => message.id === messageId ? { ...message, feedback: message.feedback === feedback ? undefined : feedback } : message ); persistMessages(nextMessages); } function ask(question: string) { const trimmed = question.trim(); if (!trimmed) return; 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, { id: `user-${timestamp}`, role: "user", content: trimmed, createdAt: nowText() }, assistantMessage ]; persistMessages(nextMessages); setInput(""); setActiveTab("qa"); } function submit(event: FormEvent) { event.preventDefault(); ask(input); } return (

千问千答助手

{qaStatusText}
{copiedMessageId ? 已复制回答 : null}
{activeTab === "qa" ? ( <>
回答会优先遵守审核边界;涉及医疗、法律、极端风险或个人隐私时,请以人工复核为准。
{!loadingQa && (qaLoadError || callableQa.length === 0) ? (
{qaLoadError ? `可调用库接口异常:${qaLoadError}` : "当前后端可调用库为空,H5 不会用本地 mock 内容代替真实答案。"}
) : null}
{messages.map((message) => ( ))}
{suggestedQuestions.map((item) => ( ))}