Skip to content

Commit e449521

Browse files
authored
Merge pull request lidge-jun#741 from lidge-jun/stack/gui-dense-workspaces
fix(gui): denser Storage, API Access, Subagents, Usage, and Claude layouts
2 parents 085f8f1 + f0acc36 commit e449521

47 files changed

Lines changed: 5369 additions & 996 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

gui/src/App.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,8 +300,8 @@ export default function App() {
300300
{page === "startup" && <Startup apiBase={API_BASE} />}
301301
{page === "providers" && <Providers apiBase={API_BASE} />}
302302
{page === "models" && <Models apiBase={API_BASE} />}
303-
{page === "combos" && <Combos apiBase={API_BASE} />}
304-
{page === "subagents" && <Subagents apiBase={API_BASE} />}
303+
{page === "combos" && <Combos key={API_BASE} apiBase={API_BASE} />}
304+
{page === "subagents" && <Subagents key={API_BASE} apiBase={API_BASE} />}
305305
{page === "logs" && <Logs apiBase={API_BASE} />}
306306
{page === "usage" && <Usage apiBase={API_BASE} />}
307307
{page === "storage" && <Storage apiBase={API_BASE} />}
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
/**
2+
* ApiKeysWorkspace — rail + main for the API tab. Overview hosts the existing
3+
* endpoint/auth/generate/models/usage panels; selecting a key opens detail.
4+
*/
5+
import { useEffect, useState } from "react";
6+
import { IconChevron, IconTrash } from "../../icons";
7+
import { useT } from "../../i18n/shared";
8+
import type { ExternalModelRow } from "../../api-access-models";
9+
import {
10+
formatCreatedDate,
11+
type ApiEndpointInfo,
12+
type ApiKeyEntry,
13+
type ModelTestState,
14+
} from "../../pages/api-keys-utils";
15+
import {
16+
ApiKeysEndpointsPanel,
17+
ApiKeysManagePanel,
18+
ApiKeysModelsPanel,
19+
ApiKeysUsagePanel,
20+
} from "../../pages/api-keys-panels";
21+
22+
export interface ApiKeysWorkspaceProps {
23+
keys: ApiKeyEntry[];
24+
keysLoading: boolean;
25+
keysLoadFailed: boolean;
26+
endpoints: ApiEndpointInfo;
27+
claudeCodeEnabled: boolean;
28+
localeTag?: string;
29+
newName: string;
30+
creating: boolean;
31+
newKey: string | null;
32+
copied: boolean;
33+
filteredModels: ExternalModelRow[];
34+
modelsLoading: boolean;
35+
modelsLoadFailed: boolean;
36+
modelQuery: string;
37+
copiedModelId: string | null;
38+
modelTests: Record<string, { state: ModelTestState; detail?: string }>;
39+
onNewNameChange: (value: string) => void;
40+
onCreate: () => void;
41+
onDismissNewKey: () => void;
42+
onCopyKey: () => void;
43+
onDelete: (id: string) => void;
44+
onModelQueryChange: (value: string) => void;
45+
onCopyModelId: (modelId: string) => void;
46+
onTestModel: (model: ExternalModelRow) => void;
47+
sourceLabel: (model: ExternalModelRow) => string;
48+
protocolLabel: (protocol: string) => string;
49+
}
50+
51+
export default function ApiKeysWorkspace({
52+
keys,
53+
keysLoading,
54+
keysLoadFailed,
55+
endpoints,
56+
claudeCodeEnabled,
57+
localeTag,
58+
newName,
59+
creating,
60+
newKey,
61+
copied,
62+
filteredModels,
63+
modelsLoading,
64+
modelsLoadFailed,
65+
modelQuery,
66+
copiedModelId,
67+
modelTests,
68+
onNewNameChange,
69+
onCreate,
70+
onDismissNewKey,
71+
onCopyKey,
72+
onDelete,
73+
onModelQueryChange,
74+
onCopyModelId,
75+
onTestModel,
76+
sourceLabel,
77+
protocolLabel,
78+
}: ApiKeysWorkspaceProps) {
79+
const t = useT();
80+
const [selectedId, setSelectedId] = useState<string | null>(null);
81+
const [confirmDelete, setConfirmDelete] = useState(false);
82+
/** Armed after a short delay so a double-click / retained focus cannot confirm immediately. */
83+
const [confirmArmed, setConfirmArmed] = useState(false);
84+
85+
const selected = selectedId ? (keys.find(k => k.id === selectedId) ?? null) : null;
86+
87+
const clearDeleteConfirm = () => {
88+
setConfirmDelete(false);
89+
setConfirmArmed(false);
90+
};
91+
92+
const showOverview = () => {
93+
setSelectedId(null);
94+
clearDeleteConfirm();
95+
};
96+
97+
useEffect(() => {
98+
if (!confirmDelete) {
99+
setConfirmArmed(false);
100+
return;
101+
}
102+
const timer = window.setTimeout(() => setConfirmArmed(true), 300);
103+
return () => window.clearTimeout(timer);
104+
}, [confirmDelete]);
105+
106+
const handleRequestDelete = () => {
107+
if (!selected) return;
108+
setConfirmDelete(true);
109+
};
110+
111+
const handleConfirmDelete = () => {
112+
if (!selected || !confirmArmed) return;
113+
onDelete(selected.id);
114+
clearDeleteConfirm();
115+
setSelectedId(null);
116+
};
117+
118+
return (
119+
<div className="apikeys-workspace-shell">
120+
<div className="apikeys-workspace-root">
121+
<aside className="apikeys-workspace-rail" aria-label={t("api.title")}>
122+
<div className="apikeys-workspace-rail-header">
123+
<span className="apikeys-workspace-rail-title">
124+
{keysLoading ? t("api.activeKeysLoading") : t("api.activeKeys", { count: keys.length })}
125+
</span>
126+
</div>
127+
<div className="apikeys-workspace-rail-list">
128+
<button
129+
type="button"
130+
className={`apikeys-workspace-rail-row${selectedId === null ? " apikeys-workspace-rail-row--selected" : ""}`}
131+
onClick={showOverview}
132+
aria-current={selectedId === null ? "page" : undefined}
133+
>
134+
<span className="apikeys-workspace-rail-name">{t("api.workspace.overview")}</span>
135+
</button>
136+
{keysLoading ? (
137+
<span className="apikeys-workspace-rail-empty">{t("common.loading")}</span>
138+
) : keys.length === 0 ? (
139+
<span className="apikeys-workspace-rail-empty">
140+
{keysLoadFailed ? t("api.keysLoadFailed") : t("api.workspace.noKeysHint")}
141+
</span>
142+
) : (
143+
keys.map(k => (
144+
<button
145+
key={k.id}
146+
type="button"
147+
className={`apikeys-workspace-rail-row${selectedId === k.id ? " apikeys-workspace-rail-row--selected" : ""}`}
148+
onClick={() => { setSelectedId(k.id); clearDeleteConfirm(); }}
149+
aria-current={selectedId === k.id ? "page" : undefined}
150+
>
151+
<span className="apikeys-workspace-rail-name">{k.name}</span>
152+
<span className="apikeys-workspace-rail-meta">
153+
{k.prefix} · {formatCreatedDate(k.createdAt, localeTag)}
154+
</span>
155+
</button>
156+
))
157+
)}
158+
</div>
159+
</aside>
160+
161+
<section className="apikeys-workspace-main" aria-label={t("api.workspace.details")}>
162+
{selected ? (
163+
<div className="awi-detail">
164+
<div className="awi-detail-toolbar">
165+
<button type="button" className="awi-back" onClick={showOverview}>
166+
<IconChevron className="awi-back-chevron" aria-hidden="true" />
167+
{t("modal.back")}
168+
</button>
169+
</div>
170+
<div className="awi-detail-body">
171+
<div className="awi-detail-head">
172+
<h2 className="awi-detail-title">{selected.name}</h2>
173+
<span className="awi-detail-actions">
174+
{confirmDelete ? (
175+
<>
176+
<button
177+
key="confirm-delete"
178+
type="button"
179+
className="btn btn-danger btn-sm awi-confirm-delete"
180+
onClick={handleConfirmDelete}
181+
disabled={!confirmArmed}
182+
>
183+
<IconTrash /> {t("api.confirm")}
184+
</button>
185+
<button type="button" className="btn btn-ghost btn-sm" onClick={clearDeleteConfirm}>
186+
{t("common.cancel")}
187+
</button>
188+
</>
189+
) : (
190+
<button
191+
key="request-delete"
192+
type="button"
193+
className="btn btn-danger btn-sm"
194+
onClick={handleRequestDelete}
195+
aria-label={t("api.deleteAria")}
196+
>
197+
<IconTrash /> {t("api.workspace.deleteKey")}
198+
</button>
199+
)}
200+
</span>
201+
</div>
202+
{confirmDelete && (
203+
<p className="muted awi-delete-hint">{t("api.workspace.deleteConfirm")}</p>
204+
)}
205+
<div className="awi-section">
206+
<h3 className="awi-section-title">{t("api.workspace.keyDetails")}</h3>
207+
<dl className="awi-kv">
208+
<div className="awi-kv-row">
209+
<dt>{t("api.colName")}</dt>
210+
<dd>{selected.name}</dd>
211+
</div>
212+
<div className="awi-kv-row">
213+
<dt>{t("api.workspace.keyPrefix")}</dt>
214+
<dd><code>{selected.prefix}</code></dd>
215+
</div>
216+
<div className="awi-kv-row">
217+
<dt>{t("api.colCreated")}</dt>
218+
<dd>{formatCreatedDate(selected.createdAt, localeTag)}</dd>
219+
</div>
220+
</dl>
221+
</div>
222+
</div>
223+
</div>
224+
) : (
225+
<div className="awi-overview">
226+
<div className="awi-overview-left">
227+
<ApiKeysManagePanel
228+
keys={keys}
229+
keysLoading={keysLoading}
230+
keysLoadFailed={keysLoadFailed}
231+
newName={newName}
232+
creating={creating}
233+
newKey={newKey}
234+
copied={copied}
235+
confirmDelete={null}
236+
localeTag={localeTag}
237+
showKeyList={false}
238+
onNewNameChange={onNewNameChange}
239+
onCreate={onCreate}
240+
onDismissNewKey={onDismissNewKey}
241+
onCopyKey={onCopyKey}
242+
onConfirmDelete={() => {}}
243+
onCancelDelete={() => {}}
244+
onDelete={() => {}}
245+
/>
246+
<ApiKeysEndpointsPanel endpoints={endpoints} claudeCodeEnabled={claudeCodeEnabled} />
247+
<ApiKeysUsagePanel endpoints={endpoints} claudeCodeEnabled={claudeCodeEnabled} />
248+
</div>
249+
<div className="awi-overview-right">
250+
<ApiKeysModelsPanel
251+
filteredModels={filteredModels}
252+
modelsLoading={modelsLoading}
253+
modelsLoadFailed={modelsLoadFailed}
254+
modelQuery={modelQuery}
255+
copiedModelId={copiedModelId}
256+
modelTests={modelTests}
257+
claudeCodeEnabled={claudeCodeEnabled}
258+
onModelQueryChange={onModelQueryChange}
259+
onCopyModelId={onCopyModelId}
260+
onTestModel={onTestModel}
261+
sourceLabel={sourceLabel}
262+
protocolLabel={protocolLabel}
263+
/>
264+
</div>
265+
</div>
266+
)}
267+
</section>
268+
</div>
269+
</div>
270+
);
271+
}

0 commit comments

Comments
 (0)