返回 DeepSeek-Reasonix
ReadOnlyBatch.tsx
根目录 / desktop / frontend / src / components / ReadOnlyBatch.tsx
1 import { memo, useEffect, useRef, useState } from "react";
2 import { ChevronRight } from "lucide-react";
3 import { useT } from "../lib/i18n";
4 import { useCollapseAnimation } from "../lib/useCollapseAnimation";
5 import type { Item } from "../lib/useController";
6 import { ToolCard } from "./ToolCard";
7 import { useWorkProcessPresentation } from "../lib/sessionExperience";
8
9 type ToolItem = Extract<Item, { kind: "tool" }>;
10
11 type ReadOnlyBatchProps = {
12 items: ToolItem[];
13 subcalls: ReadonlyMap<string, ToolItem[]>;
14 tabId?: string;
15 };
16
17 export const ReadOnlyBatch = memo(function ReadOnlyBatch({ items, subcalls, tabId }: ReadOnlyBatchProps) {
18 const t = useT();
19 const presentation = useWorkProcessPresentation();
20 const [open, setOpen] = useState(presentation.keepExpandedAfterCompletion);
21 const userOverridden = useRef(false);
22 const previousExperience = useRef(presentation.experience);
23 useEffect(() => {
24 if (previousExperience.current === presentation.experience) return;
25 previousExperience.current = presentation.experience;
26 userOverridden.current = false;
27 setOpen(presentation.keepExpandedAfterCompletion);
28 }, [presentation.experience, presentation.keepExpandedAfterCompletion]);
29 const bodyRef = useRef<HTMLDivElement>(null);
30 useCollapseAnimation(bodyRef, open);
31
32 const readCount = items.filter((it) => it.name === "read_file" || it.name === "ls").length;
33 const searchCount = items.filter((it) => it.name === "grep" || it.name === "glob" || it.name === "web_fetch").length;
34
35 const parts: string[] = [];
36 if (readCount > 0) parts.push(t("tool.readCount", { n: readCount }));
37 if (searchCount > 0) parts.push(t("tool.searchCount", { n: searchCount }));
38 const otherCount = items.length - readCount - searchCount;
39 if (otherCount > 0) parts.push(t("tool.otherReadCount", { n: otherCount }));
40 const label = parts.join(" · ");
41
42 if (!label || items.length === 0) return null;
43
44 return (
45 <div
46 className={`readonly-batch${open ? " readonly-batch--open" : ""}`}
47 data-entrance={items[0]?.id}
48 data-transcript-layout-variant={open ? "tool-batch-expanded" : "tool-batch-collapsed"}
49 >
50 <button type="button" className="reasoning__head" onClick={() => { userOverridden.current = true; setOpen((v) => !v); }} aria-expanded={open}>
51 <ChevronRight className={`reasoning__chevron${open ? " reasoning__chevron--open" : ""}`} size={12} />
52 <span className="readonly-batch__label" data-creation-label={t("creation.toolCallsLabel")}>{label}</span>
53 </button>
54 <div ref={bodyRef} className="readonly-batch__body">
55 {items.map((it) => (
56 <ToolCard key={it.id} item={it} subcalls={subcalls.get(it.id)} tabId={tabId} />
57 ))}
58 </div>
59 </div>
60 );
61 });
62
62 lines Plain Text