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