@frockbot/plugin-custom-models 0.0.0 → 0.3.13
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/frockbot.json +60 -0
- package/package.json +38 -6
- package/src/client/AccountModelsSection.vue +165 -0
- package/src/client/BotModelSection.vue +139 -0
- package/src/client/ProviderAccounts.vue +615 -0
- package/src/client/ProviderPackageSettings.vue +188 -0
- package/src/client/index.test.ts +182 -0
- package/src/client/index.ts +47 -0
- package/src/client/state.ts +94 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +5 -0
- package/src/manifest.test.ts +57 -0
- package/src/manifest.ts +3 -0
- package/src/model-resolution.test.ts +151 -0
- package/src/model-settings.ts +19 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +32 -0
- package/README.md +0 -3
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { clientSurfaceRegistryKey } from "@frockbot/client-core";
|
|
3
|
+
import { UiButton } from "@frockbot/client-ui";
|
|
4
|
+
import type { ConnectionView } from "@frockbot/configuration-core";
|
|
5
|
+
import {
|
|
6
|
+
frockBotWebDataKey,
|
|
7
|
+
type PluginCatalogItem,
|
|
8
|
+
} from "@frockbot/plugin-shell/shared";
|
|
9
|
+
import { computed, inject, ref } from "vue";
|
|
10
|
+
import ProviderPackageSettings from "./ProviderPackageSettings.vue";
|
|
11
|
+
|
|
12
|
+
const providedSurfaces = inject(clientSurfaceRegistryKey);
|
|
13
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
14
|
+
if (!providedSurfaces || !providedWeb) {
|
|
15
|
+
throw new Error("Custom models client services were not provided");
|
|
16
|
+
}
|
|
17
|
+
const surfaces = providedSurfaces;
|
|
18
|
+
const web = providedWeb;
|
|
19
|
+
|
|
20
|
+
const providers = computed(() =>
|
|
21
|
+
web.value.pluginCatalog.filter(
|
|
22
|
+
(item) =>
|
|
23
|
+
item.capabilities.some((capability) => capability.kind === "model") &&
|
|
24
|
+
web.value.userSettings?.packages.some(
|
|
25
|
+
(installation) =>
|
|
26
|
+
installation.packageId === item.packageId &&
|
|
27
|
+
installation.version === item.version &&
|
|
28
|
+
installation.state === "installed",
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const apiKeyPackageId = ref<string>();
|
|
34
|
+
const apiKeyConnectionTypeId = ref<string>();
|
|
35
|
+
const apiKeyLabel = ref("");
|
|
36
|
+
const apiKey = ref("");
|
|
37
|
+
const apiKeyBaseUrl = ref("");
|
|
38
|
+
const labelingConnectionId = ref<string>();
|
|
39
|
+
const connectionLabel = ref("");
|
|
40
|
+
const rotatingConnectionId = ref<string>();
|
|
41
|
+
const rotationKey = ref("");
|
|
42
|
+
/**
|
|
43
|
+
* Errors belong to the action that produced them, not to one global slot: a
|
|
44
|
+
* failure here disappears when that same action next succeeds, and closing the
|
|
45
|
+
* form takes it with it.
|
|
46
|
+
*/
|
|
47
|
+
const connectError = ref<string>();
|
|
48
|
+
const rowErrors = ref<Record<string, string>>({});
|
|
49
|
+
/** Whether the connect form has a command in flight. */
|
|
50
|
+
const connecting = ref(false);
|
|
51
|
+
/** Connection ids with a command in flight, so their row can say so. */
|
|
52
|
+
const busyConnections = ref<string[]>([]);
|
|
53
|
+
|
|
54
|
+
function failureMessage(error: unknown, fallback: string): string {
|
|
55
|
+
return error instanceof Error && error.message ? error.message : fallback;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function rowError(connectionId: string): string | undefined {
|
|
59
|
+
return rowErrors.value[connectionId];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isBusy(connectionId: string): boolean {
|
|
63
|
+
return busyConnections.value.includes(connectionId);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Run a Connection-scoped action, holding its busy and error state. */
|
|
67
|
+
async function runForConnection(
|
|
68
|
+
connectionId: string,
|
|
69
|
+
fallback: string,
|
|
70
|
+
action: () => Promise<unknown>,
|
|
71
|
+
): Promise<boolean> {
|
|
72
|
+
const { [connectionId]: _cleared, ...rest } = rowErrors.value;
|
|
73
|
+
rowErrors.value = rest;
|
|
74
|
+
busyConnections.value = [...busyConnections.value, connectionId];
|
|
75
|
+
try {
|
|
76
|
+
await action();
|
|
77
|
+
return true;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
rowErrors.value = {
|
|
80
|
+
...rowErrors.value,
|
|
81
|
+
[connectionId]: failureMessage(error, fallback),
|
|
82
|
+
};
|
|
83
|
+
return false;
|
|
84
|
+
} finally {
|
|
85
|
+
busyConnections.value = busyConnections.value.filter(
|
|
86
|
+
(id) => id !== connectionId,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function accountCount(item: PluginCatalogItem): string {
|
|
92
|
+
const count = connections(item).length;
|
|
93
|
+
if (count === 0) return "No account connected";
|
|
94
|
+
return count === 1 ? "1 account" : `${count} accounts`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** A `failed` Connection is retried in place rather than left to accumulate. */
|
|
98
|
+
function retryConnection(
|
|
99
|
+
item: PluginCatalogItem,
|
|
100
|
+
connection: ConnectionView,
|
|
101
|
+
): void {
|
|
102
|
+
beginConnect(item);
|
|
103
|
+
apiKeyLabel.value = connection.displayName;
|
|
104
|
+
const baseUrl = connection.settings?.["api-base-url"];
|
|
105
|
+
apiKeyBaseUrl.value = typeof baseUrl === "string" ? baseUrl : "";
|
|
106
|
+
retryingConnectionId.value = connection.connectionId;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const retryingConnectionId = ref<string>();
|
|
110
|
+
|
|
111
|
+
function connections(item: PluginCatalogItem): ConnectionView[] {
|
|
112
|
+
return (web.value.userSettings?.connections ?? []).filter(
|
|
113
|
+
(connection) =>
|
|
114
|
+
connection.packageId === item.packageId && connection.state !== "revoked",
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function apiKeyConnectionType(item: PluginCatalogItem) {
|
|
119
|
+
return item.connectionTypes.find(
|
|
120
|
+
(connectionType) => connectionType.authorizationKind === "api-key",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function mayConnect(item: PluginCatalogItem): boolean {
|
|
125
|
+
const connectionType = apiKeyConnectionType(item);
|
|
126
|
+
return Boolean(
|
|
127
|
+
connectionType &&
|
|
128
|
+
(connections(item).length === 0 || connectionType.allowMultiple),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function beginConnect(item: PluginCatalogItem): void {
|
|
133
|
+
const connectionType = apiKeyConnectionType(item);
|
|
134
|
+
if (!connectionType) return;
|
|
135
|
+
apiKeyPackageId.value = item.packageId;
|
|
136
|
+
apiKeyConnectionTypeId.value = connectionType.id;
|
|
137
|
+
apiKeyLabel.value = item.displayName;
|
|
138
|
+
apiKey.value = "";
|
|
139
|
+
apiKeyBaseUrl.value = "";
|
|
140
|
+
connectError.value = undefined;
|
|
141
|
+
retryingConnectionId.value = undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function cancelConnect(): void {
|
|
145
|
+
apiKeyPackageId.value = undefined;
|
|
146
|
+
apiKeyConnectionTypeId.value = undefined;
|
|
147
|
+
apiKey.value = "";
|
|
148
|
+
apiKeyBaseUrl.value = "";
|
|
149
|
+
connectError.value = undefined;
|
|
150
|
+
retryingConnectionId.value = undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function connectApiKey(): Promise<void> {
|
|
154
|
+
if (!apiKeyPackageId.value || !apiKeyConnectionTypeId.value) return;
|
|
155
|
+
if (connecting.value) return;
|
|
156
|
+
connecting.value = true;
|
|
157
|
+
connectError.value = undefined;
|
|
158
|
+
const superseded = retryingConnectionId.value;
|
|
159
|
+
try {
|
|
160
|
+
const apiBaseUrl = apiKeyBaseUrl.value.trim();
|
|
161
|
+
await web.value.createApiKeyConnection({
|
|
162
|
+
packageId: apiKeyPackageId.value,
|
|
163
|
+
connectionTypeId: apiKeyConnectionTypeId.value,
|
|
164
|
+
label: apiKeyLabel.value,
|
|
165
|
+
apiKey: apiKey.value,
|
|
166
|
+
...(apiBaseUrl ? { settings: { "api-base-url": apiBaseUrl } } : {}),
|
|
167
|
+
});
|
|
168
|
+
// The attempt this one replaces goes away instead of accumulating as a
|
|
169
|
+
// dead row the User has to clear by hand.
|
|
170
|
+
if (superseded) {
|
|
171
|
+
await web.value.disconnectConnection(superseded).catch(() => undefined);
|
|
172
|
+
}
|
|
173
|
+
cancelConnect();
|
|
174
|
+
} catch (error) {
|
|
175
|
+
apiKey.value = "";
|
|
176
|
+
connectError.value = failureMessage(error, "Could not create Connection");
|
|
177
|
+
} finally {
|
|
178
|
+
connecting.value = false;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function beginLabeling(connection: ConnectionView): void {
|
|
183
|
+
labelingConnectionId.value = connection.connectionId;
|
|
184
|
+
connectionLabel.value = connection.displayName;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function saveLabel(connectionId: string): Promise<void> {
|
|
188
|
+
const saved = await runForConnection(
|
|
189
|
+
connectionId,
|
|
190
|
+
"Could not rename Connection",
|
|
191
|
+
() => web.value.updateConnectionLabel(connectionId, connectionLabel.value),
|
|
192
|
+
);
|
|
193
|
+
if (!saved) return;
|
|
194
|
+
labelingConnectionId.value = undefined;
|
|
195
|
+
connectionLabel.value = "";
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function rotateApiKey(connectionId: string): Promise<void> {
|
|
199
|
+
const key = rotationKey.value;
|
|
200
|
+
rotationKey.value = "";
|
|
201
|
+
const rotated = await runForConnection(
|
|
202
|
+
connectionId,
|
|
203
|
+
"Could not rotate credential",
|
|
204
|
+
() => web.value.rotateApiKeyConnection(connectionId, key),
|
|
205
|
+
);
|
|
206
|
+
if (rotated) rotatingConnectionId.value = undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function refreshModels(connectionId: string): Promise<void> {
|
|
210
|
+
await runForConnection(connectionId, "Could not refresh models", () =>
|
|
211
|
+
web.value.refreshConnectionModels(connectionId),
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function setEnabled(
|
|
216
|
+
connectionId: string,
|
|
217
|
+
enabled: boolean,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
await runForConnection(connectionId, "Could not update Connection", () =>
|
|
220
|
+
web.value.setConnectionEnabled(connectionId, enabled),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function disconnect(connectionId: string): Promise<void> {
|
|
225
|
+
await runForConnection(connectionId, "Could not disconnect", () =>
|
|
226
|
+
web.value.disconnectConnection(connectionId),
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function revoke(packageId: string, connectionId: string): Promise<void> {
|
|
231
|
+
await runForConnection(connectionId, "Could not revoke Connection", () =>
|
|
232
|
+
web.value.revokeConnection(packageId, connectionId),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
</script>
|
|
236
|
+
|
|
237
|
+
<template>
|
|
238
|
+
<div class="provider-grid">
|
|
239
|
+
<article
|
|
240
|
+
v-for="item in providers"
|
|
241
|
+
:key="item.packageId"
|
|
242
|
+
class="provider-card"
|
|
243
|
+
>
|
|
244
|
+
<div class="provider-summary">
|
|
245
|
+
<span class="provider-logo" aria-hidden="true">
|
|
246
|
+
{{ item.displayName.slice(0, 1) }}
|
|
247
|
+
</span>
|
|
248
|
+
<span class="provider-copy">
|
|
249
|
+
<strong>{{ item.displayName }}</strong>
|
|
250
|
+
<small class="provider-count">{{ accountCount(item) }}</small>
|
|
251
|
+
</span>
|
|
252
|
+
<UiButton v-if="mayConnect(item)" @click="beginConnect(item)">
|
|
253
|
+
{{
|
|
254
|
+
connections(item).length === 0 ? "Connect" : "Add another account"
|
|
255
|
+
}}
|
|
256
|
+
</UiButton>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
<div
|
|
260
|
+
v-for="connection in connections(item)"
|
|
261
|
+
:key="connection.connectionId"
|
|
262
|
+
class="provider-account"
|
|
263
|
+
>
|
|
264
|
+
<div class="account-identity">
|
|
265
|
+
<span
|
|
266
|
+
class="account-dot"
|
|
267
|
+
:class="{
|
|
268
|
+
'account-dot--ready': connection.state === 'ready',
|
|
269
|
+
'account-dot--attention':
|
|
270
|
+
connection.state === 'failed' ||
|
|
271
|
+
connection.state === 'reconciliation-required',
|
|
272
|
+
}"
|
|
273
|
+
aria-hidden="true"
|
|
274
|
+
/>
|
|
275
|
+
<strong>{{ connection.displayName }}</strong>
|
|
276
|
+
<small>{{ connection.state }}</small>
|
|
277
|
+
<small v-if="connection.modelCatalog">
|
|
278
|
+
· models {{ connection.modelCatalog.state }}
|
|
279
|
+
</small>
|
|
280
|
+
</div>
|
|
281
|
+
<div class="account-actions">
|
|
282
|
+
<UiButton @click="beginLabeling(connection)">Rename</UiButton>
|
|
283
|
+
<template v-if="connection.authorization?.kind === 'api-key'">
|
|
284
|
+
<UiButton
|
|
285
|
+
v-if="connection.modelCatalog && connection.state === 'ready'"
|
|
286
|
+
@click="refreshModels(connection.connectionId)"
|
|
287
|
+
>
|
|
288
|
+
Refresh models
|
|
289
|
+
</UiButton>
|
|
290
|
+
<UiButton
|
|
291
|
+
v-if="connection.state === 'ready'"
|
|
292
|
+
@click="setEnabled(connection.connectionId, false)"
|
|
293
|
+
>
|
|
294
|
+
Disable
|
|
295
|
+
</UiButton>
|
|
296
|
+
<UiButton
|
|
297
|
+
v-if="connection.state === 'disabled'"
|
|
298
|
+
@click="setEnabled(connection.connectionId, true)"
|
|
299
|
+
>
|
|
300
|
+
Enable
|
|
301
|
+
</UiButton>
|
|
302
|
+
<UiButton
|
|
303
|
+
v-if="
|
|
304
|
+
connection.state === 'ready' || connection.state === 'disabled'
|
|
305
|
+
"
|
|
306
|
+
@click="rotatingConnectionId = connection.connectionId"
|
|
307
|
+
>
|
|
308
|
+
Rotate key
|
|
309
|
+
</UiButton>
|
|
310
|
+
<UiButton
|
|
311
|
+
v-if="connection.state === 'failed'"
|
|
312
|
+
variant="primary"
|
|
313
|
+
@click="retryConnection(item, connection)"
|
|
314
|
+
>
|
|
315
|
+
Try again
|
|
316
|
+
</UiButton>
|
|
317
|
+
<UiButton
|
|
318
|
+
v-if="connection.state !== 'revoking'"
|
|
319
|
+
variant="danger"
|
|
320
|
+
:disabled="isBusy(connection.connectionId)"
|
|
321
|
+
@click="disconnect(connection.connectionId)"
|
|
322
|
+
>
|
|
323
|
+
Disconnect
|
|
324
|
+
</UiButton>
|
|
325
|
+
</template>
|
|
326
|
+
<UiButton
|
|
327
|
+
v-else-if="connection.state !== 'revoking'"
|
|
328
|
+
variant="danger"
|
|
329
|
+
@click="revoke(connection.packageId, connection.connectionId)"
|
|
330
|
+
>
|
|
331
|
+
Revoke
|
|
332
|
+
</UiButton>
|
|
333
|
+
</div>
|
|
334
|
+
<p v-if="isBusy(connection.connectionId)" class="connection-busy">
|
|
335
|
+
Working…
|
|
336
|
+
</p>
|
|
337
|
+
<p
|
|
338
|
+
v-if="rowError(connection.connectionId)"
|
|
339
|
+
class="connection-failure"
|
|
340
|
+
role="alert"
|
|
341
|
+
>
|
|
342
|
+
{{ rowError(connection.connectionId) }}
|
|
343
|
+
</p>
|
|
344
|
+
<p v-if="connection.failure" class="connection-failure" role="alert">
|
|
345
|
+
{{ connection.failure }}
|
|
346
|
+
</p>
|
|
347
|
+
<p
|
|
348
|
+
v-if="connection.modelCatalog?.failure"
|
|
349
|
+
class="connection-failure"
|
|
350
|
+
role="alert"
|
|
351
|
+
>
|
|
352
|
+
{{ connection.modelCatalog.failure }}
|
|
353
|
+
</p>
|
|
354
|
+
<form
|
|
355
|
+
v-if="labelingConnectionId === connection.connectionId"
|
|
356
|
+
class="inline-form"
|
|
357
|
+
@submit.prevent="saveLabel(connection.connectionId)"
|
|
358
|
+
>
|
|
359
|
+
<input
|
|
360
|
+
v-model="connectionLabel"
|
|
361
|
+
maxlength="120"
|
|
362
|
+
aria-label="Connection label"
|
|
363
|
+
required
|
|
364
|
+
/>
|
|
365
|
+
<UiButton type="submit">Save label</UiButton>
|
|
366
|
+
</form>
|
|
367
|
+
<form
|
|
368
|
+
v-if="rotatingConnectionId === connection.connectionId"
|
|
369
|
+
class="inline-form"
|
|
370
|
+
@submit.prevent="rotateApiKey(connection.connectionId)"
|
|
371
|
+
>
|
|
372
|
+
<input
|
|
373
|
+
v-model="rotationKey"
|
|
374
|
+
type="password"
|
|
375
|
+
autocomplete="new-password"
|
|
376
|
+
aria-label="New API key"
|
|
377
|
+
required
|
|
378
|
+
/>
|
|
379
|
+
<UiButton type="submit">Save new key</UiButton>
|
|
380
|
+
</form>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
<form
|
|
384
|
+
v-if="apiKeyPackageId === item.packageId"
|
|
385
|
+
class="api-key-form"
|
|
386
|
+
@submit.prevent="connectApiKey"
|
|
387
|
+
>
|
|
388
|
+
<label>
|
|
389
|
+
<span>Connection label</span>
|
|
390
|
+
<input v-model="apiKeyLabel" maxlength="120" required />
|
|
391
|
+
</label>
|
|
392
|
+
<label>
|
|
393
|
+
<span>API key</span>
|
|
394
|
+
<input
|
|
395
|
+
v-model="apiKey"
|
|
396
|
+
type="password"
|
|
397
|
+
autocomplete="new-password"
|
|
398
|
+
required
|
|
399
|
+
/>
|
|
400
|
+
</label>
|
|
401
|
+
<label v-if="item.packageId === 'provider-ollama-cloud'">
|
|
402
|
+
<span>API base URL</span>
|
|
403
|
+
<input
|
|
404
|
+
v-model="apiKeyBaseUrl"
|
|
405
|
+
type="url"
|
|
406
|
+
inputmode="url"
|
|
407
|
+
autocomplete="off"
|
|
408
|
+
maxlength="2048"
|
|
409
|
+
placeholder="https://ollama.com"
|
|
410
|
+
/>
|
|
411
|
+
</label>
|
|
412
|
+
<p v-if="item.packageId === 'provider-ollama-cloud'" class="field-hint">
|
|
413
|
+
Leave empty for Ollama Cloud. Point this at a local Ollama server or
|
|
414
|
+
another Ollama-compatible endpoint.
|
|
415
|
+
</p>
|
|
416
|
+
<p v-if="connectError" class="connection-failure" role="alert">
|
|
417
|
+
{{ connectError }}
|
|
418
|
+
</p>
|
|
419
|
+
<div class="api-key-actions">
|
|
420
|
+
<UiButton :disabled="connecting" @click="cancelConnect">
|
|
421
|
+
Cancel
|
|
422
|
+
</UiButton>
|
|
423
|
+
<UiButton type="submit" variant="primary" :disabled="connecting">
|
|
424
|
+
{{ connecting ? "Connecting…" : "Connect account" }}
|
|
425
|
+
</UiButton>
|
|
426
|
+
</div>
|
|
427
|
+
</form>
|
|
428
|
+
|
|
429
|
+
<ProviderPackageSettings :item="item" />
|
|
430
|
+
</article>
|
|
431
|
+
|
|
432
|
+
<div v-if="providers.length === 0" class="provider-empty">
|
|
433
|
+
<p>No model provider is enabled.</p>
|
|
434
|
+
<UiButton @click="surfaces.open('plugins')">Open Plugins</UiButton>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</template>
|
|
438
|
+
|
|
439
|
+
<style scoped>
|
|
440
|
+
.provider-grid {
|
|
441
|
+
display: grid;
|
|
442
|
+
gap: 12px;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
.provider-card {
|
|
446
|
+
min-width: 0;
|
|
447
|
+
padding: 8px;
|
|
448
|
+
border: 1px solid var(--frock-border);
|
|
449
|
+
border-radius: var(--frock-radius-card);
|
|
450
|
+
background: var(--frock-surface-raised);
|
|
451
|
+
box-shadow: var(--frock-shadow-card);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.provider-summary {
|
|
455
|
+
display: grid;
|
|
456
|
+
width: 100%;
|
|
457
|
+
min-width: 0;
|
|
458
|
+
grid-template-columns: 44px minmax(0, 1fr) auto;
|
|
459
|
+
align-items: center;
|
|
460
|
+
gap: 12px;
|
|
461
|
+
padding: 8px;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.provider-logo {
|
|
465
|
+
display: grid;
|
|
466
|
+
width: 44px;
|
|
467
|
+
height: 44px;
|
|
468
|
+
place-items: center;
|
|
469
|
+
border-radius: 12px;
|
|
470
|
+
color: var(--frock-action-secondary-text);
|
|
471
|
+
background: var(--frock-surface-accent);
|
|
472
|
+
font-weight: 800;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.provider-copy {
|
|
476
|
+
min-width: 0;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* Written as it should read; no transform decides its capitalisation. */
|
|
480
|
+
.provider-count,
|
|
481
|
+
.connection-busy {
|
|
482
|
+
text-transform: none;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.connection-busy {
|
|
486
|
+
margin: 4px 0 0;
|
|
487
|
+
color: var(--frock-text-muted, inherit);
|
|
488
|
+
font-size: var(--frock-text-sm);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.provider-copy strong,
|
|
492
|
+
.provider-copy small {
|
|
493
|
+
display: block;
|
|
494
|
+
overflow: hidden;
|
|
495
|
+
white-space: nowrap;
|
|
496
|
+
text-overflow: ellipsis;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.provider-copy strong,
|
|
500
|
+
.account-identity strong {
|
|
501
|
+
font-size: var(--frock-text-md);
|
|
502
|
+
font-weight: 600;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.provider-copy small,
|
|
506
|
+
.account-identity small {
|
|
507
|
+
margin-top: 4px;
|
|
508
|
+
color: var(--frock-text-muted);
|
|
509
|
+
font-size: var(--frock-text-sm);
|
|
510
|
+
text-transform: capitalize;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.provider-account {
|
|
514
|
+
display: grid;
|
|
515
|
+
gap: 8px;
|
|
516
|
+
padding: 12px 8px;
|
|
517
|
+
border-top: 1px solid var(--frock-border);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
.account-identity {
|
|
521
|
+
display: flex;
|
|
522
|
+
min-width: 0;
|
|
523
|
+
flex-wrap: wrap;
|
|
524
|
+
align-items: baseline;
|
|
525
|
+
gap: 8px;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.account-dot {
|
|
529
|
+
width: 8px;
|
|
530
|
+
height: 8px;
|
|
531
|
+
align-self: center;
|
|
532
|
+
border-radius: 999px;
|
|
533
|
+
background: var(--frock-text-subtle);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.account-dot--ready {
|
|
537
|
+
background: var(--frock-success);
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.account-dot--attention {
|
|
541
|
+
background: var(--frock-danger-text);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.account-actions,
|
|
545
|
+
.api-key-actions {
|
|
546
|
+
display: flex;
|
|
547
|
+
flex-wrap: wrap;
|
|
548
|
+
justify-content: flex-end;
|
|
549
|
+
gap: 8px;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
.account-actions :deep(.ui-button),
|
|
553
|
+
.inline-form :deep(.ui-button) {
|
|
554
|
+
min-height: 28px;
|
|
555
|
+
padding: 0 10px;
|
|
556
|
+
font-size: var(--frock-text-sm);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.inline-form {
|
|
560
|
+
display: grid;
|
|
561
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
562
|
+
gap: 8px;
|
|
563
|
+
animation: frock-rise-in var(--frock-motion-panel) both;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
.inline-form input,
|
|
567
|
+
.api-key-form input {
|
|
568
|
+
min-width: 0;
|
|
569
|
+
padding: 8px 11px;
|
|
570
|
+
border: 1px solid var(--frock-border);
|
|
571
|
+
border-radius: var(--frock-radius-control);
|
|
572
|
+
background: var(--frock-surface-raised);
|
|
573
|
+
color: var(--frock-text);
|
|
574
|
+
font-size: var(--frock-text-base);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.connection-failure {
|
|
578
|
+
margin: 0;
|
|
579
|
+
color: var(--frock-danger-text);
|
|
580
|
+
font-size: var(--frock-text-sm);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.api-key-form {
|
|
584
|
+
display: grid;
|
|
585
|
+
gap: 12px;
|
|
586
|
+
margin: 0 8px;
|
|
587
|
+
padding: 12px 0 8px;
|
|
588
|
+
border-top: 1px solid var(--frock-border);
|
|
589
|
+
animation: frock-rise-in var(--frock-motion-enter) both;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.api-key-form label {
|
|
593
|
+
display: grid;
|
|
594
|
+
gap: 6px;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
.api-key-form span,
|
|
598
|
+
.field-hint,
|
|
599
|
+
.provider-empty p {
|
|
600
|
+
margin: 0;
|
|
601
|
+
color: var(--frock-text-muted);
|
|
602
|
+
font-size: var(--frock-text-sm);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.provider-empty {
|
|
606
|
+
display: flex;
|
|
607
|
+
flex-direction: column;
|
|
608
|
+
align-items: flex-start;
|
|
609
|
+
gap: 10px;
|
|
610
|
+
padding: 12px;
|
|
611
|
+
border: 1px solid var(--frock-border);
|
|
612
|
+
border-radius: var(--frock-radius-card);
|
|
613
|
+
background: var(--frock-surface-subtle);
|
|
614
|
+
}
|
|
615
|
+
</style>
|