From cd73ed3001a1ad86b98fcab0e54e7403b1827950 Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Sun, 5 Jul 2026 18:47:59 +0800 Subject: [PATCH] Enhance H5 QA session interactions --- .../frontend/components/h5/ChatBubble.tsx | 45 +++- .../frontend/components/h5/H5QAExperience.tsx | 223 +++++++++++++++--- hy_qa_asset_backend/frontend/lib/h5MockQa.ts | 36 +++ 3 files changed, 274 insertions(+), 30 deletions(-) diff --git a/hy_qa_asset_backend/frontend/components/h5/ChatBubble.tsx b/hy_qa_asset_backend/frontend/components/h5/ChatBubble.tsx index f4cdcad..ea6785d 100644 --- a/hy_qa_asset_backend/frontend/components/h5/ChatBubble.tsx +++ b/hy_qa_asset_backend/frontend/components/h5/ChatBubble.tsx @@ -1,9 +1,17 @@ "use client"; -import { Bot, UserRound } from "lucide-react"; +import { Bot, Check, Copy, ThumbsDown, ThumbsUp, UserRound } from "lucide-react"; import type { ChatMessage } from "@/lib/h5MockQa"; -export default function ChatBubble({ message }: { message: ChatMessage }) { +export default function ChatBubble({ + message, + onCopy, + onFeedback +}: { + message: ChatMessage; + onCopy?: (message: ChatMessage) => void; + onFeedback?: (messageId: string, feedback: "liked" | "disliked") => void; +}) { const isUser = message.role === "user"; return ( @@ -29,7 +37,38 @@ export default function ChatBubble({ message }: { message: ChatMessage }) {
{message.boundary}
) : null} -
{message.createdAt}
+
+ {message.createdAt} + {!isUser ? ( + + + + + + ) : null} +
{isUser ? (
diff --git a/hy_qa_asset_backend/frontend/components/h5/H5QAExperience.tsx b/hy_qa_asset_backend/frontend/components/h5/H5QAExperience.tsx index 8e5a246..a22fc83 100644 --- a/hy_qa_asset_backend/frontend/components/h5/H5QAExperience.tsx +++ b/hy_qa_asset_backend/frontend/components/h5/H5QAExperience.tsx @@ -1,20 +1,36 @@ "use client"; import { FormEvent, useEffect, useMemo, useRef, useState } from "react"; -import { BookOpenCheck, Clock3, Home, MessageCircle, SendHorizonal, Settings, ShieldCheck, UserRound } from "lucide-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, mockCallableQa, 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", { @@ -35,25 +51,14 @@ function buildAnswerMessage(question: string, qa: CallableQAItem): ChatMessage { }; } -function EmptyState({ tab }: { tab: MobileTab }) { - const copy = { - history: "登录后这里会展示你的历史提问记录。当前先用本机 mock 会话演示。", - me: "账号、学习阶段、隐私设置后续会接真实接口;现在先预留微信授权入口。", - qa: "" - }; - - if (tab === "qa") return null; - - return ( -
-
-
- {tab === "history" ? : } -
-

{copy[tab]}

-
-
- ); +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() { @@ -61,13 +66,38 @@ export default function H5QAExperience() { 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 [user, setUser] = useState(null); + const [copiedMessageId, setCopiedMessageId] = useState(null); const bottomRef = useRef(null); const qaSource = useMemo(() => (callableQa.length > 0 ? callableQa : mockCallableQa), [callableQa]); + 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") @@ -89,21 +119,73 @@ export default function H5QAExperience() { 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, qaSource); - setMessages((current) => [ - ...current, + const timestamp = Date.now(); + const nextMessages: ChatMessage[] = [ + ...messages, { - id: `user-${Date.now()}`, + id: `user-${timestamp}`, role: "user", content: trimmed, createdAt: nowText() }, - buildAnswerMessage(trimmed, matchedQa) - ]); + { ...buildAnswerMessage(trimmed, matchedQa), id: `assistant-${timestamp}` } + ]; + persistMessages(nextMessages); setInput(""); setActiveTab("qa"); } @@ -153,6 +235,16 @@ export default function H5QAExperience() { {user ? user.name.slice(0, 2) : }
+
+ + {copiedMessageId ? 已复制回答 : null} +
@@ -164,7 +256,7 @@ export default function H5QAExperience() {
{messages.map((message) => ( - + ))}
@@ -201,8 +293,85 @@ export default function H5QAExperience() {
+ ) : activeTab === "history" ? ( +
+
+
+

问答记录

+

当前保存在本机,后续接用户接口。

+
+ +
+
+ {sessions.map((session) => ( + + ))} +
+
+ ) : activeTab === "me" ? ( +
+
+
+
+ +
+
+
{user?.name ?? "未登录用户"}
+
{user?.phone ?? "登录后同步问答记录和反馈"}
+
+
+ +
+
+
+ 本机问答记录 + {sessions.length} 条 +
+
+ 答案反馈 + {messages.filter((message) => message.feedback).length} 条 +
+
+ 数据模式 + {callableQa.length > 0 ? "接口数据" : "mock 演示"} +
+
+ +
) : ( - + null )} 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,