@stellartech/voice-widget-directus 1.0.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.
Files changed (2) hide show
  1. package/dist/index.js +1702 -0
  2. package/package.json +50 -0
package/dist/index.js ADDED
@@ -0,0 +1,1702 @@
1
+ import { useApi, defineInterface } from '@directus/extensions-sdk';
2
+ import { defineComponent, ref, computed, watch, onUnmounted, resolveComponent, openBlock, createElementBlock, normalizeClass, createVNode, createElementVNode, toDisplayString, normalizeStyle, withDirectives, vModelText, createCommentVNode, renderSlot, withModifiers, createBlock, createTextVNode, Fragment, renderList, onMounted, vShow, unref, vModelCheckbox } from 'vue';
3
+
4
+ const _hoisted_1$3 = {
5
+ key: 0,
6
+ class: "audio-player__loading"
7
+ };
8
+ const _hoisted_2$3 = {
9
+ key: 1,
10
+ class: "audio-player__error"
11
+ };
12
+ const _hoisted_3$3 = {
13
+ key: 2,
14
+ class: "audio-player__controls"
15
+ };
16
+ const _hoisted_4$3 = ["src"];
17
+ const _hoisted_5$3 = ["disabled"];
18
+ const _hoisted_6$3 = { class: "audio-player__progress-track" };
19
+ const _hoisted_7$2 = { class: "audio-player__time" };
20
+ const _hoisted_8$2 = {
21
+ key: 0,
22
+ class: "audio-player__volume"
23
+ };
24
+ const _hoisted_9$2 = {
25
+ key: 3,
26
+ class: "audio-player__empty"
27
+ };
28
+ var _sfc_main$3 = /* @__PURE__ */ defineComponent({
29
+ __name: "AudioPlayer",
30
+ props: {
31
+ src: { default: null },
32
+ loading: { type: Boolean, default: false },
33
+ loadingText: { default: "Loading..." },
34
+ size: { default: "medium" }
35
+ },
36
+ setup(__props) {
37
+ const props = __props;
38
+ const audioRef = ref(null);
39
+ const isPlaying = ref(false);
40
+ const isLoaded = ref(false);
41
+ const currentTime = ref(0);
42
+ const duration = ref(0);
43
+ const volume = ref(1);
44
+ const isMuted = ref(false);
45
+ const error = ref(null);
46
+ const progressPercent = computed(() => {
47
+ if (duration.value === 0) return 0;
48
+ return currentTime.value / duration.value * 100;
49
+ });
50
+ const formattedTime = computed(() => {
51
+ const current = formatTime(currentTime.value);
52
+ const total = formatTime(duration.value);
53
+ return `${current} / ${total}`;
54
+ });
55
+ function formatTime(seconds) {
56
+ if (!seconds || isNaN(seconds)) return "0:00";
57
+ const mins = Math.floor(seconds / 60);
58
+ const secs = Math.floor(seconds % 60);
59
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
60
+ }
61
+ function togglePlay() {
62
+ if (!audioRef.value) return;
63
+ if (isPlaying.value) {
64
+ audioRef.value.pause();
65
+ } else {
66
+ audioRef.value.play().catch((e) => {
67
+ console.error("Play failed:", e);
68
+ error.value = "Failed to play audio";
69
+ });
70
+ }
71
+ isPlaying.value = !isPlaying.value;
72
+ }
73
+ function toggleMute() {
74
+ if (!audioRef.value) return;
75
+ isMuted.value = !isMuted.value;
76
+ audioRef.value.muted = isMuted.value;
77
+ }
78
+ function onVolumeChange() {
79
+ if (!audioRef.value) return;
80
+ audioRef.value.volume = volume.value;
81
+ isMuted.value = volume.value === 0;
82
+ }
83
+ function seek(event) {
84
+ if (!audioRef.value || duration.value === 0) return;
85
+ const target = event.currentTarget;
86
+ const rect = target.getBoundingClientRect();
87
+ const x = event.clientX - rect.left;
88
+ const percent = x / rect.width;
89
+ audioRef.value.currentTime = percent * duration.value;
90
+ }
91
+ function onLoadedMetadata() {
92
+ if (!audioRef.value) return;
93
+ duration.value = audioRef.value.duration;
94
+ isLoaded.value = true;
95
+ error.value = null;
96
+ }
97
+ function onTimeUpdate() {
98
+ if (!audioRef.value) return;
99
+ currentTime.value = audioRef.value.currentTime;
100
+ }
101
+ function onEnded() {
102
+ isPlaying.value = false;
103
+ currentTime.value = 0;
104
+ }
105
+ function onError() {
106
+ error.value = "Failed to load audio";
107
+ isLoaded.value = false;
108
+ }
109
+ watch(() => props.src, (newSrc) => {
110
+ isPlaying.value = false;
111
+ isLoaded.value = false;
112
+ currentTime.value = 0;
113
+ duration.value = 0;
114
+ error.value = null;
115
+ if (audioRef.value && newSrc) {
116
+ audioRef.value.load();
117
+ }
118
+ });
119
+ onUnmounted(() => {
120
+ if (audioRef.value) {
121
+ audioRef.value.pause();
122
+ }
123
+ });
124
+ return (_ctx, _cache) => {
125
+ const _component_v_icon = resolveComponent("v-icon");
126
+ return openBlock(), createElementBlock(
127
+ "div",
128
+ {
129
+ class: normalizeClass(["audio-player", [`audio-player--${__props.size}`, { "audio-player--loading": __props.loading }]])
130
+ },
131
+ [
132
+ __props.loading ? (openBlock(), createElementBlock("div", _hoisted_1$3, [
133
+ createVNode(_component_v_icon, {
134
+ name: "refresh",
135
+ class: "spinning",
136
+ small: ""
137
+ }),
138
+ createElementVNode(
139
+ "span",
140
+ null,
141
+ toDisplayString(__props.loadingText),
142
+ 1
143
+ /* TEXT */
144
+ )
145
+ ])) : error.value ? (openBlock(), createElementBlock("div", _hoisted_2$3, [
146
+ createVNode(_component_v_icon, {
147
+ name: "error",
148
+ small: ""
149
+ }),
150
+ createElementVNode(
151
+ "span",
152
+ null,
153
+ toDisplayString(error.value),
154
+ 1
155
+ /* TEXT */
156
+ )
157
+ ])) : __props.src ? (openBlock(), createElementBlock("div", _hoisted_3$3, [
158
+ (openBlock(), createElementBlock("audio", {
159
+ ref_key: "audioRef",
160
+ ref: audioRef,
161
+ key: __props.src,
162
+ src: __props.src,
163
+ preload: "metadata",
164
+ onLoadedmetadata: onLoadedMetadata,
165
+ onError,
166
+ onTimeupdate: onTimeUpdate,
167
+ onEnded
168
+ }, null, 40, _hoisted_4$3)),
169
+ createElementVNode("button", {
170
+ class: "audio-player__play-btn",
171
+ onClick: togglePlay,
172
+ disabled: !isLoaded.value
173
+ }, [
174
+ createVNode(_component_v_icon, {
175
+ name: isPlaying.value ? "pause" : "play_arrow"
176
+ }, null, 8, ["name"])
177
+ ], 8, _hoisted_5$3),
178
+ createElementVNode("div", {
179
+ class: "audio-player__progress",
180
+ onClick: seek
181
+ }, [
182
+ createElementVNode("div", _hoisted_6$3, [
183
+ createElementVNode(
184
+ "div",
185
+ {
186
+ class: "audio-player__progress-fill",
187
+ style: normalizeStyle({ width: progressPercent.value + "%" })
188
+ },
189
+ null,
190
+ 4
191
+ /* STYLE */
192
+ )
193
+ ])
194
+ ]),
195
+ createElementVNode(
196
+ "span",
197
+ _hoisted_7$2,
198
+ toDisplayString(formattedTime.value),
199
+ 1
200
+ /* TEXT */
201
+ ),
202
+ __props.size === "large" ? (openBlock(), createElementBlock("div", _hoisted_8$2, [
203
+ createElementVNode("button", {
204
+ class: "audio-player__volume-btn",
205
+ onClick: toggleMute
206
+ }, [
207
+ createVNode(_component_v_icon, {
208
+ name: isMuted.value ? "volume_off" : "volume_up",
209
+ small: ""
210
+ }, null, 8, ["name"])
211
+ ]),
212
+ withDirectives(createElementVNode(
213
+ "input",
214
+ {
215
+ type: "range",
216
+ class: "audio-player__volume-slider",
217
+ min: "0",
218
+ max: "1",
219
+ step: "0.1",
220
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => volume.value = $event),
221
+ onInput: onVolumeChange
222
+ },
223
+ null,
224
+ 544
225
+ /* NEED_HYDRATION, NEED_PATCH */
226
+ ), [
227
+ [vModelText, volume.value]
228
+ ])
229
+ ])) : createCommentVNode("v-if", true)
230
+ ])) : (openBlock(), createElementBlock("div", _hoisted_9$2, [
231
+ renderSlot(_ctx.$slots, "empty", {}, () => [
232
+ createVNode(_component_v_icon, {
233
+ name: "music_off",
234
+ small: ""
235
+ }),
236
+ _cache[1] || (_cache[1] = createElementVNode(
237
+ "span",
238
+ null,
239
+ "No audio",
240
+ -1
241
+ /* CACHED */
242
+ ))
243
+ ], true)
244
+ ]))
245
+ ],
246
+ 2
247
+ /* CLASS */
248
+ );
249
+ };
250
+ }
251
+ });
252
+
253
+ var e=[],t=[];function n(n,r){if(n&&"undefined"!=typeof document){var a,s=!0===r.prepend?"prepend":"append",d=!0===r.singleTag,i="string"==typeof r.container?document.querySelector(r.container):document.getElementsByTagName("head")[0];if(d){var u=e.indexOf(i);-1===u&&(u=e.push(i)-1,t[u]={}),a=t[u]&&t[u][s]?t[u][s]:t[u][s]=c();}else a=c();65279===n.charCodeAt(0)&&(n=n.substring(1)),a.styleSheet?a.styleSheet.cssText+=n:a.appendChild(document.createTextNode(n));}function c(){var e=document.createElement("style");if(e.setAttribute("type","text/css"),r.attributes)for(var t=Object.keys(r.attributes),n=0;n<t.length;n++)e.setAttribute(t[n],r.attributes[t[n]]);var a="prepend"===s?"afterbegin":"beforeend";return i.insertAdjacentElement(a,e),e}}
254
+
255
+ var css$3 = "\n.audio-player[data-v-9d56517d] {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n border: 1px solid var(--theme--border-color-subdued);\n}\n.audio-player--small[data-v-9d56517d] {\n padding: 4px 8px;\n gap: 6px;\n}\n.audio-player--large[data-v-9d56517d] {\n padding: 12px 16px;\n gap: 12px;\n}\n.audio-player__loading[data-v-9d56517d],\n.audio-player__error[data-v-9d56517d],\n.audio-player__empty[data-v-9d56517d] {\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--theme--foreground-subdued);\n font-size: 13px;\n width: 100%;\n}\n.audio-player__error[data-v-9d56517d] {\n color: var(--theme--danger);\n}\n.audio-player__controls[data-v-9d56517d] {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n}\n.audio-player__play-btn[data-v-9d56517d] {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 50%;\n background: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n cursor: pointer;\n transition: all 0.15s ease;\n flex-shrink: 0;\n}\n.audio-player--small .audio-player__play-btn[data-v-9d56517d] {\n width: 24px;\n height: 24px;\n}\n.audio-player--large .audio-player__play-btn[data-v-9d56517d] {\n width: 40px;\n height: 40px;\n}\n.audio-player__play-btn[data-v-9d56517d]:hover:not(:disabled) {\n background: var(--theme--primary-accent);\n transform: scale(1.05);\n}\n.audio-player__play-btn[data-v-9d56517d]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.audio-player__progress[data-v-9d56517d] {\n flex: 1;\n height: 24px;\n display: flex;\n align-items: center;\n cursor: pointer;\n}\n.audio-player__progress-track[data-v-9d56517d] {\n width: 100%;\n height: 4px;\n background: var(--theme--border-color);\n border-radius: 2px;\n overflow: hidden;\n}\n.audio-player--large .audio-player__progress-track[data-v-9d56517d] {\n height: 6px;\n border-radius: 3px;\n}\n.audio-player__progress-fill[data-v-9d56517d] {\n height: 100%;\n background: var(--theme--primary);\n border-radius: inherit;\n transition: width 0.1s linear;\n}\n.audio-player__time[data-v-9d56517d] {\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n white-space: nowrap;\n min-width: 70px;\n text-align: right;\n}\n.audio-player--small .audio-player__time[data-v-9d56517d] {\n font-size: 11px;\n min-width: 60px;\n}\n.audio-player__volume[data-v-9d56517d] {\n display: flex;\n align-items: center;\n gap: 4px;\n margin-left: 8px;\n}\n.audio-player__volume-btn[data-v-9d56517d] {\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: var(--theme--foreground-subdued);\n border-radius: 4px;\n}\n.audio-player__volume-btn[data-v-9d56517d]:hover {\n background: var(--theme--background-accent);\n color: var(--theme--foreground);\n}\n.audio-player__volume-slider[data-v-9d56517d] {\n width: 60px;\n height: 4px;\n cursor: pointer;\n accent-color: var(--theme--primary);\n}\n\n/* Spinning animation */\n.spinning[data-v-9d56517d] {\n animation: spin-9d56517d 1s linear infinite;\n}\n@keyframes spin-9d56517d {\nfrom { transform: rotate(0deg);\n}\nto { transform: rotate(360deg);\n}\n}\n";
256
+ n(css$3,{});
257
+
258
+ var _export_sfc = (sfc, props) => {
259
+ const target = sfc.__vccOpts || sfc;
260
+ for (const [key, val] of props) {
261
+ target[key] = val;
262
+ }
263
+ return target;
264
+ };
265
+
266
+ var AudioPlayer = /* @__PURE__ */ _export_sfc(_sfc_main$3, [["__scopeId", "data-v-9d56517d"], ["__file", "AudioPlayer.vue"]]);
267
+
268
+ const _hoisted_1$2 = { class: "voice-card__info" };
269
+ const _hoisted_2$2 = { class: "voice-card__radio" };
270
+ const _hoisted_3$2 = ["checked", "name"];
271
+ const _hoisted_4$2 = { class: "voice-card__text" };
272
+ const _hoisted_5$2 = { class: "voice-card__name" };
273
+ const _hoisted_6$2 = { class: "voice-card__description" };
274
+ const _hoisted_7$1 = { class: "voice-card__player" };
275
+ const _hoisted_8$1 = {
276
+ key: 0,
277
+ class: "voice-card__generating"
278
+ };
279
+ const _hoisted_9$1 = ["disabled"];
280
+ var _sfc_main$2 = /* @__PURE__ */ defineComponent({
281
+ __name: "VoiceCard",
282
+ props: {
283
+ voice: {},
284
+ isSelected: { type: Boolean },
285
+ loading: { type: Boolean },
286
+ sampleUrl: {},
287
+ provider: {}
288
+ },
289
+ emits: ["select", "generate-voice"],
290
+ setup(__props) {
291
+ return (_ctx, _cache) => {
292
+ const _component_v_icon = resolveComponent("v-icon");
293
+ return openBlock(), createElementBlock(
294
+ "div",
295
+ {
296
+ class: normalizeClass(["voice-card", { "voice-card--selected": __props.isSelected, "voice-card--loading": __props.loading }]),
297
+ onClick: _cache[3] || (_cache[3] = ($event) => _ctx.$emit("select", __props.voice.id))
298
+ },
299
+ [
300
+ createElementVNode("div", _hoisted_1$2, [
301
+ createElementVNode("div", _hoisted_2$2, [
302
+ createElementVNode("input", {
303
+ type: "radio",
304
+ checked: __props.isSelected,
305
+ name: `voice-select-${__props.provider}`,
306
+ onClick: _cache[0] || (_cache[0] = withModifiers(() => {
307
+ }, ["stop"])),
308
+ onChange: _cache[1] || (_cache[1] = ($event) => _ctx.$emit("select", __props.voice.id))
309
+ }, null, 40, _hoisted_3$2)
310
+ ]),
311
+ createElementVNode("div", _hoisted_4$2, [
312
+ createElementVNode(
313
+ "span",
314
+ _hoisted_5$2,
315
+ toDisplayString(__props.voice.name),
316
+ 1
317
+ /* TEXT */
318
+ ),
319
+ createElementVNode(
320
+ "span",
321
+ _hoisted_6$2,
322
+ toDisplayString(__props.voice.description),
323
+ 1
324
+ /* TEXT */
325
+ )
326
+ ])
327
+ ]),
328
+ createElementVNode("div", _hoisted_7$1, [
329
+ __props.loading ? (openBlock(), createElementBlock("div", _hoisted_8$1, [
330
+ createVNode(_component_v_icon, {
331
+ name: "refresh",
332
+ class: "spinning",
333
+ small: ""
334
+ }),
335
+ _cache[4] || (_cache[4] = createElementVNode(
336
+ "span",
337
+ null,
338
+ "Generating...",
339
+ -1
340
+ /* CACHED */
341
+ ))
342
+ ])) : __props.sampleUrl ? (openBlock(), createBlock(AudioPlayer, {
343
+ key: 1,
344
+ src: __props.sampleUrl,
345
+ size: "small"
346
+ }, null, 8, ["src"])) : (openBlock(), createElementBlock("button", {
347
+ key: 2,
348
+ type: "button",
349
+ class: "voice-card__generate-btn",
350
+ onClick: _cache[2] || (_cache[2] = withModifiers(($event) => _ctx.$emit("generate-voice", __props.voice.id), ["stop", "prevent"])),
351
+ disabled: __props.loading
352
+ }, [
353
+ createVNode(_component_v_icon, {
354
+ name: "play_circle",
355
+ small: ""
356
+ }),
357
+ _cache[5] || (_cache[5] = createTextVNode(
358
+ " Generate Voice ",
359
+ -1
360
+ /* CACHED */
361
+ ))
362
+ ], 8, _hoisted_9$1))
363
+ ])
364
+ ],
365
+ 2
366
+ /* CLASS */
367
+ );
368
+ };
369
+ }
370
+ });
371
+
372
+ var css$2 = "\n.voice-card[data-v-51556c63] {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 16px;\n padding: 12px 16px;\n border: 2px solid var(--theme--border-color-subdued);\n border-radius: var(--theme--border-radius);\n background: var(--theme--background);\n cursor: pointer;\n transition: all 0.15s ease;\n width: 100%;\n}\n.voice-card[data-v-51556c63]:hover {\n border-color: var(--theme--primary);\n background: var(--theme--background-subdued);\n}\n.voice-card--selected[data-v-51556c63] {\n border-color: var(--theme--primary);\n background: color-mix(in srgb, var(--theme--primary) 5%, var(--theme--background));\n}\n.voice-card--loading[data-v-51556c63] {\n pointer-events: none;\n opacity: 0.8;\n}\n.voice-card__info[data-v-51556c63] {\n display: flex;\n align-items: center;\n gap: 12px;\n flex: 0 0 200px;\n min-width: 0;\n}\n.voice-card__radio[data-v-51556c63] {\n flex-shrink: 0;\n}\n.voice-card__radio input[data-v-51556c63] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n accent-color: var(--theme--primary);\n}\n.voice-card__text[data-v-51556c63] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n flex: 1;\n}\n.voice-card__name[data-v-51556c63] {\n font-size: 14px;\n font-weight: 600;\n color: var(--theme--foreground);\n}\n.voice-card__description[data-v-51556c63] {\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.voice-card__player[data-v-51556c63] {\n flex: 1;\n min-height: 36px;\n display: flex;\n align-items: center;\n}\n.voice-card__generating[data-v-51556c63] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 12px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n}\n.voice-card__generate-btn[data-v-51556c63] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 6px;\n padding: 8px 16px;\n border: 1px dashed var(--theme--border-color);\n border-radius: var(--theme--border-radius);\n background: transparent;\n color: var(--theme--foreground-subdued);\n font-size: 12px;\n cursor: pointer;\n transition: all 0.15s ease;\n white-space: nowrap;\n}\n.voice-card__generate-btn[data-v-51556c63]:hover:not(:disabled) {\n border-color: var(--theme--primary);\n color: var(--theme--primary);\n background: var(--theme--background-subdued);\n}\n.voice-card__generate-btn[data-v-51556c63]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.spinning[data-v-51556c63] {\n animation: spin-51556c63 1s linear infinite;\n}\n@keyframes spin-51556c63 {\nfrom { transform: rotate(0deg);\n}\nto { transform: rotate(360deg);\n}\n}\n";
373
+ n(css$2,{});
374
+
375
+ var VoiceCard = /* @__PURE__ */ _export_sfc(_sfc_main$2, [["__scopeId", "data-v-51556c63"], ["__file", "VoiceCard.vue"]]);
376
+
377
+ const _hoisted_1$1 = { class: "tone-style-selector" };
378
+ const _hoisted_2$1 = { class: "tone-style-selector__options" };
379
+ const _hoisted_3$1 = ["onClick"];
380
+ const _hoisted_4$1 = { class: "tone-style-selector__option-name" };
381
+ const _hoisted_5$1 = {
382
+ key: 0,
383
+ class: "tone-style-selector__custom"
384
+ };
385
+ const _hoisted_6$1 = ["value", "placeholder"];
386
+ var _sfc_main$1 = /* @__PURE__ */ defineComponent({
387
+ __name: "ToneStyleSelector",
388
+ props: {
389
+ items: {},
390
+ selectedId: {},
391
+ customValue: {},
392
+ placeholder: {}
393
+ },
394
+ emits: ["select", "custom-change"],
395
+ setup(__props) {
396
+ return (_ctx, _cache) => {
397
+ const _component_v_icon = resolveComponent("v-icon");
398
+ return openBlock(), createElementBlock("div", _hoisted_1$1, [
399
+ createElementVNode("div", _hoisted_2$1, [
400
+ (openBlock(true), createElementBlock(
401
+ Fragment,
402
+ null,
403
+ renderList(__props.items, (item) => {
404
+ return openBlock(), createElementBlock("button", {
405
+ key: item.id,
406
+ class: normalizeClass(["tone-style-selector__option", { "tone-style-selector__option--selected": __props.selectedId === item.id }]),
407
+ onClick: ($event) => _ctx.$emit("select", item.id)
408
+ }, [
409
+ createElementVNode(
410
+ "span",
411
+ _hoisted_4$1,
412
+ toDisplayString(item.name),
413
+ 1
414
+ /* TEXT */
415
+ ),
416
+ __props.selectedId === item.id ? (openBlock(), createBlock(_component_v_icon, {
417
+ key: 0,
418
+ name: "check",
419
+ small: ""
420
+ })) : createCommentVNode("v-if", true)
421
+ ], 10, _hoisted_3$1);
422
+ }),
423
+ 128
424
+ /* KEYED_FRAGMENT */
425
+ ))
426
+ ]),
427
+ __props.selectedId === "other" ? (openBlock(), createElementBlock("div", _hoisted_5$1, [
428
+ createElementVNode("input", {
429
+ type: "text",
430
+ class: "tone-style-selector__custom-input",
431
+ value: __props.customValue,
432
+ placeholder: __props.placeholder,
433
+ onInput: _cache[0] || (_cache[0] = ($event) => _ctx.$emit("custom-change", $event.target.value))
434
+ }, null, 40, _hoisted_6$1),
435
+ _cache[1] || (_cache[1] = createElementVNode(
436
+ "p",
437
+ { class: "tone-style-selector__custom-hint" },
438
+ " Enter a custom description for the voice ",
439
+ -1
440
+ /* CACHED */
441
+ ))
442
+ ])) : createCommentVNode("v-if", true)
443
+ ]);
444
+ };
445
+ }
446
+ });
447
+
448
+ var css$1 = "\n.tone-style-selector[data-v-992c7031] {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n.tone-style-selector__options[data-v-992c7031] {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n.tone-style-selector__option[data-v-992c7031] {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 8px 14px;\n border: 1px solid var(--theme--border-color-subdued);\n border-radius: 20px;\n background: var(--theme--background);\n color: var(--theme--foreground);\n font-size: 13px;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.tone-style-selector__option[data-v-992c7031]:hover {\n border-color: var(--theme--primary);\n background: var(--theme--background-subdued);\n}\n.tone-style-selector__option--selected[data-v-992c7031] {\n border-color: var(--theme--primary);\n background: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.tone-style-selector__option--selected[data-v-992c7031]:hover {\n background: var(--theme--primary-accent);\n}\n.tone-style-selector__option-name[data-v-992c7031] {\n white-space: nowrap;\n}\n.tone-style-selector__custom[data-v-992c7031] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n}\n.tone-style-selector__custom-input[data-v-992c7031] {\n width: 100%;\n padding: 10px 12px;\n border: 1px solid var(--theme--form--field--input--border-color);\n border-radius: var(--theme--border-radius);\n background: var(--theme--form--field--input--background);\n color: var(--theme--foreground);\n font-size: 14px;\n}\n.tone-style-selector__custom-input[data-v-992c7031]:focus {\n outline: none;\n border-color: var(--theme--primary);\n box-shadow: 0 0 0 2px color-mix(in srgb, var(--theme--primary) 20%, transparent);\n}\n.tone-style-selector__custom-input[data-v-992c7031]::placeholder {\n color: var(--theme--foreground-subdued);\n}\n.tone-style-selector__custom-hint[data-v-992c7031] {\n margin: 8px 0 0 0;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n";
449
+ n(css$1,{});
450
+
451
+ var ToneStyleSelector = /* @__PURE__ */ _export_sfc(_sfc_main$1, [["__scopeId", "data-v-992c7031"], ["__file", "ToneStyleSelector.vue"]]);
452
+
453
+ const VOICE_MODELS = [
454
+ { id: "gemini", name: "Gemini" },
455
+ { id: "elevenlabs", name: "ElevenLabs" }
456
+ ];
457
+ const SAMPLE_TEXT = "Hello! This is a sample of my voice. I hope you enjoy listening to how I sound.";
458
+ const DEFAULT_FLOW_ID = "fada8e65-5ce2-477d-b5db-bf7f64a23ffa";
459
+ function useVoicingApi(api) {
460
+ async function fetchVoices(collection = "Voices") {
461
+ try {
462
+ const response = await api.get(`/items/${collection}`, {
463
+ params: {
464
+ filter: { status: { _eq: "published" } },
465
+ sort: ["sort", "name"],
466
+ fields: ["id", "name", "voice_id", "provider", "description", "sample_url", "sample_file", "sort"]
467
+ }
468
+ });
469
+ return response.data.data || [];
470
+ } catch (error) {
471
+ console.error("Failed to fetch voices:", error);
472
+ return [];
473
+ }
474
+ }
475
+ async function fetchFlowId() {
476
+ try {
477
+ const response = await api.get("/items/WidgetConfig", {
478
+ params: { fields: ["voice_flow_id"] }
479
+ });
480
+ return response.data.data?.voice_flow_id || DEFAULT_FLOW_ID;
481
+ } catch {
482
+ return DEFAULT_FLOW_ID;
483
+ }
484
+ }
485
+ async function fetchTones(collection = "VoiceTones") {
486
+ try {
487
+ const response = await api.get(`/items/${collection}`, {
488
+ params: {
489
+ filter: { status: { _eq: "published" } },
490
+ sort: ["sort", "name"],
491
+ fields: ["id", "name", "prompt", "sort", "is_custom"]
492
+ }
493
+ });
494
+ const tones = response.data.data || [];
495
+ tones.push({
496
+ id: "other",
497
+ name: "Other (Custom)",
498
+ prompt: "",
499
+ sort: 9999,
500
+ is_custom: true
501
+ });
502
+ return tones;
503
+ } catch (error) {
504
+ console.error("Failed to fetch tones:", error);
505
+ return [
506
+ { id: "teacher", name: "Teacher", prompt: "Speak in a clear, educational, and patient manner", sort: 1, is_custom: false },
507
+ { id: "storyteller", name: "Storyteller", prompt: "Speak in an engaging, narrative style with varied pacing", sort: 2, is_custom: false },
508
+ { id: "podcaster", name: "Podcaster", prompt: "Speak in a conversational, informal yet professional tone", sort: 3, is_custom: false },
509
+ { id: "other", name: "Other (Custom)", prompt: "", sort: 9999, is_custom: true }
510
+ ];
511
+ }
512
+ }
513
+ async function fetchStyles(collection = "VoiceStyles") {
514
+ try {
515
+ const response = await api.get(`/items/${collection}`, {
516
+ params: {
517
+ filter: { status: { _eq: "published" } },
518
+ sort: ["sort", "name"],
519
+ fields: ["id", "name", "prompt", "sort", "is_custom"]
520
+ }
521
+ });
522
+ const styles = response.data.data || [];
523
+ styles.push({
524
+ id: "other",
525
+ name: "Other (Custom)",
526
+ prompt: "",
527
+ sort: 9999,
528
+ is_custom: true
529
+ });
530
+ return styles;
531
+ } catch (error) {
532
+ console.error("Failed to fetch styles:", error);
533
+ return [
534
+ { id: "happy", name: "Happy", prompt: "enthusiastic and cheerful", sort: 1, is_custom: false },
535
+ { id: "chill", name: "Chill", prompt: "relaxed and calm", sort: 2, is_custom: false },
536
+ { id: "excited", name: "Excited", prompt: "energetic and animated", sort: 3, is_custom: false },
537
+ { id: "other", name: "Other (Custom)", prompt: "", sort: 9999, is_custom: true }
538
+ ];
539
+ }
540
+ }
541
+ async function generateVoiceSample(voiceId, provider, flowId = DEFAULT_FLOW_ID) {
542
+ const effectiveFlowId = flowId && flowId.trim() ? flowId.trim() : DEFAULT_FLOW_ID;
543
+ if (!effectiveFlowId) {
544
+ throw new Error("Voice flow ID is not configured. Set it in Widget Config or in the Flow ID field.");
545
+ }
546
+ const voicingPayload = {
547
+ texts: [SAMPLE_TEXT],
548
+ provider,
549
+ preprocessing: false,
550
+ title: `Voice Sample - ${voiceId}`
551
+ };
552
+ if (provider === "gemini") {
553
+ voicingPayload.speakers = [{ voice: voiceId, name: "Sample", style: "neutral" }];
554
+ voicingPayload.audio_model = "gemini-2.5-pro-tts";
555
+ voicingPayload.text_model = "gemini-2.5-flash";
556
+ } else {
557
+ voicingPayload.voice_id = voiceId;
558
+ }
559
+ try {
560
+ const response = await api.post(`/flows/trigger/${effectiveFlowId}`, voicingPayload);
561
+ const flowResponse = response.data;
562
+ const audioFileId = flowResponse?.data?.audio_file_id || flowResponse?.audio_file_id || null;
563
+ if (audioFileId) {
564
+ const url = await resolveAudioFileUrl(audioFileId);
565
+ return { url, audioFileId };
566
+ }
567
+ throw new Error("No audio file returned from voice generation");
568
+ } catch (e) {
569
+ console.error("Failed to generate voice sample:", e);
570
+ throw new Error(e.response?.data?.detail || e.message || "Failed to generate sample");
571
+ }
572
+ }
573
+ async function generateFullVoiceover(request) {
574
+ const rawFlowId = request.flowId?.trim() || "";
575
+ const effectiveFlowId = rawFlowId ? rawFlowId : DEFAULT_FLOW_ID;
576
+ if (!effectiveFlowId) {
577
+ throw new Error("Voice flow ID is not configured. Set it in Widget Config or in the Flow ID field.");
578
+ }
579
+ let texts = [];
580
+ let lessonTitle = `Lesson ${request.lessonId}`;
581
+ try {
582
+ const lessonResponse = await api.get(`/items/SM_Lessons/${request.lessonId}`, {
583
+ params: { fields: ["text", "title"] }
584
+ });
585
+ const textContent = lessonResponse.data.data?.text || "";
586
+ lessonTitle = lessonResponse.data.data?.title || lessonTitle;
587
+ if (!textContent.trim()) {
588
+ throw new Error("No text content available for voiceover generation");
589
+ }
590
+ texts = textContent.split(/\n\n+/).filter((t) => t.trim().length > 0);
591
+ if (texts.length === 0) {
592
+ texts = [textContent];
593
+ }
594
+ console.log(`[Voice Widget] Generating voiceover for "${lessonTitle}" with ${texts.length} text segments`);
595
+ } catch (e) {
596
+ console.error("Failed to fetch lesson text content:", e);
597
+ throw new Error(e.message || "Failed to fetch lesson text content");
598
+ }
599
+ const voicingPayload = {
600
+ texts,
601
+ provider: request.provider,
602
+ preprocessing: request.preprocessing,
603
+ title: `Voiceover - ${lessonTitle}`
604
+ };
605
+ if (request.provider === "gemini") {
606
+ voicingPayload.speakers = [{
607
+ voice: request.voiceId,
608
+ name: "Narrator",
609
+ style: request.style
610
+ }];
611
+ voicingPayload.audio_model = "gemini-2.5-pro-tts";
612
+ voicingPayload.text_model = "gemini-2.5-flash";
613
+ } else {
614
+ voicingPayload.voice_id = request.voiceId;
615
+ }
616
+ try {
617
+ const response = await api.post(`/flows/trigger/${effectiveFlowId}`, voicingPayload);
618
+ const flowResponse = response.data;
619
+ const data = flowResponse?.data || flowResponse;
620
+ let fileUuid = null;
621
+ if (data?.audio_file_id) {
622
+ try {
623
+ const audioFilesResponse = await api.get(`/items/AudioFiles/${data.audio_file_id}`, {
624
+ params: { fields: ["file"] }
625
+ });
626
+ fileUuid = audioFilesResponse.data.data?.file || null;
627
+ } catch (e) {
628
+ console.warn("[Voice Widget] Could not resolve file UUID:", e);
629
+ }
630
+ if (fileUuid) {
631
+ try {
632
+ await api.post("/items/VoiceVariants", {
633
+ lesson_id: request.lessonId,
634
+ audio_file_id: fileUuid,
635
+ voice_config: {
636
+ provider: request.provider,
637
+ voice_id: request.voiceId,
638
+ style: request.style,
639
+ preprocessing: request.preprocessing,
640
+ tone_id: request.toneId,
641
+ style_id: request.styleId
642
+ },
643
+ callback_data: data.callback_data || null,
644
+ status: "published"
645
+ });
646
+ console.log("[Voice Widget] Saved voice variant with file UUID:", fileUuid);
647
+ } catch (saveError) {
648
+ console.warn("Failed to save voice variant:", saveError);
649
+ }
650
+ }
651
+ }
652
+ return {
653
+ audio_file_id: fileUuid || data?.audio_file_id || null,
654
+ status: data?.status || null,
655
+ callback_data: data?.callback_data || null,
656
+ generation_time: data?.generation_time || null
657
+ };
658
+ } catch (e) {
659
+ console.error("Failed to generate voiceover:", e);
660
+ throw new Error(e.response?.data?.detail || e.message || "Failed to generate voiceover");
661
+ }
662
+ }
663
+ function getAudioUrl(fileId) {
664
+ return `/assets/${fileId}`;
665
+ }
666
+ async function resolveAudioFileUrl(audioFilesRecordId) {
667
+ try {
668
+ const response = await api.get(`/items/AudioFiles/${audioFilesRecordId}`, {
669
+ params: { fields: ["file"] }
670
+ });
671
+ const fileId = response.data.data?.file;
672
+ if (fileId) {
673
+ return `/assets/${fileId}`;
674
+ }
675
+ } catch (e) {
676
+ console.warn("[Voice Widget] Could not resolve AudioFiles record:", e);
677
+ }
678
+ return `/assets/${audioFilesRecordId}`;
679
+ }
680
+ async function fetchVoiceVariants(lessonId) {
681
+ try {
682
+ const response = await api.get("/items/VoiceVariants", {
683
+ params: {
684
+ filter: { lesson_id: { _eq: lessonId } },
685
+ sort: ["-date_created"],
686
+ fields: ["id", "lesson_id", "audio_file_id", "voice_config", "callback_data", "date_created"]
687
+ }
688
+ });
689
+ return response.data.data || [];
690
+ } catch (error) {
691
+ console.error("Failed to fetch voice variants:", error);
692
+ return [];
693
+ }
694
+ }
695
+ async function updateVoiceSampleFile(voiceId, audioFilesRecordId, collection = "Voices") {
696
+ try {
697
+ const response = await api.get(`/items/AudioFiles/${audioFilesRecordId}`, {
698
+ params: { fields: ["file"] }
699
+ });
700
+ const fileUuid = response.data.data?.file;
701
+ if (!fileUuid) {
702
+ console.warn("[Voice Widget] Could not get file UUID from AudioFiles record");
703
+ return;
704
+ }
705
+ await api.patch(`/items/${collection}/${voiceId}`, {
706
+ sample_file: fileUuid
707
+ });
708
+ console.log(`[Voice Widget] Updated voice ${voiceId} sample_file to ${fileUuid}`);
709
+ } catch (error) {
710
+ console.error("[Voice Widget] Failed to update voice sample_file:", error);
711
+ }
712
+ }
713
+ return {
714
+ fetchVoices,
715
+ fetchFlowId,
716
+ fetchTones,
717
+ fetchStyles,
718
+ generateVoiceSample,
719
+ generateFullVoiceover,
720
+ getAudioUrl,
721
+ resolveAudioFileUrl,
722
+ fetchVoiceVariants,
723
+ updateVoiceSampleFile
724
+ };
725
+ }
726
+
727
+ const _hoisted_1 = { class: "voice-widget" };
728
+ const _hoisted_2 = {
729
+ key: 0,
730
+ class: "widget__processing"
731
+ };
732
+ const _hoisted_3 = { class: "widget__progress" };
733
+ const _hoisted_4 = { class: "widget__progress-bar" };
734
+ const _hoisted_5 = {
735
+ key: 0,
736
+ class: "widget__error"
737
+ };
738
+ const _hoisted_6 = { class: "widget__loading" };
739
+ const _hoisted_7 = { class: "widget__error" };
740
+ const _hoisted_8 = { class: "widget__header" };
741
+ const _hoisted_9 = { class: "widget__header-row" };
742
+ const _hoisted_10 = { class: "widget__header-text" };
743
+ const _hoisted_11 = { class: "widget__title" };
744
+ const _hoisted_12 = ["title"];
745
+ const _hoisted_13 = { class: "widget__header-controls" };
746
+ const _hoisted_14 = { class: "widget__url-input" };
747
+ const _hoisted_15 = ["disabled"];
748
+ const _hoisted_16 = { class: "widget__section" };
749
+ const _hoisted_17 = { class: "widget__model-buttons" };
750
+ const _hoisted_18 = ["onClick"];
751
+ const _hoisted_19 = { class: "widget__section" };
752
+ const _hoisted_20 = { class: "widget__voice-list" };
753
+ const _hoisted_21 = { class: "widget__section" };
754
+ const _hoisted_22 = { class: "widget__section" };
755
+ const _hoisted_23 = { class: "widget__section widget__section--toggle" };
756
+ const _hoisted_24 = { class: "widget__toggle" };
757
+ const _hoisted_25 = { class: "widget__footer" };
758
+ const _hoisted_26 = { class: "widget__footer-right" };
759
+ const _hoisted_27 = ["disabled"];
760
+ const _hoisted_28 = ["disabled"];
761
+ const _hoisted_29 = { class: "widget__header" };
762
+ const _hoisted_30 = { class: "widget__header-row" };
763
+ const _hoisted_31 = { class: "widget__header-text" };
764
+ const _hoisted_32 = { class: "widget__title" };
765
+ const _hoisted_33 = { class: "widget__subtitle" };
766
+ const _hoisted_34 = {
767
+ key: 0,
768
+ class: "widget__variant-nav"
769
+ };
770
+ const _hoisted_35 = ["disabled"];
771
+ const _hoisted_36 = { class: "widget__variant-counter" };
772
+ const _hoisted_37 = ["disabled"];
773
+ const _hoisted_38 = { class: "widget__result" };
774
+ const _hoisted_39 = { class: "widget__result-info" };
775
+ const _hoisted_40 = {
776
+ key: 0,
777
+ class: "widget__result-date"
778
+ };
779
+ var _sfc_main = /* @__PURE__ */ defineComponent({
780
+ __name: "interface",
781
+ props: {
782
+ value: {},
783
+ collection: {},
784
+ field: {},
785
+ primaryKey: {},
786
+ flowId: { default: "" },
787
+ voicesCollection: { default: "Voices" },
788
+ tonesCollection: { default: "VoiceTones" },
789
+ stylesCollection: { default: "VoiceStyles" },
790
+ defaultCollapsed: { type: Boolean, default: true }
791
+ },
792
+ emits: ["input"],
793
+ setup(__props, { emit: __emit }) {
794
+ const props = __props;
795
+ const emit = __emit;
796
+ const api = useApi();
797
+ const {
798
+ fetchVoices,
799
+ fetchFlowId,
800
+ fetchTones,
801
+ fetchStyles,
802
+ generateVoiceSample,
803
+ generateFullVoiceover,
804
+ getAudioUrl,
805
+ resolveAudioFileUrl,
806
+ fetchVoiceVariants,
807
+ updateVoiceSampleFile
808
+ } = useVoicingApi(api);
809
+ const currentMode = ref("selection");
810
+ const loading = ref(true);
811
+ const initError = ref(null);
812
+ const headerExpanded = ref(!props.defaultCollapsed);
813
+ const savingUrl = ref(false);
814
+ const flowId = ref("fada8e65-5ce2-477d-b5db-bf7f64a23ffa");
815
+ const selectedModel = ref("gemini");
816
+ const selectedVoiceId = ref(null);
817
+ const voices = ref([]);
818
+ const voiceSamples = ref({});
819
+ const generatingSampleFor = ref(null);
820
+ const tones = ref([]);
821
+ const styles = ref([]);
822
+ const selectedToneId = ref(null);
823
+ const selectedStyleId = ref(null);
824
+ const customTone = ref("");
825
+ const customStyle = ref("");
826
+ const preprocessingEnabled = ref(false);
827
+ const processingMessage = ref("Generating voiceover...");
828
+ const progressPercent = ref(0);
829
+ const errorMessage = ref(null);
830
+ const generatedAudioId = ref(null);
831
+ const generatedAudioUrl = ref(null);
832
+ const hasExistingVoices = ref(false);
833
+ const allVariants = ref([]);
834
+ const currentVariantIndex = ref(0);
835
+ const currentVoices = computed(() => {
836
+ return voices.value.filter((v) => v.provider === selectedModel.value);
837
+ });
838
+ const canGenerate = computed(() => {
839
+ return selectedVoiceId.value !== null && selectedToneId.value !== null && selectedStyleId.value !== null;
840
+ });
841
+ const currentVariantDate = computed(() => {
842
+ const variant = allVariants.value[currentVariantIndex.value];
843
+ if (!variant?.date_created) return null;
844
+ return new Date(variant.date_created).toLocaleString();
845
+ });
846
+ function selectModel(model) {
847
+ selectedModel.value = model;
848
+ selectedVoiceId.value = null;
849
+ }
850
+ function selectVoice(voiceId) {
851
+ selectedVoiceId.value = voiceId;
852
+ }
853
+ function selectTone(toneId) {
854
+ selectedToneId.value = toneId;
855
+ }
856
+ function selectStyle(styleId) {
857
+ selectedStyleId.value = styleId;
858
+ }
859
+ function updateCustomTone(value) {
860
+ customTone.value = value;
861
+ }
862
+ function updateCustomStyle(value) {
863
+ customStyle.value = value;
864
+ }
865
+ async function generateVoice(voiceId) {
866
+ const voice = voices.value.find((v) => v.id === voiceId);
867
+ if (!voice) {
868
+ console.warn("[Voice Widget] Generate Voice: voice not found for id", voiceId);
869
+ return;
870
+ }
871
+ generatingSampleFor.value = voiceId;
872
+ try {
873
+ const effectiveFlowId = flowId.value && flowId.value.trim() ? flowId.value.trim() : void 0;
874
+ const { url, audioFileId } = await generateVoiceSample(
875
+ voice.voice_id,
876
+ selectedModel.value,
877
+ effectiveFlowId
878
+ );
879
+ voiceSamples.value[voiceId] = url;
880
+ await updateVoiceSampleFile(voiceId, audioFileId, props.voicesCollection);
881
+ const voiceIndex = voices.value.findIndex((v) => v.id === voiceId);
882
+ if (voiceIndex !== -1) {
883
+ voices.value[voiceIndex].sample_file = url.replace("/assets/", "");
884
+ }
885
+ } catch (error) {
886
+ console.error("[Voice Widget] Failed to generate voice:", error?.message ?? error);
887
+ } finally {
888
+ generatingSampleFor.value = null;
889
+ }
890
+ }
891
+ function getSelectedTonePrompt() {
892
+ if (selectedToneId.value === "other") {
893
+ return customTone.value;
894
+ }
895
+ const tone = tones.value.find((t) => t.id === selectedToneId.value);
896
+ return tone?.prompt || "";
897
+ }
898
+ function getSelectedStylePrompt() {
899
+ if (selectedStyleId.value === "other") {
900
+ return customStyle.value;
901
+ }
902
+ const style = styles.value.find((s) => s.id === selectedStyleId.value);
903
+ return style?.prompt || "";
904
+ }
905
+ async function generateVoiceover() {
906
+ if (!canGenerate.value) return;
907
+ currentMode.value = "progress";
908
+ errorMessage.value = null;
909
+ processingMessage.value = "Generating voiceover...";
910
+ progressPercent.value = 10;
911
+ try {
912
+ const tonePrompt = getSelectedTonePrompt();
913
+ const stylePrompt = getSelectedStylePrompt();
914
+ const combinedStyle = `${tonePrompt}. ${stylePrompt}`.trim();
915
+ const selectedVoice = voices.value.find((v) => v.id === selectedVoiceId.value);
916
+ const providerVoiceId = selectedVoice?.voice_id || selectedVoiceId.value;
917
+ progressPercent.value = 30;
918
+ const result = await generateFullVoiceover({
919
+ provider: selectedModel.value,
920
+ voiceId: providerVoiceId,
921
+ style: combinedStyle,
922
+ preprocessing: preprocessingEnabled.value,
923
+ flowId: flowId.value,
924
+ lessonId: props.primaryKey,
925
+ toneId: selectedToneId.value || void 0,
926
+ styleId: selectedStyleId.value || void 0
927
+ });
928
+ progressPercent.value = 90;
929
+ if (result.audio_file_id) {
930
+ generatedAudioId.value = result.audio_file_id;
931
+ generatedAudioUrl.value = getAudioUrl(result.audio_file_id);
932
+ progressPercent.value = 100;
933
+ currentMode.value = "result";
934
+ } else if (result.status === "processing" && result.callback_data) {
935
+ processingMessage.value = "Processing in background...";
936
+ await pollForCompletion(result.callback_data);
937
+ }
938
+ } catch (error) {
939
+ errorMessage.value = error.message || "Failed to generate voiceover";
940
+ progressPercent.value = 0;
941
+ }
942
+ }
943
+ async function pollForCompletion(callbackData) {
944
+ const maxAttempts = 60;
945
+ const pollInterval = 5e3;
946
+ for (let i = 0; i < maxAttempts; i++) {
947
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
948
+ progressPercent.value = Math.min(90, 30 + i / maxAttempts * 60);
949
+ try {
950
+ const variants = await fetchVoiceVariants(props.primaryKey);
951
+ const latest = variants.find((v) => v.callback_data === callbackData);
952
+ if (latest && latest.audio_file_id) {
953
+ generatedAudioId.value = latest.audio_file_id;
954
+ generatedAudioUrl.value = await resolveAudioFileUrl(latest.audio_file_id);
955
+ progressPercent.value = 100;
956
+ currentMode.value = "result";
957
+ return;
958
+ }
959
+ } catch (error) {
960
+ console.error("Poll error:", error);
961
+ }
962
+ }
963
+ errorMessage.value = "Generation timed out. Please try again.";
964
+ }
965
+ function retryGeneration() {
966
+ errorMessage.value = null;
967
+ generateVoiceover();
968
+ }
969
+ function goBackToSelection() {
970
+ currentMode.value = "selection";
971
+ errorMessage.value = null;
972
+ progressPercent.value = 0;
973
+ }
974
+ async function prevVariant() {
975
+ if (currentVariantIndex.value > 0) {
976
+ currentVariantIndex.value--;
977
+ await loadVariantAtIndex(currentVariantIndex.value);
978
+ }
979
+ }
980
+ async function nextVariant() {
981
+ if (currentVariantIndex.value < allVariants.value.length - 1) {
982
+ currentVariantIndex.value++;
983
+ await loadVariantAtIndex(currentVariantIndex.value);
984
+ }
985
+ }
986
+ async function loadVariantAtIndex(index) {
987
+ const variant = allVariants.value[index];
988
+ if (!variant) return;
989
+ const audioFileId = variant.audio_file_id;
990
+ generatedAudioId.value = audioFileId;
991
+ const isUuid = audioFileId.includes("-");
992
+ generatedAudioUrl.value = isUuid ? getAudioUrl(audioFileId) : await resolveAudioFileUrl(audioFileId);
993
+ const config = variant.voice_config;
994
+ if (config) {
995
+ if (config.provider) {
996
+ selectedModel.value = config.provider;
997
+ }
998
+ if (config.voice_id) {
999
+ const matchingVoice = voices.value.find((v) => v.name === config.voice_id);
1000
+ if (matchingVoice) {
1001
+ selectedVoiceId.value = matchingVoice.id;
1002
+ }
1003
+ }
1004
+ if (config.tone_id) {
1005
+ selectedToneId.value = config.tone_id;
1006
+ }
1007
+ if (config.style_id) {
1008
+ selectedStyleId.value = config.style_id;
1009
+ }
1010
+ if (typeof config.preprocessing === "boolean") {
1011
+ preprocessingEnabled.value = config.preprocessing;
1012
+ }
1013
+ }
1014
+ }
1015
+ function regenerateVoiceover() {
1016
+ generateVoiceover();
1017
+ }
1018
+ function confirmVoiceover() {
1019
+ const value = {
1020
+ audio_file_id: generatedAudioId.value,
1021
+ model: selectedModel.value,
1022
+ voice_id: selectedVoiceId.value,
1023
+ tone_id: selectedToneId.value,
1024
+ style_id: selectedStyleId.value,
1025
+ custom_tone: customTone.value,
1026
+ custom_style: customStyle.value,
1027
+ preprocessing: preprocessingEnabled.value,
1028
+ confirmed_at: (/* @__PURE__ */ new Date()).toISOString()
1029
+ };
1030
+ emit("input", value);
1031
+ }
1032
+ async function viewGeneratedVoices() {
1033
+ try {
1034
+ const variants = await fetchVoiceVariants(props.primaryKey);
1035
+ if (variants.length > 0) {
1036
+ allVariants.value = variants;
1037
+ currentVariantIndex.value = 0;
1038
+ await loadVariantAtIndex(0);
1039
+ currentMode.value = "result";
1040
+ }
1041
+ } catch (error) {
1042
+ console.error("Failed to fetch voice variants:", error);
1043
+ }
1044
+ }
1045
+ function getModelName(modelId) {
1046
+ const model = VOICE_MODELS.find((m) => m.id === modelId);
1047
+ return model?.name || modelId;
1048
+ }
1049
+ function getVoiceName(voiceId) {
1050
+ if (!voiceId) return "Not selected";
1051
+ const voice = voices.value.find((v) => v.id === voiceId);
1052
+ return voice?.name || voiceId;
1053
+ }
1054
+ function getToneName(toneId) {
1055
+ if (!toneId) return "Not selected";
1056
+ if (toneId === "other") return customTone.value || "Custom";
1057
+ const tone = tones.value.find((t) => t.id === toneId);
1058
+ return tone?.name || toneId;
1059
+ }
1060
+ function getStyleName(styleId) {
1061
+ if (!styleId) return "Not selected";
1062
+ if (styleId === "other") return customStyle.value || "Custom";
1063
+ const style = styles.value.find((s) => s.id === styleId);
1064
+ return style?.name || styleId;
1065
+ }
1066
+ async function initialize() {
1067
+ loading.value = true;
1068
+ initError.value = null;
1069
+ try {
1070
+ if (!props.flowId) {
1071
+ flowId.value = await fetchFlowId();
1072
+ } else {
1073
+ flowId.value = props.flowId;
1074
+ }
1075
+ const [loadedVoices, loadedTones, loadedStyles] = await Promise.all([
1076
+ fetchVoices(props.voicesCollection),
1077
+ fetchTones(props.tonesCollection),
1078
+ fetchStyles(props.stylesCollection)
1079
+ ]);
1080
+ voices.value = loadedVoices;
1081
+ tones.value = loadedTones;
1082
+ styles.value = loadedStyles;
1083
+ if (currentVoices.value.length > 0 && !selectedVoiceId.value) {
1084
+ selectedVoiceId.value = currentVoices.value[0].id;
1085
+ }
1086
+ if (tones.value.length > 0 && !selectedToneId.value) {
1087
+ selectedToneId.value = tones.value[0].id;
1088
+ }
1089
+ if (styles.value.length > 0 && !selectedStyleId.value) {
1090
+ selectedStyleId.value = styles.value[0].id;
1091
+ }
1092
+ if (props.primaryKey) {
1093
+ const variants = await fetchVoiceVariants(props.primaryKey);
1094
+ hasExistingVoices.value = variants.length > 0;
1095
+ }
1096
+ initFromValue();
1097
+ } catch (error) {
1098
+ initError.value = error.message || "Failed to load configuration";
1099
+ } finally {
1100
+ loading.value = false;
1101
+ }
1102
+ }
1103
+ async function initFromValue() {
1104
+ if (!props.value || typeof props.value !== "object") return;
1105
+ if (props.value.model) {
1106
+ selectedModel.value = props.value.model;
1107
+ }
1108
+ if (props.value.voice_id) {
1109
+ selectedVoiceId.value = props.value.voice_id;
1110
+ }
1111
+ if (props.value.tone_id) {
1112
+ selectedToneId.value = props.value.tone_id;
1113
+ }
1114
+ if (props.value.style_id) {
1115
+ selectedStyleId.value = props.value.style_id;
1116
+ }
1117
+ if (props.value.custom_tone) {
1118
+ customTone.value = props.value.custom_tone;
1119
+ }
1120
+ if (props.value.custom_style) {
1121
+ customStyle.value = props.value.custom_style;
1122
+ }
1123
+ if (props.value.preprocessing !== void 0) {
1124
+ preprocessingEnabled.value = props.value.preprocessing;
1125
+ }
1126
+ if (props.value.audio_file_id) {
1127
+ const audioFileId = props.value.audio_file_id;
1128
+ const isUuid = audioFileId.includes("-");
1129
+ if (isUuid) {
1130
+ generatedAudioId.value = audioFileId;
1131
+ generatedAudioUrl.value = getAudioUrl(audioFileId);
1132
+ } else {
1133
+ generatedAudioId.value = audioFileId;
1134
+ generatedAudioUrl.value = await resolveAudioFileUrl(audioFileId);
1135
+ }
1136
+ }
1137
+ }
1138
+ watch(() => props.value, initFromValue, { deep: true });
1139
+ watch(selectedModel, () => {
1140
+ if (currentVoices.value.length > 0) {
1141
+ selectedVoiceId.value = currentVoices.value[0].id;
1142
+ }
1143
+ });
1144
+ onMounted(initialize);
1145
+ return (_ctx, _cache) => {
1146
+ const _component_v_icon = resolveComponent("v-icon");
1147
+ return openBlock(), createElementBlock("div", _hoisted_1, [
1148
+ createCommentVNode(" Processing State (Page 2) "),
1149
+ currentMode.value === "progress" ? (openBlock(), createElementBlock("div", _hoisted_2, [
1150
+ createElementVNode("div", _hoisted_3, [
1151
+ createVNode(_component_v_icon, {
1152
+ name: "refresh",
1153
+ class: "spinning"
1154
+ }),
1155
+ createElementVNode(
1156
+ "span",
1157
+ null,
1158
+ toDisplayString(processingMessage.value),
1159
+ 1
1160
+ /* TEXT */
1161
+ )
1162
+ ]),
1163
+ createElementVNode("div", _hoisted_4, [
1164
+ createElementVNode(
1165
+ "div",
1166
+ {
1167
+ class: "widget__progress-fill",
1168
+ style: normalizeStyle({ width: progressPercent.value + "%" })
1169
+ },
1170
+ null,
1171
+ 4
1172
+ /* STYLE */
1173
+ )
1174
+ ]),
1175
+ errorMessage.value ? (openBlock(), createElementBlock("div", _hoisted_5, [
1176
+ createVNode(_component_v_icon, { name: "error" }),
1177
+ createElementVNode(
1178
+ "span",
1179
+ null,
1180
+ toDisplayString(errorMessage.value),
1181
+ 1
1182
+ /* TEXT */
1183
+ ),
1184
+ createElementVNode("div", { class: "widget__error-actions" }, [
1185
+ createElementVNode("button", {
1186
+ class: "widget__btn widget__btn--secondary",
1187
+ onClick: goBackToSelection
1188
+ }, "Back"),
1189
+ createElementVNode("button", {
1190
+ class: "widget__btn widget__btn--primary",
1191
+ onClick: retryGeneration
1192
+ }, "Retry")
1193
+ ])
1194
+ ])) : createCommentVNode("v-if", true)
1195
+ ])) : loading.value ? (openBlock(), createElementBlock(
1196
+ Fragment,
1197
+ { key: 1 },
1198
+ [
1199
+ createCommentVNode(" Loading State "),
1200
+ createElementVNode("div", _hoisted_6, [
1201
+ createVNode(_component_v_icon, {
1202
+ name: "refresh",
1203
+ class: "spinning"
1204
+ }),
1205
+ _cache[3] || (_cache[3] = createElementVNode(
1206
+ "span",
1207
+ null,
1208
+ "Loading configuration...",
1209
+ -1
1210
+ /* CACHED */
1211
+ ))
1212
+ ])
1213
+ ],
1214
+ 2112
1215
+ /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */
1216
+ )) : initError.value ? (openBlock(), createElementBlock(
1217
+ Fragment,
1218
+ { key: 2 },
1219
+ [
1220
+ createCommentVNode(" Error State "),
1221
+ createElementVNode("div", _hoisted_7, [
1222
+ createVNode(_component_v_icon, { name: "error" }),
1223
+ createElementVNode(
1224
+ "span",
1225
+ null,
1226
+ toDisplayString(initError.value),
1227
+ 1
1228
+ /* TEXT */
1229
+ ),
1230
+ createElementVNode("button", {
1231
+ class: "widget__retry",
1232
+ onClick: initialize
1233
+ }, "Retry")
1234
+ ])
1235
+ ],
1236
+ 2112
1237
+ /* STABLE_FRAGMENT, DEV_ROOT_FRAGMENT */
1238
+ )) : currentMode.value === "selection" ? (openBlock(), createElementBlock(
1239
+ Fragment,
1240
+ { key: 3 },
1241
+ [
1242
+ createCommentVNode(" Page 1: Selection Mode "),
1243
+ createElementVNode("div", _hoisted_8, [
1244
+ createElementVNode("div", _hoisted_9, [
1245
+ createElementVNode("div", _hoisted_10, [
1246
+ createElementVNode("h2", _hoisted_11, [
1247
+ createVNode(_component_v_icon, { name: "mic" }),
1248
+ _cache[4] || (_cache[4] = createTextVNode(
1249
+ " Voice Generation ",
1250
+ -1
1251
+ /* CACHED */
1252
+ )),
1253
+ createElementVNode("button", {
1254
+ type: "button",
1255
+ class: "widget__collapse-btn",
1256
+ title: headerExpanded.value ? "Collapse" : "Expand",
1257
+ onClick: _cache[0] || (_cache[0] = ($event) => headerExpanded.value = !headerExpanded.value)
1258
+ }, [
1259
+ createVNode(_component_v_icon, {
1260
+ name: headerExpanded.value ? "expand_less" : "expand_more",
1261
+ small: ""
1262
+ }, null, 8, ["name"])
1263
+ ], 8, _hoisted_12)
1264
+ ]),
1265
+ _cache[5] || (_cache[5] = createElementVNode(
1266
+ "p",
1267
+ { class: "widget__subtitle" },
1268
+ " Select a voice model, voice, tone, and style for your voiceover ",
1269
+ -1
1270
+ /* CACHED */
1271
+ ))
1272
+ ]),
1273
+ withDirectives(createElementVNode(
1274
+ "div",
1275
+ _hoisted_13,
1276
+ [
1277
+ createElementVNode("div", _hoisted_14, [
1278
+ _cache[6] || (_cache[6] = createElementVNode(
1279
+ "label",
1280
+ { class: "widget__url-label" },
1281
+ "Flow ID:",
1282
+ -1
1283
+ /* CACHED */
1284
+ )),
1285
+ withDirectives(createElementVNode("input", {
1286
+ type: "text",
1287
+ class: "widget__url-field",
1288
+ "onUpdate:modelValue": _cache[1] || (_cache[1] = ($event) => flowId.value = $event),
1289
+ disabled: savingUrl.value
1290
+ }, null, 8, _hoisted_15), [
1291
+ [vModelText, flowId.value]
1292
+ ])
1293
+ ])
1294
+ ],
1295
+ 512
1296
+ /* NEED_PATCH */
1297
+ ), [
1298
+ [vShow, headerExpanded.value]
1299
+ ])
1300
+ ])
1301
+ ]),
1302
+ createCommentVNode(" Model Selection "),
1303
+ createElementVNode("div", _hoisted_16, [
1304
+ _cache[7] || (_cache[7] = createElementVNode(
1305
+ "label",
1306
+ { class: "widget__section-label" },
1307
+ "Voice Model",
1308
+ -1
1309
+ /* CACHED */
1310
+ )),
1311
+ createElementVNode("div", _hoisted_17, [
1312
+ (openBlock(true), createElementBlock(
1313
+ Fragment,
1314
+ null,
1315
+ renderList(unref(VOICE_MODELS), (model) => {
1316
+ return openBlock(), createElementBlock("button", {
1317
+ key: model.id,
1318
+ class: normalizeClass(["widget__model-btn", { "widget__model-btn--active": selectedModel.value === model.id }]),
1319
+ onClick: ($event) => selectModel(model.id)
1320
+ }, toDisplayString(model.name), 11, _hoisted_18);
1321
+ }),
1322
+ 128
1323
+ /* KEYED_FRAGMENT */
1324
+ ))
1325
+ ])
1326
+ ]),
1327
+ createCommentVNode(" Voice Selection "),
1328
+ createElementVNode("div", _hoisted_19, [
1329
+ _cache[8] || (_cache[8] = createElementVNode(
1330
+ "label",
1331
+ { class: "widget__section-label" },
1332
+ "Voice",
1333
+ -1
1334
+ /* CACHED */
1335
+ )),
1336
+ createElementVNode("div", _hoisted_20, [
1337
+ (openBlock(true), createElementBlock(
1338
+ Fragment,
1339
+ null,
1340
+ renderList(currentVoices.value, (voice) => {
1341
+ return openBlock(), createBlock(VoiceCard, {
1342
+ key: voice.id,
1343
+ voice,
1344
+ "is-selected": selectedVoiceId.value === voice.id,
1345
+ loading: generatingSampleFor.value === voice.id,
1346
+ "sample-url": voiceSamples.value[voice.id] || (voice.sample_file ? `/assets/${voice.sample_file}` : voice.sample_url),
1347
+ provider: selectedModel.value,
1348
+ onSelect: selectVoice,
1349
+ onGenerateVoice: generateVoice
1350
+ }, null, 8, ["voice", "is-selected", "loading", "sample-url", "provider"]);
1351
+ }),
1352
+ 128
1353
+ /* KEYED_FRAGMENT */
1354
+ ))
1355
+ ])
1356
+ ]),
1357
+ createCommentVNode(" Tone Selection "),
1358
+ createElementVNode("div", _hoisted_21, [
1359
+ _cache[9] || (_cache[9] = createElementVNode(
1360
+ "label",
1361
+ { class: "widget__section-label" },
1362
+ "Voice Tone",
1363
+ -1
1364
+ /* CACHED */
1365
+ )),
1366
+ createVNode(ToneStyleSelector, {
1367
+ items: tones.value,
1368
+ "selected-id": selectedToneId.value,
1369
+ "custom-value": customTone.value,
1370
+ placeholder: "Select a tone...",
1371
+ onSelect: selectTone,
1372
+ onCustomChange: updateCustomTone
1373
+ }, null, 8, ["items", "selected-id", "custom-value"])
1374
+ ]),
1375
+ createCommentVNode(" Style Selection "),
1376
+ createElementVNode("div", _hoisted_22, [
1377
+ _cache[10] || (_cache[10] = createElementVNode(
1378
+ "label",
1379
+ { class: "widget__section-label" },
1380
+ "Voice Style",
1381
+ -1
1382
+ /* CACHED */
1383
+ )),
1384
+ createVNode(ToneStyleSelector, {
1385
+ items: styles.value,
1386
+ "selected-id": selectedStyleId.value,
1387
+ "custom-value": customStyle.value,
1388
+ placeholder: "Select a style...",
1389
+ onSelect: selectStyle,
1390
+ onCustomChange: updateCustomStyle
1391
+ }, null, 8, ["items", "selected-id", "custom-value"])
1392
+ ]),
1393
+ createCommentVNode(" Preprocessing Toggle "),
1394
+ createElementVNode("div", _hoisted_23, [
1395
+ createElementVNode("label", _hoisted_24, [
1396
+ withDirectives(createElementVNode(
1397
+ "input",
1398
+ {
1399
+ type: "checkbox",
1400
+ "onUpdate:modelValue": _cache[2] || (_cache[2] = ($event) => preprocessingEnabled.value = $event)
1401
+ },
1402
+ null,
1403
+ 512
1404
+ /* NEED_PATCH */
1405
+ ), [
1406
+ [vModelCheckbox, preprocessingEnabled.value]
1407
+ ]),
1408
+ _cache[11] || (_cache[11] = createElementVNode(
1409
+ "span",
1410
+ { class: "widget__toggle-label" },
1411
+ "Enable Text Preprocessing",
1412
+ -1
1413
+ /* CACHED */
1414
+ ))
1415
+ ]),
1416
+ _cache[12] || (_cache[12] = createElementVNode(
1417
+ "p",
1418
+ { class: "widget__toggle-note" },
1419
+ "Preprocesses text before voice generation for better results",
1420
+ -1
1421
+ /* CACHED */
1422
+ ))
1423
+ ]),
1424
+ createCommentVNode(" Footer Actions "),
1425
+ createElementVNode("div", _hoisted_25, [
1426
+ _cache[13] || (_cache[13] = createElementVNode(
1427
+ "div",
1428
+ { class: "widget__footer-left" },
1429
+ null,
1430
+ -1
1431
+ /* CACHED */
1432
+ )),
1433
+ createElementVNode("div", _hoisted_26, [
1434
+ createElementVNode("button", {
1435
+ class: "widget__btn widget__btn--secondary",
1436
+ onClick: viewGeneratedVoices,
1437
+ disabled: !hasExistingVoices.value
1438
+ }, " View Generated Voices ", 8, _hoisted_27),
1439
+ createElementVNode("button", {
1440
+ class: "widget__btn widget__btn--primary",
1441
+ onClick: generateVoiceover,
1442
+ disabled: !canGenerate.value
1443
+ }, " Generate Voiceover ", 8, _hoisted_28)
1444
+ ])
1445
+ ])
1446
+ ],
1447
+ 64
1448
+ /* STABLE_FRAGMENT */
1449
+ )) : currentMode.value === "result" ? (openBlock(), createElementBlock(
1450
+ Fragment,
1451
+ { key: 4 },
1452
+ [
1453
+ createCommentVNode(" Page 3: Result Mode "),
1454
+ createElementVNode("div", _hoisted_29, [
1455
+ createElementVNode("div", _hoisted_30, [
1456
+ createElementVNode("div", _hoisted_31, [
1457
+ createElementVNode("h2", _hoisted_32, [
1458
+ createVNode(_component_v_icon, { name: "check_circle" }),
1459
+ _cache[14] || (_cache[14] = createTextVNode(
1460
+ " Voiceover Generated ",
1461
+ -1
1462
+ /* CACHED */
1463
+ ))
1464
+ ]),
1465
+ createElementVNode("p", _hoisted_33, [
1466
+ allVariants.value.length > 1 ? (openBlock(), createElementBlock(
1467
+ Fragment,
1468
+ { key: 0 },
1469
+ [
1470
+ createTextVNode(
1471
+ " Viewing " + toDisplayString(currentVariantIndex.value + 1) + " of " + toDisplayString(allVariants.value.length) + " generated voiceovers ",
1472
+ 1
1473
+ /* TEXT */
1474
+ )
1475
+ ],
1476
+ 64
1477
+ /* STABLE_FRAGMENT */
1478
+ )) : (openBlock(), createElementBlock(
1479
+ Fragment,
1480
+ { key: 1 },
1481
+ [
1482
+ createTextVNode(" Listen to your generated voiceover and confirm or regenerate ")
1483
+ ],
1484
+ 64
1485
+ /* STABLE_FRAGMENT */
1486
+ ))
1487
+ ])
1488
+ ])
1489
+ ])
1490
+ ]),
1491
+ createCommentVNode(" Navigation for multiple variants "),
1492
+ allVariants.value.length > 1 ? (openBlock(), createElementBlock("div", _hoisted_34, [
1493
+ createElementVNode("button", {
1494
+ class: "widget__btn widget__btn--icon",
1495
+ onClick: prevVariant,
1496
+ disabled: currentVariantIndex.value === 0
1497
+ }, [
1498
+ createVNode(_component_v_icon, { name: "chevron_left" })
1499
+ ], 8, _hoisted_35),
1500
+ createElementVNode(
1501
+ "span",
1502
+ _hoisted_36,
1503
+ toDisplayString(currentVariantIndex.value + 1) + " / " + toDisplayString(allVariants.value.length),
1504
+ 1
1505
+ /* TEXT */
1506
+ ),
1507
+ createElementVNode("button", {
1508
+ class: "widget__btn widget__btn--icon",
1509
+ onClick: nextVariant,
1510
+ disabled: currentVariantIndex.value === allVariants.value.length - 1
1511
+ }, [
1512
+ createVNode(_component_v_icon, { name: "chevron_right" })
1513
+ ], 8, _hoisted_37)
1514
+ ])) : createCommentVNode("v-if", true),
1515
+ createElementVNode("div", _hoisted_38, [
1516
+ createVNode(AudioPlayer, {
1517
+ src: generatedAudioUrl.value,
1518
+ loading: false,
1519
+ size: "large"
1520
+ }, null, 8, ["src"]),
1521
+ createElementVNode("div", _hoisted_39, [
1522
+ createElementVNode("p", null, [
1523
+ _cache[15] || (_cache[15] = createElementVNode(
1524
+ "strong",
1525
+ null,
1526
+ "Model:",
1527
+ -1
1528
+ /* CACHED */
1529
+ )),
1530
+ createTextVNode(
1531
+ " " + toDisplayString(getModelName(selectedModel.value)),
1532
+ 1
1533
+ /* TEXT */
1534
+ )
1535
+ ]),
1536
+ createElementVNode("p", null, [
1537
+ _cache[16] || (_cache[16] = createElementVNode(
1538
+ "strong",
1539
+ null,
1540
+ "Voice:",
1541
+ -1
1542
+ /* CACHED */
1543
+ )),
1544
+ createTextVNode(
1545
+ " " + toDisplayString(getVoiceName(selectedVoiceId.value)),
1546
+ 1
1547
+ /* TEXT */
1548
+ )
1549
+ ]),
1550
+ createElementVNode("p", null, [
1551
+ _cache[17] || (_cache[17] = createElementVNode(
1552
+ "strong",
1553
+ null,
1554
+ "Tone:",
1555
+ -1
1556
+ /* CACHED */
1557
+ )),
1558
+ createTextVNode(
1559
+ " " + toDisplayString(getToneName(selectedToneId.value)),
1560
+ 1
1561
+ /* TEXT */
1562
+ )
1563
+ ]),
1564
+ createElementVNode("p", null, [
1565
+ _cache[18] || (_cache[18] = createElementVNode(
1566
+ "strong",
1567
+ null,
1568
+ "Style:",
1569
+ -1
1570
+ /* CACHED */
1571
+ )),
1572
+ createTextVNode(
1573
+ " " + toDisplayString(getStyleName(selectedStyleId.value)),
1574
+ 1
1575
+ /* TEXT */
1576
+ )
1577
+ ]),
1578
+ currentVariantDate.value ? (openBlock(), createElementBlock("p", _hoisted_40, [
1579
+ _cache[19] || (_cache[19] = createElementVNode(
1580
+ "strong",
1581
+ null,
1582
+ "Generated:",
1583
+ -1
1584
+ /* CACHED */
1585
+ )),
1586
+ createTextVNode(
1587
+ " " + toDisplayString(currentVariantDate.value),
1588
+ 1
1589
+ /* TEXT */
1590
+ )
1591
+ ])) : createCommentVNode("v-if", true)
1592
+ ])
1593
+ ]),
1594
+ createElementVNode("div", { class: "widget__footer" }, [
1595
+ createElementVNode("div", { class: "widget__footer-left" }, [
1596
+ createElementVNode("button", {
1597
+ class: "widget__btn widget__btn--secondary",
1598
+ onClick: goBackToSelection
1599
+ }, " Return ")
1600
+ ]),
1601
+ createElementVNode("div", { class: "widget__footer-right" }, [
1602
+ createElementVNode("button", {
1603
+ class: "widget__btn widget__btn--secondary",
1604
+ onClick: regenerateVoiceover
1605
+ }, " Regenerate "),
1606
+ createElementVNode("button", {
1607
+ class: "widget__btn widget__btn--primary",
1608
+ onClick: confirmVoiceover
1609
+ }, " Confirm ")
1610
+ ])
1611
+ ])
1612
+ ],
1613
+ 64
1614
+ /* STABLE_FRAGMENT */
1615
+ )) : createCommentVNode("v-if", true)
1616
+ ]);
1617
+ };
1618
+ }
1619
+ });
1620
+
1621
+ var css = "\n.voice-widget[data-v-f98cb13e] {\n font-family: var(--theme--fonts--sans--font-family);\n padding: 16px;\n border: 1px solid var(--theme--form--field--input--border-color);\n border-radius: var(--theme--border-radius);\n background: var(--theme--background);\n}\n.widget__header[data-v-f98cb13e] {\n margin-bottom: 20px;\n}\n.widget__header-row[data-v-f98cb13e] {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 16px;\n}\n.widget__header-text[data-v-f98cb13e] {\n flex: 1;\n}\n.widget__title[data-v-f98cb13e] {\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 0 0 4px 0;\n font-size: 18px;\n font-weight: 600;\n color: var(--theme--foreground);\n}\n.widget__collapse-btn[data-v-f98cb13e] {\n background: none;\n border: none;\n padding: 4px;\n cursor: pointer;\n color: var(--theme--foreground-subdued);\n border-radius: 4px;\n}\n.widget__collapse-btn[data-v-f98cb13e]:hover {\n background: var(--theme--background-accent);\n}\n.widget__subtitle[data-v-f98cb13e] {\n margin: 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__header-controls[data-v-f98cb13e] {\n display: flex;\n gap: 16px;\n align-items: center;\n}\n.widget__url-input[data-v-f98cb13e] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.widget__url-label[data-v-f98cb13e] {\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n white-space: nowrap;\n}\n.widget__url-field[data-v-f98cb13e] {\n padding: 6px 10px;\n border: 1px solid var(--theme--form--field--input--border-color);\n border-radius: var(--theme--border-radius);\n font-size: 12px;\n width: 280px;\n background: var(--theme--form--field--input--background);\n color: var(--theme--foreground);\n}\n.widget__section[data-v-f98cb13e] {\n margin-bottom: 20px;\n}\n.widget__section-label[data-v-f98cb13e] {\n display: block;\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__model-buttons[data-v-f98cb13e] {\n display: flex;\n gap: 8px;\n}\n.widget__model-btn[data-v-f98cb13e] {\n padding: 8px 16px;\n border: 1px solid var(--theme--form--field--input--border-color);\n border-radius: var(--theme--border-radius);\n background: var(--theme--background);\n color: var(--theme--foreground);\n cursor: pointer;\n font-size: 14px;\n transition: all 0.15s ease;\n}\n.widget__model-btn[data-v-f98cb13e]:hover {\n border-color: var(--theme--primary);\n}\n.widget__model-btn--active[data-v-f98cb13e] {\n background: var(--theme--primary);\n border-color: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.widget__voice-list[data-v-f98cb13e] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.widget__section--toggle[data-v-f98cb13e] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n}\n.widget__toggle[data-v-f98cb13e] {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n.widget__toggle input[data-v-f98cb13e] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__toggle-label[data-v-f98cb13e] {\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__toggle-note[data-v-f98cb13e] {\n margin: 4px 0 0 24px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__footer[data-v-f98cb13e] {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding-top: 16px;\n border-top: 1px solid var(--theme--border-color-subdued);\n margin-top: 20px;\n}\n.widget__footer-left[data-v-f98cb13e],\n.widget__footer-right[data-v-f98cb13e] {\n display: flex;\n gap: 8px;\n}\n.widget__variant-nav[data-v-f98cb13e] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n margin-bottom: 16px;\n padding: 8px 0;\n}\n.widget__variant-counter[data-v-f98cb13e] {\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground-subdued);\n min-width: 60px;\n text-align: center;\n}\n.widget__btn--icon[data-v-f98cb13e] {\n padding: 6px;\n min-width: 32px;\n min-height: 32px;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.widget__result-date[data-v-f98cb13e] {\n margin-top: 8px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn[data-v-f98cb13e] {\n padding: 8px 16px;\n border: none;\n border-radius: var(--theme--border-radius);\n font-size: 14px;\n font-weight: 500;\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.widget__btn[data-v-f98cb13e]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.widget__btn--primary[data-v-f98cb13e] {\n background: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.widget__btn--primary[data-v-f98cb13e]:hover:not(:disabled) {\n background: var(--theme--primary-accent);\n}\n.widget__btn--secondary[data-v-f98cb13e] {\n background: var(--theme--background-accent);\n color: var(--theme--foreground);\n border: 1px solid var(--theme--form--field--input--border-color);\n}\n.widget__btn--secondary[data-v-f98cb13e]:hover:not(:disabled) {\n background: var(--theme--background-normal);\n}\n\n/* Processing State */\n.widget__processing[data-v-f98cb13e] {\n padding: 40px 20px;\n text-align: center;\n}\n.widget__progress[data-v-f98cb13e] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n margin-bottom: 16px;\n font-size: 16px;\n color: var(--theme--foreground);\n}\n.widget__progress-bar[data-v-f98cb13e] {\n height: 8px;\n background: var(--theme--background-accent);\n border-radius: 4px;\n overflow: hidden;\n max-width: 400px;\n margin: 0 auto;\n}\n.widget__progress-fill[data-v-f98cb13e] {\n height: 100%;\n background: var(--theme--primary);\n transition: width 0.3s ease;\n}\n\n/* Error State */\n.widget__error[data-v-f98cb13e] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 12px;\n padding: 20px;\n color: var(--theme--danger);\n text-align: center;\n}\n.widget__error-actions[data-v-f98cb13e] {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n}\n.widget__retry[data-v-f98cb13e] {\n padding: 6px 12px;\n background: var(--theme--danger);\n color: #fff;\n border: none;\n border-radius: var(--theme--border-radius);\n cursor: pointer;\n}\n\n/* Loading State */\n.widget__loading[data-v-f98cb13e] {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 40px 20px;\n color: var(--theme--foreground-subdued);\n}\n\n/* Result State */\n.widget__result[data-v-f98cb13e] {\n padding: 20px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n margin-bottom: 20px;\n}\n.widget__result-info[data-v-f98cb13e] {\n margin-top: 16px;\n padding-top: 16px;\n border-top: 1px solid var(--theme--border-color-subdued);\n}\n.widget__result-info p[data-v-f98cb13e] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__result-info strong[data-v-f98cb13e] {\n color: var(--theme--foreground);\n}\n\n/* Animations */\n.spinning[data-v-f98cb13e] {\n animation: spin-f98cb13e 1s linear infinite;\n}\n@keyframes spin-f98cb13e {\nfrom { transform: rotate(0deg);\n}\nto { transform: rotate(360deg);\n}\n}\n";
1622
+ n(css,{});
1623
+
1624
+ var InterfaceComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-f98cb13e"], ["__file", "interface.vue"]]);
1625
+
1626
+ var index = defineInterface({
1627
+ id: "voice-widget",
1628
+ name: "Voice Widget",
1629
+ icon: "mic",
1630
+ description: "Voice generation with model/voice selection and audio preview",
1631
+ component: InterfaceComponent,
1632
+ options: [
1633
+ {
1634
+ field: "flowId",
1635
+ name: "Flow ID",
1636
+ type: "string",
1637
+ meta: {
1638
+ width: "full",
1639
+ interface: "input",
1640
+ note: "Flow ID for routing to voicing service (from WidgetConfig)"
1641
+ },
1642
+ schema: {
1643
+ default_value: ""
1644
+ }
1645
+ },
1646
+ {
1647
+ field: "voicesCollection",
1648
+ name: "Voices Collection",
1649
+ type: "string",
1650
+ meta: {
1651
+ width: "half",
1652
+ interface: "input",
1653
+ note: "Collection containing voice definitions (default: Voices)"
1654
+ },
1655
+ schema: {
1656
+ default_value: "Voices"
1657
+ }
1658
+ },
1659
+ {
1660
+ field: "tonesCollection",
1661
+ name: "Tones Collection",
1662
+ type: "string",
1663
+ meta: {
1664
+ width: "half",
1665
+ interface: "input",
1666
+ note: "Collection containing voice tone definitions (default: VoiceTones)"
1667
+ },
1668
+ schema: {
1669
+ default_value: "VoiceTones"
1670
+ }
1671
+ },
1672
+ {
1673
+ field: "stylesCollection",
1674
+ name: "Styles Collection",
1675
+ type: "string",
1676
+ meta: {
1677
+ width: "half",
1678
+ interface: "input",
1679
+ note: "Collection containing voice style definitions (default: VoiceStyles)"
1680
+ },
1681
+ schema: {
1682
+ default_value: "VoiceStyles"
1683
+ }
1684
+ },
1685
+ {
1686
+ field: "defaultCollapsed",
1687
+ name: "Default Collapsed",
1688
+ type: "boolean",
1689
+ meta: {
1690
+ width: "half",
1691
+ interface: "boolean",
1692
+ note: "When true (default), header starts collapsed."
1693
+ },
1694
+ schema: {
1695
+ default_value: true
1696
+ }
1697
+ }
1698
+ ],
1699
+ types: ["json", "string"]
1700
+ });
1701
+
1702
+ export { index as default };