Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac8c154eff | ||
|
|
357502d1c9 |
@@ -174,4 +174,6 @@ description: 组织项目的多个 AI 会话并行工作,将进度、证据和
|
|||||||
|
|
||||||
沟通输入、决策意见与选择、决策咨询输入应在浏览器本地持久保存,按实例、目标会话、决策与内容版本隔离;退出页面、刷新及重新登录后恢复同范围草稿。草稿不是已提交事实,不上传草稿,不将token写入草稿记录。仅收到成功确认后清除对应提交的草稿,或由用户明确清除单项;失败和结果未知保留正文与幂等重试信息,禁止自动重发。内容版本变化保留旧稿供明确重确认,不能静默覆盖新版本或沿用旧选择作为批准。
|
沟通输入、决策意见与选择、决策咨询输入应在浏览器本地持久保存,按实例、目标会话、决策与内容版本隔离;退出页面、刷新及重新登录后恢复同范围草稿。草稿不是已提交事实,不上传草稿,不将token写入草稿记录。仅收到成功确认后清除对应提交的草稿,或由用户明确清除单项;失败和结果未知保留正文与幂等重试信息,禁止自动重发。内容版本变化保留旧稿供明确重确认,不能静默覆盖新版本或沿用旧选择作为批准。
|
||||||
|
|
||||||
本地存储不可用时如实提示,不能宣称已保存;本地浏览器数据被清理、设备更换或旧版未持久化的已丢稿不承诺恢复。提供明确的单项草稿清除入口。已有唯一人类入口时,相关限制与所需用户操作仍由该入口沟通。
|
本地存储不可用时如实提示,不能宣称已保存;本地浏览器数据被清理、设备更换或旧版未持久化的已丢稿不承诺恢复。已有唯一人类入口时,相关限制与所需用户操作仍由该入口沟通。
|
||||||
|
|
||||||
|
功能修复不得擅自增加按钮和解释性文案。草稿自动保存、恢复及成功后清除在后台完成,不放常驻存储说明、清除草稿按钮或冗余成功提示;仅真正阻止操作并需要人类处理的错误或版本冲突显示最短必要提示。移除控件不删除已有草稿或改变已提交决定。
|
||||||
|
|||||||
@@ -43,10 +43,10 @@ export default function DecisionChat({ decision, token }: { decision: Decision;
|
|||||||
finally { sending.current=false; draft.sending = false; redraw(n => n + 1) }
|
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>
|
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>
|
<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>}
|
{(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>}
|
{!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>
|
</section>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export default function VersionNotice() {
|
|||||||
const entry = document.querySelector<HTMLScriptElement>('script[type="module"][src]')?.getAttribute('src')
|
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 || entry !== state.frontend_entrypoint) {
|
||||||
if (!entry || !state.frontend_entrypoint) return null
|
if (!entry || !state.frontend_entrypoint) return null
|
||||||
return <Alert role="status"><AlertDescription>页面已有新版。输入处会显示本地保存状态。若提示保存失败,请先复制草稿并记下选择;确认已保存后可重新加载并登录恢复。仅刷新消息或状态不会更新页面;不会自动重载或重发你的输入。</AlertDescription></Alert>
|
return <Alert role="status"><AlertDescription>页面已更新,请重新加载并登录。</AlertDescription></Alert>
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
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 identity=JSON.stringify(scope), ref=useRef<{identity:string;value:T;error:string;loaded:boolean;persisted:string|null} | null>(null)
|
||||||
const [, redraw]=useState(0)
|
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!
|
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 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 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}
|
return {value:record.value,error:record.error,save,clear,ready:Boolean(scope)&&record.loaded}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>}
|
</article>) : <div className="flex h-full items-center justify-center text-sm text-muted-foreground">{known ? '有什么想了解的?从这里开始交流。' : '会话准备好后,即可在这里交流。'}</div>}
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={send} className="shrink-0 border-t p-3 sm:px-8">
|
<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>
|
<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>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -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>
|
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>}
|
<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">
|
{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} />
|
<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>}
|
{decision.deadline && Number.isFinite(Date.parse(decision.deadline)) && <p className="text-sm">截止时间:{date(decision.deadline)}</p>}
|
||||||
<p className="text-xs text-muted-foreground">选择提交后需检查;接受方案不代表权限授权,也不代表已经完成。</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>}
|
{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() }} />
|
{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>
|
<Button disabled={!editable || changed || (!draft.option && !draft.comment.trim())} onClick={() => void submit()}>{draft.sending ? '正在提交…' : '提交本次选择或意见'}</Button>
|
||||||
</div>}
|
</div>}
|
||||||
{notice && <p role="status" className="text-sm">{notice}</p>}
|
{notice && <p role="status" className="text-sm">{notice}</p>}
|
||||||
|
|||||||
@@ -83,8 +83,10 @@
|
|||||||
|
|
||||||
## 草稿保存与恢复反馈
|
## 草稿保存与恢复反馈
|
||||||
|
|
||||||
沟通、决策提交和独立咨询分别保存草稿,按实例与对应目标隔离;切页、刷新、重新登录后恢复。草稿状态与发送状态分开,失败或结果未知保留输入,不自动重发;明确成功后才清对应内容。提供当前单项“清除草稿”,不能清除其他决策/会话的输入。存储不可用明确提示尚未持久保存,不显示虚假的已保存状态。
|
沟通、决策提交和独立咨询分别保存草稿,按实例与对应目标隔离;切页、刷新、重新登录后恢复。草稿状态与发送状态分开,失败或结果未知保留输入,不自动重发;明确成功后才清对应内容。存储不可用明确提示尚未持久保存,不显示虚假的已保存状态。
|
||||||
|
|
||||||
决策版本改变显示旧稿与重确认提示,重新阅读后才允许使用新版本提交,不静默把旧选项视为批准。已完成决策只读历史与草稿处理不得触发提交。页面升级提示不得自动重载丢失正在编辑内容;无本地数据的旧丢稿不声称可恢复。草稿只在网页本地,不上传,不包含token。
|
决策版本改变显示旧稿与重确认提示,重新阅读后才允许使用新版本提交,不静默把旧选项视为批准。已完成决策只读历史与草稿处理不得触发提交。页面升级提示不得自动重载丢失正在编辑内容;无本地数据的旧丢稿不声称可恢复。草稿只在网页本地,不上传,不包含token。
|
||||||
|
|
||||||
单项清除按钮须说明范围:决策/咨询包含对应范围旧版本,沟通仅当前入口session;两种决策输入互不清除。存储失败时允许继续保留当前输入和重试保存,禁止发送并提示刷新/退出可能丢失。成功但发现新编辑时保留新稿并说明,不显示全部已清。恢复旧版本内容必须保留版本提示与显式重确认;本地保存提示不代表已发送、模型已回复或决策已完成。
|
存储失败时允许继续保留当前输入和重试保存,禁止发送并提示刷新/退出可能丢失。成功但发现新编辑时保留新稿,不增加成功提示。恢复旧版本内容必须保留版本提示与显式重确认;本地保存提示不代表已发送、模型已回复或决策已完成。
|
||||||
|
|
||||||
|
保持简洁:功能修复不擅自增加按钮和解释性文案。三类草稿均后台保存/恢复,不显示清草稿按钮、常驻本地保存说明或冗余成功提示。只在阻止操作的错误与版本冲突时提供最短必要提示,普通状态不占界面。移除控件不调用清除逻辑。
|
||||||
|
|||||||
@@ -178,4 +178,4 @@ POST /consultation-credential由普通凭据签发专用token,数据库只存h
|
|||||||
|
|
||||||
每次输入写入,POST前先持久保存attempt幂等信息;失败或结果未知保留,刷新后可由用户明确重试,不自动发送。读取/保存失败显示具体风险并阻止发送,可重试保存;清除失败提示旧稿可能残留,不应重复提交。成功只清对应当前版本草稿,比较已存值防迟到响应删掉随后编辑;咨询成功不清决策选择。只确认接口成功不表示模型已回复或业务验收完成。
|
每次输入写入,POST前先持久保存attempt幂等信息;失败或结果未知保留,刷新后可由用户明确重试,不自动发送。读取/保存失败显示具体风险并阻止发送,可重试保存;清除失败提示旧稿可能残留,不应重复提交。成功只清对应当前版本草稿,比较已存值防迟到响应删掉随后编辑;咨询成功不清决策选择。只确认接口成功不表示模型已回复或业务验收完成。
|
||||||
|
|
||||||
当前版本无记录时可读取同作用域最近旧版本草稿,保留旧revision,须用户明确重确认,决策旧选项清除后重新选择/检查意见。已完成决策的残留草稿只读,不自动重交。显式单项清除:沟通仅当前session;决策仅该决策所有版本;咨询仅该决策与咨询session范围所有版本,互不影响。清除留下无正文cleared标记防旧版本草稿重新出现。绑定session变化属于不同范围,不承诺自动跨绑定迁移。
|
当前版本无记录时可读取同作用域最近旧版本草稿,保留旧revision,须用户明确重确认,决策旧选项清除后重新选择/检查意见。已完成决策的残留草稿只读,不自动重交。页面不提供额外清草稿按钮,不因切页、隐藏控件或版本提示删除旧稿;确认成功仍用原有比较清除保护。绑定session变化属于不同范围,不承诺自动跨绑定迁移。
|
||||||
|
|||||||
Reference in New Issue
Block a user