@frockbot/plugin-flock 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/README.md +8 -1
- package/assets/amber-glasses.webp +0 -0
- package/assets/background-bright-orange.webp +0 -0
- package/assets/background-canary-yellow.webp +0 -0
- package/assets/background-electric-blue.webp +0 -0
- package/assets/background-hot-pink.webp +0 -0
- package/assets/background-lime-green.webp +0 -0
- package/assets/background-vivid-purple.webp +0 -0
- package/assets/blue-sailor-cap.webp +0 -0
- package/assets/bronze-aviator-goggles.webp +0 -0
- package/assets/burgundy-beret.webp +0 -0
- package/assets/canary-party-hat.webp +0 -0
- package/assets/canary-rain-cap.webp +0 -0
- package/assets/canonical.webp +0 -0
- package/assets/chef-toque.webp +0 -0
- package/assets/cobalt-round-glasses.webp +0 -0
- package/assets/coral-bucket-hat.webp +0 -0
- package/assets/emerald-necktie.webp +0 -0
- package/assets/forest-ranger-fedora.webp +0 -0
- package/assets/gold-royal-crown.webp +0 -0
- package/assets/gold-sun-medallion.webp +0 -0
- package/assets/graduation-cap.webp +0 -0
- package/assets/green-square-glasses.webp +0 -0
- package/assets/layers.css +159 -0
- package/assets/leather-bell.webp +0 -0
- package/assets/lilac-flower-crown.webp +0 -0
- package/assets/manifest.json +769 -0
- package/assets/matte-black-top-hat.webp +0 -0
- package/assets/navy-wizard-hat.webp +0 -0
- package/assets/orange-hard-hat.webp +0 -0
- package/assets/plum-glasses.webp +0 -0
- package/assets/purple-scarf.webp +0 -0
- package/assets/purple-witch-hat.webp +0 -0
- package/assets/rainbow-propeller-cap.webp +0 -0
- package/assets/rainbow-visor.webp +0 -0
- package/assets/ranger-fedora-feather.webp +0 -0
- package/assets/red-polka-bowtie.webp +0 -0
- package/assets/retro-3d-glasses.webp +0 -0
- package/assets/rose-heart-sunglasses.webp +0 -0
- package/assets/royal-crown-rubies.webp +0 -0
- package/assets/sailor-cap-anchor.webp +0 -0
- package/assets/silver-space-helmet.webp +0 -0
- package/assets/straw-boater.webp +0 -0
- package/assets/teal-bowtie.webp +0 -0
- package/assets/teal-glasses.webp +0 -0
- package/assets/terracotta-beanie.webp +0 -0
- package/assets/tortoiseshell-readers.webp +0 -0
- package/assets/turquoise-bandana.webp +0 -0
- package/assets/violet-masquerade-mask.webp +0 -0
- package/assets/white-snow-goggles.webp +0 -0
- package/assets/white-tennis-visor.webp +0 -0
- package/assets/witch-hat-moon.webp +0 -0
- package/assets/yellow-star-glasses.webp +0 -0
- package/frockbot.json +38 -0
- package/package.json +47 -6
- package/src/agent.test.ts +401 -0
- package/src/agent.ts +571 -0
- package/src/assets.test.ts +31 -0
- package/src/backend.test.ts +205 -0
- package/src/backend.ts +283 -0
- package/src/bot.test.ts +190 -0
- package/src/bot.ts +255 -0
- package/src/client/BotAvatar.vue +20 -0
- package/src/client/FlockAvatar.vue +33 -0
- package/src/client/FlockAvatarEditor.vue +44 -0
- package/src/client/FlockCreateButton.vue +18 -0
- package/src/client/FlockIdentity.vue +31 -0
- package/src/client/FlockOverlay.vue +209 -0
- package/src/client/FlockSidebar.vue +254 -0
- package/src/client/SheepAvatar.vue +28 -0
- package/src/client/dialog-focus.test.ts +15 -0
- package/src/client/dialog-focus.ts +13 -0
- package/src/client/index.test.ts +502 -0
- package/src/client/index.ts +619 -0
- package/src/client/mobile-dialog-layout.test.ts +50 -0
- package/src/client/pending-create.test.ts +71 -0
- package/src/client/pending-create.ts +93 -0
- package/src/client/sidebar.test.ts +77 -0
- package/src/client/sidebar.ts +77 -0
- package/src/client/state.ts +53 -0
- package/src/client/styles.css +611 -0
- package/src/env.d.ts +9 -0
- package/src/index.ts +4 -0
- package/src/manifest.ts +3 -0
- package/src/package.test.ts +12 -0
- package/src/shared.test.ts +197 -0
- package/src/shared.ts +735 -0
- package/src/user.test.ts +455 -0
- package/src/user.ts +465 -0
- package/tsconfig.json +15 -0
- package/validate_readability.py +64 -0
- package/vite.config.ts +30 -0
package/src/bot.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { Plugin } from "cordis";
|
|
2
|
+
import {
|
|
3
|
+
FlockConflictError,
|
|
4
|
+
FlockDecodeError,
|
|
5
|
+
decodeBotLifecycleCommandV1,
|
|
6
|
+
decodeBotLifecycleReceiptV1,
|
|
7
|
+
decodeBotLifecycleViewV1,
|
|
8
|
+
decodeBotRegistrationV1,
|
|
9
|
+
decodeSheepIdentityViewV1,
|
|
10
|
+
decodeStoredBotLifecycleReceiptV1,
|
|
11
|
+
decodeStoredFlockReceiptV1,
|
|
12
|
+
flockCommandFingerprint,
|
|
13
|
+
type BotLifecycleCommandV1,
|
|
14
|
+
type BotLifecycleReceiptV1,
|
|
15
|
+
type BotLifecycleViewV1,
|
|
16
|
+
type BotRegistrationV1,
|
|
17
|
+
type FlockReceiptV1,
|
|
18
|
+
type SheepIdentityViewV1,
|
|
19
|
+
type UpdateSheepCommandV1,
|
|
20
|
+
} from "./shared.js";
|
|
21
|
+
|
|
22
|
+
const IDENTITY_KEY = "flock:sheep:v1";
|
|
23
|
+
const RECEIPT_PREFIX = "flock:sheep-receipt:";
|
|
24
|
+
const LIFECYCLE_KEY = "flock:lifecycle:v1";
|
|
25
|
+
const LIFECYCLE_RECEIPT_PREFIX = "flock:lifecycle-receipt:";
|
|
26
|
+
export interface FlockBotTransaction {
|
|
27
|
+
get<T>(key: string): Promise<T | undefined>;
|
|
28
|
+
list<T>(options: { prefix: string }): Promise<Map<string, T>>;
|
|
29
|
+
put<T>(key: string, value: T): Promise<void>;
|
|
30
|
+
put(entries: Record<string, unknown>): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
interface Storage extends FlockBotTransaction {
|
|
33
|
+
transaction<T>(
|
|
34
|
+
callback: (storage: FlockBotTransaction) => Promise<T>,
|
|
35
|
+
): Promise<T>;
|
|
36
|
+
}
|
|
37
|
+
export interface FlockBotBackendHost {
|
|
38
|
+
storage: Storage;
|
|
39
|
+
materializeSettings(
|
|
40
|
+
registration: BotRegistrationV1,
|
|
41
|
+
userId: string,
|
|
42
|
+
): Promise<void>;
|
|
43
|
+
archiveEligible(storage: FlockBotTransaction): Promise<boolean>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export class BotArchivedError extends Error {
|
|
47
|
+
constructor(readonly botId: string) {
|
|
48
|
+
super(`Bot "${botId}" is archived`);
|
|
49
|
+
this.name = "BotArchivedError";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class FlockBotBackendContribution {
|
|
54
|
+
constructor(private readonly host: FlockBotBackendHost) {}
|
|
55
|
+
|
|
56
|
+
async materialize(
|
|
57
|
+
registrationInput: BotRegistrationV1,
|
|
58
|
+
userId: string,
|
|
59
|
+
): Promise<SheepIdentityViewV1> {
|
|
60
|
+
const registration = decodeBotRegistrationV1(registrationInput);
|
|
61
|
+
await this.host.materializeSettings(registration, userId);
|
|
62
|
+
return this.host.storage.transaction(async (storage) => {
|
|
63
|
+
const existingValue = await storage.get<unknown>(IDENTITY_KEY);
|
|
64
|
+
const lifecycleValue = await storage.get<unknown>(LIFECYCLE_KEY);
|
|
65
|
+
const lifecycle =
|
|
66
|
+
lifecycleValue === undefined
|
|
67
|
+
? {
|
|
68
|
+
schemaVersion: 1 as const,
|
|
69
|
+
botId: registration.botId,
|
|
70
|
+
status: "active" as const,
|
|
71
|
+
revision: 0,
|
|
72
|
+
}
|
|
73
|
+
: decodeBotLifecycleViewV1(lifecycleValue);
|
|
74
|
+
if (lifecycle.botId !== registration.botId)
|
|
75
|
+
throw new Error("Bot lifecycle does not match Bot registration");
|
|
76
|
+
const existing =
|
|
77
|
+
existingValue === undefined
|
|
78
|
+
? undefined
|
|
79
|
+
: decodeSheepIdentityViewV1(existingValue);
|
|
80
|
+
if (existing) {
|
|
81
|
+
if (existing.botId !== registration.botId)
|
|
82
|
+
throw new Error("sheep identity does not match Bot registration");
|
|
83
|
+
if (lifecycleValue === undefined)
|
|
84
|
+
await storage.put(LIFECYCLE_KEY, lifecycle);
|
|
85
|
+
return existing;
|
|
86
|
+
}
|
|
87
|
+
const initial = {
|
|
88
|
+
schemaVersion: 1,
|
|
89
|
+
botId: registration.botId,
|
|
90
|
+
revision: 0,
|
|
91
|
+
sheep: structuredClone(registration.sheep),
|
|
92
|
+
} satisfies SheepIdentityViewV1;
|
|
93
|
+
await storage.put({
|
|
94
|
+
[IDENTITY_KEY]: initial,
|
|
95
|
+
[LIFECYCLE_KEY]: lifecycle,
|
|
96
|
+
});
|
|
97
|
+
return initial;
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async read(
|
|
102
|
+
registration: BotRegistrationV1,
|
|
103
|
+
userId: string,
|
|
104
|
+
): Promise<SheepIdentityViewV1> {
|
|
105
|
+
return structuredClone(await this.materialize(registration, userId));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async update(
|
|
109
|
+
registration: BotRegistrationV1,
|
|
110
|
+
userId: string,
|
|
111
|
+
command: UpdateSheepCommandV1,
|
|
112
|
+
): Promise<FlockReceiptV1> {
|
|
113
|
+
if (registration.botId !== command.botId)
|
|
114
|
+
throw new Error("sheep command does not match Bot registration");
|
|
115
|
+
await this.materialize(registration, userId);
|
|
116
|
+
const fingerprint = flockCommandFingerprint(command);
|
|
117
|
+
return this.host.storage.transaction(async (storage) => {
|
|
118
|
+
await this.assertActive(storage, registration.botId);
|
|
119
|
+
const receiptKey = `${RECEIPT_PREFIX}${command.commandId}`;
|
|
120
|
+
const storedValue = await storage.get<unknown>(receiptKey);
|
|
121
|
+
const stored =
|
|
122
|
+
storedValue === undefined
|
|
123
|
+
? undefined
|
|
124
|
+
: decodeStoredFlockReceiptV1(storedValue);
|
|
125
|
+
if (stored) {
|
|
126
|
+
if (stored.fingerprint !== fingerprint)
|
|
127
|
+
throw new FlockDecodeError(
|
|
128
|
+
`command ID collision: ${command.commandId}`,
|
|
129
|
+
);
|
|
130
|
+
return structuredClone(stored.receipt);
|
|
131
|
+
}
|
|
132
|
+
const currentValue = await storage.get<unknown>(IDENTITY_KEY);
|
|
133
|
+
if (currentValue === undefined)
|
|
134
|
+
throw new Error("sheep identity was not materialized");
|
|
135
|
+
const current = decodeSheepIdentityViewV1(currentValue);
|
|
136
|
+
if (current.revision !== command.expectedRevision)
|
|
137
|
+
throw new FlockConflictError(current.revision);
|
|
138
|
+
const next = {
|
|
139
|
+
...current,
|
|
140
|
+
revision: current.revision + 1,
|
|
141
|
+
sheep: structuredClone(command.sheep),
|
|
142
|
+
} satisfies SheepIdentityViewV1;
|
|
143
|
+
const receipt = {
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
commandId: command.commandId,
|
|
146
|
+
status: "applied",
|
|
147
|
+
revision: next.revision,
|
|
148
|
+
} satisfies FlockReceiptV1;
|
|
149
|
+
await storage.put({
|
|
150
|
+
[IDENTITY_KEY]: next,
|
|
151
|
+
[receiptKey]: { fingerprint, receipt },
|
|
152
|
+
});
|
|
153
|
+
return receipt;
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async readLifecycle(
|
|
158
|
+
registration: BotRegistrationV1,
|
|
159
|
+
userId: string,
|
|
160
|
+
): Promise<BotLifecycleViewV1> {
|
|
161
|
+
await this.materialize(registration, userId);
|
|
162
|
+
const stored = await this.host.storage.get<unknown>(LIFECYCLE_KEY);
|
|
163
|
+
if (stored === undefined)
|
|
164
|
+
throw new Error("Bot lifecycle was not materialized");
|
|
165
|
+
return structuredClone(decodeBotLifecycleViewV1(stored));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async executeLifecycle(
|
|
169
|
+
registration: BotRegistrationV1,
|
|
170
|
+
userId: string,
|
|
171
|
+
input: unknown,
|
|
172
|
+
): Promise<BotLifecycleReceiptV1> {
|
|
173
|
+
const command = decodeBotLifecycleCommandV1(input);
|
|
174
|
+
if (registration.botId !== command.botId)
|
|
175
|
+
throw new Error("lifecycle command does not match Bot registration");
|
|
176
|
+
await this.materialize(registration, userId);
|
|
177
|
+
const fingerprint = flockCommandFingerprint(command);
|
|
178
|
+
return this.host.storage.transaction(async (storage) => {
|
|
179
|
+
const receiptKey = `${LIFECYCLE_RECEIPT_PREFIX}${command.commandId}`;
|
|
180
|
+
const storedReceipt = await storage.get<unknown>(receiptKey);
|
|
181
|
+
if (storedReceipt !== undefined) {
|
|
182
|
+
const decoded = decodeStoredBotLifecycleReceiptV1(storedReceipt);
|
|
183
|
+
if (decoded.fingerprint !== fingerprint)
|
|
184
|
+
throw new FlockDecodeError(
|
|
185
|
+
`command ID collision: ${command.commandId}`,
|
|
186
|
+
);
|
|
187
|
+
return structuredClone(decoded.receipt);
|
|
188
|
+
}
|
|
189
|
+
const currentValue = await storage.get<unknown>(LIFECYCLE_KEY);
|
|
190
|
+
if (currentValue === undefined)
|
|
191
|
+
throw new Error("Bot lifecycle was not materialized");
|
|
192
|
+
const current = decodeBotLifecycleViewV1(currentValue);
|
|
193
|
+
const target = command.type === "bot/archive" ? "archived" : "active";
|
|
194
|
+
let receipt: BotLifecycleReceiptV1;
|
|
195
|
+
if (
|
|
196
|
+
target === "archived" &&
|
|
197
|
+
current.status !== "archived" &&
|
|
198
|
+
!(await this.host.archiveEligible(storage))
|
|
199
|
+
) {
|
|
200
|
+
receipt = {
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
commandId: command.commandId,
|
|
203
|
+
botId: command.botId,
|
|
204
|
+
status: "rejected",
|
|
205
|
+
lifecycle: current,
|
|
206
|
+
failure: "Bot has active or reconciling work",
|
|
207
|
+
};
|
|
208
|
+
} else {
|
|
209
|
+
const lifecycle =
|
|
210
|
+
current.status === target
|
|
211
|
+
? current
|
|
212
|
+
: ({
|
|
213
|
+
...current,
|
|
214
|
+
status: target,
|
|
215
|
+
revision: current.revision + 1,
|
|
216
|
+
} satisfies BotLifecycleViewV1);
|
|
217
|
+
receipt = {
|
|
218
|
+
schemaVersion: 1,
|
|
219
|
+
commandId: command.commandId,
|
|
220
|
+
botId: command.botId,
|
|
221
|
+
status: "applied",
|
|
222
|
+
lifecycle,
|
|
223
|
+
};
|
|
224
|
+
await storage.put(LIFECYCLE_KEY, lifecycle);
|
|
225
|
+
}
|
|
226
|
+
await storage.put(receiptKey, { fingerprint, receipt });
|
|
227
|
+
return decodeBotLifecycleReceiptV1(receipt);
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async assertActive(
|
|
232
|
+
storage: FlockBotTransaction,
|
|
233
|
+
botId: string,
|
|
234
|
+
): Promise<void> {
|
|
235
|
+
const value = await storage.get<unknown>(LIFECYCLE_KEY);
|
|
236
|
+
if (value === undefined) return;
|
|
237
|
+
const lifecycle = decodeBotLifecycleViewV1(value);
|
|
238
|
+
if (lifecycle.botId !== botId)
|
|
239
|
+
throw new Error("Bot lifecycle identity does not match");
|
|
240
|
+
if (lifecycle.status === "archived") throw new BotArchivedError(botId);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function createFlockBotBackendContribution(
|
|
245
|
+
host: FlockBotBackendHost,
|
|
246
|
+
): FlockBotBackendContribution {
|
|
247
|
+
return new FlockBotBackendContribution(host);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export function createFlockBotBackendPlugin(
|
|
251
|
+
host: FlockBotBackendHost,
|
|
252
|
+
lifecycle: { mount(value: FlockBotBackendContribution): () => void },
|
|
253
|
+
): Plugin {
|
|
254
|
+
return () => lifecycle.mount(createFlockBotBackendContribution(host));
|
|
255
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* What a Bot looks like. The Flock's generated sheep recipe is the avatar.
|
|
4
|
+
*/
|
|
5
|
+
import SheepAvatar from "./SheepAvatar.vue";
|
|
6
|
+
import type { SheepRecipeV1 } from "../shared.js";
|
|
7
|
+
|
|
8
|
+
const props = withDefaults(
|
|
9
|
+
defineProps<{
|
|
10
|
+
botId: string;
|
|
11
|
+
sheep: SheepRecipeV1;
|
|
12
|
+
label?: string;
|
|
13
|
+
size?: "mini" | "small" | "large";
|
|
14
|
+
}>(),
|
|
15
|
+
{ label: "Bot avatar", size: "small" },
|
|
16
|
+
);
|
|
17
|
+
</script>
|
|
18
|
+
<template>
|
|
19
|
+
<SheepAvatar :sheep="sheep" :label="label" :size="size" />
|
|
20
|
+
</template>
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
4
|
+
import { flockWebDataKey } from "./state.js";
|
|
5
|
+
import BotAvatar from "./BotAvatar.vue";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The active Bot's sheep, rendered beside each assistant message. The shell
|
|
9
|
+
* owns the slot and the animation; the Flock Package owns what a Bot looks
|
|
10
|
+
* like, so the shell never imports this Package.
|
|
11
|
+
*/
|
|
12
|
+
const flock = inject(flockWebDataKey);
|
|
13
|
+
const shell = inject(frockBotWebDataKey);
|
|
14
|
+
if (!flock || !shell) throw new Error("Flock client data was not provided");
|
|
15
|
+
const identity = computed(() =>
|
|
16
|
+
shell.value.activeBotId
|
|
17
|
+
? flock.value.identities[shell.value.activeBotId]
|
|
18
|
+
: undefined,
|
|
19
|
+
);
|
|
20
|
+
const name = computed(
|
|
21
|
+
() => shell.value.botSettings?.profile.name ?? "This Bot",
|
|
22
|
+
);
|
|
23
|
+
</script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<BotAvatar
|
|
27
|
+
v-if="identity && shell.activeBotId"
|
|
28
|
+
:bot-id="shell.activeBotId"
|
|
29
|
+
:sheep="identity.sheep"
|
|
30
|
+
size="mini"
|
|
31
|
+
:label="name"
|
|
32
|
+
/>
|
|
33
|
+
</template>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { UiButton } from "@frockbot/client-ui";
|
|
3
|
+
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
4
|
+
import { computed, inject } from "vue";
|
|
5
|
+
import BotAvatar from "./BotAvatar.vue";
|
|
6
|
+
import { flockWebDataKey } from "./state.js";
|
|
7
|
+
|
|
8
|
+
const flock = inject(flockWebDataKey);
|
|
9
|
+
const shell = inject(frockBotWebDataKey);
|
|
10
|
+
if (!flock || !shell) throw new Error("Flock client data was not provided");
|
|
11
|
+
flock.value.bindShell(shell);
|
|
12
|
+
|
|
13
|
+
const identity = computed(() =>
|
|
14
|
+
shell.value.activeBotId
|
|
15
|
+
? flock.value.identities[shell.value.activeBotId]
|
|
16
|
+
: undefined,
|
|
17
|
+
);
|
|
18
|
+
const label = computed(
|
|
19
|
+
() => `${shell.value.botSettings?.profile.name ?? "This Bot"} avatar`,
|
|
20
|
+
);
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<template>
|
|
24
|
+
<div v-if="identity && shell.activeBotId" class="flock-avatar-editor">
|
|
25
|
+
<BotAvatar
|
|
26
|
+
:bot-id="shell.activeBotId"
|
|
27
|
+
:sheep="identity.sheep"
|
|
28
|
+
:label="label"
|
|
29
|
+
size="large"
|
|
30
|
+
/>
|
|
31
|
+
<UiButton type="button" variant="ghost" @click="flock.openEdit">
|
|
32
|
+
Change
|
|
33
|
+
</UiButton>
|
|
34
|
+
</div>
|
|
35
|
+
</template>
|
|
36
|
+
|
|
37
|
+
<style scoped>
|
|
38
|
+
.flock-avatar-editor {
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
align-items: center;
|
|
42
|
+
gap: 12px;
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { UiIconButton } from "@frockbot/client-ui";
|
|
3
|
+
import { inject } from "vue";
|
|
4
|
+
import { flockWebDataKey } from "./state.js";
|
|
5
|
+
|
|
6
|
+
const flock = inject(flockWebDataKey);
|
|
7
|
+
if (!flock) throw new Error("Flock client data was not provided");
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<template>
|
|
11
|
+
<UiIconButton
|
|
12
|
+
class="flock-create-button"
|
|
13
|
+
icon="plus"
|
|
14
|
+
label="Create Bot"
|
|
15
|
+
size="md"
|
|
16
|
+
@click="flock.openCreate"
|
|
17
|
+
/>
|
|
18
|
+
</template>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, inject } from "vue";
|
|
3
|
+
import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
|
|
4
|
+
import { flockWebDataKey } from "./state.js";
|
|
5
|
+
import BotAvatar from "./BotAvatar.vue";
|
|
6
|
+
const flock = inject(flockWebDataKey);
|
|
7
|
+
const shell = inject(frockBotWebDataKey);
|
|
8
|
+
if (!flock || !shell) throw new Error("Flock client data was not provided");
|
|
9
|
+
flock.value.bindShell(shell);
|
|
10
|
+
const identity = computed(() =>
|
|
11
|
+
shell.value.activeBotId
|
|
12
|
+
? flock.value.identities[shell.value.activeBotId]
|
|
13
|
+
: undefined,
|
|
14
|
+
);
|
|
15
|
+
</script>
|
|
16
|
+
<template>
|
|
17
|
+
<button
|
|
18
|
+
v-if="identity"
|
|
19
|
+
class="flock-identity-button"
|
|
20
|
+
type="button"
|
|
21
|
+
title="Tailor sheep"
|
|
22
|
+
aria-label="Tailor sheep"
|
|
23
|
+
@click="flock.openEdit"
|
|
24
|
+
>
|
|
25
|
+
<BotAvatar
|
|
26
|
+
:bot-id="shell.activeBotId!"
|
|
27
|
+
:sheep="identity.sheep"
|
|
28
|
+
:label="`${shell.botSettings?.profile.name ?? 'This Bot'} avatar`"
|
|
29
|
+
/></button
|
|
30
|
+
><span v-else>⌁</span>
|
|
31
|
+
</template>
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { inject, nextTick, onBeforeUnmount, ref, watch } from "vue";
|
|
3
|
+
import { sheepCatalog } from "../shared.js";
|
|
4
|
+
import { flockWebDataKey } from "./state.js";
|
|
5
|
+
import { dialogFocusWrapTarget } from "./dialog-focus.js";
|
|
6
|
+
import SheepAvatar from "./SheepAvatar.vue";
|
|
7
|
+
const providedFlock = inject(flockWebDataKey);
|
|
8
|
+
if (!providedFlock) throw new Error("Flock client data was not provided");
|
|
9
|
+
const flock = providedFlock;
|
|
10
|
+
|
|
11
|
+
const dialog = ref<HTMLElement>();
|
|
12
|
+
let restoreFocus: HTMLElement | undefined;
|
|
13
|
+
const focusable =
|
|
14
|
+
'button:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
|
15
|
+
|
|
16
|
+
watch(
|
|
17
|
+
() => flock.value.overlay,
|
|
18
|
+
async (overlay, previous) => {
|
|
19
|
+
if (overlay && !previous) {
|
|
20
|
+
restoreFocus =
|
|
21
|
+
document.activeElement instanceof HTMLElement
|
|
22
|
+
? document.activeElement
|
|
23
|
+
: undefined;
|
|
24
|
+
await nextTick();
|
|
25
|
+
dialog.value
|
|
26
|
+
?.querySelector<HTMLElement>("[autofocus], " + focusable)
|
|
27
|
+
?.focus();
|
|
28
|
+
} else if (!overlay && previous) {
|
|
29
|
+
restoreFocus?.focus();
|
|
30
|
+
restoreFocus = undefined;
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
{ flush: "post" },
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function onDialogKeydown(event: KeyboardEvent): void {
|
|
37
|
+
if (event.key === "Escape") {
|
|
38
|
+
event.preventDefault();
|
|
39
|
+
flock.value.closeOverlay();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (event.key !== "Tab" || !dialog.value) return;
|
|
43
|
+
const controls = [...dialog.value.querySelectorAll<HTMLElement>(focusable)];
|
|
44
|
+
if (controls.length === 0) return;
|
|
45
|
+
const target = dialogFocusWrapTarget(
|
|
46
|
+
controls,
|
|
47
|
+
document.activeElement as HTMLElement | null,
|
|
48
|
+
event.shiftKey,
|
|
49
|
+
);
|
|
50
|
+
if (!target) return;
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
target.focus();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
onBeforeUnmount(() => restoreFocus?.focus());
|
|
56
|
+
</script>
|
|
57
|
+
<template>
|
|
58
|
+
<div
|
|
59
|
+
v-if="flock.overlay"
|
|
60
|
+
class="flock-backdrop"
|
|
61
|
+
@click.self="flock.closeOverlay"
|
|
62
|
+
>
|
|
63
|
+
<section
|
|
64
|
+
ref="dialog"
|
|
65
|
+
class="flock-dialog"
|
|
66
|
+
role="dialog"
|
|
67
|
+
aria-modal="true"
|
|
68
|
+
aria-labelledby="flock-title"
|
|
69
|
+
@keydown="onDialogKeydown"
|
|
70
|
+
>
|
|
71
|
+
<div v-if="flock.overlay === 'archive'" class="flock-form">
|
|
72
|
+
<span class="flock-eyebrow">Archive Bot</span>
|
|
73
|
+
<h1 id="flock-title">Archive this Bot?</h1>
|
|
74
|
+
<p>
|
|
75
|
+
Archiving stops new work and hides the Bot from your active flock.
|
|
76
|
+
History, settings, and Assignments are preserved for restoration.
|
|
77
|
+
</p>
|
|
78
|
+
<p
|
|
79
|
+
v-if="flock.error"
|
|
80
|
+
class="flock-error"
|
|
81
|
+
role="alert"
|
|
82
|
+
aria-live="assertive"
|
|
83
|
+
>
|
|
84
|
+
{{ flock.error }}
|
|
85
|
+
</p>
|
|
86
|
+
<div class="flock-actions">
|
|
87
|
+
<button type="button" @click="flock.closeOverlay">Cancel</button>
|
|
88
|
+
<button class="primary" type="button" @click="flock.archive">
|
|
89
|
+
Archive Bot
|
|
90
|
+
</button>
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
<template v-else>
|
|
94
|
+
<div class="flock-preview">
|
|
95
|
+
<SheepAvatar
|
|
96
|
+
:sheep="flock.draftSheep"
|
|
97
|
+
:label="
|
|
98
|
+
flock.draftName
|
|
99
|
+
? `${flock.draftName} sheep preview`
|
|
100
|
+
: 'Sheep preview'
|
|
101
|
+
"
|
|
102
|
+
size="large"
|
|
103
|
+
/>
|
|
104
|
+
<h2>Meet your sheep</h2>
|
|
105
|
+
<p>Randomly tailored. Entirely yours.</p>
|
|
106
|
+
<button type="button" class="flock-reroll" @click="flock.reroll">
|
|
107
|
+
↻ Surprise me
|
|
108
|
+
</button>
|
|
109
|
+
</div>
|
|
110
|
+
<form
|
|
111
|
+
class="flock-form"
|
|
112
|
+
@submit.prevent="
|
|
113
|
+
flock.overlay === 'create' ? flock.create() : flock.saveSheep()
|
|
114
|
+
"
|
|
115
|
+
>
|
|
116
|
+
<span class="flock-eyebrow">{{
|
|
117
|
+
flock.overlay === "create" ? "Create a Bot" : "Tailor your Bot"
|
|
118
|
+
}}</span>
|
|
119
|
+
<h1 id="flock-title">
|
|
120
|
+
{{
|
|
121
|
+
flock.overlay === "create"
|
|
122
|
+
? "Add to your flock"
|
|
123
|
+
: "Change the look"
|
|
124
|
+
}}
|
|
125
|
+
</h1>
|
|
126
|
+
<p>
|
|
127
|
+
Keep this look or tailor each layer. Your choice is saved with the
|
|
128
|
+
Bot.
|
|
129
|
+
</p>
|
|
130
|
+
<label v-if="flock.overlay === 'create'" class="flock-name"
|
|
131
|
+
>Bot name<input
|
|
132
|
+
v-model.trim="flock.draftName"
|
|
133
|
+
autofocus
|
|
134
|
+
maxlength="100"
|
|
135
|
+
required
|
|
136
|
+
autocomplete="off"
|
|
137
|
+
/></label>
|
|
138
|
+
<fieldset>
|
|
139
|
+
<legend>Sheep wardrobe</legend>
|
|
140
|
+
<div class="flock-select-grid">
|
|
141
|
+
<label
|
|
142
|
+
>Background<select v-model="flock.draftSheep.background">
|
|
143
|
+
<option
|
|
144
|
+
v-for="item in sheepCatalog.backgrounds"
|
|
145
|
+
:key="item.id"
|
|
146
|
+
:value="item.id"
|
|
147
|
+
>
|
|
148
|
+
{{ item.label }}
|
|
149
|
+
</option>
|
|
150
|
+
</select></label
|
|
151
|
+
>
|
|
152
|
+
<label
|
|
153
|
+
>Headwear<select v-model="flock.draftSheep.upper">
|
|
154
|
+
<option
|
|
155
|
+
v-for="item in sheepCatalog.trees.upper"
|
|
156
|
+
:key="item.id"
|
|
157
|
+
:value="item.id"
|
|
158
|
+
>
|
|
159
|
+
{{ item.label }}
|
|
160
|
+
</option>
|
|
161
|
+
</select></label
|
|
162
|
+
>
|
|
163
|
+
<label
|
|
164
|
+
>Face<select v-model="flock.draftSheep.middle">
|
|
165
|
+
<option
|
|
166
|
+
v-for="item in sheepCatalog.trees.middle"
|
|
167
|
+
:key="item.id"
|
|
168
|
+
:value="item.id"
|
|
169
|
+
>
|
|
170
|
+
{{ item.label }}
|
|
171
|
+
</option>
|
|
172
|
+
</select></label
|
|
173
|
+
>
|
|
174
|
+
<label
|
|
175
|
+
>Neckwear<select v-model="flock.draftSheep.lower">
|
|
176
|
+
<option
|
|
177
|
+
v-for="item in sheepCatalog.trees.lower"
|
|
178
|
+
:key="item.id"
|
|
179
|
+
:value="item.id"
|
|
180
|
+
>
|
|
181
|
+
{{ item.label }}
|
|
182
|
+
</option>
|
|
183
|
+
</select></label
|
|
184
|
+
>
|
|
185
|
+
</div>
|
|
186
|
+
</fieldset>
|
|
187
|
+
<div class="flock-note">
|
|
188
|
+
This identity is durable. Refreshing or switching devices won’t
|
|
189
|
+
re-roll it.
|
|
190
|
+
</div>
|
|
191
|
+
<p
|
|
192
|
+
v-if="flock.error"
|
|
193
|
+
class="flock-error"
|
|
194
|
+
role="alert"
|
|
195
|
+
aria-live="assertive"
|
|
196
|
+
>
|
|
197
|
+
{{ flock.error }}
|
|
198
|
+
</p>
|
|
199
|
+
<div class="flock-actions">
|
|
200
|
+
<button type="button" @click="flock.closeOverlay">Cancel</button
|
|
201
|
+
><button class="primary" type="submit">
|
|
202
|
+
{{ flock.overlay === "create" ? "Create Bot" : "Save look" }}
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
</form>
|
|
206
|
+
</template>
|
|
207
|
+
</section>
|
|
208
|
+
</div>
|
|
209
|
+
</template>
|