@nastechai/agent 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/eslint.config.js +23 -0
- package/index.html +24 -0
- package/package.json +54 -26
- package/package.json.bak +89 -0
- package/package.json.pub +88 -0
- package/src/App.tsx +1173 -0
- package/src/components/AuthWidget.tsx +150 -0
- package/src/components/AutoField.tsx +206 -0
- package/src/components/Backdrop.tsx +93 -0
- package/src/components/ChatSidebar.tsx +394 -0
- package/src/components/DeleteConfirmDialog.tsx +40 -0
- package/src/components/LanguageSwitcher.tsx +186 -0
- package/src/components/Markdown.tsx +383 -0
- package/src/components/ModelInfoCard.tsx +112 -0
- package/src/components/ModelPickerDialog.tsx +470 -0
- package/src/components/OAuthLoginModal.tsx +374 -0
- package/src/components/OAuthProvidersCard.tsx +287 -0
- package/src/components/PlatformsCard.tsx +97 -0
- package/src/components/ScheduleBuilder.tsx +273 -0
- package/src/components/SidebarFooter.tsx +42 -0
- package/src/components/SidebarStatusStrip.tsx +72 -0
- package/src/components/SlashPopover.tsx +171 -0
- package/src/components/ThemeSwitcher.tsx +243 -0
- package/src/components/ToolCall.tsx +228 -0
- package/src/components/ToolsetConfigDrawer.tsx +448 -0
- package/src/contexts/PageHeaderProvider.tsx +139 -0
- package/src/contexts/SystemActions.tsx +120 -0
- package/src/contexts/page-header-context.ts +12 -0
- package/src/contexts/system-actions-context.ts +18 -0
- package/src/contexts/usePageHeader.ts +10 -0
- package/src/contexts/useSystemActions.ts +15 -0
- package/src/hooks/useModalBehavior.ts +44 -0
- package/src/hooks/useSidebarStatus.ts +27 -0
- package/src/i18n/af.ts +702 -0
- package/src/i18n/context.tsx +123 -0
- package/src/i18n/de.ts +701 -0
- package/src/i18n/en.ts +708 -0
- package/src/i18n/es.ts +701 -0
- package/src/i18n/fr.ts +701 -0
- package/src/i18n/ga.ts +702 -0
- package/src/i18n/hu.ts +702 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/it.ts +701 -0
- package/src/i18n/ja.ts +702 -0
- package/src/i18n/ko.ts +702 -0
- package/src/i18n/pt.ts +702 -0
- package/src/i18n/ru.ts +702 -0
- package/src/i18n/tr.ts +702 -0
- package/src/i18n/types.ts +710 -0
- package/src/i18n/uk.ts +702 -0
- package/src/i18n/zh-hant.ts +702 -0
- package/src/i18n/zh.ts +698 -0
- package/src/index.css +274 -0
- package/src/lib/api.ts +1585 -0
- package/src/lib/dashboard-flags.ts +15 -0
- package/src/lib/format.ts +9 -0
- package/src/lib/fuzzy.ts +192 -0
- package/src/lib/gatewayClient.ts +253 -0
- package/src/lib/nested.ts +23 -0
- package/src/lib/resolve-page-title.ts +41 -0
- package/src/lib/schedule.ts +382 -0
- package/src/lib/slashExec.ts +163 -0
- package/src/lib/utils.ts +35 -0
- package/src/main.tsx +25 -0
- package/src/pages/AnalyticsPage.tsx +601 -0
- package/src/pages/ChannelsPage.tsx +772 -0
- package/src/pages/ChatPage.tsx +889 -0
- package/src/pages/ConfigPage.tsx +660 -0
- package/src/pages/CronPage.tsx +524 -0
- package/src/pages/DocsPage.tsx +69 -0
- package/src/pages/EnvPage.tsx +918 -0
- package/src/pages/LogsPage.tsx +246 -0
- package/src/pages/McpPage.tsx +757 -0
- package/src/pages/ModelsPage.tsx +994 -0
- package/src/pages/PairingPage.tsx +276 -0
- package/src/pages/PluginsPage.tsx +580 -0
- package/src/pages/ProfilesPage.tsx +559 -0
- package/src/pages/SessionsPage.tsx +936 -0
- package/src/pages/SkillsPage.tsx +557 -0
- package/src/pages/SystemPage.tsx +1259 -0
- package/src/pages/WebhooksPage.tsx +483 -0
- package/src/plugins/PluginPage.tsx +64 -0
- package/src/plugins/index.ts +6 -0
- package/src/plugins/registry.ts +151 -0
- package/src/plugins/sdk.d.ts +160 -0
- package/src/plugins/slots.ts +199 -0
- package/src/plugins/types.ts +37 -0
- package/src/plugins/usePlugins.ts +133 -0
- package/src/themes/context.tsx +443 -0
- package/src/themes/fonts.ts +160 -0
- package/src/themes/index.ts +3 -0
- package/src/themes/presets.ts +477 -0
- package/src/themes/types.ts +187 -0
- package/tsconfig.app.json +34 -0
- package/tsconfig.json +7 -0
- package/tsconfig.node.json +26 -0
- package/vite.config.ts +124 -0
- package/vite.config.ts.timestamp-1780999102396-af6b77b30ebd8.mjs +105 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { useMemo, type ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lightweight markdown renderer for LLM output.
|
|
5
|
+
* Handles: code blocks, inline code, bold, italic, headers, links, lists, horizontal rules.
|
|
6
|
+
* NOT a full CommonMark parser — optimized for typical assistant message patterns.
|
|
7
|
+
*
|
|
8
|
+
* `streaming` renders a blinking caret at the tail of the last block so it
|
|
9
|
+
* appears to hug the final character instead of wrapping onto a new line
|
|
10
|
+
* after a block element (paragraph/list/code/…).
|
|
11
|
+
*/
|
|
12
|
+
export function Markdown({
|
|
13
|
+
content,
|
|
14
|
+
highlightTerms,
|
|
15
|
+
streaming,
|
|
16
|
+
}: {
|
|
17
|
+
content: string;
|
|
18
|
+
highlightTerms?: string[];
|
|
19
|
+
streaming?: boolean;
|
|
20
|
+
}) {
|
|
21
|
+
const blocks = useMemo(() => parseBlocks(content), [content]);
|
|
22
|
+
const caret = streaming ? <StreamingCaret /> : null;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="text-sm text-foreground leading-relaxed space-y-2">
|
|
26
|
+
{blocks.map((block, i) => (
|
|
27
|
+
<Block
|
|
28
|
+
key={i}
|
|
29
|
+
block={block}
|
|
30
|
+
highlightTerms={highlightTerms}
|
|
31
|
+
caret={caret && i === blocks.length - 1 ? caret : null}
|
|
32
|
+
/>
|
|
33
|
+
))}
|
|
34
|
+
{blocks.length === 0 && caret}
|
|
35
|
+
</div>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function StreamingCaret() {
|
|
40
|
+
return (
|
|
41
|
+
<span
|
|
42
|
+
aria-hidden
|
|
43
|
+
className="inline-block w-[0.5em] h-[1em] ml-0.5 align-[-0.15em] bg-foreground/50 animate-pulse"
|
|
44
|
+
/>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* ------------------------------------------------------------------ */
|
|
49
|
+
/* Types */
|
|
50
|
+
/* ------------------------------------------------------------------ */
|
|
51
|
+
|
|
52
|
+
type BlockNode =
|
|
53
|
+
| { type: "code"; lang: string; content: string }
|
|
54
|
+
| { type: "heading"; level: number; content: string }
|
|
55
|
+
| { type: "hr" }
|
|
56
|
+
| { type: "list"; ordered: boolean; items: string[] }
|
|
57
|
+
| { type: "paragraph"; content: string };
|
|
58
|
+
|
|
59
|
+
/* ------------------------------------------------------------------ */
|
|
60
|
+
/* Block parser */
|
|
61
|
+
/* ------------------------------------------------------------------ */
|
|
62
|
+
|
|
63
|
+
function parseBlocks(text: string): BlockNode[] {
|
|
64
|
+
const lines = text.split("\n");
|
|
65
|
+
const blocks: BlockNode[] = [];
|
|
66
|
+
let i = 0;
|
|
67
|
+
|
|
68
|
+
while (i < lines.length) {
|
|
69
|
+
const line = lines[i];
|
|
70
|
+
|
|
71
|
+
// Fenced code block
|
|
72
|
+
const fenceMatch = line.match(/^```(\w*)/);
|
|
73
|
+
if (fenceMatch) {
|
|
74
|
+
const lang = fenceMatch[1] || "";
|
|
75
|
+
const codeLines: string[] = [];
|
|
76
|
+
i++;
|
|
77
|
+
while (i < lines.length && !lines[i].startsWith("```")) {
|
|
78
|
+
codeLines.push(lines[i]);
|
|
79
|
+
i++;
|
|
80
|
+
}
|
|
81
|
+
i++; // skip closing ```
|
|
82
|
+
blocks.push({ type: "code", lang, content: codeLines.join("\n") });
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Heading
|
|
87
|
+
const headingMatch = line.match(/^(#{1,4})\s+(.+)/);
|
|
88
|
+
if (headingMatch) {
|
|
89
|
+
blocks.push({
|
|
90
|
+
type: "heading",
|
|
91
|
+
level: headingMatch[1].length,
|
|
92
|
+
content: headingMatch[2],
|
|
93
|
+
});
|
|
94
|
+
i++;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Horizontal rule
|
|
99
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
100
|
+
blocks.push({ type: "hr" });
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Unordered list
|
|
106
|
+
if (/^[-*+]\s/.test(line)) {
|
|
107
|
+
const items: string[] = [];
|
|
108
|
+
while (i < lines.length && /^[-*+]\s/.test(lines[i])) {
|
|
109
|
+
items.push(lines[i].replace(/^[-*+]\s/, ""));
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
blocks.push({ type: "list", ordered: false, items });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Ordered list
|
|
117
|
+
if (/^\d+[.)]\s/.test(line)) {
|
|
118
|
+
const items: string[] = [];
|
|
119
|
+
while (i < lines.length && /^\d+[.)]\s/.test(lines[i])) {
|
|
120
|
+
items.push(lines[i].replace(/^\d+[.)]\s/, ""));
|
|
121
|
+
i++;
|
|
122
|
+
}
|
|
123
|
+
blocks.push({ type: "list", ordered: true, items });
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Empty line
|
|
128
|
+
if (line.trim() === "") {
|
|
129
|
+
i++;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Paragraph — collect consecutive non-empty, non-special lines
|
|
134
|
+
const paraLines: string[] = [];
|
|
135
|
+
while (
|
|
136
|
+
i < lines.length &&
|
|
137
|
+
lines[i].trim() !== "" &&
|
|
138
|
+
!lines[i].match(/^```/) &&
|
|
139
|
+
!lines[i].match(/^#{1,4}\s/) &&
|
|
140
|
+
!lines[i].match(/^[-*+]\s/) &&
|
|
141
|
+
!lines[i].match(/^\d+[.)]\s/) &&
|
|
142
|
+
!lines[i].match(/^[-*_]{3,}\s*$/)
|
|
143
|
+
) {
|
|
144
|
+
paraLines.push(lines[i]);
|
|
145
|
+
i++;
|
|
146
|
+
}
|
|
147
|
+
if (paraLines.length > 0) {
|
|
148
|
+
blocks.push({ type: "paragraph", content: paraLines.join("\n") });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return blocks;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ------------------------------------------------------------------ */
|
|
156
|
+
/* Block renderer */
|
|
157
|
+
/* ------------------------------------------------------------------ */
|
|
158
|
+
|
|
159
|
+
function Block({
|
|
160
|
+
block,
|
|
161
|
+
highlightTerms,
|
|
162
|
+
caret,
|
|
163
|
+
}: {
|
|
164
|
+
block: BlockNode;
|
|
165
|
+
highlightTerms?: string[];
|
|
166
|
+
caret?: ReactNode;
|
|
167
|
+
}) {
|
|
168
|
+
switch (block.type) {
|
|
169
|
+
case "code":
|
|
170
|
+
return (
|
|
171
|
+
<pre className="bg-secondary/60 border border-border px-3 py-2.5 text-xs font-mono leading-relaxed overflow-x-auto">
|
|
172
|
+
<code>
|
|
173
|
+
{block.content}
|
|
174
|
+
{caret}
|
|
175
|
+
</code>
|
|
176
|
+
</pre>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
case "heading": {
|
|
180
|
+
const Tag = `h${Math.min(block.level, 4)}` as "h1" | "h2" | "h3" | "h4";
|
|
181
|
+
const sizes: Record<string, string> = {
|
|
182
|
+
h1: "text-base font-bold",
|
|
183
|
+
h2: "text-sm font-bold",
|
|
184
|
+
h3: "text-sm font-semibold",
|
|
185
|
+
h4: "text-sm font-medium",
|
|
186
|
+
};
|
|
187
|
+
return (
|
|
188
|
+
<Tag className={sizes[Tag]}>
|
|
189
|
+
<InlineContent text={block.content} highlightTerms={highlightTerms} />
|
|
190
|
+
{caret}
|
|
191
|
+
</Tag>
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
case "hr":
|
|
196
|
+
return (
|
|
197
|
+
<>
|
|
198
|
+
<hr className="border-border" />
|
|
199
|
+
{caret}
|
|
200
|
+
</>
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
case "list": {
|
|
204
|
+
const Tag = block.ordered ? "ol" : "ul";
|
|
205
|
+
const last = block.items.length - 1;
|
|
206
|
+
return (
|
|
207
|
+
<Tag
|
|
208
|
+
className={`space-y-0.5 ${block.ordered ? "list-decimal" : "list-disc"} pl-5 text-sm`}
|
|
209
|
+
>
|
|
210
|
+
{block.items.map((item, i) => (
|
|
211
|
+
<li key={i}>
|
|
212
|
+
<InlineContent text={item} highlightTerms={highlightTerms} />
|
|
213
|
+
{i === last ? caret : null}
|
|
214
|
+
</li>
|
|
215
|
+
))}
|
|
216
|
+
</Tag>
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case "paragraph":
|
|
221
|
+
return (
|
|
222
|
+
<p>
|
|
223
|
+
<InlineContent text={block.content} highlightTerms={highlightTerms} />
|
|
224
|
+
{caret}
|
|
225
|
+
</p>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* ------------------------------------------------------------------ */
|
|
231
|
+
/* Inline parser + renderer */
|
|
232
|
+
/* ------------------------------------------------------------------ */
|
|
233
|
+
|
|
234
|
+
type InlineNode =
|
|
235
|
+
| { type: "text"; content: string }
|
|
236
|
+
| { type: "code"; content: string }
|
|
237
|
+
| { type: "bold"; content: string }
|
|
238
|
+
| { type: "italic"; content: string }
|
|
239
|
+
| { type: "link"; text: string; href: string }
|
|
240
|
+
| { type: "br" };
|
|
241
|
+
|
|
242
|
+
function parseInline(text: string): InlineNode[] {
|
|
243
|
+
const nodes: InlineNode[] = [];
|
|
244
|
+
// Pattern priority: code > link > bold > italic > bare URL > line break
|
|
245
|
+
const pattern =
|
|
246
|
+
/(`[^`]+`)|(\[([^\]]+)\]\(([^)]+)\))|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)|(\bhttps?:\/\/[^\s<>)\]]+)|(\n)/g;
|
|
247
|
+
let lastIndex = 0;
|
|
248
|
+
let match: RegExpExecArray | null;
|
|
249
|
+
|
|
250
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
251
|
+
if (match.index > lastIndex) {
|
|
252
|
+
nodes.push({ type: "text", content: text.slice(lastIndex, match.index) });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (match[1]) {
|
|
256
|
+
// Inline code
|
|
257
|
+
nodes.push({ type: "code", content: match[1].slice(1, -1) });
|
|
258
|
+
} else if (match[2]) {
|
|
259
|
+
// [text](url) link
|
|
260
|
+
nodes.push({ type: "link", text: match[3], href: match[4] });
|
|
261
|
+
} else if (match[5]) {
|
|
262
|
+
// **bold**
|
|
263
|
+
nodes.push({ type: "bold", content: match[6] });
|
|
264
|
+
} else if (match[7]) {
|
|
265
|
+
// *italic*
|
|
266
|
+
nodes.push({ type: "italic", content: match[8] });
|
|
267
|
+
} else if (match[9]) {
|
|
268
|
+
// Bare URL
|
|
269
|
+
nodes.push({ type: "link", text: match[9], href: match[9] });
|
|
270
|
+
} else if (match[10]) {
|
|
271
|
+
// Line break within paragraph
|
|
272
|
+
nodes.push({ type: "br" });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
lastIndex = match.index + match[0].length;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (lastIndex < text.length) {
|
|
279
|
+
nodes.push({ type: "text", content: text.slice(lastIndex) });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return nodes;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function InlineContent({
|
|
286
|
+
text,
|
|
287
|
+
highlightTerms,
|
|
288
|
+
}: {
|
|
289
|
+
text: string;
|
|
290
|
+
highlightTerms?: string[];
|
|
291
|
+
}) {
|
|
292
|
+
const nodes = useMemo(() => parseInline(text), [text]);
|
|
293
|
+
|
|
294
|
+
return (
|
|
295
|
+
<>
|
|
296
|
+
{nodes.map((node, i) => {
|
|
297
|
+
switch (node.type) {
|
|
298
|
+
case "text":
|
|
299
|
+
return (
|
|
300
|
+
<HighlightedText
|
|
301
|
+
key={i}
|
|
302
|
+
text={node.content}
|
|
303
|
+
terms={highlightTerms}
|
|
304
|
+
/>
|
|
305
|
+
);
|
|
306
|
+
case "code":
|
|
307
|
+
return (
|
|
308
|
+
<code
|
|
309
|
+
key={i}
|
|
310
|
+
className="bg-secondary/60 px-1.5 py-0.5 text-xs font-mono text-primary/90"
|
|
311
|
+
>
|
|
312
|
+
{node.content}
|
|
313
|
+
</code>
|
|
314
|
+
);
|
|
315
|
+
case "bold":
|
|
316
|
+
return (
|
|
317
|
+
<strong key={i} className="font-semibold">
|
|
318
|
+
<HighlightedText text={node.content} terms={highlightTerms} />
|
|
319
|
+
</strong>
|
|
320
|
+
);
|
|
321
|
+
case "italic":
|
|
322
|
+
return (
|
|
323
|
+
<em key={i}>
|
|
324
|
+
<HighlightedText text={node.content} terms={highlightTerms} />
|
|
325
|
+
</em>
|
|
326
|
+
);
|
|
327
|
+
case "link": {
|
|
328
|
+
// Security: only render http(s)/mailto links. Other schemes
|
|
329
|
+
// (javascript:, data:, vbscript:) are dropped to plain text so a
|
|
330
|
+
// crafted link in agent/message content can't execute on click.
|
|
331
|
+
const href = node.href.trim();
|
|
332
|
+
if (!/^(https?:|mailto:)/i.test(href)) {
|
|
333
|
+
return (
|
|
334
|
+
<HighlightedText
|
|
335
|
+
key={i}
|
|
336
|
+
text={node.text}
|
|
337
|
+
terms={highlightTerms}
|
|
338
|
+
/>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
return (
|
|
342
|
+
<a
|
|
343
|
+
key={i}
|
|
344
|
+
href={href}
|
|
345
|
+
target="_blank"
|
|
346
|
+
rel="noreferrer"
|
|
347
|
+
className="text-primary underline underline-offset-2 decoration-primary/30 hover:decoration-primary/60 transition-colors"
|
|
348
|
+
>
|
|
349
|
+
{node.text}
|
|
350
|
+
</a>
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
case "br":
|
|
354
|
+
return <br key={i} />;
|
|
355
|
+
}
|
|
356
|
+
})}
|
|
357
|
+
</>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Highlight search terms within a plain text string. */
|
|
362
|
+
function HighlightedText({ text, terms }: { text: string; terms?: string[] }) {
|
|
363
|
+
if (!terms || terms.length === 0) return <>{text}</>;
|
|
364
|
+
|
|
365
|
+
// Build a regex that matches any of the search terms (case-insensitive)
|
|
366
|
+
const escaped = terms.map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
367
|
+
const regex = new RegExp(`(${escaped.join("|")})`, "gi");
|
|
368
|
+
const parts = text.split(regex);
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<>
|
|
372
|
+
{parts.map((part, i) =>
|
|
373
|
+
regex.test(part) ? (
|
|
374
|
+
<mark key={i} className="bg-warning/30 text-warning px-0.5">
|
|
375
|
+
{part}
|
|
376
|
+
</mark>
|
|
377
|
+
) : (
|
|
378
|
+
<span key={i}>{part}</span>
|
|
379
|
+
),
|
|
380
|
+
)}
|
|
381
|
+
</>
|
|
382
|
+
);
|
|
383
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Brain, Eye, Gauge, Lightbulb, Wrench } from "lucide-react";
|
|
3
|
+
import { Spinner } from "@nastechai/ui/ui/components/spinner";
|
|
4
|
+
import { api } from "@/lib/api";
|
|
5
|
+
import type { ModelInfoResponse } from "@/lib/api";
|
|
6
|
+
import { formatTokenCount } from "@/lib/format";
|
|
7
|
+
|
|
8
|
+
interface ModelInfoCardProps {
|
|
9
|
+
/** Current model string from config state — used to detect changes */
|
|
10
|
+
currentModel: string;
|
|
11
|
+
/** Bumped after config saves to trigger re-fetch */
|
|
12
|
+
refreshKey?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ModelInfoCard({
|
|
16
|
+
currentModel,
|
|
17
|
+
refreshKey = 0,
|
|
18
|
+
}: ModelInfoCardProps) {
|
|
19
|
+
const [info, setInfo] = useState<ModelInfoResponse | null>(null);
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const lastFetchKeyRef = useRef("");
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!currentModel) return;
|
|
25
|
+
// Re-fetch when model changes OR when refreshKey bumps (after save)
|
|
26
|
+
const fetchKey = `${currentModel}:${refreshKey}`;
|
|
27
|
+
if (fetchKey === lastFetchKeyRef.current) return;
|
|
28
|
+
lastFetchKeyRef.current = fetchKey;
|
|
29
|
+
setLoading(true);
|
|
30
|
+
api
|
|
31
|
+
.getModelInfo()
|
|
32
|
+
.then(setInfo)
|
|
33
|
+
.catch(() => setInfo(null))
|
|
34
|
+
.finally(() => setLoading(false));
|
|
35
|
+
}, [currentModel, refreshKey]);
|
|
36
|
+
|
|
37
|
+
if (loading) {
|
|
38
|
+
return (
|
|
39
|
+
<div className="flex items-center gap-2 py-2 text-xs text-muted-foreground">
|
|
40
|
+
<Spinner className="text-xs" />
|
|
41
|
+
Loading model info…
|
|
42
|
+
</div>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!info || !info.model || info.effective_context_length <= 0) return null;
|
|
47
|
+
|
|
48
|
+
const caps = info.capabilities;
|
|
49
|
+
const hasCaps = caps && Object.keys(caps).length > 0;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="border border-border/60 bg-muted/30 px-3 py-2.5 space-y-2">
|
|
53
|
+
<div className="flex items-center gap-4 text-xs">
|
|
54
|
+
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
55
|
+
<Gauge className="h-3.5 w-3.5" />
|
|
56
|
+
<span className="font-medium">Context Window</span>
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex items-center gap-2">
|
|
59
|
+
<span className="font-mono font-semibold text-foreground">
|
|
60
|
+
{formatTokenCount(info.effective_context_length)}
|
|
61
|
+
</span>
|
|
62
|
+
{info.config_context_length > 0 ? (
|
|
63
|
+
<span className="text-amber-500 text-xs">
|
|
64
|
+
(override — auto: {formatTokenCount(info.auto_context_length)})
|
|
65
|
+
</span>
|
|
66
|
+
) : (
|
|
67
|
+
<span className="text-text-tertiary text-xs">
|
|
68
|
+
auto-detected
|
|
69
|
+
</span>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
{hasCaps && caps.max_output_tokens && caps.max_output_tokens > 0 && (
|
|
75
|
+
<div className="flex items-center gap-4 text-xs">
|
|
76
|
+
<div className="flex items-center gap-1.5 text-muted-foreground">
|
|
77
|
+
<Lightbulb className="h-3.5 w-3.5" />
|
|
78
|
+
<span className="font-medium">Max Output</span>
|
|
79
|
+
</div>
|
|
80
|
+
<span className="font-mono font-semibold text-foreground">
|
|
81
|
+
{formatTokenCount(caps.max_output_tokens)}
|
|
82
|
+
</span>
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
|
|
86
|
+
{hasCaps && (
|
|
87
|
+
<div className="flex flex-wrap items-center gap-1.5 pt-0.5">
|
|
88
|
+
{caps.supports_tools && (
|
|
89
|
+
<span className="inline-flex items-center gap-1 bg-emerald-500/10 px-2 py-0.5 text-xs font-medium text-emerald-600 dark:text-emerald-400">
|
|
90
|
+
<Wrench className="h-2.5 w-2.5" /> Tools
|
|
91
|
+
</span>
|
|
92
|
+
)}
|
|
93
|
+
{caps.supports_vision && (
|
|
94
|
+
<span className="inline-flex items-center gap-1 bg-blue-500/10 px-2 py-0.5 text-xs font-medium text-blue-600 dark:text-blue-400">
|
|
95
|
+
<Eye className="h-2.5 w-2.5" /> Vision
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
98
|
+
{caps.supports_reasoning && (
|
|
99
|
+
<span className="inline-flex items-center gap-1 bg-purple-500/10 px-2 py-0.5 text-xs font-medium text-purple-600 dark:text-purple-400">
|
|
100
|
+
<Brain className="h-2.5 w-2.5" /> Reasoning
|
|
101
|
+
</span>
|
|
102
|
+
)}
|
|
103
|
+
{caps.model_family && (
|
|
104
|
+
<span className="inline-flex items-center gap-1 bg-muted px-2 py-0.5 text-xs font-medium text-text-secondary">
|
|
105
|
+
{caps.model_family}
|
|
106
|
+
</span>
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|