@chai-ui/vue 0.13.0 → 0.14.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/DESIGN.md +3 -1
- package/README.md +3 -0
- package/dist/Composer.vue.d.ts +3 -1
- package/dist/Composer.vue.d.ts.map +1 -1
- package/dist/ResultCard.vue.d.ts +3 -1
- package/dist/ResultCard.vue.d.ts.map +1 -1
- package/dist/composer/PromptField.vue.d.ts +2 -0
- package/dist/composer/PromptField.vue.d.ts.map +1 -1
- package/dist/composer/VoiceButton.vue.d.ts +1 -0
- package/dist/composer/VoiceButton.vue.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +91 -25
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/Composer.vue +58 -9
- package/src/ResultCard.vue +40 -5
- package/src/composer/PromptField.vue +3 -0
- package/src/composer/VoiceButton.vue +2 -1
- package/src/index.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chai-ui/vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "Chai UI Vue components — smart, drop-in UI for AI apps: Composer, Media Analyzer, Result Card, Edit Card, and the useComposer composable.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "kazarazat",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"@fontsource/roboto": "^5.3.0",
|
|
49
49
|
"@material/web": "^2.5.0",
|
|
50
|
-
"@chai-ui/core": "0.
|
|
51
|
-
"@chai-ui/tokens": "0.
|
|
50
|
+
"@chai-ui/core": "0.14.0",
|
|
51
|
+
"@chai-ui/tokens": "0.14.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@vitejs/plugin-vue": "^6.0.9",
|
package/src/Composer.vue
CHANGED
|
@@ -18,6 +18,7 @@ import "@material/web/fab/fab.js";
|
|
|
18
18
|
import { computed, onScopeDispose, ref, shallowRef, watch } from "vue";
|
|
19
19
|
import {
|
|
20
20
|
composerAttachmentsFromFiles,
|
|
21
|
+
composerEvents,
|
|
21
22
|
composerAttachMenuItems,
|
|
22
23
|
composerView,
|
|
23
24
|
createVoiceInput,
|
|
@@ -28,6 +29,7 @@ import {
|
|
|
28
29
|
type ComposerSubmitPayload,
|
|
29
30
|
type ComposerUseCase,
|
|
30
31
|
type ComposerView,
|
|
32
|
+
type ChaiEvent,
|
|
31
33
|
type EditRegion,
|
|
32
34
|
type MediaKind,
|
|
33
35
|
type MenuPlacement,
|
|
@@ -113,6 +115,8 @@ const props = withDefaults(
|
|
|
113
115
|
submitting?: boolean;
|
|
114
116
|
onAbort?: () => void;
|
|
115
117
|
onAttachMenuSelect?: (action: ComposerAttachMenuAction) => void;
|
|
118
|
+
/** Front-end analytics (`@event`): what the person does in the bar, one `ChaiEvent` each, for any analytics tool. No prompt text or file contents, only lengths and counts. */
|
|
119
|
+
onEvent?: (event: ChaiEvent) => void;
|
|
116
120
|
/** Which + menu items to show. Each still shows only when it can work. */
|
|
117
121
|
attachMenuActions?: ComposerAttachMenuAction[];
|
|
118
122
|
/** An edit's regions (from `useComposer`'s `edit`), shown as colored chips while editing. */
|
|
@@ -196,6 +200,7 @@ watch(
|
|
|
196
200
|
);
|
|
197
201
|
|
|
198
202
|
function pickModel(menu: ComposerView["modelMenus"][number], id: string) {
|
|
203
|
+
props.onEvent?.(composerEvents.modelPicked(menu.key, id));
|
|
199
204
|
if (view.value.multiModel) props["onUpdate:modelIds"]?.(menu.next(id));
|
|
200
205
|
else props["onUpdate:modelId"]?.(id);
|
|
201
206
|
}
|
|
@@ -205,6 +210,7 @@ const enhancingField = ref<ComposerPromptField>("prompt");
|
|
|
205
210
|
const enhanceField = (field: ComposerPromptField) =>
|
|
206
211
|
props.onEnhance &&
|
|
207
212
|
((current: string) => {
|
|
213
|
+
props.onEvent?.(composerEvents.enhanceClicked(field, current.length));
|
|
208
214
|
enhancingField.value = field;
|
|
209
215
|
props.onEnhance?.(current, field, view.value.enhanceMode);
|
|
210
216
|
});
|
|
@@ -227,20 +233,54 @@ onScopeDispose(() => {
|
|
|
227
233
|
async function onFiles(files: FileList | null) {
|
|
228
234
|
const onAttachments = props["onUpdate:attachments"];
|
|
229
235
|
if (!files || files.length === 0 || !onAttachments) return;
|
|
236
|
+
// Counted now: the input is cleared for the next pick, which empties its live FileList.
|
|
237
|
+
const picked = files.length;
|
|
230
238
|
// Edit mode works on one image: a new one replaces it, and the edit starts over.
|
|
231
239
|
const { attachments, unsupported } = await composerAttachmentsFromFiles(files, props.attachments, view.value.editing);
|
|
232
240
|
unsupported.forEach((name) => emit("unsupportedFile", name));
|
|
241
|
+
props.onEvent?.(composerEvents.mediaAttached(picked - unsupported.length, unsupported.length));
|
|
233
242
|
if (attachments) onAttachments(attachments);
|
|
234
243
|
}
|
|
235
244
|
|
|
236
245
|
function removeUseCase(useCase: ComposerUseCase) {
|
|
246
|
+
props.onEvent?.(composerEvents.useCaseRemoved(useCase));
|
|
237
247
|
if (props.multiSelectUseCases) props["onUpdate:useCases"]?.(props.useCases.filter((u) => u.kind !== useCase.kind));
|
|
238
248
|
else props.onClearUseCase?.();
|
|
239
249
|
}
|
|
240
250
|
|
|
241
251
|
function onSubmitClick() {
|
|
242
|
-
if (view.value.showStop)
|
|
243
|
-
|
|
252
|
+
if (view.value.showStop) {
|
|
253
|
+
props.onEvent?.(composerEvents.stopped());
|
|
254
|
+
props.onAbort?.();
|
|
255
|
+
} else if (!view.value.isSubmitDisabled && !props.submitting) {
|
|
256
|
+
const payload = view.value.payload();
|
|
257
|
+
props.onEvent?.(composerEvents.submitted(payload, Boolean(props.submitError)));
|
|
258
|
+
emit("submit", payload);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function onMenuSelect(action: ComposerAttachMenuAction) {
|
|
263
|
+
if (action !== "add-media") props.onEvent?.(composerEvents.useCasePicked(action));
|
|
264
|
+
props.onAttachMenuSelect?.(action);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function toggleAutoSelect(on: boolean) {
|
|
268
|
+
props.onEvent?.(composerEvents.autoSelectToggled(on));
|
|
269
|
+
props["onUpdate:autoSelectModel"]?.(on);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function revertField(field: ComposerPromptField) {
|
|
273
|
+
props.onEvent?.(composerEvents.enhanceReverted(field));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function onVoiceStart() {
|
|
277
|
+
props.onEvent?.(composerEvents.voiceStarted());
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** An option menu's pick, reported, then passed to its handler. */
|
|
281
|
+
function pickOption(option: "aspect_ratio" | "voice" | "genre" | "frame_rate", id: string, apply: () => unknown) {
|
|
282
|
+
props.onEvent?.(composerEvents.optionPicked(option, id));
|
|
283
|
+
apply();
|
|
244
284
|
}
|
|
245
285
|
</script>
|
|
246
286
|
|
|
@@ -283,6 +323,7 @@ function onSubmitClick() {
|
|
|
283
323
|
note="(required)"
|
|
284
324
|
:hint="view.environmentHint"
|
|
285
325
|
:on-enhance="enhanceField('environment')"
|
|
326
|
+
:on-revert="() => revertField('environment')"
|
|
286
327
|
:enhancing="Boolean(enhancing) && enhancingField === 'environment'"
|
|
287
328
|
:disabled="disabled"
|
|
288
329
|
@change="emit('update:modelValue', $event)"
|
|
@@ -294,6 +335,7 @@ function onSubmitClick() {
|
|
|
294
335
|
heading="Character"
|
|
295
336
|
note="(optional)"
|
|
296
337
|
:on-enhance="enhanceField('character')"
|
|
338
|
+
:on-revert="() => revertField('character')"
|
|
297
339
|
:enhancing="Boolean(enhancing) && enhancingField === 'character'"
|
|
298
340
|
:disabled="disabled"
|
|
299
341
|
@change="props['onUpdate:character']?.($event)"
|
|
@@ -305,6 +347,7 @@ function onSubmitClick() {
|
|
|
305
347
|
:placeholder="view.placeholder"
|
|
306
348
|
:label="promptLabel"
|
|
307
349
|
:on-enhance="enhanceField('prompt')"
|
|
350
|
+
:on-revert="() => revertField('prompt')"
|
|
308
351
|
:enhancing="Boolean(enhancing)"
|
|
309
352
|
:disabled="disabled"
|
|
310
353
|
@change="emit('update:modelValue', $event)"
|
|
@@ -321,7 +364,7 @@ function onSubmitClick() {
|
|
|
321
364
|
:images-only="view.editing"
|
|
322
365
|
:disabled="disabled"
|
|
323
366
|
:placement="menuPlacement"
|
|
324
|
-
@select="
|
|
367
|
+
@select="onMenuSelect($event)"
|
|
325
368
|
@files="onFiles"
|
|
326
369
|
/>
|
|
327
370
|
|
|
@@ -344,7 +387,7 @@ function onSubmitClick() {
|
|
|
344
387
|
:multiple="menu.multiple"
|
|
345
388
|
:toggle-header="
|
|
346
389
|
showAutoSelectToggle && view.offersAutoSelect
|
|
347
|
-
? { label: 'Auto-select model', value: view.autoSelectModel, onChange:
|
|
390
|
+
? { label: 'Auto-select model', value: view.autoSelectModel, onChange: toggleAutoSelect }
|
|
348
391
|
: undefined
|
|
349
392
|
"
|
|
350
393
|
:trigger-label="menu.triggerLabel"
|
|
@@ -366,7 +409,7 @@ function onSubmitClick() {
|
|
|
366
409
|
:searchable="false"
|
|
367
410
|
trigger-variant="text"
|
|
368
411
|
:disabled="disabled"
|
|
369
|
-
@select="props['onUpdate:aspectRatio']?.($event.id)"
|
|
412
|
+
@select="pickOption('aspect_ratio', $event.id, () => props['onUpdate:aspectRatio']?.($event.id))"
|
|
370
413
|
/>
|
|
371
414
|
|
|
372
415
|
<SearchMenu
|
|
@@ -379,7 +422,7 @@ function onSubmitClick() {
|
|
|
379
422
|
:searchable="view.voiceOptions.length > view.searchableAt"
|
|
380
423
|
trigger-variant="text"
|
|
381
424
|
:disabled="disabled"
|
|
382
|
-
@select="props['onUpdate:speechVoice']?.($event.id)"
|
|
425
|
+
@select="pickOption('voice', $event.id, () => props['onUpdate:speechVoice']?.($event.id))"
|
|
383
426
|
/>
|
|
384
427
|
|
|
385
428
|
<SearchMenu
|
|
@@ -393,7 +436,7 @@ function onSubmitClick() {
|
|
|
393
436
|
:searchable="view.genreOptions.length > view.searchableAt"
|
|
394
437
|
trigger-variant="text"
|
|
395
438
|
:disabled="disabled"
|
|
396
|
-
@select="props['onUpdate:genres']?.(view.toggledGenres($event.id))"
|
|
439
|
+
@select="pickOption('genre', $event.id, () => props['onUpdate:genres']?.(view.toggledGenres($event.id)))"
|
|
397
440
|
/>
|
|
398
441
|
|
|
399
442
|
<SearchMenu
|
|
@@ -406,12 +449,18 @@ function onSubmitClick() {
|
|
|
406
449
|
:searchable="false"
|
|
407
450
|
trigger-variant="text"
|
|
408
451
|
:disabled="disabled"
|
|
409
|
-
@select="props['onUpdate:frameRate']?.(Number($event.id))"
|
|
452
|
+
@select="pickOption('frame_rate', $event.id, () => props['onUpdate:frameRate']?.(Number($event.id)))"
|
|
410
453
|
/>
|
|
411
454
|
</div>
|
|
412
455
|
|
|
413
456
|
<div class="chai-composer__controls-end">
|
|
414
|
-
<VoiceButton
|
|
457
|
+
<VoiceButton
|
|
458
|
+
v-if="voiceInput"
|
|
459
|
+
:voice="voice"
|
|
460
|
+
:state="voiceState"
|
|
461
|
+
:disabled="disabled"
|
|
462
|
+
:on-start="onVoiceStart"
|
|
463
|
+
/>
|
|
415
464
|
<!-- MD3's md-fab has no disabled state of its own; Chai's unfilled-until-ready look comes from `data-ready` in style.css. `label=""` keeps it a plain icon FAB.
|
|
416
465
|
Hidden while a recording is made or transcribed: the transcript is sent once it's in the prompt to check. -->
|
|
417
466
|
<md-fab
|
package/src/ResultCard.vue
CHANGED
|
@@ -13,8 +13,10 @@ import {
|
|
|
13
13
|
formatCost,
|
|
14
14
|
formatDuration,
|
|
15
15
|
formatTokens,
|
|
16
|
+
resultCardEvents,
|
|
16
17
|
resultModelLabel,
|
|
17
18
|
shareMedia,
|
|
19
|
+
type ChaiEvent,
|
|
18
20
|
type ModelOption,
|
|
19
21
|
type Result,
|
|
20
22
|
} from "@chai-ui/core";
|
|
@@ -46,6 +48,8 @@ const props = defineProps<{
|
|
|
46
48
|
/** Extra items in the ⋯ menu. Omit to hide the menu. */
|
|
47
49
|
moreActions?: ResultCardMoreAction[];
|
|
48
50
|
disabled?: boolean;
|
|
51
|
+
/** Front-end analytics (`@event`): a result appearing, result actions, paging and opening the details side, one `ChaiEvent` each. */
|
|
52
|
+
onEvent?: (event: ChaiEvent) => void;
|
|
49
53
|
}>();
|
|
50
54
|
const emit = defineEmits<{
|
|
51
55
|
/** `"like"`, `"dislike"`, `"download"`, `"share"`, `"copy-prompt"`, `"retry"`, or a `moreActions` id. */
|
|
@@ -98,7 +102,38 @@ const { slide, endSlide } = usePageSlide(() => ({
|
|
|
98
102
|
image: kind.value === "image" ? result.value!.output!.src : undefined,
|
|
99
103
|
}));
|
|
100
104
|
|
|
101
|
-
|
|
105
|
+
/** A result action: reported, then emitted with the result it's for. */
|
|
106
|
+
function report(action: string, r: Result) {
|
|
107
|
+
props.onEvent?.(resultCardEvents.action(action));
|
|
108
|
+
emit("action", action, r);
|
|
109
|
+
}
|
|
110
|
+
const act = (action: string) => report(action, result.value!);
|
|
111
|
+
|
|
112
|
+
/** Paging to another result, reported. */
|
|
113
|
+
function goTo(next: number) {
|
|
114
|
+
if (next !== index.value) props.onEvent?.(resultCardEvents.paged(next, props.results.length));
|
|
115
|
+
activeIndex.value = next;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function openInfo() {
|
|
119
|
+
props.onEvent?.(resultCardEvents.infoOpened());
|
|
120
|
+
flipped.value = true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Each result reports once, the first time it's shown done or failed.
|
|
124
|
+
const shownIds = new Set<string>();
|
|
125
|
+
watch(
|
|
126
|
+
() => props.results,
|
|
127
|
+
(results) => {
|
|
128
|
+
for (const r of results) {
|
|
129
|
+
if ((r.status === "done" || r.status === "error") && !shownIds.has(r.id)) {
|
|
130
|
+
shownIds.add(r.id);
|
|
131
|
+
props.onEvent?.(resultCardEvents.shown(r.status, r.output?.kind, results.length));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
{ immediate: true, deep: true }
|
|
136
|
+
);
|
|
102
137
|
|
|
103
138
|
function flash(flag: typeof copied) {
|
|
104
139
|
flag.value = true;
|
|
@@ -114,14 +149,14 @@ function copyPrompt() {
|
|
|
114
149
|
async function download() {
|
|
115
150
|
const r = result.value!;
|
|
116
151
|
if (r.output) await downloadMedia(r.output.src, downloadFilename(r.id, r.output.kind, r.output.src));
|
|
117
|
-
|
|
152
|
+
report("download", r);
|
|
118
153
|
}
|
|
119
154
|
|
|
120
155
|
async function share() {
|
|
121
156
|
const r = result.value!;
|
|
122
157
|
// With no share sheet the link is copied; say so, or the click looks like nothing happened.
|
|
123
158
|
if (r.output && (await shareMedia(r.output.src)) === "copied") flash(linkCopied);
|
|
124
|
-
|
|
159
|
+
report("share", r);
|
|
125
160
|
}
|
|
126
161
|
|
|
127
162
|
// Flipping moves focus to the side now showing, so a keyboard user isn't left on a button that just turned away.
|
|
@@ -206,7 +241,7 @@ function onOverlayKeydown(e: KeyboardEvent) {
|
|
|
206
241
|
:index="index"
|
|
207
242
|
label="Results"
|
|
208
243
|
:disabled="disabled"
|
|
209
|
-
@update:index="
|
|
244
|
+
@update:index="goTo($event)"
|
|
210
245
|
/>
|
|
211
246
|
</div>
|
|
212
247
|
|
|
@@ -223,7 +258,7 @@ function onOverlayKeydown(e: KeyboardEvent) {
|
|
|
223
258
|
<IconButton variant="pill" label="Retry" :disabled="disabled" @click="act('retry')"><RefreshIcon /></IconButton>
|
|
224
259
|
<MoreMenu v-if="moreActions?.length" :actions="moreActions" :disabled="disabled" @select="act($event.id)" />
|
|
225
260
|
</div>
|
|
226
|
-
<IconButton variant="round" label="Show details" :disabled="disabled" @click="
|
|
261
|
+
<IconButton variant="round" label="Show details" :disabled="disabled" @click="openInfo"><InfoIcon /></IconButton>
|
|
227
262
|
</div>
|
|
228
263
|
</div>
|
|
229
264
|
</div>
|
|
@@ -19,6 +19,8 @@ const props = defineProps<{
|
|
|
19
19
|
/** Shown under a headed field, e.g. why submit can't go yet. */
|
|
20
20
|
hint?: string | null;
|
|
21
21
|
onEnhance?: (value: string) => void;
|
|
22
|
+
/** After an Enhance, the button's "Revert to original" click. */
|
|
23
|
+
onRevert?: () => void;
|
|
22
24
|
enhancing: boolean;
|
|
23
25
|
disabled: boolean;
|
|
24
26
|
}>();
|
|
@@ -81,6 +83,7 @@ function onOptimizeClick() {
|
|
|
81
83
|
enhancedValue.value = null;
|
|
82
84
|
originalValue.value = null;
|
|
83
85
|
if (original != null) emit("change", original);
|
|
86
|
+
props.onRevert?.();
|
|
84
87
|
return;
|
|
85
88
|
}
|
|
86
89
|
originalValue.value = props.value; // for undo, before the rewrite lands
|
|
@@ -4,7 +4,7 @@ import { computed } from "vue";
|
|
|
4
4
|
import type { VoiceInput, VoiceInputState } from "@chai-ui/core";
|
|
5
5
|
import { MicIcon, StopIcon } from "../icons.js";
|
|
6
6
|
|
|
7
|
-
const props = defineProps<{ voice: VoiceInput; state: VoiceInputState; disabled?: boolean }>();
|
|
7
|
+
const props = defineProps<{ voice: VoiceInput; state: VoiceInputState; disabled?: boolean; onStart?: () => void }>();
|
|
8
8
|
|
|
9
9
|
const label = computed(() =>
|
|
10
10
|
props.state.status === "recording"
|
|
@@ -20,6 +20,7 @@ function onClick() {
|
|
|
20
20
|
else if (status === "transcribing") props.voice.cancel();
|
|
21
21
|
else {
|
|
22
22
|
props.voice.dismiss();
|
|
23
|
+
props.onStart?.();
|
|
23
24
|
void props.voice.start();
|
|
24
25
|
}
|
|
25
26
|
}
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,8 @@ export {
|
|
|
51
51
|
combinesWithOthers,
|
|
52
52
|
buildJudgePrompt,
|
|
53
53
|
DEFAULT_EVALUATION_CHECKS,
|
|
54
|
+
composerEvents,
|
|
55
|
+
resultCardEvents,
|
|
54
56
|
} from "@chai-ui/core";
|
|
55
57
|
export type {
|
|
56
58
|
AudioUseCase,
|
|
@@ -66,6 +68,8 @@ export type {
|
|
|
66
68
|
Evaluation,
|
|
67
69
|
EvaluationCheck,
|
|
68
70
|
JudgePromptInput,
|
|
71
|
+
ChaiEvent,
|
|
72
|
+
ChaiEventName,
|
|
69
73
|
GenerationEngine,
|
|
70
74
|
GenerationUseCase,
|
|
71
75
|
MediaAnalysisKind,
|