@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.
@@ -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>
@@ -0,0 +1,50 @@
1
+ <script setup lang="ts">
2
+ /** The mic: click to record; while recording, a stop button that transcribes; while transcribing, a spinner that cancels. */
3
+ import { computed } from "vue";
4
+ import type { VoiceInput, VoiceInputState } from "@chai-ui/core";
5
+ import { MicIcon, StopIcon } from "../icons.js";
6
+
7
+ const props = defineProps<{ voice: VoiceInput; state: VoiceInputState; disabled?: boolean }>();
8
+
9
+ const label = computed(() =>
10
+ props.state.status === "recording"
11
+ ? "Stop recording and transcribe"
12
+ : props.state.status === "transcribing"
13
+ ? "Cancel transcription"
14
+ : "Start voice input"
15
+ );
16
+
17
+ function onClick() {
18
+ const { status } = props.state;
19
+ if (status === "recording") props.voice.stop();
20
+ else if (status === "transcribing") props.voice.cancel();
21
+ else {
22
+ props.voice.dismiss();
23
+ void props.voice.start();
24
+ }
25
+ }
26
+
27
+ function onKeydown(e: KeyboardEvent) {
28
+ if (e.key === "Escape" && props.state.status === "recording") props.voice.cancel();
29
+ }
30
+ </script>
31
+
32
+ <template>
33
+ <div class="chai-composer__voice" :data-status="state.status">
34
+ <button
35
+ type="button"
36
+ class="chai-composer__voice-button"
37
+ :disabled="disabled"
38
+ :aria-label="label"
39
+ :title="label"
40
+ @click="onClick"
41
+ @keydown="onKeydown"
42
+ >
43
+ <span v-if="state.status === 'transcribing'" class="chai-composer__voice-spinner" aria-hidden="true" /><StopIcon
44
+ v-else-if="state.status === 'recording'"
45
+ /><MicIcon v-else />
46
+ </button>
47
+ <!-- Announces each change of state once, not every tick of the timer. -->
48
+ <span class="chai-visually-hidden" role="status">{{ state.status === "recording" ? "Recording" : state.status === "transcribing" ? "Transcribing" : "" }}</span>
49
+ </div>
50
+ </template>
@@ -0,0 +1,17 @@
1
+ <script setup lang="ts">
2
+ /** What replaces the prompt while recording: recent mic loudness as bars, newest on the right, and the time. Decorative. */
3
+ import { computed } from "vue";
4
+ import { formatVoiceElapsed, VOICE_LEVEL_COUNT } from "@chai-ui/core";
5
+
6
+ const props = defineProps<{ levels: number[]; elapsedMs: number }>();
7
+ const bars = computed(() => [...Array<number>(Math.max(0, VOICE_LEVEL_COUNT - props.levels.length)).fill(0), ...props.levels]);
8
+ </script>
9
+
10
+ <template>
11
+ <div class="chai-composer__waveform" aria-hidden="true">
12
+ <div class="chai-composer__waveform-bars">
13
+ <span v-for="(level, i) in bars" :key="i" class="chai-composer__waveform-bar" :style="{ height: `${Math.max(20, level * 100)}%` }" />
14
+ </div>
15
+ <span class="chai-composer__waveform-time">{{ formatVoiceElapsed(elapsedMs) }}</span>
16
+ </div>
17
+ </template>
package/src/icons.ts CHANGED
@@ -28,6 +28,8 @@ export const VisibilityIcon = icon("0 -960 960 960", [{ "d": "M600.5-379.5Q650-4
28
28
  export const VisibilityOffIcon = icon("0 -960 960 960", [{ "d": "m629-419-44-44q26-71-27-118t-115-24l-44-44q17-11 38-16t43-5q71 0 120.5 49.5T650-500q0 22-5.5 43.5T629-419Zm129 129-40-40q49-36 85.5-80.5T857-500q-50-111-150-175.5T490-740q-42 0-86 8t-69 19l-46-47q35-16 89.5-28T485-800q143 0 261.5 81.5T920-500q-26 64-67 117t-95 93Zm58 226L648-229q-35 14-79 21.5t-89 7.5q-146 0-265-81.5T40-500q20-52 55.5-101.5T182-696L56-822l42-43 757 757-39 44ZM223-654q-37 27-71.5 71T102-500q51 111 153.5 175.5T488-260q33 0 65-4t48-12l-64-64q-11 5-27 7.5t-30 2.5q-70 0-120-49t-50-121q0-15 2.5-30t7.5-27l-97-97Zm305 142Zm-116 58Z", "fill": "currentColor" }]);
29
29
  export const CheckSymbolIcon = icon("0 -960 960 960", [{ "d": "M378-246 154-470l43-43 181 181 384-384 43 43-427 427Z", "fill": "currentColor" }]);
30
30
  export const CloseSymbolIcon = icon("0 -960 960 960", [{ "d": "m249-207-42-42 231-231-231-231 42-42 231 231 231-231 42 42-231 231 231 231-42 42-231-231-231 231Z", "fill": "currentColor" }]);
31
+ /** Material Symbols "mic" — voice input. */
32
+ export const MicIcon = icon("0 -960 960 960", [{ "d": "M408-453.92q-29-30.91-29-75.08v-251q0-41.67 29.44-70.83Q437.88-880 479.94-880t71.56 29.17Q581-821.67 581-780v251q0 44.17-29 75.08Q523-423 480-423t-72-30.92ZM480-651Zm-30 531v-136q-106-11-178-89t-72-184h60q0 91 64.29 153t155.5 62q91.21 0 155.71-62Q700-438 700-529h60q0 106-72 184t-178 89v136h-60Zm59.5-376.5Q521-510 521-529v-251q0-17-11.79-28.5T480-820q-17.42 0-29.21 11.5T439-780v251q0 19 11.5 32.5T480-483q18 0 29.5-13.5Z", "fill": "currentColor" }]);
31
33
  export const StopIcon = icon("0 -960 960 960", [{ "d": "M240-240v-480h480v480H240Z", "fill": "currentColor" }]);
32
34
  export const ThumbUpIcon = icon("0 0 18 18", [{ "d": "M5.25039 15.7502V6.00019L10.5004 0.750193L11.8879 2.13769L10.9129 6.00019H17.2504V9.30019L14.5129 15.7502H5.25039ZM6.75039 14.2502H13.5004L15.7504 9.00019V7.50019H9.00039L10.0129 3.37519L6.75039 6.63769V14.2502ZM1.50039 15.7502V6.00019H5.25039V7.50019H3.00039V14.2502H5.25039V15.7502H1.50039Z", "fill": "currentColor" }], true);
33
35
  export const ThumbDownIcon = icon("0 0 18 18", [{ "d": "M2.24961 12C1.84961 12 1.49961 11.85 1.19961 11.55C0.899615 11.25 0.749615 10.9 0.749615 10.5V9C0.749615 8.9125 0.762115 8.81875 0.787115 8.71875C0.812115 8.61875 0.837115 8.525 0.862115 8.4375L3.11211 3.15C3.22461 2.9 3.41211 2.6875 3.67461 2.5125C3.93711 2.3375 4.21211 2.25 4.49961 2.25H12.7496V12L8.24961 16.4625C8.06211 16.65 7.84024 16.7594 7.58399 16.7906C7.32774 16.8219 7.08086 16.775 6.84336 16.65C6.60586 16.525 6.43086 16.35 6.31836 16.125C6.20586 15.9 6.18086 15.6688 6.24336 15.4313L7.08711 12H2.24961ZM11.2496 11.3625V3.75H4.49961L2.24961 9V10.5H8.99961L7.98711 14.625L11.2496 11.3625ZM14.9996 2.25C15.4121 2.25 15.7652 2.39688 16.059 2.69063C16.3527 2.98438 16.4996 3.3375 16.4996 3.75V10.5C16.4996 10.9125 16.3527 11.2656 16.059 11.5594C15.7652 11.8531 15.4121 12 14.9996 12H12.7496V10.5H14.9996V3.75H12.7496V2.25H14.9996Z", "fill": "currentColor" }], true);
@@ -50,6 +52,8 @@ export const OptimizeIcon = icon("0 0 25 24", [{ "d": "M5 19H6.425L16.2 9.225L14
50
52
  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
53
  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
54
  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" }]);
55
+ /** Material Symbols "globe" — Create world. */
56
+ 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
57
  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
58
  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
59
  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
@@ -16,6 +16,7 @@ export * from "./useMediaAnalyzer.js";
16
16
  // to import from @chai-ui/vue.
17
17
  export {
18
18
  createFalEngine,
19
+ createFalTranscriber,
19
20
  createOpenRouterEngine,
20
21
  createMockEngine,
21
22
  mockEngine,
@@ -46,6 +47,7 @@ export {
46
47
  } from "@chai-ui/core";
47
48
  export type {
48
49
  ComposerAttachMenuAction,
50
+ ComposerPromptField,
49
51
  ComposerAttachment,
50
52
  ComposerSelection,
51
53
  ComposerSubmitPayload,
@@ -62,6 +64,10 @@ export type {
62
64
  MediaAnalyzerSubmitPayload,
63
65
  MediaKind,
64
66
  ModelOption,
67
+ OutputKind,
68
+ Transcribe,
69
+ VoiceClip,
70
+ VoiceInputConfig,
65
71
  ParameterOption,
66
72
  Reasoning,
67
73
  RegionBox,