@frockbot/plugin-settings 0.0.0 → 0.1.1
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 +55 -0
- package/package.json +43 -6
- package/src/backend.test.ts +397 -0
- package/src/backend.ts +262 -0
- package/src/client/BotPanel.vue +57 -0
- package/src/client/BotSettingsSurface.vue +1076 -0
- package/src/client/BotSettingsTrigger.vue +26 -0
- package/src/client/ConnectionsSurface.vue +813 -0
- package/src/client/ModelsSurface.vue +419 -0
- package/src/client/PackageAccounts.vue +330 -0
- package/src/client/PackageCatalogSurface.vue +584 -0
- package/src/client/PackageSettingsForm.vue +150 -0
- package/src/client/PackageSettingsSection.vue +66 -0
- package/src/client/PluginsSurface.vue +412 -0
- package/src/client/PluginsTrigger.vue +62 -0
- package/src/client/UserProfileTrigger.vue +223 -0
- package/src/client/UserSettingsSurface.vue +242 -0
- package/src/client/assignment-operations.ts +22 -0
- package/src/client/bot-settings.test.ts +218 -0
- package/src/client/bot-settings.ts +150 -0
- package/src/client/index.test.ts +113 -0
- package/src/client/index.ts +78 -0
- package/src/client/package-settings.test.ts +63 -0
- package/src/client/package-settings.ts +77 -0
- package/src/client/package-surfaces.test.ts +147 -0
- package/src/client/package-surfaces.ts +93 -0
- package/src/client/user-display-name.test.ts +44 -0
- package/src/client/user-display-name.ts +16 -0
- package/src/env.d.ts +6 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +3 -0
- package/src/user.test.ts +1277 -0
- package/src/user.ts +1221 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +30 -0
- package/README.md +0 -3
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* Models: where a User configures model provider Packages and picks a model.
|
|
4
|
+
*
|
|
5
|
+
* Enablement is not here — Plugins turns a provider on and off, and a disabled
|
|
6
|
+
* provider disappears from this surface with its accounts and its catalogs
|
|
7
|
+
* intact.
|
|
8
|
+
*/
|
|
9
|
+
import { clientSurfaceRegistryKey } from "@frockbot/client-core";
|
|
10
|
+
import { UiAnchor, UiButton, UiField } from "@frockbot/client-ui";
|
|
11
|
+
import {
|
|
12
|
+
frockBotWebDataKey,
|
|
13
|
+
type PluginCatalogItem,
|
|
14
|
+
} from "@frockbot/plugin-shell/shared";
|
|
15
|
+
import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
|
|
16
|
+
import { computed, inject, onMounted, ref } from "vue";
|
|
17
|
+
import {
|
|
18
|
+
decodeModelSelection,
|
|
19
|
+
eligibleModelConnections,
|
|
20
|
+
encodeModelSelection,
|
|
21
|
+
modelSelectOptions,
|
|
22
|
+
} from "./bot-settings.js";
|
|
23
|
+
import { configurablePackages } from "./package-surfaces.js";
|
|
24
|
+
import PackageAccounts from "./PackageAccounts.vue";
|
|
25
|
+
import PackageSettingsForm from "./PackageSettingsForm.vue";
|
|
26
|
+
|
|
27
|
+
const providedSurfaces = inject(clientSurfaceRegistryKey);
|
|
28
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
29
|
+
if (!providedSurfaces || !providedWeb) {
|
|
30
|
+
throw new Error("settings client services were not provided");
|
|
31
|
+
}
|
|
32
|
+
const surfaces = providedSurfaces;
|
|
33
|
+
const web = providedWeb;
|
|
34
|
+
// Model configuration is User-scoped, so these links name no Bot.
|
|
35
|
+
const defaultModelLink = settingsLinkV1({ anchor: "user-default-model" });
|
|
36
|
+
const providersLink = settingsLinkV1({ anchor: "user-model-providers" });
|
|
37
|
+
|
|
38
|
+
const defaultModel = ref("");
|
|
39
|
+
const savingModel = ref(false);
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The endpoint root of an Ollama Cloud Connection, declared by that Connection
|
|
43
|
+
* Type as `api-base-url`. It is the only Connection Type setting the shared
|
|
44
|
+
* API-key form has a field for.
|
|
45
|
+
*/
|
|
46
|
+
const OLLAMA_PACKAGE_ID = "provider-ollama-cloud";
|
|
47
|
+
const apiKeyPackageId = ref<string>();
|
|
48
|
+
const apiKeyConnectionTypeId = ref<string>();
|
|
49
|
+
const apiKeyLabel = ref("");
|
|
50
|
+
const apiKey = ref("");
|
|
51
|
+
const apiKeyBaseUrl = ref("");
|
|
52
|
+
|
|
53
|
+
const providers = computed(() =>
|
|
54
|
+
configurablePackages({
|
|
55
|
+
catalog: web.value.pluginCatalog,
|
|
56
|
+
packages: web.value.userSettings?.packages ?? [],
|
|
57
|
+
home: "models",
|
|
58
|
+
}),
|
|
59
|
+
);
|
|
60
|
+
const readyConnections = computed(() =>
|
|
61
|
+
eligibleModelConnections({
|
|
62
|
+
connections: web.value.userSettings?.connections ?? [],
|
|
63
|
+
packages: web.value.userSettings?.packages ?? [],
|
|
64
|
+
catalog: web.value.pluginCatalog,
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
const modelOptions = computed(() => modelSelectOptions(readyConnections.value));
|
|
68
|
+
|
|
69
|
+
onMounted(async () => {
|
|
70
|
+
await web.value.loadPluginCatalog();
|
|
71
|
+
defaultModel.value = encodeModelSelection(
|
|
72
|
+
web.value.userSettings?.newBotModelTemplate,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
async function saveDefaultModel(): Promise<void> {
|
|
77
|
+
savingModel.value = true;
|
|
78
|
+
try {
|
|
79
|
+
await web.value.saveDefaultModel(decodeModelSelection(defaultModel.value));
|
|
80
|
+
} catch (error) {
|
|
81
|
+
web.value.settingsError =
|
|
82
|
+
error instanceof Error ? error.message : "Could not save the model";
|
|
83
|
+
} finally {
|
|
84
|
+
savingModel.value = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function connectionCount(packageId: string): number {
|
|
89
|
+
return (web.value.userSettings?.connections ?? []).filter(
|
|
90
|
+
(connection) =>
|
|
91
|
+
connection.packageId === packageId && connection.state !== "revoked",
|
|
92
|
+
).length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function beginConnect(item: PluginCatalogItem): void {
|
|
96
|
+
const connectionType = item.connectionTypes[0];
|
|
97
|
+
if (!connectionType) return;
|
|
98
|
+
apiKeyPackageId.value = item.packageId;
|
|
99
|
+
apiKeyConnectionTypeId.value = connectionType.id;
|
|
100
|
+
apiKeyLabel.value = item.displayName;
|
|
101
|
+
apiKey.value = "";
|
|
102
|
+
apiKeyBaseUrl.value = "";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function cancelConnect(): void {
|
|
106
|
+
apiKey.value = "";
|
|
107
|
+
apiKeyBaseUrl.value = "";
|
|
108
|
+
apiKeyPackageId.value = undefined;
|
|
109
|
+
apiKeyConnectionTypeId.value = undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function connectApiKey(): Promise<void> {
|
|
113
|
+
if (!apiKeyPackageId.value || !apiKeyConnectionTypeId.value) return;
|
|
114
|
+
try {
|
|
115
|
+
// An empty endpoint field means the Package default, so the command
|
|
116
|
+
// carries no settings at all rather than an empty bag.
|
|
117
|
+
const apiBaseUrl = apiKeyBaseUrl.value.trim();
|
|
118
|
+
await web.value.createApiKeyConnection({
|
|
119
|
+
packageId: apiKeyPackageId.value,
|
|
120
|
+
connectionTypeId: apiKeyConnectionTypeId.value,
|
|
121
|
+
label: apiKeyLabel.value,
|
|
122
|
+
apiKey: apiKey.value,
|
|
123
|
+
...(apiBaseUrl ? { settings: { "api-base-url": apiBaseUrl } } : {}),
|
|
124
|
+
});
|
|
125
|
+
cancelConnect();
|
|
126
|
+
} catch (error) {
|
|
127
|
+
apiKey.value = "";
|
|
128
|
+
web.value.settingsError =
|
|
129
|
+
error instanceof Error ? error.message : "Could not create Connection";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<template>
|
|
135
|
+
<div class="models-surface">
|
|
136
|
+
<UiAnchor
|
|
137
|
+
anchor="user-default-model"
|
|
138
|
+
label="Default model"
|
|
139
|
+
:href="defaultModelLink"
|
|
140
|
+
class="settings-row"
|
|
141
|
+
>
|
|
142
|
+
<UiField
|
|
143
|
+
v-if="modelOptions.length > 0"
|
|
144
|
+
label="Default model"
|
|
145
|
+
hint="used by every Bot"
|
|
146
|
+
>
|
|
147
|
+
<select v-model="defaultModel">
|
|
148
|
+
<option value="">No default model</option>
|
|
149
|
+
<option
|
|
150
|
+
v-for="model in modelOptions"
|
|
151
|
+
:key="model.value"
|
|
152
|
+
:value="model.value"
|
|
153
|
+
>
|
|
154
|
+
{{ model.label }}
|
|
155
|
+
</option>
|
|
156
|
+
</select>
|
|
157
|
+
</UiField>
|
|
158
|
+
<p v-if="modelOptions.length > 0" class="field-hint">
|
|
159
|
+
Used by every Bot unless a Bot overrides it in its own settings.
|
|
160
|
+
</p>
|
|
161
|
+
<div v-else class="model-empty">
|
|
162
|
+
<p>
|
|
163
|
+
Connect a model provider below, then choose the model your Bots use.
|
|
164
|
+
</p>
|
|
165
|
+
</div>
|
|
166
|
+
<div v-if="modelOptions.length > 0" class="settings-actions">
|
|
167
|
+
<UiButton
|
|
168
|
+
type="button"
|
|
169
|
+
variant="primary"
|
|
170
|
+
:disabled="savingModel"
|
|
171
|
+
@click="saveDefaultModel"
|
|
172
|
+
>
|
|
173
|
+
{{ savingModel ? "Saving…" : "Save model" }}
|
|
174
|
+
</UiButton>
|
|
175
|
+
</div>
|
|
176
|
+
</UiAnchor>
|
|
177
|
+
|
|
178
|
+
<UiAnchor
|
|
179
|
+
anchor="user-model-providers"
|
|
180
|
+
label="Model providers"
|
|
181
|
+
:href="providersLink"
|
|
182
|
+
class="settings-row"
|
|
183
|
+
>
|
|
184
|
+
<p class="field-hint">
|
|
185
|
+
Accounts and endpoints for the model providers you have enabled.
|
|
186
|
+
</p>
|
|
187
|
+
<div class="provider-grid">
|
|
188
|
+
<article
|
|
189
|
+
v-for="item in providers"
|
|
190
|
+
:key="item.packageId"
|
|
191
|
+
class="provider-card"
|
|
192
|
+
>
|
|
193
|
+
<div class="provider-summary">
|
|
194
|
+
<span class="provider-logo" aria-hidden="true">
|
|
195
|
+
{{ item.displayName.slice(0, 1) }}
|
|
196
|
+
</span>
|
|
197
|
+
<span class="provider-copy">
|
|
198
|
+
<strong>{{ item.displayName }}</strong>
|
|
199
|
+
<small>
|
|
200
|
+
{{
|
|
201
|
+
connectionCount(item.packageId) === 0
|
|
202
|
+
? "No account connected"
|
|
203
|
+
: `${connectionCount(item.packageId)} account(s)`
|
|
204
|
+
}}
|
|
205
|
+
</small>
|
|
206
|
+
</span>
|
|
207
|
+
<UiButton
|
|
208
|
+
v-if="
|
|
209
|
+
connectionCount(item.packageId) === 0 ||
|
|
210
|
+
item.connectionTypes[0]?.allowMultiple
|
|
211
|
+
"
|
|
212
|
+
@click="beginConnect(item)"
|
|
213
|
+
>
|
|
214
|
+
{{
|
|
215
|
+
connectionCount(item.packageId) === 0
|
|
216
|
+
? "Connect"
|
|
217
|
+
: "Add another account"
|
|
218
|
+
}}
|
|
219
|
+
</UiButton>
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<PackageAccounts :item="item" />
|
|
223
|
+
|
|
224
|
+
<form
|
|
225
|
+
v-if="apiKeyPackageId === item.packageId"
|
|
226
|
+
class="api-key-form"
|
|
227
|
+
@submit.prevent="connectApiKey"
|
|
228
|
+
>
|
|
229
|
+
<label>
|
|
230
|
+
<span>Connection label</span>
|
|
231
|
+
<input v-model="apiKeyLabel" maxlength="120" required />
|
|
232
|
+
</label>
|
|
233
|
+
<label>
|
|
234
|
+
<span>API key</span>
|
|
235
|
+
<input
|
|
236
|
+
v-model="apiKey"
|
|
237
|
+
type="password"
|
|
238
|
+
autocomplete="new-password"
|
|
239
|
+
required
|
|
240
|
+
/>
|
|
241
|
+
</label>
|
|
242
|
+
<label v-if="item.packageId === OLLAMA_PACKAGE_ID">
|
|
243
|
+
<span>API base URL</span>
|
|
244
|
+
<input
|
|
245
|
+
v-model="apiKeyBaseUrl"
|
|
246
|
+
type="url"
|
|
247
|
+
inputmode="url"
|
|
248
|
+
autocomplete="off"
|
|
249
|
+
maxlength="2048"
|
|
250
|
+
placeholder="https://ollama.com"
|
|
251
|
+
/>
|
|
252
|
+
</label>
|
|
253
|
+
<p v-if="item.packageId === OLLAMA_PACKAGE_ID" class="field-hint">
|
|
254
|
+
Leave empty for Ollama Cloud. Point this at a local Ollama server
|
|
255
|
+
(http://127.0.0.1:11434) or any Ollama-compatible endpoint.
|
|
256
|
+
</p>
|
|
257
|
+
<div class="api-key-actions">
|
|
258
|
+
<UiButton @click="cancelConnect">Cancel</UiButton>
|
|
259
|
+
<UiButton type="submit" variant="primary">
|
|
260
|
+
Connect account
|
|
261
|
+
</UiButton>
|
|
262
|
+
</div>
|
|
263
|
+
</form>
|
|
264
|
+
|
|
265
|
+
<PackageSettingsForm :item="item" />
|
|
266
|
+
</article>
|
|
267
|
+
</div>
|
|
268
|
+
<div v-if="providers.length === 0" class="model-empty">
|
|
269
|
+
<p>No model provider is enabled.</p>
|
|
270
|
+
<UiButton type="button" @click="surfaces.open('plugins')">
|
|
271
|
+
Open Plugins
|
|
272
|
+
</UiButton>
|
|
273
|
+
</div>
|
|
274
|
+
</UiAnchor>
|
|
275
|
+
|
|
276
|
+
<p v-if="web.settingsError" class="settings-error" role="alert">
|
|
277
|
+
{{ web.settingsError }}
|
|
278
|
+
</p>
|
|
279
|
+
</div>
|
|
280
|
+
</template>
|
|
281
|
+
|
|
282
|
+
<style scoped>
|
|
283
|
+
.models-surface {
|
|
284
|
+
display: flex;
|
|
285
|
+
flex-direction: column;
|
|
286
|
+
gap: 16px;
|
|
287
|
+
padding: 16px;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.settings-row {
|
|
291
|
+
display: flex;
|
|
292
|
+
flex-direction: column;
|
|
293
|
+
gap: 16px;
|
|
294
|
+
padding-right: var(--frock-control-sm);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.field-hint {
|
|
298
|
+
margin: 0;
|
|
299
|
+
color: var(--frock-text-muted);
|
|
300
|
+
font-size: var(--frock-text-sm);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.model-empty {
|
|
304
|
+
display: flex;
|
|
305
|
+
flex-direction: column;
|
|
306
|
+
align-items: flex-start;
|
|
307
|
+
gap: 10px;
|
|
308
|
+
padding: 12px;
|
|
309
|
+
border: 1px solid var(--frock-border);
|
|
310
|
+
border-radius: var(--frock-radius-card);
|
|
311
|
+
background: var(--frock-surface-subtle);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.model-empty p {
|
|
315
|
+
margin: 0;
|
|
316
|
+
color: var(--frock-text-muted);
|
|
317
|
+
font-size: var(--frock-text-sm);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.provider-grid {
|
|
321
|
+
display: grid;
|
|
322
|
+
gap: 12px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.provider-card {
|
|
326
|
+
min-width: 0;
|
|
327
|
+
padding: 8px;
|
|
328
|
+
border: 1px solid var(--frock-border);
|
|
329
|
+
border-radius: var(--frock-radius-card);
|
|
330
|
+
background: var(--frock-surface-raised);
|
|
331
|
+
box-shadow: var(--frock-shadow-card);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.provider-summary {
|
|
335
|
+
display: grid;
|
|
336
|
+
width: 100%;
|
|
337
|
+
min-width: 0;
|
|
338
|
+
grid-template-columns: 44px minmax(0, 1fr) auto;
|
|
339
|
+
align-items: center;
|
|
340
|
+
gap: 12px;
|
|
341
|
+
padding: 8px;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.provider-logo {
|
|
345
|
+
display: grid;
|
|
346
|
+
width: 44px;
|
|
347
|
+
height: 44px;
|
|
348
|
+
place-items: center;
|
|
349
|
+
border-radius: 12px;
|
|
350
|
+
color: var(--frock-action-secondary-text);
|
|
351
|
+
background: var(--frock-surface-accent);
|
|
352
|
+
font-weight: 800;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
.provider-copy {
|
|
356
|
+
min-width: 0;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
.provider-copy strong,
|
|
360
|
+
.provider-copy small {
|
|
361
|
+
display: block;
|
|
362
|
+
overflow: hidden;
|
|
363
|
+
white-space: nowrap;
|
|
364
|
+
text-overflow: ellipsis;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
.provider-copy strong {
|
|
368
|
+
font-size: var(--frock-text-md);
|
|
369
|
+
font-weight: 600;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
.provider-copy small {
|
|
373
|
+
margin-top: 4px;
|
|
374
|
+
color: var(--frock-text-muted);
|
|
375
|
+
font-size: var(--frock-text-sm);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.api-key-form {
|
|
379
|
+
display: grid;
|
|
380
|
+
gap: 12px;
|
|
381
|
+
margin: 0 8px;
|
|
382
|
+
padding: 12px 0 8px;
|
|
383
|
+
border-top: 1px solid var(--frock-border);
|
|
384
|
+
animation: frock-rise-in var(--frock-motion-enter) both;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.api-key-form label {
|
|
388
|
+
display: grid;
|
|
389
|
+
gap: 6px;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.api-key-form span {
|
|
393
|
+
color: var(--frock-text-muted);
|
|
394
|
+
font-size: var(--frock-text-sm);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.api-key-form input {
|
|
398
|
+
min-width: 0;
|
|
399
|
+
padding: 8px 11px;
|
|
400
|
+
border: 1px solid var(--frock-border);
|
|
401
|
+
border-radius: 9px;
|
|
402
|
+
background: var(--frock-surface-raised);
|
|
403
|
+
color: var(--frock-text);
|
|
404
|
+
font-size: var(--frock-text-base);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.api-key-actions,
|
|
408
|
+
.settings-actions {
|
|
409
|
+
display: flex;
|
|
410
|
+
justify-content: flex-end;
|
|
411
|
+
gap: 8px;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.settings-error {
|
|
415
|
+
margin: 0;
|
|
416
|
+
color: var(--frock-danger-text);
|
|
417
|
+
font-size: var(--frock-text-sm);
|
|
418
|
+
}
|
|
419
|
+
</style>
|