@smart-cloud/ai-kit-ui 1.3.11 → 1.3.13
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/ai-kit-ui.css +0 -1
- package/dist/index.cjs +6 -6
- package/dist/index.js +6 -6
- package/package.json +1 -1
- package/src/ai-chatbot/AiChatbot.tsx +56 -57
- package/src/doc-search/DocSearch.tsx +243 -161
- package/src/styles/ai-kit-ui.css +0 -1
|
@@ -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 () => {
|
|
@@ -416,7 +443,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
416
443
|
"Reusing cached audio (no re-upload needed within",
|
|
417
444
|
Math.round(
|
|
418
445
|
(AUDIO_CACHE_TTL - (now - audioCacheRef.current!.uploadTimestamp)) /
|
|
419
|
-
|
|
446
|
+
1000,
|
|
420
447
|
),
|
|
421
448
|
"seconds)",
|
|
422
449
|
);
|
|
@@ -438,8 +465,8 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
438
465
|
}),
|
|
439
466
|
...(enableUserFilters &&
|
|
440
467
|
selectedSubcategories.length > 0 && {
|
|
441
|
-
|
|
442
|
-
|
|
468
|
+
userSelectedSubcategories: selectedSubcategories,
|
|
469
|
+
}),
|
|
443
470
|
...(enableUserFilters &&
|
|
444
471
|
selectedTags.length > 0 && { userSelectedTags: selectedTags }),
|
|
445
472
|
},
|
|
@@ -565,14 +592,24 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
565
592
|
}
|
|
566
593
|
|
|
567
594
|
const safeEnd = Math.min(end, summaryText.length);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
cursor
|
|
595
|
+
const snappedEnd = snapEndToBoundary(summaryText, safeEnd);
|
|
596
|
+
|
|
597
|
+
segments.push(summaryText.slice(cursor, snappedEnd));
|
|
598
|
+
|
|
599
|
+
const supHtml = refs
|
|
600
|
+
.map(
|
|
601
|
+
(n) =>
|
|
602
|
+
`<a href="#docsearch-source-${n}" data-docsearch-cite="${n}" style="color: inherit; text-decoration: none;">${n}</a>`,
|
|
603
|
+
)
|
|
604
|
+
.join(",");
|
|
605
|
+
|
|
606
|
+
segments.push(`<sup>${supHtml}</sup>`);
|
|
607
|
+
cursor = snappedEnd;
|
|
571
608
|
}
|
|
572
609
|
|
|
573
610
|
segments.push(summaryText.slice(cursor));
|
|
574
611
|
return segments.join("");
|
|
575
|
-
}, [citationAnchors, summaryText]);
|
|
612
|
+
}, [citationAnchors, summaryText, docNumberMap, chunkDocMap]);
|
|
576
613
|
|
|
577
614
|
const RootComponent: typeof Modal.Root | typeof Group =
|
|
578
615
|
variation === "modal" ? Modal.Root : Group;
|
|
@@ -731,49 +768,48 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
731
768
|
}}
|
|
732
769
|
/>
|
|
733
770
|
|
|
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
|
-
|
|
771
|
+
{/* Microphone button */}
|
|
772
|
+
{USE_AUDIO && (
|
|
773
|
+
<>
|
|
774
|
+
{audioBlob ? (
|
|
775
|
+
<Button
|
|
776
|
+
variant="outline"
|
|
777
|
+
size="sm"
|
|
778
|
+
color="red"
|
|
779
|
+
onClick={clearAudio}
|
|
780
|
+
disabled={busy}
|
|
781
|
+
title={I18n.get("Clear audio")}
|
|
782
|
+
>
|
|
783
|
+
<IconMicrophoneOff size={18} />
|
|
784
|
+
</Button>
|
|
785
|
+
) : (
|
|
786
|
+
<Button
|
|
787
|
+
variant={recording ? "filled" : "outline"}
|
|
788
|
+
size="sm"
|
|
789
|
+
color={recording ? "red" : "gray"}
|
|
790
|
+
onClick={
|
|
791
|
+
recording ? stopRecording : startRecording
|
|
792
|
+
}
|
|
793
|
+
disabled={busy}
|
|
794
|
+
title={
|
|
795
|
+
recording
|
|
796
|
+
? I18n.get("Stop recording")
|
|
797
|
+
: I18n.get("Record audio")
|
|
798
|
+
}
|
|
799
|
+
style={
|
|
800
|
+
recording
|
|
801
|
+
? {
|
|
765
802
|
transform: `scale(${1 + audioLevel / 300})`,
|
|
766
803
|
transition: "transform 0.1s ease-out",
|
|
767
804
|
}
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
}
|
|
805
|
+
: undefined
|
|
806
|
+
}
|
|
807
|
+
>
|
|
808
|
+
<IconMicrophone size={18} />
|
|
809
|
+
</Button>
|
|
810
|
+
)}
|
|
811
|
+
</>
|
|
812
|
+
)}
|
|
777
813
|
|
|
778
814
|
<Button
|
|
779
815
|
variant="filled"
|
|
@@ -830,51 +866,51 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
830
866
|
{/* Main categories as checkboxes */}
|
|
831
867
|
{Object.keys(metadataOptions.allowedCategories)
|
|
832
868
|
.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
|
-
|
|
869
|
+
<div>
|
|
870
|
+
<Text size="sm" fw={500} mb="xs">
|
|
871
|
+
{I18n.get("Categories")}
|
|
872
|
+
</Text>
|
|
873
|
+
<Group gap="md">
|
|
874
|
+
{Object.keys(
|
|
875
|
+
metadataOptions.allowedCategories,
|
|
876
|
+
).map((category) => (
|
|
877
|
+
<Checkbox
|
|
878
|
+
key={category}
|
|
879
|
+
label={I18n.get(category)}
|
|
880
|
+
checked={selectedCategories.includes(
|
|
881
|
+
category,
|
|
882
|
+
)}
|
|
883
|
+
onChange={(e) => {
|
|
884
|
+
if (e.currentTarget.checked) {
|
|
885
|
+
setSelectedCategories([
|
|
886
|
+
...selectedCategories,
|
|
887
|
+
category,
|
|
888
|
+
]);
|
|
889
|
+
} else {
|
|
890
|
+
setSelectedCategories(
|
|
891
|
+
selectedCategories.filter(
|
|
892
|
+
(c) => c !== category,
|
|
893
|
+
),
|
|
894
|
+
);
|
|
895
|
+
// Remove subcategories of unchecked category
|
|
896
|
+
const subcatsToRemove =
|
|
897
|
+
metadataOptions.allowedCategories[
|
|
862
898
|
category
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
899
|
+
] || [];
|
|
900
|
+
setSelectedSubcategories(
|
|
901
|
+
selectedSubcategories.filter(
|
|
902
|
+
(sc) =>
|
|
903
|
+
!subcatsToRemove.includes(sc),
|
|
904
|
+
),
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
}}
|
|
908
|
+
disabled={busy || loadingMetadata}
|
|
909
|
+
/>
|
|
910
|
+
))}
|
|
911
|
+
</Group>
|
|
912
|
+
</div>
|
|
913
|
+
)}
|
|
878
914
|
|
|
879
915
|
{/* Subcategories for selected categories */}
|
|
880
916
|
{subcategories.length > 0 && (
|
|
@@ -883,31 +919,30 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
883
919
|
{I18n.get("Subcategories")}
|
|
884
920
|
</Text>
|
|
885
921
|
<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
|
-
))}
|
|
922
|
+
{subcategories.map((subcat) => (
|
|
923
|
+
<Checkbox
|
|
924
|
+
key={subcat}
|
|
925
|
+
label={I18n.get(subcat)}
|
|
926
|
+
checked={selectedSubcategories.includes(
|
|
927
|
+
subcat,
|
|
928
|
+
)}
|
|
929
|
+
onChange={(e) => {
|
|
930
|
+
if (e.currentTarget.checked) {
|
|
931
|
+
setSelectedSubcategories([
|
|
932
|
+
...selectedSubcategories,
|
|
933
|
+
subcat,
|
|
934
|
+
]);
|
|
935
|
+
} else {
|
|
936
|
+
setSelectedSubcategories(
|
|
937
|
+
selectedSubcategories.filter(
|
|
938
|
+
(sc) => sc !== subcat,
|
|
939
|
+
),
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
}}
|
|
943
|
+
disabled={busy || loadingMetadata}
|
|
944
|
+
/>
|
|
945
|
+
))}
|
|
911
946
|
</Group>
|
|
912
947
|
</div>
|
|
913
948
|
)}
|
|
@@ -941,40 +976,39 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
941
976
|
</Stack>
|
|
942
977
|
)}
|
|
943
978
|
|
|
944
|
-
{
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
979
|
+
{/* Audio level indicator when recording */}
|
|
980
|
+
{USE_AUDIO && (
|
|
981
|
+
<>
|
|
982
|
+
{recording && (
|
|
983
|
+
<Stack gap="xs">
|
|
984
|
+
<Text size="xs" c="dimmed">
|
|
985
|
+
{I18n.get("Recording...")} 🎤
|
|
986
|
+
</Text>
|
|
987
|
+
<Progress
|
|
988
|
+
value={audioLevel}
|
|
989
|
+
size="sm"
|
|
990
|
+
color="red"
|
|
991
|
+
animated
|
|
992
|
+
striped
|
|
993
|
+
/>
|
|
994
|
+
</Stack>
|
|
995
|
+
)}
|
|
961
996
|
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
}
|
|
997
|
+
{/* Audio playback when recorded */}
|
|
998
|
+
{audioBlob && !recording && (
|
|
999
|
+
<Stack gap="xs">
|
|
1000
|
+
<Text size="xs" c="dimmed">
|
|
1001
|
+
{I18n.get("Recorded audio:")}
|
|
1002
|
+
</Text>
|
|
1003
|
+
<audio
|
|
1004
|
+
controls
|
|
1005
|
+
src={URL.createObjectURL(audioBlob)}
|
|
1006
|
+
className="ai-kit-audio-player"
|
|
1007
|
+
/>
|
|
1008
|
+
</Stack>
|
|
1009
|
+
)}
|
|
1010
|
+
</>
|
|
1011
|
+
)}
|
|
978
1012
|
|
|
979
1013
|
{error ? (
|
|
980
1014
|
<Alert color="red" title={I18n.get("Error")}>
|
|
@@ -1017,6 +1051,44 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1017
1051
|
<ReactMarkdown
|
|
1018
1052
|
remarkPlugins={[remarkGfm]}
|
|
1019
1053
|
rehypePlugins={[rehypeRaw]}
|
|
1054
|
+
components={{
|
|
1055
|
+
a: ({ href, children, ...rest }) => {
|
|
1056
|
+
const isCite =
|
|
1057
|
+
typeof href === "string" &&
|
|
1058
|
+
href.startsWith("#docsearch-source-");
|
|
1059
|
+
|
|
1060
|
+
return (
|
|
1061
|
+
<a
|
|
1062
|
+
href={href}
|
|
1063
|
+
{...rest}
|
|
1064
|
+
onClick={(e) => {
|
|
1065
|
+
if (!isCite) return;
|
|
1066
|
+
e.preventDefault();
|
|
1067
|
+
|
|
1068
|
+
const id = href!.slice(1);
|
|
1069
|
+
const selector = `#${escapeCssId(id)}`;
|
|
1070
|
+
|
|
1071
|
+
const scope: ParentNode =
|
|
1072
|
+
(rootElement as unknown as ParentNode) ??
|
|
1073
|
+
document;
|
|
1074
|
+
|
|
1075
|
+
const el = (
|
|
1076
|
+
scope as Document | Element
|
|
1077
|
+
).querySelector?.(
|
|
1078
|
+
selector,
|
|
1079
|
+
) as HTMLElement | null;
|
|
1080
|
+
|
|
1081
|
+
el?.scrollIntoView({
|
|
1082
|
+
block: "start",
|
|
1083
|
+
behavior: "smooth",
|
|
1084
|
+
});
|
|
1085
|
+
}}
|
|
1086
|
+
>
|
|
1087
|
+
{children}
|
|
1088
|
+
</a>
|
|
1089
|
+
);
|
|
1090
|
+
},
|
|
1091
|
+
}}
|
|
1020
1092
|
data-doc-search-result-content
|
|
1021
1093
|
>
|
|
1022
1094
|
{annotatedSummary || summaryText}
|
|
@@ -1026,8 +1098,8 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1026
1098
|
) : null}
|
|
1027
1099
|
|
|
1028
1100
|
{showSources &&
|
|
1029
|
-
|
|
1030
|
-
|
|
1101
|
+
(result?.citations?.docs?.length ||
|
|
1102
|
+
result?.citations?.chunks?.length) ? (
|
|
1031
1103
|
<>
|
|
1032
1104
|
<Divider />
|
|
1033
1105
|
<Stack gap="sm" data-doc-search-sources>
|
|
@@ -1051,12 +1123,18 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1051
1123
|
{titleText}
|
|
1052
1124
|
</Text>
|
|
1053
1125
|
);
|
|
1126
|
+
|
|
1054
1127
|
return (
|
|
1055
1128
|
<Paper
|
|
1056
1129
|
key={doc.docId}
|
|
1057
1130
|
withBorder
|
|
1058
1131
|
radius="md"
|
|
1059
1132
|
p="sm"
|
|
1133
|
+
id={
|
|
1134
|
+
docNumber
|
|
1135
|
+
? `docsearch-source-${docNumber}`
|
|
1136
|
+
: undefined
|
|
1137
|
+
}
|
|
1060
1138
|
>
|
|
1061
1139
|
<Stack gap="xs">
|
|
1062
1140
|
<Group
|
|
@@ -1086,6 +1164,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1086
1164
|
) : (
|
|
1087
1165
|
titleNode
|
|
1088
1166
|
)}
|
|
1167
|
+
|
|
1089
1168
|
<Anchor
|
|
1090
1169
|
href={href}
|
|
1091
1170
|
target="_blank"
|
|
@@ -1100,6 +1179,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1100
1179
|
>
|
|
1101
1180
|
{doc.sourceUrl}
|
|
1102
1181
|
</Anchor>
|
|
1182
|
+
|
|
1103
1183
|
{doc.author ? (
|
|
1104
1184
|
<Text
|
|
1105
1185
|
size="xs"
|
|
@@ -1109,6 +1189,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1109
1189
|
{doc.author}
|
|
1110
1190
|
</Text>
|
|
1111
1191
|
) : null}
|
|
1192
|
+
|
|
1112
1193
|
{doc.description ? (
|
|
1113
1194
|
<Text
|
|
1114
1195
|
size="sm"
|
|
@@ -1128,6 +1209,7 @@ const DocSearchBase: FC<Props> = (props) => {
|
|
|
1128
1209
|
</Stack>
|
|
1129
1210
|
</>
|
|
1130
1211
|
) : null}
|
|
1212
|
+
|
|
1131
1213
|
<PoweredBy variation={variation} />
|
|
1132
1214
|
</Stack>
|
|
1133
1215
|
</Paper>
|