返回 DeepSeek-Reasonix
ExtensionCard.tsx
根目录 / desktop / frontend / src / components / ExtensionCard.tsx
1 import { ErrorMessage } from "./ErrorMessage";
2 import { useState } from "react";
3 import { Puzzle } from "lucide-react";
4 import { app } from "../lib/bridge";
5 import { useT } from "../lib/i18n";
6 import { useToast } from "../lib/toast";
7 import type { ExtensionItem } from "../lib/useController";
8 import { Markdown } from "./Markdown";
9
10 // ExtensionCard renders one extension-published card surface in the
11 // transcript: title, markdown body (via the shared safe renderer), a key-value
12 // grid, an optional progress bar, and the action buttons declared in the
13 // extension's handshake. Action results surface inline and, when the extension
14 // returned a message, as a toast.
15 export function ExtensionCard({ item, tabId }: { item: ExtensionItem; tabId?: string }) {
16 const t = useT();
17 const { showToast } = useToast();
18 const [busyAction, setBusyAction] = useState<string | null>(null);
19 const [result, setResult] = useState<{ text: string; error: boolean } | null>(null);
20 const { card } = item;
21 const progress = typeof card.progress === "number" ? Math.max(0, Math.min(1, card.progress)) : undefined;
22
23 const invoke = async (actionId: string) => {
24 if (busyAction) return;
25 setBusyAction(actionId);
26 try {
27 const message = await app.InvokeExtensionAction(tabId ?? "", `/${item.pluginId}:${actionId}`, {});
28 const text = message || t("ext.action.done");
29 setResult({ text, error: false });
30 if (message) showToast(message, "info");
31 } catch (err) {
32 const text = err instanceof Error ? err.message : String(err);
33 setResult({ text, error: true });
34 showToast(text, "error");
35 } finally {
36 setBusyAction(null);
37 }
38 };
39
40 return (
41 <div className="extension-card" data-entrance={item.id} data-extension-surface={item.surfaceKey}>
42 <div className="extension-card__header">
43 <Puzzle size={14} className="extension-card__icon" aria-hidden="true" />
44 {card.title ? <span className="extension-card__title">{card.title}</span> : null}
45 <span className="extension-card__plugin">{item.pluginId}</span>
46 </div>
47 {card.markdown ? (
48 <div className="extension-card__body">
49 <Markdown text={card.markdown} />
50 </div>
51 ) : null}
52 {!card.markdown && card.text ? <div className="extension-card__body">{card.text}</div> : null}
53 {card.fields && card.fields.length > 0 ? (
54 <dl className="extension-card__fields">
55 {card.fields.map((field, index) => (
56 <div className="extension-card__field" key={`${field.key}-${index}`}>
57 <dt>{field.key}</dt>
58 <dd>{field.value}</dd>
59 </div>
60 ))}
61 </dl>
62 ) : null}
63 {progress !== undefined ? (
64 <div
65 className="extension-card__progress"
66 role="progressbar"
67 aria-label={t("ext.card.progress")}
68 aria-valuemin={0}
69 aria-valuemax={100}
70 aria-valuenow={Math.round(progress * 100)}
71 >
72 <div className="extension-card__progress-fill" style={{ width: `${Math.round(progress * 100)}%` }} />
73 </div>
74 ) : null}
75 {card.actions && card.actions.length > 0 ? (
76 <div className="extension-card__actions">
77 {card.actions.map((action) => (
78 <button
79 key={action.actionId}
80 type="button"
81 className="btn btn--small"
82 disabled={busyAction !== null}
83 onClick={() => void invoke(action.actionId)}
84 >
85 {action.label || action.actionId}
86 </button>
87 ))}
88 </div>
89 ) : null}
90 {result ? (
91 <div className={`extension-card__result${result.error ? " extension-card__result--error" : ""}`}>{result.error ? <ErrorMessage error={result.text} /> : result.text}</div>
92 ) : null}
93 </div>
94 );
95 }
96
96 lines Plain Text