This repository has been archived on 2026-09-13. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
BPOS-MVP/apps/web/src/features/desk-task/internal/TaskChat.tsx
T
ChaiHaoandClaude Opus 5 c59e73754b 工作台空占位补齐 —— 有内容渲染内容,没内容才落空态
任务面原来缺两处占位:中栏是一行裸文字,对话区在没选中任务时**整栏不渲染**。
补齐后三栏恒在:中栏落设计空态「手上没有任务」,对话区给全壳(题头 +
空态 + 禁用输入条),空着也看得出这是干什么的地方。

但真起一单之后暴露了更重的问题:占位在**说谎**。待处理角标写着 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
2026-07-28 10:25:19 +08:00

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}
/>
);
}