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