@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,1076 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { clientSurfaceRegistryKey } from "@frockbot/client-core";
|
|
3
|
+
import {
|
|
4
|
+
UiAnchor,
|
|
5
|
+
UiButton,
|
|
6
|
+
UiField,
|
|
7
|
+
UiIcon,
|
|
8
|
+
UI_ANCHOR_EVENT,
|
|
9
|
+
type UiAnchorEvent,
|
|
10
|
+
} from "@frockbot/client-ui";
|
|
11
|
+
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
12
|
+
import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
|
|
13
|
+
import {
|
|
14
|
+
computed,
|
|
15
|
+
inject,
|
|
16
|
+
onBeforeUnmount,
|
|
17
|
+
onMounted,
|
|
18
|
+
reactive,
|
|
19
|
+
ref,
|
|
20
|
+
nextTick,
|
|
21
|
+
watch,
|
|
22
|
+
} from "vue";
|
|
23
|
+
import {
|
|
24
|
+
assignmentHasPendingOperation,
|
|
25
|
+
projectAssignmentOperations,
|
|
26
|
+
} from "./assignment-operations.js";
|
|
27
|
+
import {
|
|
28
|
+
describeModelAssignment,
|
|
29
|
+
eligibleModelConnections,
|
|
30
|
+
encodeModelSelection,
|
|
31
|
+
modelSelectOptions,
|
|
32
|
+
resolveBotSettingsModel,
|
|
33
|
+
} from "./bot-settings.js";
|
|
34
|
+
|
|
35
|
+
const providedSurfaces = inject(clientSurfaceRegistryKey);
|
|
36
|
+
const providedWeb = inject(frockBotWebDataKey);
|
|
37
|
+
if (!providedSurfaces || !providedWeb) {
|
|
38
|
+
throw new Error("settings client services were not provided");
|
|
39
|
+
}
|
|
40
|
+
const surfaces = providedSurfaces;
|
|
41
|
+
const web = providedWeb;
|
|
42
|
+
|
|
43
|
+
/*
|
|
44
|
+
* Every row below is deep-linkable. The scheme and the anchor table live in
|
|
45
|
+
* `@frockbot/plugin-shell/settings-links`, so a Bot citing a row and the panel
|
|
46
|
+
* rendering it read the same list — a link nobody registered does not resolve,
|
|
47
|
+
* and a row nobody linked has no link to copy.
|
|
48
|
+
*/
|
|
49
|
+
function link(anchor: string): string {
|
|
50
|
+
return settingsLinkV1({ anchor, botId: web.value.activeBotId });
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* Model and Assignments live under the Advanced disclosure, and a collapsed
|
|
55
|
+
* `details` cannot be scrolled to. A link into either opens it first; the User
|
|
56
|
+
* can still close it, and the `toggle` handler keeps their choice.
|
|
57
|
+
*/
|
|
58
|
+
const ADVANCED_ANCHORS = new Set([
|
|
59
|
+
"bot-title",
|
|
60
|
+
"bot-hidden-from-sidebar",
|
|
61
|
+
"bot-model",
|
|
62
|
+
"bot-capabilities",
|
|
63
|
+
"bot-routines",
|
|
64
|
+
"bot-audit",
|
|
65
|
+
"bot-info-identity",
|
|
66
|
+
"bot-info-members",
|
|
67
|
+
]);
|
|
68
|
+
const advancedOpen = ref(false);
|
|
69
|
+
|
|
70
|
+
function openAdvancedFor(anchor: string): void {
|
|
71
|
+
if (ADVANCED_ANCHORS.has(anchor)) advancedOpen.value = true;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function onAnchorAnnounced(event: Event): void {
|
|
75
|
+
const anchor = (event as UiAnchorEvent).detail;
|
|
76
|
+
openAdvancedFor(anchor);
|
|
77
|
+
void nextTick(() =>
|
|
78
|
+
document.getElementById(anchor)?.scrollIntoView({ block: "nearest" }),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const name = ref("");
|
|
83
|
+
const label = ref("");
|
|
84
|
+
const description = ref("");
|
|
85
|
+
const title = ref("");
|
|
86
|
+
const hiddenFromSidebar = ref(false);
|
|
87
|
+
const notifications = ref(false);
|
|
88
|
+
/**
|
|
89
|
+
* The Bot's undecided approval cards. Read from the same backend state the
|
|
90
|
+
* conversation renders, so the two surfaces cannot disagree about what is
|
|
91
|
+
* still waiting.
|
|
92
|
+
*/
|
|
93
|
+
const pendingApprovals = computed(() =>
|
|
94
|
+
web.value.approvals.filter((approval) => approval.decision === "pending"),
|
|
95
|
+
);
|
|
96
|
+
const decidingApproval = ref<string>();
|
|
97
|
+
|
|
98
|
+
async function decideApproval(
|
|
99
|
+
approvalId: string,
|
|
100
|
+
decision: "approved" | "denied",
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
decidingApproval.value = approvalId;
|
|
103
|
+
try {
|
|
104
|
+
await web.value.decideApproval(approvalId, decision);
|
|
105
|
+
} finally {
|
|
106
|
+
decidingApproval.value = undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const saving = ref(false);
|
|
110
|
+
const modelMode = ref<"default" | "custom">("default");
|
|
111
|
+
const assignmentBusy = ref<string>();
|
|
112
|
+
const selectedConnections = reactive<Record<string, string>>({});
|
|
113
|
+
|
|
114
|
+
const capabilityItems = computed(() =>
|
|
115
|
+
web.value.pluginCatalog.flatMap((pkg) =>
|
|
116
|
+
web.value.userSettings?.packages.some(
|
|
117
|
+
(installation) =>
|
|
118
|
+
installation.packageId === pkg.packageId &&
|
|
119
|
+
installation.version === pkg.version &&
|
|
120
|
+
installation.state === "installed",
|
|
121
|
+
)
|
|
122
|
+
? pkg.capabilities.map((capability) => {
|
|
123
|
+
const existing = web.value.botSettings?.assignments.find(
|
|
124
|
+
(assignment) =>
|
|
125
|
+
assignment.packageId === pkg.packageId &&
|
|
126
|
+
assignment.capabilityId === capability.id,
|
|
127
|
+
);
|
|
128
|
+
const pending = web.value.botSettings?.assignmentOperations.find(
|
|
129
|
+
(operation) =>
|
|
130
|
+
operation.assignmentId === existing?.assignmentId ||
|
|
131
|
+
(operation.target?.packageId === pkg.packageId &&
|
|
132
|
+
operation.target.capabilityId === capability.id),
|
|
133
|
+
);
|
|
134
|
+
const connections =
|
|
135
|
+
web.value.userSettings?.connections.filter(
|
|
136
|
+
(connection) =>
|
|
137
|
+
connection.packageId === pkg.packageId &&
|
|
138
|
+
connection.state === "ready" &&
|
|
139
|
+
capability.connectionTypes.includes(
|
|
140
|
+
connection.connectionTypeId,
|
|
141
|
+
),
|
|
142
|
+
) ?? [];
|
|
143
|
+
const key = `${pkg.packageId}:${capability.id}`;
|
|
144
|
+
if (!(key in selectedConnections) && existing?.connectionId) {
|
|
145
|
+
selectedConnections[key] = existing.connectionId;
|
|
146
|
+
}
|
|
147
|
+
return { key, pkg, capability, existing, pending, connections };
|
|
148
|
+
})
|
|
149
|
+
: [],
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const assignmentOperations = computed(() =>
|
|
154
|
+
projectAssignmentOperations(web.value.botSettings),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const orphanAssignments = computed(
|
|
158
|
+
() =>
|
|
159
|
+
web.value.botSettings?.assignments.filter(
|
|
160
|
+
(assignment) =>
|
|
161
|
+
!capabilityItems.value.some(
|
|
162
|
+
(item) => item.existing?.assignmentId === assignment.assignmentId,
|
|
163
|
+
),
|
|
164
|
+
) ?? [],
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
function assignmentOperationPending(assignmentId: string): boolean {
|
|
168
|
+
return assignmentHasPendingOperation(
|
|
169
|
+
assignmentOperations.value,
|
|
170
|
+
assignmentId,
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const selectedModel = ref("");
|
|
175
|
+
const useExactModel = ref(false);
|
|
176
|
+
const exactConnectionId = ref("");
|
|
177
|
+
const exactProviderModelId = ref("");
|
|
178
|
+
const readyConnections = computed(() =>
|
|
179
|
+
eligibleModelConnections({
|
|
180
|
+
connections: web.value.userSettings?.connections ?? [],
|
|
181
|
+
packages: web.value.userSettings?.packages ?? [],
|
|
182
|
+
catalog: web.value.pluginCatalog,
|
|
183
|
+
}),
|
|
184
|
+
);
|
|
185
|
+
const modelOptions = computed(() => modelSelectOptions(readyConnections.value));
|
|
186
|
+
const defaultModelName = computed(
|
|
187
|
+
() =>
|
|
188
|
+
describeModelAssignment(
|
|
189
|
+
web.value.userSettings?.newBotModelTemplate,
|
|
190
|
+
web.value.userSettings?.connections ?? [],
|
|
191
|
+
) ?? "none set",
|
|
192
|
+
);
|
|
193
|
+
const overriding = computed(() => Boolean(web.value.botSettings?.model));
|
|
194
|
+
|
|
195
|
+
onMounted(() => {
|
|
196
|
+
window.addEventListener(UI_ANCHOR_EVENT, onAnchorAnnounced);
|
|
197
|
+
openAdvancedFor(decodeURIComponent(window.location.hash.replace(/^#/u, "")));
|
|
198
|
+
void web.value.loadPluginCatalog();
|
|
199
|
+
void web.value.loadBotSettings();
|
|
200
|
+
void web.value.loadUserSettings();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/*
|
|
204
|
+
* The form fills itself from whichever Bot's durable settings arrive, rather
|
|
205
|
+
* than from whatever had loaded by the time this panel mounted. A deep link
|
|
206
|
+
* opens the panel before the Flock has selected a Bot, and a User can switch
|
|
207
|
+
* Bots with the panel open; both used to leave the fields on screen belonging
|
|
208
|
+
* to nobody.
|
|
209
|
+
*/
|
|
210
|
+
const hydratedBotId = ref<string>();
|
|
211
|
+
|
|
212
|
+
watch(
|
|
213
|
+
() => web.value.botSettings,
|
|
214
|
+
(settings) => {
|
|
215
|
+
if (!settings || hydratedBotId.value === settings.botId) return;
|
|
216
|
+
hydratedBotId.value = settings.botId;
|
|
217
|
+
name.value = settings.profile.name;
|
|
218
|
+
label.value = settings.profile.label ?? "";
|
|
219
|
+
description.value = settings.profile.description ?? "";
|
|
220
|
+
title.value = settings.profile.title ?? "";
|
|
221
|
+
hiddenFromSidebar.value = settings.profile.hiddenFromSidebar === true;
|
|
222
|
+
notifications.value = settings.notifications.enabled;
|
|
223
|
+
modelMode.value = settings.model ? "custom" : "default";
|
|
224
|
+
selectedModel.value = encodeModelSelection(settings.model);
|
|
225
|
+
exactConnectionId.value =
|
|
226
|
+
settings.model?.connectionId ??
|
|
227
|
+
readyConnections.value[0]?.connectionId ??
|
|
228
|
+
"";
|
|
229
|
+
exactProviderModelId.value = settings.model?.providerModelId ?? "";
|
|
230
|
+
useExactModel.value = Boolean(
|
|
231
|
+
settings.model &&
|
|
232
|
+
!modelOptions.value.some((model) => model.value === selectedModel.value),
|
|
233
|
+
);
|
|
234
|
+
},
|
|
235
|
+
{ immediate: true },
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
// The Connections may land after the Bot did; an empty exact-model Connection
|
|
239
|
+
// takes the first ready one the moment there is one.
|
|
240
|
+
watch(readyConnections, (connections) => {
|
|
241
|
+
if (!exactConnectionId.value && connections[0]) {
|
|
242
|
+
exactConnectionId.value = connections[0].connectionId;
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
onBeforeUnmount(() =>
|
|
247
|
+
window.removeEventListener(UI_ANCHOR_EVENT, onAnchorAnnounced),
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
async function saveModel(): Promise<void> {
|
|
251
|
+
if (modelMode.value === "default") {
|
|
252
|
+
// Following the default means holding no Bot binding at all.
|
|
253
|
+
if (web.value.botSettings?.model) await web.value.clearBotModel();
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const selected = resolveBotSettingsModel({
|
|
257
|
+
current: web.value.botSettings?.model,
|
|
258
|
+
useExactModel: useExactModel.value,
|
|
259
|
+
selectedModel: selectedModel.value,
|
|
260
|
+
exactConnectionId: exactConnectionId.value,
|
|
261
|
+
exactProviderModelId: exactProviderModelId.value,
|
|
262
|
+
});
|
|
263
|
+
if (selected) await web.value.saveBotModel(selected);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function save(): Promise<void> {
|
|
267
|
+
saving.value = true;
|
|
268
|
+
try {
|
|
269
|
+
// A partial update: the empty string clears an optional field.
|
|
270
|
+
await web.value.setBotProfile({
|
|
271
|
+
name: name.value,
|
|
272
|
+
label: label.value,
|
|
273
|
+
description: description.value,
|
|
274
|
+
title: title.value,
|
|
275
|
+
hiddenFromSidebar: hiddenFromSidebar.value,
|
|
276
|
+
});
|
|
277
|
+
await saveModel();
|
|
278
|
+
await web.value.saveBotNotifications({ enabled: notifications.value });
|
|
279
|
+
surfaces.close();
|
|
280
|
+
} catch (error) {
|
|
281
|
+
web.value.settingsError =
|
|
282
|
+
error instanceof Error ? error.message : "Could not save settings";
|
|
283
|
+
} finally {
|
|
284
|
+
saving.value = false;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function assign(
|
|
289
|
+
item: (typeof capabilityItems.value)[number],
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
assignmentBusy.value = item.key;
|
|
292
|
+
try {
|
|
293
|
+
await web.value.assignCapability({
|
|
294
|
+
assignmentId: crypto.randomUUID(),
|
|
295
|
+
packageId: item.pkg.packageId,
|
|
296
|
+
capabilityId: item.capability.id,
|
|
297
|
+
connectionId: selectedConnections[item.key] || undefined,
|
|
298
|
+
});
|
|
299
|
+
} catch (error) {
|
|
300
|
+
web.value.settingsError =
|
|
301
|
+
error instanceof Error ? error.message : "Could not assign Capability";
|
|
302
|
+
await web.value.loadBotSettings();
|
|
303
|
+
} finally {
|
|
304
|
+
assignmentBusy.value = undefined;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function replace(
|
|
309
|
+
item: (typeof capabilityItems.value)[number],
|
|
310
|
+
): Promise<void> {
|
|
311
|
+
if (!item.existing) return;
|
|
312
|
+
assignmentBusy.value = item.key;
|
|
313
|
+
try {
|
|
314
|
+
await web.value.replaceCapability({
|
|
315
|
+
assignmentId: item.existing.assignmentId,
|
|
316
|
+
packageId: item.pkg.packageId,
|
|
317
|
+
capabilityId: item.capability.id,
|
|
318
|
+
connectionId: selectedConnections[item.key] || undefined,
|
|
319
|
+
});
|
|
320
|
+
} catch (error) {
|
|
321
|
+
web.value.settingsError =
|
|
322
|
+
error instanceof Error ? error.message : "Could not replace Assignment";
|
|
323
|
+
await web.value.loadBotSettings();
|
|
324
|
+
} finally {
|
|
325
|
+
assignmentBusy.value = undefined;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function unassignAssignment(
|
|
330
|
+
assignmentId: string,
|
|
331
|
+
key: string,
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
assignmentBusy.value = key;
|
|
334
|
+
try {
|
|
335
|
+
await web.value.unassignCapability(assignmentId);
|
|
336
|
+
} catch (error) {
|
|
337
|
+
web.value.settingsError =
|
|
338
|
+
error instanceof Error ? error.message : "Could not unassign Capability";
|
|
339
|
+
await web.value.loadBotSettings();
|
|
340
|
+
} finally {
|
|
341
|
+
assignmentBusy.value = undefined;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function unassign(
|
|
346
|
+
item: (typeof capabilityItems.value)[number],
|
|
347
|
+
): Promise<void> {
|
|
348
|
+
if (!item.existing) return;
|
|
349
|
+
await unassignAssignment(item.existing.assignmentId, item.key);
|
|
350
|
+
}
|
|
351
|
+
</script>
|
|
352
|
+
|
|
353
|
+
<template>
|
|
354
|
+
<form class="settings-form" @submit.prevent="save">
|
|
355
|
+
<UiAnchor
|
|
356
|
+
anchor="bot-avatar"
|
|
357
|
+
label="Avatar"
|
|
358
|
+
:href="link('bot-avatar')"
|
|
359
|
+
class="settings-row avatar-setting"
|
|
360
|
+
>
|
|
361
|
+
<k-slot name="frockbot.bot-avatar-editor" />
|
|
362
|
+
</UiAnchor>
|
|
363
|
+
<UiAnchor
|
|
364
|
+
anchor="bot-name"
|
|
365
|
+
label="Name"
|
|
366
|
+
:href="link('bot-name')"
|
|
367
|
+
class="settings-row"
|
|
368
|
+
>
|
|
369
|
+
<UiField label="Name">
|
|
370
|
+
<input v-model="name" maxlength="100" required />
|
|
371
|
+
</UiField>
|
|
372
|
+
</UiAnchor>
|
|
373
|
+
<UiAnchor
|
|
374
|
+
anchor="bot-label"
|
|
375
|
+
label="Label"
|
|
376
|
+
:href="link('bot-label')"
|
|
377
|
+
class="settings-row"
|
|
378
|
+
>
|
|
379
|
+
<UiField label="Label" hint="optional">
|
|
380
|
+
<input
|
|
381
|
+
v-model="label"
|
|
382
|
+
maxlength="120"
|
|
383
|
+
placeholder="Research, marketing, admin"
|
|
384
|
+
/>
|
|
385
|
+
</UiField>
|
|
386
|
+
</UiAnchor>
|
|
387
|
+
<UiAnchor
|
|
388
|
+
anchor="bot-description"
|
|
389
|
+
label="Description"
|
|
390
|
+
:href="link('bot-description')"
|
|
391
|
+
class="settings-row"
|
|
392
|
+
>
|
|
393
|
+
<UiField label="Description">
|
|
394
|
+
<textarea v-model="description" maxlength="10000" rows="7" />
|
|
395
|
+
</UiField>
|
|
396
|
+
</UiAnchor>
|
|
397
|
+
<div id="bot-info-notifications">
|
|
398
|
+
<UiAnchor
|
|
399
|
+
anchor="bot-notifications"
|
|
400
|
+
label="Notifications"
|
|
401
|
+
:href="link('bot-notifications')"
|
|
402
|
+
class="settings-row"
|
|
403
|
+
>
|
|
404
|
+
<label class="notification-setting">
|
|
405
|
+
<span>
|
|
406
|
+
<strong>Notifications</strong>
|
|
407
|
+
<small>Get notified when this Bot finishes or needs input</small>
|
|
408
|
+
</span>
|
|
409
|
+
<input v-model="notifications" type="checkbox" />
|
|
410
|
+
</label>
|
|
411
|
+
</UiAnchor>
|
|
412
|
+
</div>
|
|
413
|
+
<!--
|
|
414
|
+
Pending decisions. The card in the conversation is where a decision is
|
|
415
|
+
normally answered; this is where the ones nobody scrolled back to are
|
|
416
|
+
still findable, because "a request for more authority becomes a durable
|
|
417
|
+
pending decision for the User" is only true if the User can find it.
|
|
418
|
+
-->
|
|
419
|
+
<UiAnchor
|
|
420
|
+
v-if="pendingApprovals.length > 0"
|
|
421
|
+
anchor="bot-approvals"
|
|
422
|
+
label="Waiting on you"
|
|
423
|
+
:href="link('bot-approvals')"
|
|
424
|
+
class="settings-row"
|
|
425
|
+
>
|
|
426
|
+
<ul class="pending-approvals">
|
|
427
|
+
<li v-for="approval in pendingApprovals" :key="approval.approvalId">
|
|
428
|
+
<span class="pending-approvals__risk">{{ approval.risk }}</span>
|
|
429
|
+
<span class="pending-approvals__action">{{ approval.action }}</span>
|
|
430
|
+
<span class="pending-approvals__actions">
|
|
431
|
+
<UiButton
|
|
432
|
+
:disabled="decidingApproval !== undefined"
|
|
433
|
+
@click="decideApproval(approval.approvalId, 'approved')"
|
|
434
|
+
>Approve</UiButton
|
|
435
|
+
>
|
|
436
|
+
<UiButton
|
|
437
|
+
variant="ghost"
|
|
438
|
+
:disabled="decidingApproval !== undefined"
|
|
439
|
+
@click="decideApproval(approval.approvalId, 'denied')"
|
|
440
|
+
>Deny</UiButton
|
|
441
|
+
>
|
|
442
|
+
</span>
|
|
443
|
+
</li>
|
|
444
|
+
</ul>
|
|
445
|
+
</UiAnchor>
|
|
446
|
+
|
|
447
|
+
<details
|
|
448
|
+
class="advanced"
|
|
449
|
+
:open="advancedOpen"
|
|
450
|
+
@toggle="advancedOpen = ($event.target as HTMLDetailsElement).open"
|
|
451
|
+
>
|
|
452
|
+
<summary>
|
|
453
|
+
<span>Advanced</span>
|
|
454
|
+
<span class="advanced__marker" aria-hidden="true"
|
|
455
|
+
><UiIcon name="arrow-down" size="sm"
|
|
456
|
+
/></span>
|
|
457
|
+
</summary>
|
|
458
|
+
<div class="advanced__body">
|
|
459
|
+
<UiAnchor
|
|
460
|
+
anchor="bot-title"
|
|
461
|
+
label="Title"
|
|
462
|
+
:href="link('bot-title')"
|
|
463
|
+
class="settings-row"
|
|
464
|
+
>
|
|
465
|
+
<UiField label="Title" hint="optional">
|
|
466
|
+
<input
|
|
467
|
+
v-model="title"
|
|
468
|
+
maxlength="120"
|
|
469
|
+
placeholder="Chief of staff, night-shift researcher"
|
|
470
|
+
/>
|
|
471
|
+
</UiField>
|
|
472
|
+
</UiAnchor>
|
|
473
|
+
<UiAnchor
|
|
474
|
+
anchor="bot-hidden-from-sidebar"
|
|
475
|
+
label="Hidden from sidebar"
|
|
476
|
+
:href="link('bot-hidden-from-sidebar')"
|
|
477
|
+
class="settings-row"
|
|
478
|
+
>
|
|
479
|
+
<label class="notification-setting">
|
|
480
|
+
<span>
|
|
481
|
+
<strong>Hidden from sidebar</strong>
|
|
482
|
+
<small
|
|
483
|
+
>Keeps this Bot out of the list without archiving it.</small
|
|
484
|
+
>
|
|
485
|
+
</span>
|
|
486
|
+
<input v-model="hiddenFromSidebar" type="checkbox" />
|
|
487
|
+
</label>
|
|
488
|
+
</UiAnchor>
|
|
489
|
+
<UiAnchor
|
|
490
|
+
anchor="bot-info-identity"
|
|
491
|
+
label="Identity"
|
|
492
|
+
:href="link('bot-info-identity')"
|
|
493
|
+
class="bot-members"
|
|
494
|
+
>
|
|
495
|
+
<div>
|
|
496
|
+
<strong>Identity</strong>
|
|
497
|
+
<p>{{ name || "This Bot" }} · Bot {{ web.activeBotId }}</p>
|
|
498
|
+
</div>
|
|
499
|
+
<span>
|
|
500
|
+
Named by {{ web.botSettings?.profile.namedBy ?? "user" }}
|
|
501
|
+
</span>
|
|
502
|
+
</UiAnchor>
|
|
503
|
+
<UiAnchor
|
|
504
|
+
anchor="bot-info-members"
|
|
505
|
+
label="Members"
|
|
506
|
+
:href="link('bot-info-members')"
|
|
507
|
+
class="bot-members"
|
|
508
|
+
>
|
|
509
|
+
<div>
|
|
510
|
+
<strong>Members</strong>
|
|
511
|
+
<p>This Bot and the authority explicitly assigned to it.</p>
|
|
512
|
+
</div>
|
|
513
|
+
<span>
|
|
514
|
+
{{ web.botSettings?.assignments.length ?? 0 }} Capability
|
|
515
|
+
Assignment(s)
|
|
516
|
+
</span>
|
|
517
|
+
</UiAnchor>
|
|
518
|
+
<p v-if="overriding" class="model-note">Overrides default model</p>
|
|
519
|
+
<UiAnchor
|
|
520
|
+
anchor="bot-model"
|
|
521
|
+
label="Model"
|
|
522
|
+
:href="link('bot-model')"
|
|
523
|
+
class="settings-row"
|
|
524
|
+
>
|
|
525
|
+
<fieldset class="model-mode">
|
|
526
|
+
<legend>Model</legend>
|
|
527
|
+
<label>
|
|
528
|
+
<input v-model="modelMode" type="radio" value="default" />
|
|
529
|
+
<span>Use default model ({{ defaultModelName }})</span>
|
|
530
|
+
</label>
|
|
531
|
+
<label>
|
|
532
|
+
<input v-model="modelMode" type="radio" value="custom" />
|
|
533
|
+
<span>Custom model</span>
|
|
534
|
+
</label>
|
|
535
|
+
</fieldset>
|
|
536
|
+
</UiAnchor>
|
|
537
|
+
<template v-if="modelMode === 'custom'">
|
|
538
|
+
<label class="exact-model-setting">
|
|
539
|
+
<span>
|
|
540
|
+
<strong>Use exact model ID</strong>
|
|
541
|
+
<small>Choose a model not listed in the advisory catalog.</small>
|
|
542
|
+
</span>
|
|
543
|
+
<input v-model="useExactModel" type="checkbox" />
|
|
544
|
+
</label>
|
|
545
|
+
<template v-if="useExactModel">
|
|
546
|
+
<UiField label="Connection">
|
|
547
|
+
<select v-model="exactConnectionId">
|
|
548
|
+
<option disabled value="">Select a Connection</option>
|
|
549
|
+
<option
|
|
550
|
+
v-for="connection in readyConnections"
|
|
551
|
+
:key="connection.connectionId"
|
|
552
|
+
:value="connection.connectionId"
|
|
553
|
+
>
|
|
554
|
+
{{ connection.displayName }}
|
|
555
|
+
</option>
|
|
556
|
+
</select>
|
|
557
|
+
</UiField>
|
|
558
|
+
<UiField label="Exact provider model ID">
|
|
559
|
+
<input
|
|
560
|
+
v-model="exactProviderModelId"
|
|
561
|
+
maxlength="256"
|
|
562
|
+
placeholder="model-name:cloud"
|
|
563
|
+
/>
|
|
564
|
+
</UiField>
|
|
565
|
+
</template>
|
|
566
|
+
<UiField v-else label="Model">
|
|
567
|
+
<select v-model="selectedModel">
|
|
568
|
+
<option disabled value="">Select a connected model</option>
|
|
569
|
+
<option
|
|
570
|
+
v-for="model in modelOptions"
|
|
571
|
+
:key="model.value"
|
|
572
|
+
:value="model.value"
|
|
573
|
+
>
|
|
574
|
+
{{ model.label }}
|
|
575
|
+
</option>
|
|
576
|
+
</select>
|
|
577
|
+
</UiField>
|
|
578
|
+
</template>
|
|
579
|
+
<UiAnchor
|
|
580
|
+
anchor="bot-capabilities"
|
|
581
|
+
label="Capability Assignments"
|
|
582
|
+
:href="link('bot-capabilities')"
|
|
583
|
+
class="settings-row"
|
|
584
|
+
>
|
|
585
|
+
<section class="assignment-settings">
|
|
586
|
+
<div>
|
|
587
|
+
<strong>Capability Assignments</strong>
|
|
588
|
+
<p>
|
|
589
|
+
Grant this Bot an installed Capability and required Connection.
|
|
590
|
+
</p>
|
|
591
|
+
</div>
|
|
592
|
+
<article
|
|
593
|
+
v-for="item in capabilityItems"
|
|
594
|
+
:key="item.key"
|
|
595
|
+
class="assignment-card"
|
|
596
|
+
>
|
|
597
|
+
<div>
|
|
598
|
+
<strong
|
|
599
|
+
>{{ item.pkg.displayName }} · {{ item.capability.id }}</strong
|
|
600
|
+
>
|
|
601
|
+
<small v-if="item.pending">
|
|
602
|
+
{{ item.pending.kind }} · {{ item.pending.state }}
|
|
603
|
+
</small>
|
|
604
|
+
<small v-else-if="item.existing">
|
|
605
|
+
{{ item.existing.state }}
|
|
606
|
+
</small>
|
|
607
|
+
<small v-else>Not assigned</small>
|
|
608
|
+
</div>
|
|
609
|
+
<select
|
|
610
|
+
v-if="item.capability.connectionTypes.length > 0"
|
|
611
|
+
v-model="selectedConnections[item.key]"
|
|
612
|
+
:disabled="Boolean(item.pending)"
|
|
613
|
+
:aria-label="`Connection for ${item.capability.id}`"
|
|
614
|
+
>
|
|
615
|
+
<option value="">Choose a ready Connection</option>
|
|
616
|
+
<option
|
|
617
|
+
v-for="connection in item.connections"
|
|
618
|
+
:key="connection.connectionId"
|
|
619
|
+
:value="connection.connectionId"
|
|
620
|
+
>
|
|
621
|
+
{{ connection.displayName }}
|
|
622
|
+
</option>
|
|
623
|
+
</select>
|
|
624
|
+
<div class="assignment-actions">
|
|
625
|
+
<UiButton
|
|
626
|
+
v-if="!item.existing"
|
|
627
|
+
type="button"
|
|
628
|
+
:disabled="
|
|
629
|
+
Boolean(item.pending) ||
|
|
630
|
+
assignmentBusy === item.key ||
|
|
631
|
+
(item.capability.connectionTypes.length > 0 &&
|
|
632
|
+
!selectedConnections[item.key])
|
|
633
|
+
"
|
|
634
|
+
@click="assign(item)"
|
|
635
|
+
>
|
|
636
|
+
Assign
|
|
637
|
+
</UiButton>
|
|
638
|
+
<template v-else>
|
|
639
|
+
<UiButton
|
|
640
|
+
type="button"
|
|
641
|
+
:disabled="
|
|
642
|
+
Boolean(item.pending) || assignmentBusy === item.key
|
|
643
|
+
"
|
|
644
|
+
@click="replace(item)"
|
|
645
|
+
>
|
|
646
|
+
Replace
|
|
647
|
+
</UiButton>
|
|
648
|
+
<UiButton
|
|
649
|
+
type="button"
|
|
650
|
+
variant="danger"
|
|
651
|
+
:disabled="
|
|
652
|
+
Boolean(item.pending) || assignmentBusy === item.key
|
|
653
|
+
"
|
|
654
|
+
@click="unassign(item)"
|
|
655
|
+
>
|
|
656
|
+
Unassign
|
|
657
|
+
</UiButton>
|
|
658
|
+
</template>
|
|
659
|
+
</div>
|
|
660
|
+
</article>
|
|
661
|
+
<article
|
|
662
|
+
v-for="operation in assignmentOperations"
|
|
663
|
+
:key="`operation:${operation.commandId}`"
|
|
664
|
+
class="assignment-card"
|
|
665
|
+
data-assignment-operation
|
|
666
|
+
>
|
|
667
|
+
<div>
|
|
668
|
+
<strong>
|
|
669
|
+
{{ operation.target?.packageId ?? "Unavailable Package" }} ·
|
|
670
|
+
{{ operation.target?.capabilityId ?? operation.assignmentId }}
|
|
671
|
+
</strong>
|
|
672
|
+
<small>{{ operation.kind }} · {{ operation.state }}</small>
|
|
673
|
+
</div>
|
|
674
|
+
</article>
|
|
675
|
+
<article
|
|
676
|
+
v-for="assignment in orphanAssignments"
|
|
677
|
+
:key="assignment.assignmentId"
|
|
678
|
+
class="assignment-card"
|
|
679
|
+
>
|
|
680
|
+
<div>
|
|
681
|
+
<strong
|
|
682
|
+
>{{ assignment.packageId }} ·
|
|
683
|
+
{{ assignment.capabilityId }}</strong
|
|
684
|
+
>
|
|
685
|
+
<small
|
|
686
|
+
>{{ assignment.state }} · no longer available in the
|
|
687
|
+
catalog</small
|
|
688
|
+
>
|
|
689
|
+
</div>
|
|
690
|
+
<div class="assignment-actions">
|
|
691
|
+
<UiButton
|
|
692
|
+
type="button"
|
|
693
|
+
variant="danger"
|
|
694
|
+
:disabled="
|
|
695
|
+
assignmentBusy === assignment.assignmentId ||
|
|
696
|
+
assignmentOperationPending(assignment.assignmentId)
|
|
697
|
+
"
|
|
698
|
+
@click="
|
|
699
|
+
unassignAssignment(
|
|
700
|
+
assignment.assignmentId,
|
|
701
|
+
assignment.assignmentId,
|
|
702
|
+
)
|
|
703
|
+
"
|
|
704
|
+
>
|
|
705
|
+
Unassign
|
|
706
|
+
</UiButton>
|
|
707
|
+
</div>
|
|
708
|
+
</article>
|
|
709
|
+
<p
|
|
710
|
+
v-if="
|
|
711
|
+
capabilityItems.length === 0 && orphanAssignments.length === 0
|
|
712
|
+
"
|
|
713
|
+
class="assignment-empty"
|
|
714
|
+
>
|
|
715
|
+
No assignable Capabilities are available in the production
|
|
716
|
+
catalog.
|
|
717
|
+
</p>
|
|
718
|
+
</section>
|
|
719
|
+
</UiAnchor>
|
|
720
|
+
<k-slot name="frockbot.bot-settings-sections" />
|
|
721
|
+
</div>
|
|
722
|
+
</details>
|
|
723
|
+
<div class="primary-contributions">
|
|
724
|
+
<k-slot name="frockbot.bot-settings-primary-sections" />
|
|
725
|
+
</div>
|
|
726
|
+
<p v-if="web.settingsError" class="settings-error" role="alert">
|
|
727
|
+
{{ web.settingsError }}
|
|
728
|
+
</p>
|
|
729
|
+
<div class="settings-actions">
|
|
730
|
+
<UiButton type="submit" variant="primary" :disabled="saving">
|
|
731
|
+
{{ saving ? "Saving…" : "Save settings" }}
|
|
732
|
+
</UiButton>
|
|
733
|
+
</div>
|
|
734
|
+
</form>
|
|
735
|
+
</template>
|
|
736
|
+
|
|
737
|
+
<style scoped>
|
|
738
|
+
/*
|
|
739
|
+
* A deep-linkable row. The anchor floats its copy control in the top-right
|
|
740
|
+
* corner, so every row keeps that corner clear.
|
|
741
|
+
*/
|
|
742
|
+
.settings-row {
|
|
743
|
+
display: flex;
|
|
744
|
+
flex-direction: column;
|
|
745
|
+
gap: 8px;
|
|
746
|
+
padding-right: var(--frock-control-sm);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
.settings-form {
|
|
750
|
+
display: flex;
|
|
751
|
+
flex-direction: column;
|
|
752
|
+
gap: 16px;
|
|
753
|
+
padding: 16px;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.assignment-card,
|
|
757
|
+
.notification-setting,
|
|
758
|
+
.bot-members {
|
|
759
|
+
border: 1px solid var(--frock-border);
|
|
760
|
+
border-radius: var(--frock-radius-card);
|
|
761
|
+
background: var(--frock-surface-subtle);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
.avatar-setting {
|
|
765
|
+
display: flex;
|
|
766
|
+
flex-direction: column;
|
|
767
|
+
align-items: center;
|
|
768
|
+
gap: 12px;
|
|
769
|
+
padding-right: 0;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
.assignment-settings p,
|
|
773
|
+
.assignment-card small {
|
|
774
|
+
margin-top: 4px;
|
|
775
|
+
color: var(--frock-text-muted);
|
|
776
|
+
font-size: var(--frock-text-sm);
|
|
777
|
+
line-height: var(--frock-leading-normal);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.assignment-settings {
|
|
781
|
+
display: grid;
|
|
782
|
+
gap: 10px;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
.bot-members {
|
|
786
|
+
display: flex;
|
|
787
|
+
align-items: center;
|
|
788
|
+
justify-content: space-between;
|
|
789
|
+
gap: 12px;
|
|
790
|
+
padding: 12px;
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
.bot-members strong,
|
|
794
|
+
.bot-members p {
|
|
795
|
+
display: block;
|
|
796
|
+
margin: 0;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
.bot-members strong {
|
|
800
|
+
color: var(--frock-text);
|
|
801
|
+
font-size: var(--frock-text-md);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
.bot-members p,
|
|
805
|
+
.bot-members > span {
|
|
806
|
+
margin-top: 4px;
|
|
807
|
+
color: var(--frock-text-muted);
|
|
808
|
+
font-size: var(--frock-text-sm);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
.bot-members > span {
|
|
812
|
+
max-width: 110px;
|
|
813
|
+
flex: 0 0 auto;
|
|
814
|
+
text-align: right;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
.assignment-settings strong {
|
|
818
|
+
font-size: var(--frock-text-md);
|
|
819
|
+
font-weight: 600;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
.assignment-card {
|
|
823
|
+
display: grid;
|
|
824
|
+
gap: 8px;
|
|
825
|
+
padding: 12px;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
.assignment-card strong {
|
|
829
|
+
font-size: var(--frock-text-md);
|
|
830
|
+
font-weight: 600;
|
|
831
|
+
overflow-wrap: anywhere;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
.assignment-card small {
|
|
835
|
+
display: block;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.assignment-card select {
|
|
839
|
+
width: 100%;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
.assignment-actions {
|
|
843
|
+
display: flex;
|
|
844
|
+
flex-wrap: wrap;
|
|
845
|
+
justify-content: flex-end;
|
|
846
|
+
gap: 8px;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/* The right panel is ~360px wide, so Assignment controls stay compact. */
|
|
850
|
+
.assignment-actions :deep(.ui-button) {
|
|
851
|
+
min-height: 30px;
|
|
852
|
+
padding: 0 10px;
|
|
853
|
+
font-size: var(--frock-text-sm);
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
.assignment-empty {
|
|
857
|
+
padding: 12px;
|
|
858
|
+
color: var(--frock-text-muted);
|
|
859
|
+
font-size: var(--frock-text-sm);
|
|
860
|
+
border: 1px dashed var(--frock-border);
|
|
861
|
+
border-radius: var(--frock-radius-card);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.exact-model-setting,
|
|
865
|
+
.notification-setting {
|
|
866
|
+
display: flex;
|
|
867
|
+
align-items: center;
|
|
868
|
+
justify-content: space-between;
|
|
869
|
+
gap: 12px;
|
|
870
|
+
padding: 12px;
|
|
871
|
+
border: 1px solid var(--frock-border);
|
|
872
|
+
border-radius: var(--frock-radius-card);
|
|
873
|
+
background: var(--frock-surface-subtle);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
.exact-model-setting strong,
|
|
877
|
+
.exact-model-setting small,
|
|
878
|
+
.notification-setting strong,
|
|
879
|
+
.notification-setting small {
|
|
880
|
+
display: block;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
.notification-setting strong,
|
|
884
|
+
.exact-model-setting strong {
|
|
885
|
+
font-size: var(--frock-text-md);
|
|
886
|
+
font-weight: 600;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.exact-model-setting small,
|
|
890
|
+
.notification-setting small {
|
|
891
|
+
margin-top: 4px;
|
|
892
|
+
color: var(--frock-text-muted);
|
|
893
|
+
font-size: var(--frock-text-sm);
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.exact-model-setting input[type="checkbox"],
|
|
897
|
+
.notification-setting input[type="checkbox"] {
|
|
898
|
+
position: relative;
|
|
899
|
+
width: 38px;
|
|
900
|
+
height: 22px;
|
|
901
|
+
flex: 0 0 auto;
|
|
902
|
+
appearance: none;
|
|
903
|
+
border: 1px solid var(--frock-border-strong);
|
|
904
|
+
border-radius: 999px;
|
|
905
|
+
background: var(--frock-fill-pressed);
|
|
906
|
+
cursor: pointer;
|
|
907
|
+
transition: background-color var(--frock-motion-fast);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
.exact-model-setting input[type="checkbox"]::before,
|
|
911
|
+
.notification-setting input[type="checkbox"]::before {
|
|
912
|
+
position: absolute;
|
|
913
|
+
top: 2px;
|
|
914
|
+
left: 2px;
|
|
915
|
+
width: 16px;
|
|
916
|
+
height: 16px;
|
|
917
|
+
border-radius: 50%;
|
|
918
|
+
background: var(--frock-surface-raised);
|
|
919
|
+
box-shadow: var(--frock-shadow-control);
|
|
920
|
+
content: "";
|
|
921
|
+
transition: transform var(--frock-motion-fast);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
.exact-model-setting input[type="checkbox"]:checked,
|
|
925
|
+
.notification-setting input[type="checkbox"]:checked {
|
|
926
|
+
border-color: var(--frock-action-primary);
|
|
927
|
+
background: var(--frock-action-primary);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.exact-model-setting input[type="checkbox"]:checked::before,
|
|
931
|
+
.notification-setting input[type="checkbox"]:checked::before {
|
|
932
|
+
transform: translateX(16px);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
.exact-model-setting input[type="checkbox"]:focus-visible,
|
|
936
|
+
.notification-setting input[type="checkbox"]:focus-visible {
|
|
937
|
+
outline: 2px solid var(--frock-focus-ring);
|
|
938
|
+
outline-offset: 2px;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
.pending-approvals {
|
|
942
|
+
display: flex;
|
|
943
|
+
flex-direction: column;
|
|
944
|
+
gap: 0.5rem;
|
|
945
|
+
margin: 0;
|
|
946
|
+
padding: 0;
|
|
947
|
+
list-style: none;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
.pending-approvals li {
|
|
951
|
+
display: flex;
|
|
952
|
+
flex-wrap: wrap;
|
|
953
|
+
gap: 0.5rem;
|
|
954
|
+
align-items: center;
|
|
955
|
+
border: 1px solid var(--frock-border);
|
|
956
|
+
border-radius: var(--frock-radius-card);
|
|
957
|
+
background: var(--frock-surface-raised);
|
|
958
|
+
padding: 0.5rem 0.75rem;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
.pending-approvals__risk {
|
|
962
|
+
border: 1px solid var(--frock-border);
|
|
963
|
+
border-radius: var(--frock-radius-control);
|
|
964
|
+
padding: 0.125rem 0.5rem;
|
|
965
|
+
color: var(--frock-text-muted);
|
|
966
|
+
font-size: var(--frock-text-xs);
|
|
967
|
+
text-transform: uppercase;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
.pending-approvals__action {
|
|
971
|
+
flex: 1 1 12rem;
|
|
972
|
+
color: var(--frock-text);
|
|
973
|
+
font-size: var(--frock-text-sm);
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
.pending-approvals__actions {
|
|
977
|
+
display: flex;
|
|
978
|
+
gap: 0.5rem;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.model-note {
|
|
982
|
+
margin: 0;
|
|
983
|
+
color: var(--frock-text-muted);
|
|
984
|
+
font-size: var(--frock-text-sm);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
.advanced {
|
|
988
|
+
border-top: 1px solid var(--frock-border);
|
|
989
|
+
padding-top: 12px;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
.advanced summary {
|
|
993
|
+
display: flex;
|
|
994
|
+
align-items: center;
|
|
995
|
+
justify-content: space-between;
|
|
996
|
+
gap: 8px;
|
|
997
|
+
color: var(--frock-text-muted);
|
|
998
|
+
font-size: var(--frock-text-sm);
|
|
999
|
+
font-weight: 600;
|
|
1000
|
+
list-style: none;
|
|
1001
|
+
cursor: pointer;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
.advanced summary::-webkit-details-marker {
|
|
1005
|
+
display: none;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
.advanced__marker {
|
|
1009
|
+
display: grid;
|
|
1010
|
+
place-items: center;
|
|
1011
|
+
transition: transform var(--frock-motion-fast);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.advanced[open] .advanced__marker {
|
|
1015
|
+
transform: rotate(180deg);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.advanced__body {
|
|
1019
|
+
display: flex;
|
|
1020
|
+
flex-direction: column;
|
|
1021
|
+
gap: 12px;
|
|
1022
|
+
padding-top: 12px;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
.model-mode {
|
|
1026
|
+
display: flex;
|
|
1027
|
+
flex-direction: column;
|
|
1028
|
+
gap: 10px;
|
|
1029
|
+
margin: 0;
|
|
1030
|
+
border: 0;
|
|
1031
|
+
padding: 0;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
.model-mode legend {
|
|
1035
|
+
float: left;
|
|
1036
|
+
width: 100%;
|
|
1037
|
+
margin-bottom: 4px;
|
|
1038
|
+
padding: 0;
|
|
1039
|
+
color: var(--frock-text);
|
|
1040
|
+
font-size: var(--frock-text-md);
|
|
1041
|
+
font-weight: 600;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
.model-mode label {
|
|
1045
|
+
display: flex;
|
|
1046
|
+
align-items: center;
|
|
1047
|
+
gap: 8px;
|
|
1048
|
+
color: var(--frock-text);
|
|
1049
|
+
font-size: var(--frock-text-md);
|
|
1050
|
+
cursor: pointer;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
.model-mode input {
|
|
1054
|
+
width: 17px;
|
|
1055
|
+
height: 17px;
|
|
1056
|
+
flex: 0 0 auto;
|
|
1057
|
+
accent-color: var(--frock-action-primary);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
.settings-error {
|
|
1061
|
+
margin: 0;
|
|
1062
|
+
color: var(--frock-danger-text);
|
|
1063
|
+
font-size: var(--frock-text-sm);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
.settings-actions {
|
|
1067
|
+
display: flex;
|
|
1068
|
+
justify-content: flex-end;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
.primary-contributions {
|
|
1072
|
+
display: flex;
|
|
1073
|
+
flex-direction: column;
|
|
1074
|
+
gap: 12px;
|
|
1075
|
+
}
|
|
1076
|
+
</style>
|