返回 DeepSeek-Reasonix
ProcessCard.tsx
根目录 / desktop / frontend / src / components / ProcessCard.tsx
1 import { Children, type ReactNode, type SVGProps, useState } from "react";
2
3 type IconProps = SVGProps<SVGSVGElement> & { size?: number };
4 export type ProcessTone = "default" | "success" | "warning" | "danger" | "accent" | "violet";
5 export type ProcessState = "running" | "done" | "failed" | "waiting" | "stopped";
6
7 function ProcessIcon({ size = 14, children, ...rest }: IconProps & { children: ReactNode }) {
8 return (
9 <svg
10 xmlns="http://www.w3.org/2000/svg"
11 width={size}
12 height={size}
13 viewBox="0 0 24 24"
14 fill="none"
15 stroke="currentColor"
16 strokeWidth="1.8"
17 strokeLinecap="round"
18 strokeLinejoin="round"
19 {...rest}
20 >
21 {children}
22 </svg>
23 );
24 }
25
26 export function ProcessChevronIcon(props: IconProps) {
27 return (
28 <ProcessIcon {...props}>
29 <path d="m6 9 6 6 6-6" />
30 </ProcessIcon>
31 );
32 }
33
34 export function ProcessCheckIcon(props: IconProps) {
35 return (
36 <ProcessIcon {...props}>
37 <path d="m5 12 5 5L20 7" />
38 </ProcessIcon>
39 );
40 }
41
42 export function ProcessXIcon(props: IconProps) {
43 return (
44 <ProcessIcon {...props}>
45 <path d="M6 6l12 12M18 6 6 18" />
46 </ProcessIcon>
47 );
48 }
49
50 export function ProcessBrainIcon(props: IconProps) {
51 return (
52 <ProcessIcon {...props}>
53 <path d="M9 4a3 3 0 0 0-3 3v0a3 3 0 0 0-2 5 3 3 0 0 0 2 5 3 3 0 0 0 3 3h0a3 3 0 0 0 3-3V4" />
54 <path d="M15 4a3 3 0 0 1 3 3 3 3 0 0 1 2 5 3 3 0 0 1-2 5 3 3 0 0 1-3 3" />
55 </ProcessIcon>
56 );
57 }
58
59 export function ProcessToolIcon(props: IconProps) {
60 return (
61 <ProcessIcon {...props}>
62 <path d="M14 7a4 4 0 1 0 4 4l3 3-3 3-3-3a4 4 0 0 1-4-4l-3-3-3 3 3 3a4 4 0 0 0 6 0" />
63 </ProcessIcon>
64 );
65 }
66
67 export function ProcessInfoIcon(props: IconProps) {
68 return (
69 <ProcessIcon {...props}>
70 <circle cx="12" cy="12" r="9" />
71 <path d="M12 16v-4" />
72 <path d="M12 8h.01" />
73 </ProcessIcon>
74 );
75 }
76
77 export function ProcessPhaseIcon(props: IconProps) {
78 return (
79 <ProcessIcon {...props}>
80 <path d="M4 7h9" />
81 <path d="M4 12h13" />
82 <path d="M4 17h7" />
83 <path d="m17 7 3 3-3 3" />
84 </ProcessIcon>
85 );
86 }
87
88 export function ProcessCompactIcon(props: IconProps) {
89 return (
90 <ProcessIcon {...props}>
91 <path d="M8 4h8" />
92 <path d="M6 8h12" />
93 <rect x="4" y="12" width="16" height="8" rx="2" />
94 <path d="M8 16h8" />
95 </ProcessIcon>
96 );
97 }
98
99 export function ProcessStatusIcon({ state, label }: { state: ProcessState; label: string }) {
100 if (state === "running") return <span className="process-card__spin" role="img" aria-label={label} title={label} />;
101 if (state === "done") return <ProcessCheckIcon className="process-card__status process-card__status--done" size={12} aria-label={label} />;
102 if (state === "failed") return <ProcessXIcon className="process-card__status process-card__status--failed" size={12} aria-label={label} />;
103 return <span className={`process-card__dot process-card__dot--${state}`} role="img" aria-label={label} title={label} />;
104 }
105
106 export function ProcessCard({
107 tone = "default",
108 icon,
109 kind,
110 name,
111 meta,
112 defaultOpen = false,
113 open,
114 onOpenChange,
115 children,
116 className,
117 }: {
118 tone?: ProcessTone;
119 icon: ReactNode;
120 kind: ReactNode;
121 name?: ReactNode;
122 meta?: ReactNode;
123 defaultOpen?: boolean;
124 open?: boolean;
125 onOpenChange?: (open: boolean) => void;
126 children?: ReactNode;
127 className?: string;
128 }) {
129 const [internalOpen, setInternalOpen] = useState(defaultOpen);
130 const actualOpen = open ?? internalOpen;
131 const hasBody = Children.count(children) > 0;
132 const toggle = () => {
133 if (!hasBody) return;
134 const next = !actualOpen;
135 if (open === undefined) setInternalOpen(next);
136 onOpenChange?.(next);
137 };
138
139 return (
140 <div className={`process-card${className ? ` ${className}` : ""}`} data-tone={tone} data-open={actualOpen} data-has-body={hasBody}>
141 <button
142 type="button"
143 className="process-card__head"
144 onClick={toggle}
145 onKeyDown={(e) => { if (e.key === "Escape") { e.preventDefault(); toggle(); } }}
146 aria-expanded={hasBody ? actualOpen : undefined}
147 >
148 <span className="process-card__icon">{icon}</span>
149 <span className="process-card__kind">{kind}</span>
150 {name && <span className="process-card__name">{name}</span>}
151 <span className="process-card__grow" />
152 {meta && <span className="process-card__meta">{meta}</span>}
153 {hasBody && (
154 <span className="process-card__chevron">
155 <ProcessChevronIcon size={12} />
156 </span>
157 )}
158 </button>
159 {hasBody && (
160 <div className="process-card__wrap" aria-hidden={!actualOpen}>
161 <div>
162 <div className="process-card__body">{children}</div>
163 </div>
164 </div>
165 )}
166 </div>
167 );
168 }
169
169 lines Plain Text