@smart-cloud/ai-kit-ui 1.3.12 → 1.3.14
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/index.cjs +9 -9
- package/dist/index.js +9 -9
- package/package.json +1 -1
- package/src/doc-search/DocSearch.tsx +242 -172
package/package.json
CHANGED
|
@@ -76,6 +76,41 @@ function groupChunksByDoc(result: SearchResult | null) {
|
|
|
76
76
|
return Array.from(byDoc.values());
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
function isWordChar(ch: string) {
|
|
80
|
+
// Unicode letter/number + underscore; good for Cyrillic too
|
|
81
|
+
return Boolean(ch) && /[\p{L}\p{N}_]/u.test(ch);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Backend sometimes returns anchor `end` that lands inside a word.
|
|
86
|
+
* Snap the insertion point to a word boundary (end of current word).
|
|
87
|
+
*/
|
|
88
|
+
function snapEndToBoundary(text: string, end: number) {
|
|
89
|
+
const len = text.length;
|
|
90
|
+
if (end <= 0) return 0;
|
|
91
|
+
if (end >= len) return len;
|
|
92
|
+
|
|
93
|
+
const prev = text[end - 1] ?? "";
|
|
94
|
+
const cur = text[end] ?? "";
|
|
95
|
+
|
|
96
|
+
// If we're inside a word, walk forward until word ends.
|
|
97
|
+
if (isWordChar(prev) && isWordChar(cur)) {
|
|
98
|
+
let i = end;
|
|
99
|
+
while (i < len && isWordChar(text[i] ?? "")) i++;
|
|
100
|
+
return i;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return end;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function escapeCssId(id: string) {
|
|
107
|
+
// Prefer native CSS.escape, fallback to a minimal safe escape.
|
|
108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
109
|
+
const esc = (globalThis as any)?.CSS?.escape;
|
|
110
|
+
if (typeof esc === "function") return esc(id);
|
|
111
|
+
return id.replace(/[^a-zA-Z0-9_-]/g, "\\$&");
|
|
112
|
+
}
|
|
113
|
+
|
|
79
114
|
const DocSearchBase: FC<Props> = (props) => {
|
|
80
115
|
const {
|
|
81
116
|
autoRun = true,
|
|
@@ -202,16 +237,8 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
202
237
|
return [];
|
|
203
238
|
}
|
|
204
239
|
return selectedCategories
|
|
205
|
-
.flatMap(
|
|
206
|
-
|
|
207
|
-
metadataOptions.allowedCategories[
|
|
208
|
-
cat
|
|
209
|
-
] || [],
|
|
210
|
-
)
|
|
211
|
-
.filter(
|
|
212
|
-
(subcat, index, self) =>
|
|
213
|
-
self.indexOf(subcat) === index,
|
|
214
|
-
)
|
|
240
|
+
.flatMap((cat) => metadataOptions.allowedCategories[cat] || [])
|
|
241
|
+
.filter((subcat, index, self) => self.indexOf(subcat) === index);
|
|
215
242
|
}, [selectedCategories, metadataOptions]);
|
|
216
243
|
|
|
217
244
|
const startRecording = useCallback(async () => {
|
|
@@ -395,8 +422,6 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
395
422
|
setQuery(q);
|
|
396
423
|
}
|
|
397
424
|
|
|
398
|
-
console.log("Starting search with query:", q, "and audio:", audioBlob);
|
|
399
|
-
|
|
400
425
|
// Check if we can reuse cached audio (same blob within TTL)
|
|
401
426
|
const now = Date.now();
|
|
402
427
|
const isSameAudio =
|
|
@@ -410,16 +435,6 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
410
435
|
blob: audioBlob,
|
|
411
436
|
uploadTimestamp: now,
|
|
412
437
|
};
|
|
413
|
-
console.log("Audio cache updated for new recording");
|
|
414
|
-
} else if (isSameAudio) {
|
|
415
|
-
console.log(
|
|
416
|
-
"Reusing cached audio (no re-upload needed within",
|
|
417
|
-
Math.round(
|
|
418
|
-
(AUDIO_CACHE_TTL - (now - audioCacheRef.current!.uploadTimestamp)) /
|
|
419
|
-
1000,
|
|
420
|
-
),
|
|
421
|
-
"seconds)",
|
|
422
|
-
);
|
|
423
438
|
}
|
|
424
439
|
|
|
425
440
|
reset();
|
|
@@ -438,8 +453,8 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
438
453
|
}),
|
|
439
454
|
...(enableUserFilters &&
|
|
440
455
|
selectedSubcategories.length > 0 && {
|
|
441
|
-
|
|
442
|
-
|
|
456
|
+
userSelectedSubcategories: selectedSubcategories,
|
|
457
|
+
}),
|
|
443
458
|
...(enableUserFilters &&
|
|
444
459
|
selectedTags.length > 0 && { userSelectedTags: selectedTags }),
|
|
445
460
|
},
|
|
@@ -565,14 +580,24 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
565
580
|
}
|
|
566
581
|
|
|
567
582
|
const safeEnd = Math.min(end, summaryText.length);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
cursor
|
|
583
|
+
const snappedEnd = snapEndToBoundary(summaryText, safeEnd);
|
|
584
|
+
|
|
585
|
+
segments.push(summaryText.slice(cursor, snappedEnd));
|
|
586
|
+
|
|
587
|
+
const supHtml = refs
|
|
588
|
+
.map(
|
|
589
|
+
(n) =>
|
|
590
|
+
`<a href="#docsearch-source-${n}" data-docsearch-cite="${n}" style="color: inherit; text-decoration: none;">${n}</a>`,
|
|
591
|
+
)
|
|
592
|
+
.join(",");
|
|
593
|
+
|
|
594
|
+
segments.push(`<sup>${supHtml}</sup>`);
|
|
595
|
+
cursor = snappedEnd;
|
|
571
596
|
}
|
|
572
597
|
|
|
573
598
|
segments.push(summaryText.slice(cursor));
|
|
574
599
|
return segments.join("");
|
|
575
|
-
}, [citationAnchors, summaryText]);
|
|
600
|
+
}, [citationAnchors, summaryText, docNumberMap, chunkDocMap]);
|
|
576
601
|
|
|
577
602
|
const RootComponent: typeof Modal.Root | typeof Group =
|
|
578
603
|
variation === "modal" ? Modal.Root : Group;
|
|
@@ -731,49 +756,48 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
731
756
|
}}
|
|
732
757
|
/>
|
|
733
758
|
|
|
734
|
-
{
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
759
|
+
{/* Microphone button */}
|
|
760
|
+
{USE_AUDIO && (
|
|
761
|
+
<>
|
|
762
|
+
{audioBlob ? (
|
|
763
|
+
<Button
|
|
764
|
+
variant="outline"
|
|
765
|
+
size="sm"
|
|
766
|
+
color="red"
|
|
767
|
+
onClick={clearAudio}
|
|
768
|
+
disabled={busy}
|
|
769
|
+
title={I18n.get("Clear audio")}
|
|
770
|
+
>
|
|
771
|
+
<IconMicrophoneOff size={18} />
|
|
772
|
+
</Button>
|
|
773
|
+
) : (
|
|
774
|
+
<Button
|
|
775
|
+
variant={recording ? "filled" : "outline"}
|
|
776
|
+
size="sm"
|
|
777
|
+
color={recording ? "red" : "gray"}
|
|
778
|
+
onClick={
|
|
779
|
+
recording ? stopRecording : startRecording
|
|
780
|
+
}
|
|
781
|
+
disabled={busy}
|
|
782
|
+
title={
|
|
783
|
+
recording
|
|
784
|
+
? I18n.get("Stop recording")
|
|
785
|
+
: I18n.get("Record audio")
|
|
786
|
+
}
|
|
787
|
+
style={
|
|
788
|
+
recording
|
|
789
|
+
? {
|
|
765
790
|
transform: `scale(${1 + audioLevel / 300})`,
|
|
766
791
|
transition: "transform 0.1s ease-out",
|
|
767
792
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
793
|
+
: undefined
|
|
794
|
+
}
|
|
795
|
+
>
|
|
796
|
+
<IconMicrophone size={18} />
|
|
797
|
+
</Button>
|
|
798
|
+
)}
|
|
799
|
+
</>
|
|
800
|
+
)}
|
|
777
801
|
|
|
778
802
|
<Button
|
|
779
803
|
variant="filled"
|
|
@@ -830,51 +854,51 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
830
854
|
{/* Main categories as checkboxes */}
|
|
831
855
|
{Object.keys(metadataOptions.allowedCategories)
|
|
832
856
|
.length > 0 && (
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
857
|
+
<div>
|
|
858
|
+
<Text size="sm" fw={500} mb="xs">
|
|
859
|
+
{I18n.get("Categories")}
|
|
860
|
+
</Text>
|
|
861
|
+
<Group gap="md">
|
|
862
|
+
{Object.keys(
|
|
863
|
+
metadataOptions.allowedCategories,
|
|
864
|
+
).map((category) => (
|
|
865
|
+
<Checkbox
|
|
866
|
+
key={category}
|
|
867
|
+
label={I18n.get(category)}
|
|
868
|
+
checked={selectedCategories.includes(
|
|
869
|
+
category,
|
|
870
|
+
)}
|
|
871
|
+
onChange={(e) => {
|
|
872
|
+
if (e.currentTarget.checked) {
|
|
873
|
+
setSelectedCategories([
|
|
874
|
+
...selectedCategories,
|
|
875
|
+
category,
|
|
876
|
+
]);
|
|
877
|
+
} else {
|
|
878
|
+
setSelectedCategories(
|
|
879
|
+
selectedCategories.filter(
|
|
880
|
+
(c) => c !== category,
|
|
881
|
+
),
|
|
882
|
+
);
|
|
883
|
+
// Remove subcategories of unchecked category
|
|
884
|
+
const subcatsToRemove =
|
|
885
|
+
metadataOptions.allowedCategories[
|
|
862
886
|
category
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
887
|
+
] || [];
|
|
888
|
+
setSelectedSubcategories(
|
|
889
|
+
selectedSubcategories.filter(
|
|
890
|
+
(sc) =>
|
|
891
|
+
!subcatsToRemove.includes(sc),
|
|
892
|
+
),
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
}}
|
|
896
|
+
disabled={busy || loadingMetadata}
|
|
897
|
+
/>
|
|
898
|
+
))}
|
|
899
|
+
</Group>
|
|
900
|
+
</div>
|
|
901
|
+
)}
|
|
878
902
|
|
|
879
903
|
{/* Subcategories for selected categories */}
|
|
880
904
|
{subcategories.length > 0 && (
|
|
@@ -883,31 +907,30 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
883
907
|
{I18n.get("Subcategories")}
|
|
884
908
|
</Text>
|
|
885
909
|
<Group gap="md">
|
|
886
|
-
{subcategories
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
))}
|
|
910
|
+
{subcategories.map((subcat) => (
|
|
911
|
+
<Checkbox
|
|
912
|
+
key={subcat}
|
|
913
|
+
label={I18n.get(subcat)}
|
|
914
|
+
checked={selectedSubcategories.includes(
|
|
915
|
+
subcat,
|
|
916
|
+
)}
|
|
917
|
+
onChange={(e) => {
|
|
918
|
+
if (e.currentTarget.checked) {
|
|
919
|
+
setSelectedSubcategories([
|
|
920
|
+
...selectedSubcategories,
|
|
921
|
+
subcat,
|
|
922
|
+
]);
|
|
923
|
+
} else {
|
|
924
|
+
setSelectedSubcategories(
|
|
925
|
+
selectedSubcategories.filter(
|
|
926
|
+
(sc) => sc !== subcat,
|
|
927
|
+
),
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
}}
|
|
931
|
+
disabled={busy || loadingMetadata}
|
|
932
|
+
/>
|
|
933
|
+
))}
|
|
911
934
|
</Group>
|
|
912
935
|
</div>
|
|
913
936
|
)}
|
|
@@ -941,40 +964,39 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
941
964
|
</Stack>
|
|
942
965
|
)}
|
|
943
966
|
|
|
944
|
-
{
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
967
|
+
{/* Audio level indicator when recording */}
|
|
968
|
+
{USE_AUDIO && (
|
|
969
|
+
<>
|
|
970
|
+
{recording && (
|
|
971
|
+
<Stack gap="xs">
|
|
972
|
+
<Text size="xs" c="dimmed">
|
|
973
|
+
{I18n.get("Recording...")} 🎤
|
|
974
|
+
</Text>
|
|
975
|
+
<Progress
|
|
976
|
+
value={audioLevel}
|
|
977
|
+
size="sm"
|
|
978
|
+
color="red"
|
|
979
|
+
animated
|
|
980
|
+
striped
|
|
981
|
+
/>
|
|
982
|
+
</Stack>
|
|
983
|
+
)}
|
|
961
984
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
}
|
|
985
|
+
{/* Audio playback when recorded */}
|
|
986
|
+
{audioBlob && !recording && (
|
|
987
|
+
<Stack gap="xs">
|
|
988
|
+
<Text size="xs" c="dimmed">
|
|
989
|
+
{I18n.get("Recorded audio:")}
|
|
990
|
+
</Text>
|
|
991
|
+
<audio
|
|
992
|
+
controls
|
|
993
|
+
src={URL.createObjectURL(audioBlob)}
|
|
994
|
+
className="ai-kit-audio-player"
|
|
995
|
+
/>
|
|
996
|
+
</Stack>
|
|
997
|
+
)}
|
|
998
|
+
</>
|
|
999
|
+
)}
|
|
978
1000
|
|
|
979
1001
|
{error ? (
|
|
980
1002
|
<Alert color="red" title={I18n.get("Error")}>
|
|
@@ -1017,6 +1039,44 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1017
1039
|
<ReactMarkdown
|
|
1018
1040
|
remarkPlugins={[remarkGfm]}
|
|
1019
1041
|
rehypePlugins={[rehypeRaw]}
|
|
1042
|
+
components={{
|
|
1043
|
+
a: ({ href, children, ...rest }) => {
|
|
1044
|
+
const isCite =
|
|
1045
|
+
typeof href === "string" &&
|
|
1046
|
+
href.startsWith("#docsearch-source-");
|
|
1047
|
+
|
|
1048
|
+
return (
|
|
1049
|
+
<a
|
|
1050
|
+
href={href}
|
|
1051
|
+
{...rest}
|
|
1052
|
+
onClick={(e) => {
|
|
1053
|
+
if (!isCite) return;
|
|
1054
|
+
e.preventDefault();
|
|
1055
|
+
|
|
1056
|
+
const id = href!.slice(1);
|
|
1057
|
+
const selector = `#${escapeCssId(id)}`;
|
|
1058
|
+
|
|
1059
|
+
const scope: ParentNode =
|
|
1060
|
+
(rootElement as unknown as ParentNode) ??
|
|
1061
|
+
document;
|
|
1062
|
+
|
|
1063
|
+
const el = (
|
|
1064
|
+
scope as Document | Element
|
|
1065
|
+
).querySelector?.(
|
|
1066
|
+
selector,
|
|
1067
|
+
) as HTMLElement | null;
|
|
1068
|
+
|
|
1069
|
+
el?.scrollIntoView({
|
|
1070
|
+
block: "start",
|
|
1071
|
+
behavior: "smooth",
|
|
1072
|
+
});
|
|
1073
|
+
}}
|
|
1074
|
+
>
|
|
1075
|
+
{children}
|
|
1076
|
+
</a>
|
|
1077
|
+
);
|
|
1078
|
+
},
|
|
1079
|
+
}}
|
|
1020
1080
|
data-doc-search-result-content
|
|
1021
1081
|
>
|
|
1022
1082
|
{annotatedSummary || summaryText}
|
|
@@ -1026,8 +1086,8 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1026
1086
|
) : null}
|
|
1027
1087
|
|
|
1028
1088
|
{showSources &&
|
|
1029
|
-
|
|
1030
|
-
|
|
1089
|
+
(result?.citations?.docs?.length ||
|
|
1090
|
+
result?.citations?.chunks?.length) ? (
|
|
1031
1091
|
<>
|
|
1032
1092
|
<Divider />
|
|
1033
1093
|
<Stack gap="sm" data-doc-search-sources>
|
|
@@ -1051,12 +1111,18 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1051
1111
|
{titleText}
|
|
1052
1112
|
</Text>
|
|
1053
1113
|
);
|
|
1114
|
+
|
|
1054
1115
|
return (
|
|
1055
1116
|
<Paper
|
|
1056
1117
|
key={doc.docId}
|
|
1057
1118
|
withBorder
|
|
1058
1119
|
radius="md"
|
|
1059
1120
|
p="sm"
|
|
1121
|
+
id={
|
|
1122
|
+
docNumber
|
|
1123
|
+
? `docsearch-source-${docNumber}`
|
|
1124
|
+
: undefined
|
|
1125
|
+
}
|
|
1060
1126
|
>
|
|
1061
1127
|
<Stack gap="xs">
|
|
1062
1128
|
<Group
|
|
@@ -1086,6 +1152,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1086
1152
|
) : (
|
|
1087
1153
|
titleNode
|
|
1088
1154
|
)}
|
|
1155
|
+
|
|
1089
1156
|
<Anchor
|
|
1090
1157
|
href={href}
|
|
1091
1158
|
target="_blank"
|
|
@@ -1100,6 +1167,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1100
1167
|
>
|
|
1101
1168
|
{doc.sourceUrl}
|
|
1102
1169
|
</Anchor>
|
|
1170
|
+
|
|
1103
1171
|
{doc.author ? (
|
|
1104
1172
|
<Text
|
|
1105
1173
|
size="xs"
|
|
@@ -1109,6 +1177,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1109
1177
|
{doc.author}
|
|
1110
1178
|
</Text>
|
|
1111
1179
|
) : null}
|
|
1180
|
+
|
|
1112
1181
|
{doc.description ? (
|
|
1113
1182
|
<Text
|
|
1114
1183
|
size="sm"
|
|
@@ -1128,6 +1197,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1128
1197
|
</Stack>
|
|
1129
1198
|
</>
|
|
1130
1199
|
) : null}
|
|
1200
|
+
|
|
1131
1201
|
<PoweredBy variation={variation} />
|
|
1132
1202
|
</Stack>
|
|
1133
1203
|
</Paper>
|