@schlessera/brain-ui-react 0.4.0 → 0.5.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/dist/components/chat/chat-page.js +4 -4
- package/dist/components/chat/chat-page.js.map +1 -1
- package/dist/components/layout/mobile-tab-bar.d.ts.map +1 -1
- package/dist/components/layout/mobile-tab-bar.js +41 -4
- package/dist/components/layout/mobile-tab-bar.js.map +1 -1
- package/dist/components/layout/side-rail.js +3 -3
- package/dist/components/layout/side-rail.js.map +1 -1
- package/dist/components/settings/models-tab.d.ts +11 -0
- package/dist/components/settings/models-tab.d.ts.map +1 -0
- package/dist/components/settings/models-tab.js +107 -0
- package/dist/components/settings/models-tab.js.map +1 -0
- package/dist/components/settings/passkey-tab.d.ts +13 -0
- package/dist/components/settings/passkey-tab.d.ts.map +1 -0
- package/dist/components/settings/{passkey-panel.js → passkey-tab.js} +14 -11
- package/dist/components/settings/passkey-tab.js.map +1 -0
- package/dist/components/settings/settings-panel.d.ts +10 -0
- package/dist/components/settings/settings-panel.d.ts.map +1 -0
- package/dist/components/settings/settings-panel.js +24 -0
- package/dist/components/settings/settings-panel.js.map +1 -0
- package/dist/lib/api-client.d.ts +7 -1
- package/dist/lib/api-client.d.ts.map +1 -1
- package/dist/lib/api-client.js +9 -0
- package/dist/lib/api-client.js.map +1 -1
- package/dist/stores/ui-store.d.ts +9 -3
- package/dist/stores/ui-store.d.ts.map +1 -1
- package/dist/stores/ui-store.js +6 -3
- package/dist/stores/ui-store.js.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +2 -2
- package/src/components/chat/chat-page.tsx +6 -6
- package/src/components/layout/mobile-tab-bar.tsx +95 -8
- package/src/components/layout/side-rail.tsx +6 -6
- package/src/components/settings/models-tab.tsx +205 -0
- package/src/components/settings/{passkey-panel.tsx → passkey-tab.tsx} +74 -79
- package/src/components/settings/settings-panel.tsx +61 -0
- package/src/lib/api-client.ts +15 -0
- package/src/stores/ui-store.ts +18 -7
- package/dist/components/settings/passkey-panel.d.ts +0 -10
- package/dist/components/settings/passkey-panel.d.ts.map +0 -1
- package/dist/components/settings/passkey-panel.js.map +0 -1
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { Eye, EyeOff, Loader2, RefreshCw } from "lucide-react";
|
|
3
|
+
import type {
|
|
4
|
+
ModelCatalogEntry,
|
|
5
|
+
ModelCatalogResponse,
|
|
6
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
7
|
+
import { api } from "../../lib/api-client.js";
|
|
8
|
+
import { cn } from "../../lib/utils.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The model picker's contents, and which of them to show.
|
|
12
|
+
*
|
|
13
|
+
* The roster comes from the server (Anthropic model discovery plus any
|
|
14
|
+
* host-declared profiles), so this tab never hardcodes model names. Hiding is
|
|
15
|
+
* presentation only — a session already pinned to a hidden model keeps running.
|
|
16
|
+
*/
|
|
17
|
+
export function ModelsTab({ active }: { active: boolean }) {
|
|
18
|
+
const [catalog, setCatalog] = useState<ModelCatalogResponse | null>(null);
|
|
19
|
+
const [loading, setLoading] = useState(false);
|
|
20
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (!active) return;
|
|
25
|
+
let cancelled = false;
|
|
26
|
+
setLoading(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
api
|
|
29
|
+
.models()
|
|
30
|
+
.then((data) => {
|
|
31
|
+
if (!cancelled) setCatalog(data);
|
|
32
|
+
})
|
|
33
|
+
.catch((err: unknown) => {
|
|
34
|
+
if (!cancelled) {
|
|
35
|
+
setError(err instanceof Error ? err.message : "Could not load models");
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
.finally(() => {
|
|
39
|
+
if (!cancelled) setLoading(false);
|
|
40
|
+
});
|
|
41
|
+
return () => {
|
|
42
|
+
cancelled = true;
|
|
43
|
+
};
|
|
44
|
+
}, [active]);
|
|
45
|
+
|
|
46
|
+
async function toggleHidden(entry: ModelCatalogEntry) {
|
|
47
|
+
if (!catalog) return;
|
|
48
|
+
const previous = catalog;
|
|
49
|
+
const hidden = catalog.models
|
|
50
|
+
.filter((model) =>
|
|
51
|
+
model.id === entry.id ? !entry.hidden : model.hidden
|
|
52
|
+
)
|
|
53
|
+
.map((model) => model.id);
|
|
54
|
+
|
|
55
|
+
// Optimistic: the list is the user's own click, so reflect it immediately
|
|
56
|
+
// and roll back if the write fails.
|
|
57
|
+
setCatalog({
|
|
58
|
+
...catalog,
|
|
59
|
+
models: catalog.models.map((model) =>
|
|
60
|
+
model.id === entry.id ? { ...model, hidden: !model.hidden } : model
|
|
61
|
+
),
|
|
62
|
+
});
|
|
63
|
+
setError(null);
|
|
64
|
+
try {
|
|
65
|
+
setCatalog(await api.setHiddenModels(hidden));
|
|
66
|
+
} catch (err) {
|
|
67
|
+
setCatalog(previous);
|
|
68
|
+
setError(err instanceof Error ? err.message : "Could not save");
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function onRefresh() {
|
|
73
|
+
if (refreshing) return;
|
|
74
|
+
setRefreshing(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
try {
|
|
77
|
+
setCatalog(await api.refreshModels());
|
|
78
|
+
} catch (err) {
|
|
79
|
+
setError(err instanceof Error ? err.message : "Refresh failed");
|
|
80
|
+
} finally {
|
|
81
|
+
setRefreshing(false);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const visibleCount = catalog?.models.filter((m) => !m.hidden).length ?? 0;
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<div className="flex h-full flex-col">
|
|
89
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
90
|
+
<h3 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
91
|
+
Models
|
|
92
|
+
</h3>
|
|
93
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
94
|
+
The list is read from the provider and refreshes on its own. Hide the
|
|
95
|
+
ones you never pick — running sessions are unaffected.
|
|
96
|
+
</p>
|
|
97
|
+
|
|
98
|
+
{loading ? (
|
|
99
|
+
<div className="mt-6 flex justify-center">
|
|
100
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
101
|
+
</div>
|
|
102
|
+
) : (
|
|
103
|
+
<ul className="mt-4 flex flex-col gap-2">
|
|
104
|
+
{catalog?.models.map((entry) => (
|
|
105
|
+
<ModelRow
|
|
106
|
+
key={entry.id}
|
|
107
|
+
entry={entry}
|
|
108
|
+
onToggle={() => toggleHidden(entry)}
|
|
109
|
+
/>
|
|
110
|
+
))}
|
|
111
|
+
{catalog?.models.length === 0 && (
|
|
112
|
+
<li className="rounded-lg border border-dashed border-border-subtle p-4 text-center text-xs text-muted-foreground">
|
|
113
|
+
No models available.
|
|
114
|
+
</li>
|
|
115
|
+
)}
|
|
116
|
+
</ul>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{error && (
|
|
120
|
+
<p role="alert" className="mt-3 text-xs text-destructive">
|
|
121
|
+
{error}
|
|
122
|
+
</p>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{catalog?.discovery.error && (
|
|
126
|
+
<p className="mt-3 text-xs text-muted-foreground">
|
|
127
|
+
Last refresh failed ({catalog.discovery.error}). Showing the last
|
|
128
|
+
known list.
|
|
129
|
+
</p>
|
|
130
|
+
)}
|
|
131
|
+
{catalog && !catalog.discovery.enabled && (
|
|
132
|
+
<p className="mt-3 text-xs text-muted-foreground">
|
|
133
|
+
Discovery is disabled (BRAIN_UI_MODEL_DISCOVERY). Only configured
|
|
134
|
+
profiles are listed.
|
|
135
|
+
</p>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<div className="flex items-center justify-between gap-3 border-t border-border p-4">
|
|
140
|
+
<span className="text-[11px] text-muted-foreground">
|
|
141
|
+
{visibleCount} in picker · {formatRefreshed(catalog?.refreshedAt ?? null)}
|
|
142
|
+
</span>
|
|
143
|
+
<button
|
|
144
|
+
onClick={onRefresh}
|
|
145
|
+
disabled={refreshing}
|
|
146
|
+
className="flex items-center gap-2 rounded-lg border border-border-subtle bg-surface px-3 py-2 text-xs font-medium text-foreground transition-colors hover:border-primary hover:text-primary disabled:opacity-50"
|
|
147
|
+
>
|
|
148
|
+
<RefreshCw className={cn("h-3.5 w-3.5", refreshing && "animate-spin")} />
|
|
149
|
+
Refresh
|
|
150
|
+
</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ModelRow({
|
|
157
|
+
entry,
|
|
158
|
+
onToggle,
|
|
159
|
+
}: {
|
|
160
|
+
entry: ModelCatalogEntry;
|
|
161
|
+
onToggle: () => void;
|
|
162
|
+
}) {
|
|
163
|
+
const Icon = entry.hidden ? EyeOff : Eye;
|
|
164
|
+
return (
|
|
165
|
+
<li
|
|
166
|
+
className={cn(
|
|
167
|
+
"flex items-center gap-3 rounded-lg border border-border-subtle bg-surface p-3",
|
|
168
|
+
entry.hidden && "opacity-50"
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
<div className="min-w-0 flex-1">
|
|
172
|
+
<p className="truncate text-sm text-foreground">{entry.label}</p>
|
|
173
|
+
<p className="truncate text-[11px] text-muted-foreground">
|
|
174
|
+
{entry.id}
|
|
175
|
+
{entry.contextWindow ? ` · ${formatTokens(entry.contextWindow)}` : ""}
|
|
176
|
+
{entry.source === "declared" ? " · configured" : ""}
|
|
177
|
+
</p>
|
|
178
|
+
</div>
|
|
179
|
+
<button
|
|
180
|
+
onClick={onToggle}
|
|
181
|
+
title={entry.hidden ? "Show in picker" : "Hide from picker"}
|
|
182
|
+
aria-pressed={!entry.hidden}
|
|
183
|
+
className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-surface-raised hover:text-foreground"
|
|
184
|
+
>
|
|
185
|
+
<Icon className="h-4 w-4" />
|
|
186
|
+
</button>
|
|
187
|
+
</li>
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function formatTokens(tokens: number): string {
|
|
192
|
+
if (tokens >= 1_000_000) return `${tokens / 1_000_000}M context`;
|
|
193
|
+
if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}K context`;
|
|
194
|
+
return `${tokens} context`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function formatRefreshed(refreshedAt: number | null): string {
|
|
198
|
+
if (refreshedAt === null) return "never refreshed";
|
|
199
|
+
const minutes = Math.round((Date.now() - refreshedAt) / 60_000);
|
|
200
|
+
if (minutes < 1) return "refreshed just now";
|
|
201
|
+
if (minutes < 60) return `refreshed ${minutes}m ago`;
|
|
202
|
+
const hours = Math.round(minutes / 60);
|
|
203
|
+
if (hours < 24) return `refreshed ${hours}h ago`;
|
|
204
|
+
return `refreshed ${Math.round(hours / 24)}d ago`;
|
|
205
|
+
}
|
|
@@ -13,21 +13,18 @@ import {
|
|
|
13
13
|
import type { PasskeySummary } from "@schlessera/brain-ui-sdk/protocol";
|
|
14
14
|
import { api } from "../../lib/api-client.js";
|
|
15
15
|
import { isUserCancel, registerPasskey, supportsPasskeys } from "../../lib/passkeys.js";
|
|
16
|
-
import { SlidePanel } from "../layout/slide-panel.js";
|
|
17
16
|
import { cn } from "../../lib/utils.js";
|
|
18
17
|
|
|
19
18
|
/**
|
|
20
|
-
* Passkey management + sign out
|
|
21
|
-
* (routes are behind the auth guard);
|
|
22
|
-
* registered on another hostname are
|
|
19
|
+
* Passkey management + sign out — the Security tab of the settings panel.
|
|
20
|
+
* Registration requires the current session (routes are behind the auth guard);
|
|
21
|
+
* credentials are RP-scoped, so entries registered on another hostname are
|
|
22
|
+
* badged and only useful there.
|
|
23
|
+
*
|
|
24
|
+
* `active` is "this tab is on screen": the list reloads when it becomes visible
|
|
25
|
+
* rather than on mount, so switching tabs picks up changes made elsewhere.
|
|
23
26
|
*/
|
|
24
|
-
export function
|
|
25
|
-
open,
|
|
26
|
-
onClose,
|
|
27
|
-
}: {
|
|
28
|
-
open: boolean;
|
|
29
|
-
onClose: () => void;
|
|
30
|
-
}) {
|
|
27
|
+
export function PasskeyTab({ active }: { active: boolean }) {
|
|
31
28
|
const [credentials, setCredentials] = useState<PasskeySummary[]>([]);
|
|
32
29
|
const [loading, setLoading] = useState(false);
|
|
33
30
|
const [busy, setBusy] = useState(false);
|
|
@@ -52,11 +49,11 @@ export function PasskeyPanel({
|
|
|
52
49
|
}
|
|
53
50
|
|
|
54
51
|
useEffect(() => {
|
|
55
|
-
if (
|
|
52
|
+
if (active) {
|
|
56
53
|
setError(null);
|
|
57
54
|
void refresh();
|
|
58
55
|
}
|
|
59
|
-
}, [
|
|
56
|
+
}, [active]);
|
|
60
57
|
|
|
61
58
|
async function onAdd() {
|
|
62
59
|
if (busy) return;
|
|
@@ -106,79 +103,77 @@ export function PasskeyPanel({
|
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
return (
|
|
109
|
-
<
|
|
110
|
-
<div className="flex
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
</p>
|
|
106
|
+
<div className="flex h-full flex-col">
|
|
107
|
+
<div className="flex-1 overflow-y-auto p-4">
|
|
108
|
+
<h3 className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
109
|
+
Passkeys
|
|
110
|
+
</h3>
|
|
111
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
112
|
+
Sign in with Face ID, fingerprint, or a security key instead of the
|
|
113
|
+
password. Passkeys only work on the site they were added on.
|
|
114
|
+
</p>
|
|
119
115
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
{error && (
|
|
143
|
-
<p role="alert" className="mt-3 text-xs text-destructive">
|
|
144
|
-
{error}
|
|
145
|
-
</p>
|
|
146
|
-
)}
|
|
116
|
+
{loading ? (
|
|
117
|
+
<div className="mt-6 flex justify-center">
|
|
118
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
119
|
+
</div>
|
|
120
|
+
) : (
|
|
121
|
+
<ul className="mt-4 flex flex-col gap-2">
|
|
122
|
+
{credentials.map((credential) => (
|
|
123
|
+
<PasskeyRow
|
|
124
|
+
key={credential.id}
|
|
125
|
+
credential={credential}
|
|
126
|
+
onDelete={() => onDelete(credential.id)}
|
|
127
|
+
onRename={(label) => onRename(credential.id, label)}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
{credentials.length === 0 && passkeyMode && (
|
|
131
|
+
<li className="rounded-lg border border-dashed border-border-subtle p-4 text-center text-xs text-muted-foreground">
|
|
132
|
+
No passkeys yet.
|
|
133
|
+
</li>
|
|
134
|
+
)}
|
|
135
|
+
</ul>
|
|
136
|
+
)}
|
|
147
137
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
>
|
|
154
|
-
{busy ? (
|
|
155
|
-
<Loader2 className="h-4 w-4 animate-spin" />
|
|
156
|
-
) : (
|
|
157
|
-
<Plus className="h-4 w-4" />
|
|
158
|
-
)}
|
|
159
|
-
Add a passkey
|
|
160
|
-
</button>
|
|
161
|
-
) : (
|
|
162
|
-
<p className="mt-4 text-xs text-muted-foreground">
|
|
163
|
-
{passkeyMode
|
|
164
|
-
? "This browser does not support passkeys."
|
|
165
|
-
: "Passkeys need password auth mode on the server."}
|
|
166
|
-
</p>
|
|
167
|
-
)}
|
|
168
|
-
</div>
|
|
138
|
+
{error && (
|
|
139
|
+
<p role="alert" className="mt-3 text-xs text-destructive">
|
|
140
|
+
{error}
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
169
143
|
|
|
170
|
-
{
|
|
171
|
-
<div className="border-t border-border p-4">
|
|
144
|
+
{passkeyMode && webAuthnSupported ? (
|
|
172
145
|
<button
|
|
173
|
-
onClick={
|
|
174
|
-
|
|
146
|
+
onClick={onAdd}
|
|
147
|
+
disabled={busy}
|
|
148
|
+
className="mt-4 flex w-full items-center justify-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:brightness-110 disabled:opacity-50"
|
|
175
149
|
>
|
|
176
|
-
|
|
177
|
-
|
|
150
|
+
{busy ? (
|
|
151
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
152
|
+
) : (
|
|
153
|
+
<Plus className="h-4 w-4" />
|
|
154
|
+
)}
|
|
155
|
+
Add a passkey
|
|
178
156
|
</button>
|
|
179
|
-
|
|
157
|
+
) : (
|
|
158
|
+
<p className="mt-4 text-xs text-muted-foreground">
|
|
159
|
+
{passkeyMode
|
|
160
|
+
? "This browser does not support passkeys."
|
|
161
|
+
: "Passkeys need password auth mode on the server."}
|
|
162
|
+
</p>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* Sign out */}
|
|
167
|
+
<div className="border-t border-border p-4">
|
|
168
|
+
<button
|
|
169
|
+
onClick={onSignOut}
|
|
170
|
+
className="flex w-full items-center justify-center gap-2 rounded-lg border border-border-subtle bg-surface px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-destructive hover:text-destructive"
|
|
171
|
+
>
|
|
172
|
+
<LogOut className="h-4 w-4" />
|
|
173
|
+
Sign out
|
|
174
|
+
</button>
|
|
180
175
|
</div>
|
|
181
|
-
</
|
|
176
|
+
</div>
|
|
182
177
|
);
|
|
183
178
|
}
|
|
184
179
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { KeyRound, SlidersHorizontal } from "lucide-react";
|
|
2
|
+
import { useUIStore, type SettingsTab } from "../../stores/ui-store.js";
|
|
3
|
+
import { SlidePanel } from "../layout/slide-panel.js";
|
|
4
|
+
import { cn } from "../../lib/utils.js";
|
|
5
|
+
import { ModelsTab } from "./models-tab.js";
|
|
6
|
+
import { PasskeyTab } from "./passkey-tab.js";
|
|
7
|
+
|
|
8
|
+
const TABS: Array<{ id: SettingsTab; label: string; icon: typeof KeyRound }> = [
|
|
9
|
+
{ id: "models", label: "Models", icon: SlidersHorizontal },
|
|
10
|
+
{ id: "security", label: "Security", icon: KeyRound },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The settings surface: one panel, one entry point in the menus, a tab per
|
|
15
|
+
* area. Only the selected tab is mounted — each fetches its own data on becoming
|
|
16
|
+
* active, so switching tabs (or reopening the panel) always shows current state.
|
|
17
|
+
*/
|
|
18
|
+
export function SettingsPanel({
|
|
19
|
+
open,
|
|
20
|
+
onClose,
|
|
21
|
+
}: {
|
|
22
|
+
open: boolean;
|
|
23
|
+
onClose: () => void;
|
|
24
|
+
}) {
|
|
25
|
+
const tab = useUIStore((s) => s.settingsTab);
|
|
26
|
+
const setTab = useUIStore((s) => s.setSettingsTab);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<SlidePanel open={open} onClose={onClose} title="Settings">
|
|
30
|
+
<div className="flex h-full flex-col">
|
|
31
|
+
<div className="flex shrink-0 gap-1 border-b border-border px-2 pt-2">
|
|
32
|
+
{TABS.map(({ id, label, icon: Icon }) => (
|
|
33
|
+
<button
|
|
34
|
+
key={id}
|
|
35
|
+
onClick={() => setTab(id)}
|
|
36
|
+
aria-selected={tab === id}
|
|
37
|
+
role="tab"
|
|
38
|
+
className={cn(
|
|
39
|
+
"flex items-center gap-1.5 rounded-t-lg px-3 py-2 text-xs font-medium transition-colors",
|
|
40
|
+
tab === id
|
|
41
|
+
? "border-b-2 border-primary text-foreground"
|
|
42
|
+
: "text-muted-foreground hover:text-foreground"
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
<Icon className="h-3.5 w-3.5" />
|
|
46
|
+
{label}
|
|
47
|
+
</button>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div className="min-h-0 flex-1">
|
|
52
|
+
{tab === "models" ? (
|
|
53
|
+
<ModelsTab active={open && tab === "models"} />
|
|
54
|
+
) : (
|
|
55
|
+
<PasskeyTab active={open && tab === "security"} />
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</SlidePanel>
|
|
60
|
+
);
|
|
61
|
+
}
|
package/src/lib/api-client.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
PronunciationOverride,
|
|
7
7
|
ProviderInfo,
|
|
8
8
|
PasskeySummary,
|
|
9
|
+
ModelCatalogResponse,
|
|
9
10
|
} from "@schlessera/brain-ui-sdk/protocol";
|
|
10
11
|
import type {
|
|
11
12
|
AuthenticationResponseJSON,
|
|
@@ -119,6 +120,20 @@ export const api = {
|
|
|
119
120
|
backends?: Record<string, BackendInfo>;
|
|
120
121
|
}>("/providers"),
|
|
121
122
|
|
|
123
|
+
/** Full model catalog for the settings screen — hidden entries included. */
|
|
124
|
+
models: () => fetchJson<ModelCatalogResponse>("/models"),
|
|
125
|
+
|
|
126
|
+
/** Replace the hidden set (full list, not a delta); returns the new catalog. */
|
|
127
|
+
setHiddenModels: (hidden: string[]) =>
|
|
128
|
+
fetchJson<ModelCatalogResponse>("/models/hidden", {
|
|
129
|
+
method: "PUT",
|
|
130
|
+
body: JSON.stringify({ hidden }),
|
|
131
|
+
}),
|
|
132
|
+
|
|
133
|
+
/** Force a discovery refresh, bypassing the TTL. */
|
|
134
|
+
refreshModels: () =>
|
|
135
|
+
fetchJson<ModelCatalogResponse>("/models/refresh", { method: "POST" }),
|
|
136
|
+
|
|
122
137
|
/** Password-mode login. Resolves on success; throws the server error otherwise. */
|
|
123
138
|
login: (password: string) =>
|
|
124
139
|
fetchJson<{ ok: true }>("/auth/login", {
|
package/src/stores/ui-store.ts
CHANGED
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
2
|
|
|
3
|
+
/** Which tab the settings panel opens on. */
|
|
4
|
+
export type SettingsTab = "models" | "security";
|
|
5
|
+
|
|
3
6
|
interface UIState {
|
|
4
7
|
sessionPanelOpen: boolean;
|
|
5
8
|
syncPanelOpen: boolean;
|
|
6
9
|
whatsupPanelOpen: boolean;
|
|
7
10
|
filePanelOpen: boolean;
|
|
8
|
-
|
|
11
|
+
settingsPanelOpen: boolean;
|
|
12
|
+
settingsTab: SettingsTab;
|
|
9
13
|
toggleSessionPanel: () => void;
|
|
10
14
|
toggleSyncPanel: () => void;
|
|
11
15
|
toggleWhatsupPanel: () => void;
|
|
12
16
|
toggleFilePanel: () => void;
|
|
13
|
-
|
|
17
|
+
toggleSettingsPanel: () => void;
|
|
14
18
|
closeAllPanels: () => void;
|
|
15
19
|
setSyncPanelOpen: (open: boolean) => void;
|
|
16
20
|
setWhatsupPanelOpen: (open: boolean) => void;
|
|
17
21
|
setSessionPanelOpen: (open: boolean) => void;
|
|
18
22
|
setFilePanelOpen: (open: boolean) => void;
|
|
19
|
-
|
|
23
|
+
setSettingsPanelOpen: (open: boolean) => void;
|
|
24
|
+
/** Open settings straight onto a tab (menu entries, deep links). */
|
|
25
|
+
openSettings: (tab: SettingsTab) => void;
|
|
26
|
+
setSettingsTab: (tab: SettingsTab) => void;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
const CLOSED = {
|
|
@@ -24,11 +31,12 @@ const CLOSED = {
|
|
|
24
31
|
syncPanelOpen: false,
|
|
25
32
|
whatsupPanelOpen: false,
|
|
26
33
|
filePanelOpen: false,
|
|
27
|
-
|
|
34
|
+
settingsPanelOpen: false,
|
|
28
35
|
};
|
|
29
36
|
|
|
30
37
|
export const useUIStore = create<UIState>((set) => ({
|
|
31
38
|
...CLOSED,
|
|
39
|
+
settingsTab: "models",
|
|
32
40
|
toggleSessionPanel: () =>
|
|
33
41
|
set((s) => ({ ...CLOSED, sessionPanelOpen: !s.sessionPanelOpen })),
|
|
34
42
|
toggleSyncPanel: () =>
|
|
@@ -37,12 +45,15 @@ export const useUIStore = create<UIState>((set) => ({
|
|
|
37
45
|
set((s) => ({ ...CLOSED, whatsupPanelOpen: !s.whatsupPanelOpen })),
|
|
38
46
|
toggleFilePanel: () =>
|
|
39
47
|
set((s) => ({ ...CLOSED, filePanelOpen: !s.filePanelOpen })),
|
|
40
|
-
|
|
41
|
-
set((s) => ({ ...CLOSED,
|
|
48
|
+
toggleSettingsPanel: () =>
|
|
49
|
+
set((s) => ({ ...CLOSED, settingsPanelOpen: !s.settingsPanelOpen })),
|
|
42
50
|
closeAllPanels: () => set({ ...CLOSED }),
|
|
43
51
|
setSyncPanelOpen: (open) => set({ syncPanelOpen: open }),
|
|
44
52
|
setWhatsupPanelOpen: (open) => set({ whatsupPanelOpen: open }),
|
|
45
53
|
setSessionPanelOpen: (open) => set({ sessionPanelOpen: open }),
|
|
46
54
|
setFilePanelOpen: (open) => set({ filePanelOpen: open }),
|
|
47
|
-
|
|
55
|
+
setSettingsPanelOpen: (open) => set({ settingsPanelOpen: open }),
|
|
56
|
+
openSettings: (tab) =>
|
|
57
|
+
set({ ...CLOSED, settingsPanelOpen: true, settingsTab: tab }),
|
|
58
|
+
setSettingsTab: (tab) => set({ settingsTab: tab }),
|
|
48
59
|
}));
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Passkey management + sign out. Registration requires the current session
|
|
3
|
-
* (routes are behind the auth guard); credentials are RP-scoped, so entries
|
|
4
|
-
* registered on another hostname are badged and only useful there.
|
|
5
|
-
*/
|
|
6
|
-
export declare function PasskeyPanel({ open, onClose, }: {
|
|
7
|
-
open: boolean;
|
|
8
|
-
onClose: () => void;
|
|
9
|
-
}): import("react").JSX.Element;
|
|
10
|
-
//# sourceMappingURL=passkey-panel.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"passkey-panel.d.ts","sourceRoot":"","sources":["../../../src/components/settings/passkey-panel.tsx"],"names":[],"mappings":"AAkBA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EAC3B,IAAI,EACJ,OAAO,GACR,EAAE;IACD,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,+BAyJA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"passkey-panel.js","sourceRoot":"","sources":["../../../src/components/settings/passkey-panel.tsx"],"names":[],"mappings":"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"}
|