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,