@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
@@ -26911,7 +26911,8 @@
26911
26911
  userId: "",
26912
26912
  host: DEFAULT_HOST,
26913
26913
  debug: false,
26914
- webSocketFactory: void 0
26914
+ webSocketFactory: void 0,
26915
+ settingsToken: void 0
26915
26916
  };
26916
26917
  /**
26917
26918
  * Validates and merges user config with defaults
@@ -26919,13 +26920,14 @@
26919
26920
  * @returns Validated and merged configuration
26920
26921
  */
26921
26922
  function validateConfig(userConfig) {
26922
- if (!userConfig.apiKey) throw new Error("SpeechOS requires an apiKey. Get one from your team dashboard at /a/<team-slug>/.");
26923
+ if (!userConfig.apiKey) throw new Error("SpeechOS requires an apiKey. Get one from your team dashboard at /a/.");
26923
26924
  return {
26924
26925
  apiKey: userConfig.apiKey,
26925
26926
  userId: userConfig.userId ?? defaultConfig.userId,
26926
26927
  host: userConfig.host ?? defaultConfig.host,
26927
26928
  debug: userConfig.debug ?? defaultConfig.debug,
26928
- webSocketFactory: userConfig.webSocketFactory ?? defaultConfig.webSocketFactory
26929
+ webSocketFactory: userConfig.webSocketFactory ?? defaultConfig.webSocketFactory,
26930
+ settingsToken: userConfig.settingsToken ?? defaultConfig.settingsToken
26929
26931
  };
26930
26932
  }
26931
26933
  /**
@@ -26962,6 +26964,22 @@
26962
26964
  };
26963
26965
  }
26964
26966
  /**
26967
+ * Get the settings token from the current configuration
26968
+ * @returns The settings token or undefined if not configured
26969
+ */
26970
+ function getSettingsToken() {
26971
+ return currentConfig.settingsToken;
26972
+ }
26973
+ /**
26974
+ * Clear the settings token (e.g., when it expires)
26975
+ */
26976
+ function clearSettingsToken() {
26977
+ currentConfig = {
26978
+ ...currentConfig,
26979
+ settingsToken: void 0
26980
+ };
26981
+ }
26982
+ /**
26965
26983
  * LocalStorage key for anonymous ID persistence
26966
26984
  */
26967
26985
  const ANONYMOUS_ID_KEY = "speechos_anonymous_id";
@@ -28778,6 +28796,7 @@
28778
28796
  commands: [],
28779
28797
  zIndex: 999999,
28780
28798
  alwaysVisible: false,
28799
+ useExternalSettings: false,
28781
28800
  };
28782
28801
  /**
28783
28802
  * Current client configuration singleton
@@ -28793,6 +28812,7 @@
28793
28812
  commands: config.commands ?? defaultClientConfig.commands,
28794
28813
  zIndex: config.zIndex ?? defaultClientConfig.zIndex,
28795
28814
  alwaysVisible: config.alwaysVisible ?? defaultClientConfig.alwaysVisible,
28815
+ useExternalSettings: config.useExternalSettings ?? defaultClientConfig.useExternalSettings,
28796
28816
  };
28797
28817
  // Validate zIndex
28798
28818
  if (typeof resolved.zIndex !== "number" || resolved.zIndex < 0) {
@@ -28844,6 +28864,12 @@
28844
28864
  function isAlwaysVisible() {
28845
28865
  return currentClientConfig.alwaysVisible;
28846
28866
  }
28867
+ /**
28868
+ * Check if external settings page should be used
28869
+ */
28870
+ function useExternalSettings() {
28871
+ return currentClientConfig.useExternalSettings;
28872
+ }
28847
28873
 
28848
28874
  /**
28849
28875
  * Form field focus detection for SpeechOS Client SDK
@@ -29247,15 +29273,23 @@
29247
29273
  * Persists input language preferences to localStorage
29248
29274
  */
29249
29275
  const STORAGE_KEY$4 = "speechos_language_settings";
29276
+ /**
29277
+ * In-memory cache for language settings. When server sync is enabled, this is the
29278
+ * source of truth. localStorage is only used when server sync is disabled.
29279
+ */
29280
+ let memoryCache$3 = null;
29250
29281
  /**
29251
29282
  * Supported input languages for speech recognition
29252
29283
  * Each language has a name, primary code, and available variants
29253
29284
  * Sorted alphabetically by name for dropdown display
29254
29285
  */
29255
29286
  const SUPPORTED_LANGUAGES = [
29287
+ { name: "Belarusian", code: "be", variants: ["be"] },
29288
+ { name: "Bengali", code: "bn", variants: ["bn"] },
29289
+ { name: "Bosnian", code: "bs", variants: ["bs"] },
29256
29290
  { name: "Bulgarian", code: "bg", variants: ["bg"] },
29257
29291
  { name: "Catalan", code: "ca", variants: ["ca"] },
29258
- { name: "Chinese", code: "zh", variants: ["zh"] },
29292
+ { name: "Croatian", code: "hr", variants: ["hr"] },
29259
29293
  { name: "Czech", code: "cs", variants: ["cs"] },
29260
29294
  { name: "Danish", code: "da", variants: ["da", "da-DK"] },
29261
29295
  { name: "Dutch", code: "nl", variants: ["nl"] },
@@ -29276,18 +29310,26 @@
29276
29310
  { name: "Indonesian", code: "id", variants: ["id"] },
29277
29311
  { name: "Italian", code: "it", variants: ["it"] },
29278
29312
  { name: "Japanese", code: "ja", variants: ["ja"] },
29313
+ { name: "Kannada", code: "kn", variants: ["kn"] },
29279
29314
  { name: "Korean", code: "ko", variants: ["ko", "ko-KR"] },
29280
29315
  { name: "Latvian", code: "lv", variants: ["lv"] },
29281
29316
  { name: "Lithuanian", code: "lt", variants: ["lt"] },
29317
+ { name: "Macedonian", code: "mk", variants: ["mk"] },
29282
29318
  { name: "Malay", code: "ms", variants: ["ms"] },
29319
+ { name: "Marathi", code: "mr", variants: ["mr"] },
29283
29320
  { name: "Norwegian", code: "no", variants: ["no"] },
29284
29321
  { name: "Polish", code: "pl", variants: ["pl"] },
29285
29322
  { name: "Portuguese", code: "pt", variants: ["pt", "pt-BR", "pt-PT"] },
29286
29323
  { name: "Romanian", code: "ro", variants: ["ro"] },
29287
29324
  { name: "Russian", code: "ru", variants: ["ru"] },
29325
+ { name: "Serbian", code: "sr", variants: ["sr"] },
29288
29326
  { name: "Slovak", code: "sk", variants: ["sk"] },
29327
+ { name: "Slovenian", code: "sl", variants: ["sl"] },
29289
29328
  { name: "Spanish", code: "es", variants: ["es", "es-419"] },
29290
29329
  { name: "Swedish", code: "sv", variants: ["sv", "sv-SE"] },
29330
+ { name: "Tagalog", code: "tl", variants: ["tl"] },
29331
+ { name: "Tamil", code: "ta", variants: ["ta"] },
29332
+ { name: "Telugu", code: "te", variants: ["te"] },
29291
29333
  { name: "Turkish", code: "tr", variants: ["tr"] },
29292
29334
  { name: "Ukrainian", code: "uk", variants: ["uk"] },
29293
29335
  { name: "Vietnamese", code: "vi", variants: ["vi"] },
@@ -29298,9 +29340,13 @@
29298
29340
  smartFormat: true,
29299
29341
  };
29300
29342
  /**
29301
- * Get current language settings from localStorage
29343
+ * Get current language settings. Prefers in-memory cache (from server sync),
29344
+ * then falls back to localStorage.
29302
29345
  */
29303
29346
  function getLanguageSettings() {
29347
+ if (memoryCache$3 !== null) {
29348
+ return { ...memoryCache$3 };
29349
+ }
29304
29350
  try {
29305
29351
  const stored = localStorage.getItem(STORAGE_KEY$4);
29306
29352
  if (!stored)
@@ -29312,14 +29358,27 @@
29312
29358
  }
29313
29359
  }
29314
29360
  /**
29315
- * Save language settings to localStorage
29361
+ * Set language settings directly (used by settings sync from server data).
29362
+ */
29363
+ function setLanguageSettings(settings) {
29364
+ memoryCache$3 = { ...defaultSettings$1, ...settings };
29365
+ }
29366
+ /**
29367
+ * Reset memory cache (for testing only)
29368
+ */
29369
+ function resetMemoryCache$2() {
29370
+ memoryCache$3 = null;
29371
+ }
29372
+ /**
29373
+ * Save language settings (updates memory cache and tries localStorage)
29316
29374
  */
29317
29375
  function saveLanguageSettings(settings) {
29376
+ memoryCache$3 = settings;
29318
29377
  try {
29319
29378
  localStorage.setItem(STORAGE_KEY$4, JSON.stringify(settings));
29320
29379
  }
29321
29380
  catch {
29322
- // localStorage full or unavailable - silently fail
29381
+ // localStorage full or unavailable - memory cache still updated
29323
29382
  }
29324
29383
  }
29325
29384
  /**
@@ -29423,6 +29482,7 @@
29423
29482
  * Reset language settings to defaults
29424
29483
  */
29425
29484
  function resetLanguageSettings() {
29485
+ memoryCache$3 = null;
29426
29486
  try {
29427
29487
  localStorage.removeItem(STORAGE_KEY$4);
29428
29488
  }
@@ -29432,6 +29492,7 @@
29432
29492
  }
29433
29493
  const languageSettings = {
29434
29494
  getLanguageSettings,
29495
+ setLanguageSettings,
29435
29496
  getInputLanguageCode,
29436
29497
  setInputLanguageCode,
29437
29498
  getOutputLanguageCode,
@@ -29442,6 +29503,7 @@
29442
29503
  getSmartFormatEnabled,
29443
29504
  setSmartFormatEnabled,
29444
29505
  resetLanguageSettings,
29506
+ resetMemoryCache: resetMemoryCache$2,
29445
29507
  SUPPORTED_LANGUAGES,
29446
29508
  // Legacy aliases
29447
29509
  getLanguageCode,
@@ -29457,6 +29519,11 @@
29457
29519
  const MAX_SNIPPETS = 25;
29458
29520
  const MAX_TRIGGER_LENGTH = 30;
29459
29521
  const MAX_EXPANSION_LENGTH = 300;
29522
+ /**
29523
+ * In-memory cache for snippets. When server sync is enabled, this is the
29524
+ * source of truth. localStorage is only used when server sync is disabled.
29525
+ */
29526
+ let memoryCache$2 = null;
29460
29527
  /**
29461
29528
  * Generate a unique ID for snippet entries
29462
29529
  */
@@ -29464,15 +29531,18 @@
29464
29531
  return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
29465
29532
  }
29466
29533
  /**
29467
- * Get all snippets from localStorage
29534
+ * Get all snippets. Prefers in-memory cache (from server sync),
29535
+ * then falls back to localStorage.
29468
29536
  */
29469
29537
  function getSnippets() {
29538
+ if (memoryCache$2 !== null) {
29539
+ return [...memoryCache$2].sort((a, b) => b.createdAt - a.createdAt);
29540
+ }
29470
29541
  try {
29471
29542
  const stored = localStorage.getItem(STORAGE_KEY$3);
29472
29543
  if (!stored)
29473
29544
  return [];
29474
29545
  const entries = JSON.parse(stored);
29475
- // Return newest first
29476
29546
  return entries.sort((a, b) => b.createdAt - a.createdAt);
29477
29547
  }
29478
29548
  catch {
@@ -29480,14 +29550,27 @@
29480
29550
  }
29481
29551
  }
29482
29552
  /**
29483
- * Save snippets to localStorage
29553
+ * Set snippets directly (used by settings sync from server data).
29554
+ */
29555
+ function setSnippets(snippets) {
29556
+ memoryCache$2 = snippets.slice(0, MAX_SNIPPETS);
29557
+ }
29558
+ /**
29559
+ * Reset memory cache (for testing only)
29560
+ */
29561
+ function resetMemoryCache$1() {
29562
+ memoryCache$2 = null;
29563
+ }
29564
+ /**
29565
+ * Save snippets (updates memory cache and tries localStorage)
29484
29566
  */
29485
29567
  function saveSnippets(snippets) {
29568
+ memoryCache$2 = snippets;
29486
29569
  try {
29487
29570
  localStorage.setItem(STORAGE_KEY$3, JSON.stringify(snippets));
29488
29571
  }
29489
29572
  catch {
29490
- // localStorage full or unavailable - silently fail
29573
+ // localStorage full or unavailable - memory cache still updated
29491
29574
  }
29492
29575
  }
29493
29576
  /**
@@ -29605,13 +29688,14 @@
29605
29688
  * Clear all snippets
29606
29689
  */
29607
29690
  function clearSnippets() {
29691
+ memoryCache$2 = [];
29608
29692
  try {
29609
29693
  localStorage.removeItem(STORAGE_KEY$3);
29610
- events.emit("settings:changed", { setting: "snippets" });
29611
29694
  }
29612
29695
  catch {
29613
29696
  // Silently fail
29614
29697
  }
29698
+ events.emit("settings:changed", { setting: "snippets" });
29615
29699
  }
29616
29700
  /**
29617
29701
  * Get snippet count info
@@ -29627,12 +29711,14 @@
29627
29711
  }
29628
29712
  const snippetsStore = {
29629
29713
  getSnippets,
29714
+ setSnippets,
29630
29715
  addSnippet,
29631
29716
  updateSnippet,
29632
29717
  deleteSnippet,
29633
29718
  clearSnippets,
29634
29719
  getSnippetCount,
29635
29720
  isAtSnippetLimit,
29721
+ resetMemoryCache: resetMemoryCache$1,
29636
29722
  MAX_SNIPPETS,
29637
29723
  MAX_TRIGGER_LENGTH,
29638
29724
  MAX_EXPANSION_LENGTH,
@@ -29645,6 +29731,11 @@
29645
29731
  const STORAGE_KEY$2 = "speechos_vocabulary";
29646
29732
  const MAX_TERMS = 50;
29647
29733
  const MAX_TERM_LENGTH = 50;
29734
+ /**
29735
+ * In-memory cache for vocabulary. When server sync is enabled, this is the
29736
+ * source of truth. localStorage is only used when server sync is disabled.
29737
+ */
29738
+ let memoryCache$1 = null;
29648
29739
  /**
29649
29740
  * Generate a unique ID for vocabulary entries
29650
29741
  */
@@ -29652,15 +29743,18 @@
29652
29743
  return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
29653
29744
  }
29654
29745
  /**
29655
- * Get all vocabulary terms from localStorage
29746
+ * Get all vocabulary terms. Prefers in-memory cache (from server sync),
29747
+ * then falls back to localStorage.
29656
29748
  */
29657
29749
  function getVocabulary() {
29750
+ if (memoryCache$1 !== null) {
29751
+ return [...memoryCache$1].sort((a, b) => b.createdAt - a.createdAt);
29752
+ }
29658
29753
  try {
29659
29754
  const stored = localStorage.getItem(STORAGE_KEY$2);
29660
29755
  if (!stored)
29661
29756
  return [];
29662
29757
  const entries = JSON.parse(stored);
29663
- // Return newest first
29664
29758
  return entries.sort((a, b) => b.createdAt - a.createdAt);
29665
29759
  }
29666
29760
  catch {
@@ -29668,14 +29762,27 @@
29668
29762
  }
29669
29763
  }
29670
29764
  /**
29671
- * Save vocabulary to localStorage
29765
+ * Set vocabulary directly (used by settings sync from server data).
29766
+ */
29767
+ function setVocabulary(terms) {
29768
+ memoryCache$1 = terms.slice(0, MAX_TERMS);
29769
+ }
29770
+ /**
29771
+ * Reset memory cache (for testing only)
29772
+ */
29773
+ function resetMemoryCache() {
29774
+ memoryCache$1 = null;
29775
+ }
29776
+ /**
29777
+ * Save vocabulary (updates memory cache and tries localStorage)
29672
29778
  */
29673
29779
  function saveVocabulary(terms) {
29780
+ memoryCache$1 = terms;
29674
29781
  try {
29675
29782
  localStorage.setItem(STORAGE_KEY$2, JSON.stringify(terms));
29676
29783
  }
29677
29784
  catch {
29678
- // localStorage full or unavailable - silently fail
29785
+ // localStorage full or unavailable - memory cache still updated
29679
29786
  }
29680
29787
  }
29681
29788
  /**
@@ -29743,13 +29850,14 @@
29743
29850
  * Clear all vocabulary
29744
29851
  */
29745
29852
  function clearVocabulary() {
29853
+ memoryCache$1 = [];
29746
29854
  try {
29747
29855
  localStorage.removeItem(STORAGE_KEY$2);
29748
- events.emit("settings:changed", { setting: "vocabulary" });
29749
29856
  }
29750
29857
  catch {
29751
29858
  // Silently fail
29752
29859
  }
29860
+ events.emit("settings:changed", { setting: "vocabulary" });
29753
29861
  }
29754
29862
  /**
29755
29863
  * Get vocabulary count info
@@ -29765,11 +29873,13 @@
29765
29873
  }
29766
29874
  const vocabularyStore = {
29767
29875
  getVocabulary,
29876
+ setVocabulary,
29768
29877
  addTerm,
29769
29878
  deleteTerm,
29770
29879
  clearVocabulary,
29771
29880
  getVocabularyCount,
29772
29881
  isAtVocabularyLimit,
29882
+ resetMemoryCache,
29773
29883
  MAX_TERMS,
29774
29884
  MAX_TERM_LENGTH,
29775
29885
  };
@@ -29867,6 +29977,451 @@
29867
29977
  resetAudioSettings,
29868
29978
  };
29869
29979
 
29980
+ /**
29981
+ * Transcript history store
29982
+ * Persists transcripts to localStorage for viewing in the settings modal
29983
+ */
29984
+ const STORAGE_KEY = "speechos_transcripts";
29985
+ const MAX_ENTRIES = 50;
29986
+ /**
29987
+ * In-memory cache for transcripts. When server sync is enabled, this is the
29988
+ * source of truth. localStorage is only used when server sync is disabled.
29989
+ */
29990
+ let memoryCache = null;
29991
+ /**
29992
+ * Generate a unique ID for transcript entries
29993
+ */
29994
+ function generateId() {
29995
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
29996
+ }
29997
+ /**
29998
+ * Get all transcripts. Prefers in-memory cache (from server sync),
29999
+ * then falls back to localStorage.
30000
+ */
30001
+ function getTranscripts() {
30002
+ // If we have in-memory data (from server sync), use it
30003
+ if (memoryCache !== null) {
30004
+ return [...memoryCache].sort((a, b) => b.timestamp - a.timestamp);
30005
+ }
30006
+ // Fall back to localStorage (when server sync is disabled)
30007
+ try {
30008
+ const stored = localStorage.getItem(STORAGE_KEY);
30009
+ if (!stored)
30010
+ return [];
30011
+ const entries = JSON.parse(stored);
30012
+ return entries.sort((a, b) => b.timestamp - a.timestamp);
30013
+ }
30014
+ catch {
30015
+ return [];
30016
+ }
30017
+ }
30018
+ /**
30019
+ * Set transcripts directly (used by settings sync from server data).
30020
+ * Server data is the source of truth - just update memory cache.
30021
+ */
30022
+ function setTranscripts(entries) {
30023
+ memoryCache = entries.slice(0, MAX_ENTRIES);
30024
+ }
30025
+ /**
30026
+ * Save a new transcript entry
30027
+ */
30028
+ function saveTranscript(text, action, originalTextOrOptions) {
30029
+ const entry = {
30030
+ id: generateId(),
30031
+ text,
30032
+ timestamp: Date.now(),
30033
+ action,
30034
+ };
30035
+ // Handle edit action with originalText string
30036
+ if (action === "edit" && typeof originalTextOrOptions === "string") {
30037
+ entry.originalText = originalTextOrOptions;
30038
+ }
30039
+ // Handle command action with options object
30040
+ if (action === "command" && typeof originalTextOrOptions === "object") {
30041
+ const options = originalTextOrOptions;
30042
+ if (options.inputText !== undefined)
30043
+ entry.inputText = options.inputText;
30044
+ if (options.commandResult !== undefined)
30045
+ entry.commandResult = options.commandResult;
30046
+ if (options.commandConfig !== undefined)
30047
+ entry.commandConfig = options.commandConfig;
30048
+ }
30049
+ const entries = getTranscripts();
30050
+ entries.unshift(entry);
30051
+ // Prune to max entries
30052
+ const pruned = entries.slice(0, MAX_ENTRIES);
30053
+ // Update memory cache (always)
30054
+ memoryCache = pruned;
30055
+ // Try to persist to localStorage (for when server sync is disabled)
30056
+ try {
30057
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(pruned));
30058
+ }
30059
+ catch {
30060
+ // Quota exceeded - memory cache is still updated
30061
+ }
30062
+ // Emit settings change event to trigger sync
30063
+ events.emit("settings:changed", { setting: "history" });
30064
+ return entry;
30065
+ }
30066
+ /**
30067
+ * Clear all transcript history
30068
+ */
30069
+ function clearTranscripts() {
30070
+ memoryCache = [];
30071
+ try {
30072
+ localStorage.removeItem(STORAGE_KEY);
30073
+ }
30074
+ catch {
30075
+ // Silently fail
30076
+ }
30077
+ events.emit("settings:changed", { setting: "history" });
30078
+ }
30079
+ /**
30080
+ * Delete a single transcript by ID
30081
+ */
30082
+ function deleteTranscript(id) {
30083
+ const entries = getTranscripts().filter((e) => e.id !== id);
30084
+ memoryCache = entries;
30085
+ try {
30086
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
30087
+ }
30088
+ catch {
30089
+ // Silently fail
30090
+ }
30091
+ events.emit("settings:changed", { setting: "history" });
30092
+ }
30093
+ const transcriptStore = {
30094
+ getTranscripts,
30095
+ setTranscripts,
30096
+ saveTranscript,
30097
+ clearTranscripts,
30098
+ deleteTranscript,
30099
+ };
30100
+
30101
+ /**
30102
+ * Settings sync manager
30103
+ * Syncs user settings (language, vocabulary, snippets, history) with the server
30104
+ */
30105
+ // Sync debounce delay in milliseconds
30106
+ const SYNC_DEBOUNCE_MS = 2000;
30107
+ // Maximum retry attempts
30108
+ const MAX_RETRIES = 3;
30109
+ // Base retry delay in milliseconds (exponential backoff)
30110
+ const BASE_RETRY_DELAY_MS = 2000;
30111
+ /**
30112
+ * Settings sync manager singleton
30113
+ */
30114
+ class SettingsSync {
30115
+ constructor() {
30116
+ this.syncTimer = null;
30117
+ this.isSyncing = false;
30118
+ this.retryCount = 0;
30119
+ this.isInitialized = false;
30120
+ this.unsubscribe = null;
30121
+ /** When true, sync is disabled due to CSP or network restrictions */
30122
+ this.syncDisabled = false;
30123
+ }
30124
+ /**
30125
+ * Make a fetch request using native fetch.
30126
+ */
30127
+ async doFetch(url, options) {
30128
+ const config = getConfig();
30129
+ if (config.debug) {
30130
+ console.log("[SpeechOS] Using native fetch", options.method, url);
30131
+ }
30132
+ return fetch(url, options);
30133
+ }
30134
+ /**
30135
+ * Initialize the settings sync manager
30136
+ * If a settingsToken is configured, loads settings from server
30137
+ */
30138
+ async init() {
30139
+ const token = getSettingsToken();
30140
+ if (!token) {
30141
+ // No token configured, sync is disabled
30142
+ return;
30143
+ }
30144
+ if (this.isInitialized) {
30145
+ return;
30146
+ }
30147
+ this.isInitialized = true;
30148
+ // Subscribe to settings changes
30149
+ this.unsubscribe = events.on("settings:changed", () => {
30150
+ this.scheduleSyncToServer();
30151
+ });
30152
+ // Load settings from server
30153
+ await this.loadFromServer();
30154
+ }
30155
+ /**
30156
+ * Stop the sync manager and clean up
30157
+ */
30158
+ destroy() {
30159
+ if (this.syncTimer) {
30160
+ clearTimeout(this.syncTimer);
30161
+ this.syncTimer = null;
30162
+ }
30163
+ if (this.unsubscribe) {
30164
+ this.unsubscribe();
30165
+ this.unsubscribe = null;
30166
+ }
30167
+ this.isInitialized = false;
30168
+ this.retryCount = 0;
30169
+ this.syncDisabled = false;
30170
+ }
30171
+ /**
30172
+ * Load settings from the server and merge with local
30173
+ */
30174
+ async loadFromServer() {
30175
+ const token = getSettingsToken();
30176
+ if (!token) {
30177
+ return;
30178
+ }
30179
+ const config = getConfig();
30180
+ try {
30181
+ const response = await this.doFetch(`${config.host}/api/user-settings/`, {
30182
+ method: "GET",
30183
+ headers: {
30184
+ Authorization: `Bearer ${token}`,
30185
+ "Content-Type": "application/json",
30186
+ },
30187
+ });
30188
+ if (config.debug) {
30189
+ console.log("[SpeechOS] Settings fetch response:", response.status, response.ok ? "OK" : response.statusText);
30190
+ }
30191
+ if (response.status === 404) {
30192
+ // No settings on server yet (new user) - sync local settings to server
30193
+ if (config.debug) {
30194
+ console.log("[SpeechOS] No server settings found, syncing local to server");
30195
+ }
30196
+ await this.syncToServer();
30197
+ return;
30198
+ }
30199
+ if (response.status === 401 || response.status === 403) {
30200
+ // Token expired or invalid
30201
+ this.handleTokenExpired();
30202
+ return;
30203
+ }
30204
+ if (!response.ok) {
30205
+ throw new Error(`Server returned ${response.status}`);
30206
+ }
30207
+ const serverSettings = (await response.json());
30208
+ if (config.debug) {
30209
+ console.log("[SpeechOS] Settings received from server:", {
30210
+ language: serverSettings.language,
30211
+ vocabularyCount: serverSettings.vocabulary?.length ?? 0,
30212
+ snippetsCount: serverSettings.snippets?.length ?? 0,
30213
+ historyCount: serverSettings.history?.length ?? 0,
30214
+ lastSyncedAt: serverSettings.lastSyncedAt,
30215
+ });
30216
+ if (serverSettings.history?.length > 0) {
30217
+ console.log("[SpeechOS] History entries:", serverSettings.history);
30218
+ }
30219
+ }
30220
+ this.mergeSettings(serverSettings);
30221
+ events.emit("settings:loaded", undefined);
30222
+ if (config.debug) {
30223
+ console.log("[SpeechOS] Settings merged and loaded");
30224
+ }
30225
+ }
30226
+ catch (error) {
30227
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
30228
+ // Check if this is a CSP/network restriction - disable sync permanently for this session
30229
+ if (this.isNetworkRestrictionError(error)) {
30230
+ this.syncDisabled = true;
30231
+ if (config.debug) {
30232
+ console.log("[SpeechOS] Settings sync disabled (CSP/network restriction), using localStorage only");
30233
+ }
30234
+ }
30235
+ else if (config.debug) {
30236
+ console.warn("[SpeechOS] Failed to load settings from server:", errorMessage);
30237
+ }
30238
+ events.emit("settings:syncFailed", { error: errorMessage });
30239
+ // Continue with local settings on error
30240
+ }
30241
+ }
30242
+ /**
30243
+ * Merge server settings with local (server wins).
30244
+ * Uses store setters to update memory cache - localStorage is a fallback.
30245
+ */
30246
+ mergeSettings(serverSettings) {
30247
+ // Language settings - server wins
30248
+ if (serverSettings.language) {
30249
+ setLanguageSettings(serverSettings.language);
30250
+ }
30251
+ // Vocabulary - server wins
30252
+ if (serverSettings.vocabulary) {
30253
+ setVocabulary(serverSettings.vocabulary);
30254
+ }
30255
+ // Snippets - server wins
30256
+ if (serverSettings.snippets) {
30257
+ setSnippets(serverSettings.snippets);
30258
+ }
30259
+ // History - server wins
30260
+ if (serverSettings.history) {
30261
+ setTranscripts(serverSettings.history);
30262
+ }
30263
+ }
30264
+ /**
30265
+ * Schedule a debounced sync to server
30266
+ */
30267
+ scheduleSyncToServer() {
30268
+ const token = getSettingsToken();
30269
+ if (!token || this.syncDisabled) {
30270
+ return;
30271
+ }
30272
+ // Cancel any pending sync
30273
+ if (this.syncTimer) {
30274
+ clearTimeout(this.syncTimer);
30275
+ }
30276
+ // Schedule new sync
30277
+ this.syncTimer = setTimeout(() => {
30278
+ this.syncToServer();
30279
+ }, SYNC_DEBOUNCE_MS);
30280
+ }
30281
+ /**
30282
+ * Sync current settings to server
30283
+ */
30284
+ async syncToServer() {
30285
+ const token = getSettingsToken();
30286
+ if (!token || this.isSyncing || this.syncDisabled) {
30287
+ return;
30288
+ }
30289
+ this.isSyncing = true;
30290
+ const config = getConfig();
30291
+ try {
30292
+ const languageSettings = getLanguageSettings();
30293
+ const vocabulary = getVocabulary();
30294
+ const snippets = getSnippets();
30295
+ const transcripts = getTranscripts();
30296
+ const payload = {
30297
+ language: {
30298
+ inputLanguageCode: languageSettings.inputLanguageCode,
30299
+ outputLanguageCode: languageSettings.outputLanguageCode,
30300
+ smartFormat: languageSettings.smartFormat,
30301
+ },
30302
+ vocabulary: vocabulary.map((v) => ({
30303
+ id: v.id,
30304
+ term: v.term,
30305
+ createdAt: v.createdAt,
30306
+ })),
30307
+ snippets: snippets.map((s) => ({
30308
+ id: s.id,
30309
+ trigger: s.trigger,
30310
+ expansion: s.expansion,
30311
+ createdAt: s.createdAt,
30312
+ })),
30313
+ // Sync history (excluding commandConfig to reduce payload size)
30314
+ history: transcripts.map((t) => ({
30315
+ id: t.id,
30316
+ text: t.text,
30317
+ timestamp: t.timestamp,
30318
+ action: t.action,
30319
+ ...(t.originalText && { originalText: t.originalText }),
30320
+ ...(t.inputText && { inputText: t.inputText }),
30321
+ ...(t.commandResult !== undefined && { commandResult: t.commandResult }),
30322
+ })),
30323
+ };
30324
+ const response = await this.doFetch(`${config.host}/api/user-settings/`, {
30325
+ method: "PUT",
30326
+ headers: {
30327
+ Authorization: `Bearer ${token}`,
30328
+ "Content-Type": "application/json",
30329
+ },
30330
+ body: JSON.stringify(payload),
30331
+ });
30332
+ if (response.status === 401 || response.status === 403) {
30333
+ this.handleTokenExpired();
30334
+ return;
30335
+ }
30336
+ if (!response.ok) {
30337
+ throw new Error(`Server returned ${response.status}`);
30338
+ }
30339
+ // Reset retry count on success
30340
+ this.retryCount = 0;
30341
+ events.emit("settings:synced", undefined);
30342
+ if (config.debug) {
30343
+ console.log("[SpeechOS] Settings synced to server");
30344
+ }
30345
+ }
30346
+ catch (error) {
30347
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
30348
+ // Check if this is a CSP/network restriction - disable sync permanently for this session
30349
+ if (this.isNetworkRestrictionError(error)) {
30350
+ this.syncDisabled = true;
30351
+ if (config.debug) {
30352
+ console.log("[SpeechOS] Settings sync disabled (CSP/network restriction), using localStorage only");
30353
+ }
30354
+ events.emit("settings:syncFailed", { error: errorMessage });
30355
+ // Don't retry - CSP errors are permanent
30356
+ }
30357
+ else {
30358
+ if (config.debug) {
30359
+ console.warn("[SpeechOS] Failed to sync settings to server:", errorMessage);
30360
+ }
30361
+ events.emit("settings:syncFailed", { error: errorMessage });
30362
+ // Retry with exponential backoff (only for transient errors)
30363
+ this.scheduleRetry();
30364
+ }
30365
+ }
30366
+ finally {
30367
+ this.isSyncing = false;
30368
+ }
30369
+ }
30370
+ /**
30371
+ * Schedule a retry with exponential backoff
30372
+ */
30373
+ scheduleRetry() {
30374
+ if (this.retryCount >= MAX_RETRIES) {
30375
+ const config = getConfig();
30376
+ if (config.debug) {
30377
+ console.warn("[SpeechOS] Max retries reached, giving up sync");
30378
+ }
30379
+ this.retryCount = 0;
30380
+ return;
30381
+ }
30382
+ this.retryCount++;
30383
+ const delay = BASE_RETRY_DELAY_MS * Math.pow(2, this.retryCount - 1);
30384
+ this.syncTimer = setTimeout(() => {
30385
+ this.syncToServer();
30386
+ }, delay);
30387
+ }
30388
+ /**
30389
+ * Check if an error is a CSP or network restriction error
30390
+ * These errors are permanent and shouldn't trigger retries
30391
+ */
30392
+ isNetworkRestrictionError(error) {
30393
+ if (error instanceof TypeError) {
30394
+ const message = error.message.toLowerCase();
30395
+ // Common CSP/network error messages
30396
+ return (message.includes("failed to fetch") ||
30397
+ message.includes("network request failed") ||
30398
+ message.includes("content security policy") ||
30399
+ message.includes("csp") ||
30400
+ message.includes("blocked"));
30401
+ }
30402
+ return false;
30403
+ }
30404
+ /**
30405
+ * Handle token expiration
30406
+ */
30407
+ handleTokenExpired() {
30408
+ clearSettingsToken();
30409
+ events.emit("settings:tokenExpired", undefined);
30410
+ const config = getConfig();
30411
+ if (config.debug) {
30412
+ console.warn("[SpeechOS] Settings token expired");
30413
+ }
30414
+ }
30415
+ /**
30416
+ * Check if sync is enabled (token is configured)
30417
+ */
30418
+ isEnabled() {
30419
+ return !!getSettingsToken();
30420
+ }
30421
+ }
30422
+ // Singleton instance
30423
+ const settingsSync = new SettingsSync();
30424
+
29870
30425
  /******************************************************************************
29871
30426
  Copyright (c) Microsoft Corporation.
29872
30427
 
@@ -29950,6 +30505,9 @@
29950
30505
  */
29951
30506
  const themeStyles = i$4 `
29952
30507
  :host {
30508
+ /* Font stack - system fonts for consistent rendering across sites */
30509
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
30510
+
29953
30511
  /* Color tokens */
29954
30512
  --speechos-primary: #10B981;
29955
30513
  --speechos-primary-hover: #059669;
@@ -30073,100 +30631,6 @@
30073
30631
  }
30074
30632
  `;
30075
30633
 
30076
- /**
30077
- * Transcript history store
30078
- * Persists transcripts to localStorage for viewing in the settings modal
30079
- */
30080
- const STORAGE_KEY = "speechos_transcripts";
30081
- const MAX_ENTRIES = 50;
30082
- /**
30083
- * Generate a unique ID for transcript entries
30084
- */
30085
- function generateId() {
30086
- return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
30087
- }
30088
- /**
30089
- * Get all transcripts from localStorage
30090
- */
30091
- function getTranscripts() {
30092
- try {
30093
- const stored = localStorage.getItem(STORAGE_KEY);
30094
- if (!stored)
30095
- return [];
30096
- const entries = JSON.parse(stored);
30097
- // Return newest first
30098
- return entries.sort((a, b) => b.timestamp - a.timestamp);
30099
- }
30100
- catch {
30101
- return [];
30102
- }
30103
- }
30104
- /**
30105
- * Save a new transcript entry
30106
- */
30107
- function saveTranscript(text, action, originalTextOrOptions) {
30108
- const entry = {
30109
- id: generateId(),
30110
- text,
30111
- timestamp: Date.now(),
30112
- action,
30113
- };
30114
- // Handle edit action with originalText string
30115
- if (action === "edit" && typeof originalTextOrOptions === "string") {
30116
- entry.originalText = originalTextOrOptions;
30117
- }
30118
- // Handle command action with options object
30119
- if (action === "command" && typeof originalTextOrOptions === "object") {
30120
- const options = originalTextOrOptions;
30121
- if (options.inputText !== undefined)
30122
- entry.inputText = options.inputText;
30123
- if (options.commandResult !== undefined)
30124
- entry.commandResult = options.commandResult;
30125
- if (options.commandConfig !== undefined)
30126
- entry.commandConfig = options.commandConfig;
30127
- }
30128
- const entries = getTranscripts();
30129
- entries.unshift(entry);
30130
- // Prune to max entries
30131
- const pruned = entries.slice(0, MAX_ENTRIES);
30132
- try {
30133
- localStorage.setItem(STORAGE_KEY, JSON.stringify(pruned));
30134
- }
30135
- catch {
30136
- // localStorage full or unavailable - silently fail
30137
- }
30138
- return entry;
30139
- }
30140
- /**
30141
- * Clear all transcript history
30142
- */
30143
- function clearTranscripts() {
30144
- try {
30145
- localStorage.removeItem(STORAGE_KEY);
30146
- }
30147
- catch {
30148
- // Silently fail
30149
- }
30150
- }
30151
- /**
30152
- * Delete a single transcript by ID
30153
- */
30154
- function deleteTranscript(id) {
30155
- const entries = getTranscripts().filter((e) => e.id !== id);
30156
- try {
30157
- localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
30158
- }
30159
- catch {
30160
- // Silently fail
30161
- }
30162
- }
30163
- const transcriptStore = {
30164
- getTranscripts: getTranscripts,
30165
- saveTranscript: saveTranscript,
30166
- clearTranscripts: clearTranscripts,
30167
- deleteTranscript: deleteTranscript,
30168
- };
30169
-
30170
30634
  function isNativeField(field) {
30171
30635
  return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement;
30172
30636
  }
@@ -32164,6 +32628,8 @@
32164
32628
  inset: 0;
32165
32629
  pointer-events: none;
32166
32630
  z-index: calc(var(--speechos-z-base) + 100);
32631
+ /* Ensure consistent font rendering across all sites */
32632
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
32167
32633
  }
32168
32634
 
32169
32635
  .modal-overlay {
@@ -32652,6 +33118,7 @@
32652
33118
  constructor() {
32653
33119
  super(...arguments);
32654
33120
  this.transcripts = [];
33121
+ this.unsubscribeSettingsLoaded = null;
32655
33122
  }
32656
33123
  static { this.styles = [
32657
33124
  themeStyles,
@@ -32788,11 +33255,36 @@
32788
33255
  background: rgba(239, 68, 68, 0.18);
32789
33256
  border-color: rgba(239, 68, 68, 0.25);
32790
33257
  }
33258
+
33259
+ .command-matched {
33260
+ font-size: 12px;
33261
+ color: rgba(255, 255, 255, 0.5);
33262
+ margin-top: 6px;
33263
+ }
33264
+
33265
+ .command-matched code {
33266
+ background: rgba(245, 158, 11, 0.15);
33267
+ color: #fbbf24;
33268
+ padding: 2px 6px;
33269
+ border-radius: 4px;
33270
+ font-family: monospace;
33271
+ }
32791
33272
  `,
32792
33273
  ]; }
32793
33274
  connectedCallback() {
32794
33275
  super.connectedCallback();
32795
33276
  this.loadTranscripts();
33277
+ // Refresh when settings are loaded from the server (history may have changed)
33278
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
33279
+ this.loadTranscripts();
33280
+ });
33281
+ }
33282
+ disconnectedCallback() {
33283
+ super.disconnectedCallback();
33284
+ if (this.unsubscribeSettingsLoaded) {
33285
+ this.unsubscribeSettingsLoaded();
33286
+ this.unsubscribeSettingsLoaded = null;
33287
+ }
32796
33288
  }
32797
33289
  /** Reload transcripts from store */
32798
33290
  refresh() {
@@ -32863,7 +33355,13 @@
32863
33355
  renderCommandDetails(entry) {
32864
33356
  // Show the transcript text (what the user said)
32865
33357
  const displayText = entry.inputText || entry.text;
32866
- return b `<div class="transcript-text">${displayText}</div>`;
33358
+ const commandName = entry.commandResult?.name;
33359
+ return b `
33360
+ <div class="transcript-text">${displayText}</div>
33361
+ ${commandName
33362
+ ? b `<div class="command-matched">Matched: <code>${commandName}</code></div>`
33363
+ : null}
33364
+ `;
32867
33365
  }
32868
33366
  getCopyText(entry) {
32869
33367
  if (entry.action === "command") {
@@ -33413,6 +33911,7 @@
33413
33911
  this.permissionGranted = false;
33414
33912
  this.smartFormatEnabled = true;
33415
33913
  this.savedIndicatorTimeout = null;
33914
+ this.unsubscribeSettingsLoaded = null;
33416
33915
  }
33417
33916
  static { this.styles = [
33418
33917
  themeStyles,
@@ -33506,6 +34005,16 @@
33506
34005
  padding: 8px;
33507
34006
  }
33508
34007
 
34008
+ .settings-select:disabled {
34009
+ opacity: 0.4;
34010
+ cursor: not-allowed;
34011
+ }
34012
+
34013
+ .settings-select:disabled:hover {
34014
+ border-color: rgba(255, 255, 255, 0.08);
34015
+ background: rgba(0, 0, 0, 0.3);
34016
+ }
34017
+
33509
34018
  .settings-select-arrow {
33510
34019
  position: absolute;
33511
34020
  right: 14px;
@@ -33587,12 +34096,20 @@
33587
34096
  connectedCallback() {
33588
34097
  super.connectedCallback();
33589
34098
  this.loadSettings();
34099
+ // Refresh when settings are loaded from the server
34100
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
34101
+ this.loadSettings();
34102
+ });
33590
34103
  }
33591
34104
  disconnectedCallback() {
33592
34105
  super.disconnectedCallback();
33593
34106
  if (this.savedIndicatorTimeout) {
33594
34107
  clearTimeout(this.savedIndicatorTimeout);
33595
34108
  }
34109
+ if (this.unsubscribeSettingsLoaded) {
34110
+ this.unsubscribeSettingsLoaded();
34111
+ this.unsubscribeSettingsLoaded = null;
34112
+ }
33596
34113
  this.isTestingMic = false;
33597
34114
  }
33598
34115
  /** Called when tab becomes active */
@@ -33679,13 +34196,14 @@
33679
34196
  setSmartFormatEnabled(this.smartFormatEnabled);
33680
34197
  this.showSaved();
33681
34198
  }
33682
- renderLanguageSelector(selectedCode, onChange) {
34199
+ renderLanguageSelector(selectedCode, onChange, disabled = false) {
33683
34200
  return b `
33684
34201
  <div class="settings-select-wrapper">
33685
34202
  <select
33686
34203
  class="settings-select"
33687
34204
  .value="${selectedCode}"
33688
34205
  @change="${onChange}"
34206
+ ?disabled="${disabled}"
33689
34207
  >
33690
34208
  ${SUPPORTED_LANGUAGES.map((lang) => b `
33691
34209
  <option
@@ -33803,7 +34321,7 @@
33803
34321
  <div class="settings-section-description">
33804
34322
  AI automatically removes filler words, adds punctuation, and polishes
33805
34323
  your text. Disable for raw transcription output. Note: disabling also
33806
- turns off text snippets.
34324
+ turns off text snippets and output language translation.
33807
34325
  </div>
33808
34326
  <div class="settings-toggle-row">
33809
34327
  <span class="settings-toggle-label">Enable AI formatting</span>
@@ -33844,9 +34362,13 @@
33844
34362
  </div>
33845
34363
  <div class="settings-section-description">
33846
34364
  The language for your transcribed text. Usually the same as input, but
33847
- can differ for translation.
34365
+ can differ for translation.${!this.smartFormatEnabled
34366
+ ? " Requires Smart Format to be enabled."
34367
+ : ""}
33848
34368
  </div>
33849
- ${this.renderLanguageSelector(this.selectedOutputLanguageCode, this.handleOutputLanguageChange.bind(this))}
34369
+ ${this.renderLanguageSelector(this.smartFormatEnabled
34370
+ ? this.selectedOutputLanguageCode
34371
+ : this.selectedInputLanguageCode, this.handleOutputLanguageChange.bind(this), !this.smartFormatEnabled)}
33850
34372
  </div>
33851
34373
  `;
33852
34374
  }
@@ -33891,6 +34413,7 @@
33891
34413
  this.trigger = "";
33892
34414
  this.expansion = "";
33893
34415
  this.error = "";
34416
+ this.unsubscribeSettingsLoaded = null;
33894
34417
  }
33895
34418
  static { this.styles = [
33896
34419
  themeStyles,
@@ -33996,6 +34519,17 @@
33996
34519
  connectedCallback() {
33997
34520
  super.connectedCallback();
33998
34521
  this.loadSnippets();
34522
+ // Refresh when settings are loaded from the server
34523
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
34524
+ this.loadSnippets();
34525
+ });
34526
+ }
34527
+ disconnectedCallback() {
34528
+ super.disconnectedCallback();
34529
+ if (this.unsubscribeSettingsLoaded) {
34530
+ this.unsubscribeSettingsLoaded();
34531
+ this.unsubscribeSettingsLoaded = null;
34532
+ }
33999
34533
  }
34000
34534
  /** Reload snippets from store */
34001
34535
  refresh() {
@@ -34238,6 +34772,7 @@
34238
34772
  this.showForm = false;
34239
34773
  this.term = "";
34240
34774
  this.error = "";
34775
+ this.unsubscribeSettingsLoaded = null;
34241
34776
  }
34242
34777
  static { this.styles = [
34243
34778
  themeStyles,
@@ -34290,6 +34825,17 @@
34290
34825
  connectedCallback() {
34291
34826
  super.connectedCallback();
34292
34827
  this.loadVocabulary();
34828
+ // Refresh when settings are loaded from the server
34829
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
34830
+ this.loadVocabulary();
34831
+ });
34832
+ }
34833
+ disconnectedCallback() {
34834
+ super.disconnectedCallback();
34835
+ if (this.unsubscribeSettingsLoaded) {
34836
+ this.unsubscribeSettingsLoaded();
34837
+ this.unsubscribeSettingsLoaded = null;
34838
+ }
34293
34839
  }
34294
34840
  /** Reload vocabulary from store */
34295
34841
  refresh() {
@@ -35808,7 +36354,14 @@
35808
36354
  state.hide();
35809
36355
  }
35810
36356
  handleSettingsClick() {
35811
- this.settingsOpen = true;
36357
+ if (useExternalSettings()) {
36358
+ const host = getConfig().host;
36359
+ const fullUrl = `${host}/a/extension-settings`;
36360
+ window.open(fullUrl, '_blank', 'noopener,noreferrer');
36361
+ }
36362
+ else {
36363
+ this.settingsOpen = true;
36364
+ }
35812
36365
  }
35813
36366
  handleDragStart(e) {
35814
36367
  if (e.button !== 0)
@@ -36349,10 +36902,17 @@
36349
36902
  this.editSelectionStart = null;
36350
36903
  this.editSelectionEnd = null;
36351
36904
  this.editSelectedText = "";
36352
- // Open settings modal
36353
- this.settingsOpen = true;
36905
+ // Open settings - either external URL or modal
36906
+ if (useExternalSettings()) {
36907
+ const host = getConfig().host;
36908
+ const fullUrl = `${host}/a/extension-settings`;
36909
+ window.open(fullUrl, '_blank', 'noopener,noreferrer');
36910
+ }
36911
+ else {
36912
+ this.settingsOpen = true;
36913
+ }
36354
36914
  if (getConfig().debug) {
36355
- console.log("[SpeechOS] Settings modal opened from no-audio warning");
36915
+ console.log("[SpeechOS] Settings opened from no-audio warning", { useExternalSettings: useExternalSettings() });
36356
36916
  }
36357
36917
  await disconnectPromise;
36358
36918
  }
@@ -36561,6 +37121,30 @@
36561
37121
  t$1("speechos-widget")
36562
37122
  ], SpeechOSWidget);
36563
37123
 
37124
+ /**
37125
+ * UI module exports
37126
+ * Lit-based Shadow DOM components
37127
+ */
37128
+ // Patch customElements.define to silently ignore duplicate registrations for speechos-* elements.
37129
+ // This prevents errors when the extension loads on a page that already has SpeechOS.
37130
+ // The patch is scoped to only affect speechos-* tags to avoid unintended effects on host pages.
37131
+ const originalDefine = customElements.define.bind(customElements);
37132
+ customElements.define = (name, constructor, options) => {
37133
+ // Only intercept speechos-* elements
37134
+ if (name.startsWith("speechos-")) {
37135
+ if (customElements.get(name) === undefined) {
37136
+ originalDefine(name, constructor, options);
37137
+ }
37138
+ // Skip silently if already registered
37139
+ }
37140
+ else {
37141
+ // Pass through for non-speechos elements
37142
+ originalDefine(name, constructor, options);
37143
+ }
37144
+ };
37145
+ // Restore original customElements.define after our components are registered
37146
+ customElements.define = originalDefine;
37147
+
36564
37148
  /**
36565
37149
  * Main SpeechOS Client SDK class
36566
37150
  * Composes core logic with UI components
@@ -36642,6 +37226,13 @@
36642
37226
  state.show();
36643
37227
  }
36644
37228
  this.isInitialized = true;
37229
+ // Initialize settings sync if token is configured
37230
+ // This loads settings from server and subscribes to changes
37231
+ settingsSync.init().catch((error) => {
37232
+ if (finalConfig.debug) {
37233
+ console.warn("[SpeechOS] Settings sync initialization failed:", error);
37234
+ }
37235
+ });
36645
37236
  // Log initialization in debug mode
36646
37237
  if (finalConfig.debug) {
36647
37238
  console.log("[SpeechOS] Initialized with config:", finalConfig);
@@ -36672,6 +37263,8 @@
36672
37263
  resetClientConfig();
36673
37264
  // Reset text input handler to default
36674
37265
  resetTextInputHandler();
37266
+ // Stop settings sync
37267
+ settingsSync.destroy();
36675
37268
  // Clear instance
36676
37269
  this.instance = null;
36677
37270
  this.isInitialized = false;
@@ -36895,10 +37488,12 @@
36895
37488
  exports.setOutputLanguageCode = setOutputLanguageCode;
36896
37489
  exports.setSmartFormatEnabled = setSmartFormatEnabled;
36897
37490
  exports.setTextInputHandler = setTextInputHandler;
37491
+ exports.settingsSync = settingsSync;
36898
37492
  exports.snippetsStore = snippetsStore;
36899
37493
  exports.state = state;
36900
37494
  exports.transcriptStore = transcriptStore;
36901
37495
  exports.updateSnippet = updateSnippet;
37496
+ exports.useExternalSettings = useExternalSettings;
36902
37497
  exports.vocabularyStore = vocabularyStore;
36903
37498
 
36904
37499
  Object.defineProperty(exports, '__esModule', { value: true });