diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ff03083..905f8ec 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,16 +1,18 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useCallback } from 'react'; import { TopBar } from './components/Layout/TopBar'; import { CollapsibleSidebar } from './components/Layout/CollapsibleSidebar'; import { SettingsLayout } from './components/Settings/SettingsLayout'; import { AgentLayout } from './components/Agent/AgentLayout'; -import { PluginLayout } from './components/Plugin/PluginLayout'; // Will rename to PluginLayout soon +import { PluginLayout } from './components/Plugin/PluginLayout'; import { LeftPanel } from './components/Chat/LeftPanel'; import { ChatPanel } from './components/Chat/ChatPanel'; import { RightPanel } from './components/Chat/RightPanel'; import { WorkflowListView } from './components/Chat/WorkflowListView'; +import { NewWorkflowDialog } from './components/Chat/NewWorkflowDialog'; import { AuthPage } from './components/Auth/AuthPage'; +import apiClient from './api/client'; +import type { ChatSessionDB } from './types'; -// For Chat Module State Persistence export interface Message { id: string; role: 'user' | 'assistant' | 'system'; @@ -25,33 +27,43 @@ export interface ChatSession { updatedAt: number; } +function mapSessionFromDB(s: ChatSessionDB): ChatSession { + return { + id: s.chat_id, + title: s.title, + messages: [], + updatedAt: new Date(s.updated_at).getTime(), + }; +} + function App() { const [isAuthenticated, setIsAuthenticated] = useState(false); - // Layout State const [mode, setMode] = useState<'work' | 'agent'>('work'); const [showSettings, setShowSettings] = useState(false); const [isSidebarOpen, setIsSidebarOpen] = useState(true); - // Module Sub-navigation States - // Work Mode const [workTab, setWorkTab] = useState<'chat' | 'workflow'>('chat'); const [selectedWorkflow, setSelectedWorkflow] = useState(null); - // Agent Mode const [agentTab, setAgentTab] = useState<'plugin' | 'agents'>('plugin'); - - // Settings Sub-tab const [settingsTab, setSettingsTab] = useState('users'); - - // Inner Agent Tab (temporary until full Agent layout rewrite) const [innerAgentTab, setInnerAgentTab] = useState('worker'); const [resourceTab, setResourceTab] = useState('skill'); - // Chat State Hoisted for Persistence const [chatSessions, setChatSessions] = useState([]); const [activeSessionId, setActiveSessionId] = useState(null); + const loadChatSessions = useCallback(async () => { + try { + const response = await apiClient.get('/api/v1/chat'); + const sessions: ChatSessionDB[] = response.data?.sessions || []; + setChatSessions(sessions.map(mapSessionFromDB)); + } catch (error) { + console.error('Failed to load chat sessions', error); + } + }, []); + useEffect(() => { const token = localStorage.getItem('token'); if (token) { @@ -59,13 +71,18 @@ function App() { } }, []); + useEffect(() => { + if (isAuthenticated) { + loadChatSessions(); + } + }, [isAuthenticated, loadChatSessions]); + if (!isAuthenticated) { return setIsAuthenticated(true)} />; } return (
- {/* 1. Top Bar */} - {/* 2. Main Content Area */}
{showSettings ? ( ) : ( <> - {/* Collapsible Main Sidebar */} - {/* Dynamic View based on Mode and Tab */}
{mode === 'work' && workTab === 'chat' && (
@@ -99,17 +113,18 @@ function App() { activeTab="chats" selectedWorkflow={null} setSelectedWorkflow={() => {}} - // Pass hoisted state down chatSessions={chatSessions} setChatSessions={setChatSessions} activeSessionId={activeSessionId} setActiveSessionId={setActiveSessionId} + onSessionsChanged={loadChatSessions} />
@@ -117,7 +132,19 @@ function App() { {mode === 'work' && workTab === 'workflow' && ( <> - {selectedWorkflow ? ( + {selectedWorkflow === 'new' ? ( + <> + + setSelectedWorkflow(null)} + onSuccess={(traceId: string) => setSelectedWorkflow(traceId)} + /> + + ) : selectedWorkflow ? ( <> >; activeSessionId: string | null; setActiveSessionId: React.Dispatch>; + onSessionsChanged?: () => void; } -export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId }: ChatPanelProps) { +export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setActiveSessionId, onSessionsChanged }: ChatPanelProps) { const [input, setInput] = useState(''); const [loading, setLoading] = useState(false); const [mode, setMode] = useState<'chat' | 'deploy'>('chat'); + const [loadingMessages, setLoadingMessages] = useState(false); const fileInputRef = useRef(null); const messagesEndRef = useRef(null); @@ -28,6 +31,36 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA scrollToBottom(); }, [messages]); + const loadMessages = useCallback(async (chatId: string) => { + setLoadingMessages(true); + try { + const response = await apiClient.get(`/api/v1/chat/${chatId}`); + const dbMessages: ChatMessageDB[] = response.data?.messages || []; + const mapped: Message[] = dbMessages.map((m) => ({ + id: m.message_id, + role: m.message_owner === 'user' ? 'user' : 'assistant', + content: m.message, + timestamp: new Date(m.created_at).getTime(), + })); + setChatSessions((prev) => + prev.map((s) => (s.id === chatId ? { ...s, messages: mapped } : s)) + ); + } catch (error) { + console.error('Failed to load messages', error); + } finally { + setLoadingMessages(false); + } + }, [setChatSessions]); + + useEffect(() => { + if (activeSessionId) { + const session = chatSessions.find((s) => s.id === activeSessionId); + if (session && session.messages.length === 0) { + loadMessages(activeSessionId); + } + } + }, [activeSessionId]); + const updateSessionMessages = (newMessages: Message[]) => { if (!activeSessionId) return; setChatSessions((prev) => @@ -39,39 +72,30 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA ); }; - const handleFileUpload = async (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (!file || !activeSessionId) return; - - const formData = new FormData(); - formData.append('file', file); - - setLoading(true); + const handleNewChat = async () => { + if (!onSessionsChanged) return; try { - const response = await apiClient.post('/api/v1/adapter/client/upload', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, + const response = await apiClient.post('/api/v1/chat', { + title: '新对话', + initial_message: '你好', }); - const aiMessage: Message = { - id: Date.now().toString(), - role: 'assistant', - content: `已上传文件: ${response.data.filename}`, - timestamp: Date.now(), + const chatId: string = response.data.chat_id; + const reply: string = response.data.reply || '你好!我是 kilostar 助手,有什么可以帮你的吗?'; + + const newSession: ChatSession = { + id: chatId, + title: '新对话', + messages: [ + { id: chatId + '_user', role: 'user', content: '你好', timestamp: Date.now() }, + { id: chatId + '_ai', role: 'assistant', content: reply, timestamp: Date.now() }, + ], + updatedAt: Date.now(), }; - updateSessionMessages([...messages, aiMessage]); + setChatSessions((prev) => [newSession, ...prev]); + setActiveSessionId(chatId); + onSessionsChanged(); } catch (error) { - console.error('Error uploading file', error); - const errorMessage: Message = { - id: Date.now().toString(), - role: 'assistant', - content: '文件上传失败。', - timestamp: Date.now(), - }; - updateSessionMessages([...messages, errorMessage]); - } finally { - setLoading(false); - if (fileInputRef.current) { - fileInputRef.current.value = ''; - } + console.error('Failed to create chat session', error); } }; @@ -86,21 +110,19 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA timestamp: Date.now(), }; - updateSessionMessages([...messages, userMessage]); + const currentMessages = activeSession?.messages || []; + updateSessionMessages([...currentMessages, userMessage]); setInput(''); setLoading(true); try { - const promptModifier = mode === 'deploy' ? '[DEPLOY TASK] ' : ''; - const response = await apiClient.post('/api/v1/adapter/client', { - message: promptModifier + userMessage.content, + const response = await apiClient.post(`/api/v1/chat/${activeSessionId}/reply`, { + message: userText, }); - const responseData = response.data.message; - let aiContent = responseData || 'I received your message.'; + const replyContent: string = response.data?.reply || '收到你的消息。'; - // Auto-update title if it's the first user message - if (messages.length <= 1 && userText.length > 0) { + if (currentMessages.length <= 1 && userText.length > 0) { setChatSessions((prev) => prev.map((s) => s.id === activeSessionId @@ -113,20 +135,21 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA const aiMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', - content: aiContent, + content: replyContent, timestamp: Date.now(), }; - updateSessionMessages([...messages, userMessage, aiMessage]); + const updatedMessages = [...(currentMessages.length > 0 ? currentMessages : []), userMessage, aiMessage]; + updateSessionMessages(updatedMessages); } catch (error) { console.error('Error sending message', error); const errorMessage: Message = { id: (Date.now() + 1).toString(), role: 'assistant', - content: 'Sorry, I encountered an error communicating with the server.', + content: '抱歉,与服务器通信时出错。', timestamp: Date.now(), }; - updateSessionMessages([...messages, userMessage, errorMessage]); + updateSessionMessages([...currentMessages, userMessage, errorMessage]); } finally { setLoading(false); } @@ -139,23 +162,7 @@ export function ChatPanel({ chatSessions, setChatSessions, activeSessionId, setA

kilostar Assistant

Select a chat history or create a new one to start.

-
+ const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + handleSubmit(); + } + }; -
+ return ( +
+
+ +
+ +

新建工作流

+
+
+
+ + +
{error && ( -
+
+ ⚠️ {error}
)} -
- +
+ +

为你的工作流起一个简洁、描述性的名称

setTitle(e.target.value)} - placeholder="例如:爬取最新的技术新闻" - className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500" + placeholder="例如:爬取最新技术新闻并生成摘要" + className="w-full px-4 py-3 text-lg font-medium text-slate-800 bg-slate-50 border border-slate-200 rounded-xl placeholder:text-slate-300 focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-400 focus:bg-white transition-all" autoFocus />
-
- +
+ +

详细描述你希望 AI 完成的任务,越具体效果越好