Hide H5 auth and document phase one scope
This commit is contained in:
@@ -1,18 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { FormEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { FormEvent, useEffect, 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";
|
||||
@@ -26,10 +20,10 @@ import {
|
||||
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";
|
||||
const SHOW_AUTH_ENTRY = false;
|
||||
const SHOW_HISTORY_AND_PROFILE = false;
|
||||
|
||||
function nowText() {
|
||||
return new Intl.DateTimeFormat("zh-CN", {
|
||||
@@ -66,19 +60,7 @@ function buildUnavailableMessage(reason: "loading" | "empty" | "error"): ChatMes
|
||||
};
|
||||
}
|
||||
|
||||
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<MobileTab>("qa");
|
||||
const [authOpen, setAuthOpen] = useState(false);
|
||||
const [input, setInput] = useState("");
|
||||
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
|
||||
const [sessions, setSessions] = useState<ChatSession[]>([]);
|
||||
@@ -86,7 +68,6 @@ export default function H5QAExperience() {
|
||||
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);
|
||||
|
||||
@@ -128,41 +109,9 @@ export default function H5QAExperience() {
|
||||
.finally(() => setLoadingQa(false));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get("code");
|
||||
const state = params.get("state");
|
||||
if (code && state === "hyqa_h5") {
|
||||
setUser({
|
||||
mode: "wechat",
|
||||
name: "微信已授权",
|
||||
phone: "等待后端换取登录态"
|
||||
});
|
||||
window.history.replaceState(null, "", window.location.pathname);
|
||||
}
|
||||
}, []);
|
||||
|
||||
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 mockWechatLogin() {
|
||||
setUser({
|
||||
mode: "wechat",
|
||||
name: "微信用户",
|
||||
phone: "微信授权演示"
|
||||
});
|
||||
setAuthOpen(false);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
function persistMessages(nextMessages: ChatMessage[]) {
|
||||
setMessages(nextMessages);
|
||||
@@ -179,21 +128,6 @@ export default function H5QAExperience() {
|
||||
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) {
|
||||
@@ -239,7 +173,6 @@ export default function H5QAExperience() {
|
||||
];
|
||||
persistMessages(nextMessages);
|
||||
setInput("");
|
||||
setActiveTab("qa");
|
||||
}
|
||||
|
||||
function submit(event: FormEvent<HTMLFormElement>) {
|
||||
@@ -259,8 +192,8 @@ export default function H5QAExperience() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm leading-6 text-slate-600">
|
||||
<p>当前页面只做前端加法:登录注册为 mock 状态,问答只读取后端已审核可调用库。</p>
|
||||
<p>后续封装微信小程序时,主交互可以保留:顶部身份区、聊天流、快捷问题、底部导航和账号面板。</p>
|
||||
<p>一期用户端先免登录使用,H5 只保留问答主链路,降低首次体验门槛。</p>
|
||||
<p>登录、历史记录和个人中心已预留实现,等后端用户体系和会话接口确定后再打开。</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
@@ -280,12 +213,7 @@ export default function H5QAExperience() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setAuthOpen(true)}
|
||||
className="flex h-10 min-w-10 items-center justify-center rounded-md border border-line bg-white px-3 text-sm font-medium hover:bg-slate-50"
|
||||
>
|
||||
{user ? user.name.slice(0, 2) : <UserRound className="h-5 w-5" aria-hidden />}
|
||||
</button>
|
||||
{SHOW_AUTH_ENTRY ? null : null}
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between gap-2">
|
||||
<button
|
||||
@@ -300,180 +228,87 @@ export default function H5QAExperience() {
|
||||
</header>
|
||||
|
||||
<section className="relative flex min-h-0 flex-1 flex-col">
|
||||
{activeTab === "qa" ? (
|
||||
<>
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<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} />
|
||||
))}
|
||||
</div>
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-t border-line bg-white px-4 py-3">
|
||||
<div className="mb-3 flex gap-2 overflow-x-auto pb-1">
|
||||
{suggestedQuestions.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => ask(item.text)}
|
||||
className="shrink-0 rounded-md border border-line bg-panel px-3 py-2 text-left text-xs leading-5 text-slate-700 hover:bg-teal-50 hover:text-jade"
|
||||
>
|
||||
{item.text}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<form onSubmit={submit} className="flex items-end gap-2">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
rows={1}
|
||||
placeholder="输入你的问题..."
|
||||
className="max-h-28 min-h-11 flex-1 resize-none rounded-md border border-line bg-panel px-3 py-3 text-[15px] leading-5 outline-none focus:border-jade focus:bg-white"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!input.trim()}
|
||||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-md bg-jade text-white hover:bg-teal-800 disabled:bg-slate-300"
|
||||
aria-label="发送"
|
||||
>
|
||||
<SendHorizonal className="h-5 w-5" aria-hidden />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</>
|
||||
) : activeTab === "history" ? (
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">问答记录</h2>
|
||||
<p className="mt-1 text-xs text-slate-500">当前保存在本机,后续接用户接口。</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={clearHistory}
|
||||
className="flex h-9 items-center gap-1 rounded-md border border-line px-2 text-xs text-slate-500 hover:bg-slate-50"
|
||||
>
|
||||
<Trash2 className="h-3.5 w-3.5" aria-hidden />
|
||||
清空
|
||||
</button>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{sessions.map((session) => (
|
||||
<button
|
||||
key={session.id}
|
||||
onClick={() => openSession(session)}
|
||||
className={`w-full rounded-md border bg-white p-3 text-left shadow-sm ${
|
||||
session.id === currentSessionId ? "border-teal-200 ring-2 ring-teal-50" : "border-line"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="truncate text-sm font-medium text-ink">{session.title}</div>
|
||||
<div className="mt-1 line-clamp-2 text-xs leading-5 text-slate-500">{session.preview}</div>
|
||||
</div>
|
||||
<span className="shrink-0 text-xs text-slate-400">{formatSessionTime(session.updatedAt)}</span>
|
||||
</div>
|
||||
<div className="mt-3 text-xs text-slate-400">{session.questionCount} 个问题</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<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>
|
||||
) : activeTab === "me" ? (
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
<section className="rounded-md border border-line bg-white p-4 shadow-sm">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-md bg-jade text-white">
|
||||
<UserRound className="h-6 w-6" aria-hidden />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-semibold text-ink">{user?.name ?? "未登录用户"}</div>
|
||||
<div className="mt-1 text-sm text-slate-500">{user?.phone ?? "登录后同步问答记录和反馈"}</div>
|
||||
</div>
|
||||
</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} />
|
||||
))}
|
||||
</div>
|
||||
<div ref={bottomRef} />
|
||||
</div>
|
||||
|
||||
<div className="shrink-0 border-t border-line bg-white px-4 py-3">
|
||||
<div className="mb-3 flex gap-2 overflow-x-auto pb-1">
|
||||
{suggestedQuestions.map((item) => (
|
||||
<button
|
||||
onClick={() => setAuthOpen(true)}
|
||||
className="mt-4 h-10 w-full rounded-md bg-jade text-sm font-medium text-white hover:bg-teal-800"
|
||||
key={item.id}
|
||||
onClick={() => ask(item.text)}
|
||||
className="shrink-0 rounded-md border border-line bg-panel px-3 py-2 text-left text-xs leading-5 text-slate-700 hover:bg-teal-50 hover:text-jade"
|
||||
>
|
||||
{user ? "管理 mock 账号" : "登录 / 注册"}
|
||||
{item.text}
|
||||
</button>
|
||||
</section>
|
||||
<section className="mt-4 space-y-3 rounded-md border border-line bg-white p-4 text-sm text-slate-600 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>本机问答记录</span>
|
||||
<span className="font-medium text-ink">{sessions.length} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>答案反馈</span>
|
||||
<span className="font-medium text-ink">{messages.filter((message) => message.feedback).length} 条</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span>数据模式</span>
|
||||
<span className="font-medium text-ink">{qaLoadError ? "接口异常" : callableQa.length > 0 ? "接口数据" : "等待入库"}</span>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
<form onSubmit={submit} className="flex items-end gap-2">
|
||||
<textarea
|
||||
value={input}
|
||||
onChange={(event) => setInput(event.target.value)}
|
||||
rows={1}
|
||||
placeholder="输入你的问题..."
|
||||
className="max-h-28 min-h-11 flex-1 resize-none rounded-md border border-line bg-panel px-3 py-3 text-[15px] leading-5 outline-none focus:border-jade focus:bg-white"
|
||||
/>
|
||||
<button
|
||||
onClick={startNewSession}
|
||||
className="mt-4 flex h-10 w-full items-center justify-center gap-2 rounded-md border border-line bg-white text-sm font-medium text-ink hover:bg-slate-50"
|
||||
type="submit"
|
||||
disabled={!input.trim()}
|
||||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-md bg-jade text-white hover:bg-teal-800 disabled:bg-slate-300"
|
||||
aria-label="发送"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" aria-hidden />
|
||||
开始新的问答
|
||||
<SendHorizonal className="h-5 w-5" aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
|
||||
<MockAuthPanel
|
||||
open={authOpen}
|
||||
user={user}
|
||||
onClose={() => setAuthOpen(false)}
|
||||
onMockLogin={mockLogin}
|
||||
onWechatMockLogin={mockWechatLogin}
|
||||
onLogout={() => {
|
||||
setUser(null);
|
||||
setAuthOpen(false);
|
||||
}}
|
||||
/>
|
||||
</form>
|
||||
{SHOW_HISTORY_AND_PROFILE ? (
|
||||
<div className="mt-3 rounded-md bg-panel px-3 py-2 text-xs text-slate-500">
|
||||
登录、历史记录和个人中心入口已隐藏,二期接入用户体系后恢复。
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<nav className="grid h-[62px] shrink-0 grid-cols-3 border-t border-line bg-white text-xs">
|
||||
{[
|
||||
{ key: "qa" as const, label: "问答", icon: Home },
|
||||
{ key: "history" as const, label: "记录", icon: Clock3 },
|
||||
{ key: "me" as const, label: "我的", icon: Settings }
|
||||
].map((item) => {
|
||||
const Icon = item.icon;
|
||||
const active = activeTab === item.key;
|
||||
return (
|
||||
{SHOW_HISTORY_AND_PROFILE ? (
|
||||
<nav className="hidden h-[62px] shrink-0 grid-cols-3 border-t border-line bg-white text-xs">
|
||||
{[
|
||||
{ label: "问答" },
|
||||
{ label: "记录" },
|
||||
{ label: "我的" }
|
||||
].map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
onClick={() => setActiveTab(item.key)}
|
||||
className={`flex flex-col items-center justify-center gap-1 ${active ? "text-jade" : "text-slate-400"}`}
|
||||
key={item.label}
|
||||
className="flex flex-col items-center justify-center gap-1 text-slate-400"
|
||||
type="button"
|
||||
>
|
||||
<Icon className="h-5 w-5" aria-hidden />
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
))}
|
||||
</nav>
|
||||
) : null}
|
||||
</main>
|
||||
|
||||
<aside className="hidden px-8 py-10 lg:block">
|
||||
<div className="sticky top-10 max-w-sm rounded-md border border-line bg-white p-5 shadow-sm">
|
||||
<h2 className="text-base font-semibold">后续接真实能力时的接口边界</h2>
|
||||
<h2 className="text-base font-semibold">一期用户端范围</h2>
|
||||
<ul className="mt-4 space-y-3 text-sm leading-6 text-slate-600">
|
||||
<li>登录注册:替换 mock 面板,接微信授权或手机号验证码。</li>
|
||||
<li>问答检索:从当前可调用库接口升级为语义检索或智能体接口。</li>
|
||||
<li>历史记录:按用户维度保存问题、答案、反馈和风险提示。</li>
|
||||
<li>安全策略:继续只允许调用已审核、可调用、低/中风险内容。</li>
|
||||
<li>免登录进入 H5,优先验证问答主体验。</li>
|
||||
<li>问答只读取后端已审核可调用库,不使用本地 mock 答案。</li>
|
||||
<li>登录、会话记录、个人中心暂不展示,等后端用户体系确定后再开放。</li>
|
||||
<li>后续智能体接口会替代当前前端匹配逻辑,由后端负责检索和大模型调用。</li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -51,7 +51,7 @@ export const initialMessages: ChatMessage[] = [
|
||||
role: "assistant",
|
||||
content:
|
||||
"你好,我是千问千答助手。你可以把最近遇到的亲子、关系、情绪或行动问题发给我,我会优先基于已审核的答疑资产给你一个可落地的回应。",
|
||||
boundary: "当前为 H5 体验版,登录暂用 mock 占位;问答答案只读取后端可调用库。",
|
||||
boundary: "当前为 H5 体验版,一期免登录试用;问答答案只读取后端可调用库。",
|
||||
createdAt: "09:30"
|
||||
}
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user