"use client"; import { Bot, MessageCircle, RotateCw, Send, X } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { apiFetch } from "@/lib/api"; import { getToken } from "@/lib/auth"; import { chatAgentApi, type ChatAgentMode } from "@/lib/chat-agent-api"; import { USER_TYPE } from "@/lib/roles"; interface ChatMessage { id: number; role: "user" | "agent"; content: string; } const GREETING = "您好,我是华夏基金智能助手。您可以咨询基金产品、风险等级和净值信息。"; /** * 选择用哪个 Agent: * - 已登录客户 → /agent/client/*(能读本人持仓与交易) * - 游客 / 员工 → /agent/customer/*(匿名客服,不需要 token) * 探测失败一律退回匿名客服,不阻断用户。 */ async function resolveMode(): Promise { if (!getToken()) return "customer"; try { const result = await apiFetch<{ user?: { user_type?: string | null } }>("/auth/me"); return result?.user?.user_type === USER_TYPE.CUSTOMER ? "client" : "customer"; } catch { return "customer"; } } export function AgentChat() { const [open, setOpen] = useState(false); const [input, setInput] = useState(""); const [sending, setSending] = useState(false); const [connecting, setConnecting] = useState(false); const [sessionId, setSessionId] = useState(null); const [mode, setMode] = useState("customer"); const [error, setError] = useState(null); const [messages, setMessages] = useState([ { id: 1, role: "agent", content: GREETING }, ]); const abortRef = useRef(null); const listRef = useRef(null); useEffect(() => () => abortRef.current?.abort(), []); useEffect(() => { listRef.current?.scrollTo({ top: listRef.current.scrollHeight, behavior: "smooth" }); }, [messages, connecting]); /** 建立会话:探测 Agent 类型 → session/create 换取后端签发的 session_id。 */ const connect = useCallback(async () => { setConnecting(true); setError(null); try { const resolved = await resolveMode(); const session = await chatAgentApi.createSession(resolved); setMode(resolved); setSessionId(session.session_id); } catch (err) { setError(err instanceof Error ? err.message : "会话建立失败,请重试"); } finally { setConnecting(false); } }, []); /** 打开面板即建立会话(不是等用户第一次提问才建)。 */ const openChat = useCallback(() => { setOpen(true); if (!sessionId && !connecting) void connect(); }, [connect, connecting, sessionId]); /** 关闭面板即结束会话:通知后端清理上下文与限流键,本地一并重置。 */ const closeChat = useCallback(async () => { abortRef.current?.abort(); setOpen(false); setInput(""); setError(null); setMessages([{ id: Date.now(), role: "agent", content: GREETING }]); const current = sessionId; setSessionId(null); if (!current) return; try { await chatAgentApi.endSession(mode, current); } catch { /* 会话清理失败不阻断用户 */ } }, [mode, sessionId]); const sendMessage = useCallback(async () => { const content = input.trim(); if (!content || sending || connecting) return; if (!sessionId) { setError("会话尚未建立,请点击重试。"); return; } setInput(""); setError(null); setMessages((prev) => [...prev, { id: Date.now(), role: "user", content }]); setSending(true); const controller = new AbortController(); abortRef.current = controller; try { const result = await chatAgentApi.chat( mode, { session_id: sessionId, query: content }, controller.signal ); setMessages((prev) => [ ...prev, { id: Date.now() + 1, role: "agent", content: result.error || result.text || "暂时没有找到合适的回答,请稍后再试。", }, ]); } catch (err) { if ((err as Error)?.name === "AbortError") return; setMessages((prev) => [ ...prev, { id: Date.now() + 1, role: "agent", content: err instanceof Error ? err.message : "当前服务暂时不可用,请稍后再试。", }, ]); } finally { setSending(false); abortRef.current = null; } }, [connecting, input, mode, sending, sessionId]); const ready = !!sessionId; return (
{open && (

华夏基金智能助手

{mode === "client" ? "已接入您的账户数据" : "在线为您提供基金信息服务"}

{messages.map((message) => (
{message.content}
))} {connecting && (
正在建立会话...
)} {sending && !connecting &&
正在整理信息...
}
{error && (
{error} {!ready && ( )}
)}
{ event.preventDefault(); void sendMessage(); }} className="flex gap-2 border-t border-slate-200 bg-white p-3" > setInput(event.target.value)} placeholder={ready ? "输入您的问题" : "正在准备会话..."} disabled={connecting} className="min-w-0 flex-1 rounded-xl border border-slate-200 px-3 py-2.5 text-sm outline-none transition focus:border-[var(--brand-primary)] disabled:bg-slate-50" />
)}
); }