@brainpilot/web 0.0.9 → 0.0.11
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/dist/assets/index-DkoqxJfs.css +1 -0
- package/dist/assets/index-DtLW483q.js +451 -0
- package/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/__tests__/api.test.ts +49 -1
- package/src/__tests__/messageGroups.test.ts +150 -0
- package/src/__tests__/newUiEvents.test.ts +32 -0
- package/src/__tests__/runningScripts.test.ts +139 -0
- package/src/components/chat/MessageStream.tsx +103 -43
- package/src/components/chat/PromptComposer.tsx +28 -10
- package/src/components/chat/RunningScriptsPanel.tsx +118 -0
- package/src/components/chat/runningScripts.ts +88 -0
- package/src/components/demo/DemoView.tsx +1 -1
- package/src/components/session/AgentTraceViews.tsx +5 -9
- package/src/components/settings/KnowledgeBasePanel.tsx +758 -0
- package/src/components/settings/SettingsDialog.tsx +127 -61
- package/src/components/shell/SandboxStatus.tsx +128 -84
- package/src/contexts/messageGroups.ts +110 -4
- package/src/contexts/messageReducer.ts +11 -1
- package/src/i18n/messages/chat.ts +14 -0
- package/src/i18n/messages/sandbox.ts +3 -0
- package/src/i18n/messages/settings.ts +93 -0
- package/src/i18n/messages/trace.ts +0 -2
- package/src/styles/global.css +970 -80
- package/src/utils/api.ts +188 -3
- package/src/utils/format.ts +9 -0
- package/dist/assets/index-CJNvdeGz.js +0 -445
- package/dist/assets/index-DWOsU22G.css +0 -1
|
@@ -1,35 +1,45 @@
|
|
|
1
1
|
import { FormEvent, useEffect, useState } from "react";
|
|
2
|
-
import { Check, Eye, EyeOff, Loader2, Plug, Plus, Settings, SlidersHorizontal, Trash2, UserRound, X } from "lucide-react";
|
|
2
|
+
import { Check, Database, Eye, EyeOff, Loader2, Plug, Plus, Settings, SlidersHorizontal, Trash2, UserRound, X } from "lucide-react";
|
|
3
3
|
import type { LucideIcon } from "lucide-react";
|
|
4
4
|
import type { McpServerEntry, ProviderProfile, ProviderApi } from "../../contracts/backend";
|
|
5
5
|
import { useAuth } from "../../contexts/AuthContext";
|
|
6
6
|
import { usePreferences } from "../../contexts/PreferencesContext";
|
|
7
7
|
import { useT } from "../../i18n/useT";
|
|
8
8
|
import { api } from "../../utils/api";
|
|
9
|
+
import { runtimeConfig } from "../../config";
|
|
10
|
+
import { EXAMPLE_MODEL } from "@brainpilot/protocol";
|
|
9
11
|
import { CustomSelect } from "../primitives/CustomSelect";
|
|
10
12
|
import { IconButton } from "../primitives/IconButton";
|
|
13
|
+
import { KnowledgeBasePanel } from "./KnowledgeBasePanel";
|
|
11
14
|
|
|
12
|
-
type SettingsTab = "account" | "providers" | "mcp" | "preferences";
|
|
15
|
+
type SettingsTab = "account" | "providers" | "mcp" | "knowledgeBase" | "preferences";
|
|
13
16
|
|
|
14
17
|
type SettingsDialogProps = {
|
|
15
18
|
isOpen: boolean;
|
|
16
19
|
onClose: () => void;
|
|
17
20
|
};
|
|
18
21
|
|
|
19
|
-
const
|
|
22
|
+
const ALL_TABS: Array<{ id: SettingsTab; labelKey: string; icon: LucideIcon }> = [
|
|
20
23
|
{ id: "account", labelKey: "settings.tab.account", icon: UserRound },
|
|
21
24
|
{ id: "providers", labelKey: "settings.tab.providers", icon: SlidersHorizontal },
|
|
22
25
|
{ id: "mcp", labelKey: "settings.tab.mcp", icon: Plug },
|
|
26
|
+
{ id: "knowledgeBase", labelKey: "settings.tab.knowledgeBase", icon: Database },
|
|
23
27
|
{ id: "preferences", labelKey: "settings.tab.preferences", icon: Settings },
|
|
24
28
|
];
|
|
25
29
|
|
|
30
|
+
// Local single-user mode has no host-managed identity — the account tab would
|
|
31
|
+
// only show placeholder "local / local / 1970" values, so drop it entirely.
|
|
32
|
+
const tabs = runtimeConfig.localMode
|
|
33
|
+
? ALL_TABS.filter((tab) => tab.id !== "account")
|
|
34
|
+
: ALL_TABS;
|
|
35
|
+
|
|
26
36
|
const DEFAULT_PROVIDER_FORM = {
|
|
27
37
|
name: "",
|
|
28
38
|
baseUrl: "https://api.anthropic.com",
|
|
29
39
|
api: "anthropic-messages" as ProviderApi,
|
|
30
40
|
apiKey: "",
|
|
31
41
|
apiKeyMasked: "",
|
|
32
|
-
models: [
|
|
42
|
+
models: [EXAMPLE_MODEL],
|
|
33
43
|
iconColor: "#111111",
|
|
34
44
|
notes: "",
|
|
35
45
|
};
|
|
@@ -56,7 +66,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|
|
56
66
|
const { user } = useAuth();
|
|
57
67
|
const preferences = usePreferences();
|
|
58
68
|
const t = useT();
|
|
59
|
-
const [activeTab, setActiveTab] = useState<SettingsTab>(
|
|
69
|
+
const [activeTab, setActiveTab] = useState<SettingsTab>(tabs[0].id);
|
|
60
70
|
const [providers, setProviders] = useState<ProviderProfile[]>([]);
|
|
61
71
|
const [mcpServers, setMcpServers] = useState<McpServerEntry[]>([]);
|
|
62
72
|
const [providerForm, setProviderForm] = useState(DEFAULT_PROVIDER_FORM);
|
|
@@ -364,7 +374,6 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|
|
364
374
|
</div>
|
|
365
375
|
</dl>
|
|
366
376
|
<p className="settings-note">{t("settings.account.managedByHost")}</p>
|
|
367
|
-
{version ? <span className="settings-version">{version}</span> : null}
|
|
368
377
|
</section>
|
|
369
378
|
) : null}
|
|
370
379
|
|
|
@@ -440,6 +449,19 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|
|
440
449
|
</article>
|
|
441
450
|
);
|
|
442
451
|
|
|
452
|
+
if (providers.length === 0) {
|
|
453
|
+
return (
|
|
454
|
+
<div className="settings-empty">
|
|
455
|
+
<SlidersHorizontal size={22} />
|
|
456
|
+
<strong>{t("settings.providers.empty")}</strong>
|
|
457
|
+
<p>{t("settings.providers.emptyHint")}</p>
|
|
458
|
+
<button className="settings-button" onClick={openProviderForm} type="button">
|
|
459
|
+
{t("settings.providers.add")}
|
|
460
|
+
</button>
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
443
465
|
return (
|
|
444
466
|
<>
|
|
445
467
|
{sharedProviders.length > 0 ? (
|
|
@@ -475,72 +497,113 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|
|
475
497
|
{t("settings.mcp.addServer")}
|
|
476
498
|
</button>
|
|
477
499
|
</div>
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
<
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
500
|
+
{mcpServers.length === 0 ? (
|
|
501
|
+
<div className="settings-empty">
|
|
502
|
+
<Plug size={22} />
|
|
503
|
+
<strong>{t("settings.mcp.empty")}</strong>
|
|
504
|
+
<p>{t("settings.mcp.emptyHint")}</p>
|
|
505
|
+
<button className="settings-button" onClick={openMcpForm} type="button">
|
|
506
|
+
{t("settings.mcp.addServer")}
|
|
507
|
+
</button>
|
|
508
|
+
</div>
|
|
509
|
+
) : (
|
|
510
|
+
<div className="settings-list">
|
|
511
|
+
{mcpServers.map((server) => (
|
|
512
|
+
<article className="settings-list-item" key={server.name}>
|
|
513
|
+
<div>
|
|
514
|
+
<strong>
|
|
515
|
+
{server.name}
|
|
516
|
+
<span className={`mcp-transport-chip mcp-transport-chip--${server.type}`}>{server.type}</span>
|
|
517
|
+
</strong>
|
|
518
|
+
<span>{server.type === "stdio" ? [server.command, ...(server.args || [])].filter(Boolean).join(" ") : server.url}</span>
|
|
519
|
+
</div>
|
|
520
|
+
<div className="settings-list-item__actions mcp-actions">
|
|
521
|
+
<button onClick={() => editMcpServer(server)} type="button">{t("settings.mcp.edit")}</button>
|
|
522
|
+
<button onClick={() => void removeMcpServer(server.name)} type="button">{t("settings.mcp.remove")}</button>
|
|
523
|
+
</div>
|
|
524
|
+
</article>
|
|
525
|
+
))}
|
|
526
|
+
</div>
|
|
527
|
+
)}
|
|
493
528
|
</section>
|
|
494
529
|
) : null}
|
|
495
530
|
|
|
531
|
+
{activeTab === "knowledgeBase" ? <KnowledgeBasePanel /> : null}
|
|
532
|
+
|
|
496
533
|
{activeTab === "preferences" ? (
|
|
497
534
|
<section className="settings-section">
|
|
498
535
|
<h3>{t("settings.prefs.title")}</h3>
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
<
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
{
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
536
|
+
|
|
537
|
+
<div className="settings-group">
|
|
538
|
+
<h4 className="settings-group__title">{t("settings.prefs.groupAppearance")}</h4>
|
|
539
|
+
<div className="settings-field settings-field--split">
|
|
540
|
+
<div className="settings-field__label">
|
|
541
|
+
<span>{t("settings.prefs.theme")}</span>
|
|
542
|
+
<small>{t("settings.prefs.themeDesc")}</small>
|
|
543
|
+
</div>
|
|
544
|
+
<CustomSelect
|
|
545
|
+
ariaLabel={t("settings.prefs.theme")}
|
|
546
|
+
onChange={(value) => preferences.setTheme(value as typeof preferences.theme)}
|
|
547
|
+
options={[
|
|
548
|
+
{ label: t("settings.prefs.themeLight"), value: "light" },
|
|
549
|
+
{ label: t("settings.prefs.themeDark"), value: "dark" },
|
|
550
|
+
{ label: t("settings.prefs.themeSystem"), value: "system" },
|
|
551
|
+
]}
|
|
552
|
+
value={preferences.theme}
|
|
553
|
+
/>
|
|
554
|
+
</div>
|
|
555
|
+
<div className="settings-field settings-field--split">
|
|
556
|
+
<div className="settings-field__label">
|
|
557
|
+
<span>{t("settings.prefs.language")}</span>
|
|
558
|
+
<small>{t("settings.prefs.languageDesc")}</small>
|
|
559
|
+
</div>
|
|
560
|
+
<CustomSelect
|
|
561
|
+
ariaLabel={t("settings.prefs.language")}
|
|
562
|
+
onChange={(value) => preferences.setLanguage(value as typeof preferences.language)}
|
|
563
|
+
options={[
|
|
564
|
+
{ label: "简体中文", value: "zh-CN" },
|
|
565
|
+
{ label: "English", value: "en-US" },
|
|
566
|
+
]}
|
|
567
|
+
value={preferences.language}
|
|
568
|
+
/>
|
|
569
|
+
</div>
|
|
511
570
|
</div>
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
<
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
{
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
571
|
+
|
|
572
|
+
<div className="settings-group">
|
|
573
|
+
<h4 className="settings-group__title">{t("settings.prefs.groupBehavior")}</h4>
|
|
574
|
+
<label className="settings-toggle-row">
|
|
575
|
+
<span className="settings-toggle-row__text">
|
|
576
|
+
<span>{t("settings.prefs.confirmDangerous")}</span>
|
|
577
|
+
<small>{t("settings.prefs.confirmDangerousDesc")}</small>
|
|
578
|
+
</span>
|
|
579
|
+
<input
|
|
580
|
+
checked={preferences.security.confirmDangerousActions}
|
|
581
|
+
onChange={(event) => preferences.setSecurity({ ...preferences.security, confirmDangerousActions: event.target.checked })}
|
|
582
|
+
type="checkbox"
|
|
583
|
+
/>
|
|
584
|
+
</label>
|
|
585
|
+
<label className="settings-toggle-row">
|
|
586
|
+
<span className="settings-toggle-row__text">
|
|
587
|
+
<span>{t("settings.prefs.notifyDone")}</span>
|
|
588
|
+
<small>{t("settings.prefs.notifyDoneDesc")}</small>
|
|
589
|
+
</span>
|
|
590
|
+
<input
|
|
591
|
+
checked={preferences.notifications.agentDone}
|
|
592
|
+
onChange={(event) => preferences.setNotifications({ ...preferences.notifications, agentDone: event.target.checked })}
|
|
593
|
+
type="checkbox"
|
|
594
|
+
/>
|
|
595
|
+
</label>
|
|
523
596
|
</div>
|
|
524
|
-
<label className="settings-check">
|
|
525
|
-
<input
|
|
526
|
-
checked={preferences.security.confirmDangerousActions}
|
|
527
|
-
onChange={(event) => preferences.setSecurity({ ...preferences.security, confirmDangerousActions: event.target.checked })}
|
|
528
|
-
type="checkbox"
|
|
529
|
-
/>
|
|
530
|
-
<span>{t("settings.prefs.confirmDangerous")}</span>
|
|
531
|
-
</label>
|
|
532
|
-
<label className="settings-check">
|
|
533
|
-
<input
|
|
534
|
-
checked={preferences.notifications.agentDone}
|
|
535
|
-
onChange={(event) => preferences.setNotifications({ ...preferences.notifications, agentDone: event.target.checked })}
|
|
536
|
-
type="checkbox"
|
|
537
|
-
/>
|
|
538
|
-
<span>{t("settings.prefs.notifyDone")}</span>
|
|
539
|
-
</label>
|
|
540
597
|
</section>
|
|
541
598
|
) : null}
|
|
542
599
|
</div>
|
|
543
600
|
</div>
|
|
601
|
+
|
|
602
|
+
{version ? (
|
|
603
|
+
<footer className="settings-modal__footer">
|
|
604
|
+
<span className="settings-version">{version}</span>
|
|
605
|
+
</footer>
|
|
606
|
+
) : null}
|
|
544
607
|
</section>
|
|
545
608
|
|
|
546
609
|
{isProviderFormOpen ? (
|
|
@@ -611,7 +674,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|
|
611
674
|
{providerForm.models.map((model, index) => (
|
|
612
675
|
<label className="provider-model-row" key={`${index}-${providerForm.models.length}`}>
|
|
613
676
|
<input
|
|
614
|
-
placeholder=
|
|
677
|
+
placeholder={EXAMPLE_MODEL}
|
|
615
678
|
value={model}
|
|
616
679
|
onChange={(event) => updateProviderModel(index, event.target.value)}
|
|
617
680
|
/>
|
|
@@ -626,6 +689,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
|
|
626
689
|
</label>
|
|
627
690
|
))}
|
|
628
691
|
</div>
|
|
692
|
+
<p className="provider-form__models-hint">
|
|
693
|
+
{t("settings.providerForm.modelsHint")}
|
|
694
|
+
</p>
|
|
629
695
|
</div>
|
|
630
696
|
|
|
631
697
|
<div className="provider-form__appearance">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { Radio, Server } from "lucide-react";
|
|
2
3
|
import { useSandbox } from "../../contexts/SandboxContext";
|
|
3
4
|
import { useSessions } from "../../contexts/SessionContext";
|
|
4
5
|
import { useT } from "../../i18n/useT";
|
|
@@ -24,6 +25,13 @@ function formatBytes(bytes?: number) {
|
|
|
24
25
|
return `${value >= 10 ? value.toFixed(0) : value.toFixed(1)} ${units[index]}`;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
/** Clamp a percentage into [0, 100] and pick a severity band for the meter fill. */
|
|
29
|
+
function meterLevel(percent?: number): { pct: number; level: "ok" | "warning" | "critical" } {
|
|
30
|
+
const pct = Math.max(0, Math.min(100, Math.round(percent ?? 0)));
|
|
31
|
+
const level = pct >= 90 ? "critical" : pct >= 75 ? "warning" : "ok";
|
|
32
|
+
return { pct, level };
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
function getConnectionState(status: string, isConnected: boolean): SandboxConnectionState {
|
|
28
36
|
if (status === "running" && isConnected) {
|
|
29
37
|
return "connected";
|
|
@@ -64,6 +72,7 @@ function getStatusKey(status: string, isConnected: boolean) {
|
|
|
64
72
|
export function SandboxStatus() {
|
|
65
73
|
const [isOpen, setIsOpen] = useState(false);
|
|
66
74
|
const [logs, setLogs] = useState("");
|
|
75
|
+
const [showLogs, setShowLogs] = useState(false);
|
|
67
76
|
const [health, setHealth] = useState<Record<string, unknown> | null>(null);
|
|
68
77
|
const [detailsLoading, setDetailsLoading] = useState(false);
|
|
69
78
|
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
@@ -157,98 +166,119 @@ export function SandboxStatus() {
|
|
|
157
166
|
id="sandbox-status-popover"
|
|
158
167
|
role="dialog"
|
|
159
168
|
>
|
|
160
|
-
<div className=
|
|
161
|
-
<span className="sandbox-
|
|
162
|
-
<
|
|
169
|
+
<div className={`sandbox-status__hero sandbox-status__hero--${connection}`}>
|
|
170
|
+
<span className="sandbox-status__hero-dot" aria-hidden="true" />
|
|
171
|
+
<div className="sandbox-status__hero-text">
|
|
172
|
+
<strong>{t(connectionLabelKey[connection])}</strong>
|
|
173
|
+
<span className={isLoading ? "is-loading" : ""}>
|
|
174
|
+
{(() => {
|
|
175
|
+
const notice = getStatusKey(effectiveStatus, isConnected);
|
|
176
|
+
return t(notice.key, notice.vars);
|
|
177
|
+
})()}
|
|
178
|
+
</span>
|
|
179
|
+
</div>
|
|
163
180
|
</div>
|
|
164
|
-
|
|
165
|
-
<p className={`sandbox-status__notice ${isLoading ? "is-loading" : ""}`}>
|
|
166
|
-
{(() => {
|
|
167
|
-
const notice = getStatusKey(effectiveStatus, isConnected);
|
|
168
|
-
return t(notice.key, notice.vars);
|
|
169
|
-
})()}
|
|
170
|
-
</p>
|
|
171
181
|
{error ? <p className="sandbox-status__empty">{error}</p> : null}
|
|
172
182
|
|
|
173
183
|
{hasSandbox ? (
|
|
174
184
|
<>
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
<
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
<div>
|
|
211
|
-
<span>{t("sandbox.label.cpu")}</span>
|
|
212
|
-
<strong>{t("sandbox.cpuUsed", { percent: stats?.cpu.usedPercent ?? 0 })}</strong>
|
|
213
|
-
<small>
|
|
214
|
-
{t("sandbox.cpuQuota", { quota: stats?.cpu.quotaPercent ?? 0, cpus: stats?.cpu.onlineCpus ?? 0 })}
|
|
215
|
-
</small>
|
|
216
|
-
</div>
|
|
217
|
-
<div>
|
|
218
|
-
<span>{t("sandbox.label.pids")}</span>
|
|
219
|
-
<strong>
|
|
220
|
-
{stats?.pids.current ?? 0} / {stats?.pids.limit ?? t("sandbox.unlimited")}
|
|
221
|
-
</strong>
|
|
222
|
-
</div>
|
|
223
|
-
<div>
|
|
224
|
-
<span>{t("sandbox.label.disk")}</span>
|
|
225
|
-
<strong>
|
|
226
|
-
{formatBytes(stats?.disk.workspaceUsedBytes)} / {formatBytes(stats?.disk.quotaBytes)}
|
|
227
|
-
</strong>
|
|
228
|
-
<small>{stats?.disk.percentOfQuota ?? 0}%</small>
|
|
229
|
-
</div>
|
|
185
|
+
<div className="sandbox-status__meters">
|
|
186
|
+
{(() => {
|
|
187
|
+
const rows: Array<{ label: string; percent?: number; detail: string }> = [
|
|
188
|
+
{
|
|
189
|
+
label: t("sandbox.label.memory"),
|
|
190
|
+
percent: stats?.memory.percent,
|
|
191
|
+
detail: `${formatBytes(stats?.memory.usedBytes)} / ${formatBytes(stats?.memory.limitBytes)}`,
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
label: t("sandbox.label.disk"),
|
|
195
|
+
percent: stats?.disk.percentOfQuota,
|
|
196
|
+
detail: `${formatBytes(stats?.disk.workspaceUsedBytes)} / ${formatBytes(stats?.disk.quotaBytes)}`,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
label: t("sandbox.label.cpu"),
|
|
200
|
+
percent: stats?.cpu.usedPercent,
|
|
201
|
+
detail: t("sandbox.cpuQuota", { quota: stats?.cpu.quotaPercent ?? 0, cpus: stats?.cpu.onlineCpus ?? 0 }),
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
return rows.map((row) => {
|
|
205
|
+
const { pct, level } = meterLevel(row.percent);
|
|
206
|
+
return (
|
|
207
|
+
<div className="sandbox-meter" key={row.label}>
|
|
208
|
+
<div className="sandbox-meter__head">
|
|
209
|
+
<span>{row.label}</span>
|
|
210
|
+
<strong>{pct}%</strong>
|
|
211
|
+
</div>
|
|
212
|
+
<div className="sandbox-meter__track" aria-hidden="true">
|
|
213
|
+
<span className={`sandbox-meter__fill sandbox-meter__fill--${level}`} style={{ width: `${pct}%` }} />
|
|
214
|
+
</div>
|
|
215
|
+
<small>{row.detail}</small>
|
|
216
|
+
</div>
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
})()}
|
|
230
220
|
</div>
|
|
231
221
|
|
|
232
|
-
<div className="sandbox-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
</
|
|
222
|
+
<div className="sandbox-status__chips">
|
|
223
|
+
{(() => {
|
|
224
|
+
const runtimeOnline = health?.agent_runtime !== false && !!health?.agent_runtime;
|
|
225
|
+
const runtimeKnown = health?.agent_runtime !== undefined;
|
|
226
|
+
return (
|
|
227
|
+
<span className={`sandbox-chip sandbox-chip--${runtimeKnown ? (runtimeOnline ? "ok" : "off") : "unknown"}`}>
|
|
228
|
+
<Server size={13} />
|
|
229
|
+
{t("sandbox.label.runtime")}
|
|
230
|
+
<i className="sandbox-chip__dot" aria-hidden="true" />
|
|
231
|
+
</span>
|
|
232
|
+
);
|
|
233
|
+
})()}
|
|
234
|
+
<span className={`sandbox-chip sandbox-chip--${isConnected ? "ok" : "off"}`}>
|
|
235
|
+
<Radio size={13} />
|
|
236
|
+
{t("sandbox.label.sse")}
|
|
237
|
+
<i className="sandbox-chip__dot" aria-hidden="true" />
|
|
238
|
+
</span>
|
|
249
239
|
</div>
|
|
250
240
|
|
|
251
|
-
<
|
|
241
|
+
<details className="sandbox-status__details">
|
|
242
|
+
<summary>{t("sandbox.details.more")}</summary>
|
|
243
|
+
<dl className="sandbox-status__grid">
|
|
244
|
+
<div>
|
|
245
|
+
<dt>{t("sandbox.label.name")}</dt>
|
|
246
|
+
<dd>{currentSandbox.name}</dd>
|
|
247
|
+
</div>
|
|
248
|
+
<div>
|
|
249
|
+
<dt>{t("sandbox.label.id")}</dt>
|
|
250
|
+
<dd>{currentSandbox.id}</dd>
|
|
251
|
+
</div>
|
|
252
|
+
<div>
|
|
253
|
+
<dt>{t("sandbox.label.container")}</dt>
|
|
254
|
+
<dd>{currentSandbox.containerName || "-"}</dd>
|
|
255
|
+
</div>
|
|
256
|
+
<div>
|
|
257
|
+
<dt>{t("sandbox.label.hostApi")}</dt>
|
|
258
|
+
<dd>{currentSandbox.hostApiUrl || "-"}</dd>
|
|
259
|
+
</div>
|
|
260
|
+
<div>
|
|
261
|
+
<dt>{t("sandbox.label.port")}</dt>
|
|
262
|
+
<dd>{currentSandbox.port ?? "-"}</dd>
|
|
263
|
+
</div>
|
|
264
|
+
<div>
|
|
265
|
+
<dt>{t("sandbox.label.pids")}</dt>
|
|
266
|
+
<dd>{stats?.pids.current ?? 0} / {stats?.pids.limit ?? t("sandbox.unlimited")}</dd>
|
|
267
|
+
</div>
|
|
268
|
+
<div>
|
|
269
|
+
<dt>{t("sandbox.label.created")}</dt>
|
|
270
|
+
<dd>{new Date(currentSandbox.createdAt).toLocaleString()}</dd>
|
|
271
|
+
</div>
|
|
272
|
+
<div>
|
|
273
|
+
<dt>{t("sandbox.label.checked")}</dt>
|
|
274
|
+
<dd>{health?.checked_at ? new Date(String(health.checked_at)).toLocaleTimeString() : "-"}</dd>
|
|
275
|
+
</div>
|
|
276
|
+
</dl>
|
|
277
|
+
</details>
|
|
278
|
+
|
|
279
|
+
{showLogs ? (
|
|
280
|
+
<pre className="sandbox-status__logs">{detailsLoading ? t("sandbox.logs.loading") : logs || t("sandbox.logs.empty")}</pre>
|
|
281
|
+
) : null}
|
|
252
282
|
</>
|
|
253
283
|
) : (
|
|
254
284
|
<p className="sandbox-status__empty">{t("sandbox.empty")}</p>
|
|
@@ -263,7 +293,21 @@ export function SandboxStatus() {
|
|
|
263
293
|
<button disabled={isLoading} onClick={() => void refresh()} type="button">
|
|
264
294
|
{t("sandbox.action.refresh")}
|
|
265
295
|
</button>
|
|
266
|
-
<button
|
|
296
|
+
<button
|
|
297
|
+
aria-pressed={showLogs}
|
|
298
|
+
className={showLogs ? "is-active" : ""}
|
|
299
|
+
disabled={detailsLoading}
|
|
300
|
+
onClick={() => {
|
|
301
|
+
setShowLogs((current) => {
|
|
302
|
+
const next = !current;
|
|
303
|
+
if (next) {
|
|
304
|
+
void loadDetails();
|
|
305
|
+
}
|
|
306
|
+
return next;
|
|
307
|
+
});
|
|
308
|
+
}}
|
|
309
|
+
type="button"
|
|
310
|
+
>
|
|
267
311
|
{t("sandbox.action.logs")}
|
|
268
312
|
</button>
|
|
269
313
|
<button
|