diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e00609d..f5968bb 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -15,7 +15,8 @@ "Bash(ssh:*)", "Bash(ping:*)", "Bash(curl:*)", - "Bash(echo \"No health endpoint\" curl -s https://iconsulting.szaiai.com/api/v1/users/profile -H \"x-user-id: test\")" + "Bash(echo \"No health endpoint\" curl -s https://iconsulting.szaiai.com/api/v1/users/profile -H \"x-user-id: test\")", + "Bash(scp:*)" ] } } diff --git a/packages/services/conversation-service/src/conversation/conversation.controller.ts b/packages/services/conversation-service/src/conversation/conversation.controller.ts index de447df..010d52d 100644 --- a/packages/services/conversation-service/src/conversation/conversation.controller.ts +++ b/packages/services/conversation-service/src/conversation/conversation.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, + Delete, Param, Body, Headers, @@ -144,4 +145,21 @@ export class ConversationController { message: 'Conversation ended', }; } + + /** + * Delete a conversation + */ + @Delete(':id') + @HttpCode(HttpStatus.OK) + async deleteConversation( + @Headers('x-user-id') userId: string, + @Param('id') conversationId: string, + ) { + await this.conversationService.deleteConversation(conversationId, userId); + + return { + success: true, + message: 'Conversation deleted', + }; + } } diff --git a/packages/services/conversation-service/src/conversation/conversation.service.ts b/packages/services/conversation-service/src/conversation/conversation.service.ts index 33c894e..eba93a7 100644 --- a/packages/services/conversation-service/src/conversation/conversation.service.ts +++ b/packages/services/conversation-service/src/conversation/conversation.service.ts @@ -187,6 +187,20 @@ export class ConversationService { }); } + /** + * Delete a conversation and its messages + */ + async deleteConversation(conversationId: string, userId: string): Promise { + // Verify user owns the conversation + const conversation = await this.getConversation(conversationId, userId); + + // Delete messages first (due to foreign key constraint) + await this.messageRepo.delete({ conversationId: conversation.id }); + + // Delete conversation + await this.conversationRepo.delete(conversation.id); + } + /** * Generate a title from the first message */ diff --git a/packages/web-client/src/features/chat/presentation/components/ChatSidebar.tsx b/packages/web-client/src/features/chat/presentation/components/ChatSidebar.tsx index 22ee0bc..3041ab4 100644 --- a/packages/web-client/src/features/chat/presentation/components/ChatSidebar.tsx +++ b/packages/web-client/src/features/chat/presentation/components/ChatSidebar.tsx @@ -1,13 +1,15 @@ +import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { Plus, MessageSquare, Trash2 } from 'lucide-react'; +import { Plus, MessageSquare, Trash2, ChevronLeft } from 'lucide-react'; import { clsx } from 'clsx'; import { useChatStore } from '../stores/chatStore'; import { useChat } from '../hooks/useChat'; export function ChatSidebar() { const navigate = useNavigate(); - const { conversations, currentConversationId } = useChatStore(); + const { conversations, currentConversationId, userId, deleteConversation, toggleSidebar } = useChatStore(); const { createConversation } = useChat(); + const [deletingId, setDeletingId] = useState(null); const handleNewChat = async () => { const conversationId = await createConversation(); @@ -20,17 +22,38 @@ export function ChatSidebar() { navigate(`/chat/${id}`); }; + const handleDeleteConversation = async (id: string) => { + if (!userId || deletingId) return; + + setDeletingId(id); + const success = await deleteConversation(userId, id); + setDeletingId(null); + + if (success && currentConversationId === id) { + navigate('/chat'); + } + }; + return (
{/* Header */}
- +
+ + +
{/* Conversation list */} @@ -49,7 +72,9 @@ export function ChatSidebar() { title={conv.title || '新对话'} isActive={conv.id === currentConversationId} updatedAt={conv.updatedAt} + isDeleting={deletingId === conv.id} onClick={() => handleSelectConversation(conv.id)} + onDelete={() => handleDeleteConversation(conv.id)} /> ))}
@@ -71,14 +96,18 @@ interface ConversationItemProps { title: string; isActive: boolean; updatedAt: string; + isDeleting: boolean; onClick: () => void; + onDelete: () => void; } function ConversationItem({ title, isActive, updatedAt, + isDeleting, onClick, + onDelete, }: ConversationItemProps) { const formatTime = (dateStr: string) => { const date = new Date(dateStr); @@ -106,11 +135,13 @@ function ConversationItem({ return ( + {isDeleting ? ( +
+ ) : ( + + )} ); } diff --git a/packages/web-client/src/features/chat/presentation/pages/ChatPage.tsx b/packages/web-client/src/features/chat/presentation/pages/ChatPage.tsx index 2c17b06..db9e1ed 100644 --- a/packages/web-client/src/features/chat/presentation/pages/ChatPage.tsx +++ b/packages/web-client/src/features/chat/presentation/pages/ChatPage.tsx @@ -1,5 +1,7 @@ import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; +import { Menu, X } from 'lucide-react'; +import { clsx } from 'clsx'; import { ChatWindow } from '../components/ChatWindow'; import { ChatSidebar } from '../components/ChatSidebar'; import { useChatStore } from '../stores/chatStore'; @@ -8,7 +10,7 @@ import { useAnonymousAuth } from '@/shared/hooks/useAnonymousAuth'; export default function ChatPage() { const { conversationId } = useParams(); const { userId, isLoading: authLoading } = useAnonymousAuth(); - const { setCurrentConversation, loadConversations } = useChatStore(); + const { setCurrentConversation, loadConversations, sidebarOpen, toggleSidebar, setSidebarOpen } = useChatStore(); useEffect(() => { if (userId) { @@ -22,6 +24,13 @@ export default function ChatPage() { } }, [conversationId, setCurrentConversation]); + // Close sidebar on mobile when selecting a conversation + useEffect(() => { + if (conversationId && window.innerWidth < 768) { + setSidebarOpen(false); + } + }, [conversationId, setSidebarOpen]); + if (authLoading) { return (
@@ -34,14 +43,52 @@ export default function ChatPage() { } return ( -
- {/* Sidebar - hidden on mobile */} -
+
+ {/* Mobile overlay */} + {sidebarOpen && ( +
setSidebarOpen(false)} + /> + )} + + {/* Sidebar */} +
{/* Main chat area */} -
+
+ {/* Mobile header with menu button */} +
+ + 香港移民咨询 +
+ + {/* Desktop toggle button */} + {!sidebarOpen && ( + + )} +
diff --git a/packages/web-client/src/features/chat/presentation/stores/chatStore.ts b/packages/web-client/src/features/chat/presentation/stores/chatStore.ts index 4213f6c..7859daa 100644 --- a/packages/web-client/src/features/chat/presentation/stores/chatStore.ts +++ b/packages/web-client/src/features/chat/presentation/stores/chatStore.ts @@ -21,13 +21,20 @@ interface ChatState { userId: string | null; setUserId: (id: string) => void; + // Sidebar + sidebarOpen: boolean; + setSidebarOpen: (open: boolean) => void; + toggleSidebar: () => void; + // Conversations conversations: Conversation[]; currentConversationId: string | null; setConversations: (conversations: Conversation[]) => void; addConversation: (conversation: Conversation) => void; + removeConversation: (id: string) => void; setCurrentConversation: (id: string | null) => void; loadConversations: (userId: string) => Promise; + deleteConversation: (userId: string, conversationId: string) => Promise; // Messages messages: Record; @@ -46,11 +53,16 @@ interface ChatState { setConnected: (connected: boolean) => void; } -export const useChatStore = create((set) => ({ +export const useChatStore = create((set, get) => ({ // User userId: null, setUserId: (id) => set({ userId: id }), + // Sidebar - default open on desktop + sidebarOpen: window.innerWidth >= 768, + setSidebarOpen: (open) => set({ sidebarOpen: open }), + toggleSidebar: () => set((state) => ({ sidebarOpen: !state.sidebarOpen })), + // Conversations conversations: [], currentConversationId: null, @@ -59,6 +71,12 @@ export const useChatStore = create((set) => ({ set((state) => ({ conversations: [conversation, ...state.conversations], })), + removeConversation: (id) => + set((state) => ({ + conversations: state.conversations.filter((c) => c.id !== id), + currentConversationId: + state.currentConversationId === id ? null : state.currentConversationId, + })), setCurrentConversation: (id) => set({ currentConversationId: id }), loadConversations: async (userId) => { try { @@ -75,6 +93,24 @@ export const useChatStore = create((set) => ({ console.error('Failed to load conversations:', error); } }, + deleteConversation: async (userId, conversationId) => { + try { + const response = await fetch(`/api/v1/conversations/${conversationId}`, { + method: 'DELETE', + headers: { + 'x-user-id': userId, + }, + }); + const data = await response.json(); + if (data.success) { + get().removeConversation(conversationId); + return true; + } + } catch (error) { + console.error('Failed to delete conversation:', error); + } + return false; + }, // Messages messages: {},