diff --git a/frontend/public/env-config.js b/frontend/public/env-config.js new file mode 100644 index 0000000..3ec3798 --- /dev/null +++ b/frontend/public/env-config.js @@ -0,0 +1,3 @@ +// Runtime config injection — generated at container startup +// Uncomment and set the value for production deployment: +// window.__ENV = { NEXT_PUBLIC_API_BASE_URL: "http://your-backend:8091/api/v1" }; diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css new file mode 100644 index 0000000..7f146c3 --- /dev/null +++ b/frontend/src/app/globals.css @@ -0,0 +1,31 @@ +@import "tailwindcss"; + +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +body { + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} + +/* 自定义滚动条 */ +::-webkit-scrollbar { + width: 6px; +} +::-webkit-scrollbar-track { + background: transparent; +} +::-webkit-scrollbar-thumb { + background: #94a3b8; + border-radius: 3px; +} diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx new file mode 100644 index 0000000..1b57ac8 --- /dev/null +++ b/frontend/src/app/layout.tsx @@ -0,0 +1,19 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "AI Agent Chat", + description: "AI 智能体对话平台", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + {children} + + ); +} diff --git a/frontend/src/app/login/page.tsx b/frontend/src/app/login/page.tsx new file mode 100644 index 0000000..5ea5886 --- /dev/null +++ b/frontend/src/app/login/page.tsx @@ -0,0 +1,119 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { setUserInfo, getUserInfo, clearUserInfo } from '@/utils/cookie'; + +export default function Login() { + const router = useRouter(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [msg, setMsg] = useState({ text: '', type: '' }); + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [currentUser, setCurrentUser] = useState(''); + + useEffect(() => { + const info = getUserInfo(); + if (info?.user) { + setIsLoggedIn(true); + setCurrentUser(info.user); + router.push('/'); + } + }, [router]); + + const handleLogin = (e: React.FormEvent) => { + e.preventDefault(); + setMsg({ text: '', type: '' }); + if (!username || !password) { + setMsg({ text: '请输入账号与密码。', type: 'error' }); + return; + } + if (username !== 'admin' || password !== 'admin') { + setMsg({ text: '账号或密码错误(演示:admin / admin)。', type: 'error' }); + return; + } + setUserInfo(username); + setMsg({ text: '登录成功,正在跳转…', type: 'info' }); + setTimeout(() => router.push('/'), 500); + }; + + const handleLogout = () => { + clearUserInfo(); + setIsLoggedIn(false); + setCurrentUser(''); + setMsg({ text: '已退出登录。', type: 'info' }); + }; + + return ( +
+
+
+

AI 智能体工作台

+

登录后开始与 AI 对话

+
+ + {isLoggedIn ? ( +
+

+ 当前用户:{currentUser} +

+ +
+ ) : ( +
+
+ + setUsername(e.target.value)} + className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-emerald-400 transition" + placeholder="admin" + /> +
+
+ + setPassword(e.target.value)} + className="w-full px-4 py-3 bg-slate-700 border border-slate-600 rounded-lg text-white placeholder-slate-500 focus:outline-none focus:border-emerald-400 transition" + placeholder="admin" + /> +
+ + +
+ )} + + {msg.text && ( +
+ {msg.text} +
+ )} +
+
+ ); +} diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx new file mode 100644 index 0000000..ba6a0e5 --- /dev/null +++ b/frontend/src/app/page.tsx @@ -0,0 +1,367 @@ +'use client'; + +import { useState, useEffect, useRef } from 'react'; +import { useRouter } from 'next/navigation'; +import { getUserInfo, clearUserInfo } from '@/utils/cookie'; +import { agentApi } from '@/api/agent'; +import { AiAgentConfigResponseDTO } from '@/types/api'; + +interface Message { + id: string; + role: 'user' | 'agent'; + content: string; + timestamp: number; +} + +interface Session { + id: string; + backendSessionId?: string; + title: string; + messages: Message[]; + lastModified: number; +} + +const STORAGE_KEY = 'ai_agent_sessions'; + +function loadSessions(): Session[] { + if (typeof window === 'undefined') return []; + try { + const raw = localStorage.getItem(STORAGE_KEY); + return raw ? JSON.parse(raw) : []; + } catch { + return []; + } +} + +function saveSessions(sessions: Session[]) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(sessions)); +} + +export default function Home() { + const router = useRouter(); + const messagesEndRef = useRef(null); + + // 用户状态 + const [currentUser, setCurrentUser] = useState(''); + + // Agent 状态 + const [agents, setAgents] = useState([]); + const [selectedAgentId, setSelectedAgentId] = useState(''); + + // 会话状态 + const [sessions, setSessions] = useState([]); + const [currentSessionId, setCurrentSessionId] = useState(''); + + // 聊天状态 + const [messages, setMessages] = useState([]); + const [inputValue, setInputValue] = useState(''); + const [isSending, setIsSending] = useState(false); + + // 检查登录状态 & 加载数据 + useEffect(() => { + const info = getUserInfo(); + if (!info?.user) { + router.push('/login'); + return; + } + setCurrentUser(info.user); + + // 加载 Agent 列表 + agentApi + .queryAgentList() + .then(res => { + setAgents(res.data); + // 恢复上次选择的 Agent + const last = localStorage.getItem('ai_agent_last_agent'); + if (last && res.data.some(a => a.agentId === last)) { + setSelectedAgentId(last); + } else if (res.data.length > 0) { + setSelectedAgentId(res.data[0].agentId); + } + }) + .catch(console.error); + + // 加载本地会话 + const saved = loadSessions(); + setSessions(saved); + if (saved.length > 0) { + setCurrentSessionId(saved[0].id); + setMessages(saved[0].messages); + } + }, [router]); + + // 自动滚动到最新消息 + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + // 会话变更时持久化 + useEffect(() => { + if (sessions.length > 0) saveSessions(sessions); + }, [sessions]); + + const currentSession = sessions.find(s => s.id === currentSessionId); + + const createSession = () => { + const id = Date.now().toString(); + const newSession: Session = { + id, + title: `对话 ${sessions.length + 1}`, + messages: [], + lastModified: Date.now(), + }; + setSessions(prev => [newSession, ...prev]); + setCurrentSessionId(id); + setMessages([]); + }; + + const deleteSession = (id: string) => { + setSessions(prev => { + const next = prev.filter(s => s.id !== id); + if (currentSessionId === id) { + if (next.length > 0) { + setCurrentSessionId(next[0].id); + setMessages(next[0].messages); + } else { + setCurrentSessionId(''); + setMessages([]); + } + } + return next; + }); + }; + + const switchSession = (id: string) => { + setCurrentSessionId(id); + const s = sessions.find(s => s.id === id); + setMessages(s?.messages || []); + }; + + const updateSessionMessages = (sessionId: string, msgs: Message[]) => { + setSessions(prev => + prev.map(s => + s.id === sessionId + ? { + ...s, + messages: msgs, + lastModified: Date.now(), + title: + msgs.length === 1 + ? msgs[0].content.slice(0, 20) + : s.title, + } + : s + ) + ); + }; + + const sendMessage = async () => { + if (!inputValue.trim() || !selectedAgentId || isSending) return; + if (!currentSessionId) createSession(); + + const userMsg: Message = { + id: Date.now().toString(), + role: 'user', + content: inputValue.trim(), + timestamp: Date.now(), + }; + + const newMessages = [...messages, userMsg]; + setMessages(newMessages); + setInputValue(''); + setIsSending(true); + + // 记住上次选择的 Agent + localStorage.setItem('ai_agent_last_agent', selectedAgentId); + + try { + // 获取或创建后端会话 + let sessionId = currentSession?.backendSessionId; + if (!sessionId) { + const res = await agentApi.createSession(selectedAgentId, currentUser); + sessionId = res.data.sessionId; + setSessions(prev => + prev.map(s => + s.id === (currentSessionId || sessions[0]?.id) + ? { ...s, backendSessionId: sessionId } + : s + ) + ); + } + + // 发送消息 + const res = await agentApi.chat({ + agentId: selectedAgentId, + userId: currentUser, + sessionId: sessionId!, + message: userMsg.content, + }); + + const agentMsg: Message = { + id: (Date.now() + 1).toString(), + role: 'agent', + content: res.data.content, + timestamp: Date.now(), + }; + + const updated = [...newMessages, agentMsg]; + setMessages(updated); + updateSessionMessages(currentSessionId || sessions[0]?.id, updated); + } catch (err) { + const errMsg: Message = { + id: (Date.now() + 1).toString(), + role: 'agent', + content: `错误:${err instanceof Error ? err.message : '请求失败'}`, + timestamp: Date.now(), + }; + const updated = [...newMessages, errMsg]; + setMessages(updated); + } finally { + setIsSending(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }; + + const handleLogout = () => { + clearUserInfo(); + router.push('/login'); + }; + + return ( +
+ {/* 左侧栏 — 会话列表 */} +
+
+ +
+
+ {sessions.map(s => ( +
switchSession(s.id)} + className={`group flex items-center justify-between p-3 rounded-lg cursor-pointer transition ${ + s.id === currentSessionId + ? 'bg-slate-700' + : 'hover:bg-slate-700/50' + }`} + > + {s.title} + +
+ ))} +
+
+ + {/* 主聊天区域 */} +
+ {/* 顶部栏 */} +
+
+ AI 智能体 + +
+
+ {currentUser} + +
+
+ + {/* 消息列表 */} +
+ {messages.length === 0 && ( +
+

💬

+

选择一个 Agent,开始对话

+
+ )} + {messages.map(msg => ( +
+
+

{msg.content}

+

+ {new Date(msg.timestamp).toLocaleTimeString()} +

+
+
+ ))} + {isSending && ( +
+
+
+ 思考中… +
+
+
+ )} +
+
+ + {/* 输入区域 */} +
+
+