任务面原来缺两处占位:中栏是一行裸文字,对话区在没选中任务时**整栏不渲染**。 补齐后三栏恒在:中栏落设计空态「手上没有任务」,对话区给全壳(题头 + 空态 + 禁用输入条),空着也看得出这是干什么的地方。 但真起一单之后暴露了更重的问题:占位在**说谎**。待处理角标写着 1,正文却 说「手上没有待办」—— 因为 ChatStream(643 行)与 WorkOrderReport(449 行) 早就写好、也从 @features/desk-task 导出了,却没有一处在用。占位不是缺内容, 是内容没接上。三处接回真渲染: 待处理 → WorkOrderReport(工单正文、编号、派发时间、数据与分析) 工作日志 → .jc-log 行,按归侧四类各一色(机器/人类/裁决/等世界) 工作成果 → .jc-file 行,点开进 FileViewer;打开时右槽换「返回」 对话区 → ChatStream,三个回传(说一句 / 答卡 / 答工单)在这里收口 顺带治了窄对话区的抗压:工作区优先保「五按钮 + 翻页」,对话区在 1280 下被 压到 212px,卡头标题 `flex:1` 没设 `min-width:0`,一挤就逐字折断成竖排单字。 降级顺序改成「状态芯片换行下沉、标题始终整词可读」—— 截断能让布局不崩, 但读的人不知道这是什么卡。不改任何一栏尺寸。 五闸:tsc ✓ eslint ✓ depcruise 373 模块 ✓ api:check 公开契约无变化 ✓ 481 测试 ✓ 走查后清了运行数据(仓库自带 reset-runs,配置原样留着),换业务化主题重起一单。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
96 lines
3.6 KiB
TypeScript
96 lines
3.6 KiB
TypeScript
/**
|
|
* 对话区 —— 「对话流是**一切回环输入的唯一入口**」(OS 4.1)。
|
|
*
|
|
* 壳(`jc-tch` 题头 / 滚动区 / composer)由 `ChatStream` 自己带;这里只做取数与回传:
|
|
* 说一句、答卡、答工单,回传后按键失效,画面由服务端重投影(前端零过程状态)。
|
|
*
|
|
* **有内容就渲染内容,没内容才落空态** —— 空对话是这一单刚开始时的常态,不是缺陷;
|
|
* 但一旦真有话,占位就变成了谎:角标说有、正文说没有,人会以为坏了。
|
|
*/
|
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import type { ChatCardReply, TaskDetail, WorkOrderReply } from '@contracts';
|
|
import { replyChatCardRoute, replyWorkOrderRoute, sayRoute } from '@contracts';
|
|
import { call } from '@web-shared/typed-fetch.ts';
|
|
import { Ico } from '@web-shared/Ico.tsx';
|
|
import { EmptyState } from '@web-shared/desk/module-kit.tsx';
|
|
import { ChatStream } from './chat-bubbles.tsx';
|
|
|
|
export function TaskChat({ detail, onLocate }: { detail: TaskDetail; onLocate: (orderId: string) => void }) {
|
|
const queryClient = useQueryClient();
|
|
const taskId = detail.card.id;
|
|
|
|
/** 回传后一律重取任务投影 —— 卡与工单的状态由服务端说,前端不自行推演 */
|
|
const refresh = () => {
|
|
void queryClient.invalidateQueries({ queryKey: ['desk', 'task', taskId] });
|
|
void queryClient.invalidateQueries({ queryKey: ['desk', 'tasks'] });
|
|
};
|
|
|
|
const say = useMutation({
|
|
mutationFn: (text: string) => call(sayRoute, { params: { taskId }, body: { text } }),
|
|
onSuccess: refresh,
|
|
});
|
|
const replyCard = useMutation({
|
|
mutationFn: (reply: ChatCardReply) => call(replyChatCardRoute, { body: { reply } }),
|
|
onSuccess: refresh,
|
|
});
|
|
const replyOrder = useMutation({
|
|
mutationFn: (reply: WorkOrderReply) => call(replyWorkOrderRoute, { body: { reply } }),
|
|
onSuccess: refresh,
|
|
});
|
|
|
|
if (detail.chat.length === 0) {
|
|
return (
|
|
<div className="jc-tch">
|
|
<header className="jc-tch-head">
|
|
<span className="jc-tch-mark">
|
|
<Ico name="bot" s={15} />
|
|
</span>
|
|
<b className="jc-tch-name">任务助理</b>
|
|
<em className="jc-tch-tag">Agent</em>
|
|
<span className="jc-tch-desc">盯着这单推进</span>
|
|
</header>
|
|
<div className="jc-tch-scroll">
|
|
<EmptyState
|
|
ico="messages-square"
|
|
tone="brand"
|
|
title="还没有消息"
|
|
desc="AI 有结论、要派工、或者要你拿个主意,都会在这里说。你也可以随时交代两句。"
|
|
/>
|
|
</div>
|
|
<div className="copilot-composer session-composer">
|
|
<div className="composer-row">
|
|
<textarea
|
|
rows={2}
|
|
placeholder="有什么要补充或交代的?"
|
|
onKeyDown={(e) => {
|
|
if (e.key !== 'Enter' || e.shiftKey) return;
|
|
e.preventDefault();
|
|
const text = e.currentTarget.value.trim();
|
|
if (text !== '') {
|
|
say.mutate(text);
|
|
e.currentTarget.value = '';
|
|
}
|
|
}}
|
|
/>
|
|
<button className="copilot-send" disabled={say.isPending}>
|
|
<Ico name="send" s={14} /> 发送
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ChatStream
|
|
chat={detail.chat}
|
|
orders={detail.workOrders}
|
|
thinking={say.isPending}
|
|
onSay={(text) => say.mutate(text)}
|
|
onReplyCard={(reply) => replyCard.mutate(reply)}
|
|
onReplyOrder={(reply) => replyOrder.mutate(reply)}
|
|
onLocate={onLocate}
|
|
/>
|
|
);
|
|
}
|