From 19ead3506bac59ca46d4d3576f1a0e85be227ae9 Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Wed, 7 Jan 2026 07:13:28 -0600 Subject: [PATCH] feat(frontend): Add AI Integration UI components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add aiApi client with types (ChatMessage, ChatRequest, ChatResponse, AIConfig, AIModel, AIUsageStats) - Add useAI hooks (useAIConfig, useUpdateAIConfig, useAIModels, useAIChat, useAIUsage, useCurrentAIUsage, useAIHealth) - Add AI components (AIChat, AISettings, ChatMessage) - Add AIPage with chat interface and usage stats - Add AI tab to Settings page - Add AI Assistant link to dashboard navigation - 979 lines added across 13 files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/frontend/src/components/ai/AIChat.tsx | 232 ++++++++++++++ .../frontend/src/components/ai/AISettings.tsx | 300 ++++++++++++++++++ .../src/components/ai/ChatMessage.tsx | 83 +++++ apps/frontend/src/components/ai/index.ts | 4 + apps/frontend/src/components/index.ts | 3 + apps/frontend/src/hooks/index.ts | 1 + apps/frontend/src/hooks/useAI.ts | 134 ++++++++ apps/frontend/src/layouts/DashboardLayout.tsx | 2 + apps/frontend/src/pages/dashboard/AIPage.tsx | 107 +++++++ apps/frontend/src/pages/dashboard/index.ts | 1 + .../src/pages/settings/SettingsPage.tsx | 11 +- apps/frontend/src/router/index.tsx | 2 + apps/frontend/src/services/api.ts | 101 ++++++ 13 files changed, 979 insertions(+), 2 deletions(-) create mode 100644 apps/frontend/src/components/ai/AIChat.tsx create mode 100644 apps/frontend/src/components/ai/AISettings.tsx create mode 100644 apps/frontend/src/components/ai/ChatMessage.tsx create mode 100644 apps/frontend/src/components/ai/index.ts create mode 100644 apps/frontend/src/hooks/useAI.ts create mode 100644 apps/frontend/src/pages/dashboard/AIPage.tsx diff --git a/apps/frontend/src/components/ai/AIChat.tsx b/apps/frontend/src/components/ai/AIChat.tsx new file mode 100644 index 00000000..d4f73a2e --- /dev/null +++ b/apps/frontend/src/components/ai/AIChat.tsx @@ -0,0 +1,232 @@ +import { useState, useRef, useEffect, FormEvent } from 'react'; +import { Send, Trash2, Settings2, Loader2 } from 'lucide-react'; +import clsx from 'clsx'; +import { ChatMessage, ChatMessageProps } from './ChatMessage'; +import { useAIChat, useAIConfig, useAIModels } from '@/hooks/useAI'; +import { ChatMessage as ChatMessageType } from '@/services/api'; + +interface Message extends ChatMessageProps { + id: string; +} + +interface AIChatProps { + systemPrompt?: string; + placeholder?: string; + className?: string; + showModelSelector?: boolean; + onUsageUpdate?: () => void; +} + +export function AIChat({ + systemPrompt, + placeholder = 'Type your message...', + className, + showModelSelector = false, + onUsageUpdate, +}: AIChatProps) { + const [messages, setMessages] = useState([]); + const [input, setInput] = useState(''); + const [selectedModel, setSelectedModel] = useState(); + const messagesEndRef = useRef(null); + const inputRef = useRef(null); + + const { data: config } = useAIConfig(); + const { data: models } = useAIModels(); + const chatMutation = useAIChat(); + + // Set default model from config + useEffect(() => { + if (config?.default_model && !selectedModel) { + setSelectedModel(config.default_model); + } + }, [config, selectedModel]); + + // Auto scroll to bottom + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + // Auto resize textarea + const handleInputChange = (e: React.ChangeEvent) => { + setInput(e.target.value); + e.target.style.height = 'auto'; + e.target.style.height = `${Math.min(e.target.scrollHeight, 120)}px`; + }; + + const handleSubmit = async (e: FormEvent) => { + e.preventDefault(); + const trimmedInput = input.trim(); + if (!trimmedInput || chatMutation.isPending) return; + + const userMessage: Message = { + id: `user-${Date.now()}`, + role: 'user', + content: trimmedInput, + timestamp: new Date(), + }; + + setMessages((prev) => [...prev, userMessage]); + setInput(''); + + // Reset textarea height + if (inputRef.current) { + inputRef.current.style.height = 'auto'; + } + + // Build message history for API + const apiMessages: ChatMessageType[] = []; + + // Add system prompt if provided + if (systemPrompt) { + apiMessages.push({ role: 'system', content: systemPrompt }); + } else if (config?.system_prompt) { + apiMessages.push({ role: 'system', content: config.system_prompt }); + } + + // Add conversation history + messages.forEach((msg) => { + if (msg.role !== 'system') { + apiMessages.push({ role: msg.role, content: msg.content }); + } + }); + + // Add current user message + apiMessages.push({ role: 'user', content: trimmedInput }); + + // Add loading message + const loadingId = `assistant-${Date.now()}`; + setMessages((prev) => [ + ...prev, + { id: loadingId, role: 'assistant', content: '', isLoading: true }, + ]); + + try { + const response = await chatMutation.mutateAsync({ + messages: apiMessages, + model: selectedModel, + temperature: config?.temperature, + max_tokens: config?.max_tokens, + }); + + // Replace loading message with actual response + setMessages((prev) => + prev.map((msg) => + msg.id === loadingId + ? { + ...msg, + content: response.choices[0]?.message?.content || 'No response', + isLoading: false, + timestamp: new Date(), + } + : msg + ) + ); + + onUsageUpdate?.(); + } catch { + // Remove loading message on error + setMessages((prev) => prev.filter((msg) => msg.id !== loadingId)); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSubmit(e); + } + }; + + const clearChat = () => { + setMessages([]); + }; + + return ( +
+ {/* Header */} +
+
+ + + AI Assistant + +
+ +
+ {showModelSelector && models && models.length > 0 && ( + + )} + + +
+
+ + {/* Messages Area */} +
+ {messages.length === 0 ? ( +
+ +

Start a conversation

+

Type a message below to begin

+
+ ) : ( +
+ {messages.map((message) => ( + + ))} +
+
+ )} +
+ + {/* Input Area */} +
+
+