@frockbot/plugin-settings 0.0.0 → 0.1.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/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,330 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* The accounts of one Package: every Connection it owns, with the durable
|
|
4
|
+
* actions each one offers.
|
|
5
|
+
*
|
|
6
|
+
* Shared by Models and Connections, which differ in what they add around it —
|
|
7
|
+
* a provider catalog and a model choice on one, an authorization handoff on
|
|
8
|
+
* the other — and not in how an account is renamed, rotated, disabled, or
|
|
9
|
+
* disconnected.
|
|
10
|
+
*/
|
|
11
|
+
import { UiButton } from "@frockbot/client-ui";
|
|
12
|
+
import type { ConnectionView } from "@frockbot/configuration-core";
|
|
13
|
+
import {
|
|
14
|
+
frockBotWebDataKey,
|
|
15
|
+
type PluginCatalogItem,
|
|
16
|
+
} from "@frockbot/plugin-shell/shared";
|
|
17
|
+
import { computed, inject, ref } from "vue";
|
|
18
|
+
|
|
19
|
+
const props = defineProps<{ item: PluginCatalogItem }>();
|
|
20
|
+
|
|
21
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
22
|
+
if (!providedWeb) throw new Error("shell client data was not provided");
|
|
23
|
+
const web = providedWeb;
|
|
24
|
+
|
|
25
|
+
const rotatingConnectionId = ref<string>();
|
|
26
|
+
const rotationKey = ref("");
|
|
27
|
+
const labelingConnectionId = ref<string>();
|
|
28
|
+
const connectionLabel = ref("");
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The accounts this card speaks for. A revoked Connection is a tombstone the
|
|
32
|
+
* User has already dismissed, so it is not listed.
|
|
33
|
+
*/
|
|
34
|
+
const connections = computed<ConnectionView[]>(() =>
|
|
35
|
+
(web.value.userSettings?.connections ?? []).filter(
|
|
36
|
+
(connection) =>
|
|
37
|
+
connection.packageId === props.item.packageId &&
|
|
38
|
+
connection.state !== "revoked",
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
type StatusTone = "ready" | "muted" | "attention";
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The User's default model, when this account is the one serving it. Shown on
|
|
46
|
+
* the account it belongs to; changing it is the row at the top of Models.
|
|
47
|
+
*/
|
|
48
|
+
function defaultModelName(connection: ConnectionView): string | undefined {
|
|
49
|
+
const selected = web.value.userSettings?.newBotModelTemplate;
|
|
50
|
+
if (selected?.connectionId !== connection.connectionId) return undefined;
|
|
51
|
+
return (
|
|
52
|
+
connection.modelCatalog?.models.find(
|
|
53
|
+
(model) => model.providerModelId === selected.providerModelId,
|
|
54
|
+
)?.displayName ?? selected.providerModelId
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function connectionTone(connection: ConnectionView): StatusTone {
|
|
59
|
+
if (connection.state === "ready") return "ready";
|
|
60
|
+
if (connection.state === "failed") return "attention";
|
|
61
|
+
if (connection.state === "reconciliation-required") return "attention";
|
|
62
|
+
return "muted";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function beginLabeling(connection: ConnectionView): void {
|
|
66
|
+
labelingConnectionId.value = connection.connectionId;
|
|
67
|
+
connectionLabel.value = connection.displayName;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function saveLabel(connectionId: string): Promise<void> {
|
|
71
|
+
try {
|
|
72
|
+
await web.value.updateConnectionLabel(connectionId, connectionLabel.value);
|
|
73
|
+
labelingConnectionId.value = undefined;
|
|
74
|
+
connectionLabel.value = "";
|
|
75
|
+
} catch (error) {
|
|
76
|
+
web.value.settingsError =
|
|
77
|
+
error instanceof Error ? error.message : "Could not rename Connection";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function rotateApiKey(connectionId: string): Promise<void> {
|
|
82
|
+
try {
|
|
83
|
+
await web.value.rotateApiKeyConnection(connectionId, rotationKey.value);
|
|
84
|
+
rotationKey.value = "";
|
|
85
|
+
rotatingConnectionId.value = undefined;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
rotationKey.value = "";
|
|
88
|
+
web.value.settingsError =
|
|
89
|
+
error instanceof Error ? error.message : "Could not rotate credential";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function refreshModels(connectionId: string): Promise<void> {
|
|
94
|
+
try {
|
|
95
|
+
await web.value.refreshConnectionModels(connectionId);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
web.value.settingsError =
|
|
98
|
+
error instanceof Error ? error.message : "Could not refresh models";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function setEnabled(
|
|
103
|
+
connectionId: string,
|
|
104
|
+
enabled: boolean,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
try {
|
|
107
|
+
await web.value.setConnectionEnabled(connectionId, enabled);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
web.value.settingsError =
|
|
110
|
+
error instanceof Error ? error.message : "Could not update Connection";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function disconnect(connectionId: string): Promise<void> {
|
|
115
|
+
try {
|
|
116
|
+
await web.value.disconnectConnection(connectionId);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
web.value.settingsError =
|
|
119
|
+
error instanceof Error ? error.message : "Could not disconnect";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function revoke(packageId: string, connectionId: string): Promise<void> {
|
|
124
|
+
try {
|
|
125
|
+
await web.value.revokeConnection(packageId, connectionId);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
web.value.settingsError =
|
|
128
|
+
error instanceof Error ? error.message : "Could not revoke Connection";
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<template>
|
|
134
|
+
<div class="package-accounts">
|
|
135
|
+
<div
|
|
136
|
+
v-for="connection in connections"
|
|
137
|
+
:key="connection.connectionId"
|
|
138
|
+
class="package-account"
|
|
139
|
+
>
|
|
140
|
+
<div class="account-identity">
|
|
141
|
+
<span
|
|
142
|
+
class="account-dot"
|
|
143
|
+
:class="`account-dot--${connectionTone(connection)}`"
|
|
144
|
+
aria-hidden="true"
|
|
145
|
+
/>
|
|
146
|
+
<strong>{{ connection.displayName }}</strong>
|
|
147
|
+
<small>{{ connection.state }}</small>
|
|
148
|
+
<small v-if="connection.modelCatalog">
|
|
149
|
+
· models {{ connection.modelCatalog.state }}
|
|
150
|
+
</small>
|
|
151
|
+
</div>
|
|
152
|
+
<div class="account-actions">
|
|
153
|
+
<UiButton @click="beginLabeling(connection)">Rename</UiButton>
|
|
154
|
+
<template v-if="connection.authorization?.kind === 'api-key'">
|
|
155
|
+
<UiButton
|
|
156
|
+
v-if="connection.modelCatalog && connection.state === 'ready'"
|
|
157
|
+
@click="refreshModels(connection.connectionId)"
|
|
158
|
+
>
|
|
159
|
+
Refresh models
|
|
160
|
+
</UiButton>
|
|
161
|
+
<UiButton
|
|
162
|
+
v-if="connection.state === 'ready'"
|
|
163
|
+
@click="setEnabled(connection.connectionId, false)"
|
|
164
|
+
>
|
|
165
|
+
Disable
|
|
166
|
+
</UiButton>
|
|
167
|
+
<UiButton
|
|
168
|
+
v-if="connection.state === 'disabled'"
|
|
169
|
+
@click="setEnabled(connection.connectionId, true)"
|
|
170
|
+
>
|
|
171
|
+
Enable
|
|
172
|
+
</UiButton>
|
|
173
|
+
<UiButton
|
|
174
|
+
v-if="
|
|
175
|
+
connection.state === 'ready' || connection.state === 'disabled'
|
|
176
|
+
"
|
|
177
|
+
@click="rotatingConnectionId = connection.connectionId"
|
|
178
|
+
>
|
|
179
|
+
Rotate key
|
|
180
|
+
</UiButton>
|
|
181
|
+
<UiButton
|
|
182
|
+
v-if="connection.state !== 'revoking'"
|
|
183
|
+
variant="danger"
|
|
184
|
+
@click="disconnect(connection.connectionId)"
|
|
185
|
+
>
|
|
186
|
+
Disconnect
|
|
187
|
+
</UiButton>
|
|
188
|
+
</template>
|
|
189
|
+
<UiButton
|
|
190
|
+
v-else-if="connection.state !== 'revoking'"
|
|
191
|
+
variant="danger"
|
|
192
|
+
@click="revoke(connection.packageId, connection.connectionId)"
|
|
193
|
+
>
|
|
194
|
+
Revoke
|
|
195
|
+
</UiButton>
|
|
196
|
+
</div>
|
|
197
|
+
<p v-if="defaultModelName(connection)" class="account-default">
|
|
198
|
+
Default model: {{ defaultModelName(connection) }}
|
|
199
|
+
</p>
|
|
200
|
+
<p v-if="connection.failure" class="connection-failure" role="alert">
|
|
201
|
+
{{ connection.failure }}
|
|
202
|
+
</p>
|
|
203
|
+
<p
|
|
204
|
+
v-if="connection.modelCatalog?.failure"
|
|
205
|
+
class="connection-failure"
|
|
206
|
+
role="alert"
|
|
207
|
+
>
|
|
208
|
+
{{ connection.modelCatalog.failure }}
|
|
209
|
+
</p>
|
|
210
|
+
<form
|
|
211
|
+
v-if="labelingConnectionId === connection.connectionId"
|
|
212
|
+
class="inline-form"
|
|
213
|
+
@submit.prevent="saveLabel(connection.connectionId)"
|
|
214
|
+
>
|
|
215
|
+
<input
|
|
216
|
+
v-model="connectionLabel"
|
|
217
|
+
maxlength="120"
|
|
218
|
+
aria-label="Connection label"
|
|
219
|
+
required
|
|
220
|
+
/>
|
|
221
|
+
<UiButton type="submit">Save label</UiButton>
|
|
222
|
+
</form>
|
|
223
|
+
<form
|
|
224
|
+
v-if="rotatingConnectionId === connection.connectionId"
|
|
225
|
+
class="inline-form"
|
|
226
|
+
@submit.prevent="rotateApiKey(connection.connectionId)"
|
|
227
|
+
>
|
|
228
|
+
<input
|
|
229
|
+
v-model="rotationKey"
|
|
230
|
+
type="password"
|
|
231
|
+
autocomplete="new-password"
|
|
232
|
+
aria-label="New API key"
|
|
233
|
+
required
|
|
234
|
+
/>
|
|
235
|
+
<UiButton type="submit">Save new key</UiButton>
|
|
236
|
+
</form>
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</template>
|
|
240
|
+
|
|
241
|
+
<style scoped>
|
|
242
|
+
.package-account {
|
|
243
|
+
display: grid;
|
|
244
|
+
gap: 8px;
|
|
245
|
+
padding: 12px 8px;
|
|
246
|
+
border-top: 1px solid var(--frock-border);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.account-identity {
|
|
250
|
+
display: flex;
|
|
251
|
+
min-width: 0;
|
|
252
|
+
flex-wrap: wrap;
|
|
253
|
+
align-items: baseline;
|
|
254
|
+
gap: 8px;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.account-identity strong {
|
|
258
|
+
overflow: hidden;
|
|
259
|
+
font-size: var(--frock-text-base);
|
|
260
|
+
white-space: nowrap;
|
|
261
|
+
text-overflow: ellipsis;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.account-identity small {
|
|
265
|
+
color: var(--frock-text-muted);
|
|
266
|
+
font-size: var(--frock-text-sm);
|
|
267
|
+
text-transform: capitalize;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.account-dot {
|
|
271
|
+
width: 8px;
|
|
272
|
+
height: 8px;
|
|
273
|
+
align-self: center;
|
|
274
|
+
border-radius: 999px;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.account-dot--ready {
|
|
278
|
+
background: var(--frock-success);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.account-dot--muted {
|
|
282
|
+
background: var(--frock-text-subtle);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.account-dot--attention {
|
|
286
|
+
background: var(--frock-danger-text);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
.account-actions {
|
|
290
|
+
display: flex;
|
|
291
|
+
flex-wrap: wrap;
|
|
292
|
+
gap: 8px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.account-actions :deep(.ui-button),
|
|
296
|
+
.inline-form :deep(.ui-button) {
|
|
297
|
+
min-height: 28px;
|
|
298
|
+
padding: 0 10px;
|
|
299
|
+
font-size: var(--frock-text-sm);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.inline-form {
|
|
303
|
+
display: grid;
|
|
304
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
305
|
+
gap: 8px;
|
|
306
|
+
animation: frock-rise-in var(--frock-motion-panel) both;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.inline-form input {
|
|
310
|
+
min-width: 0;
|
|
311
|
+
padding: 8px 11px;
|
|
312
|
+
border: 1px solid var(--frock-border);
|
|
313
|
+
border-radius: 9px;
|
|
314
|
+
background: var(--frock-surface-raised);
|
|
315
|
+
color: var(--frock-text);
|
|
316
|
+
font-size: var(--frock-text-base);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
.account-default {
|
|
320
|
+
margin: 0;
|
|
321
|
+
color: var(--frock-text-muted);
|
|
322
|
+
font-size: var(--frock-text-sm);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.connection-failure {
|
|
326
|
+
margin: 0;
|
|
327
|
+
color: var(--frock-danger-text);
|
|
328
|
+
font-size: var(--frock-text-sm);
|
|
329
|
+
}
|
|
330
|
+
</style>
|