@chai-ui/vue 0.7.1 → 0.9.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 +2 -0
- package/dist/Composer.vue.d.ts +21 -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/composer/VoiceButton.vue.d.ts +10 -0
- package/dist/composer/VoiceButton.vue.d.ts.map +1 -0
- package/dist/composer/VoiceWaveform.vue.d.ts +8 -0
- package/dist/composer/VoiceWaveform.vue.d.ts.map +1 -0
- package/dist/icons.d.ts +4 -0
- package/dist/icons.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +412 -140
- package/dist/index.js.map +1 -1
- package/dist/style.css +166 -1
- package/dist/useComposer.d.ts +1 -1
- package/package.json +3 -3
- package/src/Composer.vue +100 -88
- package/src/composer/AttachMenu.vue +2 -2
- package/src/composer/PromptField.vue +157 -0
- package/src/composer/VoiceButton.vue +50 -0
- package/src/composer/VoiceWaveform.vue +17 -0
- package/src/icons.ts +4 -0
- package/src/index.ts +6 -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
|
|
@@ -682,6 +751,99 @@
|
|
|
682
751
|
flex-wrap: wrap;
|
|
683
752
|
}
|
|
684
753
|
|
|
754
|
+
.chai-composer__controls-end {
|
|
755
|
+
display: flex;
|
|
756
|
+
align-items: center;
|
|
757
|
+
gap: var(--chai-space-2);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/* Voice input: the mic left of submit. While recording it becomes a stop
|
|
761
|
+
button (a filled square in a ringed circle) and a waveform replaces the
|
|
762
|
+
prompt; while transcribing, a spinner. */
|
|
763
|
+
.chai-composer__voice {
|
|
764
|
+
display: flex;
|
|
765
|
+
align-items: center;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
.chai-composer__voice-button {
|
|
769
|
+
display: flex;
|
|
770
|
+
align-items: center;
|
|
771
|
+
justify-content: center;
|
|
772
|
+
width: 40px;
|
|
773
|
+
height: 40px;
|
|
774
|
+
padding: 0;
|
|
775
|
+
border: 2px solid transparent;
|
|
776
|
+
border-radius: var(--chai-radius-full);
|
|
777
|
+
background: transparent;
|
|
778
|
+
color: var(--chai-composer-text);
|
|
779
|
+
cursor: pointer;
|
|
780
|
+
transition: background-color var(--chai-motion-duration-fast) var(--chai-motion-easing-standard);
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
.chai-composer__voice-button:hover:not(:disabled) {
|
|
784
|
+
background: var(--chai-composer-hover);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.chai-composer__voice-button:disabled {
|
|
788
|
+
cursor: not-allowed;
|
|
789
|
+
opacity: 0.5;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
.chai-composer__voice-button svg {
|
|
793
|
+
width: 24px;
|
|
794
|
+
height: 24px;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
.chai-composer__voice[data-status="recording"] .chai-composer__voice-button {
|
|
798
|
+
border-color: var(--chai-composer-text);
|
|
799
|
+
background: var(--chai-composer-hover);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
.chai-composer__voice[data-status="recording"] .chai-composer__voice-button svg {
|
|
803
|
+
width: 18px;
|
|
804
|
+
height: 18px;
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/* Same row height as the prompt it stands in for. */
|
|
808
|
+
.chai-composer__waveform {
|
|
809
|
+
display: flex;
|
|
810
|
+
align-items: center;
|
|
811
|
+
gap: var(--chai-space-3);
|
|
812
|
+
min-height: var(--chai-type-leading-body-large);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
.chai-composer__waveform-bars {
|
|
816
|
+
flex: 1;
|
|
817
|
+
display: flex;
|
|
818
|
+
align-items: center;
|
|
819
|
+
justify-content: space-between;
|
|
820
|
+
height: var(--chai-type-leading-body-large);
|
|
821
|
+
min-width: 0;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
.chai-composer__waveform-bar {
|
|
825
|
+
width: 2px;
|
|
826
|
+
border-radius: var(--chai-radius-full);
|
|
827
|
+
background: var(--chai-composer-text);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
.chai-composer__waveform-time {
|
|
831
|
+
color: var(--chai-composer-text);
|
|
832
|
+
font-family: var(--chai-type-family-material);
|
|
833
|
+
font-size: var(--chai-type-size-label-large);
|
|
834
|
+
font-weight: var(--chai-type-weight-material-medium);
|
|
835
|
+
font-variant-numeric: tabular-nums;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
.chai-composer__voice-spinner {
|
|
839
|
+
width: 20px;
|
|
840
|
+
height: 20px;
|
|
841
|
+
border-radius: 50%;
|
|
842
|
+
border: 2px solid var(--chai-color-semantic-control-border-faint);
|
|
843
|
+
border-top-color: var(--chai-color-semantic-accent);
|
|
844
|
+
animation: chai-spin var(--chai-motion-duration-steep) linear infinite;
|
|
845
|
+
}
|
|
846
|
+
|
|
685
847
|
/* Neutral trigger text, not `--md-sys-color-primary` green (account
|
|
686
848
|
holder correction: "don't make dropdown select menu buttons green when
|
|
687
849
|
they don't need to be"). */
|
|
@@ -2339,7 +2501,9 @@
|
|
|
2339
2501
|
draw `<md-focus-ring>`. The Composer's textarea is the one control that
|
|
2340
2502
|
had none — its ring goes on the whole pill, matching MD3's ring width
|
|
2341
2503
|
and color so every ring in a component reads as the same system. */
|
|
2342
|
-
.chai-composer:has(.chai-composer__input:focus-visible)
|
|
2504
|
+
.chai-composer:has(.chai-composer__field:not(.chai-composer__field--boxed) .chai-composer__input:focus-visible),
|
|
2505
|
+
/* A world's two boxes: the ring marks the box being typed in, not the bar. */
|
|
2506
|
+
.chai-composer__field--boxed:has(.chai-composer__input:focus-visible) {
|
|
2343
2507
|
outline: 3px solid var(--chai-color-semantic-accent);
|
|
2344
2508
|
outline-offset: 2px;
|
|
2345
2509
|
}
|
|
@@ -2365,6 +2529,7 @@
|
|
|
2365
2529
|
}
|
|
2366
2530
|
|
|
2367
2531
|
[data-chai-focus-ring="hidden"] .chai-composer:has(.chai-composer__input:focus-visible),
|
|
2532
|
+
[data-chai-focus-ring="hidden"] .chai-composer__field--boxed:has(.chai-composer__input:focus-visible),
|
|
2368
2533
|
[data-chai-focus-ring="hidden"] :is(
|
|
2369
2534
|
.chai-composer,
|
|
2370
2535
|
.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, mode?: import("@chai-ui/core").GenerationUseCase) => 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.9.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.9.0",
|
|
50
|
+
"@chai-ui/tokens": "0.9.0"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"@vitejs/plugin-vue": "^6.0.9",
|
package/src/Composer.vue
CHANGED
|
@@ -9,30 +9,37 @@
|
|
|
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
|
+
* `voiceInput` (the mic), `@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, onScopeDispose, ref, watch } from "vue";
|
|
18
|
+
import { computed, onScopeDispose, ref, shallowRef, watch } from "vue";
|
|
18
19
|
import {
|
|
19
20
|
composerAttachmentsFromFiles,
|
|
20
21
|
composerAttachMenuItems,
|
|
21
22
|
composerView,
|
|
22
|
-
|
|
23
|
-
MAX_ANIMATE_ENHANCE_LENGTH,
|
|
23
|
+
createVoiceInput,
|
|
24
24
|
type ComposerAttachment,
|
|
25
25
|
type ComposerAttachMenuAction,
|
|
26
|
+
type ComposerPromptField,
|
|
27
|
+
type GenerationUseCase,
|
|
26
28
|
type ComposerSubmitPayload,
|
|
27
29
|
type ComposerUseCase,
|
|
28
30
|
type EditRegion,
|
|
29
31
|
type MediaKind,
|
|
30
32
|
type ModelOption,
|
|
33
|
+
type OutputKind,
|
|
31
34
|
type ParameterOption,
|
|
35
|
+
type VoiceInputConfig,
|
|
32
36
|
} from "@chai-ui/core";
|
|
33
37
|
import AttachMenu from "./composer/AttachMenu.vue";
|
|
34
38
|
import Chip from "./composer/Chip.vue";
|
|
35
|
-
import
|
|
39
|
+
import PromptField from "./composer/PromptField.vue";
|
|
40
|
+
import VoiceButton from "./composer/VoiceButton.vue";
|
|
41
|
+
import VoiceWaveform from "./composer/VoiceWaveform.vue";
|
|
42
|
+
import { ArrowForwardIcon, CloseSymbolIcon, ErrorIcon, ImageIcon, MusicNoteIcon, RetryIcon, StopIcon, VideocamIcon } from "./icons.js";
|
|
36
43
|
import SearchMenu from "./primitives/SearchMenu.vue";
|
|
37
44
|
|
|
38
45
|
const props = withDefaults(
|
|
@@ -55,7 +62,7 @@ const props = withDefaults(
|
|
|
55
62
|
useCases?: ComposerUseCase[];
|
|
56
63
|
"onUpdate:useCases"?: (next: ComposerUseCase[]) => void;
|
|
57
64
|
/** Models per use-case kind, for `multiSelectUseCases`. A kind left out gets Chai's suggested models. */
|
|
58
|
-
modelsByKind?: Partial<Record<
|
|
65
|
+
modelsByKind?: Partial<Record<OutputKind, ModelOption[]>>;
|
|
59
66
|
/** The end user can pick several models, each producing its own result, with `modelIds`. */
|
|
60
67
|
multiSelectModels?: boolean;
|
|
61
68
|
modelIds?: string[];
|
|
@@ -73,10 +80,16 @@ const props = withDefaults(
|
|
|
73
80
|
aspectRatios?: ParameterOption<string>[];
|
|
74
81
|
aspectRatio?: string | null;
|
|
75
82
|
"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;
|
|
83
|
+
/** Shows the Enhance button; your handler runs the rewrite and updates the prompt. Gets the current text, which field, and the use case on screen (`mode`). */
|
|
84
|
+
onEnhance?: (value: string, field: ComposerPromptField, mode?: GenerationUseCase) => void;
|
|
78
85
|
/** True while your Enhance handler is running. */
|
|
79
86
|
enhancing?: boolean;
|
|
87
|
+
/** A world's character field (Create world), beside the environment (`v-model`). Shows with `v-model:character`. */
|
|
88
|
+
character?: string;
|
|
89
|
+
"onUpdate:character"?: (value: string) => void;
|
|
90
|
+
/** A world's frame rate menu, from the selected models' `frameRates`. */
|
|
91
|
+
frameRate?: number | null;
|
|
92
|
+
"onUpdate:frameRate"?: (value: number) => void;
|
|
80
93
|
/** The last submit's failure: submit becomes retry, and the message shows inline. */
|
|
81
94
|
submitError?: string | null;
|
|
82
95
|
/** Overrides the built-in "empty prompt and no attachments" check. Ignored while `submitError` is set. */
|
|
@@ -90,6 +103,13 @@ const props = withDefaults(
|
|
|
90
103
|
/** An edit's regions (from `useComposer`'s `edit`), shown as colored chips while editing. */
|
|
91
104
|
regions?: EditRegion[];
|
|
92
105
|
"onUpdate:regions"?: (next: EditRegion[]) => void;
|
|
106
|
+
/**
|
|
107
|
+
* The mic, left of submit: the transcript is added to the prompt, never
|
|
108
|
+
* submitted. `{ model: "fal-ai/wizper" }` goes through your
|
|
109
|
+
* `/api/chai/fal` route; `{ transcribe }` uses any provider. Off by
|
|
110
|
+
* default, with no default model.
|
|
111
|
+
*/
|
|
112
|
+
voiceInput?: VoiceInputConfig;
|
|
93
113
|
disabled?: boolean;
|
|
94
114
|
}>(),
|
|
95
115
|
{
|
|
@@ -99,6 +119,8 @@ const props = withDefaults(
|
|
|
99
119
|
modelIds: () => [],
|
|
100
120
|
modelId: null,
|
|
101
121
|
aspectRatio: null,
|
|
122
|
+
character: "",
|
|
123
|
+
frameRate: null,
|
|
102
124
|
submitError: null,
|
|
103
125
|
// Left undefined unless set: Vue would make an absent boolean `false`,
|
|
104
126
|
// which reads as "never disable".
|
|
@@ -159,64 +181,29 @@ function toggleModel(id: string) {
|
|
|
159
181
|
else props["onUpdate:modelId"]?.(id);
|
|
160
182
|
}
|
|
161
183
|
|
|
162
|
-
//
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
() => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
el.style.height = `${el.scrollHeight}px`;
|
|
171
|
-
},
|
|
172
|
-
{ flush: "post", immediate: true }
|
|
173
|
-
);
|
|
184
|
+
// Which field's Enhance is running: `enhancing` is one flag for the bar.
|
|
185
|
+
const enhancingField = ref<ComposerPromptField>("prompt");
|
|
186
|
+
const enhanceField = (field: ComposerPromptField) =>
|
|
187
|
+
props.onEnhance &&
|
|
188
|
+
((current: string) => {
|
|
189
|
+
enhancingField.value = field;
|
|
190
|
+
props.onEnhance?.(current, field, view.value.enhanceMode);
|
|
191
|
+
});
|
|
174
192
|
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
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);
|
|
193
|
+
// Voice input: a transcript is added to the prompt, never submitted.
|
|
194
|
+
const voice = createVoiceInput(
|
|
195
|
+
() => props.voiceInput ?? { transcribe: async () => "" },
|
|
196
|
+
(text) => {
|
|
197
|
+
const current = props.modelValue;
|
|
198
|
+
emit("update:modelValue", current.trim() ? `${current.trimEnd()} ${text}` : text);
|
|
201
199
|
}
|
|
202
200
|
);
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
}
|
|
201
|
+
const voiceState = shallowRef(voice.getState());
|
|
202
|
+
const stopVoice = voice.subscribe(() => (voiceState.value = voice.getState()));
|
|
203
|
+
onScopeDispose(() => {
|
|
204
|
+
stopVoice();
|
|
205
|
+
voice.dispose();
|
|
206
|
+
});
|
|
220
207
|
|
|
221
208
|
async function onFiles(files: FileList | null) {
|
|
222
209
|
const onAttachments = props["onUpdate:attachments"];
|
|
@@ -268,36 +255,44 @@ function onSubmitClick() {
|
|
|
268
255
|
</li>
|
|
269
256
|
</ul>
|
|
270
257
|
|
|
271
|
-
<
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
:class="['chai-composer__input chai-composer__type', { 'chai-composer__input--revealing': animating }]"
|
|
258
|
+
<VoiceWaveform v-if="voiceInput && voiceState.status === 'recording'" :levels="voiceState.levels" :elapsed-ms="voiceState.elapsedMs" />
|
|
259
|
+
<div v-else-if="view.world" class="chai-composer__world-fields">
|
|
260
|
+
<PromptField
|
|
275
261
|
:value="modelValue"
|
|
276
262
|
:placeholder="view.placeholder"
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
:
|
|
280
|
-
|
|
263
|
+
heading="Environment"
|
|
264
|
+
note="(required)"
|
|
265
|
+
:hint="view.environmentHint"
|
|
266
|
+
:on-enhance="enhanceField('environment')"
|
|
267
|
+
:enhancing="Boolean(enhancing) && enhancingField === 'environment'"
|
|
268
|
+
:disabled="disabled"
|
|
269
|
+
@change="emit('update:modelValue', $event)"
|
|
270
|
+
/>
|
|
271
|
+
<PromptField
|
|
272
|
+
v-if="props['onUpdate:character']"
|
|
273
|
+
:value="character"
|
|
274
|
+
:placeholder="view.characterPlaceholder"
|
|
275
|
+
heading="Character"
|
|
276
|
+
note="(optional)"
|
|
277
|
+
:on-enhance="enhanceField('character')"
|
|
278
|
+
:enhancing="Boolean(enhancing) && enhancingField === 'character'"
|
|
279
|
+
:disabled="disabled"
|
|
280
|
+
@change="props['onUpdate:character']?.($event)"
|
|
281
281
|
/>
|
|
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
282
|
</div>
|
|
283
|
+
<PromptField
|
|
284
|
+
v-else
|
|
285
|
+
:value="modelValue"
|
|
286
|
+
:placeholder="view.placeholder"
|
|
287
|
+
:label="promptLabel"
|
|
288
|
+
:on-enhance="enhanceField('prompt')"
|
|
289
|
+
:enhancing="Boolean(enhancing)"
|
|
290
|
+
:disabled="disabled"
|
|
291
|
+
@change="emit('update:modelValue', $event)"
|
|
292
|
+
/>
|
|
299
293
|
|
|
300
294
|
<p v-if="submitError" class="chai-composer__error" role="alert"><ErrorIcon />{{ submitError }}</p>
|
|
295
|
+
<p v-if="voiceInput && voiceState.status === 'error'" class="chai-composer__error" role="alert"><ErrorIcon />{{ voiceState.error }}</p>
|
|
301
296
|
|
|
302
297
|
<div class="chai-composer__controls">
|
|
303
298
|
<div class="chai-composer__controls-start">
|
|
@@ -350,10 +345,26 @@ function onSubmitClick() {
|
|
|
350
345
|
:disabled="disabled"
|
|
351
346
|
@select="props['onUpdate:aspectRatio']?.($event.id)"
|
|
352
347
|
/>
|
|
348
|
+
|
|
349
|
+
<SearchMenu
|
|
350
|
+
v-if="view.frameRateOptions.length > 0 && props['onUpdate:frameRate']"
|
|
351
|
+
:options="view.frameRateOptions.map((r) => ({ id: String(r), label: `${r}fps` }))"
|
|
352
|
+
:value="view.offeredFrameRate == null ? null : String(view.offeredFrameRate)"
|
|
353
|
+
:trigger-label="view.offeredFrameRate == null ? 'Frame rate' : `${view.offeredFrameRate}fps`"
|
|
354
|
+
menu-label="Frame rate"
|
|
355
|
+
:searchable="false"
|
|
356
|
+
trigger-variant="text"
|
|
357
|
+
:disabled="disabled"
|
|
358
|
+
@select="props['onUpdate:frameRate']?.(Number($event.id))"
|
|
359
|
+
/>
|
|
353
360
|
</div>
|
|
354
361
|
|
|
355
|
-
|
|
362
|
+
<div class="chai-composer__controls-end">
|
|
363
|
+
<VoiceButton v-if="voiceInput" :voice="voice" :state="voiceState" :disabled="disabled" />
|
|
364
|
+
<!-- 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.
|
|
365
|
+
Hidden while a recording is made or transcribed: the transcript is sent once it's in the prompt to check. -->
|
|
356
366
|
<md-fab
|
|
367
|
+
v-if="!(voiceInput && (voiceState.status === 'recording' || voiceState.status === 'transcribing'))"
|
|
357
368
|
class="chai-composer__submit"
|
|
358
369
|
size="small"
|
|
359
370
|
variant="secondary"
|
|
@@ -367,6 +378,7 @@ function onSubmitClick() {
|
|
|
367
378
|
>
|
|
368
379
|
<span slot="icon"><StopIcon v-if="view.showStop" /><RetryIcon v-else-if="submitError" /><ArrowForwardIcon v-else /></span>
|
|
369
380
|
</md-fab>
|
|
381
|
+
</div>
|
|
370
382
|
</div>
|
|
371
383
|
</div>
|
|
372
384
|
</template>
|
|
@@ -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>();
|