console: React 生产级重写(左列表 + 中对话 + 右详情)

旧单页 console.html 换成 Vite + React + TypeScript 单文件构建产物
(309525 字节,sha 90345ec8…,重建字节一致),源码在
cloud/console-react/,改页面只改 src 再 npm run build:console,
产物不手改。布局:左侧列表(模块导航 + 任务列表)、中间标准对话
(实时链路时间线 + 继续会话聊天框,共用 Chat 组件)、右侧详情
(元数据、操作、评审、审批、协调子任务、子代理、线程)。
API 与后端语义未改,key 只存浏览器 localStorage。
6 个 console 标记单测随实现改名更新(意图不变:壳确实调了
这些接口、turn 信息仍在详情里、旧 goal 输入仍不存在);
P1 标记单测未动且通过。
This commit is contained in:
host committed 2026-10-29 06:32:38 +08:00
1 parent 3f4e849a93
commit ebc5624893
57 files changed
+4789 -1746

No files matched your search

@@ -0,0 +1,72 @@
import { useEffect, useRef, useState } from "react";
import type { ChatMessage } from "./mapTrace";
export function useAutoFollow(dep: unknown) {
const ref = useRef<HTMLDivElement>(null);
const [following, setFollowing] = useState(true);
const followingRef = useRef(true);
const onScroll = () => {
const el = ref.current;
if (!el) return;
const nearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
followingRef.current = nearBottom;
setFollowing(nearBottom);
};
useEffect(() => {
if (followingRef.current && ref.current) ref.current.scrollTop = ref.current.scrollHeight;
}, [dep]);
return { ref, following, onScroll };
}
export function ChatBubble({ msg }: { msg: ChatMessage }) {
if (msg.role === "status") {
return <div className="chat-status">{msg.text}</div>;
}
return (
<div className={"chat-row " + msg.role}>
<div className={"chat-bubble " + msg.role}>
{msg.label ? <div className="chat-label">{msg.label}</div> : null}
<div className="chat-text">{msg.text}</div>
</div>
</div>
);
}
export function ChatList({ messages, empty }: { messages: ChatMessage[]; empty?: string }) {
const { ref, following, onScroll } = useAutoFollow(messages.length);
return (
<div className="chat-list" ref={ref} onScroll={onScroll}>
{messages.length === 0 ? <div className="muted">{empty || "(暂无消息)"}</div> : messages.map((m) => <ChatBubble key={m.id} msg={m} />)}
{!following && messages.length > 0 ? (
<button className="chat-follow" onClick={() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }}>回到最新 ↓</button>
) : null}
</div>
);
}
export function ChatInput({ onSend, disabled, placeholder }: {
onSend: (text: string) => void; disabled?: boolean; placeholder?: string;
}) {
const [text, setText] = useState("");
const send = () => {
const v = text.trim();
if (!v || disabled) return;
onSend(v);
setText("");
};
return (
<div className="chat-input-bar">
<textarea
aria-label="会话输入"
className="chat-input"
rows={1}
placeholder={placeholder || "输入消息,回车发送,Shift+回车换行"}
value={text}
disabled={disabled}
onChange={(e) => setText(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); } }}
/>
<button className="primary chat-send" disabled={disabled || !text.trim()} onClick={send}>发送</button>
</div>
);
}