@speechos/client 0.2.7 → 0.2.9

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 (38) hide show
  1. package/dist/index.cjs +651 -110
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.iife.js +684 -114
  6. package/dist/index.iife.js.map +1 -1
  7. package/dist/index.iife.min.js +122 -100
  8. package/dist/index.iife.min.js.map +1 -1
  9. package/dist/index.js +652 -112
  10. package/dist/index.js.map +1 -1
  11. package/dist/settings-sync.d.ts +68 -0
  12. package/dist/settings-sync.d.ts.map +1 -0
  13. package/dist/settings-sync.test.d.ts +5 -0
  14. package/dist/settings-sync.test.d.ts.map +1 -0
  15. package/dist/speechos.d.ts.map +1 -1
  16. package/dist/stores/language-settings.d.ts +12 -1
  17. package/dist/stores/language-settings.d.ts.map +1 -1
  18. package/dist/stores/language-settings.test.d.ts +5 -0
  19. package/dist/stores/language-settings.test.d.ts.map +1 -0
  20. package/dist/stores/snippets-store.d.ts +12 -1
  21. package/dist/stores/snippets-store.d.ts.map +1 -1
  22. package/dist/stores/transcript-store.d.ts +13 -2
  23. package/dist/stores/transcript-store.d.ts.map +1 -1
  24. package/dist/stores/vocabulary-store.d.ts +12 -1
  25. package/dist/stores/vocabulary-store.d.ts.map +1 -1
  26. package/dist/ui/index.d.ts.map +1 -1
  27. package/dist/ui/styles/modal-styles.d.ts.map +1 -1
  28. package/dist/ui/styles/theme.d.ts.map +1 -1
  29. package/dist/ui/tabs/history-tab.d.ts +2 -0
  30. package/dist/ui/tabs/history-tab.d.ts.map +1 -1
  31. package/dist/ui/tabs/settings-tab.d.ts +1 -0
  32. package/dist/ui/tabs/settings-tab.d.ts.map +1 -1
  33. package/dist/ui/tabs/snippets-tab.d.ts +2 -0
  34. package/dist/ui/tabs/snippets-tab.d.ts.map +1 -1
  35. package/dist/ui/tabs/vocabulary-tab.d.ts +2 -0
  36. package/dist/ui/tabs/vocabulary-tab.d.ts.map +1 -1
  37. package/dist/ui/widget.d.ts.map +1 -1
  38. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { state, events, getConfig, getBackend, setConfig, updateUserId } from '@speechos/core';
1
+ import { state, events, getConfig, getFetchHandler, getSettingsToken, clearSettingsToken, getBackend, setConfig, updateUserId } from '@speechos/core';
2
2
  export { DEFAULT_HOST, events, getConfig, livekit, resetConfig, setConfig, state } from '@speechos/core';
3
3
 
4
4
  /**
@@ -481,6 +481,11 @@ function resetTextInputHandler() {
481
481
  * Persists input language preferences to localStorage
482
482
  */
483
483
  const STORAGE_KEY$4 = "speechos_language_settings";
484
+ /**
485
+ * In-memory cache for language settings. When server sync is enabled, this is the
486
+ * source of truth. localStorage is only used when server sync is disabled.
487
+ */
488
+ let memoryCache$3 = null;
484
489
  /**
485
490
  * Supported input languages for speech recognition
486
491
  * Each language has a name, primary code, and available variants
@@ -532,9 +537,13 @@ const defaultSettings$1 = {
532
537
  smartFormat: true,
533
538
  };
534
539
  /**
535
- * Get current language settings from localStorage
540
+ * Get current language settings. Prefers in-memory cache (from server sync),
541
+ * then falls back to localStorage.
536
542
  */
537
543
  function getLanguageSettings() {
544
+ if (memoryCache$3 !== null) {
545
+ return { ...memoryCache$3 };
546
+ }
538
547
  try {
539
548
  const stored = localStorage.getItem(STORAGE_KEY$4);
540
549
  if (!stored)
@@ -546,14 +555,27 @@ function getLanguageSettings() {
546
555
  }
547
556
  }
548
557
  /**
549
- * Save language settings to localStorage
558
+ * Set language settings directly (used by settings sync from server data).
559
+ */
560
+ function setLanguageSettings(settings) {
561
+ memoryCache$3 = { ...defaultSettings$1, ...settings };
562
+ }
563
+ /**
564
+ * Reset memory cache (for testing only)
565
+ */
566
+ function resetMemoryCache$2() {
567
+ memoryCache$3 = null;
568
+ }
569
+ /**
570
+ * Save language settings (updates memory cache and tries localStorage)
550
571
  */
551
572
  function saveLanguageSettings(settings) {
573
+ memoryCache$3 = settings;
552
574
  try {
553
575
  localStorage.setItem(STORAGE_KEY$4, JSON.stringify(settings));
554
576
  }
555
577
  catch {
556
- // localStorage full or unavailable - silently fail
578
+ // localStorage full or unavailable - memory cache still updated
557
579
  }
558
580
  }
559
581
  /**
@@ -657,6 +679,7 @@ function getLanguageName() {
657
679
  * Reset language settings to defaults
658
680
  */
659
681
  function resetLanguageSettings() {
682
+ memoryCache$3 = null;
660
683
  try {
661
684
  localStorage.removeItem(STORAGE_KEY$4);
662
685
  }
@@ -666,6 +689,7 @@ function resetLanguageSettings() {
666
689
  }
667
690
  const languageSettings = {
668
691
  getLanguageSettings,
692
+ setLanguageSettings,
669
693
  getInputLanguageCode,
670
694
  setInputLanguageCode,
671
695
  getOutputLanguageCode,
@@ -676,6 +700,7 @@ const languageSettings = {
676
700
  getSmartFormatEnabled,
677
701
  setSmartFormatEnabled,
678
702
  resetLanguageSettings,
703
+ resetMemoryCache: resetMemoryCache$2,
679
704
  SUPPORTED_LANGUAGES,
680
705
  // Legacy aliases
681
706
  getLanguageCode,
@@ -691,6 +716,11 @@ const STORAGE_KEY$3 = "speechos_snippets";
691
716
  const MAX_SNIPPETS = 25;
692
717
  const MAX_TRIGGER_LENGTH = 30;
693
718
  const MAX_EXPANSION_LENGTH = 300;
719
+ /**
720
+ * In-memory cache for snippets. When server sync is enabled, this is the
721
+ * source of truth. localStorage is only used when server sync is disabled.
722
+ */
723
+ let memoryCache$2 = null;
694
724
  /**
695
725
  * Generate a unique ID for snippet entries
696
726
  */
@@ -698,15 +728,18 @@ function generateId$2() {
698
728
  return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
699
729
  }
700
730
  /**
701
- * Get all snippets from localStorage
731
+ * Get all snippets. Prefers in-memory cache (from server sync),
732
+ * then falls back to localStorage.
702
733
  */
703
734
  function getSnippets() {
735
+ if (memoryCache$2 !== null) {
736
+ return [...memoryCache$2].sort((a, b) => b.createdAt - a.createdAt);
737
+ }
704
738
  try {
705
739
  const stored = localStorage.getItem(STORAGE_KEY$3);
706
740
  if (!stored)
707
741
  return [];
708
742
  const entries = JSON.parse(stored);
709
- // Return newest first
710
743
  return entries.sort((a, b) => b.createdAt - a.createdAt);
711
744
  }
712
745
  catch {
@@ -714,14 +747,27 @@ function getSnippets() {
714
747
  }
715
748
  }
716
749
  /**
717
- * Save snippets to localStorage
750
+ * Set snippets directly (used by settings sync from server data).
751
+ */
752
+ function setSnippets(snippets) {
753
+ memoryCache$2 = snippets.slice(0, MAX_SNIPPETS);
754
+ }
755
+ /**
756
+ * Reset memory cache (for testing only)
757
+ */
758
+ function resetMemoryCache$1() {
759
+ memoryCache$2 = null;
760
+ }
761
+ /**
762
+ * Save snippets (updates memory cache and tries localStorage)
718
763
  */
719
764
  function saveSnippets(snippets) {
765
+ memoryCache$2 = snippets;
720
766
  try {
721
767
  localStorage.setItem(STORAGE_KEY$3, JSON.stringify(snippets));
722
768
  }
723
769
  catch {
724
- // localStorage full or unavailable - silently fail
770
+ // localStorage full or unavailable - memory cache still updated
725
771
  }
726
772
  }
727
773
  /**
@@ -839,13 +885,14 @@ function deleteSnippet(id) {
839
885
  * Clear all snippets
840
886
  */
841
887
  function clearSnippets() {
888
+ memoryCache$2 = [];
842
889
  try {
843
890
  localStorage.removeItem(STORAGE_KEY$3);
844
- events.emit("settings:changed", { setting: "snippets" });
845
891
  }
846
892
  catch {
847
893
  // Silently fail
848
894
  }
895
+ events.emit("settings:changed", { setting: "snippets" });
849
896
  }
850
897
  /**
851
898
  * Get snippet count info
@@ -861,12 +908,14 @@ function isAtSnippetLimit() {
861
908
  }
862
909
  const snippetsStore = {
863
910
  getSnippets,
911
+ setSnippets,
864
912
  addSnippet,
865
913
  updateSnippet,
866
914
  deleteSnippet,
867
915
  clearSnippets,
868
916
  getSnippetCount,
869
917
  isAtSnippetLimit,
918
+ resetMemoryCache: resetMemoryCache$1,
870
919
  MAX_SNIPPETS,
871
920
  MAX_TRIGGER_LENGTH,
872
921
  MAX_EXPANSION_LENGTH,
@@ -879,6 +928,11 @@ const snippetsStore = {
879
928
  const STORAGE_KEY$2 = "speechos_vocabulary";
880
929
  const MAX_TERMS = 50;
881
930
  const MAX_TERM_LENGTH = 50;
931
+ /**
932
+ * In-memory cache for vocabulary. When server sync is enabled, this is the
933
+ * source of truth. localStorage is only used when server sync is disabled.
934
+ */
935
+ let memoryCache$1 = null;
882
936
  /**
883
937
  * Generate a unique ID for vocabulary entries
884
938
  */
@@ -886,15 +940,18 @@ function generateId$1() {
886
940
  return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
887
941
  }
888
942
  /**
889
- * Get all vocabulary terms from localStorage
943
+ * Get all vocabulary terms. Prefers in-memory cache (from server sync),
944
+ * then falls back to localStorage.
890
945
  */
891
946
  function getVocabulary() {
947
+ if (memoryCache$1 !== null) {
948
+ return [...memoryCache$1].sort((a, b) => b.createdAt - a.createdAt);
949
+ }
892
950
  try {
893
951
  const stored = localStorage.getItem(STORAGE_KEY$2);
894
952
  if (!stored)
895
953
  return [];
896
954
  const entries = JSON.parse(stored);
897
- // Return newest first
898
955
  return entries.sort((a, b) => b.createdAt - a.createdAt);
899
956
  }
900
957
  catch {
@@ -902,14 +959,27 @@ function getVocabulary() {
902
959
  }
903
960
  }
904
961
  /**
905
- * Save vocabulary to localStorage
962
+ * Set vocabulary directly (used by settings sync from server data).
963
+ */
964
+ function setVocabulary(terms) {
965
+ memoryCache$1 = terms.slice(0, MAX_TERMS);
966
+ }
967
+ /**
968
+ * Reset memory cache (for testing only)
969
+ */
970
+ function resetMemoryCache() {
971
+ memoryCache$1 = null;
972
+ }
973
+ /**
974
+ * Save vocabulary (updates memory cache and tries localStorage)
906
975
  */
907
976
  function saveVocabulary(terms) {
977
+ memoryCache$1 = terms;
908
978
  try {
909
979
  localStorage.setItem(STORAGE_KEY$2, JSON.stringify(terms));
910
980
  }
911
981
  catch {
912
- // localStorage full or unavailable - silently fail
982
+ // localStorage full or unavailable - memory cache still updated
913
983
  }
914
984
  }
915
985
  /**
@@ -977,13 +1047,14 @@ function deleteTerm(id) {
977
1047
  * Clear all vocabulary
978
1048
  */
979
1049
  function clearVocabulary() {
1050
+ memoryCache$1 = [];
980
1051
  try {
981
1052
  localStorage.removeItem(STORAGE_KEY$2);
982
- events.emit("settings:changed", { setting: "vocabulary" });
983
1053
  }
984
1054
  catch {
985
1055
  // Silently fail
986
1056
  }
1057
+ events.emit("settings:changed", { setting: "vocabulary" });
987
1058
  }
988
1059
  /**
989
1060
  * Get vocabulary count info
@@ -999,11 +1070,13 @@ function isAtVocabularyLimit() {
999
1070
  }
1000
1071
  const vocabularyStore = {
1001
1072
  getVocabulary,
1073
+ setVocabulary,
1002
1074
  addTerm,
1003
1075
  deleteTerm,
1004
1076
  clearVocabulary,
1005
1077
  getVocabularyCount,
1006
1078
  isAtVocabularyLimit,
1079
+ resetMemoryCache,
1007
1080
  MAX_TERMS,
1008
1081
  MAX_TERM_LENGTH,
1009
1082
  };
@@ -1101,6 +1174,462 @@ const audioSettings = {
1101
1174
  resetAudioSettings,
1102
1175
  };
1103
1176
 
1177
+ /**
1178
+ * Transcript history store
1179
+ * Persists transcripts to localStorage for viewing in the settings modal
1180
+ */
1181
+ const STORAGE_KEY = "speechos_transcripts";
1182
+ const MAX_ENTRIES = 50;
1183
+ /**
1184
+ * In-memory cache for transcripts. When server sync is enabled, this is the
1185
+ * source of truth. localStorage is only used when server sync is disabled.
1186
+ */
1187
+ let memoryCache = null;
1188
+ /**
1189
+ * Generate a unique ID for transcript entries
1190
+ */
1191
+ function generateId() {
1192
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1193
+ }
1194
+ /**
1195
+ * Get all transcripts. Prefers in-memory cache (from server sync),
1196
+ * then falls back to localStorage.
1197
+ */
1198
+ function getTranscripts() {
1199
+ // If we have in-memory data (from server sync), use it
1200
+ if (memoryCache !== null) {
1201
+ return [...memoryCache].sort((a, b) => b.timestamp - a.timestamp);
1202
+ }
1203
+ // Fall back to localStorage (when server sync is disabled)
1204
+ try {
1205
+ const stored = localStorage.getItem(STORAGE_KEY);
1206
+ if (!stored)
1207
+ return [];
1208
+ const entries = JSON.parse(stored);
1209
+ return entries.sort((a, b) => b.timestamp - a.timestamp);
1210
+ }
1211
+ catch {
1212
+ return [];
1213
+ }
1214
+ }
1215
+ /**
1216
+ * Set transcripts directly (used by settings sync from server data).
1217
+ * Server data is the source of truth - just update memory cache.
1218
+ */
1219
+ function setTranscripts(entries) {
1220
+ memoryCache = entries.slice(0, MAX_ENTRIES);
1221
+ }
1222
+ /**
1223
+ * Save a new transcript entry
1224
+ */
1225
+ function saveTranscript(text, action, originalTextOrOptions) {
1226
+ const entry = {
1227
+ id: generateId(),
1228
+ text,
1229
+ timestamp: Date.now(),
1230
+ action,
1231
+ };
1232
+ // Handle edit action with originalText string
1233
+ if (action === "edit" && typeof originalTextOrOptions === "string") {
1234
+ entry.originalText = originalTextOrOptions;
1235
+ }
1236
+ // Handle command action with options object
1237
+ if (action === "command" && typeof originalTextOrOptions === "object") {
1238
+ const options = originalTextOrOptions;
1239
+ if (options.inputText !== undefined)
1240
+ entry.inputText = options.inputText;
1241
+ if (options.commandResult !== undefined)
1242
+ entry.commandResult = options.commandResult;
1243
+ if (options.commandConfig !== undefined)
1244
+ entry.commandConfig = options.commandConfig;
1245
+ }
1246
+ const entries = getTranscripts();
1247
+ entries.unshift(entry);
1248
+ // Prune to max entries
1249
+ const pruned = entries.slice(0, MAX_ENTRIES);
1250
+ // Update memory cache (always)
1251
+ memoryCache = pruned;
1252
+ // Try to persist to localStorage (for when server sync is disabled)
1253
+ try {
1254
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(pruned));
1255
+ }
1256
+ catch {
1257
+ // Quota exceeded - memory cache is still updated
1258
+ }
1259
+ // Emit settings change event to trigger sync
1260
+ events.emit("settings:changed", { setting: "history" });
1261
+ return entry;
1262
+ }
1263
+ /**
1264
+ * Clear all transcript history
1265
+ */
1266
+ function clearTranscripts() {
1267
+ memoryCache = [];
1268
+ try {
1269
+ localStorage.removeItem(STORAGE_KEY);
1270
+ }
1271
+ catch {
1272
+ // Silently fail
1273
+ }
1274
+ events.emit("settings:changed", { setting: "history" });
1275
+ }
1276
+ /**
1277
+ * Delete a single transcript by ID
1278
+ */
1279
+ function deleteTranscript(id) {
1280
+ const entries = getTranscripts().filter((e) => e.id !== id);
1281
+ memoryCache = entries;
1282
+ try {
1283
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
1284
+ }
1285
+ catch {
1286
+ // Silently fail
1287
+ }
1288
+ events.emit("settings:changed", { setting: "history" });
1289
+ }
1290
+ const transcriptStore = {
1291
+ getTranscripts,
1292
+ setTranscripts,
1293
+ saveTranscript,
1294
+ clearTranscripts,
1295
+ deleteTranscript,
1296
+ };
1297
+
1298
+ /**
1299
+ * Settings sync manager
1300
+ * Syncs user settings (language, vocabulary, snippets, history) with the server
1301
+ */
1302
+ // Sync debounce delay in milliseconds
1303
+ const SYNC_DEBOUNCE_MS = 2000;
1304
+ // Maximum retry attempts
1305
+ const MAX_RETRIES = 3;
1306
+ // Base retry delay in milliseconds (exponential backoff)
1307
+ const BASE_RETRY_DELAY_MS = 2000;
1308
+ /**
1309
+ * Settings sync manager singleton
1310
+ */
1311
+ class SettingsSync {
1312
+ constructor() {
1313
+ this.syncTimer = null;
1314
+ this.isSyncing = false;
1315
+ this.retryCount = 0;
1316
+ this.isInitialized = false;
1317
+ this.unsubscribe = null;
1318
+ /** When true, sync is disabled due to CSP or network restrictions */
1319
+ this.syncDisabled = false;
1320
+ }
1321
+ /**
1322
+ * Make a fetch request using custom fetchHandler if configured, otherwise native fetch.
1323
+ * This allows the Chrome extension to route fetch traffic through the service worker
1324
+ * to bypass page CSP restrictions.
1325
+ */
1326
+ async doFetch(url, options) {
1327
+ const config = getConfig();
1328
+ const customHandler = getFetchHandler();
1329
+ if (customHandler) {
1330
+ if (config.debug) {
1331
+ console.log("[SpeechOS] Using custom fetch handler (extension proxy)", options.method, url);
1332
+ }
1333
+ return customHandler(url, options);
1334
+ }
1335
+ if (config.debug) {
1336
+ console.log("[SpeechOS] Using native fetch", options.method, url);
1337
+ }
1338
+ // Use native fetch and wrap response to match FetchResponse interface
1339
+ const response = await fetch(url, options);
1340
+ return response;
1341
+ }
1342
+ /**
1343
+ * Initialize the settings sync manager
1344
+ * If a settingsToken is configured, loads settings from server
1345
+ */
1346
+ async init() {
1347
+ const token = getSettingsToken();
1348
+ if (!token) {
1349
+ // No token configured, sync is disabled
1350
+ return;
1351
+ }
1352
+ if (this.isInitialized) {
1353
+ return;
1354
+ }
1355
+ this.isInitialized = true;
1356
+ // Subscribe to settings changes
1357
+ this.unsubscribe = events.on("settings:changed", () => {
1358
+ this.scheduleSyncToServer();
1359
+ });
1360
+ // Load settings from server
1361
+ await this.loadFromServer();
1362
+ }
1363
+ /**
1364
+ * Stop the sync manager and clean up
1365
+ */
1366
+ destroy() {
1367
+ if (this.syncTimer) {
1368
+ clearTimeout(this.syncTimer);
1369
+ this.syncTimer = null;
1370
+ }
1371
+ if (this.unsubscribe) {
1372
+ this.unsubscribe();
1373
+ this.unsubscribe = null;
1374
+ }
1375
+ this.isInitialized = false;
1376
+ this.retryCount = 0;
1377
+ this.syncDisabled = false;
1378
+ }
1379
+ /**
1380
+ * Load settings from the server and merge with local
1381
+ */
1382
+ async loadFromServer() {
1383
+ const token = getSettingsToken();
1384
+ if (!token) {
1385
+ return;
1386
+ }
1387
+ const config = getConfig();
1388
+ try {
1389
+ const response = await this.doFetch(`${config.host}/api/user-settings/`, {
1390
+ method: "GET",
1391
+ headers: {
1392
+ Authorization: `Bearer ${token}`,
1393
+ "Content-Type": "application/json",
1394
+ },
1395
+ });
1396
+ if (config.debug) {
1397
+ console.log("[SpeechOS] Settings fetch response:", response.status, response.ok ? "OK" : response.statusText);
1398
+ }
1399
+ if (response.status === 404) {
1400
+ // No settings on server yet (new user) - sync local settings to server
1401
+ if (config.debug) {
1402
+ console.log("[SpeechOS] No server settings found, syncing local to server");
1403
+ }
1404
+ await this.syncToServer();
1405
+ return;
1406
+ }
1407
+ if (response.status === 401 || response.status === 403) {
1408
+ // Token expired or invalid
1409
+ this.handleTokenExpired();
1410
+ return;
1411
+ }
1412
+ if (!response.ok) {
1413
+ throw new Error(`Server returned ${response.status}`);
1414
+ }
1415
+ const serverSettings = (await response.json());
1416
+ if (config.debug) {
1417
+ console.log("[SpeechOS] Settings received from server:", {
1418
+ language: serverSettings.language,
1419
+ vocabularyCount: serverSettings.vocabulary?.length ?? 0,
1420
+ snippetsCount: serverSettings.snippets?.length ?? 0,
1421
+ historyCount: serverSettings.history?.length ?? 0,
1422
+ lastSyncedAt: serverSettings.lastSyncedAt,
1423
+ });
1424
+ if (serverSettings.history?.length > 0) {
1425
+ console.log("[SpeechOS] History entries:", serverSettings.history);
1426
+ }
1427
+ }
1428
+ this.mergeSettings(serverSettings);
1429
+ events.emit("settings:loaded", undefined);
1430
+ if (config.debug) {
1431
+ console.log("[SpeechOS] Settings merged and loaded");
1432
+ }
1433
+ }
1434
+ catch (error) {
1435
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1436
+ // Check if this is a CSP/network restriction - disable sync permanently for this session
1437
+ if (this.isNetworkRestrictionError(error)) {
1438
+ this.syncDisabled = true;
1439
+ if (config.debug) {
1440
+ console.log("[SpeechOS] Settings sync disabled (CSP/network restriction), using localStorage only");
1441
+ }
1442
+ }
1443
+ else if (config.debug) {
1444
+ console.warn("[SpeechOS] Failed to load settings from server:", errorMessage);
1445
+ }
1446
+ events.emit("settings:syncFailed", { error: errorMessage });
1447
+ // Continue with local settings on error
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Merge server settings with local (server wins).
1452
+ * Uses store setters to update memory cache - localStorage is a fallback.
1453
+ */
1454
+ mergeSettings(serverSettings) {
1455
+ // Language settings - server wins
1456
+ if (serverSettings.language) {
1457
+ setLanguageSettings(serverSettings.language);
1458
+ }
1459
+ // Vocabulary - server wins
1460
+ if (serverSettings.vocabulary) {
1461
+ setVocabulary(serverSettings.vocabulary);
1462
+ }
1463
+ // Snippets - server wins
1464
+ if (serverSettings.snippets) {
1465
+ setSnippets(serverSettings.snippets);
1466
+ }
1467
+ // History - server wins
1468
+ if (serverSettings.history) {
1469
+ setTranscripts(serverSettings.history);
1470
+ }
1471
+ }
1472
+ /**
1473
+ * Schedule a debounced sync to server
1474
+ */
1475
+ scheduleSyncToServer() {
1476
+ const token = getSettingsToken();
1477
+ if (!token || this.syncDisabled) {
1478
+ return;
1479
+ }
1480
+ // Cancel any pending sync
1481
+ if (this.syncTimer) {
1482
+ clearTimeout(this.syncTimer);
1483
+ }
1484
+ // Schedule new sync
1485
+ this.syncTimer = setTimeout(() => {
1486
+ this.syncToServer();
1487
+ }, SYNC_DEBOUNCE_MS);
1488
+ }
1489
+ /**
1490
+ * Sync current settings to server
1491
+ */
1492
+ async syncToServer() {
1493
+ const token = getSettingsToken();
1494
+ if (!token || this.isSyncing || this.syncDisabled) {
1495
+ return;
1496
+ }
1497
+ this.isSyncing = true;
1498
+ const config = getConfig();
1499
+ try {
1500
+ const languageSettings = getLanguageSettings();
1501
+ const vocabulary = getVocabulary();
1502
+ const snippets = getSnippets();
1503
+ const transcripts = getTranscripts();
1504
+ const payload = {
1505
+ language: {
1506
+ inputLanguageCode: languageSettings.inputLanguageCode,
1507
+ outputLanguageCode: languageSettings.outputLanguageCode,
1508
+ smartFormat: languageSettings.smartFormat,
1509
+ },
1510
+ vocabulary: vocabulary.map((v) => ({
1511
+ id: v.id,
1512
+ term: v.term,
1513
+ createdAt: v.createdAt,
1514
+ })),
1515
+ snippets: snippets.map((s) => ({
1516
+ id: s.id,
1517
+ trigger: s.trigger,
1518
+ expansion: s.expansion,
1519
+ createdAt: s.createdAt,
1520
+ })),
1521
+ // Sync history (excluding commandConfig to reduce payload size)
1522
+ history: transcripts.map((t) => ({
1523
+ id: t.id,
1524
+ text: t.text,
1525
+ timestamp: t.timestamp,
1526
+ action: t.action,
1527
+ ...(t.originalText && { originalText: t.originalText }),
1528
+ ...(t.inputText && { inputText: t.inputText }),
1529
+ ...(t.commandResult !== undefined && { commandResult: t.commandResult }),
1530
+ })),
1531
+ };
1532
+ const response = await this.doFetch(`${config.host}/api/user-settings/`, {
1533
+ method: "PUT",
1534
+ headers: {
1535
+ Authorization: `Bearer ${token}`,
1536
+ "Content-Type": "application/json",
1537
+ },
1538
+ body: JSON.stringify(payload),
1539
+ });
1540
+ if (response.status === 401 || response.status === 403) {
1541
+ this.handleTokenExpired();
1542
+ return;
1543
+ }
1544
+ if (!response.ok) {
1545
+ throw new Error(`Server returned ${response.status}`);
1546
+ }
1547
+ // Reset retry count on success
1548
+ this.retryCount = 0;
1549
+ events.emit("settings:synced", undefined);
1550
+ if (config.debug) {
1551
+ console.log("[SpeechOS] Settings synced to server");
1552
+ }
1553
+ }
1554
+ catch (error) {
1555
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1556
+ // Check if this is a CSP/network restriction - disable sync permanently for this session
1557
+ if (this.isNetworkRestrictionError(error)) {
1558
+ this.syncDisabled = true;
1559
+ if (config.debug) {
1560
+ console.log("[SpeechOS] Settings sync disabled (CSP/network restriction), using localStorage only");
1561
+ }
1562
+ events.emit("settings:syncFailed", { error: errorMessage });
1563
+ // Don't retry - CSP errors are permanent
1564
+ }
1565
+ else {
1566
+ if (config.debug) {
1567
+ console.warn("[SpeechOS] Failed to sync settings to server:", errorMessage);
1568
+ }
1569
+ events.emit("settings:syncFailed", { error: errorMessage });
1570
+ // Retry with exponential backoff (only for transient errors)
1571
+ this.scheduleRetry();
1572
+ }
1573
+ }
1574
+ finally {
1575
+ this.isSyncing = false;
1576
+ }
1577
+ }
1578
+ /**
1579
+ * Schedule a retry with exponential backoff
1580
+ */
1581
+ scheduleRetry() {
1582
+ if (this.retryCount >= MAX_RETRIES) {
1583
+ const config = getConfig();
1584
+ if (config.debug) {
1585
+ console.warn("[SpeechOS] Max retries reached, giving up sync");
1586
+ }
1587
+ this.retryCount = 0;
1588
+ return;
1589
+ }
1590
+ this.retryCount++;
1591
+ const delay = BASE_RETRY_DELAY_MS * Math.pow(2, this.retryCount - 1);
1592
+ this.syncTimer = setTimeout(() => {
1593
+ this.syncToServer();
1594
+ }, delay);
1595
+ }
1596
+ /**
1597
+ * Check if an error is a CSP or network restriction error
1598
+ * These errors are permanent and shouldn't trigger retries
1599
+ */
1600
+ isNetworkRestrictionError(error) {
1601
+ if (error instanceof TypeError) {
1602
+ const message = error.message.toLowerCase();
1603
+ // Common CSP/network error messages
1604
+ return (message.includes("failed to fetch") ||
1605
+ message.includes("network request failed") ||
1606
+ message.includes("content security policy") ||
1607
+ message.includes("csp") ||
1608
+ message.includes("blocked"));
1609
+ }
1610
+ return false;
1611
+ }
1612
+ /**
1613
+ * Handle token expiration
1614
+ */
1615
+ handleTokenExpired() {
1616
+ clearSettingsToken();
1617
+ events.emit("settings:tokenExpired", undefined);
1618
+ const config = getConfig();
1619
+ if (config.debug) {
1620
+ console.warn("[SpeechOS] Settings token expired");
1621
+ }
1622
+ }
1623
+ /**
1624
+ * Check if sync is enabled (token is configured)
1625
+ */
1626
+ isEnabled() {
1627
+ return !!getSettingsToken();
1628
+ }
1629
+ }
1630
+ // Singleton instance
1631
+ const settingsSync = new SettingsSync();
1632
+
1104
1633
  /******************************************************************************
1105
1634
  Copyright (c) Microsoft Corporation.
1106
1635
 
@@ -1184,6 +1713,9 @@ const t$1=t=>(e,o)=>{ void 0!==o?o.addInitializer(()=>{customElements.define(t,e
1184
1713
  */
1185
1714
  const themeStyles = i$4 `
1186
1715
  :host {
1716
+ /* Font stack - system fonts for consistent rendering across sites */
1717
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
1718
+
1187
1719
  /* Color tokens */
1188
1720
  --speechos-primary: #10B981;
1189
1721
  --speechos-primary-hover: #059669;
@@ -1307,100 +1839,6 @@ i$4 `
1307
1839
  }
1308
1840
  `;
1309
1841
 
1310
- /**
1311
- * Transcript history store
1312
- * Persists transcripts to localStorage for viewing in the settings modal
1313
- */
1314
- const STORAGE_KEY = "speechos_transcripts";
1315
- const MAX_ENTRIES = 50;
1316
- /**
1317
- * Generate a unique ID for transcript entries
1318
- */
1319
- function generateId() {
1320
- return `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
1321
- }
1322
- /**
1323
- * Get all transcripts from localStorage
1324
- */
1325
- function getTranscripts() {
1326
- try {
1327
- const stored = localStorage.getItem(STORAGE_KEY);
1328
- if (!stored)
1329
- return [];
1330
- const entries = JSON.parse(stored);
1331
- // Return newest first
1332
- return entries.sort((a, b) => b.timestamp - a.timestamp);
1333
- }
1334
- catch {
1335
- return [];
1336
- }
1337
- }
1338
- /**
1339
- * Save a new transcript entry
1340
- */
1341
- function saveTranscript(text, action, originalTextOrOptions) {
1342
- const entry = {
1343
- id: generateId(),
1344
- text,
1345
- timestamp: Date.now(),
1346
- action,
1347
- };
1348
- // Handle edit action with originalText string
1349
- if (action === "edit" && typeof originalTextOrOptions === "string") {
1350
- entry.originalText = originalTextOrOptions;
1351
- }
1352
- // Handle command action with options object
1353
- if (action === "command" && typeof originalTextOrOptions === "object") {
1354
- const options = originalTextOrOptions;
1355
- if (options.inputText !== undefined)
1356
- entry.inputText = options.inputText;
1357
- if (options.commandResult !== undefined)
1358
- entry.commandResult = options.commandResult;
1359
- if (options.commandConfig !== undefined)
1360
- entry.commandConfig = options.commandConfig;
1361
- }
1362
- const entries = getTranscripts();
1363
- entries.unshift(entry);
1364
- // Prune to max entries
1365
- const pruned = entries.slice(0, MAX_ENTRIES);
1366
- try {
1367
- localStorage.setItem(STORAGE_KEY, JSON.stringify(pruned));
1368
- }
1369
- catch {
1370
- // localStorage full or unavailable - silently fail
1371
- }
1372
- return entry;
1373
- }
1374
- /**
1375
- * Clear all transcript history
1376
- */
1377
- function clearTranscripts() {
1378
- try {
1379
- localStorage.removeItem(STORAGE_KEY);
1380
- }
1381
- catch {
1382
- // Silently fail
1383
- }
1384
- }
1385
- /**
1386
- * Delete a single transcript by ID
1387
- */
1388
- function deleteTranscript(id) {
1389
- const entries = getTranscripts().filter((e) => e.id !== id);
1390
- try {
1391
- localStorage.setItem(STORAGE_KEY, JSON.stringify(entries));
1392
- }
1393
- catch {
1394
- // Silently fail
1395
- }
1396
- }
1397
- const transcriptStore = {
1398
- getTranscripts: getTranscripts,
1399
- saveTranscript: saveTranscript,
1400
- clearTranscripts: clearTranscripts,
1401
- deleteTranscript: deleteTranscript,
1402
- };
1403
-
1404
1842
  function isNativeField(field) {
1405
1843
  return field instanceof HTMLInputElement || field instanceof HTMLTextAreaElement;
1406
1844
  }
@@ -3398,6 +3836,8 @@ const modalLayoutStyles = i$4 `
3398
3836
  inset: 0;
3399
3837
  pointer-events: none;
3400
3838
  z-index: calc(var(--speechos-z-base) + 100);
3839
+ /* Ensure consistent font rendering across all sites */
3840
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
3401
3841
  }
3402
3842
 
3403
3843
  .modal-overlay {
@@ -3886,6 +4326,7 @@ let SpeechOSHistoryTab = class SpeechOSHistoryTab extends i$1 {
3886
4326
  constructor() {
3887
4327
  super(...arguments);
3888
4328
  this.transcripts = [];
4329
+ this.unsubscribeSettingsLoaded = null;
3889
4330
  }
3890
4331
  static { this.styles = [
3891
4332
  themeStyles,
@@ -4022,11 +4463,36 @@ let SpeechOSHistoryTab = class SpeechOSHistoryTab extends i$1 {
4022
4463
  background: rgba(239, 68, 68, 0.18);
4023
4464
  border-color: rgba(239, 68, 68, 0.25);
4024
4465
  }
4466
+
4467
+ .command-matched {
4468
+ font-size: 12px;
4469
+ color: rgba(255, 255, 255, 0.5);
4470
+ margin-top: 6px;
4471
+ }
4472
+
4473
+ .command-matched code {
4474
+ background: rgba(245, 158, 11, 0.15);
4475
+ color: #fbbf24;
4476
+ padding: 2px 6px;
4477
+ border-radius: 4px;
4478
+ font-family: monospace;
4479
+ }
4025
4480
  `,
4026
4481
  ]; }
4027
4482
  connectedCallback() {
4028
4483
  super.connectedCallback();
4029
4484
  this.loadTranscripts();
4485
+ // Refresh when settings are loaded from the server (history may have changed)
4486
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
4487
+ this.loadTranscripts();
4488
+ });
4489
+ }
4490
+ disconnectedCallback() {
4491
+ super.disconnectedCallback();
4492
+ if (this.unsubscribeSettingsLoaded) {
4493
+ this.unsubscribeSettingsLoaded();
4494
+ this.unsubscribeSettingsLoaded = null;
4495
+ }
4030
4496
  }
4031
4497
  /** Reload transcripts from store */
4032
4498
  refresh() {
@@ -4097,7 +4563,13 @@ let SpeechOSHistoryTab = class SpeechOSHistoryTab extends i$1 {
4097
4563
  renderCommandDetails(entry) {
4098
4564
  // Show the transcript text (what the user said)
4099
4565
  const displayText = entry.inputText || entry.text;
4100
- return b `<div class="transcript-text">${displayText}</div>`;
4566
+ const commandName = entry.commandResult?.name;
4567
+ return b `
4568
+ <div class="transcript-text">${displayText}</div>
4569
+ ${commandName
4570
+ ? b `<div class="command-matched">Matched: <code>${commandName}</code></div>`
4571
+ : null}
4572
+ `;
4101
4573
  }
4102
4574
  getCopyText(entry) {
4103
4575
  if (entry.action === "command") {
@@ -4647,6 +5119,7 @@ let SpeechOSSettingsTab = class SpeechOSSettingsTab extends i$1 {
4647
5119
  this.permissionGranted = false;
4648
5120
  this.smartFormatEnabled = true;
4649
5121
  this.savedIndicatorTimeout = null;
5122
+ this.unsubscribeSettingsLoaded = null;
4650
5123
  }
4651
5124
  static { this.styles = [
4652
5125
  themeStyles,
@@ -4821,12 +5294,20 @@ let SpeechOSSettingsTab = class SpeechOSSettingsTab extends i$1 {
4821
5294
  connectedCallback() {
4822
5295
  super.connectedCallback();
4823
5296
  this.loadSettings();
5297
+ // Refresh when settings are loaded from the server
5298
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
5299
+ this.loadSettings();
5300
+ });
4824
5301
  }
4825
5302
  disconnectedCallback() {
4826
5303
  super.disconnectedCallback();
4827
5304
  if (this.savedIndicatorTimeout) {
4828
5305
  clearTimeout(this.savedIndicatorTimeout);
4829
5306
  }
5307
+ if (this.unsubscribeSettingsLoaded) {
5308
+ this.unsubscribeSettingsLoaded();
5309
+ this.unsubscribeSettingsLoaded = null;
5310
+ }
4830
5311
  this.isTestingMic = false;
4831
5312
  }
4832
5313
  /** Called when tab becomes active */
@@ -5125,6 +5606,7 @@ let SpeechOSSnippetsTab = class SpeechOSSnippetsTab extends i$1 {
5125
5606
  this.trigger = "";
5126
5607
  this.expansion = "";
5127
5608
  this.error = "";
5609
+ this.unsubscribeSettingsLoaded = null;
5128
5610
  }
5129
5611
  static { this.styles = [
5130
5612
  themeStyles,
@@ -5230,6 +5712,17 @@ let SpeechOSSnippetsTab = class SpeechOSSnippetsTab extends i$1 {
5230
5712
  connectedCallback() {
5231
5713
  super.connectedCallback();
5232
5714
  this.loadSnippets();
5715
+ // Refresh when settings are loaded from the server
5716
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
5717
+ this.loadSnippets();
5718
+ });
5719
+ }
5720
+ disconnectedCallback() {
5721
+ super.disconnectedCallback();
5722
+ if (this.unsubscribeSettingsLoaded) {
5723
+ this.unsubscribeSettingsLoaded();
5724
+ this.unsubscribeSettingsLoaded = null;
5725
+ }
5233
5726
  }
5234
5727
  /** Reload snippets from store */
5235
5728
  refresh() {
@@ -5472,6 +5965,7 @@ let SpeechOSVocabularyTab = class SpeechOSVocabularyTab extends i$1 {
5472
5965
  this.showForm = false;
5473
5966
  this.term = "";
5474
5967
  this.error = "";
5968
+ this.unsubscribeSettingsLoaded = null;
5475
5969
  }
5476
5970
  static { this.styles = [
5477
5971
  themeStyles,
@@ -5524,6 +6018,17 @@ let SpeechOSVocabularyTab = class SpeechOSVocabularyTab extends i$1 {
5524
6018
  connectedCallback() {
5525
6019
  super.connectedCallback();
5526
6020
  this.loadVocabulary();
6021
+ // Refresh when settings are loaded from the server
6022
+ this.unsubscribeSettingsLoaded = events.on("settings:loaded", () => {
6023
+ this.loadVocabulary();
6024
+ });
6025
+ }
6026
+ disconnectedCallback() {
6027
+ super.disconnectedCallback();
6028
+ if (this.unsubscribeSettingsLoaded) {
6029
+ this.unsubscribeSettingsLoaded();
6030
+ this.unsubscribeSettingsLoaded = null;
6031
+ }
5527
6032
  }
5528
6033
  /** Reload vocabulary from store */
5529
6034
  refresh() {
@@ -7137,6 +7642,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7137
7642
  const originalContent = this.getElementContent(target) || "";
7138
7643
  if (tagName === "input" || tagName === "textarea") {
7139
7644
  const inputEl = target;
7645
+ // Ensure DOM focus is on the input before inserting
7646
+ inputEl.focus();
7140
7647
  // Restore cursor position before inserting
7141
7648
  const start = this.dictationCursorStart ?? inputEl.value.length;
7142
7649
  const end = this.dictationCursorEnd ?? inputEl.value.length;
@@ -7473,8 +7980,6 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7473
7980
  // Note: command:complete event is already emitted by the backend
7474
7981
  // when the command_result message is received, so we don't emit here
7475
7982
  state.completeRecording();
7476
- // Keep widget visible but collapsed (just mic button, no action bubbles)
7477
- state.setState({ isExpanded: false });
7478
7983
  // Show command feedback
7479
7984
  this.showActionFeedback(result ? "command-success" : "command-none");
7480
7985
  backend.disconnect().catch(() => { });
@@ -7632,6 +8137,8 @@ let SpeechOSWidget = class SpeechOSWidget extends i$1 {
7632
8137
  if (tagName === "input" || tagName === "textarea") {
7633
8138
  const inputEl = target;
7634
8139
  originalContent = inputEl.value;
8140
+ // Ensure DOM focus is on the input before editing
8141
+ inputEl.focus();
7635
8142
  // Restore the original selection/cursor position
7636
8143
  const selectionStart = this.editSelectionStart ?? 0;
7637
8144
  const selectionEnd = this.editSelectionEnd ?? inputEl.value.length;
@@ -7799,6 +8306,30 @@ SpeechOSWidget = SpeechOSWidget_1 = __decorate([
7799
8306
  t$1("speechos-widget")
7800
8307
  ], SpeechOSWidget);
7801
8308
 
8309
+ /**
8310
+ * UI module exports
8311
+ * Lit-based Shadow DOM components
8312
+ */
8313
+ // Patch customElements.define to silently ignore duplicate registrations for speechos-* elements.
8314
+ // This prevents errors when the extension loads on a page that already has SpeechOS.
8315
+ // The patch is scoped to only affect speechos-* tags to avoid unintended effects on host pages.
8316
+ const originalDefine = customElements.define.bind(customElements);
8317
+ customElements.define = (name, constructor, options) => {
8318
+ // Only intercept speechos-* elements
8319
+ if (name.startsWith("speechos-")) {
8320
+ if (customElements.get(name) === undefined) {
8321
+ originalDefine(name, constructor, options);
8322
+ }
8323
+ // Skip silently if already registered
8324
+ }
8325
+ else {
8326
+ // Pass through for non-speechos elements
8327
+ originalDefine(name, constructor, options);
8328
+ }
8329
+ };
8330
+ // Restore original customElements.define after our components are registered
8331
+ customElements.define = originalDefine;
8332
+
7802
8333
  /**
7803
8334
  * Main SpeechOS Client SDK class
7804
8335
  * Composes core logic with UI components
@@ -7880,6 +8411,13 @@ class SpeechOS {
7880
8411
  state.show();
7881
8412
  }
7882
8413
  this.isInitialized = true;
8414
+ // Initialize settings sync if token is configured
8415
+ // This loads settings from server and subscribes to changes
8416
+ settingsSync.init().catch((error) => {
8417
+ if (finalConfig.debug) {
8418
+ console.warn("[SpeechOS] Settings sync initialization failed:", error);
8419
+ }
8420
+ });
7883
8421
  // Log initialization in debug mode
7884
8422
  if (finalConfig.debug) {
7885
8423
  console.log("[SpeechOS] Initialized with config:", finalConfig);
@@ -7910,6 +8448,8 @@ class SpeechOS {
7910
8448
  resetClientConfig();
7911
8449
  // Reset text input handler to default
7912
8450
  resetTextInputHandler();
8451
+ // Stop settings sync
8452
+ settingsSync.destroy();
7913
8453
  // Clear instance
7914
8454
  this.instance = null;
7915
8455
  this.isInitialized = false;
@@ -8071,5 +8611,5 @@ class SpeechOS {
8071
8611
  // Version
8072
8612
  const VERSION = "0.1.0";
8073
8613
 
8074
- export { DefaultTextInputHandler, FormDetector, SUPPORTED_LANGUAGES, SpeechOS, VERSION, addSnippet, addTerm, audioSettings, clearSnippets, clearTranscripts, clearVocabulary, SpeechOS as default, defaultTextInputHandler, deleteSnippet, deleteTerm, deleteTranscript, formDetector, getAudioDeviceId, getAudioInputDevices, getAudioSettings, getClientConfig, getCommands, getInputLanguageCode, getInputLanguageName, getLanguageByCode, getLanguageCode, getLanguageName, getLanguageSettings, getOutputLanguageCode, getOutputLanguageName, getSessionSettings, getSmartFormatEnabled, getSnippetCount, getSnippets, getTextInputHandler, getTranscripts, getVocabulary, getVocabularyCount, getZIndex, hasCommands, isAtSnippetLimit, isAtVocabularyLimit, isSelectedDeviceAvailable, languageSettings, resetAudioSettings, resetClientConfig, resetLanguageSettings, resetTextInputHandler, saveTranscript, setAudioDeviceId, setClientConfig, setInputLanguageCode, setLanguageCode, setOutputLanguageCode, setSmartFormatEnabled, setTextInputHandler, snippetsStore, transcriptStore, updateSnippet, vocabularyStore };
8614
+ export { DefaultTextInputHandler, FormDetector, SUPPORTED_LANGUAGES, SpeechOS, VERSION, addSnippet, addTerm, audioSettings, clearSnippets, clearTranscripts, clearVocabulary, SpeechOS as default, defaultTextInputHandler, deleteSnippet, deleteTerm, deleteTranscript, formDetector, getAudioDeviceId, getAudioInputDevices, getAudioSettings, getClientConfig, getCommands, getInputLanguageCode, getInputLanguageName, getLanguageByCode, getLanguageCode, getLanguageName, getLanguageSettings, getOutputLanguageCode, getOutputLanguageName, getSessionSettings, getSmartFormatEnabled, getSnippetCount, getSnippets, getTextInputHandler, getTranscripts, getVocabulary, getVocabularyCount, getZIndex, hasCommands, isAtSnippetLimit, isAtVocabularyLimit, isSelectedDeviceAvailable, languageSettings, resetAudioSettings, resetClientConfig, resetLanguageSettings, resetTextInputHandler, saveTranscript, setAudioDeviceId, setClientConfig, setInputLanguageCode, setLanguageCode, setOutputLanguageCode, setSmartFormatEnabled, setTextInputHandler, settingsSync, snippetsStore, transcriptStore, updateSnippet, vocabularyStore };
8075
8615
  //# sourceMappingURL=index.js.map