feat: 添加swarms团队编排功能并优化agent委派系统
- 引入AgentTeamOrchestrator支持多agent协同任务执行 - 增加第三方swarms库依赖并配置git协议替换以改善包管理 - 扩展DelegationManager支持团队任务调度和进度跟踪 - 实现中文bigram分词算法提升中文任务检索准确性 - 调整A2AClient和DelegationManager超时时间从30秒增至600秒 - 优化AgentRunResult状态判断逻辑增加有意义摘要检测 - 修改Dockerfile配置npm仓库镜像地址和git协议映射 - 更新CLI命令行接口支持网关端口配置传递 - 调整提供者超时配置机制增强请求稳定性 - 移除过时的support_group字段简化agent描述符结构 - 增强错误处理和进度事件报告机制改进用户体验
This commit is contained in:
@ -14,7 +14,6 @@ import {
|
||||
createSession,
|
||||
deleteSession,
|
||||
getSession,
|
||||
getStatus,
|
||||
listCommands,
|
||||
listSessions,
|
||||
sendMessage,
|
||||
@ -22,29 +21,10 @@ import {
|
||||
wsManager,
|
||||
} from '@/lib/api';
|
||||
import { buildOfficeTaskList, isOfficeTaskTerminal } from '@/lib/office';
|
||||
import { pickAppText } from '@/lib/i18n/core';
|
||||
import { useAppI18n } from '@/lib/i18n/provider';
|
||||
import { useChatStore } from '@/lib/store';
|
||||
import type { ChatMessage, FileAttachment, ProcessWsEvent, SessionUpdatedEvent, SlashCommand, WsEvent } from '@/types';
|
||||
|
||||
function scheduleWhenIdle(task: () => void, timeout = 1200): () => void {
|
||||
if (typeof window === 'undefined') {
|
||||
task();
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const idleWindow = window as Window &
|
||||
typeof globalThis & {
|
||||
requestIdleCallback?: (callback: IdleRequestCallback, options?: IdleRequestOptions) => number;
|
||||
cancelIdleCallback?: (handle: number) => void;
|
||||
};
|
||||
|
||||
if (typeof idleWindow.requestIdleCallback === 'function') {
|
||||
const id = idleWindow.requestIdleCallback(() => task(), { timeout });
|
||||
return () => idleWindow.cancelIdleCallback?.(id);
|
||||
}
|
||||
|
||||
const id = globalThis.setTimeout(task, 250);
|
||||
return () => globalThis.clearTimeout(id);
|
||||
}
|
||||
import type { ChatMessage, FileAttachment, SessionUpdatedEvent, SlashCommand, WsEvent } from '@/types';
|
||||
|
||||
function messageFingerprint(msg: ChatMessage): string {
|
||||
const attachmentKey = (msg.attachments ?? [])
|
||||
@ -76,16 +56,12 @@ function mergeServerWithPendingUsers(serverMessages: ChatMessage[], localMessage
|
||||
return [...serverMessages, ...pendingUsers];
|
||||
}
|
||||
|
||||
function isProcessEvent(data: WsEvent | Record<string, unknown>): data is ProcessWsEvent {
|
||||
const type = typeof data.type === 'string' ? data.type : '';
|
||||
return type.startsWith('process_') || type === 'process_cancel_ack';
|
||||
}
|
||||
|
||||
function isSessionUpdatedEvent(data: WsEvent | Record<string, unknown>): data is SessionUpdatedEvent {
|
||||
return data.type === 'session_updated' && typeof data.session_id === 'string';
|
||||
}
|
||||
|
||||
export default function ChatPage() {
|
||||
const { locale } = useAppI18n();
|
||||
const {
|
||||
sessionId,
|
||||
messages,
|
||||
@ -100,13 +76,8 @@ export default function ChatPage() {
|
||||
setMessages,
|
||||
addMessage,
|
||||
setIsLoading,
|
||||
setSessions,
|
||||
clearMessages,
|
||||
setWsStatus,
|
||||
setIsThinking,
|
||||
setNanobotReady,
|
||||
resetProcessState,
|
||||
ingestProcessEvent,
|
||||
setSelectedRunId,
|
||||
} = useChatStore();
|
||||
|
||||
@ -124,9 +95,8 @@ export default function ChatPage() {
|
||||
const commandsLoadedRef = useRef(false);
|
||||
const refreshSessionOnReconnectRef = useRef(false);
|
||||
const hasConnectedRef = useRef(false);
|
||||
const statusCheckCleanupRef = useRef<(() => void) | null>(null);
|
||||
const statusCheckInFlightRef = useRef(false);
|
||||
const shouldSnapToLatestRef = useRef(true);
|
||||
const wsStatus = useChatStore((state) => state.wsStatus);
|
||||
|
||||
const filteredCommands = useMemo(() => {
|
||||
if (!input.startsWith('/') || input.includes(' ')) return [];
|
||||
@ -136,6 +106,28 @@ export default function ChatPage() {
|
||||
);
|
||||
}, [commands, input]);
|
||||
|
||||
const sessionProcessRuns = useMemo(
|
||||
() => processRuns.filter((run) => run.session_id === sessionId),
|
||||
[processRuns, sessionId]
|
||||
);
|
||||
|
||||
const sessionRunIds = useMemo(
|
||||
() => new Set(sessionProcessRuns.map((run) => run.run_id)),
|
||||
[sessionProcessRuns]
|
||||
);
|
||||
|
||||
const sessionProcessEvents = useMemo(
|
||||
() => processEvents.filter((event) => sessionRunIds.has(event.run_id)),
|
||||
[processEvents, sessionRunIds]
|
||||
);
|
||||
|
||||
const sessionProcessArtifacts = useMemo(
|
||||
() => processArtifacts.filter((artifact) => sessionRunIds.has(artifact.run_id)),
|
||||
[processArtifacts, sessionRunIds]
|
||||
);
|
||||
|
||||
const selectedSessionRunId = selectedRunId && sessionRunIds.has(selectedRunId) ? selectedRunId : null;
|
||||
|
||||
const officeTasks = useMemo(
|
||||
() => buildOfficeTaskList({
|
||||
sessionId,
|
||||
@ -143,8 +135,8 @@ export default function ChatPage() {
|
||||
processRuns,
|
||||
processEvents,
|
||||
processArtifacts,
|
||||
}),
|
||||
[processArtifacts, processEvents, processRuns, sessionId, sessions]
|
||||
}, locale),
|
||||
[locale, processArtifacts, processEvents, processRuns, sessionId, sessions]
|
||||
);
|
||||
|
||||
const currentOfficeTask = officeTasks.find((task) => !isOfficeTaskTerminal(task.status)) ?? officeTasks[0] ?? null;
|
||||
@ -152,11 +144,11 @@ export default function ChatPage() {
|
||||
const loadSessions = useCallback(async () => {
|
||||
try {
|
||||
const list = await listSessions();
|
||||
setSessions(list);
|
||||
useChatStore.getState().setSessions(list);
|
||||
} catch {
|
||||
// backend may be offline during first render
|
||||
}
|
||||
}, [setSessions]);
|
||||
}, []);
|
||||
|
||||
const loadSessionMessages = useCallback(async (key: string) => {
|
||||
const reqSeq = ++loadSessionReqSeq.current;
|
||||
@ -170,6 +162,7 @@ export default function ChatPage() {
|
||||
? mergeServerWithPendingUsers(detail.messages, localSnapshot)
|
||||
: detail.messages;
|
||||
setMessages(nextMessages);
|
||||
shouldSnapToLatestRef.current = true;
|
||||
const last = nextMessages[nextMessages.length - 1];
|
||||
if (last?.role === 'assistant') {
|
||||
setIsThinking(false);
|
||||
@ -192,23 +185,6 @@ export default function ChatPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const scheduleStatusCheck = useCallback(() => {
|
||||
if (statusCheckInFlightRef.current) return;
|
||||
|
||||
statusCheckCleanupRef.current?.();
|
||||
statusCheckCleanupRef.current = scheduleWhenIdle(async () => {
|
||||
statusCheckInFlightRef.current = true;
|
||||
try {
|
||||
await getStatus();
|
||||
setNanobotReady(true);
|
||||
} catch {
|
||||
setNanobotReady(false);
|
||||
} finally {
|
||||
statusCheckInFlightRef.current = false;
|
||||
}
|
||||
});
|
||||
}, [setNanobotReady]);
|
||||
|
||||
useEffect(() => {
|
||||
if (input.startsWith('/') && !input.includes(' ')) {
|
||||
void loadCommands();
|
||||
@ -220,40 +196,29 @@ export default function ChatPage() {
|
||||
setPickerIndex(0);
|
||||
}, [filteredCommands]);
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, [loadSessions]);
|
||||
|
||||
useEffect(() => {
|
||||
clearMessages();
|
||||
setIsLoading(false);
|
||||
setIsThinking(false);
|
||||
resetProcessState();
|
||||
const wsSessionId = sessionId.startsWith('web:') ? sessionId.slice(4) : sessionId;
|
||||
wsManager.connect(wsSessionId);
|
||||
loadSessionMessages(sessionId);
|
||||
}, [clearMessages, loadSessionMessages, resetProcessState, sessionId, setIsLoading, setIsThinking]);
|
||||
void loadSessionMessages(sessionId);
|
||||
}, [clearMessages, loadSessionMessages, sessionId, setIsLoading, setIsThinking]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubStatus = wsManager.onStatusChange(async (status) => {
|
||||
setWsStatus(status);
|
||||
if (status === 'connected') {
|
||||
if (hasConnectedRef.current && refreshSessionOnReconnectRef.current) {
|
||||
refreshSessionOnReconnectRef.current = false;
|
||||
void loadSessionMessages(useChatStore.getState().sessionId);
|
||||
}
|
||||
hasConnectedRef.current = true;
|
||||
scheduleStatusCheck();
|
||||
} else {
|
||||
if (status === 'disconnected' && hasConnectedRef.current) {
|
||||
refreshSessionOnReconnectRef.current = true;
|
||||
}
|
||||
statusCheckCleanupRef.current?.();
|
||||
statusCheckCleanupRef.current = null;
|
||||
setNanobotReady(null);
|
||||
if (wsStatus === 'connected') {
|
||||
if (hasConnectedRef.current && refreshSessionOnReconnectRef.current) {
|
||||
refreshSessionOnReconnectRef.current = false;
|
||||
void loadSessionMessages(useChatStore.getState().sessionId);
|
||||
}
|
||||
});
|
||||
hasConnectedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (wsStatus === 'disconnected' && hasConnectedRef.current) {
|
||||
refreshSessionOnReconnectRef.current = true;
|
||||
}
|
||||
}, [loadSessionMessages, wsStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubMessage = wsManager.onMessage((data) => {
|
||||
if (isSessionUpdatedEvent(data)) {
|
||||
void loadSessions();
|
||||
@ -263,11 +228,6 @@ export default function ChatPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isProcessEvent(data)) {
|
||||
ingestProcessEvent(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'status' && data.status === 'thinking') {
|
||||
setIsThinking(true);
|
||||
} else if (data.type === 'message' && data.role === 'assistant') {
|
||||
@ -284,12 +244,9 @@ export default function ChatPage() {
|
||||
});
|
||||
|
||||
return () => {
|
||||
statusCheckCleanupRef.current?.();
|
||||
statusCheckCleanupRef.current = null;
|
||||
unsubStatus();
|
||||
unsubMessage();
|
||||
};
|
||||
}, [addMessage, ingestProcessEvent, loadSessionMessages, loadSessions, scheduleStatusCheck, setIsLoading, setIsThinking, setNanobotReady, setWsStatus]);
|
||||
}, [addMessage, loadSessionMessages, loadSessions, setIsLoading, setIsThinking]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isThinking) {
|
||||
@ -304,21 +261,34 @@ export default function ChatPage() {
|
||||
const scrollMessagesToLatest = useCallback((behavior: ScrollBehavior) => {
|
||||
const viewport = messageViewportRef.current;
|
||||
if (!viewport) return;
|
||||
messagesEndRef.current?.scrollIntoView({ block: 'end', behavior });
|
||||
viewport.scrollTo({ top: viewport.scrollHeight, behavior });
|
||||
}, []);
|
||||
|
||||
const scheduleScrollToLatest = useCallback((behavior: ScrollBehavior) => {
|
||||
if (typeof window === 'undefined') {
|
||||
scrollMessagesToLatest(behavior);
|
||||
return;
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(() => {
|
||||
scrollMessagesToLatest(behavior);
|
||||
});
|
||||
});
|
||||
}, [scrollMessagesToLatest]);
|
||||
|
||||
useEffect(() => {
|
||||
shouldSnapToLatestRef.current = true;
|
||||
}, [sessionId]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (messages.length === 0 && !isThinking && processEvents.length === 0) {
|
||||
if (messages.length === 0 && !isThinking && sessionProcessEvents.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollMessagesToLatest(shouldSnapToLatestRef.current ? 'auto' : 'smooth');
|
||||
scheduleScrollToLatest(shouldSnapToLatestRef.current ? 'auto' : 'smooth');
|
||||
shouldSnapToLatestRef.current = false;
|
||||
}, [isThinking, messages, processEvents, scrollMessagesToLatest]);
|
||||
}, [isThinking, messages.length, scheduleScrollToLatest, sessionProcessEvents.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showCommandPicker || !pickerRef.current) return;
|
||||
@ -348,7 +318,7 @@ export default function ChatPage() {
|
||||
setPendingFiles([]);
|
||||
setShowCommandPicker(false);
|
||||
|
||||
const msgContent = text || '(仅附件)';
|
||||
const msgContent = text || pickAppText(locale, '(仅附件)', '(Attachments only)');
|
||||
addMessage({
|
||||
role: 'user',
|
||||
content: msgContent,
|
||||
@ -392,12 +362,12 @@ export default function ChatPage() {
|
||||
}
|
||||
addMessage({
|
||||
role: 'assistant',
|
||||
content: '发送失败,请检查后端服务是否正在运行。',
|
||||
content: pickAppText(locale, '发送失败,请检查后端服务是否正在运行。', 'Send failed. Please check whether the backend service is running.'),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [addMessage, input, isLoading, loadSessionMessages, loadSessions, pendingFiles, sessionId, setIsLoading, setIsThinking]);
|
||||
}, [addMessage, input, isLoading, loadSessionMessages, loadSessions, locale, pendingFiles, sessionId, setIsLoading, setIsThinking]);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (showCommandPicker && filteredCommands.length > 0) {
|
||||
@ -436,7 +406,7 @@ export default function ChatPage() {
|
||||
|
||||
for (const file of files) {
|
||||
if (file.size > 50 * 1024 * 1024) {
|
||||
setPendingFiles((prev) => [...prev, { file, progress: 0, error: '文件过大(最大 50MB)' }]);
|
||||
setPendingFiles((prev) => [...prev, { file, progress: 0, error: pickAppText(locale, '文件过大(最大 50MB)', 'File is too large (max 50MB)') }]);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -447,16 +417,17 @@ export default function ChatPage() {
|
||||
});
|
||||
setPendingFiles((prev) => prev.map((item) => (item.file === file ? { ...item, id: result.file_id, progress: 100 } : item)));
|
||||
} catch (err: any) {
|
||||
setPendingFiles((prev) => prev.map((item) => (item.file === file ? { ...item, error: err.message || '上传失败' } : item)));
|
||||
setPendingFiles((prev) => prev.map((item) => (item.file === file ? { ...item, error: err.message || pickAppText(locale, '上传失败', 'Upload failed') } : item)));
|
||||
}
|
||||
}
|
||||
}, [sessionId]);
|
||||
}, [locale, sessionId]);
|
||||
|
||||
const handleNewSession = async () => {
|
||||
const id = `web:${Date.now()}`;
|
||||
setSessionId(id);
|
||||
setSelectedRunId(null);
|
||||
clearMessages();
|
||||
resetProcessState();
|
||||
useChatStore.getState().resetProcessState();
|
||||
try {
|
||||
await createSession(id);
|
||||
} catch {
|
||||
@ -472,7 +443,7 @@ export default function ChatPage() {
|
||||
if (key === sessionId) {
|
||||
setSessionId('web:default');
|
||||
clearMessages();
|
||||
resetProcessState();
|
||||
useChatStore.getState().resetProcessState();
|
||||
}
|
||||
loadSessions();
|
||||
} catch {
|
||||
@ -481,20 +452,21 @@ export default function ChatPage() {
|
||||
};
|
||||
|
||||
const handleSelectSession = (key: string) => {
|
||||
setSelectedRunId(null);
|
||||
setSessionId(key);
|
||||
};
|
||||
|
||||
const handleCancelRun = async (runId: string) => {
|
||||
const handleCancelRun = useCallback(async (runId: string) => {
|
||||
try {
|
||||
await cancelDelegation(runId);
|
||||
} catch (err: any) {
|
||||
addMessage({
|
||||
role: 'assistant',
|
||||
content: `取消任务 ${runId} 失败:${err.message || '未知错误'}`,
|
||||
content: pickAppText(locale, `取消任务 ${runId} 失败:${err.message || '未知错误'}`, `Failed to cancel task ${runId}: ${err.message || 'Unknown error'}`),
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
}, [addMessage, locale]);
|
||||
|
||||
const removePendingFile = useCallback((file: File) => {
|
||||
setPendingFiles((prev) => prev.filter((item) => item.file !== file));
|
||||
@ -503,10 +475,10 @@ export default function ChatPage() {
|
||||
const formatSessionName = (key: string) => {
|
||||
if (key.startsWith('web:')) {
|
||||
const id = key.slice(4);
|
||||
if (id === 'default') return '默认';
|
||||
if (id === 'default') return pickAppText(locale, '默认', 'Default');
|
||||
const numeric = Number(id);
|
||||
if (!Number.isNaN(numeric)) {
|
||||
return new Date(numeric).toLocaleDateString('zh-CN', {
|
||||
return new Date(numeric).toLocaleDateString(locale, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
@ -524,14 +496,14 @@ export default function ChatPage() {
|
||||
<div className="p-3">
|
||||
<Button onClick={handleNewSession} variant="outline" className="w-full justify-start gap-2" size="sm">
|
||||
<Plus className="w-4 h-4" />
|
||||
新对话
|
||||
{pickAppText(locale, '新对话', 'New chat')}
|
||||
</Button>
|
||||
</div>
|
||||
<Separator />
|
||||
<ScrollArea className="flex-1">
|
||||
<div className="p-2 space-y-1">
|
||||
{sessions.length === 0 && (
|
||||
<p className="text-xs text-muted-foreground px-2 py-4 text-center">暂无对话记录</p>
|
||||
<p className="text-xs text-muted-foreground px-2 py-4 text-center">{pickAppText(locale, '暂无对话记录', 'No chat history yet')}</p>
|
||||
)}
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
@ -567,22 +539,22 @@ export default function ChatPage() {
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<Building2 className="h-4 w-4" />
|
||||
当前任务现场
|
||||
{pickAppText(locale, '当前任务现场', 'Current task floor')}
|
||||
</div>
|
||||
<OfficeStatusBadge status={currentOfficeTask.status} />
|
||||
</div>
|
||||
<div className="mt-1 truncate text-sm text-muted-foreground">
|
||||
{currentOfficeTask.title}
|
||||
<span className="ml-2">主 Agent: {currentOfficeTask.rootActorName}</span>
|
||||
<span className="ml-2">{pickAppText(locale, '主 Agent', 'Lead agent')}: {currentOfficeTask.rootActorName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/office">查看全部 Office</Link>
|
||||
<Link href="/office">{pickAppText(locale, '查看全部 Office', 'View all office tasks')}</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm">
|
||||
<Link href={`/office/${encodeURIComponent(currentOfficeTask.taskId)}`}>
|
||||
查看任务现场
|
||||
{pickAppText(locale, '查看任务现场', 'Open task floor')}
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
@ -597,11 +569,11 @@ export default function ChatPage() {
|
||||
isThinking={isThinking || (isLoading && messages[messages.length - 1]?.role === 'user')}
|
||||
messagesEndRef={messagesEndRef}
|
||||
messageViewportRef={messageViewportRef}
|
||||
processRuns={processRuns}
|
||||
processEvents={processEvents}
|
||||
processArtifacts={processArtifacts}
|
||||
selectedRunId={selectedRunId}
|
||||
onSelectRun={(runId) => setSelectedRunId(selectedRunId === runId ? null : runId)}
|
||||
processRuns={sessionProcessRuns}
|
||||
processEvents={sessionProcessEvents}
|
||||
processArtifacts={sessionProcessArtifacts}
|
||||
selectedRunId={selectedSessionRunId}
|
||||
onSelectRun={(runId) => setSelectedRunId(selectedSessionRunId === runId ? null : runId)}
|
||||
onCancelRun={handleCancelRun}
|
||||
/>
|
||||
</div>
|
||||
@ -623,7 +595,7 @@ export default function ChatPage() {
|
||||
<div className="h-full bg-primary rounded-full transition-all" style={{ width: `${item.progress}%` }} />
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-green-500 text-xs">就绪</span>
|
||||
<span className="text-green-500 text-xs">{pickAppText(locale, '就绪', 'Ready')}</span>
|
||||
)}
|
||||
<button onClick={() => removePendingFile(item.file)} className="text-muted-foreground hover:text-foreground">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
@ -660,7 +632,7 @@ export default function ChatPage() {
|
||||
<span className="text-muted-foreground text-xs truncate ml-auto">{command.description}</span>
|
||||
{command.plugin_name !== 'builtin' && (
|
||||
<span className={`text-xs px-1 rounded shrink-0 ${command.plugin_name === 'skill' ? 'bg-blue-500/10 text-blue-500' : 'bg-muted'}`}>
|
||||
{command.plugin_name === 'skill' ? '技能' : command.plugin_name}
|
||||
{command.plugin_name === 'skill' ? pickAppText(locale, '技能', 'Skill') : command.plugin_name}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@ -675,7 +647,7 @@ export default function ChatPage() {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-10 w-10 flex-shrink-0"
|
||||
title="添加附件"
|
||||
title={pickAppText(locale, '添加附件', 'Add attachment')}
|
||||
>
|
||||
<Paperclip className="w-4 h-4" />
|
||||
</Button>
|
||||
@ -685,7 +657,7 @@ export default function ChatPage() {
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder="输入消息或 / 呼出命令…(回车发送,Shift+回车换行)"
|
||||
placeholder={pickAppText(locale, '输入消息或 / 呼出命令…(回车发送,Shift+回车换行)', 'Type a message or use / for commands... (Enter to send, Shift+Enter for a new line)')}
|
||||
rows={1}
|
||||
className="flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
style={{ minHeight: '40px', maxHeight: '200px' }}
|
||||
|
||||
Reference in New Issue
Block a user