From 596153085a4d6b1b610fb9ae07065037d4dca5dc Mon Sep 17 00:00:00 2001 From: chihlasm Date: Fri, 27 Feb 2026 07:20:04 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20add=20AI=20chat=20builder=20frontend=20?= =?UTF-8?q?=E2=80=94=20types,=20API=20client,=20store,=20components,=20pag?= =?UTF-8?q?e,=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - TypeScript types for chat session, messages, and responses - API client module with all 6 endpoints - Zustand store with session management, message sending, tree generation, import, resume - 7 chat components: ChatMessage, ChatInput, ChatPanel, PhaseIndicator, ChatToolbar, EmptyPreview, StaticTreePreview - AIChatBuilderPage with split-panel layout (60% chat / 40% preview) - Route at /ai/chat with lazy loading - "Build with AI" button on TreeLibraryPage - Session resume via URL search params Co-Authored-By: Claude Opus 4.6 --- frontend/src/api/aiChat.ts | 44 ++++ frontend/src/api/index.ts | 1 + frontend/src/components/ai-chat/ChatInput.tsx | 72 +++++++ .../src/components/ai-chat/ChatMessage.tsx | 41 ++++ frontend/src/components/ai-chat/ChatPanel.tsx | 47 +++++ .../src/components/ai-chat/ChatToolbar.tsx | 76 +++++++ .../src/components/ai-chat/EmptyPreview.tsx | 13 ++ .../src/components/ai-chat/PhaseIndicator.tsx | 50 +++++ .../components/ai-chat/StaticTreePreview.tsx | 80 ++++++++ frontend/src/pages/AIChatBuilderPage.tsx | 151 ++++++++++++++ frontend/src/pages/TreeLibraryPage.tsx | 23 ++- frontend/src/router.tsx | 9 + frontend/src/store/aiChatStore.ts | 190 ++++++++++++++++++ frontend/src/types/ai-chat.ts | 43 ++++ frontend/src/types/index.ts | 10 + 15 files changed, 844 insertions(+), 6 deletions(-) create mode 100644 frontend/src/api/aiChat.ts create mode 100644 frontend/src/components/ai-chat/ChatInput.tsx create mode 100644 frontend/src/components/ai-chat/ChatMessage.tsx create mode 100644 frontend/src/components/ai-chat/ChatPanel.tsx create mode 100644 frontend/src/components/ai-chat/ChatToolbar.tsx create mode 100644 frontend/src/components/ai-chat/EmptyPreview.tsx create mode 100644 frontend/src/components/ai-chat/PhaseIndicator.tsx create mode 100644 frontend/src/components/ai-chat/StaticTreePreview.tsx create mode 100644 frontend/src/pages/AIChatBuilderPage.tsx create mode 100644 frontend/src/store/aiChatStore.ts create mode 100644 frontend/src/types/ai-chat.ts diff --git a/frontend/src/api/aiChat.ts b/frontend/src/api/aiChat.ts new file mode 100644 index 00000000..1722b272 --- /dev/null +++ b/frontend/src/api/aiChat.ts @@ -0,0 +1,44 @@ +import { apiClient } from './client' +import type { + AIChatStartResponse, + AIChatMessageResponse, + AIChatSessionResponse, + AIChatGenerateResponse, + AIChatImportResponse, +} from '@/types' + +export const aiChatApi = { + startSession: async (flowType: 'troubleshooting' | 'procedural'): Promise => { + const { data } = await apiClient.post('/ai/chat/sessions', { flow_type: flowType }) + return data + }, + + sendMessage: async (sessionId: string, content: string): Promise => { + const { data } = await apiClient.post(`/ai/chat/sessions/${sessionId}/messages`, { content }) + return data + }, + + getSession: async (sessionId: string): Promise => { + const { data } = await apiClient.get(`/ai/chat/sessions/${sessionId}`) + return data + }, + + generateTree: async (sessionId: string): Promise => { + const { data } = await apiClient.post(`/ai/chat/sessions/${sessionId}/generate`) + return data + }, + + importTree: async ( + sessionId: string, + params?: { name?: string; description?: string; category_id?: string; tags?: string[] } + ): Promise => { + const { data } = await apiClient.post(`/ai/chat/sessions/${sessionId}/import`, params || {}) + return data + }, + + abandonSession: async (sessionId: string): Promise => { + await apiClient.delete(`/ai/chat/sessions/${sessionId}`) + }, +} + +export default aiChatApi diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index fba65d24..016efcbe 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -17,3 +17,4 @@ export { targetListsApi } from './targetLists' export { maintenanceSchedulesApi, batchLaunchApi } from './maintenanceSchedules' export { default as feedbackApi } from './feedback' export { default as aiBuilderApi } from './aiBuilder' +export { default as aiChatApi } from './aiChat' diff --git a/frontend/src/components/ai-chat/ChatInput.tsx b/frontend/src/components/ai-chat/ChatInput.tsx new file mode 100644 index 00000000..c10f4970 --- /dev/null +++ b/frontend/src/components/ai-chat/ChatInput.tsx @@ -0,0 +1,72 @@ +import { useState, useRef, useCallback } from 'react' +import { Send } from 'lucide-react' +import { cn } from '@/lib/utils' + +interface ChatInputProps { + onSend: (content: string) => void + disabled?: boolean + placeholder?: string +} + +export function ChatInput({ onSend, disabled, placeholder = 'Type a message...' }: ChatInputProps) { + const [value, setValue] = useState('') + const textareaRef = useRef(null) + + const handleSend = useCallback(() => { + const trimmed = value.trim() + if (!trimmed || disabled) return + onSend(trimmed) + setValue('') + if (textareaRef.current) { + textareaRef.current.style.height = 'auto' + } + }, [value, disabled, onSend]) + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault() + handleSend() + } + } + + const handleInput = () => { + if (textareaRef.current) { + textareaRef.current.style.height = 'auto' + textareaRef.current.style.height = Math.min(textareaRef.current.scrollHeight, 160) + 'px' + } + } + + return ( +
+