| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473 |
- import { useCallback, useEffect, useState, useRef } from "react";
- import type { FC } from "react";
- import ReactMarkdown from "react-markdown";
- import { Modal, Form, Toast } from "@douyinfe/semi-ui";
- import JSZip from "jszip";
- import { traceApi } from "../../api/traceApi";
- import type { Goal } from "../../types/goal";
- import type { Message } from "../../types/message";
- import { AgentControlPanel } from "../AgentControlPanel/AgentControlPanel";
- import styles from "./TopBar.module.css";
- interface TopBarProps {
- selectedTraceId: string | null;
- selectedNode: Goal | Message | null;
- title: string;
- onTraceSelect: (traceId: string, title?: string) => void;
- onTraceCreated?: () => void;
- onMessageInserted?: () => void;
- }
- export const TopBar: FC<TopBarProps> = ({
- selectedTraceId,
- selectedNode,
- title,
- onTraceSelect,
- onTraceCreated,
- onMessageInserted,
- }) => {
- const [isModalVisible, setIsModalVisible] = useState(false);
- const [isInsertModalVisible, setIsInsertModalVisible] = useState(false);
- const [isReflectModalVisible, setIsReflectModalVisible] = useState(false);
- const [isExperienceModalVisible, setIsExperienceModalVisible] = useState(false);
- const [experienceContent, setExperienceContent] = useState("");
- const [exampleProjects, setExampleProjects] = useState<Array<{ name: string; path: string; has_prompt: boolean }>>(
- [],
- );
- const [isUploading, setIsUploading] = useState(false);
- // 控制中心面板
- const [isControlPanelVisible, setIsControlPanelVisible] = useState(false);
- const formApiRef = useRef<{
- getValues: () => { system_prompt?: string; user_prompt?: string; example_project?: string };
- setValue: (field: "system_prompt" | "user_prompt" | "example_project", value: string) => void;
- } | null>(null);
- const insertFormApiRef = useRef<{ getValues: () => { insert_prompt: string } } | null>(null);
- const reflectFormApiRef = useRef<{ getValues: () => { reflect_focus: string } } | null>(null);
- const isMessageNode = (node: Goal | Message): node is Message =>
- "message_id" in node || "role" in node || "content" in node || "goal_id" in node || "tokens" in node;
- const getMessageId = (node: Message) => {
- if (typeof node.message_id === "string" && node.message_id) return node.message_id;
- if (typeof node.id === "string" && node.id) return node.id;
- return null;
- };
- const loadTraces = useCallback(
- async (status?: string) => {
- try {
- const data = await traceApi.fetchTraces({
- status: status || undefined,
- limit: 20,
- });
- // 初始加载时不自动选择,或者如果 selectedTraceId 为空才选择第一个
- if (!selectedTraceId && data.traces.length > 0) {
- const firstTrace = data.traces[0];
- const traceId = firstTrace?.parent_trace_id || firstTrace.trace_id;
- onTraceSelect(traceId, firstTrace.task);
- } else if (data.traces.length === 0) {
- onTraceSelect("", "流程图可视化系统");
- }
- } catch (error) {
- console.error("Failed to load traces:", error);
- Toast.error("加载任务列表失败");
- }
- },
- [onTraceSelect, selectedTraceId],
- );
- useEffect(() => {
- loadTraces();
- }, [loadTraces]);
- const handleNewTask = async () => {
- setIsModalVisible(true);
- // 加载 example 项目列表
- try {
- const data = await traceApi.fetchExamples();
- setExampleProjects(data.projects);
- } catch (error) {
- console.error("Failed to load examples:", error);
- }
- };
- const handleExampleChange = async (value: string) => {
- if (!value) return;
- try {
- const promptData = await traceApi.fetchExamplePrompt(value);
- // 自动填充表单
- if (formApiRef.current) {
- formApiRef.current.setValue("system_prompt", promptData.system_prompt);
- formApiRef.current.setValue("user_prompt", promptData.user_prompt);
- }
- } catch (error) {
- // 若某个 example 没有 production.prompt,只清空即可,不需阻断
- if (formApiRef.current) {
- formApiRef.current.setValue("system_prompt", "");
- formApiRef.current.setValue("user_prompt", "");
- }
- }
- };
- const handleConfirm = async () => {
- try {
- const values = formApiRef.current?.getValues();
- if (!values) return;
- const messages: Array<{ role: "system" | "user"; content: string }> = [];
- if (values.system_prompt) {
- messages.push({ role: "system", content: values.system_prompt });
- }
- if (values.user_prompt) {
- messages.push({ role: "user", content: values.user_prompt });
- }
- const payload: Parameters<typeof traceApi.createTrace>[0] = { messages };
- if (values.example_project) {
- payload.project_name = values.example_project;
- }
- const created = await traceApi.createTrace(payload);
- const nextTitle =
- (typeof values.user_prompt === "string" && values.user_prompt.trim()
- ? values.user_prompt.trim().split("\n")[0]
- : "新任务") || "新任务";
- onTraceSelect(created.trace_id, nextTitle);
- await loadTraces();
- onTraceCreated?.();
- setIsModalVisible(false);
- Toast.success("创建成功");
- } catch (error) {
- console.error("Failed to create trace:", error);
- Toast.error("创建失败");
- }
- };
- const handleRun = () => {
- if (!selectedNode) {
- Toast.warning("请选择插入节点");
- return;
- }
- if (!isMessageNode(selectedNode)) {
- Toast.warning("插入位置错误");
- return;
- }
- setIsInsertModalVisible(true);
- };
- const handleInsertConfirm = async () => {
- const node = selectedNode;
- if (!node) {
- Toast.warning("请选择插入节点");
- return;
- }
- if (!selectedTraceId) {
- Toast.warning("请先选择一个 Trace");
- return;
- }
- if (!isMessageNode(node)) {
- Toast.warning("插入位置错误");
- return;
- }
- const values = insertFormApiRef.current?.getValues();
- const insertPrompt = values?.insert_prompt?.trim();
- if (!insertPrompt) {
- Toast.warning("请输入指令");
- return;
- }
- const messageId = getMessageId(node);
- if (!messageId) {
- Toast.error("消息ID缺失");
- return;
- }
- try {
- const payload = {
- messages: [{ role: "user" as const, content: insertPrompt }],
- after_message_id: messageId,
- };
- await traceApi.runTrace(selectedTraceId, payload);
- Toast.success("插入成功");
- setIsInsertModalVisible(false);
- onMessageInserted?.();
- } catch (error) {
- console.error("Failed to run trace:", error);
- Toast.error("插入失败");
- }
- };
- const handleReflect = async () => {
- if (!selectedTraceId) {
- Toast.warning("请先选择一个 Trace");
- return;
- }
- setIsReflectModalVisible(true);
- };
- const handleReflectConfirm = async () => {
- if (!selectedTraceId) {
- Toast.warning("请先选择一个 Trace");
- return;
- }
- const values = reflectFormApiRef.current?.getValues();
- const focus = values?.reflect_focus?.trim();
- try {
- await traceApi.reflectTrace(selectedTraceId, focus ? { focus } : {});
- Toast.success("已触发反思");
- setIsReflectModalVisible(false);
- } catch (error) {
- console.error("Failed to reflect trace:", error);
- Toast.error("反思请求失败");
- }
- };
- const handleExperience = async () => {
- try {
- const content = await traceApi.getExperiences();
- // 尝试解析 JSON 格式
- let displayContent = typeof content === "string" ? content : JSON.stringify(content, null, 2);
- try {
- const parsed = typeof content === "string" ? JSON.parse(content) : content;
- if (parsed && typeof parsed === "object" && "content" in parsed) {
- displayContent = parsed.content;
- }
- } catch (e) {
- // 解析失败则使用原始字符串
- }
- setExperienceContent(displayContent);
- setIsExperienceModalVisible(true);
- } catch (error) {
- console.error("Failed to get experiences:", error);
- Toast.error("获取经验失败");
- }
- };
- const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
- const files = event.target.files;
- if (!files || files.length === 0) return;
- setIsUploading(true);
- try {
- // 创建 ZIP 文件
- const zip = new JSZip();
- // 将所有文件添加到 ZIP
- for (let i = 0; i < files.length; i++) {
- const file = files[i];
- // 使用 webkitRelativePath 保持文件夹结构
- const path = file.webkitRelativePath || file.name;
- zip.file(path, file);
- }
- // 生成 ZIP blob
- Toast.info("正在压缩文件...");
- const zipBlob = await zip.generateAsync({ type: "blob" });
- // 创建 File 对象
- const zipFile = new File([zipBlob], "traces.zip", { type: "application/zip" });
- // 上传
- Toast.info("正在上传...");
- const result = await traceApi.uploadTraces(zipFile);
- if (result.success) {
- Toast.success(result.message);
- // 刷新 trace 列表
- loadTraces();
- if (onTraceCreated) {
- onTraceCreated();
- }
- } else {
- Toast.warning(result.message);
- }
- // 显示详细信息
- if (result.failed_traces.length > 0) {
- console.warn("Failed traces:", result.failed_traces);
- }
- } catch (error) {
- console.error("Failed to upload traces:", error);
- Toast.error("上传失败");
- } finally {
- setIsUploading(false);
- // 清空 input,允许重复上传同一文件
- event.target.value = "";
- }
- };
- return (
- <>
- <header className={styles.topbar}>
- <div
- className={styles.title}
- title={title}
- >
- <h1>{title}</h1>
- </div>
- <div className={styles.actions}>
- <button
- className={`${styles.button} ${styles.success}`}
- onClick={handleNewTask}
- >
- 新任务
- </button>
- <button
- className={`${styles.button} ${styles.primary}`}
- onClick={handleRun}
- >
- 插入
- </button>
- <button
- className={`${styles.button} ${styles.primary}`}
- onClick={() => {
- if (!selectedTraceId) { Toast.warning("请先选择一个 Trace"); return; }
- setIsControlPanelVisible(true);
- }}
- >
- 🎛 控制中心
- </button>
- <button
- className={styles.button}
- onClick={handleReflect}
- >
- 反思
- </button>
- <button
- className={`${styles.button} ${styles.warning}`}
- onClick={handleExperience}
- >
- 经验
- </button>
- <label className={`${styles.button} ${styles.info}`} style={{ cursor: 'pointer' }}>
- {isUploading ? "上传中..." : "📤 导入"}
- <input
- type="file"
- webkitdirectory=""
- directory=""
- multiple
- onChange={handleUpload}
- disabled={isUploading}
- style={{ display: 'none' }}
- />
- </label>
- </div>
- <Modal
- title={<div className="w-full text-center">新建任务</div>}
- visible={isModalVisible}
- onOk={handleConfirm}
- onCancel={() => setIsModalVisible(false)}
- centered
- style={{ width: 600 }}
- >
- <Form
- getFormApi={(api: unknown) => {
- formApiRef.current = api as unknown as NonNullable<typeof formApiRef.current>;
- }}
- >
- <Form.Select
- field="example_project"
- label="选择示例项目(可选)"
- placeholder="选择一个示例项目自动填充"
- style={{ width: "100%" }}
- onChange={handleExampleChange}
- showClear
- >
- {exampleProjects.map((project) => (
- <Form.Select.Option
- key={project.name}
- value={project.name}
- >
- {project.name}
- </Form.Select.Option>
- ))}
- </Form.Select>
- <Form.TextArea
- field="system_prompt"
- label="System Prompt"
- placeholder="请输入 System Prompt"
- autosize={{ minRows: 3, maxRows: 6 }}
- />
- <Form.TextArea
- field="user_prompt"
- label="User Prompt"
- placeholder="请输入 User Prompt"
- autosize={{ minRows: 3, maxRows: 6 }}
- />
- </Form>
- </Modal>
- <Modal
- title={<div className="w-full text-center">插入指令</div>}
- visible={isInsertModalVisible}
- onOk={handleInsertConfirm}
- onCancel={() => setIsInsertModalVisible(false)}
- centered
- style={{ width: 600 }}
- >
- <Form
- getFormApi={(api: unknown) => {
- insertFormApiRef.current = api as unknown as NonNullable<typeof insertFormApiRef.current>;
- }}
- >
- <Form.TextArea
- field="insert_prompt"
- label=" "
- placeholder="请输入插入指令"
- autosize={{ minRows: 3, maxRows: 6 }}
- />
- </Form>
- </Modal>
- <Modal
- title={<div className="w-full text-center">反思</div>}
- visible={isReflectModalVisible}
- onOk={handleReflectConfirm}
- onCancel={() => setIsReflectModalVisible(false)}
- centered
- style={{ width: 600 }}
- >
- <Form
- getFormApi={(api: unknown) => {
- reflectFormApiRef.current = api as unknown as NonNullable<typeof reflectFormApiRef.current>;
- }}
- >
- <Form.TextArea
- field="reflect_focus"
- label=" "
- placeholder="请输入反思重点(可选)"
- autosize={{ minRows: 3, maxRows: 6 }}
- />
- </Form>
- </Modal>
- <Modal
- title={<div className="w-full text-center">经验列表</div>}
- visible={isExperienceModalVisible}
- onCancel={() => setIsExperienceModalVisible(false)}
- footer={null}
- centered
- style={{ width: 800 }}
- bodyStyle={{ maxHeight: "70vh", overflow: "auto" }}
- >
- <div style={{ whiteSpace: "pre-wrap", wordWrap: "break-word" }}>
- {experienceContent ? <ReactMarkdown>{experienceContent}</ReactMarkdown> : "暂无经验数据"}
- </div>
- </Modal>
- </header>
- {
- isControlPanelVisible && (
- <AgentControlPanel
- traceId={selectedTraceId}
- onClose={() => setIsControlPanelVisible(false)}
- onMessageInserted={onMessageInserted}
- />
- )
- }
- </>
- );
- };
|