Simplify human conversation and hide internal audit envelopes
This commit is contained in:
@@ -1,34 +1,28 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type { FormEvent } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { Send, RotateCcw, MessageSquare } from 'lucide-react'
|
||||
import { Send, RefreshCw } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { PageHeading, Panel, Empty, Status } from '@/components/shared'
|
||||
import { PageHeading } from '@/components/shared'
|
||||
import { useWorkbench } from '@/lib/context'
|
||||
import { request, errorText, messageKey } from '@/lib/api'
|
||||
import type { Message } from '@/lib/types'
|
||||
import { date } from '@/lib/types'
|
||||
export default function Communication() {
|
||||
const { state, token, refresh } = useWorkbench()
|
||||
const primary = state.primary_session_id || 'wp4-progress'
|
||||
const [params, setParams] = useSearchParams()
|
||||
const selected = params.get('session') || primary
|
||||
const primary = state.primary_session_id || 'human-progress'
|
||||
const selected = primary
|
||||
const session = state.sessions.find(s => s.id === selected)
|
||||
const [messages, setMessages] = useState<{ session: string; items: Message[] }>({ session: '', items: [] })
|
||||
const [drafts, setDrafts] = useState<Record<string, string>>({})
|
||||
const [error, setError] = useState('')
|
||||
const [sending, setSending] = useState(false)
|
||||
const [requesting, setRequesting] = useState(false)
|
||||
const [refreshVersion, setRefreshVersion] = useState(0)
|
||||
const pending = useRef<{ session_id: string; text: string; idempotency_key: string } | null>(null)
|
||||
const scroll = useRef<HTMLDivElement>(null)
|
||||
const loaded = useRef('')
|
||||
const known = Boolean(session)
|
||||
const originalUrl = session?.thread_url && /^codex:\/\/threads\/[a-f0-9-]{36}$/i.test(session.thread_url) ? session.thread_url : undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (!known) return
|
||||
const controller = new AbortController()
|
||||
@@ -37,7 +31,7 @@ export default function Communication() {
|
||||
if (inFlight) return
|
||||
inFlight = true
|
||||
try {
|
||||
const result = await request<{ messages: Message[] }>(token, `/api/messages?session_id=${encodeURIComponent(selected)}`, undefined, controller.signal)
|
||||
const result = await request<{ messages: Message[] }>(token, `/api/conversation?session_id=${encodeURIComponent(selected)}`, undefined, controller.signal)
|
||||
if (!controller.signal.aborted) { setMessages({ session: selected, items: result.messages }); setError('') }
|
||||
} catch (e) { if (!controller.signal.aborted) setError(errorText(e)) }
|
||||
finally { inFlight = false }
|
||||
@@ -48,7 +42,12 @@ export default function Communication() {
|
||||
}, [selected, token, known, refreshVersion])
|
||||
const items = messages.session === selected ? messages.items : []
|
||||
const lastId = items.at(-1)?.id
|
||||
useEffect(() => { const box = scroll.current; if (box && box.scrollHeight - box.scrollTop - box.clientHeight < 300) box.scrollTop = box.scrollHeight }, [lastId])
|
||||
useEffect(() => {
|
||||
const box = scroll.current
|
||||
if (!box || messages.session !== selected) return
|
||||
if (loaded.current !== selected || box.scrollHeight - box.scrollTop - box.clientHeight < 300) box.scrollTop = box.scrollHeight
|
||||
loaded.current = selected
|
||||
}, [lastId, selected, messages.session])
|
||||
const text = drafts[selected] || ''
|
||||
async function send(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
@@ -65,19 +64,24 @@ export default function Communication() {
|
||||
} catch (e) { setError(errorText(e)) }
|
||||
finally { setSending(false) }
|
||||
}
|
||||
async function newSession(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault(); const form = event.currentTarget, data = new FormData(form)
|
||||
setRequesting(true)
|
||||
try { await request(token, '/api/session-requests', { name: data.get('name'), wp: data.get('wp'), prompt: data.get('prompt') }); form.reset(); await refresh() }
|
||||
catch (e) { setError(errorText(e)) }
|
||||
finally { setRequesting(false) }
|
||||
}
|
||||
return <div className="space-y-6"><PageHeading title="进度与方案沟通" description="当前绑定会话用于进度询问、汇报和方案讨论。" action={<Button variant="outline" onClick={() => setParams({})}><RotateCcw />返回默认会话</Button>} />
|
||||
return <div className="mx-auto flex w-full max-w-4xl flex-col gap-5">
|
||||
<PageHeading title="项目沟通" description="询问进度、讨论方案,或告诉我你的新想法。" action={<Button variant="ghost" size="sm" onClick={() => { setRefreshVersion(n => n + 1); void refresh() }}><RefreshCw />刷新</Button>} />
|
||||
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
|
||||
<div className="grid items-start gap-6 xl:grid-cols-[minmax(0,1fr)_300px]"><Panel title="项目沟通" description="消息始终发送给当前选中的会话。"><div className="space-y-5">{originalUrl && <Button asChild variant="outline" size="sm"><a href={originalUrl}>在本机打开原会话</a></Button>}<div className="space-y-2"><Label htmlFor="session">当前会话</Label><Select value={selected} onValueChange={value => setParams({ session: value })}><SelectTrigger id="session" className="w-full"><SelectValue /></SelectTrigger><SelectContent>{!session && <SelectItem value={selected}>默认会话尚未登记</SelectItem>}{state.sessions.map(s => <SelectItem key={s.id} value={s.id}>{s.id === primary ? '默认会话 · ' : ''}{s.wp || s.role} · {s.name}</SelectItem>)}</SelectContent></Select></div>
|
||||
<div className="flex items-start gap-3 rounded-lg bg-muted/60 p-4 text-sm"><MessageSquare className="mt-0.5 size-4 shrink-0 text-primary" /><div><p className="font-medium">{selected === primary ? '同一绑定会话的消息同步' : '当前工作会话'}</p><p className="mt-1 text-muted-foreground">{!session ? '等待登记。不会改投其他会话。' : session.connection === 'connected' ? '已接入真实 AI。' : '等待 AI 执行桥接接入;新消息将持久排队。'}</p>{session?.history_synced_at && <p className="mt-1 text-xs text-muted-foreground">原会话同步于 {date(session.history_synced_at)}</p>}</div></div>
|
||||
<div ref={scroll} role="log" aria-label="会话消息" aria-live="polite" className="h-[min(48vh,480px)] min-h-64 space-y-4 overflow-y-auto rounded-lg border bg-muted/20 p-4">{items.length ? items.map(m => <article key={m.id} className={`rounded-lg border bg-card p-4 ${m.source === 'agent' ? 'border-l-2 border-l-primary' : ''}`}><div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-muted-foreground"><span className="font-medium text-foreground">{m.source === 'agent' ? 'Agent' : '人工'}</span>{m.origin === 'session_history' && <span>原会话同步</span>}<Status value={m.status} /><time>{date(m.created)}</time></div><p className="whitespace-pre-wrap wrap-anywhere text-sm leading-7">{m.text}</p></article>) : <Empty>{known ? '可以询问项目进度,或提出需要比较的方案。' : '默认会话接入后,在这里继续讨论。'}</Empty>}</div>
|
||||
<form onSubmit={send} className="space-y-3"><Label htmlFor="message">发送消息</Label><Textarea id="message" value={text} onChange={e => setDrafts(old => ({ ...old, [selected]: e.target.value }))} placeholder="目前进展如何?有哪些方案需要我决定?" maxLength={32000} rows={3} required /><div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between"><p className="text-xs text-muted-foreground">消息来源字段不证明人类批准。</p><Button type="submit" disabled={!known || sending || !text.trim()}><Send />{sending ? '发送中…' : '发送到当前会话'}</Button></div></form>
|
||||
</div></Panel><Panel title="沟通与协作" description="进度、汇报、方案,集中在一个入口。"><p className="text-sm leading-7 text-muted-foreground">历史正文会保留原会话来源。历史人工消息只展示,不会重新进入执行队列。</p><details className="mt-6 border-t pt-5"><summary className="cursor-pointer text-sm font-medium">请求其他工作会话</summary><form onSubmit={newSession} className="mt-4 space-y-4"><div className="space-y-2"><Label htmlFor="name">会话名称</Label><Input id="name" name="name" required maxLength={200} /></div><div className="space-y-2"><Label htmlFor="wp">工作包</Label><Input id="wp" name="wp" maxLength={100} /></div><div className="space-y-2"><Label htmlFor="prompt">任务说明</Label><Textarea id="prompt" name="prompt" required maxLength={16000} /></div><Button variant="outline" disabled={requesting}>{requesting ? '登记中…' : '登记新建请求'}</Button></form><p className="mt-4 text-xs leading-6 text-muted-foreground">登记请求不会自动创建真实 AI 会话。</p>{state.requests.map(r => <p key={r.id} className="mt-3 text-xs text-muted-foreground">{r.name} · 等待接入</p>)}</details></Panel></div>
|
||||
<section className="overflow-hidden rounded-xl border bg-card">
|
||||
<div ref={scroll} role="log" aria-label="对话" aria-live="polite" className="h-[min(60vh,660px)] min-h-72 space-y-6 overflow-y-auto p-5 sm:p-7">
|
||||
{items.length ? items.map(m => <article key={m.id} className={`flex ${m.source === 'human' ? 'justify-end' : 'justify-start'}`}>
|
||||
<div className={`max-w-[92%] rounded-2xl px-4 py-3 sm:max-w-[85%] ${m.source === 'human' ? 'bg-primary text-primary-foreground' : 'bg-muted/60'}`}>
|
||||
<p className="mb-1 text-xs opacity-65">{m.source === 'human' ? '你' : '项目助手'}</p>
|
||||
<p className="whitespace-pre-wrap wrap-anywhere text-sm leading-7">{m.text}</p>
|
||||
{m.source === 'human' && ['queued', 'claimed'].includes(m.status) && <p className="mt-2 text-xs opacity-70">正在等待回复…</p>}
|
||||
{m.source === 'human' && ['failed', 'uncertain'].includes(m.status) && <p className="mt-2 text-xs opacity-70">暂未确认回复,请稍后查看;无需重复发送。</p>}
|
||||
</div>
|
||||
</article>) : <div className="flex h-full items-center justify-center text-sm text-muted-foreground">{known ? '有什么想了解的?从这里开始交流。' : '会话准备好后,即可在这里交流。'}</div>}
|
||||
</div>
|
||||
<form onSubmit={send} className="border-t p-4">
|
||||
<Textarea id="message" aria-label="输入消息" value={text} onChange={e => setDrafts(old => ({ ...old, [selected]: e.target.value }))} placeholder="输入你的问题或想法…" maxLength={32000} rows={3} required className="resize-none border-0 shadow-none focus-visible:ring-0" />
|
||||
<div className="mt-2 flex items-center justify-between gap-3"><p className="text-xs text-muted-foreground">{!known ? '正在准备会话' : session?.connection === 'connected' ? '' : '暂时离线,消息会保存并等待回复。'}</p><Button type="submit" disabled={!known || sending || !text.trim()}><Send />{sending ? '发送中…' : '发送'}</Button></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user