diff --git a/frontend/web/src/api/client.ts b/frontend/web/src/api/client.ts index c14c6cb..1499f74 100644 --- a/frontend/web/src/api/client.ts +++ b/frontend/web/src/api/client.ts @@ -33,9 +33,13 @@ export const api = { health: () => request<{ status: string }>('/health'), sessions: { - list: () => request<{ sessions: { id: string; title: string }[] }>('/sessions'), + list: () => request<{ sessions: { id: string; title: string; updated_at: string }[] }>('/sessions'), create: () => request<{ session_id: string }>('/sessions', { method: 'POST' }), delete: (id: string) => request(`/sessions/${id}`, { method: 'DELETE' }), + messages: (id: string) => + request<{ messages: { id: number; session_id: string; role: string; content: string; created_at: string }[] }>(`/sessions/${id}/messages`), + exportUrl: (id: string, format: 'markdown' | 'txt' | 'json' = 'markdown') => + `${BASE}/sessions/${id}/export?format=${format}`, }, admin: { diff --git a/frontend/web/src/components/layout/Sidebar.tsx b/frontend/web/src/components/layout/Sidebar.tsx index 253bc7a..126b1a9 100644 --- a/frontend/web/src/components/layout/Sidebar.tsx +++ b/frontend/web/src/components/layout/Sidebar.tsx @@ -1,10 +1,35 @@ +import { useCallback, useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import { useChatStore } from '../../store/chatStore' import { api } from '../../api/client' +interface SessionItem { id: string; title: string; updated_at: string } + +function relativeTime(iso: string): string { + const diff = Date.now() - new Date(iso).getTime() + const min = Math.floor(diff / 60000) + if (min < 1) return '刚刚' + if (min < 60) return `${min} 分钟前` + const hour = Math.floor(min / 60) + if (hour < 24) return `${hour} 小时前` + return `${Math.floor(hour / 24)} 天前` +} + export default function Sidebar() { const navigate = useNavigate() - const { currentSessionId, setSessionId, clearMessages } = useChatStore() + const { currentSessionId, setSessionId, clearMessages, messages } = useChatStore() + const [sessions, setSessions] = useState([]) + + const fetchSessions = useCallback(async () => { + try { + const data = await api.sessions.list() + setSessions(data.sessions || []) + } catch { /* ignore */ } + }, []) + + useEffect(() => { fetchSessions() }, [fetchSessions]) + // Refresh the list when a round finishes so new titles/ordering appear + useEffect(() => { fetchSessions() }, [messages.length, fetchSessions]) const handleNewChat = async () => { try { @@ -12,6 +37,19 @@ export default function Sidebar() { setSessionId(session_id) clearMessages() navigate(`/chat/${session_id}`) + fetchSessions() + } catch { /* ignore */ } + } + + const handleDelete = async (id: string) => { + if (!window.confirm('删除这个会话及其全部消息?')) return + try { + await api.sessions.delete(id) + if (id === currentSessionId) { + clearMessages() + navigate('/chat') + } + fetchSessions() } catch { /* ignore */ } } @@ -25,13 +63,32 @@ export default function Sidebar() { -
- {currentSessionId ? ( -
-

{currentSessionId.slice(0, 12)}...

-
- ) : ( +
+ {sessions.length === 0 ? (

暂无对话

+ ) : ( + sessions.map((s) => ( +
navigate(`/chat/${s.id}`)} + className="group px-3 py-2 rounded-[14px] cursor-pointer transition-colors" + style={{ + background: s.id === currentSessionId ? 'color-mix(in srgb, var(--mx-primary) 12%, transparent)' : 'transparent', + }} + > +
+

+ {s.title || '未命名会话'} +

+ +
+

{relativeTime(s.updated_at)}

+
+ )) )}
diff --git a/frontend/web/src/hooks/useWebSocket.ts b/frontend/web/src/hooks/useWebSocket.ts index e70e6c2..eeaef54 100644 --- a/frontend/web/src/hooks/useWebSocket.ts +++ b/frontend/web/src/hooks/useWebSocket.ts @@ -1,5 +1,6 @@ import { useEffect, useRef, useCallback } from 'react' import { useChatStore } from '../store/chatStore' +import { useAuthStore } from '../store/authStore' import { getToken } from '../api/client' import type { WSMessage } from '../types/chat' @@ -12,7 +13,8 @@ export function useWebSocket() { const token = getToken() if (!token) return - const ws = new WebSocket(`ws://localhost:8080/ws/chat?token=${encodeURIComponent(token)}`) + const uid = useAuthStore.getState().username || 'web' + const ws = new WebSocket(`ws://localhost:8080/ws/chat?token=${encodeURIComponent(token)}&user_id=${encodeURIComponent(uid)}`) wsRef.current = ws ws.onopen = () => { diff --git a/frontend/web/src/pages/chat/ChatPage.tsx b/frontend/web/src/pages/chat/ChatPage.tsx index 4b712df..df09c66 100644 --- a/frontend/web/src/pages/chat/ChatPage.tsx +++ b/frontend/web/src/pages/chat/ChatPage.tsx @@ -2,15 +2,40 @@ import { useEffect } from 'react' import { useParams } from 'react-router-dom' import { useChatStore } from '../../store/chatStore' import ChatContainer from '../../components/chat/ChatContainer' +import Sidebar from '../../components/layout/Sidebar' import { api } from '../../api/client' +import type { Message } from '../../types/chat' export default function ChatPage() { const { sessionId } = useParams() - const { setSessionId } = useChatStore() + const { setSessionId, setMessages } = useChatStore() useEffect(() => { - if (sessionId) setSessionId(sessionId) - }, [sessionId, setSessionId]) + if (!sessionId) return + setSessionId(sessionId) - return + // Load persisted history for the session + let cancelled = false + api.sessions.messages(sessionId).then((data) => { + if (cancelled) return + const msgs: Message[] = (data.messages || []).map((m) => ({ + id: `db-${m.id}`, + session_id: m.session_id, + role: m.role === 'user' ? 'user' : 'assistant', + content: m.content, + created_at: m.created_at, + })) + setMessages(msgs) + }).catch(() => { /* history unavailable */ }) + return () => { cancelled = true } + }, [sessionId, setSessionId, setMessages]) + + return ( +
+ +
+ +
+
+ ) } diff --git a/frontend/web/src/store/chatStore.ts b/frontend/web/src/store/chatStore.ts index c5cb795..f32f80a 100644 --- a/frontend/web/src/store/chatStore.ts +++ b/frontend/web/src/store/chatStore.ts @@ -10,6 +10,7 @@ interface ChatState { setInput: (text: string) => void setSessionId: (id: string) => void + setMessages: (msgs: Message[]) => void addMessage: (msg: Message) => void appendToLast: (content: string) => void setStreaming: (v: boolean) => void @@ -26,6 +27,7 @@ export const useChatStore = create((set, get) => ({ setInput: (text) => set({ inputText: text }), setSessionId: (id) => set({ currentSessionId: id }), + setMessages: (msgs) => set({ messages: msgs }), setStreaming: (v) => set({ isStreaming: v }), setIotDevices: (d) => set({ iotDevices: d }),