Polish personal Agent conversation workspace
This commit is contained in:
1 parent
1aef423df0
commit
a340c0fd94
7 files changed
+599
-53
No files matched your search
@@ -1,11 +1,13 @@
|
||||
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react';
|
||||
import { History, MessageSquarePlus, Send, Square } from 'lucide-react';
|
||||
import { ArrowUpRight, History, MessageCircle, MessageSquarePlus, Send, Sparkles, Square } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { cloudAgentsApi } from '@/lib/cloud-agents-api';
|
||||
import type { CloudHistory, CloudInterrupt, CloudPrompt, CloudRequest, CloudRun, CloudThread, CloudScheduleProposal } from '../../../shared/cloud-agents';
|
||||
import { CloudFiles } from './CloudFiles';
|
||||
import { CloudAvatar } from './CloudAvatar';
|
||||
import './desktop-workspace.css';
|
||||
import './agent-chat.css';
|
||||
import ReactMarkdown, { defaultUrlTransform } from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import { CloudPendingContext, useCloudPendingState, usePendingCloudInput } from './CloudPending';
|
||||
@@ -21,8 +23,8 @@ const message = (e: unknown) => e instanceof Error ? e.message : '暂时无法
|
||||
const object = (v: unknown): Record<string, unknown> => v && typeof v === 'object' ? v as Record<string, unknown> : {};
|
||||
|
||||
export function CloudChat({ slug, previewRevision: latestRevision, initialThread, initialThreadId, onSchedule, activeView = true, headerAction, previewReady = true,
|
||||
examples = agentExamples(), hasUnsavedChanges = false, saveDisabled = false, onSaveForPreview }: {
|
||||
slug: string; headerAction?: ReactNode; previewReady?: boolean; activeView?: boolean; previewRevision?: number; initialThread?: CloudThread; initialThreadId?: string; onSchedule?: (proposal: CloudScheduleProposal) => void;
|
||||
assistantName = '助手', assistantAvatarUrl, examples = agentExamples(), hasUnsavedChanges = false, saveDisabled = false, onSaveForPreview }: {
|
||||
slug: string; assistantName?: string; assistantAvatarUrl?: string | null; headerAction?: ReactNode; previewReady?: boolean; activeView?: boolean; previewRevision?: number; initialThread?: CloudThread; initialThreadId?: string; onSchedule?: (proposal: CloudScheduleProposal) => void;
|
||||
examples?: readonly string[]; hasUnsavedChanges?: boolean; saveDisabled?: boolean; onSaveForPreview?: () => Promise<number | undefined>;
|
||||
}) {
|
||||
const [previewRevision, setPreviewRevision] = useState(latestRevision === undefined ? undefined : initialThread?.draft_revision ?? latestRevision);
|
||||
@@ -256,16 +258,16 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread
|
||||
else if (runId) await cloudAgentsApi.call('cancelRun', { run_id: runId });
|
||||
} catch (e) { setError(message(e)); } finally { setBusy(false); }
|
||||
};
|
||||
return <CloudPendingContext.Provider value={childrenPending.report}><section className="agent-chat bg-background" aria-label={previewRevision === undefined ? '智能体对话' : '草稿预览'}>
|
||||
<div className="agent-chat-toolbar"><div className="flex min-h-12 shrink-0 items-center justify-between gap-3 border-b border-border py-2">
|
||||
<div className="min-w-0 text-sm">{previewRevision === undefined ? '和助手聊一聊' : '试一试'}
|
||||
return <CloudPendingContext.Provider value={childrenPending.report}><section className="agent-chat agent-chat-styled" aria-label={previewRevision === undefined ? '智能体对话' : '草稿预览'}>
|
||||
<div className="agent-chat-toolbar"><div className="agent-chat-toolbar-row">
|
||||
<div className="agent-chat-toolbar-heading"><span className="agent-chat-toolbar-icon" aria-hidden="true"><MessageCircle /></span><div className="min-w-0"><p className="agent-chat-toolbar-title">{previewRevision === undefined ? '和助手聊一聊' : '试一试'}</p>
|
||||
{previewRevision !== undefined && <p className="mt-1 text-xs text-muted-foreground">当前试用 · 修订 {previewRevision}</p>}
|
||||
<p className="mt-1 text-xs text-muted-foreground">由创建者支付词元点数{run?.version && ` · 版本 ${run.version}`}</p></div>
|
||||
<div className="flex shrink-0"><Button variant="ghost" size="icon" aria-label="查看历史对话" aria-expanded={showHistory} onClick={() => setShowHistory(v => !v)}><History className="h-4 w-4" /></Button>
|
||||
<Button variant="ghost" disabled={busy || Boolean(intent) || loading} onClick={() => (query.trim() || attachmentIds.length > 0 || childrenPending.hasPending) ? setLeaving(true) : void choose(null)} aria-label="新对话"><MessageSquarePlus className="h-4 w-4" /></Button>{headerAction}</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground">由创建者支付词元点数{run?.version && ` · 版本 ${run.version}`}</p></div></div>
|
||||
<div className="agent-chat-toolbar-actions"><Button variant="ghost" aria-label="查看历史对话" title="查看历史对话" aria-expanded={showHistory} onClick={() => setShowHistory(v => !v)}><History aria-hidden="true" className="h-4 w-4" /><span>历史对话</span></Button>
|
||||
<Button variant="ghost" disabled={busy || Boolean(intent) || loading} onClick={() => (query.trim() || attachmentIds.length > 0 || childrenPending.hasPending) ? setLeaving(true) : void choose(null)} aria-label="新对话"><MessageSquarePlus aria-hidden="true" className="h-4 w-4" /><span>新对话</span></Button>{headerAction}</div>
|
||||
</div>
|
||||
{showHistory && threads.length === 0 && <p className="py-2 text-xs text-muted-foreground">还没有历史对话</p>}
|
||||
{showHistory && threads.length > 0 && <select aria-label="历史对话" className="mt-3 h-10 rounded-md border border-input bg-background px-3 text-sm"
|
||||
{showHistory && threads.length > 0 && <select aria-label="历史对话" className="agent-chat-history-select"
|
||||
value={thread?.thread_id ?? ''} disabled={busy || Boolean(intent) || Boolean(query.trim()) || attachmentIds.length > 0 || childrenPending.hasPending} onChange={e => void choose(threads.find(t => t.thread_id === e.target.value) ?? null)}>
|
||||
<option value="">新对话</option>{threads.map(t => <option key={t.thread_id} value={t.thread_id}>{t.mode === 'preview' ? `修订 ${t.draft_revision ?? previewRevision} · ` : ''}{t.title || '未命名对话'} · {cloudStatus(t.status)}</option>)}
|
||||
</select>}
|
||||
@@ -278,23 +280,28 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread
|
||||
</div>}
|
||||
</div>
|
||||
<div ref={transcript} data-testid="cloud-chat-messages" className="agent-chat-scroll"
|
||||
onScroll={e => { const el = e.currentTarget; following.current = el.scrollHeight - el.scrollTop - el.clientHeight < 64; }}><div className="space-y-5 py-5 pr-2">
|
||||
{loading ? <p role="status" className="text-sm text-muted-foreground">正在读取对话…</p>
|
||||
: !history?.messages.length && !liveText && <div className="space-y-4 py-6 text-sm leading-7">
|
||||
<h2 className="font-medium">{mode === 'preview' ? '问它一个小问题' : '今天想让它帮你做什么?'}</h2>
|
||||
<p className="text-muted-foreground">{mode === 'preview' ? '看看它的回答是否符合你的要求。不满意就改一改,再试一次。' : '从下面选一句,或写下你自己的想法。'}</p>
|
||||
<div className="grid gap-2">{examples.map(example => <Button key={example} type="button" variant="outline"
|
||||
className="h-auto min-h-10 justify-start whitespace-normal py-2 text-left font-normal leading-6"
|
||||
disabled={busy || Boolean(intent) || Boolean(query.trim()) || Boolean(thread?.archived)} onClick={() => setQuery(example)}>{example}</Button>)}</div>
|
||||
<p className="text-xs text-muted-foreground">点选只会填入输入框,由你决定何时发送。</p>
|
||||
onScroll={e => { const el = e.currentTarget; following.current = el.scrollHeight - el.scrollTop - el.clientHeight < 64; }}><div className="agent-chat-transcript">
|
||||
{loading ? <div role="status" className="agent-chat-loading"><MessageCircle aria-hidden="true" /><p>正在读取对话…</p></div>
|
||||
: !history?.messages.length && !liveText && <div className="agent-chat-welcome">
|
||||
<div className="agent-chat-welcome-portrait"><CloudAvatar src={assistantAvatarUrl} name={assistantName} className="agent-chat-welcome-avatar" /><Sparkles aria-hidden="true" className="agent-chat-welcome-spark" /></div>
|
||||
<p className="agent-chat-welcome-name">{assistantName}</p>
|
||||
<h2>{mode === 'preview' ? '问它一个小问题' : 'Hi,今天想聊些什么?'}</h2>
|
||||
<p className="agent-chat-welcome-description">{mode === 'preview' ? '看看它的回答是否符合你的要求。不满意就改一改,再试一次。' : '从下面选一句,或写下你自己的想法。'}</p>
|
||||
<div className="agent-chat-examples">{examples.map((example, index) => <Button key={example} type="button" variant="outline"
|
||||
className="agent-chat-example" data-tone={index % 3}
|
||||
disabled={busy || Boolean(intent) || Boolean(query.trim()) || Boolean(thread?.archived)} onClick={() => setQuery(example)}><span>{example}</span><ArrowUpRight aria-hidden="true" /></Button>)}</div>
|
||||
<p className="agent-chat-example-note">点选只会填入输入框,由你决定何时发送。</p>
|
||||
</div>}
|
||||
{previousTrial && <details className="rounded-lg border border-border p-3 text-sm">
|
||||
<summary className="min-h-10 cursor-pointer content-center font-medium">对比上一次试用 · 修订 {previousTrial.revision}</summary>
|
||||
<p className="my-2 text-xs text-muted-foreground">这是修改前的回答。用同一个问题再问一次,更容易看出变化。</p>
|
||||
<TrialMessages history={previousTrial.history} onError={setError} />
|
||||
<TrialMessages history={previousTrial.history} onError={setError} assistantName={assistantName} assistantAvatarUrl={assistantAvatarUrl} />
|
||||
</details>}
|
||||
{history && <TrialMessages history={history} onError={setError} />}
|
||||
{liveText && <div className="whitespace-pre-wrap break-words p-2 text-sm leading-7">{liveText}</div>}
|
||||
{history && <TrialMessages history={history} onError={setError} assistantName={assistantName} assistantAvatarUrl={assistantAvatarUrl} />}
|
||||
{liveText && <article className="agent-chat-message" data-role="assistant" aria-label="助手正在回复">
|
||||
<CloudAvatar src={assistantAvatarUrl} name={assistantName} className="agent-chat-message-avatar" />
|
||||
<div className="agent-chat-message-content"><p className="agent-chat-speaker">{assistantName}</p><div className="agent-chat-message-body whitespace-pre-wrap">{liveText}</div></div>
|
||||
</article>}
|
||||
{onSchedule && history?.schedule_proposals?.map(proposal => <article key={proposal.id} className="space-y-3 rounded-xl border border-violet-200 bg-violet-50/30 p-4 text-sm">
|
||||
<h3 className="font-medium">自动任务建议 · {proposal.name}</h3><p className="whitespace-pre-wrap">{proposal.prompt}</p>
|
||||
<p className="text-xs text-muted-foreground">{proposal.cron_expression} · {proposal.timezone} · 尚未启用</p>
|
||||
@@ -336,27 +343,30 @@ export function CloudChat({ slug, previewRevision: latestRevision, initialThread
|
||||
}} /></div>
|
||||
{leaving && <div className="mb-3 flex flex-wrap items-center gap-2 text-sm"><span>放弃未发送的内容并新建对话?</span>
|
||||
<Button variant="outline" onClick={() => { setLeaving(false); void choose(null); }}>放弃内容</Button><Button variant="ghost" onClick={() => setLeaving(false)}>继续编辑</Button></div>}
|
||||
<form data-testid="cloud-chat-composer" className="agent-chat-composer rounded-xl border border-input p-3 shadow-sm" onSubmit={e => { e.preventDefault(); void send(); }}>
|
||||
<Textarea aria-label="消息" className="h-20 min-h-20 max-h-40 resize-y border-0 p-1 shadow-none focus-visible:ring-0" maxLength={32000}
|
||||
value={query} disabled={busy || Boolean(intent) || loading || thread?.archived} placeholder="输入消息或任务目标…" onChange={e => setQuery(e.target.value)}
|
||||
<form data-testid="cloud-chat-composer" className="agent-chat-composer" onSubmit={e => { e.preventDefault(); void send(); }}>
|
||||
<Textarea aria-label="消息" className="agent-chat-input resize-y border-0 shadow-none focus-visible:ring-0" maxLength={32000}
|
||||
value={query} disabled={busy || Boolean(intent) || loading || thread?.archived} placeholder="说说你的想法,或问一个问题…" onChange={e => setQuery(e.target.value)}
|
||||
onKeyDown={e => { if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing && e.keyCode !== 229) { e.preventDefault(); void send(); } }} />
|
||||
<div className="mt-2 flex items-center justify-between gap-3">
|
||||
<p role="status" className="text-xs text-muted-foreground">{progress || (running ? cloudStatus(run?.status ?? request?.status ?? '') : run ? cloudStatus(run.status) : '就绪')}</p>
|
||||
{running ? <Button size="sm" variant="outline" disabled={busy} onClick={() => void cancel()} type="button"><Square className="mr-2 h-3 w-3" />停止</Button>
|
||||
: <Button size="sm" type="submit" disabled={busy || loading || thread?.archived || (!intent && (hasUnsavedChanges || stalePreview || !previewReady || !query.trim()))}><Send className="mr-2 h-3 w-3" />{busy ? '正在提交…' : intent ? '重试本次发送' : '发送'}</Button>}
|
||||
<div className="agent-chat-composer-footer">
|
||||
<div className="agent-chat-composer-meta"><p role="status" className="agent-chat-status" data-active={running}>{progress || (running ? cloudStatus(run?.status ?? request?.status ?? '') : run ? cloudStatus(run.status) : '就绪')}</p><span className="agent-chat-keyboard-hint">Enter 发送 · Shift + Enter 换行</span></div>
|
||||
{running ? <Button className="agent-chat-stop" size="sm" variant="outline" disabled={busy} onClick={() => void cancel()} type="button"><Square className="mr-2 h-3 w-3" />停止</Button>
|
||||
: <Button className="agent-chat-send" size="sm" type="submit" disabled={busy || loading || thread?.archived || (!intent && (hasUnsavedChanges || stalePreview || !previewReady || !query.trim()))}><Send className="mr-2 h-3 w-3" />{busy ? '正在提交…' : intent ? '重试本次发送' : '发送'}</Button>}
|
||||
</div>
|
||||
</form>
|
||||
</section></CloudPendingContext.Provider>;
|
||||
}
|
||||
|
||||
function TrialMessages({ history, onError }: { history: CloudHistory; onError: (error: string) => void }) {
|
||||
return history.messages.map(item => <article key={item.id} className={item.role === 'user' ? 'ml-8 rounded-xl bg-muted/60 p-4' : 'p-2'}>
|
||||
<p className="mb-2 text-xs text-muted-foreground">{item.role === 'user' ? '你' : '助手'}</p>
|
||||
<div className="prose prose-sm max-w-none break-words"><ReactMarkdown urlTransform={url => url.startsWith('sandbox:/') ? url.slice(8) : defaultUrlTransform(url)} remarkPlugins={[remarkGfm]} components={{
|
||||
function TrialMessages({ history, onError, assistantName = '助手', assistantAvatarUrl }: {
|
||||
history: CloudHistory; onError: (error: string) => void; assistantName?: string; assistantAvatarUrl?: string | null;
|
||||
}) {
|
||||
return history.messages.map(item => <article key={item.id} className="agent-chat-message" data-role={item.role === 'user' ? 'user' : 'assistant'}>
|
||||
{item.role !== 'user' && <CloudAvatar src={assistantAvatarUrl} name={assistantName} className="agent-chat-message-avatar" />}
|
||||
<div className="agent-chat-message-content"><p className="agent-chat-speaker">{item.role === 'user' ? '你' : assistantName}</p>
|
||||
<div className="agent-chat-message-body prose prose-sm max-w-none break-words"><ReactMarkdown urlTransform={url => url.startsWith('sandbox:/') ? url.slice(8) : defaultUrlTransform(url)} remarkPlugins={[remarkGfm]} components={{
|
||||
a: ({ href, children }) => href && !/^(https?:|mailto:|#)/i.test(href)
|
||||
? <button className="text-primary underline" onClick={() => void cloudAgentsApi.download(history.thread_id, href.replace(/^\/api\/chat\/thread\/[^/]+\/artifacts\//, '/')).catch(e => onError(message(e)))}>{children}</button>
|
||||
: <a href={href} target="_blank" rel="noreferrer">{children}</a>,
|
||||
}}>{item.content}</ReactMarkdown></div>
|
||||
}}>{item.content}</ReactMarkdown></div></div>
|
||||
</article>);
|
||||
}
|
||||
|
||||
|
||||
@@ -124,11 +124,12 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
|
||||
return () => window.removeEventListener('keydown', shortcut);
|
||||
});
|
||||
|
||||
return <CloudPendingContext.Provider value={pending.report}><section className="agent-desktop" data-testid="cloud-agent-editor">
|
||||
<header className="agent-workspace-header">
|
||||
return <CloudPendingContext.Provider value={pending.report}><section className={`agent-desktop${tab === 'chat' ? ' agent-chat-editor' : ''}`} data-testid="cloud-agent-editor"
|
||||
data-configuration-settled={!dirty && !busy && !publication.pending && !publication.error && !remote && publication.appliedRevision === saved.draft_revision}>
|
||||
<header className={`agent-workspace-header${tab === 'chat' ? ' agent-chat-workspace-header' : ''}`}>
|
||||
<Button variant="ghost" size="icon" className="h-10 w-10 shrink-0" aria-label="我的智能体" disabled={busy}
|
||||
onClick={() => dirty || hasPending ? setLeaving(true) : onBack()}><ArrowLeft className="h-4 w-4" /></Button>
|
||||
<Button variant="ghost" className="relative h-12 w-12 shrink-0 p-0" aria-label="修改头像" title="修改头像" disabled={busy} onClick={() => setEditingAvatar(true)}>
|
||||
<Button variant="ghost" className="agent-chat-identity-avatar relative h-12 w-12 shrink-0 p-0" aria-label="修改头像" title="修改头像" disabled={busy} onClick={() => setEditingAvatar(true)}>
|
||||
<CloudAvatar src={draft.icon_url} name={draft.name} identity={saved.slug} />
|
||||
<span className="absolute -bottom-1 -right-1 rounded-full border-2 border-background bg-primary p-1 text-white"><ImagePlus aria-hidden="true" className="h-3 w-3" /></span>
|
||||
</Button>
|
||||
@@ -141,12 +142,12 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
|
||||
</div>
|
||||
<p className="px-2 text-xs text-muted-foreground">{saved.published_version ? '我的智能体' : '配置智能体'}</p>
|
||||
</div>
|
||||
<span role="status" className={`shrink-0 text-xs ${needsApply || dirty ? 'text-amber-800' : 'text-muted-foreground'}`}>{savingDraft ? '正在保存…' : dirty ? '有未保存修改' : saved.published_version ? applicationStatus : '云端已保存'}</span>
|
||||
<Button className="min-h-10" size="sm" variant={dirty ? 'default' : 'outline'} disabled={!canSave} onClick={() => void save()} title="保存草稿(Ctrl / ⌘ S)">
|
||||
<span role="status" className={`agent-chat-config-status shrink-0 text-xs ${needsApply || dirty ? 'text-amber-800' : 'text-muted-foreground'}`}>{savingDraft ? '正在保存…' : dirty ? '有未保存修改' : saved.published_version ? applicationStatus : '云端已保存'}</span>
|
||||
<Button className="agent-chat-save-action min-h-10" size="sm" variant={dirty ? 'default' : 'outline'} disabled={!canSave} onClick={() => void save()} title="保存草稿(Ctrl / ⌘ S)">
|
||||
{savingDraft ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : dirty ? <Save className="mr-2 h-4 w-4" /> : <Check className="mr-2 h-4 w-4" />}
|
||||
{savingDraft ? '保存中…' : dirty ? '保存草稿' : '已保存'}
|
||||
</Button>
|
||||
{saved.published_version && <Button className="min-h-10 shrink-0" disabled={busy || dirty || remote !== null || !saved.configuration.model || !publication.access || (!publication.pending && publication.appliedRevision === saved.draft_revision)}
|
||||
{saved.published_version && <Button className="agent-chat-apply-action min-h-10 shrink-0" disabled={busy || dirty || remote !== null || !saved.configuration.model || !publication.access || (!publication.pending && publication.appliedRevision === saved.draft_revision)}
|
||||
title={dirty ? '请先保存草稿,再应用修改' : '将已保存的修改应用到正在使用的助手'}
|
||||
onClick={() => void publication.publish()}>
|
||||
{publication.busy && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
@@ -157,10 +158,6 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
|
||||
{publication.busy ? '正在开始…' : publication.pending ? '重试开始使用' : '开始使用'}
|
||||
</Button>}
|
||||
</header>
|
||||
{tab !== 'configuration' && <nav aria-label="智能体操作" className="agent-workspace-nav"><Button variant="ghost" onClick={() => {
|
||||
if (formalPending.hasPending) { setError('对话中还有未完成的内容,请先发送或处理后再调整智能体。'); return; }
|
||||
setError(''); navigate('configuration');
|
||||
}}><Pencil aria-hidden="true" className="mr-2 h-4 w-4" />调整智能体</Button></nav>}
|
||||
{saved.published_version && (needsApply || dirty) && <div className="shrink-0 border-b border-amber-200 bg-amber-50 px-6 py-3 text-sm text-amber-900">
|
||||
<p className="font-medium">{dirty ? '修改尚未保存和应用' : '这次修改还没有应用到正在使用的助手'}</p>
|
||||
<p className="mt-1 text-xs leading-5">保存只更新草稿。请点击顶部“应用最新修改”,再到微信等渠道使用。已开始的任务保持原配置。</p>
|
||||
@@ -201,17 +198,25 @@ export function DraftEditor({ initial, recent, onBack, onSaved, startingPrompt,
|
||||
</TabsContent>
|
||||
</Tabs>}
|
||||
</div>
|
||||
<div hidden={tab !== 'chat'} className="agent-workspace-view p-6">
|
||||
{visited.has('chat') && (saved.published_version ? <CloudPendingContext.Provider value={formalPending.report}>
|
||||
<nav aria-label="对话来源" className="mb-3 flex shrink-0 gap-2">
|
||||
<Button variant={conversationSource === 'desktop' ? 'secondary' : 'ghost'} disabled={formalPending.hasPending && conversationSource !== 'desktop'} onClick={() => setConversationSource('desktop')}>客户端对话</Button>
|
||||
<Button variant={conversationSource === 'wechat' ? 'secondary' : 'ghost'} disabled={formalPending.hasPending && conversationSource !== 'wechat'} onClick={() => { setWechatVisited(true); setConversationSource('wechat'); }}>微信对话</Button>
|
||||
<div hidden={tab !== 'chat'} className="agent-workspace-view agent-conversation-workspace">
|
||||
{visited.has('chat') && <CloudPendingContext.Provider value={formalPending.report}>
|
||||
<nav aria-label="对话来源" className="agent-conversation-sources">
|
||||
{saved.published_version && <div className="agent-conversation-source-options">
|
||||
<Button variant={conversationSource === 'desktop' ? 'secondary' : 'ghost'} aria-pressed={conversationSource === 'desktop'} disabled={formalPending.hasPending && conversationSource !== 'desktop'} onClick={() => setConversationSource('desktop')}>客户端对话</Button>
|
||||
<Button variant={conversationSource === 'wechat' ? 'secondary' : 'ghost'} aria-pressed={conversationSource === 'wechat'} disabled={formalPending.hasPending && conversationSource !== 'wechat'} onClick={() => { setWechatVisited(true); setConversationSource('wechat'); }}>微信对话</Button>
|
||||
</div>}
|
||||
<Button variant="ghost" className="agent-conversation-adjust" onClick={() => {
|
||||
if (formalPending.hasPending) { setError('对话中还有未完成的内容,请先发送或处理后再调整智能体。'); return; }
|
||||
setError(''); navigate('configuration');
|
||||
}}><Pencil aria-hidden="true" className="mr-2 h-4 w-4" />调整智能体</Button>
|
||||
</nav>
|
||||
{saved.published_version ? <>
|
||||
<div hidden={conversationSource !== 'desktop'} className="flex min-h-0 flex-1 flex-col">
|
||||
<CloudChat key="published" slug={saved.slug} activeView={tab === 'chat' && conversationSource === 'desktop'} /></div>
|
||||
<CloudChat key="published" slug={saved.slug} assistantName={saved.name} assistantAvatarUrl={saved.icon_url} activeView={tab === 'chat' && conversationSource === 'desktop'} /></div>
|
||||
<div hidden={conversationSource !== 'wechat'} className="min-h-0 flex-1 overflow-auto">{wechatVisited && <CloudChannelConversations slug={saved.slug} activeView={tab === 'chat' && conversationSource === 'wechat'} />}</div>
|
||||
</CloudPendingContext.Provider>
|
||||
: <div className="m-auto space-y-4 text-center text-sm"><p>保存配置后,开始使用这个智能体。</p><Button disabled={busy || remote !== null || !publication.access} onClick={startUsing}>{publication.pending ? '重试开始使用' : '开始使用'}</Button></div>)}
|
||||
</>
|
||||
: <div className="m-auto space-y-4 text-center text-sm"><p>保存配置后,开始使用这个智能体。</p><Button disabled={busy || remote !== null || !publication.access} onClick={startUsing}>{publication.pending ? '重试开始使用' : '开始使用'}</Button></div>}
|
||||
</CloudPendingContext.Provider>}
|
||||
</div>
|
||||
<Dialog open={editingAvatar} onOpenChange={open => { if (!uploading) setEditingAvatar(open); }}>
|
||||
<DialogContent><DialogTitle>修改头像</DialogTitle><DialogDescription>选择这个智能体的头像。</DialogDescription>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
/* Personal Agent chat shares the homepage/configuration palette. Keep these
|
||||
* rules local so Code, teacher and channel conversation renderers stay intact. */
|
||||
.agent-chat-editor { background: #fffdf9; }
|
||||
.agent-chat-workspace-header { background: #fffdf9; border-color: #e9e5de; }
|
||||
.agent-chat-workspace-header > button { border-radius: 13px; }
|
||||
.agent-conversation-workspace { padding: 0; background: #fffdf9; container-type: inline-size; }
|
||||
.agent-conversation-sources { display: flex; flex-shrink: 0; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; padding: 10px 24px; border-bottom: 1px solid #e9e5de; }
|
||||
.agent-conversation-source-options { display: flex; gap: 4px; padding: 4px; border: 1px solid #e5e2ee; border-radius: 15px; background: #f2eff7; }
|
||||
.agent-conversation-source-options button { min-height: 36px; height: auto; border-radius: 10px; padding: 8px 14px; color: #62677b; box-shadow: none; }
|
||||
.agent-conversation-source-options button[aria-pressed="true"] { background: #fff; color: #775fa2; box-shadow: 0 1px 4px #27324608; }
|
||||
.agent-conversation-adjust { gap: 7px; border-radius: 12px; color: #606c7e; }
|
||||
.agent-conversation-adjust svg { margin: 0; }
|
||||
.agent-chat-styled { container-type: inline-size; background: #fffdf9; color: #273246; }
|
||||
.agent-chat-styled .agent-chat-toolbar { max-height: 32%; padding: 0 24px; }
|
||||
.agent-chat-toolbar-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 18px; min-height: 72px; padding: 12px 0; }
|
||||
.agent-chat-toolbar-heading { display: flex; min-width: 0; align-items: center; gap: 11px; }
|
||||
.agent-chat-toolbar-icon { display: grid; width: 38px; height: 38px; flex-shrink: 0; place-items: center; border-radius: 13px; background: #e7f0eb; color: #5c8170; }
|
||||
.agent-chat-toolbar-icon svg { width: 21px; height: 21px; stroke-width: 1.8; }
|
||||
.agent-chat-toolbar-title { font-size: 15px; font-weight: 600; line-height: 1.5; }
|
||||
.agent-chat-toolbar-actions { display: flex; flex-shrink: 0; gap: 6px; }
|
||||
.agent-chat-toolbar-actions button { display: flex; min-height: 40px; align-items: center; gap: 7px; border: 1px solid #e7e3dd; border-radius: 12px; padding: 8px 12px; background: #fff; color: #59657a; }
|
||||
.agent-chat-toolbar-actions button[aria-expanded="true"] { border-color: #cbbce4; background: #f3eefb; color: #70568e; }
|
||||
.agent-chat-history-select { display: block; width: 100%; min-width: 0; min-height: 44px; margin: 0 0 12px; padding: 10px 12px; border: 1px solid #d6cee3; border-radius: 12px; background: #fff; color: #35435b; font-size: 14px; }
|
||||
.agent-chat-styled .agent-chat-scroll { display: flex; flex-direction: column; }
|
||||
.agent-chat-transcript { display: flex; width: calc(100% - 48px); max-width: 860px; min-height: min-content; flex: 1; flex-direction: column; gap: 26px; margin-inline: auto; padding: 16px 0 24px; }
|
||||
.agent-chat-loading { display: flex; min-height: 160px; align-items: center; justify-content: center; gap: 10px; color: #606c7e; font-size: 14px; }
|
||||
.agent-chat-loading svg { width: 22px; height: 22px; color: #8a72ba; }
|
||||
.agent-chat-welcome { width: 100%; max-width: 780px; margin: auto; padding: 24px 0; text-align: center; }
|
||||
.agent-chat-welcome-portrait { position: relative; width: 92px; margin: 0 auto 12px; }
|
||||
.agent-chat-styled .agent-chat-welcome-avatar { width: 92px; height: 92px; border: 4px solid #fff; border-radius: 28px; background: #fff0d4; box-shadow: 0 3px 0 #e8dfd2; }
|
||||
.agent-chat-welcome-spark { position: absolute; top: -7px; right: -18px; width: 30px; height: 30px; color: #9175b9; transform: rotate(10deg); }
|
||||
.agent-chat-welcome-name { overflow-wrap: anywhere; color: #7263a6; font-size: 14px; font-weight: 600; line-height: 1.6; }
|
||||
.agent-chat-welcome h2 { margin: 10px 0 8px; font-size: clamp(23px, 2.2vw, 30px); font-weight: 650; letter-spacing: -.5px; line-height: 1.5; }
|
||||
.agent-chat-welcome-description { margin-inline: auto; max-width: 540px; font-size: 14px; line-height: 1.8; color: #606c7e; }
|
||||
.agent-chat-examples { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: 12px; margin-top: 26px; text-align: left; }
|
||||
.agent-chat-styled .agent-chat-example { display: flex; width: 100%; min-height: 82px; height: auto; justify-content: space-between; align-items: flex-start; gap: 12px; padding: 16px; border: 1px solid #dce5f3; border-radius: 19px; background: #f1f6ff; color: #35435b; font-size: 14px; font-weight: 400; line-height: 1.7; white-space: normal; text-align: left; box-shadow: none; }
|
||||
.agent-chat-styled .agent-chat-example span { min-width: 0; overflow-wrap: anywhere; }
|
||||
.agent-chat-styled .agent-chat-example svg { width: 17px; height: 17px; flex-shrink: 0; margin-top: 3px; color: #6a7e9a; }
|
||||
.agent-chat-styled .agent-chat-example[data-tone="1"] { border-color: #e5dcf5; background: #f6f2ff; }
|
||||
.agent-chat-styled .agent-chat-example[data-tone="2"] { border-color: #f0e2ce; background: #fff5e9; }
|
||||
.agent-chat-styled .agent-chat-example:hover { border-color: #b8c8de; background: #eaf1fb; }
|
||||
.agent-chat-styled .agent-chat-example[data-tone="1"]:hover { border-color: #ccbbe8; background: #efe8fa; }
|
||||
.agent-chat-styled .agent-chat-example[data-tone="2"]:hover { border-color: #dfc49e; background: #fff0db; }
|
||||
.agent-chat-example-note { margin-top: 13px; color: #6a7384; font-size: 12px; line-height: 1.7; }
|
||||
.agent-chat-message { display: flex; min-width: 0; align-items: flex-start; gap: 13px; }
|
||||
.agent-chat-styled .agent-chat-message-avatar { width: 38px; height: 38px; flex-shrink: 0; border: 2px solid #fff; border-radius: 13px; background: #fff0d4; }
|
||||
.agent-chat-message-content { min-width: 0; flex: 1; }
|
||||
.agent-chat-speaker { margin-bottom: 7px; color: #606c7e; font-size: 12px; font-weight: 500; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.agent-chat-styled .agent-chat-message-body { color: #273246; font-size: 15px; line-height: 1.9; overflow-wrap: anywhere; }
|
||||
.agent-chat-message[data-role="user"] { align-self: flex-end; width: fit-content; max-width: 85%; padding: 14px 18px; border: 1px solid #e4dbf3; border-radius: 20px 20px 6px 20px; background: #f1ebfb; }
|
||||
.agent-chat-message[data-role="user"] .agent-chat-speaker { color: #75628e; }
|
||||
.agent-chat-styled .agent-chat-message-body :is(pre, table) { display: block; max-width: 100%; overflow-x: auto; }
|
||||
.agent-chat-styled .agent-chat-message-body pre { padding: 14px 16px; border: 1px solid #e4e2dc; border-radius: 13px; background: #f3f2ef; font-size: 13px; line-height: 1.7; overflow-wrap: normal; }
|
||||
.agent-chat-styled .agent-chat-message-body img { max-width: 100%; height: auto; border-radius: 12px; }
|
||||
.agent-chat-styled .agent-chat-message-body :is(a, button) { color: #b43e17; text-decoration: none; overflow-wrap: anywhere; }
|
||||
.agent-chat-styled .agent-chat-message-body :is(a, button):hover { text-decoration: underline; }
|
||||
.agent-chat-styled .agent-chat-message-body blockquote { padding: 8px 14px; border-left: 3px solid #c8b8e0; border-radius: 0 10px 10px 0; background: #f4f0fa; color: #5b6173; }
|
||||
.agent-chat-styled .agent-chat-files { width: calc(100% - 48px); max-width: 860px; margin: 0 auto; }
|
||||
.agent-chat-styled .agent-chat-files details { margin: 4px 0 8px; padding: 0 12px; border: 1px solid #e5e7e2; border-radius: 13px; background: #f3f6f1; }
|
||||
.agent-chat-styled .agent-chat-files summary { min-height: 38px; color: #5e7162; font-size: 13px; line-height: 1.8; }
|
||||
.agent-chat-styled .agent-chat-files details > div { padding-bottom: 12px; }
|
||||
.agent-chat-styled .agent-chat-composer { width: calc(100% - 48px); max-width: 860px; margin: 0 auto 20px; padding: 14px 16px 12px; border: 1px solid #ddd5e8; border-radius: 23px; background: #fff; box-shadow: 0 4px 16px #7161820b, 0 3px 0 #eee8f3; }
|
||||
.agent-chat-styled .agent-chat-composer:focus-within { border-color: #b49dce; box-shadow: 0 0 0 3px #eee7f6; }
|
||||
.agent-chat-styled .agent-chat-input { width: 100%; min-height: 64px; height: clamp(64px, 10vh, 92px); max-height: min(152px, 22vh); padding: 2px; background: transparent; color: #273246; font-size: 15px; line-height: 1.8; }
|
||||
.agent-chat-input::placeholder { color: #727c8c; }
|
||||
.agent-chat-composer-footer { display: flex; align-items: flex-end; justify-content: space-between; gap: 12px; margin-top: 8px; }
|
||||
.agent-chat-composer-meta { display: flex; min-width: 0; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding-bottom: 5px; }
|
||||
.agent-chat-status { display: flex; min-width: 0; align-items: center; gap: 6px; color: #626f7e; font-size: 12px; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.agent-chat-status::before { width: 6px; height: 6px; flex-shrink: 0; border-radius: 50%; background: #7d9f8b; content: ''; }
|
||||
.agent-chat-status[data-active="true"]::before { background: #bf8b49; }
|
||||
.agent-chat-keyboard-hint { color: #747c8a; font-size: 11px; line-height: 1.6; }
|
||||
.agent-chat-styled .agent-chat-send, .agent-chat-styled .agent-chat-stop { min-height: 44px; height: auto; flex-shrink: 0; border-radius: 13px; padding: 10px 16px; font-size: 14px; }
|
||||
.agent-chat-styled .agent-chat-send { box-shadow: 0 3px 0 #ad3812; }
|
||||
.agent-chat-styled :is(button, summary, select) { cursor: pointer; }
|
||||
.agent-chat-styled :is(button, textarea, select):focus-visible { outline: 2px solid #9275b4; outline-offset: 3px; }
|
||||
.agent-chat-styled :is(button, textarea, select):disabled { cursor: not-allowed; }
|
||||
@container (max-width: 620px) {
|
||||
.agent-chat-styled .agent-chat-toolbar { padding-inline: 14px; }
|
||||
.agent-chat-toolbar-row { min-height: 64px; }
|
||||
.agent-chat-toolbar-actions button { padding-inline: 10px; }
|
||||
.agent-chat-toolbar-actions button span { display: none; }
|
||||
.agent-chat-examples { grid-template-columns: minmax(0, 1fr); gap: 8px; margin-top: 20px; }
|
||||
.agent-chat-styled .agent-chat-example { min-height: 48px; padding: 12px 14px; border-radius: 15px; }
|
||||
.agent-chat-transcript { width: calc(100% - 28px); gap: 22px; }
|
||||
.agent-chat-welcome { padding-block: 16px; }
|
||||
.agent-chat-welcome-portrait { width: 74px; }
|
||||
.agent-chat-styled .agent-chat-welcome-avatar { width: 74px; height: 74px; border-radius: 23px; }
|
||||
.agent-chat-welcome h2 { font-size: 23px; }
|
||||
.agent-chat-message { gap: 9px; }
|
||||
.agent-chat-styled .agent-chat-message-avatar { width: 30px; height: 30px; border-radius: 10px; }
|
||||
.agent-chat-styled .agent-chat-message-body { font-size: 14px; }
|
||||
.agent-chat-message[data-role="user"] { max-width: 91%; padding: 12px 14px; }
|
||||
.agent-chat-styled .agent-chat-files, .agent-chat-styled .agent-chat-composer { width: calc(100% - 28px); }
|
||||
.agent-chat-styled .agent-chat-composer { margin-bottom: 14px; padding: 12px 13px; border-radius: 20px; }
|
||||
.agent-chat-keyboard-hint { display: none; }
|
||||
}
|
||||
@container (max-width: 440px) {
|
||||
.agent-conversation-sources { padding-inline: 12px; gap: 8px; }
|
||||
.agent-conversation-source-options button { padding-inline: 9px; font-size: 13px; }
|
||||
.agent-conversation-adjust { padding-inline: 8px; font-size: 13px; }
|
||||
}
|
||||
@media (max-height: 640px) {
|
||||
.agent-chat-workspace-header { min-height: 56px; gap: 8px; padding: 6px 12px; }
|
||||
.agent-chat-workspace-header > .agent-workspace-identity { min-width: min(120px, 100%); flex-basis: 120px; }
|
||||
.agent-chat-workspace-header .agent-workspace-identity > p { display: none; }
|
||||
.agent-chat-workspace-header > .agent-chat-identity-avatar,
|
||||
.agent-chat-workspace-header > .agent-chat-identity-avatar img { width: 40px; height: 40px; }
|
||||
.agent-chat-editor[data-configuration-settled="true"] .agent-chat-workspace-header > :is(.agent-chat-config-status, .agent-chat-save-action, .agent-chat-apply-action) { display: none; }
|
||||
.agent-conversation-sources { padding: 6px 12px; }
|
||||
.agent-conversation-source-options { padding: 2px; }
|
||||
.agent-chat-styled .agent-chat-files summary { min-height: 32px; }
|
||||
.agent-chat-styled .agent-chat-files details { margin: 3px 0 5px; }
|
||||
.agent-chat-styled .agent-chat-input { min-height: 40px; height: 40px; }
|
||||
.agent-chat-styled .agent-chat-composer { margin-bottom: 10px; padding-block: 10px; }
|
||||
.agent-chat-styled .agent-chat-composer-footer { margin-top: 4px; }
|
||||
.agent-chat-transcript { padding-block: 8px 12px; }
|
||||
.agent-chat-toolbar-row { min-height: 50px; padding-block: 5px; }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.agent-chat-styled :is(button, textarea, select) { transition: none; }
|
||||
}
|
||||
Reference in new issue
Block a user