| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273 |
- "use client";
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
- import {
- fetchConversations,
- searchConversations,
- } from "../../agent/services/conversationApi";
- import type { ConversationListType } from "../../agent/services/conversationApi";
- import { ConversationSummary, VisitorStatusUpdatePayload } from "../../agent/types";
- import { useWebSocket } from "./useWebSocket";
- import { WSMessage } from "@/lib/websocket";
- import { ChatWebSocketPayload } from "../../agent/types";
- const sortByUpdatedAtDesc = (list: ConversationSummary[]) =>
- [...list].sort(
- (a, b) =>
- new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
- );
- import type { ConversationFilter } from "@/components/dashboard/ConversationHeader";
- interface UseConversationsOptions {
- agentId?: number | null;
- filter?: ConversationFilter;
- /** 内部对话(知识库测试)时传 "internal",默认访客对话 "visitor" */
- listType?: ConversationListType;
- }
- export function useConversations(options?: UseConversationsOptions) {
- const { agentId, filter = "all", listType = "visitor" } = options || {};
- const [conversations, setConversations] = useState<ConversationSummary[]>([]);
- const [filteredConversations, setFilteredConversations] = useState<
- ConversationSummary[]
- >([]);
- const [selectedConversationId, setSelectedConversationId] = useState<
- number | null
- >(null);
- const [searchQuery, setSearchQuery] = useState("");
- const [loading, setLoading] = useState(true);
- const [isInitialLoad, setIsInitialLoad] = useState(true);
- const searchRef = useRef("");
- // 根据 filter 过滤会话
- const applyFilter = useCallback(
- (conversations: ConversationSummary[]): ConversationSummary[] => {
- if (!agentId) {
- return conversations;
- }
- switch (filter) {
- case "mine":
- // 只显示当前用户参与过的会话(has_participated === true)
- // 即当前用户在该会话中发送过消息的会话
- return conversations.filter((conv) => conv.has_participated === true);
- case "others":
- // 显示除了当前用户参与过的其他人的会话(has_participated !== true)
- return conversations.filter((conv) => conv.has_participated !== true);
- case "all":
- default:
- return conversations;
- }
- },
- [agentId, filter]
- );
- const loadConversations = useCallback(async () => {
- setLoading(true);
- try {
- // 内部对话(知识库测试)必须带 user_id,后端否则返回 400;未登录或 agentId 未就绪时不请求
- if (listType === "internal" && !agentId) {
- setConversations([]);
- setFilteredConversations([]);
- setSelectedConversationId(null);
- return;
- }
- const data = await fetchConversations(agentId ?? undefined, listType === "internal" ? { type: "internal" } : undefined);
- setConversations(data);
- const filtered = listType === "internal" ? data : applyFilter(data);
- if (!searchRef.current.trim()) {
- setFilteredConversations(filtered);
- }
- setSelectedConversationId((prev) => {
- if (prev) {
- return prev;
- }
- return filtered.length > 0 ? filtered[0].id : null;
- });
- } catch (error) {
- console.error(error);
- } finally {
- setLoading(false);
- setIsInitialLoad(false);
- }
- }, [applyFilter, agentId, filter, listType]);
- useEffect(() => {
- loadConversations();
- }, [loadConversations]);
- // 当 filter / listType 改变时,重新应用过滤(不重新加载数据)
- useEffect(() => {
- if (isInitialLoad) {
- return;
- }
- const filtered = listType === "internal" ? conversations : applyFilter(conversations);
- setFilteredConversations(sortByUpdatedAtDesc(filtered));
- }, [filter, listType, conversations, isInitialLoad, applyFilter]);
- useEffect(() => {
- if (isInitialLoad) {
- return;
- }
- const handler = setTimeout(async () => {
- const query = searchQuery.trim();
- searchRef.current = query;
- if (!query) {
- const filtered = listType === "internal" ? conversations : applyFilter(conversations);
- setFilteredConversations(sortByUpdatedAtDesc(filtered));
- return;
- }
- if (listType === "internal") {
- setFilteredConversations(sortByUpdatedAtDesc(conversations.filter((c) => (c.last_message?.content ?? "").toLowerCase().includes(query.toLowerCase()))));
- setLoading(false);
- return;
- }
- try {
- setLoading(true);
- const data = await searchConversations(query, agentId ?? undefined);
- const filtered = applyFilter(data);
- setFilteredConversations(sortByUpdatedAtDesc(filtered));
- } catch (error) {
- console.error(error);
- setFilteredConversations([]);
- } finally {
- setLoading(false);
- }
- }, 300);
- return () => clearTimeout(handler);
- }, [searchQuery, conversations, isInitialLoad, applyFilter, agentId, listType]);
- const selectConversation = useCallback((conversationId: number | null) => {
- setSelectedConversationId((prev) =>
- prev === conversationId ? prev : conversationId
- );
- }, []);
- const updateConversation = useCallback(
- (
- conversationId: number,
- updater: (conversation: ConversationSummary) => ConversationSummary,
- options?: { skipResort?: boolean }
- ) => {
- const applyUpdate = (list: ConversationSummary[]) => {
- let changed = false;
- const next = list.map((conv) => {
- if (conv.id === conversationId) {
- changed = true;
- return updater(conv);
- }
- return conv;
- });
- if (!changed) {
- return list;
- }
- if (options?.skipResort) {
- return next;
- }
- return sortByUpdatedAtDesc(next);
- };
- setConversations((prev) => applyUpdate(prev));
- setFilteredConversations((prev) => {
- if (searchRef.current && !prev.some((item) => item.id === conversationId)) {
- return prev;
- }
- return applyUpdate(prev);
- });
- },
- []
- );
- const setAllConversations = useCallback((data: ConversationSummary[]) => {
- setConversations(data);
- if (!searchRef.current.trim()) {
- const filtered = applyFilter(data);
- setFilteredConversations(filtered);
- }
- }, [applyFilter]);
- const hasConversation = useCallback(
- (conversationId: number) => {
- return conversations.some((conv) => conv.id === conversationId);
- },
- [conversations]
- );
- // 建立全局 WebSocket 连接以接收 visitor_status_update 等全局事件
- // 使用第一个对话的 ID(如果存在),否则不建立连接
- const globalConversationId = conversations.length > 0 ? conversations[0].id : null;
- // 处理 visitor_status_update 事件
- const handleVisitorStatusUpdate = useCallback(
- (event: WSMessage<ChatWebSocketPayload>) => {
- if (event.type === "visitor_status_update" && event.data) {
- const payload = event.data as VisitorStatusUpdatePayload;
- if (payload?.conversation_id) {
- if (payload.is_online === true) {
- // 在线:更新为当前时间(实时更新在线状态)
- updateConversation(payload.conversation_id, (conv) => ({
- ...conv,
- last_seen_at: new Date().toISOString(),
- }));
- }
- // 离线时,last_seen_at 会在后端更新,这里不需要特殊处理
- // 因为对话列表会定期刷新,或者通过其他方式更新
- }
- }
- },
- [updateConversation]
- );
- // 建立全局 WebSocket 连接(用于接收全局事件)
- useWebSocket<ChatWebSocketPayload>({
- conversationId: globalConversationId,
- enabled: Boolean(globalConversationId && agentId),
- isVisitor: false,
- agentId: agentId ?? undefined,
- onMessage: handleVisitorStatusUpdate,
- onError: (error) => {
- // 静默处理错误,避免影响用户体验
- },
- onClose: () => {
- // 静默处理关闭,避免影响用户体验
- },
- });
- const contextValue = useMemo(
- () => ({
- conversations,
- filteredConversations,
- selectedConversationId,
- searchQuery,
- loading,
- isInitialLoad,
- setSearchQuery,
- selectConversation,
- refresh: loadConversations,
- updateConversation,
- setAllConversations,
- hasConversation,
- }),
- [
- conversations,
- filteredConversations,
- selectedConversationId,
- searchQuery,
- loading,
- isInitialLoad,
- selectConversation,
- loadConversations,
- updateConversation,
- setAllConversations,
- setSearchQuery,
- hasConversation,
- ]
- );
- return contextValue;
- }
|