feat(frontend): 步骤 6.6 — 页面与布局(登录页、聊天主页)
This commit is contained in:
3
frontend/public/env-config.js
Normal file
3
frontend/public/env-config.js
Normal file
@@ -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" };
|
||||||
31
frontend/src/app/globals.css
Normal file
31
frontend/src/app/globals.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
19
frontend/src/app/layout.tsx
Normal file
19
frontend/src/app/layout.tsx
Normal file
@@ -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 (
|
||||||
|
<html lang="zh">
|
||||||
|
<body className="antialiased">{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
119
frontend/src/app/login/page.tsx
Normal file
119
frontend/src/app/login/page.tsx
Normal file
@@ -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 (
|
||||||
|
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900">
|
||||||
|
<div className="w-full max-w-md p-8 bg-slate-800/80 backdrop-blur rounded-2xl shadow-2xl border border-slate-700">
|
||||||
|
<div className="text-center mb-8">
|
||||||
|
<h1 className="text-3xl font-bold text-white mb-2">AI 智能体工作台</h1>
|
||||||
|
<p className="text-slate-400">登录后开始与 AI 对话</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isLoggedIn ? (
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<p className="text-slate-300">
|
||||||
|
当前用户:<span className="text-emerald-400 font-semibold">{currentUser}</span>
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="w-full py-3 bg-slate-700 hover:bg-slate-600 text-white rounded-lg transition"
|
||||||
|
>
|
||||||
|
退出登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleLogin} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-1">账号</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm text-slate-400 mb-1">密码</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={e => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="w-full py-3 bg-emerald-500 hover:bg-emerald-600 text-white font-semibold rounded-lg transition"
|
||||||
|
>
|
||||||
|
登 录
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setUsername('admin'); setPassword('admin'); }}
|
||||||
|
className="w-full py-2 text-sm text-slate-400 hover:text-slate-300 transition"
|
||||||
|
>
|
||||||
|
填充演示账号
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{msg.text && (
|
||||||
|
<div
|
||||||
|
className={`mt-4 p-3 rounded-lg text-sm text-center ${
|
||||||
|
msg.type === 'error'
|
||||||
|
? 'bg-red-500/20 text-red-400'
|
||||||
|
: 'bg-emerald-500/20 text-emerald-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{msg.text}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
367
frontend/src/app/page.tsx
Normal file
367
frontend/src/app/page.tsx
Normal file
@@ -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<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// 用户状态
|
||||||
|
const [currentUser, setCurrentUser] = useState('');
|
||||||
|
|
||||||
|
// Agent 状态
|
||||||
|
const [agents, setAgents] = useState<AiAgentConfigResponseDTO[]>([]);
|
||||||
|
const [selectedAgentId, setSelectedAgentId] = useState('');
|
||||||
|
|
||||||
|
// 会话状态
|
||||||
|
const [sessions, setSessions] = useState<Session[]>([]);
|
||||||
|
const [currentSessionId, setCurrentSessionId] = useState('');
|
||||||
|
|
||||||
|
// 聊天状态
|
||||||
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="h-screen flex bg-slate-900 text-white">
|
||||||
|
{/* 左侧栏 — 会话列表 */}
|
||||||
|
<div className="w-64 bg-slate-800 border-r border-slate-700 flex flex-col">
|
||||||
|
<div className="p-4 border-b border-slate-700">
|
||||||
|
<button
|
||||||
|
onClick={createSession}
|
||||||
|
className="w-full py-2 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg transition text-sm"
|
||||||
|
>
|
||||||
|
+ 新建对话
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-y-auto p-2 space-y-1">
|
||||||
|
{sessions.map(s => (
|
||||||
|
<div
|
||||||
|
key={s.id}
|
||||||
|
onClick={() => 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'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="text-sm truncate flex-1">{s.title}</span>
|
||||||
|
<button
|
||||||
|
onClick={e => {
|
||||||
|
e.stopPropagation();
|
||||||
|
deleteSession(s.id);
|
||||||
|
}}
|
||||||
|
className="opacity-0 group-hover:opacity-100 text-slate-400 hover:text-red-400 transition ml-2"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 主聊天区域 */}
|
||||||
|
<div className="flex-1 flex flex-col">
|
||||||
|
{/* 顶部栏 */}
|
||||||
|
<div className="h-14 bg-slate-800 border-b border-slate-700 flex items-center justify-between px-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-lg font-semibold">AI 智能体</span>
|
||||||
|
<select
|
||||||
|
value={selectedAgentId}
|
||||||
|
onChange={e => setSelectedAgentId(e.target.value)}
|
||||||
|
className="bg-slate-700 text-sm text-slate-300 px-3 py-1 rounded-lg border border-slate-600 focus:outline-none"
|
||||||
|
>
|
||||||
|
{agents.map(a => (
|
||||||
|
<option key={a.agentId} value={a.agentId}>
|
||||||
|
{a.agentName}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span className="text-sm text-slate-400">{currentUser}</span>
|
||||||
|
<button
|
||||||
|
onClick={handleLogout}
|
||||||
|
className="text-sm text-slate-400 hover:text-white transition"
|
||||||
|
>
|
||||||
|
退出
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 消息列表 */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-6 space-y-4">
|
||||||
|
{messages.length === 0 && (
|
||||||
|
<div className="text-center text-slate-500 mt-20">
|
||||||
|
<p className="text-4xl mb-4">💬</p>
|
||||||
|
<p>选择一个 Agent,开始对话</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{messages.map(msg => (
|
||||||
|
<div
|
||||||
|
key={msg.id}
|
||||||
|
className={`flex ${
|
||||||
|
msg.role === 'user' ? 'justify-end' : 'justify-start'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[70%] rounded-2xl px-4 py-3 ${
|
||||||
|
msg.role === 'user'
|
||||||
|
? 'bg-emerald-600 text-white'
|
||||||
|
: 'bg-slate-700 text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="text-sm whitespace-pre-wrap">{msg.content}</p>
|
||||||
|
<p className="text-xs mt-1 opacity-50">
|
||||||
|
{new Date(msg.timestamp).toLocaleTimeString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{isSending && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="bg-slate-700 rounded-2xl px-4 py-3">
|
||||||
|
<div className="flex items-center gap-2 text-slate-400 text-sm">
|
||||||
|
<span className="animate-spin">⏳</span> 思考中…
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 输入区域 */}
|
||||||
|
<div className="p-4 bg-slate-800 border-t border-slate-700">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
<textarea
|
||||||
|
value={inputValue}
|
||||||
|
onChange={e => setInputValue(e.target.value)}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
rows={1}
|
||||||
|
placeholder="输入消息… (Enter 发送, Shift+Enter 换行)"
|
||||||
|
className="flex-1 px-4 py-3 bg-slate-700 border border-slate-600 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:border-emerald-400 transition resize-none"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={sendMessage}
|
||||||
|
disabled={isSending || !inputValue.trim()}
|
||||||
|
className="px-6 py-3 bg-emerald-500 hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-xl transition font-medium"
|
||||||
|
>
|
||||||
|
发送
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user