@chai-ui/vue 0.7.1 → 0.8.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 +1 -0
- package/dist/Composer.vue.d.ts +14 -5
- package/dist/Composer.vue.d.ts.map +1 -1
- package/dist/composer/PromptField.vue.d.ts +21 -0
- package/dist/composer/PromptField.vue.d.ts.map +1 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +277 -131
- package/dist/index.js.map +1 -1
- package/dist/style.css +73 -1
- package/dist/useComposer.d.ts +1 -1
- package/package.json +3 -3
- package/src/Composer.vue +68 -90
- package/src/composer/AttachMenu.vue +2 -2
- package/src/composer/PromptField.vue +157 -0
- package/src/icons.ts +2 -0
- package/src/index.ts +2 -0
package/dist/style.css
CHANGED
|
@@ -593,6 +593,75 @@
|
|
|
593
593
|
opacity: 0.6;
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
+
/* Create world's two prompts, side by side (stacked when narrow): each a
|
|
597
|
+
label and its own Enhance button above a filled box. */
|
|
598
|
+
.chai-composer__world-fields {
|
|
599
|
+
display: flex;
|
|
600
|
+
flex-wrap: wrap;
|
|
601
|
+
gap: var(--chai-space-4);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.chai-composer__world-field {
|
|
605
|
+
flex: 1 1 240px;
|
|
606
|
+
min-width: 0;
|
|
607
|
+
display: flex;
|
|
608
|
+
flex-direction: column;
|
|
609
|
+
gap: var(--chai-space-2);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
.chai-composer__world-field-head {
|
|
613
|
+
display: flex;
|
|
614
|
+
align-items: center;
|
|
615
|
+
justify-content: space-between;
|
|
616
|
+
min-height: 24px;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.chai-composer__world-field-label {
|
|
620
|
+
font-size: var(--chai-type-size-xs);
|
|
621
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
622
|
+
line-height: var(--chai-type-leading-label-small);
|
|
623
|
+
letter-spacing: var(--chai-type-tracking-label-small);
|
|
624
|
+
color: var(--chai-color-semantic-control-text-strong);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.chai-composer__world-field-label strong {
|
|
628
|
+
font-weight: var(--chai-type-weight-semibold);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/* In the heading row, not over the text: no absolute position. */
|
|
632
|
+
.chai-composer__world-field-head .chai-composer__optimize {
|
|
633
|
+
position: static;
|
|
634
|
+
width: 24px;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
.chai-composer__field--boxed {
|
|
638
|
+
min-height: 144px;
|
|
639
|
+
padding: var(--chai-space-3);
|
|
640
|
+
border-radius: var(--chai-radius-md);
|
|
641
|
+
background: var(--chai-color-semantic-control-field);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/* Smaller text than the bar's prompt, and no room kept for an Enhance button. */
|
|
645
|
+
.chai-composer__field--boxed .chai-composer__type {
|
|
646
|
+
padding-right: 0;
|
|
647
|
+
font-size: var(--chai-type-size-body-medium);
|
|
648
|
+
line-height: var(--chai-type-leading-body-medium);
|
|
649
|
+
letter-spacing: var(--chai-type-tracking-body-medium);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/* Why submit can't go yet (a world with no environment). */
|
|
653
|
+
.chai-composer__field-hint {
|
|
654
|
+
margin: 0;
|
|
655
|
+
font-size: var(--chai-type-size-xs);
|
|
656
|
+
line-height: var(--chai-type-leading-label-small);
|
|
657
|
+
letter-spacing: var(--chai-type-tracking-label-small);
|
|
658
|
+
color: var(--chai-composer-placeholder);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
.chai-composer__field--boxed .chai-composer__reveal {
|
|
662
|
+
inset: var(--chai-space-3);
|
|
663
|
+
}
|
|
664
|
+
|
|
596
665
|
/* The optimize/undo reveal (Composer.tsx's own doc comment on the overlay
|
|
597
666
|
for the full mechanics) — the real textarea's text goes fully
|
|
598
667
|
transparent so only the overlay's progressive, gradient-lit copy shows
|
|
@@ -2339,7 +2408,9 @@
|
|
|
2339
2408
|
draw `<md-focus-ring>`. The Composer's textarea is the one control that
|
|
2340
2409
|
had none — its ring goes on the whole pill, matching MD3's ring width
|
|
2341
2410
|
and color so every ring in a component reads as the same system. */
|
|
2342
|
-
.chai-composer:has(.chai-composer__input:focus-visible)
|
|
2411
|
+
.chai-composer:has(.chai-composer__field:not(.chai-composer__field--boxed) .chai-composer__input:focus-visible),
|
|
2412
|
+
/* A world's two boxes: the ring marks the box being typed in, not the bar. */
|
|
2413
|
+
.chai-composer__field--boxed:has(.chai-composer__input:focus-visible) {
|
|
2343
2414
|
outline: 3px solid var(--chai-color-semantic-accent);
|
|
2344
2415
|
outline-offset: 2px;
|
|
2345
2416
|
}
|
|
@@ -2365,6 +2436,7 @@
|
|
|
2365
2436
|
}
|
|
2366
2437
|
|
|
2367
2438
|
[data-chai-focus-ring="hidden"] .chai-composer:has(.chai-composer__input:focus-visible),
|
|
2439
|
+
[data-chai-focus-ring="hidden"] .chai-composer__field--boxed:has(.chai-composer__input:focus-visible),
|
|
2368
2440
|
[data-chai-focus-ring="hidden"] :is(
|
|
2369
2441
|
.chai-composer,
|
|
2370
2442
|
.chai-media-analyzer,
|
package/dist/useComposer.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export declare function useComposer(options?: MaybeRefOrGetter<UseComposerOption
|
|
|
12
12
|
submit: (payload: import("@chai-ui/core").ComposerSubmitPayload) => void;
|
|
13
13
|
cancel: () => void;
|
|
14
14
|
/** `undefined` without reasoning, so the Composer hides its Enhance button. */
|
|
15
|
-
enhance: import("vue").ComputedRef<((value: string) => Promise<string>) | undefined>;
|
|
15
|
+
enhance: import("vue").ComputedRef<((value: string, field?: import("@chai-ui/core").ComposerPromptField) => Promise<string>) | undefined>;
|
|
16
16
|
enhancing: import("vue").ComputedRef<boolean>;
|
|
17
17
|
routing: import("vue").ComputedRef<boolean>;
|
|
18
18
|
/** Props for an `EditCard`, and `regions` for `Composer`. `null` when there's no `editImage`. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chai-ui/vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@fontsource/roboto": "^5.3.0",
|
|
48
48
|
"@material/web": "^2.5.0",
|
|
49
|
-
"@chai-ui/
|
|
50
|
-
"@chai-ui/
|
|
49
|
+
"@chai-ui/core": "0.8.0",
|
|
50
|
+
"@chai-ui/tokens": "0.8.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@vitejs/plugin-vue": "^6.0.9",
|
package/src/Composer.vue
CHANGED
|
@@ -9,30 +9,32 @@
|
|
|
9
9
|
* Some controls show only with somewhere for them to go, so these listeners
|
|
10
10
|
* are props, checked for: `@update:attachments` (add and remove media),
|
|
11
11
|
* `@update:model-id` / `@update:model-ids` (the Model menu),
|
|
12
|
-
* `@update:aspect-ratio`, `@update:
|
|
12
|
+
* `@update:aspect-ratio`, `@update:character` and `@update:frame-rate` (a
|
|
13
|
+
* world's fields), `@update:use-cases` and `@clear-use-case` (chip
|
|
13
14
|
* remove), `@update:regions`, `@enhance` (the Enhance button), `@abort` (stop
|
|
14
15
|
* while submitting) and `@attach-menu-select` (the + menu's use cases).
|
|
15
16
|
*/
|
|
16
17
|
import "@material/web/fab/fab.js";
|
|
17
|
-
import { computed,
|
|
18
|
+
import { computed, ref, watch } from "vue";
|
|
18
19
|
import {
|
|
19
20
|
composerAttachmentsFromFiles,
|
|
20
21
|
composerAttachMenuItems,
|
|
21
22
|
composerView,
|
|
22
|
-
enhanceRevealDurationMs,
|
|
23
|
-
MAX_ANIMATE_ENHANCE_LENGTH,
|
|
24
23
|
type ComposerAttachment,
|
|
25
24
|
type ComposerAttachMenuAction,
|
|
25
|
+
type ComposerPromptField,
|
|
26
26
|
type ComposerSubmitPayload,
|
|
27
27
|
type ComposerUseCase,
|
|
28
28
|
type EditRegion,
|
|
29
29
|
type MediaKind,
|
|
30
30
|
type ModelOption,
|
|
31
|
+
type OutputKind,
|
|
31
32
|
type ParameterOption,
|
|
32
33
|
} from "@chai-ui/core";
|
|
33
34
|
import AttachMenu from "./composer/AttachMenu.vue";
|
|
34
35
|
import Chip from "./composer/Chip.vue";
|
|
35
|
-
import
|
|
36
|
+
import PromptField from "./composer/PromptField.vue";
|
|
37
|
+
import { ArrowForwardIcon, CloseSymbolIcon, ErrorIcon, ImageIcon, MusicNoteIcon, RetryIcon, StopIcon, VideocamIcon } from "./icons.js";
|
|
36
38
|
import SearchMenu from "./primitives/SearchMenu.vue";
|
|
37
39
|
|
|
38
40
|
const props = withDefaults(
|
|
@@ -55,7 +57,7 @@ const props = withDefaults(
|
|
|
55
57
|
useCases?: ComposerUseCase[];
|
|
56
58
|
"onUpdate:useCases"?: (next: ComposerUseCase[]) => void;
|
|
57
59
|
/** Models per use-case kind, for `multiSelectUseCases`. A kind left out gets Chai's suggested models. */
|
|
58
|
-
modelsByKind?: Partial<Record<
|
|
60
|
+
modelsByKind?: Partial<Record<OutputKind, ModelOption[]>>;
|
|
59
61
|
/** The end user can pick several models, each producing its own result, with `modelIds`. */
|
|
60
62
|
multiSelectModels?: boolean;
|
|
61
63
|
modelIds?: string[];
|
|
@@ -73,10 +75,16 @@ const props = withDefaults(
|
|
|
73
75
|
aspectRatios?: ParameterOption<string>[];
|
|
74
76
|
aspectRatio?: string | null;
|
|
75
77
|
"onUpdate:aspectRatio"?: (value: string) => void;
|
|
76
|
-
/** Shows the Enhance button; your handler runs the rewrite and updates the prompt. Gets the current
|
|
77
|
-
onEnhance?: (value: string) => void;
|
|
78
|
+
/** Shows the Enhance button; your handler runs the rewrite and updates the prompt. Gets the current text, and with a world which field it's for. */
|
|
79
|
+
onEnhance?: (value: string, field: ComposerPromptField) => void;
|
|
78
80
|
/** True while your Enhance handler is running. */
|
|
79
81
|
enhancing?: boolean;
|
|
82
|
+
/** A world's character field (Create world), beside the environment (`v-model`). Shows with `v-model:character`. */
|
|
83
|
+
character?: string;
|
|
84
|
+
"onUpdate:character"?: (value: string) => void;
|
|
85
|
+
/** A world's frame rate menu, from the selected models' `frameRates`. */
|
|
86
|
+
frameRate?: number | null;
|
|
87
|
+
"onUpdate:frameRate"?: (value: number) => void;
|
|
80
88
|
/** The last submit's failure: submit becomes retry, and the message shows inline. */
|
|
81
89
|
submitError?: string | null;
|
|
82
90
|
/** Overrides the built-in "empty prompt and no attachments" check. Ignored while `submitError` is set. */
|
|
@@ -99,6 +107,8 @@ const props = withDefaults(
|
|
|
99
107
|
modelIds: () => [],
|
|
100
108
|
modelId: null,
|
|
101
109
|
aspectRatio: null,
|
|
110
|
+
character: "",
|
|
111
|
+
frameRate: null,
|
|
102
112
|
submitError: null,
|
|
103
113
|
// Left undefined unless set: Vue would make an absent boolean `false`,
|
|
104
114
|
// which reads as "never disable".
|
|
@@ -159,64 +169,14 @@ function toggleModel(id: string) {
|
|
|
159
169
|
else props["onUpdate:modelId"]?.(id);
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
() => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
el.style.height = `${el.scrollHeight}px`;
|
|
171
|
-
},
|
|
172
|
-
{ flush: "post", immediate: true }
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Enhance and undo. The prompt already holds the rewrite once `enhancing`
|
|
176
|
-
// drops; the reveal is a decorative overlay painting it in, then gone.
|
|
177
|
-
const originalValue = ref<string | null>(null);
|
|
178
|
-
const enhancedValue = ref<string | null>(null);
|
|
179
|
-
const animatingText = ref<string | null>(null);
|
|
180
|
-
const revealedLength = ref(0);
|
|
181
|
-
let raf = 0;
|
|
182
|
-
|
|
183
|
-
watch(
|
|
184
|
-
() => props.enhancing,
|
|
185
|
-
(now, was) => {
|
|
186
|
-
if (!was || now || originalValue.value == null || props.modelValue === originalValue.value) return;
|
|
187
|
-
const value = props.modelValue;
|
|
188
|
-
enhancedValue.value = value;
|
|
189
|
-
// Long rewrites and reduced-motion requests appear at once.
|
|
190
|
-
if (value.length > MAX_ANIMATE_ENHANCE_LENGTH || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
191
|
-
animatingText.value = value;
|
|
192
|
-
revealedLength.value = 0;
|
|
193
|
-
const duration = enhanceRevealDurationMs(value.length);
|
|
194
|
-
const start = performance.now();
|
|
195
|
-
const tick = (time: number) => {
|
|
196
|
-
revealedLength.value = Math.min(value.length, Math.floor(((time - start) / duration) * value.length));
|
|
197
|
-
if (revealedLength.value < value.length) raf = requestAnimationFrame(tick);
|
|
198
|
-
else animatingText.value = null;
|
|
199
|
-
};
|
|
200
|
-
raf = requestAnimationFrame(tick);
|
|
201
|
-
}
|
|
202
|
-
);
|
|
203
|
-
onScopeDispose(() => cancelAnimationFrame(raf));
|
|
204
|
-
|
|
205
|
-
const animating = computed(() => animatingText.value != null);
|
|
206
|
-
const isEnhanced = computed(() => !animating.value && enhancedValue.value != null && props.modelValue === enhancedValue.value);
|
|
207
|
-
const enhanceBusy = computed(() => Boolean(props.enhancing) || animating.value);
|
|
208
|
-
|
|
209
|
-
function onOptimizeClick() {
|
|
210
|
-
if (isEnhanced.value) {
|
|
211
|
-
const original = originalValue.value;
|
|
212
|
-
enhancedValue.value = null;
|
|
213
|
-
originalValue.value = null;
|
|
214
|
-
if (original != null) emit("update:modelValue", original);
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
217
|
-
originalValue.value = props.modelValue; // for undo, before the rewrite lands
|
|
218
|
-
props.onEnhance?.(props.modelValue);
|
|
219
|
-
}
|
|
172
|
+
// Which field's Enhance is running: `enhancing` is one flag for the bar.
|
|
173
|
+
const enhancingField = ref<ComposerPromptField>("prompt");
|
|
174
|
+
const enhanceField = (field: ComposerPromptField) =>
|
|
175
|
+
props.onEnhance &&
|
|
176
|
+
((current: string) => {
|
|
177
|
+
enhancingField.value = field;
|
|
178
|
+
props.onEnhance?.(current, field);
|
|
179
|
+
});
|
|
220
180
|
|
|
221
181
|
async function onFiles(files: FileList | null) {
|
|
222
182
|
const onAttachments = props["onUpdate:attachments"];
|
|
@@ -268,34 +228,40 @@ function onSubmitClick() {
|
|
|
268
228
|
</li>
|
|
269
229
|
</ul>
|
|
270
230
|
|
|
271
|
-
<div class="chai-
|
|
272
|
-
<
|
|
273
|
-
ref="textarea"
|
|
274
|
-
:class="['chai-composer__input chai-composer__type', { 'chai-composer__input--revealing': animating }]"
|
|
231
|
+
<div v-if="view.world" class="chai-composer__world-fields">
|
|
232
|
+
<PromptField
|
|
275
233
|
:value="modelValue"
|
|
276
234
|
:placeholder="view.placeholder"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
:
|
|
280
|
-
|
|
235
|
+
heading="Environment"
|
|
236
|
+
note="(required)"
|
|
237
|
+
:hint="view.environmentHint"
|
|
238
|
+
:on-enhance="enhanceField('environment')"
|
|
239
|
+
:enhancing="Boolean(enhancing) && enhancingField === 'environment'"
|
|
240
|
+
:disabled="disabled"
|
|
241
|
+
@change="emit('update:modelValue', $event)"
|
|
242
|
+
/>
|
|
243
|
+
<PromptField
|
|
244
|
+
v-if="props['onUpdate:character']"
|
|
245
|
+
:value="character"
|
|
246
|
+
:placeholder="view.characterPlaceholder"
|
|
247
|
+
heading="Character"
|
|
248
|
+
note="(optional)"
|
|
249
|
+
:on-enhance="enhanceField('character')"
|
|
250
|
+
:enhancing="Boolean(enhancing) && enhancingField === 'character'"
|
|
251
|
+
:disabled="disabled"
|
|
252
|
+
@change="props['onUpdate:character']?.($event)"
|
|
281
253
|
/>
|
|
282
|
-
<!-- The Enhance reveal: decorative, so hidden from screen readers, which read the textarea's final text. -->
|
|
283
|
-
<div v-if="animating" class="chai-composer__reveal chai-composer__type" aria-hidden="true">
|
|
284
|
-
<span class="chai-composer__reveal-text">{{ animatingText!.slice(0, revealedLength) }}</span>
|
|
285
|
-
</div>
|
|
286
|
-
<!-- Optimize, or once a rewrite lands, undo it: until the prompt is edited by hand. -->
|
|
287
|
-
<button
|
|
288
|
-
v-if="onEnhance"
|
|
289
|
-
type="button"
|
|
290
|
-
:class="['chai-composer__optimize', { 'chai-composer__optimize--active': enhanceBusy }]"
|
|
291
|
-
:disabled="disabled || (isEnhanced ? false : !modelValue.trim() || enhanceBusy)"
|
|
292
|
-
:aria-label="isEnhanced ? 'Revert to original prompt' : 'Optimize prompt'"
|
|
293
|
-
:title="isEnhanced ? 'Revert to original prompt' : 'Optimize prompt'"
|
|
294
|
-
@click="onOptimizeClick"
|
|
295
|
-
>
|
|
296
|
-
<UndoIcon v-if="isEnhanced" /><OptimizeIcon v-else />
|
|
297
|
-
</button>
|
|
298
254
|
</div>
|
|
255
|
+
<PromptField
|
|
256
|
+
v-else
|
|
257
|
+
:value="modelValue"
|
|
258
|
+
:placeholder="view.placeholder"
|
|
259
|
+
:label="promptLabel"
|
|
260
|
+
:on-enhance="enhanceField('prompt')"
|
|
261
|
+
:enhancing="Boolean(enhancing)"
|
|
262
|
+
:disabled="disabled"
|
|
263
|
+
@change="emit('update:modelValue', $event)"
|
|
264
|
+
/>
|
|
299
265
|
|
|
300
266
|
<p v-if="submitError" class="chai-composer__error" role="alert"><ErrorIcon />{{ submitError }}</p>
|
|
301
267
|
|
|
@@ -350,6 +316,18 @@ function onSubmitClick() {
|
|
|
350
316
|
:disabled="disabled"
|
|
351
317
|
@select="props['onUpdate:aspectRatio']?.($event.id)"
|
|
352
318
|
/>
|
|
319
|
+
|
|
320
|
+
<SearchMenu
|
|
321
|
+
v-if="view.frameRateOptions.length > 0 && props['onUpdate:frameRate']"
|
|
322
|
+
:options="view.frameRateOptions.map((r) => ({ id: String(r), label: `${r}fps` }))"
|
|
323
|
+
:value="view.offeredFrameRate == null ? null : String(view.offeredFrameRate)"
|
|
324
|
+
:trigger-label="view.offeredFrameRate == null ? 'Frame rate' : `${view.offeredFrameRate}fps`"
|
|
325
|
+
menu-label="Frame rate"
|
|
326
|
+
:searchable="false"
|
|
327
|
+
trigger-variant="text"
|
|
328
|
+
:disabled="disabled"
|
|
329
|
+
@select="props['onUpdate:frameRate']?.(Number($event.id))"
|
|
330
|
+
/>
|
|
353
331
|
</div>
|
|
354
332
|
|
|
355
333
|
<!-- 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. -->
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { ref } from "vue";
|
|
4
4
|
import type { ComposerAttachMenuAction, composerAttachMenuItems } from "@chai-ui/core";
|
|
5
5
|
import { useDismiss } from "../dismiss.js";
|
|
6
|
-
import { AttachFileIcon, CloseSymbolIcon, ContentCutIcon, ImageIcon, PlusIcon, VideocamIcon } from "../icons.js";
|
|
6
|
+
import { AttachFileIcon, CloseSymbolIcon, ContentCutIcon, ImageIcon, PlusIcon, GlobeIcon, VideocamIcon } from "../icons.js";
|
|
7
7
|
|
|
8
8
|
defineProps<{
|
|
9
9
|
items: ReturnType<typeof composerAttachMenuItems>;
|
|
@@ -13,7 +13,7 @@ defineProps<{
|
|
|
13
13
|
}>();
|
|
14
14
|
const emit = defineEmits<{ select: [action: ComposerAttachMenuAction]; files: [files: FileList | null] }>();
|
|
15
15
|
|
|
16
|
-
const ICONS = { "add-media": AttachFileIcon, "create-image": ImageIcon, "create-video": VideocamIcon, "edit-media": ContentCutIcon };
|
|
16
|
+
const ICONS = { "add-media": AttachFileIcon, "create-image": ImageIcon, "create-video": VideocamIcon, "create-world": GlobeIcon, "edit-media": ContentCutIcon };
|
|
17
17
|
const open = ref(false);
|
|
18
18
|
const root = ref<HTMLElement>();
|
|
19
19
|
const input = ref<HTMLInputElement>();
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* One prompt textarea with its Enhance button: grows with wrapped text,
|
|
4
|
+
* reveals a rewrite as it lands, and offers undo until the text is edited by
|
|
5
|
+
* hand. A `heading` gives it a visible label above a filled box (a world's
|
|
6
|
+
* Environment and Character); without one it's the bar's plain prompt.
|
|
7
|
+
*/
|
|
8
|
+
import { computed, onScopeDispose, ref, useId, watch } from "vue";
|
|
9
|
+
import { enhanceRevealDurationMs, MAX_ANIMATE_ENHANCE_LENGTH } from "@chai-ui/core";
|
|
10
|
+
import { OptimizeIcon, UndoIcon } from "../icons.js";
|
|
11
|
+
|
|
12
|
+
const props = defineProps<{
|
|
13
|
+
value: string;
|
|
14
|
+
placeholder: string;
|
|
15
|
+
/** Accessible name for the plain prompt. Defaults to `placeholder`. */
|
|
16
|
+
label?: string;
|
|
17
|
+
heading?: string;
|
|
18
|
+
note?: string;
|
|
19
|
+
/** Shown under a headed field, e.g. why submit can't go yet. */
|
|
20
|
+
hint?: string | null;
|
|
21
|
+
onEnhance?: (value: string) => void;
|
|
22
|
+
enhancing: boolean;
|
|
23
|
+
disabled: boolean;
|
|
24
|
+
}>();
|
|
25
|
+
const emit = defineEmits<{ change: [value: string] }>();
|
|
26
|
+
const id = useId();
|
|
27
|
+
|
|
28
|
+
// Grows with wrapped text from one line, instead of a fixed row count.
|
|
29
|
+
const textarea = ref<HTMLTextAreaElement>();
|
|
30
|
+
watch(
|
|
31
|
+
() => props.value,
|
|
32
|
+
() => {
|
|
33
|
+
const el = textarea.value;
|
|
34
|
+
if (!el) return;
|
|
35
|
+
el.style.height = "auto";
|
|
36
|
+
el.style.height = `${el.scrollHeight}px`;
|
|
37
|
+
},
|
|
38
|
+
{ flush: "post", immediate: true }
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// Enhance and undo. The prompt already holds the rewrite once `enhancing`
|
|
42
|
+
// drops; the reveal is a decorative overlay painting it in, then gone.
|
|
43
|
+
const originalValue = ref<string | null>(null);
|
|
44
|
+
const enhancedValue = ref<string | null>(null);
|
|
45
|
+
const animatingText = ref<string | null>(null);
|
|
46
|
+
const revealedLength = ref(0);
|
|
47
|
+
let raf = 0;
|
|
48
|
+
|
|
49
|
+
watch(
|
|
50
|
+
() => props.enhancing,
|
|
51
|
+
(now, was) => {
|
|
52
|
+
if (!was || now || originalValue.value == null || props.value === originalValue.value) return;
|
|
53
|
+
const value = props.value;
|
|
54
|
+
enhancedValue.value = value;
|
|
55
|
+
// Long rewrites and reduced-motion requests appear at once.
|
|
56
|
+
if (value.length > MAX_ANIMATE_ENHANCE_LENGTH || window.matchMedia?.("(prefers-reduced-motion: reduce)").matches) return;
|
|
57
|
+
animatingText.value = value;
|
|
58
|
+
revealedLength.value = 0;
|
|
59
|
+
const duration = enhanceRevealDurationMs(value.length);
|
|
60
|
+
const start = performance.now();
|
|
61
|
+
const tick = (time: number) => {
|
|
62
|
+
revealedLength.value = Math.min(value.length, Math.floor(((time - start) / duration) * value.length));
|
|
63
|
+
if (revealedLength.value < value.length) raf = requestAnimationFrame(tick);
|
|
64
|
+
else animatingText.value = null;
|
|
65
|
+
};
|
|
66
|
+
raf = requestAnimationFrame(tick);
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
onScopeDispose(() => cancelAnimationFrame(raf));
|
|
70
|
+
|
|
71
|
+
const animating = computed(() => animatingText.value != null);
|
|
72
|
+
const isEnhanced = computed(() => !animating.value && enhancedValue.value != null && props.value === enhancedValue.value);
|
|
73
|
+
const enhanceBusy = computed(() => props.enhancing || animating.value);
|
|
74
|
+
const optimizeName = computed(
|
|
75
|
+
() => `${isEnhanced.value ? "Revert to original" : "Optimize"} ${props.heading ? props.heading.toLowerCase() : "prompt"}`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
function onOptimizeClick() {
|
|
79
|
+
if (isEnhanced.value) {
|
|
80
|
+
const original = originalValue.value;
|
|
81
|
+
enhancedValue.value = null;
|
|
82
|
+
originalValue.value = null;
|
|
83
|
+
if (original != null) emit("change", original);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
originalValue.value = props.value; // for undo, before the rewrite lands
|
|
87
|
+
props.onEnhance?.(props.value);
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<template>
|
|
92
|
+
<!-- A world's labeled field: heading and Enhance above a filled box. -->
|
|
93
|
+
<div v-if="heading" class="chai-composer__world-field">
|
|
94
|
+
<div class="chai-composer__world-field-head">
|
|
95
|
+
<label :for="id" class="chai-composer__world-field-label"><strong>{{ heading }}</strong> {{ note }}</label>
|
|
96
|
+
<!-- Optimize, or once a rewrite lands, undo it: until the text is edited by hand. -->
|
|
97
|
+
<button
|
|
98
|
+
v-if="onEnhance"
|
|
99
|
+
type="button"
|
|
100
|
+
:class="['chai-composer__optimize', { 'chai-composer__optimize--active': enhanceBusy }]"
|
|
101
|
+
:disabled="disabled || (isEnhanced ? false : !value.trim() || enhanceBusy)"
|
|
102
|
+
:aria-label="optimizeName"
|
|
103
|
+
:title="optimizeName"
|
|
104
|
+
@click="onOptimizeClick"
|
|
105
|
+
>
|
|
106
|
+
<UndoIcon v-if="isEnhanced" /><OptimizeIcon v-else />
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
<div class="chai-composer__field chai-composer__field--boxed">
|
|
110
|
+
<textarea
|
|
111
|
+
:id="id"
|
|
112
|
+
ref="textarea"
|
|
113
|
+
:class="['chai-composer__input chai-composer__type', { 'chai-composer__input--revealing': animating }]"
|
|
114
|
+
:value="value"
|
|
115
|
+
:placeholder="placeholder"
|
|
116
|
+
:aria-describedby="hint ? `${id}-hint` : undefined"
|
|
117
|
+
rows="1"
|
|
118
|
+
:disabled="disabled || animating"
|
|
119
|
+
@input="emit('change', ($event.target as HTMLTextAreaElement).value)"
|
|
120
|
+
/>
|
|
121
|
+
<div v-if="animating" class="chai-composer__reveal chai-composer__type" aria-hidden="true">
|
|
122
|
+
<span class="chai-composer__reveal-text">{{ animatingText!.slice(0, revealedLength) }}</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<p v-if="hint" :id="`${id}-hint`" class="chai-composer__field-hint">{{ hint }}</p>
|
|
126
|
+
</div>
|
|
127
|
+
|
|
128
|
+
<div v-else class="chai-composer__field">
|
|
129
|
+
<textarea
|
|
130
|
+
:id="id"
|
|
131
|
+
ref="textarea"
|
|
132
|
+
:class="['chai-composer__input chai-composer__type', { 'chai-composer__input--revealing': animating }]"
|
|
133
|
+
:value="value"
|
|
134
|
+
:placeholder="placeholder"
|
|
135
|
+
:aria-label="label ?? placeholder"
|
|
136
|
+
rows="1"
|
|
137
|
+
:disabled="disabled || animating"
|
|
138
|
+
@input="emit('change', ($event.target as HTMLTextAreaElement).value)"
|
|
139
|
+
/>
|
|
140
|
+
<!-- The Enhance reveal: decorative, so hidden from screen readers, which read the textarea's final text. -->
|
|
141
|
+
<div v-if="animating" class="chai-composer__reveal chai-composer__type" aria-hidden="true">
|
|
142
|
+
<span class="chai-composer__reveal-text">{{ animatingText!.slice(0, revealedLength) }}</span>
|
|
143
|
+
</div>
|
|
144
|
+
<!-- Optimize, or once a rewrite lands, undo it: until the prompt is edited by hand. -->
|
|
145
|
+
<button
|
|
146
|
+
v-if="onEnhance"
|
|
147
|
+
type="button"
|
|
148
|
+
:class="['chai-composer__optimize', { 'chai-composer__optimize--active': enhanceBusy }]"
|
|
149
|
+
:disabled="disabled || (isEnhanced ? false : !value.trim() || enhanceBusy)"
|
|
150
|
+
:aria-label="optimizeName"
|
|
151
|
+
:title="optimizeName"
|
|
152
|
+
@click="onOptimizeClick"
|
|
153
|
+
>
|
|
154
|
+
<UndoIcon v-if="isEnhanced" /><OptimizeIcon v-else />
|
|
155
|
+
</button>
|
|
156
|
+
</div>
|
|
157
|
+
</template>
|
package/src/icons.ts
CHANGED
|
@@ -50,6 +50,8 @@ export const OptimizeIcon = icon("0 0 25 24", [{ "d": "M5 19H6.425L16.2 9.225L14
|
|
|
50
50
|
export const UndoIcon = icon("0 -960 960 960", [{ "d": "M259-200v-60h310q70 0 120.5-46.5T740-422q0-69-50.5-115.5T569-584H274l114 114-42 42-186-186 186-186 42 42-114 114h294q95 0 163.5 64T800-422q0 94-68.5 158T568-200H259Z", "fill": "currentColor" }]);
|
|
51
51
|
export const AttachFileIcon = icon("0 -960 960 960", [{ "d": "M728-326q0 103-72.18 174.5-72.17 71.5-175 71.5Q378-80 305.5-151.5T233-326v-380q0-72.5 51.5-123.25T408-880q72 0 123.5 50.75T583-706v360q0 42-30 72t-72.5 30q-42.5 0-72.5-29.67-30-29.68-30-72.33v-370h60v370q0 17 12.5 29.5t30.64 12.5q18.14 0 30-12.5T523-346v-360q0-48-33.5-81t-81.71-33q-48.21 0-81.5 33.06T293-706v380q0 78 54.97 132T481-140q77.92 0 132.46-54Q668-248 668-326v-390h60v390Z", "fill": "currentColor" }]);
|
|
52
52
|
export const ImageIcon = icon("0 -960 960 960", [{ "d": "M180-120q-24 0-42-18t-18-42v-600q0-24 18-42t42-18h600q24 0 42 18t18 42v600q0 24-18 42t-42 18H180Zm0-60h600v-600H180v600Zm56-97h489L578-473 446-302l-93-127-117 152Zm-56 97v-600 600Z", "fill": "currentColor" }]);
|
|
53
|
+
/** Material Symbols "globe" — Create world. */
|
|
54
|
+
export const GlobeIcon = icon("0 -960 960 960", [{ "d": "M480.27-80q-82.74 0-155.5-31.5Q252-143 197.5-197.5t-86-127.34Q80-397.68 80-480.5t31.5-155.66Q143-709 197.5-763t127.34-85.5Q397.68-880 480.5-880t155.66 31.5Q709-817 763-763t85.5 127Q880-563 880-480.27q0 82.74-31.5 155.5Q817-252 763-197.68q-54 54.31-127 86Q563-80 480.27-80Zm-.27-60q142.38 0 241.19-99.5T820-480v-13q-6 26-27.41 43.5Q771.19-432 742-432h-80q-33 0-56.5-23.5T582-512v-40H422v-80q0-33 23.5-56.5T502-712h40v-22q0-16 13.5-40t30.5-29q-25-8-51.36-12.5Q508.29-820 480-820q-141 0-240.5 98.81T140-480h150q66 0 113 47t47 113v40H330v105q34 17 71.7 26t78.3 9Z", "fill": "currentColor" }]);
|
|
53
55
|
export const VideocamIcon = icon("0 -960 960 960", [{ "d": "M140-160q-24 0-42-18t-18-42v-520q0-24 18-42t42-18h520q24 0 42 18t18 42v215l160-160v410L720-435v215q0 24-18 42t-42 18H140Zm0-60h520v-520H140v520Zm0 0v-520 520Z", "fill": "currentColor" }]);
|
|
54
56
|
export const ContentCutIcon = icon("0 -960 960 960", [{ "d": "M782-114 481-415 364-298q11 17 13.5 33t2.5 35q0 64-43 107T230-80q-64 0-107-43T80-230q0-64 43-107t107-43q18 0 35.5 5t36.5 15l116-116-118-118q-17 8-34.5 11t-35.5 3q-64 0-107-43T80-730q0-64 43-107t107-43q64 0 107 43t43 107q0 19-2.5 36T367-662l514 514v34h-99ZM599-527l-66-66 249-249h99v33L599-527ZM294-666q26-26 26-64t-26-64q-26-26-64-26t-64 26q-26 26-26 64t26 64q26 26 64 26t64-26Zm202.5 203.5Q502-468 502-476t-5.5-13.5Q491-495 483-495t-13.5 5.5Q464-484 464-476t5.5 13.5Q475-457 483-457t13.5-5.5ZM294-166q26-26 26-64t-26-64q-26-26-64-26t-64 26q-26 26-26 64t26 64q26 26 64 26t64-26Z", "fill": "currentColor" }]);
|
|
55
57
|
export const MusicNoteIcon = icon("0 -960 960 960", [{ "d": "M286.5-163.5Q243-207 243-270t43.5-106.5Q330-420 393-420q28 0 50.5 8t39.5 22v-450h234v135H543v435q0 63-43.5 106.5T393-120q-63 0-106.5-43.5Z", "fill": "currentColor" }]);
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ export {
|
|
|
46
46
|
} from "@chai-ui/core";
|
|
47
47
|
export type {
|
|
48
48
|
ComposerAttachMenuAction,
|
|
49
|
+
ComposerPromptField,
|
|
49
50
|
ComposerAttachment,
|
|
50
51
|
ComposerSelection,
|
|
51
52
|
ComposerSubmitPayload,
|
|
@@ -62,6 +63,7 @@ export type {
|
|
|
62
63
|
MediaAnalyzerSubmitPayload,
|
|
63
64
|
MediaKind,
|
|
64
65
|
ModelOption,
|
|
66
|
+
OutputKind,
|
|
65
67
|
ParameterOption,
|
|
66
68
|
Reasoning,
|
|
67
69
|
RegionBox,
|