From 7438323e9680473196c1a67f373077cbe25a012d Mon Sep 17 00:00:00 2001 From: WangDL Date: Fri, 22 May 2026 11:07:58 +0800 Subject: [PATCH] feat: message persistence + inline title edit + redesigned input + fix delete --- src/pages/TaskAssistant.tsx | 258 +++++++++++++++---------------- src/services/conversation-api.ts | 13 +- 2 files changed, 137 insertions(+), 134 deletions(-) diff --git a/src/pages/TaskAssistant.tsx b/src/pages/TaskAssistant.tsx index 889ad45..8d8034a 100644 --- a/src/pages/TaskAssistant.tsx +++ b/src/pages/TaskAssistant.tsx @@ -1,15 +1,17 @@ import { useState, useRef, useEffect, useCallback } from 'react' -import { Typography, Input, Button, Space, Avatar, Spin, theme, Modal, Tooltip } from 'antd' +import { Input, Button, Avatar, Spin, theme, Modal, Tooltip, Typography } from 'antd' import { SendOutlined, RobotOutlined, UserOutlined, PlusOutlined, DeleteOutlined, StopOutlined, MessageOutlined, } from '@ant-design/icons' import { sendMessage } from '@/services/ai-chat' -import { listConversations, createConversation, deleteConversation } from '@/services/conversation-api' +import { + listConversations, createConversation, deleteConversation, + getMessages, updateConversation, type Conversation, +} from '@/services/conversation-api' import Markdown from '@/components/Markdown' const { Text } = Typography -const { TextArea } = Input interface Message { id: string @@ -18,88 +20,93 @@ interface Message { timestamp: number } -interface Conversation { - id: string - title: string - updatedAt: string -} - export default function TaskAssistant() { const [conversations, setConversations] = useState([]) const [activeId, setActiveId] = useState(null) const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [loading, setLoading] = useState(false) + const [editingId, setEditingId] = useState(null) + const [editTitle, setEditTitle] = useState('') const abortRef = useRef(null) const messagesEndRef = useRef(null) + const editInputRef = useRef(null) const { token } = theme.useToken() // Load conversations - useEffect(() => { - listConversations().then(setConversations).catch(() => {}) + const loadConversations = useCallback(async () => { + try { setConversations(await listConversations()) } catch { /* */ } }, []) + useEffect(() => { loadConversations() }, [loadConversations]) // Scroll to bottom useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }) }, [messages]) - // Switch conversation - const switchConversation = useCallback((id: string) => { - if (loading) { - abortRef.current?.abort() - setLoading(false) - } + // Load messages when switching conversation + const switchConversation = useCallback(async (id: string) => { + if (loading) { abortRef.current?.abort(); setLoading(false) } setActiveId(id) setMessages([]) - setInput('') + try { + const records = await getMessages(id) + setMessages(records.map(m => ({ + id: m.id, role: m.role, content: m.content, + timestamp: new Date(m.createdAt).getTime(), + }))) + } catch { /* */ } }, [loading]) // New conversation const handleNew = async () => { - if (loading) { - abortRef.current?.abort() - setLoading(false) - } + if (loading) { abortRef.current?.abort(); setLoading(false) } try { const conv = await createConversation() setConversations(prev => [conv, ...prev]) setActiveId(conv.id) setMessages([]) setInput('') - } catch { /* ignore */ } + } catch { /* */ } } // Delete conversation - const handleDelete = async (id: string, e: React.MouseEvent) => { - e.stopPropagation() + const handleDelete = async (id: string) => { Modal.confirm({ - title: '删除对话', - content: '确定要删除这个对话吗?', - okText: '删除', - okType: 'danger', - cancelText: '取消', + title: '删除对话', content: '确定要删除这个对话吗?', + okText: '删除', okType: 'danger', cancelText: '取消', onOk: async () => { await deleteConversation(id).catch(() => {}) setConversations(prev => prev.filter(c => c.id !== id)) - if (activeId === id) { - setActiveId(null) - setMessages([]) - } + if (activeId === id) { setActiveId(null); setMessages([]) } }, }) } + // Start editing title + const startEdit = (conv: Conversation) => { + setEditingId(conv.id) + setEditTitle(conv.title) + setTimeout(() => editInputRef.current?.focus(), 50) + } + + // Save title + const saveTitle = async (id: string) => { + const title = editTitle.trim() + if (title && title !== conversations.find(c => c.id === id)?.title) { + await updateConversation(id, title).catch(() => {}) + setConversations(prev => prev.map(c => c.id === id ? { ...c, title } : c)) + } + setEditingId(null) + } + // Send message const handleSend = async () => { const text = input.trim() if (!text || loading) return const userMsg: Message = { - id: Date.now().toString(), - role: 'user', - content: text, - timestamp: Date.now(), + id: Date.now().toString(), role: 'user', content: text, timestamp: Date.now(), } const newMessages = [...messages, userMsg] setMessages(newMessages) @@ -112,95 +119,81 @@ export default function TaskAssistant() { try { const result = await sendMessage( newMessages.map(m => ({ role: m.role, content: m.content })), - activeId ?? undefined, - controller.signal, + activeId ?? undefined, controller.signal, ) - // Use returned conversationId if this was auto-created - if (result.conversationId && !activeId) { - setActiveId(result.conversationId) - const conv = await createConversation( - newMessages[newMessages.length - 1]?.content?.slice(0, 30) - ).catch(() => null) - if (conv) setConversations(prev => [conv, ...prev]) + const convId = result.conversationId || activeId + if (convId && convId !== activeId) { + setActiveId(convId) + loadConversations() } const assistantMsg: Message = { - id: (Date.now() + 1).toString(), - role: 'assistant', - content: result.content, - timestamp: Date.now(), + id: (Date.now() + 1).toString(), role: 'assistant', + content: result.content, timestamp: Date.now(), } setMessages(prev => [...prev, assistantMsg]) - - // Refresh conversation list for updated timestamps - listConversations().then(setConversations).catch(() => {}) + loadConversations() } catch (err: any) { if (err.name === 'AbortError') return - const errorMsg: Message = { - id: (Date.now() + 1).toString(), - role: 'assistant', + setMessages(prev => [...prev, { + id: (Date.now() + 1).toString(), role: 'assistant', content: '请求失败:' + (err instanceof Error ? err.message : '未知错误'), timestamp: Date.now(), - } - setMessages(prev => [...prev, errorMsg]) + }]) } finally { setLoading(false) abortRef.current = null } } - // Stop generation - const handleStop = () => { - abortRef.current?.abort() - setLoading(false) - } + const handleStop = () => { abortRef.current?.abort(); setLoading(false) } const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault() - handleSend() - } + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } } return ( -
+
{/* Sidebar */}
-
- +
+
-
+
{conversations.map(conv => ( -
switchConversation(conv.id)} +
activeId !== conv.id && switchConversation(conv.id)} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', - padding: '8px 12px', margin: '2px 0', borderRadius: 8, cursor: 'pointer', + padding: '8px 10px', marginBottom: 2, borderRadius: 8, cursor: 'pointer', background: activeId === conv.id ? token.colorFillSecondary : 'transparent', - transition: 'background 0.2s', - }} - onMouseEnter={e => (e.currentTarget.style.background = token.colorFillSecondary)} - onMouseLeave={e => { - if (activeId !== conv.id) e.currentTarget.style.background = 'transparent' - }} - > - - - {conv.title} - + }}> + + {editingId === conv.id ? ( + setEditTitle(e.target.value)} + onBlur={() => saveTitle(conv.id)} + onPressEnter={() => saveTitle(conv.id)} + onClick={e => e.stopPropagation()} + style={{ flex: 1, fontSize: 13 }} + /> + ) : ( + { e.stopPropagation(); startEdit(conv) }} + >{conv.title} + )} handleDelete(conv.id, e)} - style={{ fontSize: 12, color: token.colorTextQuaternary, opacity: 0, transition: 'opacity 0.2s' }} - className="conv-delete-icon" + onClick={e => { e.stopPropagation(); handleDelete(conv.id) }} + style={{ fontSize: 12, color: token.colorTextQuaternary, marginLeft: 4, cursor: 'pointer' }} />
@@ -208,6 +201,8 @@ export default function TaskAssistant() { {conversations.length === 0 && (
暂无对话 +
+ 双击标题可重命名
)}
@@ -215,10 +210,12 @@ export default function TaskAssistant() { {/* Chat area */}
+ {/* Messages */}
{messages.length === 0 && (
( -
- + : } style={{ backgroundColor: msg.role === 'user' ? token.colorPrimary : token.colorSuccess, flexShrink: 0, }} /> -
- {msg.role === 'assistant' - ? - : msg.content - } +
+ {msg.role === 'assistant' ? : msg.content}
))} @@ -279,44 +267,48 @@ export default function TaskAssistant() {
)} -
- {/* Input area */} + {/* Input area — redesigned */}
- -