Remove unsolicited draft controls and persistent guidance

This commit is contained in:
aiagent
2026-10-19 22:55:43 -07:00
parent 357502d1c9
commit ac8c154eff
8 changed files with 17 additions and 13 deletions
@@ -43,10 +43,10 @@ export default function DecisionChat({ decision, token }: { decision: Decision;
finally { sending.current=false; draft.sending = false; redraw(n => n + 1) }
}
return <section className="flex min-w-0 flex-col rounded-xl border bg-card" aria-label="本决策咨询聊天"><div className="flex items-center justify-between border-b p-3"><h3 className="font-medium">本决策咨询</h3><Button size="sm" variant="ghost" disabled={!bound} onClick={() => void refresh.current()}>刷新</Button></div>
<p className="px-3 pt-3 text-xs text-muted-foreground">{!bound ? '咨询会话待接通,暂不能发送。' : readOnly ? '本决策已完成,咨询历史只读。' : '消息仅发送到本决策绑定的咨询会话。绑定不代表回复链路已验收。'}</p>
{(!bound || readOnly) && <p className="px-3 pt-3 text-xs text-muted-foreground">{!bound ? '咨询暂不可用。' : '已完成,只读。'}</p>}
<div ref={scroll} role="log" aria-label="本决策消息" className="max-h-[45vh] min-h-40 space-y-3 overflow-y-auto p-3">{messages.map(message => <article key={message.id} className={`rounded-lg p-3 text-sm ${message.source === 'human' ? 'ml-6 bg-primary text-primary-foreground' : 'mr-6 bg-muted'}`}><p className="mb-1 text-xs opacity-70">{message.source === 'human' ? '你' : '咨询助手'}</p><p className="whitespace-pre-wrap break-words">{message.text}</p>{message.source === 'human' && ['queued','claimed'].includes(message.status) && <p className="mt-1 text-xs">等待回复…</p>}{['failed','uncertain'].includes(message.status) && <p className="mt-1 text-xs">回复尚未确认,请勿重复发送。</p>}</article>)}{!messages.length && <p className="text-sm text-muted-foreground">{!bound ? '暂无已接通的咨询会话。' : loaded ? '暂无咨询消息。' : '正在读取咨询历史…'}</p>}</div>
{(error || draft.error) && <p role="status" className="px-3 pb-3 text-sm text-destructive">{draft.error || error}</p>}
<div className="space-y-2 px-3 pb-3"><p role="status" className="text-xs text-muted-foreground">{local.error || '咨询草稿单独保存在此浏览器。'}</p>{local.error && <Button variant="outline" size="sm" disabled={draft.sending} onClick={() => persist()}>重试保存草稿</Button>}<Button variant="ghost" size="sm" disabled={draft.sending} onClick={() => local.clear(true)}>清除本咨询本地草稿</Button>{draft.revision !== revision && <div className="text-sm"><p>这是版本 {draft.revision} 的咨询草稿,请重新确认当前问题。</p>{!readOnly && <Button variant="outline" size="sm" onClick={() => {draft.revision=revision;draft.attempt=undefined;persist()}}>确认用于当前版本</Button>}</div>}{readOnly && draft.text && <p className="whitespace-pre-wrap text-sm">保留的未发送草稿(只读):{draft.text}</p>}</div>
<div className="space-y-2 px-3 pb-3">{local.error && <p role="status" className="text-xs text-destructive">{local.error}</p>}{local.error && <Button variant="outline" size="sm" disabled={draft.sending} onClick={() => persist()}>重试保存草稿</Button>}{draft.revision !== revision && <div className="text-sm"><p>这是版本 {draft.revision} 的咨询草稿,请重新确认当前问题。</p>{!readOnly && <Button variant="outline" size="sm" onClick={() => {draft.revision=revision;draft.attempt=undefined;persist()}}>确认用于当前版本</Button>}</div>}{readOnly && draft.text && <p className="whitespace-pre-wrap text-sm">保留的未发送草稿(只读):{draft.text}</p>}</div>
{!readOnly && <form className="space-y-2 border-t p-3" onSubmit={event => { event.preventDefault(); void send() }}><Textarea aria-label="本决策咨询问题" value={draft.text} disabled={!bound || draft.sending} maxLength={32000} onChange={event => { draft.text = event.target.value; persist() }} placeholder="询问方案、依据或影响…" /><Button type="submit" disabled={!bound || !local.ready || draft.revision !== revision || draft.sending || !draft.text.trim()}>{draft.sending ? '正在发送…' : '发送咨询'}</Button></form>}
</section>
}
@@ -6,7 +6,7 @@ export default function VersionNotice() {
const entry = document.querySelector<HTMLScriptElement>('script[type="module"][src]')?.getAttribute('src')
if (!entry || !state.frontend_entrypoint || entry !== state.frontend_entrypoint) {
if (!entry || !state.frontend_entrypoint) return null
return <Alert role="status"><AlertDescription>页面已有新版。输入处会显示本地保存状态。若提示保存失败,请先复制草稿并记下选择;确认已保存后可重新加载并登录恢复。仅刷新消息或状态不会更新页面;不会自动重载或重发你的输入。</AlertDescription></Alert>
return <Alert role="status"><AlertDescription>页面已更新,请重新加载并登录。</AlertDescription></Alert>
}
return null
}
+3 -3
View File
@@ -3,9 +3,9 @@ import { clearDraft, clearIfUnchanged, draftKey, readDraft, writeDraft } from '.
export function useLocalDraft<T>(scope: string[] | null, empty: T, valid: (value: unknown) => value is T, olderVersions = false) {
const identity=JSON.stringify(scope), ref=useRef<{identity:string;value:T;error:string;loaded:boolean;persisted:string|null} | null>(null)
const [, redraw]=useState(0)
if(ref.current?.identity!==identity){let value=empty,error='',persisted=null;try{if(scope){value=readDraft(window.localStorage,scope,empty,valid,olderVersions);persisted=window.localStorage.getItem(draftKey(scope))}else error='实例或版本信息尚未就绪,暂不能保存或发送。'}catch{error='本地草稿读取失败,请先复制输入;暂不能确认已保存。'}ref.current={identity,value,error,loaded:!error,persisted}}
if(ref.current?.identity!==identity){let value=empty,error='',persisted=null;try{if(scope){value=readDraft(window.localStorage,scope,empty,valid,olderVersions);persisted=window.localStorage.getItem(draftKey(scope))}else error='实例或版本信息尚未就绪,暂不能保存或发送。'}catch{error='草稿读取失败,请先复制输入。'}ref.current={identity,value,error,loaded:!error,persisted}}
const record=ref.current!
function save(value:T){record.value=value;try{if(!scope)throw Error();writeDraft(window.localStorage,scope,value);record.persisted=window.localStorage.getItem(draftKey(scope));record.error='';record.loaded=true;redraw(n=>n+1);return true}catch{record.error='本地保存失败,请复制草稿;刷新或退出可能丢失。';record.loaded=false;redraw(n=>n+1);return false}}
function clear(allVersions=false,userRequested=false){try{if(!scope)throw Error();if(allVersions||userRequested)clearDraft(window.localStorage,scope,allVersions);else if(!clearIfUnchanged(window.localStorage,scope,record.persisted)){record.error='已提交;保留了随后编辑的本地草稿。';redraw(n=>n+1);return false}record.value=empty;record.persisted=window.localStorage.getItem(draftKey(scope));record.error='';record.loaded=true;redraw(n=>n+1);return true}catch{record.error='本地草稿清除失败,旧草稿可能仍保留;请勿重复提交。';record.loaded=false;redraw(n=>n+1);return false}}
function save(value:T){record.value=value;try{if(!scope)throw Error();writeDraft(window.localStorage,scope,value);record.persisted=window.localStorage.getItem(draftKey(scope));record.error='';record.loaded=true;redraw(n=>n+1);return true}catch{record.error='草稿保存失败,请复制后重试。';record.loaded=false;redraw(n=>n+1);return false}}
function clear(allVersions=false,userRequested=false){try{if(!scope)throw Error();if(allVersions||userRequested)clearDraft(window.localStorage,scope,allVersions);else if(!clearIfUnchanged(window.localStorage,scope,record.persisted)){record.error='';redraw(n=>n+1);return false}record.value=empty;record.persisted=window.localStorage.getItem(draftKey(scope));record.error='';record.loaded=true;redraw(n=>n+1);return true}catch{record.error='草稿清除失败,请勿重复提交。';record.loaded=false;redraw(n=>n+1);return false}}
return {value:record.value,error:record.error,save,clear,ready:Boolean(scope)&&record.loaded}
}
+1 -1
View File
@@ -89,7 +89,7 @@ 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">
<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" />
{local.error && <p role="status" className="mb-2 text-xs text-destructive">{local.error}</p>}<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>
+2 -2
View File
@@ -75,13 +75,13 @@ function Detail({ id, initial }: { id: string; initial?: Decision }) {
return <><SheetHeader className="pr-12"><SheetTitle>{decision?.title ?? '读取决策…'}</SheetTitle><SheetDescription>{decision?.summary || decision?.question || '查看方案、检查结论与本决策咨询。'}</SheetDescription></SheetHeader>
<div className="min-h-0 flex-1 overflow-y-auto px-4 pb-5"><VersionNotice />{error && <p role="status" className="mb-4 text-sm text-destructive">{error} 保留已读取内容。<Button variant="link" onClick={() => void reload.current()}>重试</Button></p>}
{decision && draft ? <div className="grid min-w-0 gap-6 lg:grid-cols-[minmax(0,1.1fr)_minmax(0,1fr)]"><div className="min-w-0 space-y-5">
<p role="status" className="text-xs text-muted-foreground">{local.error || '本决策草稿保存在此浏览器,提交成功才清除。'}</p>{local.error && <Button variant="outline" size="sm" disabled={draft.sending} onClick={() => persist()}>重试保存草稿</Button>}<Button variant="ghost" size="sm" disabled={draft.sending} onClick={() => local.clear(true)}>清除本决策本地草稿(含旧版本)</Button>{!writable(decision) && (draft.comment || draft.option) && <section className="rounded-lg border p-3 text-sm"><h3>保留的本地草稿(只读,未自动提交)</h3><p>版本 {draft.revision} · 选项 {draft.option || '无'}</p><p className="whitespace-pre-wrap">{draft.comment}</p></section>}<div className="flex flex-wrap gap-2"><Status decision={decision} /><Badge variant="outline">版本 {revision}</Badge>{decision.task_id && <Link to={`/tasks?task=${encodeURIComponent(decision.task_id)}`} className="text-sm text-primary underline">{state.tasks.find(task => task.id === decision.task_id)?.name || decision.task_id}</Link>}</div>
{local.error && <p role="status" className="text-xs text-destructive">{local.error}</p>}{local.error && <Button variant="outline" size="sm" disabled={draft.sending} onClick={() => persist()}>重试保存草稿</Button>}{!writable(decision) && (draft.comment || draft.option) && <section className="rounded-lg border p-3 text-sm"><h3>保留的本地草稿(只读,未自动提交)</h3><p>版本 {draft.revision} · 选项 {draft.option || '无'}</p><p className="whitespace-pre-wrap">{draft.comment}</p></section>}<div className="flex flex-wrap gap-2"><Status decision={decision} /><Badge variant="outline">版本 {revision}</Badge>{decision.task_id && <Link to={`/tasks?task=${encodeURIComponent(decision.task_id)}`} className="text-sm text-primary underline">{state.tasks.find(task => task.id === decision.task_id)?.name || decision.task_id}</Link>}</div>
<Text label="需要决定的问题" text={decision.question} /><Text label="背景与依据" text={decision.details} /><Text label="影响" text={decision.impact} /><Text label="推荐与理由(不会替你选择)" text={decision.recommendation} />
{decision.deadline && Number.isFinite(Date.parse(decision.deadline)) && <p className="text-sm">截止时间:{date(decision.deadline)}</p>}
<p className="text-xs text-muted-foreground">选择提交后需检查;接受方案不代表权限授权,也不代表已经完成。</p>
{decision.options.length > 0 && <fieldset disabled={!editable || changed} className="space-y-2"><legend className="mb-2 font-medium">全部方案</legend>{decision.options.map(option => <label key={option.id} className="flex gap-3 rounded-lg border p-3"><input type="radio" name={`decision-${id}`} checked={draft.option === option.id} onChange={() => { draft.option = option.id; persist() }} className="mt-1 size-4 shrink-0 accent-primary" /><span><span className="font-medium">{option.label}</span><span className="mt-1 block whitespace-pre-wrap text-sm text-muted-foreground">{option.description}</span></span></label>)}</fieldset>}
{writable(decision) && <div className="space-y-3"><Button variant="ghost" size="sm" disabled={!editable || changed || !draft.option} onClick={() => { draft.option = null; persist() }}>清除选项,仅填写意见</Button><label className="block text-sm font-medium" htmlFor={`opinion-${id}`}>你的意见(可不选方案,直接填写)</label><Textarea id={`opinion-${id}`} disabled={!editable} value={draft.comment} onChange={event => { draft.comment = event.target.value; persist() }} />
{changed && <div className="space-y-2 rounded-lg border p-3"><p className="text-sm">请重新阅读当前版本;之前的选择不会自动作为本次确认。</p><Button variant="outline" disabled={draft.sending || Boolean(error)} onClick={() => { confirmDraft(decision, draft); persist(); setNotice('已确认当前版本,请重新选择方案或检查保留的意见后提交。'); redraw(n => n + 1) }}>我已阅读版本 {revision},重新确认</Button></div>}
{changed && <div className="space-y-2 rounded-lg border p-3"><p className="text-sm">请重新阅读当前版本;之前的选择不会自动作为本次确认。</p><Button variant="outline" disabled={draft.sending || Boolean(error)} onClick={() => { confirmDraft(decision, draft); persist(); setNotice(''); redraw(n => n + 1) }}>我已阅读版本 {revision},重新确认</Button></div>}
<Button disabled={!editable || changed || (!draft.option && !draft.comment.trim())} onClick={() => void submit()}>{draft.sending ? '正在提交…' : '提交本次选择或意见'}</Button>
</div>}
{notice && <p role="status" className="text-sm">{notice}</p>}