fix: right-aligned user bubbles + no labels + Chinese + copy btn
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 4s
Some checks failed
Deploy Admin Frontend / build-and-deploy (push) Failing after 4s
This commit is contained in:
parent
f9c99dbd32
commit
0efc78c656
@ -1,13 +1,8 @@
|
|||||||
import { useState, useRef, useEffect, useCallback } from 'react'
|
import { useState, useRef, useEffect, useCallback } from 'react'
|
||||||
import { Input, Button, Typography, App } from 'antd'
|
import { Input, Button, Typography, App, message as antMsg } from 'antd'
|
||||||
import {
|
import { PlusOutlined, ToolOutlined, DeleteOutlined, MessageOutlined, ArrowUpOutlined, CopyOutlined } from '@ant-design/icons'
|
||||||
PlusOutlined, ToolOutlined, DeleteOutlined, MessageOutlined, ArrowUpOutlined,
|
|
||||||
} from '@ant-design/icons'
|
|
||||||
import { streamChat, resolveApproval, type StreamEvent } from '@/services/ai-chat'
|
import { streamChat, resolveApproval, type StreamEvent } from '@/services/ai-chat'
|
||||||
import {
|
import { listConversations, createConversation, deleteConversation, getMessages, updateConversation, type Conversation } from '@/services/conversation-api'
|
||||||
listConversations, createConversation, deleteConversation,
|
|
||||||
getMessages, updateConversation, type Conversation,
|
|
||||||
} from '@/services/conversation-api'
|
|
||||||
import Markdown from '@/components/Markdown'
|
import Markdown from '@/components/Markdown'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
@ -20,7 +15,7 @@ interface Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChatPage() {
|
function ChatPage() {
|
||||||
const { modal, message } = App.useApp()
|
const { modal } = App.useApp()
|
||||||
const [conversations, setConversations] = useState<Conversation[]>([])
|
const [conversations, setConversations] = useState<Conversation[]>([])
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
const [messages, setMessages] = useState<Message[]>([])
|
const [messages, setMessages] = useState<Message[]>([])
|
||||||
@ -51,10 +46,8 @@ function ChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = (id: string) => modal.confirm({
|
const handleDelete = (id: string) => modal.confirm({
|
||||||
title: 'Delete conversation?', okText: 'Delete', okType: 'danger', cancelText: 'Cancel',
|
title: '删除对话', okText: '删除', okType: 'danger', cancelText: '取消',
|
||||||
onOk: async () => {
|
onOk: async () => { try { await deleteConversation(id); setConversations(prev => prev.filter(c => c.id !== id)); if (activeId === id) { setActiveId(null); setMessages([]) } } catch {} },
|
||||||
try { await deleteConversation(id); setConversations(prev => prev.filter(c => c.id !== id)); if (activeId === id) { setActiveId(null); setMessages([]) } } catch {}
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const startEdit = (conv: Conversation) => { setEditingId(conv.id); setEditTitle(conv.title); setTimeout(() => editInputRef.current?.focus(), 50) }
|
const startEdit = (conv: Conversation) => { setEditingId(conv.id); setEditTitle(conv.title); setTimeout(() => editInputRef.current?.focus(), 50) }
|
||||||
@ -68,9 +61,11 @@ function ChatPage() {
|
|||||||
setMessages(prev => prev.map(m => m.id === approvalMsg.id ? { ...m, approval: { ...m.approval!, resolved: true } } : m))
|
setMessages(prev => prev.map(m => m.id === approvalMsg.id ? { ...m, approval: { ...m.approval!, resolved: true } } : m))
|
||||||
setWaitingApproval(false)
|
setWaitingApproval(false)
|
||||||
await resolveApproval(approvalMsg.approval!.runId, choice)
|
await resolveApproval(approvalMsg.approval!.runId, choice)
|
||||||
message.success(choice === 'deny' ? 'Denied' : 'Approved')
|
antMsg.success(choice === 'deny' ? '已拒绝' : '已批准')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyText = (text: string) => { navigator.clipboard.writeText(text); antMsg.success('已复制') }
|
||||||
|
|
||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
const text = input.trim(); if (!text || streaming) return; setInput('')
|
const text = input.trim(); if (!text || streaming) return; setInput('')
|
||||||
const userMsg: Message = { id: Date.now().toString(), role: 'user', content: text, timestamp: Date.now() }
|
const userMsg: Message = { id: Date.now().toString(), role: 'user', content: text, timestamp: Date.now() }
|
||||||
@ -87,44 +82,19 @@ function ChatPage() {
|
|||||||
try {
|
try {
|
||||||
await streamChat(prevMessages.map(m => ({ role: m.role, content: m.content })), activeId, (event: StreamEvent) => {
|
await streamChat(prevMessages.map(m => ({ role: m.role, content: m.content })), activeId, (event: StreamEvent) => {
|
||||||
switch (event.event) {
|
switch (event.event) {
|
||||||
case 'meta':
|
case 'meta': completedConvId = event.conversationId; if (event.conversationId && event.conversationId !== activeId) { setActiveId(event.conversationId); loadConversations() } break
|
||||||
completedConvId = event.conversationId
|
case 'tool.started': currentTools.push({ tool: event.tool, preview: event.preview, done: false }); update({ toolCalls: [...currentTools] }) break
|
||||||
if (event.conversationId && event.conversationId !== activeId) { setActiveId(event.conversationId); loadConversations() }
|
case 'tool.completed': { const i = currentTools.findIndex((t: any) => t.tool === event.tool && !t.done); if (i >= 0) { currentTools[i] = { ...currentTools[i], done: true, duration: event.duration, error: event.error }; update({ toolCalls: [...currentTools] }) } } break
|
||||||
break
|
case 'approval.request': setWaitingApproval(true); update({ approval: { command: event.command, description: event.description, choices: event.choices, runId: event.runId } }) break
|
||||||
case 'tool.started':
|
case 'message.delta': currentContent += event.delta || ''; update({ content: currentContent, streaming: true }) break
|
||||||
currentTools.push({ tool: event.tool, preview: event.preview, done: false })
|
case 'run.completed': if (event.output) currentContent = event.output; update({ content: currentContent, streaming: false }) break
|
||||||
update({ toolCalls: [...currentTools] })
|
case 'done': completedConvId = event.conversationId || completedConvId; update({ streaming: false }) break
|
||||||
break
|
case 'error': update({ content: 'Error: ' + event.error, streaming: false }) break
|
||||||
case 'tool.completed': {
|
|
||||||
const i = currentTools.findIndex((t: any) => t.tool === event.tool && !t.done)
|
|
||||||
if (i >= 0) { currentTools[i] = { ...currentTools[i], done: true, duration: event.duration, error: event.error }; update({ toolCalls: [...currentTools] }) }
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case 'approval.request':
|
|
||||||
setWaitingApproval(true)
|
|
||||||
update({ approval: { command: event.command, description: event.description, choices: event.choices, runId: event.runId } })
|
|
||||||
break
|
|
||||||
case 'message.delta':
|
|
||||||
currentContent += event.delta || ''
|
|
||||||
update({ content: currentContent, streaming: true })
|
|
||||||
break
|
|
||||||
case 'run.completed':
|
|
||||||
if (event.output) currentContent = event.output
|
|
||||||
update({ content: currentContent, streaming: false })
|
|
||||||
break
|
|
||||||
case 'done':
|
|
||||||
completedConvId = event.conversationId || completedConvId
|
|
||||||
update({ streaming: false })
|
|
||||||
break
|
|
||||||
case 'error':
|
|
||||||
update({ content: 'Error: ' + event.error, streaming: false })
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
}, controller.signal)
|
}, controller.signal)
|
||||||
if (completedConvId) loadConversations()
|
if (completedConvId) loadConversations()
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
if (err.name !== 'AbortError') update({ content: 'Error: ' + err.message, streaming: false })
|
if (err.name !== 'AbortError') update({ content: 'Error: ' + err.message, streaming: false }); else update({ streaming: false })
|
||||||
else update({ streaming: false })
|
|
||||||
} finally { setStreaming(false); setWaitingApproval(false); abortRef.current = null }
|
} finally { setStreaming(false); setWaitingApproval(false); abortRef.current = null }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,21 +102,15 @@ function ChatPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', height: 'calc(100vh - 112px)', background: '#fff' }}>
|
<div style={{ display: 'flex', height: 'calc(100vh - 112px)', background: '#fff' }}>
|
||||||
{/* Sidebar — DeepSeek style: no border, subtle bg */}
|
|
||||||
<div style={{ width: 260, flexShrink: 0, display: 'flex', flexDirection: 'column', background: '#f9fafb' }}>
|
<div style={{ width: 260, flexShrink: 0, display: 'flex', flexDirection: 'column', background: '#f9fafb' }}>
|
||||||
<div style={{ padding: '16px' }}>
|
<div style={{ padding: '16px' }}>
|
||||||
<Button type="primary" icon={<PlusOutlined />} block onClick={handleNew}
|
<Button type="primary" icon={<PlusOutlined />} block onClick={handleNew} style={{ borderRadius: 8, height: 40, fontWeight: 500 }}>新对话</Button>
|
||||||
style={{ borderRadius: 8, height: 40, fontWeight: 500 }}>New chat</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, overflowY: 'auto', padding: '0 8px 8px' }}>
|
<div style={{ flex: 1, overflowY: 'auto', padding: '0 8px 8px' }}>
|
||||||
{conversations.map(conv => (
|
{conversations.map(conv => (
|
||||||
<div key={conv.id}
|
<div key={conv.id} onClick={() => activeId !== conv.id && switchConversation(conv.id)}
|
||||||
onClick={() => activeId !== conv.id && switchConversation(conv.id)}
|
style={{ display: 'flex', alignItems: 'center', padding: '10px 12px', borderRadius: 8, cursor: 'pointer', marginBottom: 2,
|
||||||
style={{
|
background: activeId === conv.id ? '#e8e8ed' : 'transparent' }}
|
||||||
display: 'flex', alignItems: 'center', padding: '10px 12px', borderRadius: 8, cursor: 'pointer', marginBottom: 2,
|
|
||||||
background: activeId === conv.id ? '#e8e8ed' : 'transparent',
|
|
||||||
transition: 'background 0.15s',
|
|
||||||
}}
|
|
||||||
onMouseEnter={e => { if (activeId !== conv.id) e.currentTarget.style.background = '#ececf0' }}
|
onMouseEnter={e => { if (activeId !== conv.id) e.currentTarget.style.background = '#ececf0' }}
|
||||||
onMouseLeave={e => { if (activeId !== conv.id) e.currentTarget.style.background = 'transparent' }}>
|
onMouseLeave={e => { if (activeId !== conv.id) e.currentTarget.style.background = 'transparent' }}>
|
||||||
<MessageOutlined style={{ color: '#999', fontSize: 13, marginRight: 10, flexShrink: 0 }} />
|
<MessageOutlined style={{ color: '#999', fontSize: 13, marginRight: 10, flexShrink: 0 }} />
|
||||||
@ -155,81 +119,79 @@ function ChatPage() {
|
|||||||
onBlur={() => saveTitle(conv.id)} onPressEnter={() => saveTitle(conv.id)}
|
onBlur={() => saveTitle(conv.id)} onPressEnter={() => saveTitle(conv.id)}
|
||||||
onClick={e => e.stopPropagation()} style={{ flex: 1, fontSize: 13 }} bordered={false} />
|
onClick={e => e.stopPropagation()} style={{ flex: 1, fontSize: 13 }} bordered={false} />
|
||||||
) : (
|
) : (
|
||||||
<Text ellipsis style={{ flex: 1, fontSize: 13, color: '#333' }}
|
<Text ellipsis style={{ flex: 1, fontSize: 13, color: '#333' }} onDoubleClick={e => { e.stopPropagation(); startEdit(conv) }}>{conv.title}</Text>
|
||||||
onDoubleClick={e => { e.stopPropagation(); startEdit(conv) }}>{conv.title}</Text>
|
|
||||||
)}
|
)}
|
||||||
<DeleteOutlined onClick={e => { e.stopPropagation(); handleDelete(conv.id) }}
|
<DeleteOutlined onClick={e => { e.stopPropagation(); handleDelete(conv.id) }}
|
||||||
style={{ fontSize: 12, color: '#bbb', cursor: 'pointer', opacity: 0, transition: 'opacity 0.15s' }}
|
style={{ fontSize: 12, color: '#bbb', cursor: 'pointer', opacity: 0, transition: 'opacity 0.15s' }} className="dl-icon" />
|
||||||
className="delete-icon" />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<style>{'.delete-icon:hover { opacity: 1 !important; } div:hover > .delete-icon { opacity: 0.6 !important; }'}</style>
|
<style>{'.dl-icon:hover { opacity: 1 !important } div:hover > .dl-icon { opacity: 0.6 !important }'}</style>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat */}
|
|
||||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, background: '#fff' }}>
|
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0, background: '#fff' }}>
|
||||||
{/* Messages */}
|
|
||||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||||
{messages.length === 0 ? (
|
{messages.length === 0 ? (
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 8 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 8 }}>
|
||||||
<Text style={{ fontSize: 24, fontWeight: 600, color: '#1a1a1a' }}>What can I help with?</Text>
|
<Text style={{ fontSize: 24, fontWeight: 600, color: '#1a1a1a' }}>有什么可以帮助你的?</Text>
|
||||||
|
<Text type="secondary" style={{ fontSize: 13 }}>Hermes Agent · DeepSeek · xhigh 推理</Text>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ maxWidth: 800, margin: '0 auto', width: '100%', padding: '0 24px' }}>
|
<div style={{ maxWidth: 800, margin: '0 auto', width: '100%', padding: '0 24px' }}>
|
||||||
{messages.map(msg => (
|
{messages.map(msg => (
|
||||||
<div key={msg.id} style={{ padding: '24px 0', borderBottom: '1px solid #f0f0f0' }}>
|
<div key={msg.id} style={{ padding: '24px 0', borderBottom: '1px solid #f0f0f0' }}>
|
||||||
{/* Role label — DeepSeek style: small bold text, no avatar */}
|
{/* User: right-aligned bubble / Assistant: left-aligned plain */}
|
||||||
<div style={{ marginBottom: 12 }}>
|
{msg.role === 'user' ? (
|
||||||
<Text strong style={{ fontSize: 13, color: '#1a1a1a', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
{msg.role === 'user' ? 'You' : 'Hermes'}
|
<div style={{ maxWidth: '75%', padding: '12px 18px', borderRadius: 16, background: '#f0f0f0', fontSize: 15, lineHeight: 1.7, color: '#1a1a1a' }}>
|
||||||
</Text>
|
{msg.content}
|
||||||
{msg.streaming && !msg.content && !msg.toolCalls?.length && !msg.approval && (
|
|
||||||
<span style={{ display: 'inline-block', width: 6, height: 6, borderRadius: '50%', background: '#999', marginLeft: 8, animation: 'pulse 1.5s infinite' }} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
{/* Approval */}
|
{/* Approval */}
|
||||||
{msg.approval && !msg.approval.resolved && (
|
{msg.approval && !msg.approval.resolved && (
|
||||||
<div style={{ marginBottom: 16, padding: 16, borderRadius: 12, background: '#fff7e6', border: '1px solid #ffd591' }}>
|
<div style={{ marginBottom: 16, padding: 16, borderRadius: 12, background: '#fff7e6', border: '1px solid #ffd591' }}>
|
||||||
<Text strong style={{ fontSize: 14 }}>Approval Required</Text>
|
<Text strong style={{ fontSize: 14 }}>需要确认操作</Text>
|
||||||
<div style={{ marginTop: 8, padding: '8px 12px', borderRadius: 6, background: '#fff', fontFamily: 'SF Mono, Monaco, Menlo, monospace', fontSize: 13, border: '1px solid #eee' }}>
|
<div style={{ marginTop: 8, padding: '8px 12px', borderRadius: 6, background: '#fff', fontFamily: 'SF Mono, Monaco, Menlo, monospace', fontSize: 13, border: '1px solid #eee' }}>$ {msg.approval.command}</div>
|
||||||
$ {msg.approval.command}
|
|
||||||
</div>
|
|
||||||
<Text type="secondary" style={{ fontSize: 12, marginTop: 6, display: 'block' }}>{msg.approval.description}</Text>
|
<Text type="secondary" style={{ fontSize: 12, marginTop: 6, display: 'block' }}>{msg.approval.description}</Text>
|
||||||
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
|
<div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
|
||||||
{msg.approval.choices.map(c => (
|
{msg.approval.choices.map(c => (
|
||||||
<Button key={c} size="small" type={c === 'deny' ? 'default' : 'primary'}
|
<Button key={c} size="small" type={c === 'deny' ? 'default' : 'primary'} danger={c === 'deny'} style={{ borderRadius: 6 }} onClick={() => handleApprove(msg, c)}>
|
||||||
danger={c === 'deny'}
|
{c === 'deny' ? '拒绝' : c === 'once' ? '允许' : c === 'session' ? '本次对话允许' : c === 'always' ? '始终允许' : c}
|
||||||
style={{ borderRadius: 6 }}
|
|
||||||
onClick={() => handleApprove(msg, c)}>
|
|
||||||
{c === 'deny' ? 'Deny' : c === 'once' ? 'Approve' : c === 'session' ? 'Approve Session' : c === 'always' ? 'Approve Always' : c}
|
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Tool calls — subtle pills */}
|
{/* Tool calls */}
|
||||||
{msg.toolCalls && msg.toolCalls.length > 0 && (
|
{msg.toolCalls && msg.toolCalls.length > 0 && (
|
||||||
<div style={{ marginBottom: 12, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
<div style={{ marginBottom: 10, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||||
{msg.toolCalls.map((t, i) => (
|
{msg.toolCalls.map((t, i) => (
|
||||||
<div key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 5, padding: '4px 10px', borderRadius: 6, fontSize: 12,
|
<span key={i} style={{ display: 'inline-flex', alignItems: 'center', gap: 4, padding: '4px 10px', borderRadius: 6, fontSize: 12,
|
||||||
background: t.done ? '#f6ffed' : '#f5f5f5', border: '1px solid ' + (t.done ? '#b7eb8f' : '#e8e8e8'), color: '#666' }}>
|
background: t.done ? '#f6ffed' : '#f5f5f5', border: '1px solid ' + (t.done ? '#b7eb8f' : '#e8e8e8'), color: '#666' }}>
|
||||||
<ToolOutlined style={{ fontSize: 11 }} />
|
<ToolOutlined style={{ fontSize: 11 }} />{t.preview || t.tool}
|
||||||
{t.preview || t.tool}
|
{t.done && <span style={{ color: t.error ? '#ff4d4f' : '#52c41a', marginLeft: 4 }}>{t.error ? '失败' : '完成'}</span>}
|
||||||
{t.done && <span style={{ color: t.error ? '#ff4d4f' : '#52c41a', marginLeft: 4 }}>{t.error ? 'failed' : 'done'}</span>}
|
</span>
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Content — DeepSeek style: no bubble, just clean text */}
|
{/* Content */}
|
||||||
<div style={{ fontSize: 15, lineHeight: 1.75, color: '#1a1a1a' }}>
|
<div style={{ fontSize: 15, lineHeight: 1.75, color: '#1a1a1a' }}>
|
||||||
{msg.role === 'assistant'
|
{msg.content ? <Markdown content={msg.content + (msg.streaming ? '▊' : '')} /> : (msg.streaming ? <Text type="secondary">思考中...</Text> : null)}
|
||||||
? (msg.content ? <Markdown content={msg.content + (msg.streaming ? '▊' : '')} /> : null)
|
|
||||||
: <span>{msg.content}</span>}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Copy button after response completes */}
|
||||||
|
{msg.content && !msg.streaming && msg.role === 'assistant' && (
|
||||||
|
<div style={{ marginTop: 12 }}>
|
||||||
|
<Button type="text" size="small" icon={<CopyOutlined />} onClick={() => copyText(msg.content)}
|
||||||
|
style={{ color: '#999', fontSize: 12 }}>复制</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div ref={messagesEndRef} style={{ height: 32 }} />
|
<div ref={messagesEndRef} style={{ height: 32 }} />
|
||||||
@ -237,55 +199,35 @@ function ChatPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input — DeepSeek style: minimal, centered */}
|
|
||||||
<div style={{ padding: '0 24px 24px' }}>
|
<div style={{ padding: '0 24px 24px' }}>
|
||||||
<div style={{ maxWidth: 800, margin: '0 auto' }}>
|
<div style={{ maxWidth: 800, margin: '0 auto' }}>
|
||||||
<div style={{
|
<div style={{ display: 'flex', alignItems: 'flex-end', gap: 8, padding: '12px 12px 12px 20px', borderRadius: 16,
|
||||||
display: 'flex', alignItems: 'flex-end', gap: 8,
|
border: '1px solid #e5e5e5', background: '#fff', boxShadow: '0 2px 8px rgba(0,0,0,0.04)' }}>
|
||||||
padding: '12px 12px 12px 20px', borderRadius: 16,
|
<Input.TextArea value={input} onChange={e => setInput(e.target.value)}
|
||||||
border: '1px solid #e5e5e5', background: '#fff',
|
onCompositionStart={() => { composingRef.current = true }} onCompositionEnd={() => { composingRef.current = false }}
|
||||||
boxShadow: '0 2px 8px rgba(0,0,0,0.04)',
|
onKeyDown={e => { if (composingRef.current) return; if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() } }}
|
||||||
transition: 'border-color 0.2s, box-shadow 0.2s',
|
placeholder={activeId ? '发送消息...' : '请先选择或新建对话'}
|
||||||
}}>
|
autoSize={{ minRows: 1, maxRows: 6 }} disabled={streaming || !activeId || waitingApproval}
|
||||||
<Input.TextArea
|
variant="borderless" style={{ flex: 1, resize: 'none', padding: '4px 0', fontSize: 15, lineHeight: 1.5 }} />
|
||||||
value={input} onChange={e => setInput(e.target.value)}
|
|
||||||
onCompositionStart={() => { composingRef.current = true }}
|
|
||||||
onCompositionEnd={() => { composingRef.current = false }}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (composingRef.current) return
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend() }
|
|
||||||
}}
|
|
||||||
placeholder={activeId ? 'Ask anything' : 'Select a conversation'}
|
|
||||||
autoSize={{ minRows: 1, maxRows: 6 }}
|
|
||||||
disabled={streaming || !activeId || waitingApproval}
|
|
||||||
variant="borderless"
|
|
||||||
style={{ flex: 1, resize: 'none', padding: '4px 0', fontSize: 15, lineHeight: 1.5 }}
|
|
||||||
/>
|
|
||||||
{streaming ? (
|
{streaming ? (
|
||||||
<div onClick={handleStop} style={{ width: 36, height: 36, borderRadius: 10, background: '#ff4d4f', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer' }}>
|
<div onClick={handleStop} style={{ width: 36, height: 36, borderRadius: 10, background: '#ff4d4f', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', flexShrink: 0 }}>
|
||||||
<div style={{ width: 12, height: 12, background: '#fff', borderRadius: 2 }} />
|
<div style={{ width: 12, height: 12, background: '#fff', borderRadius: 2 }} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div onClick={handleSend}
|
<div onClick={handleSend} style={{ width: 36, height: 36, borderRadius: 10,
|
||||||
style={{
|
|
||||||
width: 36, height: 36, borderRadius: 10,
|
|
||||||
background: !input.trim() || !activeId || waitingApproval ? '#e5e5e5' : '#1a1a1a',
|
background: !input.trim() || !activeId || waitingApproval ? '#e5e5e5' : '#1a1a1a',
|
||||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||||
cursor: !input.trim() || !activeId || waitingApproval ? 'default' : 'pointer',
|
cursor: !input.trim() || !activeId || waitingApproval ? 'default' : 'pointer', flexShrink: 0 }}>
|
||||||
transition: 'background 0.2s',
|
|
||||||
}}>
|
|
||||||
<ArrowUpOutlined style={{ color: '#fff', fontSize: 16 }} />
|
<ArrowUpOutlined style={{ color: '#fff', fontSize: 16 }} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Text type="secondary" style={{ fontSize: 11, display: 'block', textAlign: 'center', marginTop: 8 }}>
|
<Text type="secondary" style={{ fontSize: 11, display: 'block', textAlign: 'center', marginTop: 8 }}>
|
||||||
Hermes Agent · DeepSeek · xhigh reasoning
|
Hermes Agent · DeepSeek · xhigh 推理
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>{'@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }'}</style>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user