diff --git a/client/electron/src/components/MailView.tsx b/client/electron/src/components/MailView.tsx index 98e962d..6f9ec6e 100644 --- a/client/electron/src/components/MailView.tsx +++ b/client/electron/src/components/MailView.tsx @@ -587,27 +587,34 @@ function ThreadCard({ mail, onForward }: { mail: Mail; onForward?: () => void }) } /** - * 权限请求的决策面板。 + * 待办面板:审批型(permission)与主动提问(question)共用入口。 + * + * 两类待办的渲染与提交语义完全不同: + * - permission:点「同意/拒绝」当场放行或拦下一个危险操作 + * - question:模型缺信息,人**回答问题**(勾选预设选项 + 自由文本) + * + * 混用一套 UI 的后果很具体:一个问「配置文件叫什么」的问题会被渲染成 + * 「同意 / 拒绝」,人只能点个毫无意义的按钮,模型拿到「同意」当答案。 * * 导出供测试单独渲染:通过整个 MailView 渲染它需要先把 mailStore 与 * sessionStore 摆到「当前正看着一封 permission_request 邮件」的状态, * 那些铺垫与这个组件本身的行为无关。 */ export function PermissionPanel({ mail }: { mail: Mail }) { + const isQuestion = mail.permission_kind === 'question'; const [note, setNote] = useState(''); const [busy, setBusy] = useState(false); const [decided, setDecided] = useState(mail.permission_result || ''); + // 问题模式下已勾选的选项(多选时是多个)。 + const [picked, setPicked] = useState([]); const fetchInbox = useMailStore(s => s.fetchInbox); const selectSession = useSessionStore(s => s.selectSession); - const options = mail.permission_options?.length ? mail.permission_options : ['同意', '拒绝']; - const isApprove = (s: string) => /同意|允许|批准|approve|yes/i.test(s); - - const decide = async (choice: string) => { + const submit = async (decision: string, noteText: string) => { setBusy(true); try { - await api.decidePermission(mail.mail_id, choice, note || undefined); - setDecided(choice); + await api.decidePermission(mail.mail_id, decision, noteText || undefined); + setDecided(decision || '(自由文本回答)'); await fetchInbox('all'); if (mail.session_id) selectSession(mail.session_id); } catch (err) { @@ -620,18 +627,93 @@ export function PermissionPanel({ mail }: { mail: Mail }) { if (decided) { return (
- 已处理:{decided} + 已处理: + {decided}
); } + // ─── 主动提问:勾选 + 自由文本 ─── + if (isQuestion) { + const options = mail.permission_options ?? []; + const multi = mail.permission_multi_select === true; + const toggle = (opt: string) => { + setPicked(prev => { + if (multi) return prev.includes(opt) ? prev.filter(p => p !== opt) : [...prev, opt]; + // 单选:再点同一项则取消,否则替换 + return prev.includes(opt) ? [] : [opt]; + }); + }; + // 回答必须非空:空提交会让模型拿到一个什么都没说的结果继续跑。 + const blank = picked.length === 0 && note.trim().length === 0; + + return ( +
+
+ {options.length === 0 + ? '这题没有预设选项,请直接填写回答:' + : multi ? '可多选,也可补充说明:' : '请选择一项,也可补充说明:'} +
+ + {options.length > 0 && ( +
+ {options.map(opt => { + const on = picked.includes(opt); + return ( + + ); + })} +
+ )} + +