Preserve isolated local drafts across navigation and reload
This commit is contained in:
@@ -7,6 +7,7 @@ import { Textarea } from '@/components/ui/textarea'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { useWorkbench } from '@/lib/context'
|
||||
import { request, errorText, messageKey } from '@/lib/api'
|
||||
import { useLocalDraft } from '@/lib/use-local-draft'
|
||||
import type { Message } from '@/lib/types'
|
||||
export default function Communication() {
|
||||
const { state, token, refresh } = useWorkbench()
|
||||
@@ -14,11 +15,11 @@ export default function Communication() {
|
||||
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 local = useLocalDraft(state.instance_id ? [state.instance_id, 'communication', selected] : null, { text: '', attempt: undefined as {text:string;key:string}|undefined }, (v): v is {text:string;attempt?:{text:string;key:string}} => Boolean(v && typeof v === 'object' && typeof (v as {text?:unknown}).text === 'string'))
|
||||
const [error, setError] = useState('')
|
||||
const [sending, setSending] = useState(false)
|
||||
const [refreshVersion, setRefreshVersion] = useState(0)
|
||||
const pending = useRef<{ session_id: string; text: string; idempotency_key: string } | null>(null)
|
||||
const sendingRef = useRef(false)
|
||||
const scroll = useRef<HTMLDivElement>(null)
|
||||
const loaded = useRef('')
|
||||
const known = Boolean(session)
|
||||
@@ -54,21 +55,22 @@ export default function Communication() {
|
||||
loaded.current = selected
|
||||
}, [lastId, selected, messages.session])
|
||||
const decisions = (state as { decisions?: { id: string; status: string }[] }).decisions?.filter(d => ['open','needs_reconsideration'].includes(d.status)) || []
|
||||
const text = drafts[selected] || ''
|
||||
const text = local.value.text
|
||||
async function send(event: FormEvent) {
|
||||
event.preventDefault()
|
||||
if (!session || !text.trim() || sending) return
|
||||
if (!session || !text.trim() || sendingRef.current || !local.ready) return
|
||||
const sid = selected, body = text
|
||||
if (!pending.current || pending.current.session_id !== sid || pending.current.text !== body) pending.current = { session_id: sid, text: body, idempotency_key: messageKey() }
|
||||
const attempt = local.value.attempt?.text === body ? local.value.attempt : {text:body,key:messageKey()}
|
||||
if (!local.save({text:body,attempt})) return
|
||||
sendingRef.current = true
|
||||
setSending(true); setError('')
|
||||
try {
|
||||
await request(token, '/api/messages', { ...pending.current, source: 'human' })
|
||||
pending.current = null
|
||||
setDrafts(old => old[sid] === body ? { ...old, [sid]: '' } : old)
|
||||
await request(token, '/api/messages', { session_id:sid, text:body, idempotency_key:attempt.key, source:'human' })
|
||||
local.clear()
|
||||
setRefreshVersion(n => n + 1)
|
||||
await refresh()
|
||||
} catch (e) { setError(errorText(e)) }
|
||||
finally { setSending(false) }
|
||||
finally { sendingRef.current=false; setSending(false) }
|
||||
}
|
||||
return <div className="flex min-h-0 w-full flex-1 flex-col">
|
||||
|
||||
@@ -87,8 +89,8 @@ export default function Communication() {
|
||||
</article>) : <div className="flex h-full items-center justify-center text-sm text-muted-foreground">{known ? '有什么想了解的?从这里开始交流。' : '会话准备好后,即可在这里交流。'}</div>}
|
||||
</div>
|
||||
<form onSubmit={send} className="shrink-0 border-t p-3 sm:px-8">
|
||||
<Textarea id="message" aria-label="输入消息" value={text} onChange={e => setDrafts(old => ({ ...old, [selected]: e.target.value }))} placeholder="输入你的问题或想法…" maxLength={32000} rows={2} 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>
|
||||
<p role="status" className="mb-2 text-xs text-muted-foreground">{local.error || '草稿保存在此浏览器,可在重新登录后恢复。'}</p><Button type="button" variant="ghost" size="sm" disabled={sending} onClick={() => local.clear(false,true)}>清除本地沟通草稿</Button><Textarea id="message" aria-label="输入消息" value={text} disabled={sending} onChange={e => local.save({...local.value,text:e.target.value})} placeholder="输入你的问题或想法…" maxLength={32000} rows={2} 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 || !local.ready || sending || !text.trim()}><Send />{sending ? '发送中…' : '发送'}</Button></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user