diff --git a/hy_qa_asset_backend/frontend/app/h5/page.tsx b/hy_qa_asset_backend/frontend/app/h5/page.tsx new file mode 100644 index 0000000..042ee2b --- /dev/null +++ b/hy_qa_asset_backend/frontend/app/h5/page.tsx @@ -0,0 +1,5 @@ +import H5QAExperience from "@/components/h5/H5QAExperience"; + +export default function H5Page() { + return ; +} diff --git a/hy_qa_asset_backend/frontend/components/Layout.tsx b/hy_qa_asset_backend/frontend/components/Layout.tsx index 53e9f4a..f81a063 100644 --- a/hy_qa_asset_backend/frontend/components/Layout.tsx +++ b/hy_qa_asset_backend/frontend/components/Layout.tsx @@ -1,9 +1,16 @@ "use client"; import { ReactNode } from "react"; +import { usePathname } from "next/navigation"; import Sidebar from "./Sidebar"; export default function Layout({ children }: { children: ReactNode }) { + const pathname = usePathname(); + + if (pathname.startsWith("/h5")) { + return
{children}
; + } + return (
diff --git a/hy_qa_asset_backend/frontend/components/Sidebar.tsx b/hy_qa_asset_backend/frontend/components/Sidebar.tsx index 6f231f6..5743240 100644 --- a/hy_qa_asset_backend/frontend/components/Sidebar.tsx +++ b/hy_qa_asset_backend/frontend/components/Sidebar.tsx @@ -8,6 +8,7 @@ import { ClipboardCheck, DatabaseZap, Gauge, + MessageCircle, Library, ListChecks, Settings, @@ -21,6 +22,7 @@ const navItems = [ { href: "/high-risk", label: "高风险", icon: ShieldAlert }, { href: "/standard-qa", label: "标准库", icon: Library }, { href: "/callable-qa", label: "可调用库", icon: DatabaseZap }, + { href: "/h5", label: "H5 问答", icon: MessageCircle }, { href: "/logs", label: "运行日志", icon: ListChecks }, { href: "/settings", label: "系统设置", icon: Settings } ]; diff --git a/hy_qa_asset_backend/frontend/components/h5/ChatBubble.tsx b/hy_qa_asset_backend/frontend/components/h5/ChatBubble.tsx new file mode 100644 index 0000000..f4cdcad --- /dev/null +++ b/hy_qa_asset_backend/frontend/components/h5/ChatBubble.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { Bot, UserRound } from "lucide-react"; +import type { ChatMessage } from "@/lib/h5MockQa"; + +export default function ChatBubble({ message }: { message: ChatMessage }) { + const isUser = message.role === "user"; + + return ( +
+ {!isUser ? ( +
+ +
+ ) : null} +
+
+
{message.content}
+ {message.source ? ( +
+ 来源:{message.source} +
+ ) : null} + {message.boundary ? ( +
{message.boundary}
+ ) : null} +
+
{message.createdAt}
+
+ {isUser ? ( +
+ +
+ ) : null} +
+ ); +} diff --git a/hy_qa_asset_backend/frontend/components/h5/H5QAExperience.tsx b/hy_qa_asset_backend/frontend/components/h5/H5QAExperience.tsx new file mode 100644 index 0000000..8e5a246 --- /dev/null +++ b/hy_qa_asset_backend/frontend/components/h5/H5QAExperience.tsx @@ -0,0 +1,255 @@ +"use client"; + +import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; +import { BookOpenCheck, Clock3, Home, MessageCircle, SendHorizonal, Settings, ShieldCheck, UserRound } from "lucide-react"; +import { api } from "@/lib/api"; +import type { CallableQAItem } from "@/lib/types"; +import { + findQaAnswer, + initialMessages, + mockCallableQa, + suggestedQuestions, + type ChatMessage +} from "@/lib/h5MockQa"; +import ChatBubble from "./ChatBubble"; +import MockAuthPanel, { type MockUser } from "./MockAuthPanel"; + +type MobileTab = "qa" | "history" | "me"; + +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 EmptyState({ tab }: { tab: MobileTab }) { + const copy = { + history: "登录后这里会展示你的历史提问记录。当前先用本机 mock 会话演示。", + me: "账号、学习阶段、隐私设置后续会接真实接口;现在先预留微信授权入口。", + qa: "" + }; + + if (tab === "qa") return null; + + return ( +
+
+
+ {tab === "history" ? : } +
+

{copy[tab]}

+
+
+ ); +} + +export default function H5QAExperience() { + const [activeTab, setActiveTab] = useState("qa"); + const [authOpen, setAuthOpen] = useState(false); + const [input, setInput] = useState(""); + const [messages, setMessages] = useState(initialMessages); + const [callableQa, setCallableQa] = useState([]); + const [loadingQa, setLoadingQa] = useState(true); + const [user, setUser] = useState(null); + const bottomRef = useRef(null); + + const qaSource = useMemo(() => (callableQa.length > 0 ? callableQa : mockCallableQa), [callableQa]); + + useEffect(() => { + api + .get("/standard-qa/callable") + .then(setCallableQa) + .catch(() => setCallableQa([])) + .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 ask(question: string) { + const trimmed = question.trim(); + if (!trimmed) return; + + const matchedQa = findQaAnswer(trimmed, qaSource); + setMessages((current) => [ + ...current, + { + id: `user-${Date.now()}`, + role: "user", + content: trimmed, + createdAt: nowText() + }, + buildAnswerMessage(trimmed, matchedQa) + ]); + setInput(""); + setActiveTab("qa"); + } + + function submit(event: FormEvent) { + event.preventDefault(); + ask(input); + } + + return ( +
+ + +
+
+
+
+
+ +
+
+

千问千答助手

+
+ + {loadingQa ? "正在检查可调用库" : callableQa.length > 0 ? "已连接可调用库" : "演示模式"} +
+
+
+ +
+
+ +
+ {activeTab === "qa" ? ( + <> +
+
+ 回答会优先遵守审核边界;涉及医疗、法律、极端风险或个人隐私时,请以人工复核为准。 +
+
+ {messages.map((message) => ( + + ))} +
+
+
+ +
+
+ {suggestedQuestions.map((item) => ( + + ))} +
+
+