@speechos/client 0.2.8 → 0.2.10

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 (40) hide show
  1. package/dist/config.d.ts +13 -0
  2. package/dist/config.d.ts.map +1 -1
  3. package/dist/index.cjs +694 -117
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.iife.js +715 -120
  8. package/dist/index.iife.js.map +1 -1
  9. package/dist/index.iife.min.js +136 -103
  10. package/dist/index.iife.min.js.map +1 -1
  11. package/dist/index.js +694 -119
  12. package/dist/index.js.map +1 -1
  13. package/dist/settings-sync.d.ts +66 -0
  14. package/dist/settings-sync.d.ts.map +1 -0
  15. package/dist/settings-sync.test.d.ts +5 -0
  16. package/dist/settings-sync.test.d.ts.map +1 -0
  17. package/dist/speechos.d.ts.map +1 -1
  18. package/dist/stores/language-settings.d.ts +12 -1
  19. package/dist/stores/language-settings.d.ts.map +1 -1
  20. package/dist/stores/language-settings.test.d.ts +5 -0
  21. package/dist/stores/language-settings.test.d.ts.map +1 -0
  22. package/dist/stores/snippets-store.d.ts +12 -1
  23. package/dist/stores/snippets-store.d.ts.map +1 -1
  24. package/dist/stores/transcript-store.d.ts +13 -2
  25. package/dist/stores/transcript-store.d.ts.map +1 -1
  26. package/dist/stores/vocabulary-store.d.ts +12 -1
  27. package/dist/stores/vocabulary-store.d.ts.map +1 -1
  28. package/dist/ui/index.d.ts.map +1 -1
  29. package/dist/ui/styles/modal-styles.d.ts.map +1 -1
  30. package/dist/ui/styles/theme.d.ts.map +1 -1
  31. package/dist/ui/tabs/history-tab.d.ts +2 -0
  32. package/dist/ui/tabs/history-tab.d.ts.map +1 -1
  33. package/dist/ui/tabs/settings-tab.d.ts +1 -0
  34. package/dist/ui/tabs/settings-tab.d.ts.map +1 -1
  35. package/dist/ui/tabs/snippets-tab.d.ts +2 -0
  36. package/dist/ui/tabs/snippets-tab.d.ts.map +1 -1
  37. package/dist/ui/tabs/vocabulary-tab.d.ts +2 -0
  38. package/dist/ui/tabs/vocabulary-tab.d.ts.map +1 -1
  39. package/dist/ui/widget.d.ts.map +1 -1
  40. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { state, events, getConfig, getBackend, setConfig, updateUserId } from '@speechos/core';
1
+ import { state, events, getConfig, getSettingsToken, clearSettingsToken, getBackend, setConfig, updateUserId } from '@speechos/core';
2
2
  export { DEFAULT_HOST, events, getConfig, livekit, resetConfig, setConfig, state } from '@speechos/core';
3
3
 
4
4
  /**
@@ -12,6 +12,7 @@ const defaultClientConfig = {
12
12
  commands: [],
13
13
  zIndex: 999999,
14
14
  alwaysVisible: false,
15
+ useExternalSettings: false,
15
16
  };
16
17
  /**
17
18
  * Current client configuration singleton
@@ -27,6 +28,7 @@ function validateClientConfig(config) {
27
28
  commands: config.commands ?? defaultClientConfig.commands,
28
29
  zIndex: config.zIndex ?? defaultClientConfig.zIndex,
29
30
  alwaysVisible: config.alwaysVisible ?? defaultClientConfig.alwaysVisible,
31
+ useExternalSettings: config.useExternalSettings ?? defaultClientConfig.useExternalSettings,
30
32
  };
31
33
  // Validate zIndex
32
34
  if (typeof resolved.zIndex !== "number" || resolved.zIndex < 0) {
@@ -78,6 +80,12 @@ function getZIndex() {
78
80
  function isAlwaysVisible() {
79
81
  return currentClientConfig.alwaysVisible;
80
82
  }
83
+ /**
84
+ * Check if external settings page should be used
85
+ */
86
+ function useExternalSettings() {
87
+ return currentClientConfig.useExternalSettings;
88
+ }
81
89
 
82
90
  /**
83
91
  * Form field focus detection for SpeechOS Client SDK
@@ -481,15 +489,23 @@ function resetTextInputHandler() {
481
489
  * Persists input language preferences to localStorage
482
490
  */
483
491
  const STORAGE_KEY$4 = "speechos_language_settings";
492
+ /**
493
+ * In-memory cache for language settings. When server sync is enabled, this is the
494
+ * source of truth. localStorage is only used when server sync is disabled.
495
+ */
496
+ let memoryCache$3 = null;
484
497
  /**
485
498
  * Supported input languages for speech recognition
486
499
  * Each language has a name, primary code, and available variants
487
500
  * Sorted alphabetically by name for dropdown display
488
501
  */
489
502
  const SUPPORTED_LANGUAGES = [
503
+ { name: "Belarusian", code: "be", variants: ["be"] },
504
+ { name: "Bengali", code: "bn", variants: ["bn"] },
505
+ { name: "Bosnian", code: "bs", variants: ["bs"] },
490
506
  { name: "Bulgarian", code: "bg", variants: ["bg"] },
491
507
  { name: "Catalan", code: "ca", variants: ["ca"] },
492
- { name: "Chinese", code: "zh", variants: ["zh"] },
508
+ { name: "Croatian", code: "hr", variants: ["hr"] },
493
509
  { name: "Czech", code: "cs", variants: ["cs"] },
494
510
  { name: "Danish", code: "da", variants: ["da", "da-DK"] },
495
511
  { name: "Dutch", code: "nl", variants: ["nl"] },
@@ -510,18 +526,26 @@ const SUPPORTED_LANGUAGES = [
510
526
  { name: "Indonesian", code: "id", variants: ["id"] },
511
527
  { name: "Italian", code: "it", variants: ["it"] },
512
528
  { name: "Japanese", code: "ja", variants: ["ja"] },
529
+ { name: "Kannada", code: "kn", variants: ["kn"] },
513
530
  { name: "Korean", code: "ko", variants: ["ko", "ko-KR"] },
514
531
  { name: "Latvian", code: "lv", variants: ["lv"] },
515
532
  { name: "Lithuanian", code: "lt", variants: ["lt"] },
533
+ { name: "Macedonian", code: "mk", variants: ["mk"] },
516
534
  { name: "Malay", code: "ms", variants: ["ms"] },
535
+ { name: "Marathi", code: "mr", variants: ["mr"] },
517
536
  { name: "Norwegian", code: "no", variants: ["no"] },
518
537
  { name: "Polish", code: "pl", variants: ["pl"] },
519
538
  { name: "Portuguese", code: "pt", variants: ["pt", "pt-BR", "pt-PT"] },
520
539
  { name: "Romanian", code: "ro", variants: ["ro"] },
521
540
  { name: "Russian", code: "ru", variants: ["ru"] },
541
+ { name: "Serbian", code: "sr", variants: ["sr"] },
522
542
  { name: "Slovak", code: "sk", variants: ["sk"] },
543
+ { name: "Slovenian", code: "sl", variants: ["sl"] },
523
544
  { name: "Spanish", code: "es", variants: ["es", "es-419"] },
524
545
  { name: "Swedish", code: "sv", variants: ["sv", "sv-SE"] },
546
+ { name: "Tagalog", code: "tl", variants: ["tl"] },
547
+ { name: "Tamil", code: "ta", variants: ["ta"] },
548
+ { name: "Telugu", code: "te", variants: ["te"] },
525
549
  { name: "Turkish", code: "tr", variants: ["tr"] },
526
550
  { name: "Ukrainian", code: "uk", variants: ["uk"] },
527
551
  { name: "Vietnamese", code: "vi", variants: ["vi"] },
@@ -532,9 +556,13 @@ const defaultSettings$1 = {
532
556
  smartFormat: true,
533
557
  };
534
558
  /**
535
- * Get current language settings from localStorage
559
+ * Get current language settings. Prefers in-memory cache (from server sync),
560
+ * then falls back to localStorage.
536
561
  */
537
562
  function getLanguageSettings() {
563
+ if (memoryCache$3 !== null) {
564
+ return { ...memoryCache$3 };
565
+ }
538
566
  try {
539
567
  const stored = localStorage.getItem(STORAGE_KEY$4);
540
568
  if (!stored)
@@ -546,14 +574,27 @@ function getLanguageSettings() {
546
574
  }
547
575
  }
548
576
  /**
549
- * Save language settings to localStorage
577
+ * Set language settings directly (used by settings sync from server data).
578
+ */
579
+ function setLanguageSettings(settings) {
580
+ memoryCache$3 = { ...defaultSettings$1, ...settings };
581
+ }
582
+ /**
583
+ * Reset memory cache (for testing only)
584
+ */
585
+ function resetMemoryCache$2() {
586
+ memoryCache$3 = null;
587
+ }
588
+ /**
589
+ * Save language settings (updates memory cache and tries localStorage)
550
590
  */
551
591
  function saveLanguageSettings(settings) {
592
+ memoryCache$3 = settings;
552
593
  try {
553
594
  localStorage.setItem(STORAGE_KEY$4, JSON.stringify(settings));
554
595
  }
555
596
  catch {
556
- // localStorage full or unavailable - silently fail
597
+ // localStorage full or unavailable - memory cache still updated
557
598
  }
558
599
  }
559
600
  /**
@@ -657,6 +698,7 @@ function getLanguageName() {
657
698
  * Reset language settings to defaults
658
699
  */
659
700
  function resetLanguageSettings() {
701
+ memoryCache$3 = null;
660
702
  try {
661
703
  localStorage.removeItem(STORAGE_KEY$4);
662
704
  }
@@ -666,6 +708,7 @@ function resetLanguageSettings() {
666
708
  }
667
709
  const languageSettings = {
668
710
  getLanguageSettings,
711
+ setLanguageSettings,
669
712
  getInputLanguageCode,
670
713
  setInputLanguageCode,
671
714
  getOutputLanguageCode,
@@ -676,6 +719,7 @@ const languageSettings = {
676
719
  getSmartFormatEnabled,
677
720
  setSmartFormatEnabled,
678
721
  resetLanguageSettings,
722
+ resetMemoryCache: resetMemoryCache$2,
679
723
  SUPPORTED_LANGUAGES,
680
724
  // Legacy aliases
681
725
  getLanguageCode,
@@ -691,6 +735,11 @@ const STORAGE_KEY$3 = "speechos_snippets";
691
735
  const MAX_SNIPPETS = 25;
692
736
  const MAX_TRIGGER_LENGTH = 30;
693
737
  const MAX_EXPANSION_LENGTH = 300;
738
+ /**
739
+ * In-memory cache for snippets. When server sync is enabled, this is the
740
+ * source of truth. localStorage is only used when server sync is disabled.
741
+ */
742
+ let memoryCache$2 = null;
694
743
  /**
695
744
  * Generate a unique ID for snippet entries
696
745
  */
@@ -698,15 +747,18 @@ function generateId$2() {
698
747
  return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
699
748
  }
700
749
  /**
701
- * Get all snippets from localStorage
750
+ * Get all snippets. Prefers in-memory cache (from server sync),
751
+ * then falls back to localStorage.
702
752
  */
703
753
  function getSnippets() {
754
+ if (memoryCache$2 !== null) {
755
+ return [...memoryCache$2].sort((a, b) => b.createdAt - a.createdAt);
756
+ }
704
757
  try {
705
758
  const stored = localStorage.getItem(STORAGE_KEY$3);
706
759
  if (!stored)
707
760
  return [];
708
761
  const entries = JSON.parse(stored);
709
- // Return newest first
710
762
  return entries.sort((a, b) => b.createdAt - a.createdAt);
711
763
  }
712
764
  catch {
@@ -714,14 +766,27 @@ function getSnippets() {
714
766
  }
715
767
  }
716
768
  /**
717
- * Save snippets to localStorage
769
+ * Set snippets directly (used by settings sync from server data).
770
+ */
771
+ function setSnippets(snippets) {
772
+ memoryCache$2 = snippets.slice(0, MAX_SNIPPETS);
773
+ }
774
+ /**
775
+ * Reset memory cache (for testing only)
776
+ */
777
+ function resetMemoryCache$1() {
778
+ memoryCache$2 = null;
779
+ }
780
+ /**
781
+ * Save snippets (updates memory cache and tries localStorage)
718
782
  */
719
783
  function saveSnippets(snippets) {
784
+ memoryCache$2 = snippets;
720
785
  try {
721
786
  localStorage.setItem(STORAGE_KEY$3, JSON.stringify(snippets));
722
787
  }
723
788
  catch {
724
- // localStorage full or unavailable - silently fail
789
+ // localStorage full or unavailable - memory cache still updated
725
790
  }
726
791
  }
727
792
  /**
@@ -839,13 +904,14 @@ function deleteSnippet(id) {
839
904
  * Clear all snippets
840
905
  */
841
906
  function clearSnippets() {
907
+ memoryCache$2 = [];
842
908
  try {
843
909
  localStorage.removeItem(STORAGE_KEY$3);
844
- events.emit("settings:changed", { setting: "snippets" });
845
910
  }
846
911
  catch {
847
912
  // Silently fail
848
913
  }
914
+ events.emit("settings:changed", { setting: "snippets" });
849
915
  }
850
916
  /**
851
917
  * Get snippet count info
@@ -861,12 +927,14 @@ function isAtSnippetLimit() {
861
927
  }
862
928
  const snippetsStore = {
863
929
  getSnippets,
930
+ setSnippets,
864
931
  addSnippet,
865
932
  updateSnippet,
866
933
  deleteSnippet,
867
934
  clearSnippets,
868
935
  getSnippetCount,
869
936
  isAtSnippetLimit,
937
+ resetMemoryCache: resetMemoryCache$1,
870
938
  MAX_SNIPPETS,
871
939
  MAX_TRIGGER_LENGTH,
872
940
  MAX_EXPANSION_LENGTH,
@@ -879,6 +947,11 @@ const snippetsStore = {
879
947
  const STORAGE_KEY$2 = "speechos_vocabulary";
880
948
  const MAX_TERMS = 50;
881
949
  const MAX_TERM_LENGTH = 50;
950
+ /**
951
+ * In-memory cache for vocabulary. When server sync is enabled, this is the
952
+ * source of truth. localStorage is only used when server sync is disabled.
953
+ */
954
+ let memoryCache$1 = null;
882
955
  /**
883
956
  * Generate a unique ID for vocabulary entries
884
957
  */
@@ -886,15 +959,18 @@ function generateId$1() {
886
959
  return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
887
960
  }
888
961
  /**
889
- * Get all vocabulary terms from localStorage
962
+ * Get all vocabulary terms. Prefers in-memory cache (from server sync),
963
+ * then falls back to localStorage.
890
964
  */
891
965
  function getVocabulary() {
966
+ if (memoryCache$1 !== null) {
967
+ return [...memoryCache$1].sort((a, b) => b.createdAt - a.createdAt);
968
+ }
892
969
  try {
893
970
  const stored = localStorage.getItem(STORAGE_KEY$2);
894
971
  if (!stored)
895
972
  return [];
896
973
  const entries = JSON.parse(stored);
897
- // Return newest first
898
974
  return entries.sort((a, b) => b.createdAt - a.createdAt);
899
975
  }
900
976
  catch {
@@ -902,14 +978,27 @@ function getVocabulary() {
902
978
  }
903
979
  }
904
980
  /**
905
- * Save vocabulary to localStorage
981
+ * Set vocabulary directly (used by settings sync from server data).
982
+ */
983
+ function setVocabulary(terms) {
984
+ memoryCache$1 = terms.slice(0, MAX_TERMS);
985
+ }
986
+ /**
987
+ * Reset memory cache (for testing only)
988
+ */
989
+ function resetMemoryCache() {
990
+ memoryCache$1 = null;
991
+ }
992
+ /**
993
+ * Save vocabulary (updates memory cache and tries localStorage)
906
994
  */
907
995
  function saveVocabulary(terms) {
996
+ memoryCache$1 = terms;
908
997
  try {
909
998
  localStorage.setItem(STORAGE_KEY$2, JSON.stringify(terms));
910
999
  }
911
1000
  catch {
912
- // localStorage full or unavailable - silently fail
1001
+ // localStorage full or unavailable - memory cache still updated
913
1002
  }
914
1003
  }
915
1004
  /**
@@ -977,13 +1066,14 @@ function deleteTerm(id) {
977
1066
  * Clear all vocabulary
978
1067
  */
979
1068
  function clearVocabulary() {
1069
+ memoryCache$1 = [];
980
1070
  try {
981
1071
  localStorage.removeItem(STORAGE_KEY$2);
982
- events.emit("settings:changed", { setting: "vocabulary" });
983
1072
  }
984
1073
  catch {
985
1074
  // Silently fail
986
1075
  }
1076
+ events.emit("settings:changed", { setting: "vocabulary" });
987
1077
  }
988
1078
  /**
989
1079
  * Get vocabulary count info
@@ -999,11 +1089,13 @@ function isAtVocabularyLimit() {
999
1089
  }
1000
1090
  const vocabularyStore = {
1001
1091
  getVocabulary,
1092
+ setVocabulary,
1002
1093
  addTerm,
1003
1094
  deleteTerm,
1004
1095
  clearVocabulary,
1005
1096
  getVocabularyCount,
1006
1097
  isAtVocabularyLimit,
1098
+ resetMemoryCache,
1007
1099
  MAX_TERMS,
1008
1100
  MAX_TERM_LENGTH,
1009
1101
  };
@@ -1101,6 +1193,451 @@ const audioSettings = {
1101
1193
  resetAudioSettings,
1102
1194
  };
1103
1195
 
1196
+ /**
1197
+ * Transcript history store
1198
+ * Persists transcripts to localStorage for viewing in the settings modal
1199
+ */
1200
+ const STORAGE_KEY = "speechos_transcripts";
1201
+ const MAX_ENTRIES = 50;
1202
+ /**
1203
+ * In-memory cache for transcripts. When server sync is enabled, this is the
1204
+ * source of truth. localStorage is only used when server sync is disabled.
1205
+ */
1206
+ let memoryCache = null;
1207
+ /**
1208
+ * Generate a unique ID for transcript entries
1209
+ */
1210
+ function generateId() {
1211
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1212
+ }
1213
+ /**
1214
+ * Get all transcripts. Prefers in-memory cache (from server sync),
1215
+ * then falls back to localStorage.
1216
+ */
1217
+ function getTranscripts() {
1218
+ // If we have in-memory data (from server sync), use it
1219
+ if (memoryCache !== null) {
1220
+ return [...memoryCache].sort((a, b) => b.timestamp - a.timestamp);
1221
+ }
1222
+ // Fall back to localStorage (when server sync is disabled)
1223
+ try {
1224
+ const stored = localStorage.getItem(STORAGE_KEY);
1225
+ if (!stored)
1226
+ return [];
1227
+ const entries = JSON.parse(stored);
1228
+ return entries.sort((a, b) => b.timestamp - a.timestamp);
1229
+ }
1230
+ catch {
1231
+ return [];
1232
+ }
1233
+ }
1234
+ /**
1235
+ * Set transcripts directly (used by settings sync from server data).
1236
+ * Server data is the source of truth - just update memory cache.
1237
+ */
1238
+ function setTranscripts(entries) {
1239
+ memoryCache = entries.slice(0, MAX_ENTRIES);
1240
+ }
1241
+ /**
1242
+ * Save a new transcript entry
1243
+ */
1244
+ function saveTranscript(text, action, originalTextOrOptions) {
1245
+ const entry = {
1246
+ id: generateId(),
1247
+ text,
1248
+ timestamp: Date.now(),
1249
+ action,
1250
+ };
1251
+ // Handle edit action with originalText string
1252
+ if (action === "edit" && typeof originalTextOrOptions === "string") {
1253
+ entry.originalText = originalTextOrOptions;
1254
+ }
1255
+ // Handle command action with options object
1256
+ if (action === "command" && typeof originalTextOrOptions === "object") {
1257
+ const options = originalTextOrOptions;
1258
+ if (options.inputText !== undefined)
1259
+ entry.inputText = options.inputText;
1260
+ if (options.commandResult !== undefined)
1261
+ entry.commandResult = options.commandResult;
1262
+ if (options.commandConfig !== undefined)
1263
+ entry.commandConfig = options.commandConfig;
1264
+ }
1265
+ const entries = getTranscripts();
1266
+ entries.unshift(entry);
1267
+ // Prune to max entries
1268
+ const pruned = entries.slice(0, MAX_ENTRIES);
1269
+ // Update memory cache (always)
1270
+ memoryCache = pruned;
1271
+ // Try to persist to localStorage (for when server sync is disabled)
1272
+ try {
1273
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(pruned));
1274
+ }
1275
+ catch {
1276
+ // Quota exceeded - memory cache is still updated
1277
+ }
1278
+ // Emit settings change event to trigger sync
1279
+ events.emit("settings:changed", { setting: "history" });
1280
+ return entry;
1281
+ }
1282
+ /**
1283
+ * Clear all transcript history
1284
+ */
1285
+ function clearTranscripts() {
1286
+ memoryCache = [];
1287
+ try {
1288
+ localStorage.removeItem(STORAGE_KEY);
1289
+ }
1290
+ catch {
1291
+ // Silently fail
1292
+ }
1293
+ events.emit("settings:changed", { setting: "history" });
1294
+ }
1295
+ /**
1296
+ * Delete a single transcript by ID
1297
+ */
1298
+ function deleteTranscript(id) {
1299
+ const entries = getTranscripts().filter((e) => e.id !== id);
1300
+ memoryCache = entries;
1301
+ try {
1302
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
1303
+ }
1304
+ catch {
1305
+ // Silently fail
1306
+ }
1307
+ events.emit("settings:changed", { setting: "history" });
1308
+ }
1309
+ const transcriptStore = {
1310
+ getTranscripts,
1311
+ setTranscripts,
1312
+ saveTranscript,
1313
+ clearTranscripts,
1314
+ deleteTranscript,
1315
+ };
1316
+
1317
+ /**
1318
+ * Settings sync manager
1319
+ * Syncs user settings (language, vocabulary, snippets, history) with the server
1320
+ */
1321
+ // Sync debounce delay in milliseconds
1322
+ const SYNC_DEBOUNCE_MS = 2000;
1323
+ // Maximum retry attempts
1324
+ const MAX_RETRIES = 3;
1325
+ // Base retry delay in milliseconds (exponential backoff)
1326
+ const BASE_RETRY_DELAY_MS = 2000;
1327
+ /**
1328
+ * Settings sync manager singleton
1329
+ */
1330
+ class SettingsSync {
1331
+ constructor() {
1332
+ this.syncTimer = null;
1333
+ this.isSyncing = false;
1334
+ this.retryCount = 0;
1335
+ this.isInitialized = false;
1336
+ this.unsubscribe = null;
1337
+ /** When true, sync is disabled due to CSP or network restrictions */
1338
+ this.syncDisabled = false;
1339
+ }
1340
+ /**
1341
+ * Make a fetch request using native fetch.
1342
+ */
1343
+ async doFetch(url, options) {
1344
+ const config = getConfig();
1345
+ if (config.debug) {
1346
+ console.log("[SpeechOS] Using native fetch", options.method, url);
1347
+ }
1348
+ return fetch(url, options);
1349
+ }
1350
+ /**
1351
+ * Initialize the settings sync manager
1352
+ * If a settingsToken is configured, loads settings from server
1353
+ */
1354
+ async init() {
1355
+ const token = getSettingsToken();
1356
+ if (!token) {
1357
+ // No token configured, sync is disabled
1358
+ return;
1359
+ }
1360
+ if (this.isInitialized) {
1361
+ return;
1362
+ }
1363
+ this.isInitialized = true;
1364
+ // Subscribe to settings changes
1365
+ this.unsubscribe = events.on("settings:changed", () => {
1366
+ this.scheduleSyncToServer();
1367
+ });
1368
+ // Load settings from server
1369
+ await this.loadFromServer();
1370
+ }
1371
+ /**
1372
+ * Stop the sync manager and clean up
1373
+ */
1374
+ destroy() {
1375
+ if (this.syncTimer) {
1376
+ clearTimeout(this.syncTimer);
1377
+ this.syncTimer = null;
1378
+ }
1379
+ if (this.unsubscribe) {
1380
+ this.unsubscribe();
1381
+ this.unsubscribe = null;
1382
+ }
1383
+ this.isInitialized = false;
1384
+ this.retryCount = 0;
1385
+ this.syncDisabled = false;
1386
+ }
1387
+ /**
1388
+ * Load settings from the server and merge with local
1389
+ */
1390
+ async loadFromServer() {
1391
+ const token = getSettingsToken();
1392
+ if (!token) {
1393
+ return;
1394
+ }
1395
+ const config = getConfig();
1396
+ try {
1397
+ const response = await this.doFetch(`${config.host}/api/user-settings/`, {
1398
+ method: "GET",
1399
+ headers: {
1400
+ Authorization: `Bearer ${token}`,
1401
+ "Content-Type": "application/json",
1402
+ },
1403
+ });
1404
+ if (config.debug) {
1405
+ console.log("[SpeechOS] Settings fetch response:", response.status, response.ok ? "OK" : response.statusText);
1406
+ }
1407
+ if (response.status === 404) {
1408
+ // No settings on server yet (new user) - sync local settings to server
1409
+ if (config.debug) {
1410
+ console.log("[SpeechOS] No server settings found, syncing local to server");
1411
+ }
1412
+ await this.syncToServer();
1413
+ return;
1414
+ }
1415
+ if (response.status === 401 || response.status === 403) {
1416
+ // Token expired or invalid
1417
+ this.handleTokenExpired();
1418
+ return;
1419
+ }
1420
+ if (!response.ok) {
1421
+ throw new Error(`Server returned ${response.status}`);
1422
+ }
1423
+ const serverSettings = (await response.json());
1424
+ if (config.debug) {
1425
+ console.log("[SpeechOS] Settings received from server:", {
1426
+ language: serverSettings.language,
1427
+ vocabularyCount: serverSettings.vocabulary?.length ?? 0,
1428
+ snippetsCount: serverSettings.snippets?.length ?? 0,
1429
+ historyCount: serverSettings.history?.length ?? 0,
1430
+ lastSyncedAt: serverSettings.lastSyncedAt,
1431
+ });
1432
+ if (serverSettings.history?.length > 0) {
1433
+ console.log("[SpeechOS] History entries:", serverSettings.history);
1434
+ }
1435
+ }
1436
+ this.mergeSettings(serverSettings);
1437
+ events.emit("settings:loaded", undefined);
1438
+ if (config.debug) {
1439
+ console.log("[SpeechOS] Settings merged and loaded");
1440
+ }
1441
+ }
1442
+ catch (error) {
1443
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1444
+ // Check if this is a CSP/network restriction - disable sync permanently for this session
1445
+ if (this.isNetworkRestrictionError(error)) {
1446
+ this.syncDisabled = true;
1447
+ if (config.debug) {
1448
+ console.log("[SpeechOS] Settings sync disabled (CSP/network restriction), using localStorage only");
1449
+ }
1450
+ }
1451
+ else if (config.debug) {
1452
+ console.warn("[SpeechOS] Failed to load settings from server:", errorMessage);
1453
+ }
1454
+ events.emit("settings:syncFailed", { error: errorMessage });
1455
+ // Continue with local settings on error
1456
+ }
1457
+ }
1458
+ /**
1459
+ * Merge server settings with local (server wins).
1460
+ * Uses store setters to update memory cache - localStorage is a fallback.
1461
+ */
1462
+ mergeSettings(serverSettings) {
1463
+ // Language settings - server wins
1464
+ if (serverSettings.language) {
1465
+ setLanguageSettings(serverSettings.language);
1466
+ }
1467
+ // Vocabulary - server wins
1468
+ if (serverSettings.vocabulary) {
1469
+ setVocabulary(serverSettings.vocabulary);
1470
+ }
1471
+ // Snippets - server wins
1472
+ if (serverSettings.snippets) {
1473
+ setSnippets(serverSettings.snippets);
1474
+ }
1475
+ // History - server wins
1476
+ if (serverSettings.history) {
1477
+ setTranscripts(serverSettings.history);
1478
+ }
1479
+ }
1480
+ /**
1481
+ * Schedule a debounced sync to server
1482
+ */
1483
+ scheduleSyncToServer() {
1484
+ const token = getSettingsToken();
1485
+ if (!token || this.syncDisabled) {
1486
+ return;
1487
+ }
1488
+ // Cancel any pending sync
1489
+ if (this.syncTimer) {
1490
+ clearTimeout(this.syncTimer);
1491
+ }
1492
+ // Schedule new sync
1493
+ this.syncTimer = setTimeout(() => {
1494
+ this.syncToServer();
1495
+ }, SYNC_DEBOUNCE_MS);
1496
+ }
1497
+ /**
1498
+ * Sync current settings to server
1499
+ */
1500
+ async syncToServer() {
1501
+ const token = getSettingsToken();
1502
+ if (!token || this.isSyncing || this.syncDisabled) {
1503
+ return;
1504
+ }
1505
+ this.isSyncing = true;
1506
+ const config = getConfig();
1507
+ try {
1508
+ const languageSettings = getLanguageSettings();
1509
+ const vocabulary = getVocabulary();
1510
+ const snippets = getSnippets();
1511
+ const transcripts = getTranscripts();
1512
+ const payload = {
1513
+ language: {
1514
+ inputLanguageCode: languageSettings.inputLanguageCode,
1515
+ outputLanguageCode: languageSettings.outputLanguageCode,
1516
+ smartFormat: languageSettings.smartFormat,
1517
+ },
1518
+ vocabulary: vocabulary.map((v) => ({
1519
+ id: v.id,
1520
+ term: v.term,
1521
+ createdAt: v.createdAt,
1522
+ })),
1523
+ snippets: snippets.map((s) => ({
1524
+ id: s.id,
1525
+ trigger: s.trigger,
1526
+ expansion: s.expansion,
1527
+ createdAt: s.createdAt,
1528
+ })),
1529
+ // Sync history (excluding commandConfig to reduce payload size)
1530
+ history: transcripts.map((t) => ({
1531
+ id: t.id,
1532
+ text: t.text,
1533
+ timestamp: t.timestamp,
1534
+ action: t.action,
1535
+ ...(t.originalText && { originalText: t.originalText }),
1536
+ ...(t.inputText && { inputText: t.inputText }),
1537
+ ...(t.commandResult !== undefined && { commandResult: t.commandResult }),
1538
+ })),
1539
+ };
1540
+ const response = await this.doFetch(`${config.host}/api/user-settings/`, {
1541
+ method: "PUT",
1542
+ headers: {
1543
+ Authorization: `Bearer ${token}`,
1544
+ "Content-Type": "application/json",
1545
+ },
1546
+ body: JSON.stringify(payload),
1547
+ });
1548
+ if (response.status === 401 || response.status === 403) {
1549
+ this.handleTokenExpired();
1550
+ return;
1551
+ }
1552
+ if (!response.ok) {
1553
+ throw new Error(`Server returned ${response.status}`);
1554
+ }
1555
+ // Reset retry count on success
1556
+ this.retryCount = 0;
1557
+ events.emit("settings:synced", undefined);
1558
+ if (config.debug) {
1559
+ console.log("[SpeechOS] Settings synced to server");
1560
+ }
1561
+ }
1562
+ catch (error) {
1563
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1564
+ // Check if this is a CSP/network restriction - disable sync permanently for this session
1565
+ if (this.isNetworkRestrictionError(error)) {
1566
+ this.syncDisabled = true;
1567
+ if (config.debug) {
1568
+ console.log("[SpeechOS] Settings sync disabled (CSP/network restriction), using localStorage only");
1569
+ }
1570
+ events.emit("settings:syncFailed", { error: errorMessage });
1571
+ // Don't retry - CSP errors are permanent
1572
+ }
1573
+ else {
1574
+ if (config.debug) {
1575
+ console.warn("[SpeechOS] Failed to sync settings to server:", errorMessage);
1576
+ }
1577
+ events.emit("settings:syncFailed", { error: errorMessage });
1578
+ // Retry with exponential backoff (only for transient errors)
1579
+ this.scheduleRetry();
1580
+ }
1581
+ }
1582
+ finally {
1583
+ this.isSyncing = false;
1584
+ }
1585
+ }
1586
+ /**
1587
+ * Schedule a retry with exponential backoff
1588
+ */
1589
+ scheduleRetry() {
1590
+ if (this.retryCount >= MAX_RETRIES) {
1591
+ const config = getConfig();
1592
+ if (config.debug) {
1593
+ console.warn("[SpeechOS] Max retries reached, giving up sync");
1594
+ }
1595
+ this.retryCount = 0;
1596
+ return;
1597
+ }
1598
+ this.retryCount++;
1599
+ const delay = BASE_RETRY_DELAY_MS * Math.pow(2, this.retryCount - 1);
1600
+ this.syncTimer = setTimeout(() => {
1601
+ this.syncToServer();
1602
+ }, delay);
1603
+ }
1604
+ /**
1605
+ * Check if an error is a CSP or network restriction error
1606
+ * These errors are permanent and shouldn't trigger retries
1607
+ */
1608
+ isNetworkRestrictionError(error) {
1609
+ if (error instanceof TypeError) {
1610
+ const message = error.message.toLowerCase();
1611
+ // Common CSP/network error messages
1612
+ return (message.includes("failed to fetch") ||
1613
+ message.includes("network request failed") ||
1614
+ message.includes("content security policy") ||
1615
+ message.includes("csp") ||
1616
+ message.includes("blocked"));
1617
+ }
1618
+ return false;
1619
+ }
1620
+ /**
1621
+ * Handle token expiration
1622
+ */
1623
+ handleTokenExpired() {
1624
+ clearSettingsToken();
1625
+ events.emit("settings:tokenExpired", undefined);
1626
+ const config = getConfig();
1627
+ if (config.debug) {
1628
+ console.warn("[SpeechOS] Settings token expired");
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Check if sync is enabled (token is configured)
1633
+ */
1634
+ isEnabled() {
1635
+ return !!getSettingsToken();
1636
+ }
1637
+ }
1638
+ // Singleton instance
1639
+ const settingsSync = new SettingsSync();
1640
+
1104
1641
  /******************************************************************************
1105
1642
  Copyright (c) Microsoft Corporation.
1106
1643
 
@@ -1184,6 +1721,9 @@ const t$1=t=>(e,o)=>{ void 0!==o?o.addInitializer(()=>{customElements.define(t,e
1184
1721
  */
1185
1722
  const themeStyles = i$4 `
1186
1723
  :host {
1724
+ /* Font stack - system fonts for consistent rendering across sites */
1725
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
1726
+
1187
1727
  /* Color tokens */
1188
1728
  --speechos-primary: #10B981;
1189
1729
  --speechos-primary-hover: #059669;
@@ -1307,100 +1847,6 @@ i$4 `
1307
1847
  }
1308
1848
  `;
1309
1849
 
1310
- /**
1311
- * Transcript history store
1312
- * Persists transcripts to localStorage for viewing in the settings modal
1313
- */
1314
- const STORAGE_KEY = "speechos_transcripts";
1315
- const MAX_ENTRIES = 50;
1316
- /**
1317
- * Generate a unique ID for transcript entries
1318
- */
1319
- function generateId() {
1320
- return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1321
- }
1322
- /**
1323
- * Get all transcripts from localStorage
1324
- */
1325
- function getTranscripts() {
1326
- try {
1327
- const stored = localStorage.getItem(STORAGE_KEY);
1328
- if (!stored)
1329
- return [];
1330
- const entries = JSON.parse(stored);
1331
- // Return newest first
1332
- return entries.sort((a, b) => b.timestamp - a.timestamp);
1333
- }
1334
- catch {
1335
- return [];
1336
- }
1337
- }
1338
- /**
1339
- * Save a new transcript entry
1340
- */
1341
- function saveTranscript(text, action, originalTextOrOptions) {
1342
- const entry = {
1343
- id: generateId(),
1344
- text,
1345
- timestamp: Date.now(),
1346
- action,
1347
- };
1348
- // Handle edit action with originalText string
1349
- if (action === "edit" && typeof originalTextOrOptions === "string") {
1350
- entry.originalText = originalTextOrOptions;
1351
- }
1352
- // Handle command action with options object
1353
- if (action === "command" && typeof originalTextOrOptions === "object") {
1354
- const options = originalTextOrOptions;
1355
- if (options.inputText !== undefined)
1356
- entry.inputText = options.inputText;
1357
- if (options.commandResult !== undefined)
1358
- entry.commandResult = options.commandResult;
1359
- if (options.commandConfig !== undefined)
1360
- entry.commandConfig = options.commandConfig;
1361
- }
1362
- const entries = getTranscripts();
1363
- entries.unshift(entry);
1364
- // Prune to max entries
1365
- const pruned = entries.slice(0, MAX_ENTRIES);
1366
- try {
1367
- localStorage.setItem(STORAGE_KEY, JSON.stringify(pruned));
1368
- }
1369
- catch {
1370
- // localStorage full or unavailable - silently fail
1371
- }
1372
- return entry;
1373
- }
1374
- /**
1375
- * Clear all transcript history
1376
- */
1377
- function clearTranscripts() {
1378
- try {
1379
- localStorage.removeItem(STORAGE_KEY);
1380
- }
1381
- catch {
1382
- // Silently fail
1383
- }
1384
- }
1385
- /**
1386
- * Delete a single transcript by ID
1387
- */
1388
- function deleteTranscript(id) {
1389
- const entries = getTranscripts().filter((e) => e.id !== id);
1390
- try {
1391
- localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
1392
- }
1393
- catch {
1394
- // Silently fail
1395
- }
1396
- }
1397
- const transcriptStore = {
1398
- getTranscripts: getTranscripts,
1399
- saveTranscript: saveTranscript,
1400
- clearTranscripts: clearTranscripts,
1401
- deleteTranscript: deleteTranscript,
1402
- };
1403
-
1404
1850
  function isNativeField(field) {
1405
1851
  return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement;
1406
1852
  }
@@ -3398,6 +3844,8 @@ const modalLayoutStyles = i$4 `
3398
3844
  inset: 0;
3399
3845
  pointer-events: none;
3400
3846
  z-index: calc(var(--speechos-z-base) + 100);
3847
+ /* Ensure consistent font rendering across all sites */
3848
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
3401
3849
  }
3402
3850
 
3403
3851
  .modal-overlay {
@@ -3886,6 +4334,7 @@ let SpeechOSHistoryTab = class SpeechOSHistoryTab extends i$1 {
3886
4334
  constructor() {
3887
4335
  super(...arguments);
3888
4336
  this.transcripts = [];
4337
+ this.unsubscribeSettingsLoaded = null;
3889
4338
  }
3890
4339
  static { this.styles = [
3891
4340
  themeStyles,
@@ -4022,11 +4471,36 @@ let SpeechOSHistoryTab = class SpeechOSHistoryTab extends i$1 {
4022
4471
  background: rgba(239, 68, 68, 0.18);
4023
4472
  border-color: rgba(239, 68, 68, 0.25);
4024
4473
  }
4474
+
4475
+ .command-matched {
4476
+ font-size: 12px;
4477
+ color: rgba(255, 255, 255, 0.5);
4478
+ margin-top: 6px;
4479
+ }
4480
+
4481
+ .command-matched code {
4482
+ background: rgba(245, 158, 11, 0.15);
4483
+ color: #fbbf24;
4484
+ padding: 2px 6px;
4485
+ border-radius: 4px;
4486
+ font-family: monospace;
4487
+ }
4025
4488
  `,
4026
4489
  ]; }
4027
4490
  connectedCallback() {
4028
4491
  super.connectedCallback();
4029
4492
  this.loadTranscripts();
4493
+ // Refresh when settings are loaded from the server (history may have changed)
4494
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
4495
+ this.loadTranscripts();
4496
+ });
4497
+ }
4498
+ disconnectedCallback() {
4499
+ super.disconnectedCallback();
4500
+ if (this.unsubscribeSettingsLoaded) {
4501
+ this.unsubscribeSettingsLoaded();
4502
+ this.unsubscribeSettingsLoaded = null;
4503
+ }
4030
4504
  }
4031
4505
  /** Reload transcripts from store */
4032
4506
  refresh() {
@@ -4097,7 +4571,13 @@ let SpeechOSHistoryTab = class SpeechOSHistoryTab extends i$1 {
4097
4571
  renderCommandDetails(entry) {
4098
4572
  // Show the transcript text (what the user said)
4099
4573
  const displayText = entry.inputText || entry.text;
4100
- return b `<div class="transcript-text">${displayText}</div>`;
4574
+ const commandName = entry.commandResult?.name;
4575
+ return b `
4576
+ <div class="transcript-text">${displayText}</div>
4577
+ ${commandName
4578
+ ? b `<div class="command-matched">Matched: <code>${commandName}</code></div>`
4579
+ : null}
4580
+ `;
4101
4581
  }
4102
4582
  getCopyText(entry) {
4103
4583
  if (entry.action === "command") {
@@ -4647,6 +5127,7 @@ let SpeechOSSettingsTab = class SpeechOSSettingsTab extends i$1 {
4647
5127
  this.permissionGranted = false;
4648
5128
  this.smartFormatEnabled = true;
4649
5129
  this.savedIndicatorTimeout = null;
5130
+ this.unsubscribeSettingsLoaded = null;
4650
5131
  }
4651
5132
  static { this.styles = [
4652
5133
  themeStyles,
@@ -4740,6 +5221,16 @@ let SpeechOSSettingsTab = class SpeechOSSettingsTab extends i$1 {
4740
5221
  padding: 8px;
4741
5222
  }
4742
5223
 
5224
+ .settings-select:disabled {
5225
+ opacity: 0.4;
5226
+ cursor: not-allowed;
5227
+ }
5228
+
5229
+ .settings-select:disabled:hover {
5230
+ border-color: rgba(255, 255, 255, 0.08);
5231
+ background: rgba(0, 0, 0, 0.3);
5232
+ }
5233
+
4743
5234
  .settings-select-arrow {
4744
5235
  position: absolute;
4745
5236
  right: 14px;
@@ -4821,12 +5312,20 @@ let SpeechOSSettingsTab = class SpeechOSSettingsTab extends i$1 {
4821
5312
  connectedCallback() {
4822
5313
  super.connectedCallback();
4823
5314
  this.loadSettings();
5315
+ // Refresh when settings are loaded from the server
5316
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
5317
+ this.loadSettings();
5318
+ });
4824
5319
  }
4825
5320
  disconnectedCallback() {
4826
5321
  super.disconnectedCallback();
4827
5322
  if (this.savedIndicatorTimeout) {
4828
5323
  clearTimeout(this.savedIndicatorTimeout);
4829
5324
  }
5325
+ if (this.unsubscribeSettingsLoaded) {
5326
+ this.unsubscribeSettingsLoaded();
5327
+ this.unsubscribeSettingsLoaded = null;
5328
+ }
4830
5329
  this.isTestingMic = false;
4831
5330
  }
4832
5331
  /** Called when tab becomes active */
@@ -4913,13 +5412,14 @@ let SpeechOSSettingsTab = class SpeechOSSettingsTab extends i$1 {
4913
5412
  setSmartFormatEnabled(this.smartFormatEnabled);
4914
5413
  this.showSaved();
4915
5414
  }
4916
- renderLanguageSelector(selectedCode, onChange) {
5415
+ renderLanguageSelector(selectedCode, onChange, disabled = false) {
4917
5416
  return b `
4918
5417
  <div class="settings-select-wrapper">
4919
5418
  <select
4920
5419
  class="settings-select"
4921
5420
  .value="${selectedCode}"
4922
5421
  @change="${onChange}"
5422
+ ?disabled="${disabled}"
4923
5423
  >
4924
5424
  ${SUPPORTED_LANGUAGES.map((lang) => b `
4925
5425
  <option
@@ -5037,7 +5537,7 @@ let SpeechOSSettingsTab = class SpeechOSSettingsTab extends i$1 {
5037
5537
  <div class="settings-section-description">
5038
5538
  AI automatically removes filler words, adds punctuation, and polishes
5039
5539
  your text. Disable for raw transcription output. Note: disabling also
5040
- turns off text snippets.
5540
+ turns off text snippets and output language translation.
5041
5541
  </div>
5042
5542
  <div class="settings-toggle-row">
5043
5543
  <span class="settings-toggle-label">Enable AI formatting</span>
@@ -5078,9 +5578,13 @@ let SpeechOSSettingsTab = class SpeechOSSettingsTab extends i$1 {
5078
5578
  </div>
5079
5579
  <div class="settings-section-description">
5080
5580
  The language for your transcribed text. Usually the same as input, but
5081
- can differ for translation.
5581
+ can differ for translation.${!this.smartFormatEnabled
5582
+ ? " Requires Smart Format to be enabled."
5583
+ : ""}
5082
5584
  </div>
5083
- ${this.renderLanguageSelector(this.selectedOutputLanguageCode, this.handleOutputLanguageChange.bind(this))}
5585
+ ${this.renderLanguageSelector(this.smartFormatEnabled
5586
+ ? this.selectedOutputLanguageCode
5587
+ : this.selectedInputLanguageCode, this.handleOutputLanguageChange.bind(this), !this.smartFormatEnabled)}
5084
5588
  </div>
5085
5589
  `;
5086
5590
  }
@@ -5125,6 +5629,7 @@ let SpeechOSSnippetsTab = class SpeechOSSnippetsTab extends i$1 {
5125
5629
  this.trigger = "";
5126
5630
  this.expansion = "";
5127
5631
  this.error = "";
5632
+ this.unsubscribeSettingsLoaded = null;
5128
5633
  }
5129
5634
  static { this.styles = [
5130
5635
  themeStyles,
@@ -5230,6 +5735,17 @@ let SpeechOSSnippetsTab = class SpeechOSSnippetsTab extends i$1 {
5230
5735
  connectedCallback() {
5231
5736
  super.connectedCallback();
5232
5737
  this.loadSnippets();
5738
+ // Refresh when settings are loaded from the server
5739
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
5740
+ this.loadSnippets();
5741
+ });
5742
+ }
5743
+ disconnectedCallback() {
5744
+ super.disconnectedCallback();
5745
+ if (this.unsubscribeSettingsLoaded) {
5746
+ this.unsubscribeSettingsLoaded();
5747
+ this.unsubscribeSettingsLoaded = null;
5748
+ }
5233
5749
  }
5234
5750
  /** Reload snippets from store */
5235
5751
  refresh() {
@@ -5472,6 +5988,7 @@ let SpeechOSVocabularyTab = class SpeechOSVocabularyTab extends i$1 {
5472
5988
  this.showForm = false;
5473
5989
  this.term = "";
5474
5990
  this.error = "";
5991
+ this.unsubscribeSettingsLoaded = null;
5475
5992
  }
5476
5993
  static { this.styles = [
5477
5994
  themeStyles,
@@ -5524,6 +6041,17 @@ let SpeechOSVocabularyTab = class SpeechOSVocabularyTab extends i$1 {
5524
6041
  connectedCallback() {
5525
6042
  super.connectedCallback();
5526
6043
  this.loadVocabulary();
6044
+ // Refresh when settings are loaded from the server
6045
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
6046
+ this.loadVocabulary();
6047
+ });
6048
+ }
6049
+ disconnectedCallback() {
6050
+ super.disconnectedCallback();
6051
+ if (this.unsubscribeSettingsLoaded) {
6052
+ this.unsubscribeSettingsLoaded();
6053
+ this.unsubscribeSettingsLoaded = null;
6054
+ }
5527
6055
  }
5528
6056
  /** Reload vocabulary from store */
5529
6057
  refresh() {
@@ -7048,7 +7576,14 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7048
7576
  state.hide();
7049
7577
  }
7050
7578
  handleSettingsClick() {
7051
- this.settingsOpen = true;
7579
+ if (useExternalSettings()) {
7580
+ const host = getConfig().host;
7581
+ const fullUrl = `${host}/a/extension-settings`;
7582
+ window.open(fullUrl, '_blank', 'noopener,noreferrer');
7583
+ }
7584
+ else {
7585
+ this.settingsOpen = true;
7586
+ }
7052
7587
  }
7053
7588
  handleDragStart(e) {
7054
7589
  if (e.button !== 0)
@@ -7589,10 +8124,17 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7589
8124
  this.editSelectionStart = null;
7590
8125
  this.editSelectionEnd = null;
7591
8126
  this.editSelectedText = "";
7592
- // Open settings modal
7593
- this.settingsOpen = true;
8127
+ // Open settings - either external URL or modal
8128
+ if (useExternalSettings()) {
8129
+ const host = getConfig().host;
8130
+ const fullUrl = `${host}/a/extension-settings`;
8131
+ window.open(fullUrl, '_blank', 'noopener,noreferrer');
8132
+ }
8133
+ else {
8134
+ this.settingsOpen = true;
8135
+ }
7594
8136
  if (getConfig().debug) {
7595
- console.log("[SpeechOS] Settings modal opened from no-audio warning");
8137
+ console.log("[SpeechOS] Settings opened from no-audio warning", { useExternalSettings: useExternalSettings() });
7596
8138
  }
7597
8139
  await disconnectPromise;
7598
8140
  }
@@ -7801,6 +8343,30 @@ SpeechOSWidget = SpeechOSWidget_1 = __decorate([
7801
8343
  t$1("speechos-widget")
7802
8344
  ], SpeechOSWidget);
7803
8345
 
8346
+ /**
8347
+ * UI module exports
8348
+ * Lit-based Shadow DOM components
8349
+ */
8350
+ // Patch customElements.define to silently ignore duplicate registrations for speechos-* elements.
8351
+ // This prevents errors when the extension loads on a page that already has SpeechOS.
8352
+ // The patch is scoped to only affect speechos-* tags to avoid unintended effects on host pages.
8353
+ const originalDefine = customElements.define.bind(customElements);
8354
+ customElements.define = (name, constructor, options) => {
8355
+ // Only intercept speechos-* elements
8356
+ if (name.startsWith("speechos-")) {
8357
+ if (customElements.get(name) === undefined) {
8358
+ originalDefine(name, constructor, options);
8359
+ }
8360
+ // Skip silently if already registered
8361
+ }
8362
+ else {
8363
+ // Pass through for non-speechos elements
8364
+ originalDefine(name, constructor, options);
8365
+ }
8366
+ };
8367
+ // Restore original customElements.define after our components are registered
8368
+ customElements.define = originalDefine;
8369
+
7804
8370
  /**
7805
8371
  * Main SpeechOS Client SDK class
7806
8372
  * Composes core logic with UI components
@@ -7882,6 +8448,13 @@ class SpeechOS {
7882
8448
  state.show();
7883
8449
  }
7884
8450
  this.isInitialized = true;
8451
+ // Initialize settings sync if token is configured
8452
+ // This loads settings from server and subscribes to changes
8453
+ settingsSync.init().catch((error) => {
8454
+ if (finalConfig.debug) {
8455
+ console.warn("[SpeechOS] Settings sync initialization failed:", error);
8456
+ }
8457
+ });
7885
8458
  // Log initialization in debug mode
7886
8459
  if (finalConfig.debug) {
7887
8460
  console.log("[SpeechOS] Initialized with config:", finalConfig);
@@ -7912,6 +8485,8 @@ class SpeechOS {
7912
8485
  resetClientConfig();
7913
8486
  // Reset text input handler to default
7914
8487
  resetTextInputHandler();
8488
+ // Stop settings sync
8489
+ settingsSync.destroy();
7915
8490
  // Clear instance
7916
8491
  this.instance = null;
7917
8492
  this.isInitialized = false;
@@ -8073,5 +8648,5 @@ class SpeechOS {
8073
8648
  // Version
8074
8649
  const VERSION = "0.1.0";
8075
8650
 
8076
- export { DefaultTextInputHandler, FormDetector, SUPPORTED_LANGUAGES, SpeechOS, VERSION, addSnippet, addTerm, audioSettings, clearSnippets, clearTranscripts, clearVocabulary, SpeechOS as default, defaultTextInputHandler, deleteSnippet, deleteTerm, deleteTranscript, formDetector, getAudioDeviceId, getAudioInputDevices, getAudioSettings, getClientConfig, getCommands, getInputLanguageCode, getInputLanguageName, getLanguageByCode, getLanguageCode, getLanguageName, getLanguageSettings, getOutputLanguageCode, getOutputLanguageName, getSessionSettings, getSmartFormatEnabled, getSnippetCount, getSnippets, getTextInputHandler, getTranscripts, getVocabulary, getVocabularyCount, getZIndex, hasCommands, isAtSnippetLimit, isAtVocabularyLimit, isSelectedDeviceAvailable, languageSettings, resetAudioSettings, resetClientConfig, resetLanguageSettings, resetTextInputHandler, saveTranscript, setAudioDeviceId, setClientConfig, setInputLanguageCode, setLanguageCode, setOutputLanguageCode, setSmartFormatEnabled, setTextInputHandler, snippetsStore, transcriptStore, updateSnippet, vocabularyStore };
8651
+ export { DefaultTextInputHandler, FormDetector, SUPPORTED_LANGUAGES, SpeechOS, VERSION, addSnippet, addTerm, audioSettings, clearSnippets, clearTranscripts, clearVocabulary, SpeechOS as default, defaultTextInputHandler, deleteSnippet, deleteTerm, deleteTranscript, formDetector, getAudioDeviceId, getAudioInputDevices, getAudioSettings, getClientConfig, getCommands, getInputLanguageCode, getInputLanguageName, getLanguageByCode, getLanguageCode, getLanguageName, getLanguageSettings, getOutputLanguageCode, getOutputLanguageName, getSessionSettings, getSmartFormatEnabled, getSnippetCount, getSnippets, getTextInputHandler, getTranscripts, getVocabulary, getVocabularyCount, getZIndex, hasCommands, isAtSnippetLimit, isAtVocabularyLimit, isSelectedDeviceAvailable, languageSettings, resetAudioSettings, resetClientConfig, resetLanguageSettings, resetTextInputHandler, saveTranscript, setAudioDeviceId, setClientConfig, setInputLanguageCode, setLanguageCode, setOutputLanguageCode, setSmartFormatEnabled, setTextInputHandler, settingsSync, snippetsStore, transcriptStore, updateSnippet, useExternalSettings, vocabularyStore };
8077
8652
  //# sourceMappingURL=index.js.map