@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.
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.cjs +694 -117
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.iife.js +715 -120
- package/dist/index.iife.js.map +1 -1
- package/dist/index.iife.min.js +136 -103
- package/dist/index.iife.min.js.map +1 -1
- package/dist/index.js +694 -119
- package/dist/index.js.map +1 -1
- package/dist/settings-sync.d.ts +66 -0
- package/dist/settings-sync.d.ts.map +1 -0
- package/dist/settings-sync.test.d.ts +5 -0
- package/dist/settings-sync.test.d.ts.map +1 -0
- package/dist/speechos.d.ts.map +1 -1
- package/dist/stores/language-settings.d.ts +12 -1
- package/dist/stores/language-settings.d.ts.map +1 -1
- package/dist/stores/language-settings.test.d.ts +5 -0
- package/dist/stores/language-settings.test.d.ts.map +1 -0
- package/dist/stores/snippets-store.d.ts +12 -1
- package/dist/stores/snippets-store.d.ts.map +1 -1
- package/dist/stores/transcript-store.d.ts +13 -2
- package/dist/stores/transcript-store.d.ts.map +1 -1
- package/dist/stores/vocabulary-store.d.ts +12 -1
- package/dist/stores/vocabulary-store.d.ts.map +1 -1
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/styles/modal-styles.d.ts.map +1 -1
- package/dist/ui/styles/theme.d.ts.map +1 -1
- package/dist/ui/tabs/history-tab.d.ts +2 -0
- package/dist/ui/tabs/history-tab.d.ts.map +1 -1
- package/dist/ui/tabs/settings-tab.d.ts +1 -0
- package/dist/ui/tabs/settings-tab.d.ts.map +1 -1
- package/dist/ui/tabs/snippets-tab.d.ts +2 -0
- package/dist/ui/tabs/snippets-tab.d.ts.map +1 -1
- package/dist/ui/tabs/vocabulary-tab.d.ts +2 -0
- package/dist/ui/tabs/vocabulary-tab.d.ts.map +1 -1
- package/dist/ui/widget.d.ts.map +1 -1
- 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: "
|
|
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
|
|
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
|
-
*
|
|
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 -
|
|
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
|
|
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
|
-
*
|
|
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 -
|
|
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
|
|
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
|
-
*
|
|
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 -
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|