@stellartech/voice-widget-directus 1.0.2 → 1.0.4

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 (3) hide show
  1. package/README.md +46 -0
  2. package/dist/index.js +321 -111
  3. package/package.json +1 -1
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @stellartech/voice-widget-directus
2
+
3
+ Voice generation widget with model/voice selection and audio preview for Directus.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @stellartech/voice-widget-directus
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Multiple TTS provider support (Gemini, ElevenLabs)
14
+ - Voice selection with audio sample previews
15
+ - Tone and style customization
16
+ - Full voiceover generation with progress tracking
17
+ - Async generation with background processing
18
+ - Audio playback and variant management
19
+
20
+ ## Requirements
21
+
22
+ - Directus ^11.0.0
23
+ - Vue ^3.4.0
24
+
25
+ ## Configuration
26
+
27
+ The widget requires the following collections in your Directus instance:
28
+
29
+ - `Voices` - Available voice options with samples
30
+ - `VoiceTones` - Tone presets for voice generation
31
+ - `VoiceStyles` - Style presets for voice generation
32
+ - `VoiceVariants` - Generated voice variants storage
33
+ - `AudioFiles` - Audio file records
34
+
35
+ ## Usage
36
+
37
+ After installation, the interface will be available in your Directus instance as a custom interface type. Configure it on a JSON field to enable voice generation for your content.
38
+
39
+ ### Widget Options
40
+
41
+ - **Voices Collection**: Collection containing voice definitions
42
+ - **Flow ID**: Directus Flow ID for voice generation proxy
43
+
44
+ ## License
45
+
46
+ MIT
package/dist/index.js CHANGED
@@ -455,7 +455,7 @@ const VOICE_MODELS = [
455
455
  { id: "elevenlabs", name: "ElevenLabs" }
456
456
  ];
457
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";
458
+ const DEFAULT_FLOW_ID = "7fa08903-ed7d-4632-81fc-f422d873b8f8";
459
459
  function useVoicingApi(api) {
460
460
  async function fetchVoices(collection = "Voices") {
461
461
  try {
@@ -525,17 +525,29 @@ function useVoicingApi(api) {
525
525
  return [];
526
526
  }
527
527
  }
528
- async function generateVoiceSample(voiceId, provider, flowId = DEFAULT_FLOW_ID) {
528
+ async function generateVoiceSample(voiceId, provider, flowId = DEFAULT_FLOW_ID, lessonId, collection) {
529
529
  const effectiveFlowId = flowId && flowId.trim() ? flowId.trim() : DEFAULT_FLOW_ID;
530
530
  if (!effectiveFlowId) {
531
531
  throw new Error("Voice flow ID is not configured. Set it in Widget Config or in the Flow ID field.");
532
532
  }
533
+ if (!collection) {
534
+ throw new Error("collection is required but was not provided");
535
+ }
533
536
  const voicingPayload = {
534
537
  texts: [SAMPLE_TEXT],
535
538
  provider,
536
539
  preprocessing: false,
537
540
  title: `Voice Sample - ${voiceId}`,
538
- audio_files_collection: "AudioFiles"
541
+ audio_files_collection: "AudioFiles",
542
+ // Include lesson context for callback tracking
543
+ lesson_id: lessonId || null,
544
+ collection,
545
+ voice_config: {
546
+ provider,
547
+ voice_id: voiceId,
548
+ style: "neutral",
549
+ is_sample: true
550
+ }
539
551
  };
540
552
  if (provider === "gemini") {
541
553
  voicingPayload.speakers = [{ voice: voiceId, name: "Sample", style: "neutral" }];
@@ -566,7 +578,8 @@ function useVoicingApi(api) {
566
578
  const url = await resolveAudioFileUrl(audioFileId);
567
579
  return { url, audioFileId };
568
580
  }
569
- throw new Error("No audio file returned from voice generation. Response: " + JSON.stringify(flowResponse));
581
+ console.log("[Voice Widget] Sample processing async, callback will update Voices.example");
582
+ return { url: "", audioFileId: "" };
570
583
  } catch (e) {
571
584
  console.error("Failed to generate voice sample:", e);
572
585
  throw new Error(e.response?.data?.detail || e.message || "Failed to generate sample");
@@ -578,7 +591,10 @@ function useVoicingApi(api) {
578
591
  if (!effectiveFlowId) {
579
592
  throw new Error("Voice flow ID is not configured. Set it in Widget Config or in the Flow ID field.");
580
593
  }
581
- const collection = request.collection || "SM_Lessons";
594
+ if (!request.collection) {
595
+ throw new Error("collection is required but was not provided");
596
+ }
597
+ const collection = request.collection;
582
598
  let texts = [];
583
599
  let lessonTitle = `Lesson ${request.lessonId}`;
584
600
  try {
@@ -672,17 +688,30 @@ function useVoicingApi(api) {
672
688
  return `/assets/${audioFilesRecordId}`;
673
689
  }
674
690
  async function fetchVoiceVariants(lessonId) {
691
+ const lessonIdStr = String(lessonId);
692
+ console.log("[Voice Widget] ========== fetchVoiceVariants START ==========");
693
+ console.log("[Voice Widget] Lesson ID:", lessonIdStr);
694
+ console.log("[Voice Widget] API object exists:", !!api);
675
695
  try {
676
- const response = await api.get("/items/VoiceVariants", {
677
- params: {
678
- filter: { lesson_id: { _eq: lessonId } },
679
- sort: ["-date_created"],
680
- fields: ["id", "lesson_id", "audio_file_id", "voice_config", "callback_data", "date_created"]
681
- }
682
- });
683
- return response.data.data || [];
696
+ console.log('[Voice Widget] Calling api.get("/items/VoiceVariants")...');
697
+ const response = await api.get("/items/VoiceVariants");
698
+ console.log("[Voice Widget] Response status:", response?.status);
699
+ console.log("[Voice Widget] Response data type:", typeof response?.data);
700
+ console.log("[Voice Widget] Response.data.data type:", typeof response?.data?.data);
701
+ const allData = response.data?.data || response.data || [];
702
+ console.log("[Voice Widget] Extracted data length:", allData?.length);
703
+ if (allData.length > 0) {
704
+ console.log("[Voice Widget] First item:", JSON.stringify(allData[0]));
705
+ }
706
+ const filtered = allData.filter((v) => String(v.lesson_id) === lessonIdStr);
707
+ console.log("[Voice Widget] Filtered count:", filtered.length);
708
+ console.log("[Voice Widget] ========== fetchVoiceVariants END ==========");
709
+ return filtered;
684
710
  } catch (error) {
685
- console.error("Failed to fetch voice variants:", error);
711
+ console.error("[Voice Widget] ========== fetchVoiceVariants ERROR ==========");
712
+ console.error("[Voice Widget] Error:", error);
713
+ console.error("[Voice Widget] Error message:", error?.message);
714
+ console.error("[Voice Widget] Error response:", error?.response?.status, error?.response?.data);
686
715
  return [];
687
716
  }
688
717
  }
@@ -699,13 +728,13 @@ function useVoicingApi(api) {
699
728
  console.warn("[Voice Widget] Could not get file UUID from AudioFiles record. fileField:", fileField);
700
729
  return;
701
730
  }
702
- console.log(`[Voice Widget] Patching ${collection}/${voiceId} with sample_file=${fileUuid}`);
731
+ console.log(`[Voice Widget] Patching ${collection}/${voiceId} with example=${fileUuid}`);
703
732
  await api.patch(`/items/${collection}/${voiceId}`, {
704
- sample_file: fileUuid
733
+ example: fileUuid
705
734
  });
706
- console.log(`[Voice Widget] Updated voice ${voiceId} sample_file to ${fileUuid}`);
735
+ console.log(`[Voice Widget] Updated voice ${voiceId} example to ${fileUuid}`);
707
736
  } catch (error) {
708
- console.error("[Voice Widget] Failed to update voice sample_file:", error);
737
+ console.error("[Voice Widget] Failed to update voice example:", error);
709
738
  }
710
739
  }
711
740
  async function deleteVoiceVariant(variantId) {
@@ -768,6 +797,59 @@ function useVoicingApi(api) {
768
797
  return null;
769
798
  }
770
799
  }
800
+ async function setVoiceExampleStatus(voiceId, status, collection = "Voices") {
801
+ try {
802
+ await api.patch(`/items/${collection}/${voiceId}`, {
803
+ example_status: status
804
+ });
805
+ console.log(`[Voice Widget] Set voice ${voiceId} example_status to ${status}`);
806
+ } catch (error) {
807
+ console.error("[Voice Widget] Failed to set example_status:", error);
808
+ }
809
+ }
810
+ async function createPendingVoiceVariant(lessonId, voiceConfig) {
811
+ try {
812
+ const response = await api.post("/items/VoiceVariants", {
813
+ lesson_id: lessonId,
814
+ voice_config: voiceConfig,
815
+ status: "processing"
816
+ });
817
+ const variantId = response.data.data?.id;
818
+ console.log(`[Voice Widget] Created pending VoiceVariant: ${variantId}`);
819
+ return variantId;
820
+ } catch (error) {
821
+ console.error("[Voice Widget] Failed to create pending VoiceVariant:", error);
822
+ return null;
823
+ }
824
+ }
825
+ async function fetchPendingVariants(lessonId) {
826
+ const lessonIdStr = String(lessonId);
827
+ console.log("[Voice Widget] ========== fetchPendingVariants START ==========");
828
+ try {
829
+ const response = await api.get("/items/VoiceVariants", {
830
+ params: {
831
+ fields: "id,lesson_id,voice_config,status,date_created",
832
+ limit: -1
833
+ }
834
+ });
835
+ const allData = response.data?.data || [];
836
+ console.log("[Voice Widget] fetchPendingVariants: got", allData.length, "total variants");
837
+ console.log("[Voice Widget] All statuses:", allData.map((v) => v.status));
838
+ const pending = allData.filter((v) => {
839
+ const matches = String(v.lesson_id) === lessonIdStr && v.status === "processing";
840
+ if (matches) {
841
+ console.log("[Voice Widget] Found PROCESSING variant:", v.id, "status:", v.status);
842
+ }
843
+ return matches;
844
+ });
845
+ console.log("[Voice Widget] fetchPendingVariants: returning", pending.length, "processing variants");
846
+ console.log("[Voice Widget] ========== fetchPendingVariants END ==========");
847
+ return pending;
848
+ } catch (error) {
849
+ console.error("[Voice Widget] Failed to fetch pending variants:", error);
850
+ return [];
851
+ }
852
+ }
771
853
  return {
772
854
  fetchVoices,
773
855
  fetchFlowId,
@@ -782,7 +864,10 @@ function useVoicingApi(api) {
782
864
  deleteVoiceVariant,
783
865
  createVoiceVariant,
784
866
  linkAudioToLesson,
785
- pollVoicingJob
867
+ pollVoicingJob,
868
+ setVoiceExampleStatus,
869
+ createPendingVoiceVariant,
870
+ fetchPendingVariants
786
871
  };
787
872
  }
788
873
 
@@ -877,14 +962,17 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
877
962
  deleteVoiceVariant,
878
963
  createVoiceVariant,
879
964
  linkAudioToLesson,
880
- pollVoicingJob
965
+ pollVoicingJob,
966
+ setVoiceExampleStatus,
967
+ createPendingVoiceVariant,
968
+ fetchPendingVariants
881
969
  } = useVoicingApi(api);
882
970
  const currentMode = ref("selection");
883
971
  const loading = ref(true);
884
972
  const initError = ref(null);
885
973
  const headerExpanded = ref(!props.defaultCollapsed);
886
974
  const savingUrl = ref(false);
887
- const flowId = ref("fada8e65-5ce2-477d-b5db-bf7f64a23ffa");
975
+ const flowId = ref("7fa08903-ed7d-4632-81fc-f422d873b8f8");
888
976
  const selectedModel = ref("gemini");
889
977
  const selectedVoiceId = ref(null);
890
978
  const voices = ref([]);
@@ -905,9 +993,11 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
905
993
  const hasExistingVoices = ref(false);
906
994
  const allVariants = ref([]);
907
995
  const currentVariantIndex = ref(0);
908
- const currentJobId = ref(null);
996
+ ref(null);
909
997
  const isPollingProgress = ref(false);
910
998
  const progressStatus = ref("");
999
+ let samplePollingInterval = null;
1000
+ let voiceoverPollingInterval = null;
911
1001
  const selectedVariant = computed(() => allVariants.value[currentVariantIndex.value]);
912
1002
  const currentVoices = computed(() => {
913
1003
  return voices.value.filter((v) => v.provider === selectedModel.value);
@@ -947,24 +1037,125 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
947
1037
  }
948
1038
  generatingSampleFor.value = voiceId;
949
1039
  try {
950
- const effectiveFlowId = flowId.value && flowId.value.trim() ? flowId.value.trim() : void 0;
951
- const { url, audioFileId } = await generateVoiceSample(
952
- voice.voice_id,
953
- selectedModel.value,
954
- effectiveFlowId
955
- );
956
- voiceSamples.value[voiceId] = url;
957
- await updateVoiceSampleFile(voiceId, audioFileId, props.voicesCollection);
1040
+ await setVoiceExampleStatus(voiceId, "processing", props.voicesCollection);
958
1041
  const voiceIndex = voices.value.findIndex((v) => v.id === voiceId);
959
1042
  if (voiceIndex !== -1) {
960
- voices.value[voiceIndex].sample_file = url.replace("/assets/", "");
1043
+ voices.value[voiceIndex].example_status = "processing";
961
1044
  }
1045
+ const effectiveFlowId = flowId.value && flowId.value.trim() ? flowId.value.trim() : void 0;
1046
+ if (!props.collection) {
1047
+ throw new Error("collection is required but was not provided");
1048
+ }
1049
+ generateVoiceSample(
1050
+ voice.voice,
1051
+ selectedModel.value,
1052
+ effectiveFlowId,
1053
+ props.primaryKey,
1054
+ props.collection
1055
+ ).then(() => {
1056
+ console.log("[Voice Widget] Sample generation request sent for", voice.voice);
1057
+ }).catch((error) => {
1058
+ console.error("[Voice Widget] Failed to start sample generation:", error?.message ?? error);
1059
+ setVoiceExampleStatus(voiceId, null, props.voicesCollection);
1060
+ });
1061
+ setTimeout(() => {
1062
+ generatingSampleFor.value = null;
1063
+ }, 1500);
1064
+ startSamplePolling();
962
1065
  } catch (error) {
963
1066
  console.error("[Voice Widget] Failed to generate voice:", error?.message ?? error);
964
- } finally {
965
1067
  generatingSampleFor.value = null;
966
1068
  }
967
1069
  }
1070
+ function startSamplePolling() {
1071
+ if (samplePollingInterval) return;
1072
+ console.log("[Voice Widget] Starting sample completion polling");
1073
+ samplePollingInterval = setInterval(async () => {
1074
+ try {
1075
+ const freshVoices = await fetchVoices(props.voicesCollection);
1076
+ let hasProcessing = false;
1077
+ for (const freshVoice of freshVoices) {
1078
+ const localVoice = voices.value.find((v) => v.id === freshVoice.id);
1079
+ if (localVoice) {
1080
+ if (localVoice.example_status === "processing" && freshVoice.example_status !== "processing") {
1081
+ console.log("[Voice Widget] Sample completed for", freshVoice.voice);
1082
+ localVoice.example_status = freshVoice.example_status;
1083
+ localVoice.example = freshVoice.example;
1084
+ }
1085
+ if (freshVoice.example_status === "processing") {
1086
+ hasProcessing = true;
1087
+ }
1088
+ }
1089
+ }
1090
+ if (!hasProcessing) {
1091
+ console.log("[Voice Widget] All samples completed, stopping polling");
1092
+ stopSamplePolling();
1093
+ }
1094
+ } catch (error) {
1095
+ console.error("[Voice Widget] Sample polling error:", error);
1096
+ }
1097
+ }, 3e3);
1098
+ }
1099
+ function stopSamplePolling() {
1100
+ if (samplePollingInterval) {
1101
+ clearInterval(samplePollingInterval);
1102
+ samplePollingInterval = null;
1103
+ }
1104
+ }
1105
+ let voiceoverPollCount = 0;
1106
+ let voiceoverLessonId = null;
1107
+ function startVoiceoverPolling() {
1108
+ stopVoiceoverPolling();
1109
+ voiceoverPollCount = 0;
1110
+ voiceoverLessonId = props.primaryKey ? String(props.primaryKey) : null;
1111
+ if (!voiceoverLessonId) {
1112
+ console.error("[Voice Widget] Cannot start polling - no primaryKey!");
1113
+ return;
1114
+ }
1115
+ console.log("[Voice Widget] Starting voiceover completion polling for lesson:", voiceoverLessonId);
1116
+ const checkCompletion = async () => {
1117
+ if (!voiceoverLessonId) {
1118
+ console.error("[Voice Widget] Poll aborted - no lesson ID");
1119
+ stopVoiceoverPolling();
1120
+ return;
1121
+ }
1122
+ voiceoverPollCount++;
1123
+ console.log(`[Voice Widget] Poll #${voiceoverPollCount} for lesson ${voiceoverLessonId}...`);
1124
+ try {
1125
+ const allVariantsNow = await fetchVoiceVariants(voiceoverLessonId);
1126
+ console.log(`[Voice Widget] Poll #${voiceoverPollCount} raw result:`, JSON.stringify(allVariantsNow));
1127
+ const completedWithAudio = allVariantsNow.filter((v) => v.audio_file_id);
1128
+ console.log(`[Voice Widget] Poll #${voiceoverPollCount}: total=${allVariantsNow.length}, withAudio=${completedWithAudio.length}`);
1129
+ if (completedWithAudio.length > 0) {
1130
+ console.log("[Voice Widget] COMPLETED! Transitioning to result...");
1131
+ stopVoiceoverPolling();
1132
+ allVariants.value = completedWithAudio;
1133
+ for (const v of allVariants.value) {
1134
+ if (v.audio_file_id) {
1135
+ const isUuid = v.audio_file_id?.includes("-");
1136
+ v.audioUrl = isUuid ? getAudioUrl(v.audio_file_id) : await resolveAudioFileUrl(v.audio_file_id);
1137
+ }
1138
+ }
1139
+ currentVariantIndex.value = 0;
1140
+ hasExistingVoices.value = true;
1141
+ currentMode.value = "result";
1142
+ console.log("[Voice Widget] Mode set to result, allVariants:", allVariants.value.length);
1143
+ } else {
1144
+ console.log(`[Voice Widget] Poll #${voiceoverPollCount} - waiting for audio_file_id...`);
1145
+ }
1146
+ } catch (error) {
1147
+ console.error("[Voice Widget] Voiceover polling error:", error);
1148
+ }
1149
+ };
1150
+ voiceoverPollingInterval = setInterval(checkCompletion, 2e3);
1151
+ setTimeout(checkCompletion, 500);
1152
+ }
1153
+ function stopVoiceoverPolling() {
1154
+ if (voiceoverPollingInterval) {
1155
+ clearInterval(voiceoverPollingInterval);
1156
+ voiceoverPollingInterval = null;
1157
+ }
1158
+ }
968
1159
  function getSelectedTonePrompt() {
969
1160
  if (selectedToneId.value === "other") {
970
1161
  return customTone.value;
@@ -991,7 +1182,19 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
991
1182
  const stylePrompt = getSelectedStylePrompt();
992
1183
  const combinedStyle = `${tonePrompt}. ${stylePrompt}`.trim();
993
1184
  const selectedVoice = voices.value.find((v) => v.id === selectedVoiceId.value);
994
- const providerVoiceId = selectedVoice?.voice_id || selectedVoiceId.value;
1185
+ const providerVoiceId = selectedVoice?.voice || selectedVoiceId.value;
1186
+ const voiceConfig = {
1187
+ provider: selectedModel.value,
1188
+ voice_id: providerVoiceId,
1189
+ style: combinedStyle,
1190
+ tone_id: selectedToneId.value,
1191
+ style_id: selectedStyleId.value
1192
+ };
1193
+ await createPendingVoiceVariant(props.primaryKey, voiceConfig);
1194
+ startVoiceoverPolling();
1195
+ if (!props.collection) {
1196
+ throw new Error("collection is required but was not provided");
1197
+ }
995
1198
  const result = await generateFullVoiceover({
996
1199
  provider: selectedModel.value,
997
1200
  voiceId: providerVoiceId,
@@ -999,21 +1202,12 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
999
1202
  preprocessing: preprocessingEnabled.value,
1000
1203
  flowId: flowId.value,
1001
1204
  lessonId: props.primaryKey,
1002
- collection: props.collection || "SM_Lessons",
1205
+ collection: props.collection,
1003
1206
  toneId: selectedToneId.value || void 0,
1004
1207
  styleId: selectedStyleId.value || void 0
1005
1208
  });
1006
- const voiceConfig = {
1007
- provider: selectedModel.value,
1008
- voice_id: providerVoiceId,
1009
- style: combinedStyle,
1010
- tone_id: selectedToneId.value,
1011
- style_id: selectedStyleId.value
1012
- };
1013
- if (result.job_id) {
1014
- processingMessage.value = "Processing in background...";
1015
- await startProgressPolling(result.job_id, voiceConfig);
1016
- } else if (result.audio_file_id) {
1209
+ if (result.audio_file_id) {
1210
+ stopVoiceoverPolling();
1017
1211
  generatedAudioId.value = result.audio_file_id;
1018
1212
  generatedAudioUrl.value = getAudioUrl(result.audio_file_id);
1019
1213
  progressPercent.value = 100;
@@ -1024,37 +1218,15 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1024
1218
  );
1025
1219
  await refreshVariants();
1026
1220
  currentMode.value = "result";
1027
- } else if (result.status === "processing" && result.callback_data) {
1028
- processingMessage.value = "Processing in background...";
1029
- await pollForCompletion(result.callback_data, voiceConfig);
1221
+ } else {
1222
+ processingMessage.value = "Generating voiceover...";
1223
+ console.log("[Voice Widget] Async generation started, polling for completion...");
1030
1224
  }
1031
1225
  } catch (error) {
1032
1226
  errorMessage.value = error.message || "Failed to generate voiceover";
1033
1227
  progressPercent.value = 0;
1034
1228
  }
1035
1229
  }
1036
- async function pollForCompletion(callbackData, voiceConfig) {
1037
- const maxAttempts = 60;
1038
- const pollInterval = 5e3;
1039
- for (let i = 0; i < maxAttempts; i++) {
1040
- await new Promise((resolve) => setTimeout(resolve, pollInterval));
1041
- progressPercent.value = Math.min(90, 30 + i / maxAttempts * 60);
1042
- try {
1043
- const variants = await fetchVoiceVariants(props.primaryKey);
1044
- const latest = variants.find((v) => v.callback_data === callbackData);
1045
- if (latest && latest.audio_file_id) {
1046
- generatedAudioId.value = latest.audio_file_id;
1047
- generatedAudioUrl.value = await resolveAudioFileUrl(latest.audio_file_id);
1048
- progressPercent.value = 100;
1049
- currentMode.value = "result";
1050
- return;
1051
- }
1052
- } catch (error) {
1053
- console.error("Poll error:", error);
1054
- }
1055
- }
1056
- errorMessage.value = "Generation timed out. Please try again.";
1057
- }
1058
1230
  function retryGeneration() {
1059
1231
  errorMessage.value = null;
1060
1232
  generateVoiceover();
@@ -1086,37 +1258,6 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1086
1258
  console.error("Failed to delete variant:", error);
1087
1259
  }
1088
1260
  }
1089
- async function startProgressPolling(jobId, voiceConfig) {
1090
- currentJobId.value = jobId;
1091
- isPollingProgress.value = true;
1092
- while (isPollingProgress.value) {
1093
- const job = await pollVoicingJob(jobId);
1094
- if (job) {
1095
- progressPercent.value = job.progress;
1096
- processingMessage.value = job.message;
1097
- progressStatus.value = job.status;
1098
- if (job.status === "completed") {
1099
- isPollingProgress.value = false;
1100
- if (job.audio_file_id) {
1101
- await createVoiceVariant(
1102
- props.primaryKey,
1103
- job.audio_file_id,
1104
- voiceConfig
1105
- );
1106
- }
1107
- await refreshVariants();
1108
- currentMode.value = "result";
1109
- return;
1110
- }
1111
- if (job.status.startsWith("failed")) {
1112
- isPollingProgress.value = false;
1113
- errorMessage.value = job.message || "Generation failed";
1114
- return;
1115
- }
1116
- }
1117
- await new Promise((r) => setTimeout(r, 2e3));
1118
- }
1119
- }
1120
1261
  async function refreshVariants() {
1121
1262
  const variants = await fetchVoiceVariants(props.primaryKey);
1122
1263
  allVariants.value = variants;
@@ -1143,11 +1284,12 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1143
1284
  generatedAudioUrl.value = isUuid ? getAudioUrl(audioFileId) : await resolveAudioFileUrl(audioFileId);
1144
1285
  const config = variant.voice_config;
1145
1286
  if (config) {
1287
+ isRestoringFromVariant.value = true;
1146
1288
  if (config.provider) {
1147
1289
  selectedModel.value = config.provider;
1148
1290
  }
1149
1291
  if (config.voice_id) {
1150
- const matchingVoice = voices.value.find((v) => v.name === config.voice_id);
1292
+ const matchingVoice = voices.value.find((v) => v.voice === config.voice_id || v.name === config.voice_id);
1151
1293
  if (matchingVoice) {
1152
1294
  selectedVoiceId.value = matchingVoice.id;
1153
1295
  }
@@ -1161,19 +1303,27 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1161
1303
  if (typeof config.preprocessing === "boolean") {
1162
1304
  preprocessingEnabled.value = config.preprocessing;
1163
1305
  }
1306
+ isRestoringFromVariant.value = false;
1164
1307
  }
1165
1308
  }
1166
- function regenerateVoiceover() {
1309
+ async function regenerateVoiceover() {
1310
+ const variant = selectedVariant.value;
1311
+ if (variant) {
1312
+ await loadVariantAtIndex(currentVariantIndex.value);
1313
+ }
1167
1314
  generateVoiceover();
1168
1315
  }
1169
1316
  async function confirmVoiceover() {
1170
1317
  const variant = selectedVariant.value;
1171
1318
  if (!variant) return;
1172
1319
  try {
1320
+ if (!props.collection) {
1321
+ throw new Error("collection is required but was not provided");
1322
+ }
1173
1323
  await linkAudioToLesson(
1174
1324
  props.primaryKey,
1175
1325
  variant.audio_file_id,
1176
- props.collection || "SM_Lessons"
1326
+ props.collection
1177
1327
  );
1178
1328
  const value = {
1179
1329
  audio_file_id: variant.audio_file_id,
@@ -1210,40 +1360,92 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1210
1360
  }
1211
1361
  }
1212
1362
  async function initialize() {
1363
+ console.log("[Voice Widget] ===== INITIALIZE START =====");
1364
+ console.log("[Voice Widget] Props:", {
1365
+ collection: props.collection,
1366
+ primaryKey: props.primaryKey,
1367
+ voicesCollection: props.voicesCollection,
1368
+ tonesCollection: props.tonesCollection,
1369
+ stylesCollection: props.stylesCollection,
1370
+ flowId: props.flowId
1371
+ });
1213
1372
  loading.value = true;
1214
1373
  initError.value = null;
1215
1374
  try {
1216
1375
  if (!props.flowId) {
1217
1376
  flowId.value = await fetchFlowId();
1377
+ console.log("[Voice Widget] Fetched flowId:", flowId.value);
1218
1378
  } else {
1219
1379
  flowId.value = props.flowId;
1380
+ console.log("[Voice Widget] Using props flowId:", flowId.value);
1220
1381
  }
1382
+ console.log("[Voice Widget] Fetching voices, tones, styles...");
1221
1383
  const [loadedVoices, loadedTones, loadedStyles] = await Promise.all([
1222
1384
  fetchVoices(props.voicesCollection),
1223
1385
  fetchTones(props.tonesCollection),
1224
1386
  fetchStyles(props.stylesCollection)
1225
1387
  ]);
1388
+ console.log("[Voice Widget] Loaded voices:", loadedVoices.length, loadedVoices);
1389
+ console.log("[Voice Widget] Loaded tones:", loadedTones.length, loadedTones);
1390
+ console.log("[Voice Widget] Loaded styles:", loadedStyles.length, loadedStyles);
1226
1391
  voices.value = loadedVoices;
1227
1392
  tones.value = loadedTones;
1228
1393
  styles.value = loadedStyles;
1394
+ console.log("[Voice Widget] currentVoices (filtered by model):", currentVoices.value.length);
1229
1395
  if (currentVoices.value.length > 0 && !selectedVoiceId.value) {
1230
1396
  selectedVoiceId.value = currentVoices.value[0].id;
1397
+ console.log("[Voice Widget] Auto-selected voice:", selectedVoiceId.value);
1231
1398
  }
1232
1399
  if (tones.value.length > 0 && !selectedToneId.value) {
1233
1400
  selectedToneId.value = tones.value[0].id;
1401
+ console.log("[Voice Widget] Auto-selected tone:", selectedToneId.value);
1234
1402
  }
1235
1403
  if (styles.value.length > 0 && !selectedStyleId.value) {
1236
1404
  selectedStyleId.value = styles.value[0].id;
1405
+ console.log("[Voice Widget] Auto-selected style:", selectedStyleId.value);
1406
+ }
1407
+ const processingVoices = voices.value.filter((v) => v.example_status === "processing");
1408
+ if (processingVoices.length > 0) {
1409
+ console.log("[Voice Widget] Found", processingVoices.length, "voices with processing samples");
1410
+ startSamplePolling();
1237
1411
  }
1238
1412
  if (props.primaryKey) {
1413
+ console.log("[Voice Widget] Checking variants for lesson:", props.primaryKey);
1239
1414
  const variants = await fetchVoiceVariants(props.primaryKey);
1240
- hasExistingVoices.value = variants.length > 0;
1415
+ console.log("[Voice Widget] All variants for lesson:", variants.length);
1416
+ const processingVariants = variants.filter((v) => v.status === "processing" && !v.audio_file_id);
1417
+ if (processingVariants.length > 0) {
1418
+ console.log("[Voice Widget] Found", processingVariants.length, "processing voiceovers, showing progress");
1419
+ currentMode.value = "progress";
1420
+ processingMessage.value = "Generating voiceover...";
1421
+ progressPercent.value = 50;
1422
+ startVoiceoverPolling();
1423
+ } else {
1424
+ const completedVariants = variants.filter((v) => v.audio_file_id);
1425
+ hasExistingVoices.value = completedVariants.length > 0;
1426
+ console.log("[Voice Widget] Completed variants with audio:", completedVariants.length);
1427
+ if (completedVariants.length > 0) {
1428
+ allVariants.value = completedVariants;
1429
+ for (const v of allVariants.value) {
1430
+ if (v.audio_file_id) {
1431
+ const isUuid = v.audio_file_id?.includes("-");
1432
+ v.audioUrl = isUuid ? getAudioUrl(v.audio_file_id) : await resolveAudioFileUrl(v.audio_file_id);
1433
+ }
1434
+ }
1435
+ currentVariantIndex.value = 0;
1436
+ currentMode.value = "result";
1437
+ console.log("[Voice Widget] AUTO-SHOWING RESULT PAGE with", completedVariants.length, "variants");
1438
+ }
1439
+ }
1241
1440
  }
1242
1441
  initFromValue();
1442
+ console.log("[Voice Widget] ===== INITIALIZE SUCCESS =====");
1243
1443
  } catch (error) {
1444
+ console.error("[Voice Widget] ===== INITIALIZE ERROR =====", error);
1244
1445
  initError.value = error.message || "Failed to load configuration";
1245
1446
  } finally {
1246
1447
  loading.value = false;
1448
+ console.log("[Voice Widget] loading set to false, currentMode:", currentMode.value);
1247
1449
  }
1248
1450
  }
1249
1451
  async function initFromValue() {
@@ -1282,12 +1484,20 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1282
1484
  }
1283
1485
  }
1284
1486
  watch(() => props.value, initFromValue, { deep: true });
1487
+ const isRestoringFromVariant = ref(false);
1285
1488
  watch(selectedModel, () => {
1286
- if (currentVoices.value.length > 0) {
1489
+ if (!isRestoringFromVariant.value && currentVoices.value.length > 0) {
1287
1490
  selectedVoiceId.value = currentVoices.value[0].id;
1288
1491
  }
1289
1492
  });
1290
- onMounted(initialize);
1493
+ onMounted(() => {
1494
+ console.log("[Voice Widget] onMounted called");
1495
+ initialize();
1496
+ });
1497
+ onUnmounted(() => {
1498
+ stopSamplePolling();
1499
+ stopVoiceoverPolling();
1500
+ });
1291
1501
  return (_ctx, _cache) => {
1292
1502
  const _component_v_icon = resolveComponent("v-icon");
1293
1503
  return openBlock(), createElementBlock("div", _hoisted_1, [
@@ -1495,8 +1705,8 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1495
1705
  key: voice.id,
1496
1706
  voice,
1497
1707
  "is-selected": selectedVoiceId.value === voice.id,
1498
- loading: generatingSampleFor.value === voice.id,
1499
- "sample-url": voiceSamples.value[voice.id] || (voice.sample_file ? `/assets/${voice.sample_file}` : voice.sample_url),
1708
+ loading: generatingSampleFor.value === voice.id || voice.example_status === "processing",
1709
+ "sample-url": voiceSamples.value[voice.id] || (voice.example ? `/assets/${voice.example}` : voice.sample_url),
1500
1710
  provider: selectedModel.value,
1501
1711
  onSelect: selectVoice,
1502
1712
  onGenerateVoice: generateVoice
@@ -1755,10 +1965,10 @@ var _sfc_main = /* @__PURE__ */ defineComponent({
1755
1965
  }
1756
1966
  });
1757
1967
 
1758
- var css = "\n.voice-widget[data-v-10fc7a16] {\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-10fc7a16] {\n margin-bottom: 20px;\n}\n.widget__header-row[data-v-10fc7a16] {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 16px;\n}\n.widget__header-text[data-v-10fc7a16] {\n flex: 1;\n}\n.widget__title[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16]:hover {\n background: var(--theme--background-accent);\n}\n.widget__subtitle[data-v-10fc7a16] {\n margin: 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__header-controls[data-v-10fc7a16] {\n display: flex;\n gap: 16px;\n align-items: center;\n}\n.widget__url-input[data-v-10fc7a16] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.widget__url-label[data-v-10fc7a16] {\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n white-space: nowrap;\n}\n.widget__url-field[data-v-10fc7a16] {\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-10fc7a16] {\n margin-bottom: 20px;\n}\n.widget__section-label[data-v-10fc7a16] {\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-10fc7a16] {\n display: flex;\n gap: 8px;\n}\n.widget__model-btn[data-v-10fc7a16] {\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-10fc7a16]:hover {\n border-color: var(--theme--primary);\n}\n.widget__model-btn--active[data-v-10fc7a16] {\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-10fc7a16] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.widget__section--toggle[data-v-10fc7a16] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n}\n.widget__toggle[data-v-10fc7a16] {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n.widget__toggle input[data-v-10fc7a16] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__toggle-label[data-v-10fc7a16] {\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__toggle-note[data-v-10fc7a16] {\n margin: 4px 0 0 24px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__footer[data-v-10fc7a16] {\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-10fc7a16],\n.widget__footer-right[data-v-10fc7a16] {\n display: flex;\n gap: 8px;\n}\n.widget__variant-nav[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\n margin-top: 8px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn[data-v-10fc7a16] {\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-10fc7a16]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.widget__btn--primary[data-v-10fc7a16] {\n background: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.widget__btn--primary[data-v-10fc7a16]:hover:not(:disabled) {\n background: var(--theme--primary-accent);\n}\n.widget__btn--secondary[data-v-10fc7a16] {\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-10fc7a16]:hover:not(:disabled) {\n background: var(--theme--background-normal);\n}\n\n/* Processing State */\n.widget__processing[data-v-10fc7a16] {\n padding: 40px 20px;\n text-align: center;\n}\n.widget__progress[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\n height: 100%;\n background: var(--theme--primary);\n transition: width 0.3s ease;\n}\n\n/* Error State */\n.widget__error[data-v-10fc7a16] {\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-10fc7a16] {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n}\n.widget__retry[data-v-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\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-10fc7a16] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__result-info strong[data-v-10fc7a16] {\n color: var(--theme--foreground);\n}\n\n/* Variants List View */\n.widget__variants-list[data-v-10fc7a16] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n max-height: 400px;\n overflow-y: auto;\n margin-bottom: 16px;\n}\n.widget__variant-item[data-v-10fc7a16] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px;\n border: 1px solid var(--theme--border-color-subdued);\n border-radius: var(--theme--border-radius);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.widget__variant-item[data-v-10fc7a16]:hover {\n border-color: var(--theme--primary);\n}\n.widget__variant-item--selected[data-v-10fc7a16] {\n border-color: var(--theme--primary);\n background: var(--theme--primary-background);\n}\n.widget__variant-radio[data-v-10fc7a16] {\n flex-shrink: 0;\n}\n.widget__variant-radio input[data-v-10fc7a16] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__variant-player[data-v-10fc7a16] {\n flex: 1;\n min-width: 200px;\n}\n.widget__variant-meta[data-v-10fc7a16] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 120px;\n}\n.widget__variant-voice[data-v-10fc7a16] {\n font-size: 13px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__variant-date[data-v-10fc7a16] {\n font-size: 11px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn--danger[data-v-10fc7a16] {\n color: var(--theme--danger);\n background: transparent;\n border: none;\n}\n.widget__btn--danger[data-v-10fc7a16]:hover {\n background: var(--theme--danger-background);\n}\n.widget__selected-info[data-v-10fc7a16] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n margin-bottom: 16px;\n}\n.widget__selected-info p[data-v-10fc7a16] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__selected-info strong[data-v-10fc7a16] {\n color: var(--theme--foreground);\n}\n.widget__progress-status[data-v-10fc7a16] {\n text-align: center;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n margin-top: 8px;\n}\n\n/* Animations */\n.spinning[data-v-10fc7a16] {\n animation: spin-10fc7a16 1s linear infinite;\n}\n@keyframes spin-10fc7a16 {\nfrom { transform: rotate(0deg);\n}\nto { transform: rotate(360deg);\n}\n}\n";
1968
+ var css = "\n.voice-widget[data-v-5371543d] {\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-5371543d] {\n margin-bottom: 20px;\n}\n.widget__header-row[data-v-5371543d] {\n display: flex;\n justify-content: space-between;\n align-items: flex-start;\n gap: 16px;\n}\n.widget__header-text[data-v-5371543d] {\n flex: 1;\n}\n.widget__title[data-v-5371543d] {\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-5371543d] {\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-5371543d]:hover {\n background: var(--theme--background-accent);\n}\n.widget__subtitle[data-v-5371543d] {\n margin: 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__header-controls[data-v-5371543d] {\n display: flex;\n gap: 16px;\n align-items: center;\n}\n.widget__url-input[data-v-5371543d] {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.widget__url-label[data-v-5371543d] {\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n white-space: nowrap;\n}\n.widget__url-field[data-v-5371543d] {\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-5371543d] {\n margin-bottom: 20px;\n}\n.widget__section-label[data-v-5371543d] {\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-5371543d] {\n display: flex;\n gap: 8px;\n}\n.widget__model-btn[data-v-5371543d] {\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-5371543d]:hover {\n border-color: var(--theme--primary);\n}\n.widget__model-btn--active[data-v-5371543d] {\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-5371543d] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n.widget__section--toggle[data-v-5371543d] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n}\n.widget__toggle[data-v-5371543d] {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n}\n.widget__toggle input[data-v-5371543d] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__toggle-label[data-v-5371543d] {\n font-size: 14px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__toggle-note[data-v-5371543d] {\n margin: 4px 0 0 24px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__footer[data-v-5371543d] {\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-5371543d],\n.widget__footer-right[data-v-5371543d] {\n display: flex;\n gap: 8px;\n}\n.widget__variant-nav[data-v-5371543d] {\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-5371543d] {\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-5371543d] {\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-5371543d] {\n margin-top: 8px;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn[data-v-5371543d] {\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-5371543d]:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n.widget__btn--primary[data-v-5371543d] {\n background: var(--theme--primary);\n color: var(--theme--primary-foreground, #fff);\n}\n.widget__btn--primary[data-v-5371543d]:hover:not(:disabled) {\n background: var(--theme--primary-accent);\n}\n.widget__btn--secondary[data-v-5371543d] {\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-5371543d]:hover:not(:disabled) {\n background: var(--theme--background-normal);\n}\n\n/* Processing State */\n.widget__processing[data-v-5371543d] {\n padding: 40px 20px;\n text-align: center;\n}\n.widget__progress[data-v-5371543d] {\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-5371543d] {\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-5371543d] {\n height: 100%;\n background: var(--theme--primary);\n transition: width 0.3s ease;\n}\n\n/* Error State */\n.widget__error[data-v-5371543d] {\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-5371543d] {\n display: flex;\n gap: 8px;\n margin-top: 8px;\n}\n.widget__retry[data-v-5371543d] {\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-5371543d] {\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-5371543d] {\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-5371543d] {\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-5371543d] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__result-info strong[data-v-5371543d] {\n color: var(--theme--foreground);\n}\n\n/* Variants List View */\n.widget__variants-list[data-v-5371543d] {\n display: flex;\n flex-direction: column;\n gap: 8px;\n max-height: 400px;\n overflow-y: auto;\n margin-bottom: 16px;\n}\n.widget__variant-item[data-v-5371543d] {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px;\n border: 1px solid var(--theme--border-color-subdued);\n border-radius: var(--theme--border-radius);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n.widget__variant-item[data-v-5371543d]:hover {\n border-color: var(--theme--primary);\n}\n.widget__variant-item--selected[data-v-5371543d] {\n border-color: var(--theme--primary);\n background: var(--theme--primary-background);\n}\n.widget__variant-radio[data-v-5371543d] {\n flex-shrink: 0;\n}\n.widget__variant-radio input[data-v-5371543d] {\n width: 16px;\n height: 16px;\n cursor: pointer;\n}\n.widget__variant-player[data-v-5371543d] {\n flex: 1;\n min-width: 200px;\n}\n.widget__variant-meta[data-v-5371543d] {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 120px;\n}\n.widget__variant-voice[data-v-5371543d] {\n font-size: 13px;\n font-weight: 500;\n color: var(--theme--foreground);\n}\n.widget__variant-date[data-v-5371543d] {\n font-size: 11px;\n color: var(--theme--foreground-subdued);\n}\n.widget__btn--danger[data-v-5371543d] {\n color: var(--theme--danger);\n background: transparent;\n border: none;\n}\n.widget__btn--danger[data-v-5371543d]:hover {\n background: var(--theme--danger-background);\n}\n.widget__selected-info[data-v-5371543d] {\n padding: 12px;\n background: var(--theme--background-subdued);\n border-radius: var(--theme--border-radius);\n margin-bottom: 16px;\n}\n.widget__selected-info p[data-v-5371543d] {\n margin: 4px 0;\n font-size: 14px;\n color: var(--theme--foreground-subdued);\n}\n.widget__selected-info strong[data-v-5371543d] {\n color: var(--theme--foreground);\n}\n.widget__progress-status[data-v-5371543d] {\n text-align: center;\n font-size: 12px;\n color: var(--theme--foreground-subdued);\n margin-top: 8px;\n}\n\n/* Animations */\n.spinning[data-v-5371543d] {\n animation: spin-5371543d 1s linear infinite;\n}\n@keyframes spin-5371543d {\nfrom { transform: rotate(0deg);\n}\nto { transform: rotate(360deg);\n}\n}\n";
1759
1969
  n(css,{});
1760
1970
 
1761
- var InterfaceComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-10fc7a16"], ["__file", "interface.vue"]]);
1971
+ var InterfaceComponent = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-5371543d"], ["__file", "interface.vue"]]);
1762
1972
 
1763
1973
  var index = defineInterface({
1764
1974
  id: "voice-widget",
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stellartech/voice-widget-directus",
3
3
  "description": "Voice generation widget with model/voice selection and audio preview for Directus",
4
4
  "icon": "mic",
5
- "version": "1.0.2",
5
+ "version": "1.0.4",
6
6
  "license": "MIT",
7
7
  "readme": "README.md",
8
8
  "repository": {