@smart-cloud/ai-kit-ui 1.3.3 → 1.3.5

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.
@@ -82,9 +82,17 @@ const DocSearchBase: FC<Props> = (props) => {
82
82
  language,
83
83
  onClose,
84
84
  onClickDoc,
85
+
86
+ // Open button props (from AiWorkerProps)
87
+ showOpenButton = false,
88
+ openButtonTitle,
89
+ openButtonIcon,
90
+ showOpenButtonTitle = true,
91
+ showOpenButtonIcon = true,
85
92
  } = props;
86
93
 
87
94
  const [query, setQuery] = useState<string>("");
95
+ const [featureOpen, setFeatureOpen] = useState<boolean>(!showOpenButton);
88
96
  const [recording, setRecording] = useState<boolean>(false);
89
97
  const [audioBlob, setAudioBlob] = useState<Blob | null>(null);
90
98
  const [audioLevel, setAudioLevel] = useState<number>(0);
@@ -134,6 +142,10 @@ const DocSearchBase: FC<Props> = (props) => {
134
142
  return I18n.get(title || "Search with AI-Kit");
135
143
  }, [language]);
136
144
 
145
+ const getOpenButtonDefaultIcon = useCallback((className?: string) => {
146
+ return <IconSearch className={className} size={18} />;
147
+ }, []);
148
+
137
149
  const statusText = useMemo(() => {
138
150
  const e: AiKitStatusEvent | null = statusEvent;
139
151
  if (!e) return null;
@@ -313,10 +325,13 @@ const DocSearchBase: FC<Props> = (props) => {
313
325
  }, [context, inputText, audioBlob, run, reset, topK, sessionId]);
314
326
 
315
327
  const close = useCallback(async () => {
328
+ setFeatureOpen(false);
316
329
  reset();
317
- onClose();
318
330
  autoRunOnceRef.current = false;
319
- }, [onClose, reset, autoRunOnceRef]);
331
+ if (!showOpenButton) {
332
+ onClose();
333
+ }
334
+ }, [onClose, reset, autoRunOnceRef, showOpenButton]);
320
335
 
321
336
  useEffect(() => {
322
337
  if (!autoRun || !canSearch || busy || autoRunOnceRef.current) {
@@ -413,7 +428,7 @@ const DocSearchBase: FC<Props> = (props) => {
413
428
  variation === "modal" ? Modal.Body : Group;
414
429
 
415
430
  useEffect(() => {
416
- if (variation !== "modal") {
431
+ if (variation !== "modal" || !featureOpen) {
417
432
  return;
418
433
  }
419
434
  document.body.style.overflow = "hidden";
@@ -431,321 +446,367 @@ const DocSearchBase: FC<Props> = (props) => {
431
446
  }, [close, variation]);
432
447
 
433
448
  return (
434
- <RootComponent
435
- opened={true}
436
- className="doc-search-root"
437
- onClose={close}
438
- padding="md"
439
- gap="md"
440
- size="xl"
441
- portalProps={
442
- variation === "modal"
443
- ? { target: rootElement, reuseTargetNode: true }
444
- : undefined
445
- }
446
- data-ai-kit-theme={colorMode}
447
- data-ai-kit-variation={variation}
448
- >
449
- {variation === "modal" && <Modal.Overlay />}
450
- <ContentComponent
451
- w="100%"
452
- style={{
453
- left: 0,
454
- }}
455
- >
456
- {variation === "modal" && (
457
- <Modal.Header style={{ zIndex: 1000 }}>
458
- <AiKitDocSearchIcon className="doc-search-title-icon" />
459
- <Modal.Title>{I18n.get(defaultTitle)}</Modal.Title>
460
- <Modal.CloseButton />
461
- </Modal.Header>
462
- )}
463
- <BodyComponent w="100%" style={{ zIndex: 1001 }}>
464
- <AiFeatureBorder
465
- enabled={variation !== "modal"}
466
- working={busy}
467
- variation={variation}
449
+ <>
450
+ {showOpenButton && (
451
+ <Button
452
+ leftSection={
453
+ showOpenButtonIcon &&
454
+ (openButtonIcon ? (
455
+ <span dangerouslySetInnerHTML={{ __html: openButtonIcon }} />
456
+ ) : (
457
+ getOpenButtonDefaultIcon()
458
+ ))
459
+ }
460
+ className={
461
+ showOpenButtonTitle
462
+ ? "doc-search-button"
463
+ : "doc-search-button-no-title"
464
+ }
465
+ variant={"filled"}
466
+ disabled={featureOpen}
467
+ onClick={() => setFeatureOpen(true)}
468
+ data-ai-kit-open-button
469
+ >
470
+ {showOpenButtonTitle && I18n.get(openButtonTitle || defaultTitle)}
471
+ </Button>
472
+ )}
473
+
474
+ {featureOpen && (
475
+ <RootComponent
476
+ opened={true}
477
+ className="doc-search-root"
478
+ onClose={close}
479
+ padding="md"
480
+ gap="md"
481
+ size="xl"
482
+ portalProps={
483
+ variation === "modal"
484
+ ? { target: rootElement, reuseTargetNode: true }
485
+ : undefined
486
+ }
487
+ data-ai-kit-theme={colorMode}
488
+ data-ai-kit-variation={variation}
489
+ >
490
+ {variation === "modal" && <Modal.Overlay />}
491
+ <ContentComponent
492
+ w="100%"
493
+ style={{
494
+ left: 0,
495
+ }}
468
496
  >
469
- <Paper shadow="sm" radius="md" p="md">
470
- <Stack gap="sm">
471
- {variation !== "modal" && (
472
- <Title order={4} style={{ margin: 0 }}>
473
- {I18n.get(defaultTitle)}
474
- </Title>
475
- )}
476
-
477
- <Group gap="sm" align="flex-end" wrap="nowrap">
478
- <TextInput
479
- style={{ flex: 1 }}
480
- value={query}
481
- onChange={(e) => {
482
- setQuery(e.currentTarget.value);
483
- // Clear audio when typing
484
- if (audioBlob) {
485
- clearAudio();
497
+ {variation === "modal" && (
498
+ <Modal.Header style={{ zIndex: 1000 }}>
499
+ <AiKitDocSearchIcon className="doc-search-title-icon" />
500
+ <Modal.Title>{I18n.get(defaultTitle)}</Modal.Title>
501
+ <Modal.CloseButton />
502
+ </Modal.Header>
503
+ )}
504
+ <BodyComponent w="100%" style={{ zIndex: 1001 }}>
505
+ <AiFeatureBorder
506
+ enabled={variation !== "modal"}
507
+ working={busy}
508
+ variation={variation}
509
+ >
510
+ <Paper shadow="sm" radius="md" p="md">
511
+ <Stack gap="sm">
512
+ {variation !== "modal" && (
513
+ <Title order={4} style={{ margin: 0 }}>
514
+ {I18n.get(defaultTitle)}
515
+ </Title>
516
+ )}
517
+
518
+ <Group gap="sm" align="flex-end" wrap="nowrap">
519
+ <TextInput
520
+ style={{ flex: 1 }}
521
+ value={query}
522
+ onChange={(e) => {
523
+ setQuery(e.currentTarget.value);
524
+ // Clear audio when typing
525
+ if (audioBlob) {
526
+ clearAudio();
527
+ }
528
+ }}
529
+ placeholder={
530
+ audioBlob
531
+ ? I18n.get("Audio recorded")
532
+ : I18n.get("Search the documentation…")
533
+ }
534
+ disabled={busy || recording || !!audioBlob}
535
+ onKeyDown={(e) => {
536
+ if (e.key === "Enter" && canSearch) {
537
+ e.preventDefault();
538
+ void onSearch();
539
+ }
540
+ }}
541
+ />
542
+
543
+ {
544
+ /* Microphone button */ USE_AUDIO && (
545
+ <>
546
+ {audioBlob ? (
547
+ <Button
548
+ variant="outline"
549
+ size="sm"
550
+ color="red"
551
+ onClick={clearAudio}
552
+ disabled={busy}
553
+ title={I18n.get("Clear audio")}
554
+ >
555
+ <IconMicrophoneOff size={18} />
556
+ </Button>
557
+ ) : (
558
+ <Button
559
+ variant={recording ? "filled" : "outline"}
560
+ size="sm"
561
+ color={recording ? "red" : "gray"}
562
+ onClick={
563
+ recording ? stopRecording : startRecording
564
+ }
565
+ disabled={busy}
566
+ title={
567
+ recording
568
+ ? I18n.get("Stop recording")
569
+ : I18n.get("Record audio")
570
+ }
571
+ style={
572
+ recording
573
+ ? {
574
+ transform: `scale(${1 + audioLevel / 300})`,
575
+ transition: "transform 0.1s ease-out",
576
+ }
577
+ : undefined
578
+ }
579
+ >
580
+ <IconMicrophone size={18} />
581
+ </Button>
582
+ )}
583
+ </>
584
+ )
486
585
  }
487
- }}
488
- placeholder={
489
- audioBlob
490
- ? I18n.get("Audio recorded")
491
- : I18n.get("Search the documentation…")
586
+
587
+ <Button
588
+ variant="filled"
589
+ size="sm"
590
+ leftSection={buttonLeftIcon}
591
+ onClick={() => void onSearch()}
592
+ disabled={!canSearch}
593
+ className={
594
+ showSearchButtonTitle
595
+ ? "doc-search-button"
596
+ : "doc-search-button-no-title"
597
+ }
598
+ >
599
+ {showSearchButtonTitle ? I18n.get("Search") : null}
600
+ </Button>
601
+
602
+ {busy ? (
603
+ <Button variant="outline" size="sm" onClick={cancel}>
604
+ {I18n.get("Stop")}
605
+ </Button>
606
+ ) : null}
607
+ </Group>
608
+
609
+ {
610
+ /* Audio level indicator when recording */ USE_AUDIO && (
611
+ <>
612
+ {recording && (
613
+ <Stack gap="xs">
614
+ <Text size="xs" c="dimmed">
615
+ {I18n.get("Recording...")} 🎤
616
+ </Text>
617
+ <Progress
618
+ value={audioLevel}
619
+ size="sm"
620
+ color="red"
621
+ animated
622
+ striped
623
+ />
624
+ </Stack>
625
+ )}
626
+
627
+ {/* Audio playback when recorded */}
628
+ {audioBlob && !recording && (
629
+ <Stack gap="xs">
630
+ <Text size="xs" c="dimmed">
631
+ {I18n.get("Recorded audio:")}
632
+ </Text>
633
+ <audio
634
+ controls
635
+ src={URL.createObjectURL(audioBlob)}
636
+ className="ai-kit-audio-player"
637
+ />
638
+ </Stack>
639
+ )}
640
+ </>
641
+ )
492
642
  }
493
- disabled={busy || recording || !!audioBlob}
494
- onKeyDown={(e) => {
495
- if (e.key === "Enter" && canSearch) {
496
- e.preventDefault();
497
- void onSearch();
498
- }
499
- }}
500
- />
501
643
 
502
- {
503
- /* Microphone button */ USE_AUDIO && (
644
+ {error ? (
645
+ <Alert color="red" title={I18n.get("Error")}>
646
+ {error}
647
+ </Alert>
648
+ ) : null}
649
+
650
+ {busy && statusText && (
651
+ <AiFeatureBorder
652
+ enabled={variation === "modal"}
653
+ working={true}
654
+ variation={variation}
655
+ >
656
+ <Group
657
+ justify="center"
658
+ align="center"
659
+ gap="sm"
660
+ m="sm"
661
+ pr="lg"
662
+ >
663
+ <Loader size="sm" />
664
+ <Text size="sm" c="dimmed">
665
+ {statusText}
666
+ </Text>
667
+ </Group>
668
+ </AiFeatureBorder>
669
+ )}
670
+
671
+ {result?.result ? (
504
672
  <>
505
- {audioBlob ? (
506
- <Button
507
- variant="outline"
673
+ <Divider />
674
+ <Stack gap="xs" data-doc-search-result>
675
+ <Text
508
676
  size="sm"
509
- color="red"
510
- onClick={clearAudio}
511
- disabled={busy}
512
- title={I18n.get("Clear audio")}
677
+ c="dimmed"
678
+ data-doc-search-result-title
513
679
  >
514
- <IconMicrophoneOff size={18} />
515
- </Button>
516
- ) : (
517
- <Button
518
- variant={recording ? "filled" : "outline"}
519
- size="sm"
520
- color={recording ? "red" : "gray"}
521
- onClick={recording ? stopRecording : startRecording}
522
- disabled={busy}
523
- title={
524
- recording
525
- ? I18n.get("Stop recording")
526
- : I18n.get("Record audio")
527
- }
528
- style={
529
- recording
530
- ? {
531
- transform: `scale(${1 + audioLevel / 300})`,
532
- transition: "transform 0.1s ease-out",
533
- }
534
- : undefined
535
- }
536
- >
537
- <IconMicrophone size={18} />
538
- </Button>
539
- )}
540
- </>
541
- )
542
- }
543
-
544
- <Button
545
- variant="filled"
546
- size="sm"
547
- leftSection={buttonLeftIcon}
548
- onClick={() => void onSearch()}
549
- disabled={!canSearch}
550
- className={
551
- showSearchButtonTitle
552
- ? "doc-search-button"
553
- : "doc-search-button-no-title"
554
- }
555
- >
556
- {showSearchButtonTitle ? I18n.get("Search") : null}
557
- </Button>
558
-
559
- {busy ? (
560
- <Button variant="outline" size="sm" onClick={cancel}>
561
- {I18n.get("Stop")}
562
- </Button>
563
- ) : null}
564
- </Group>
565
-
566
- {
567
- /* Audio level indicator when recording */ USE_AUDIO && (
568
- <>
569
- {recording && (
570
- <Stack gap="xs">
571
- <Text size="xs" c="dimmed">
572
- {I18n.get("Recording...")} 🎤
680
+ {I18n.get("AI Summary")}
573
681
  </Text>
574
- <Progress
575
- value={audioLevel}
576
- size="sm"
577
- color="red"
578
- animated
579
- striped
580
- />
682
+ <ReactMarkdown
683
+ remarkPlugins={[remarkGfm]}
684
+ rehypePlugins={[rehypeRaw]}
685
+ data-doc-search-result-content
686
+ >
687
+ {annotatedSummary || summaryText}
688
+ </ReactMarkdown>
581
689
  </Stack>
582
- )}
690
+ </>
691
+ ) : null}
583
692
 
584
- {/* Audio playback when recorded */}
585
- {audioBlob && !recording && (
586
- <Stack gap="xs">
587
- <Text size="xs" c="dimmed">
588
- {I18n.get("Recorded audio:")}
693
+ {showSources &&
694
+ (result?.citations?.docs?.length ||
695
+ result?.citations?.chunks?.length) ? (
696
+ <>
697
+ <Divider />
698
+ <Stack gap="sm" data-doc-search-sources>
699
+ <Text
700
+ size="sm"
701
+ c="dimmed"
702
+ data-doc-search-sources-title
703
+ >
704
+ {I18n.get("Sources")}
589
705
  </Text>
590
- <audio
591
- controls
592
- src={URL.createObjectURL(audioBlob)}
593
- className="ai-kit-audio-player"
594
- />
595
- </Stack>
596
- )}
597
- </>
598
- )
599
- }
600
-
601
- {error ? (
602
- <Alert color="red" title={I18n.get("Error")}>
603
- {error}
604
- </Alert>
605
- ) : null}
606
-
607
- {busy && statusText && (
608
- <AiFeatureBorder
609
- enabled={variation === "modal"}
610
- working={true}
611
- variation={variation}
612
- >
613
- <Group
614
- justify="center"
615
- align="center"
616
- gap="sm"
617
- m="sm"
618
- pr="lg"
619
- >
620
- <Loader size="sm" />
621
- <Text size="sm" c="dimmed">
622
- {statusText}
623
- </Text>
624
- </Group>
625
- </AiFeatureBorder>
626
- )}
627
-
628
- {result?.result ? (
629
- <>
630
- <Divider />
631
- <Stack gap="xs" data-doc-search-result>
632
- <Text size="sm" c="dimmed" data-doc-search-result-title>
633
- {I18n.get("AI Summary")}
634
- </Text>
635
- <ReactMarkdown
636
- remarkPlugins={[remarkGfm]}
637
- rehypePlugins={[rehypeRaw]}
638
- data-doc-search-result-content
639
- >
640
- {annotatedSummary || summaryText}
641
- </ReactMarkdown>
642
- </Stack>
643
- </>
644
- ) : null}
645
-
646
- {showSources &&
647
- (result?.citations?.docs?.length ||
648
- result?.citations?.chunks?.length) ? (
649
- <>
650
- <Divider />
651
- <Stack gap="sm" data-doc-search-sources>
652
- <Text size="sm" c="dimmed" data-doc-search-sources-title>
653
- {I18n.get("Sources")}
654
- </Text>
655
706
 
656
- {grouped.map(({ doc }) => {
657
- const href = doc.sourceUrl?.trim() || undefined;
658
- const docNumber = doc.docId
659
- ? docNumberMap.get(doc.docId)
660
- : undefined;
661
- const titleText = doc.title?.trim() || doc.docId;
662
- const titleNode = (
663
- <Text fw={600} style={{ display: "inline" }}>
664
- {docNumber ? `${docNumber}. ` : ""}
665
- {titleText}
666
- </Text>
667
- );
668
- return (
669
- <Paper key={doc.docId} withBorder radius="md" p="sm">
670
- <Stack gap="xs">
671
- <Group justify="space-between" align="flex-start">
672
- <Stack
673
- gap={2}
674
- style={{ flex: 1 }}
675
- data-doc-search-source
676
- >
677
- {href ? (
678
- <Anchor
679
- href={href}
680
- target="_blank"
681
- rel="noreferrer"
682
- style={{ textDecoration: "none" }}
683
- onClick={(e) => {
684
- if (!onClickDoc) return;
685
- e.preventDefault();
686
- onClickDoc?.(doc);
687
- }}
688
- data-doc-search-source-title
689
- >
690
- {titleNode}
691
- </Anchor>
692
- ) : (
693
- titleNode
694
- )}
695
- <Anchor
696
- href={href}
697
- target="_blank"
698
- rel="noreferrer"
699
- style={{ textDecoration: "none" }}
700
- onClick={(e) => {
701
- if (!onClickDoc) return;
702
- e.preventDefault();
703
- onClickDoc?.(doc);
704
- }}
705
- data-doc-search-source-url
707
+ {grouped.map(({ doc }) => {
708
+ const href = doc.sourceUrl?.trim() || undefined;
709
+ const docNumber = doc.docId
710
+ ? docNumberMap.get(doc.docId)
711
+ : undefined;
712
+ const titleText = doc.title?.trim() || doc.docId;
713
+ const titleNode = (
714
+ <Text fw={600} style={{ display: "inline" }}>
715
+ {docNumber ? `${docNumber}. ` : ""}
716
+ {titleText}
717
+ </Text>
718
+ );
719
+ return (
720
+ <Paper
721
+ key={doc.docId}
722
+ withBorder
723
+ radius="md"
724
+ p="sm"
725
+ >
726
+ <Stack gap="xs">
727
+ <Group
728
+ justify="space-between"
729
+ align="flex-start"
706
730
  >
707
- {doc.sourceUrl}
708
- </Anchor>
709
- {doc.author ? (
710
- <Text
711
- size="xs"
712
- c="dimmed"
713
- data-doc-search-source-author
731
+ <Stack
732
+ gap={2}
733
+ style={{ flex: 1 }}
734
+ data-doc-search-source
714
735
  >
715
- {doc.author}
716
- </Text>
717
- ) : null}
718
- {doc.description ? (
719
- <Text
720
- size="sm"
721
- c="dimmed"
722
- fs="italic"
723
- data-doc-search-source-description
724
- >
725
- {doc.description}
726
- </Text>
727
- ) : null}
736
+ {href ? (
737
+ <Anchor
738
+ href={href}
739
+ target="_blank"
740
+ rel="noreferrer"
741
+ style={{ textDecoration: "none" }}
742
+ onClick={(e) => {
743
+ if (!onClickDoc) return;
744
+ e.preventDefault();
745
+ onClickDoc?.(doc);
746
+ }}
747
+ data-doc-search-source-title
748
+ >
749
+ {titleNode}
750
+ </Anchor>
751
+ ) : (
752
+ titleNode
753
+ )}
754
+ <Anchor
755
+ href={href}
756
+ target="_blank"
757
+ rel="noreferrer"
758
+ style={{ textDecoration: "none" }}
759
+ onClick={(e) => {
760
+ if (!onClickDoc) return;
761
+ e.preventDefault();
762
+ onClickDoc?.(doc);
763
+ }}
764
+ data-doc-search-source-url
765
+ >
766
+ {doc.sourceUrl}
767
+ </Anchor>
768
+ {doc.author ? (
769
+ <Text
770
+ size="xs"
771
+ c="dimmed"
772
+ data-doc-search-source-author
773
+ >
774
+ {doc.author}
775
+ </Text>
776
+ ) : null}
777
+ {doc.description ? (
778
+ <Text
779
+ size="sm"
780
+ c="dimmed"
781
+ fs="italic"
782
+ data-doc-search-source-description
783
+ >
784
+ {doc.description}
785
+ </Text>
786
+ ) : null}
787
+ </Stack>
788
+ </Group>
728
789
  </Stack>
729
- </Group>
730
- </Stack>
731
- </Paper>
732
- );
733
- })}
734
- </Stack>
735
- </>
736
- ) : null}
737
- {!busy && !error && !result?.result ? (
738
- <Text size="sm" c="dimmed" data-doc-search-no-results>
739
- {I18n.get("Enter a search query to start.")}
740
- </Text>
741
- ) : null}
742
- <PoweredBy variation={variation} />
743
- </Stack>
744
- </Paper>
745
- </AiFeatureBorder>
746
- </BodyComponent>
747
- </ContentComponent>
748
- </RootComponent>
790
+ </Paper>
791
+ );
792
+ })}
793
+ </Stack>
794
+ </>
795
+ ) : null}
796
+ {!busy && !error && !result?.result ? (
797
+ <Text size="sm" c="dimmed" data-doc-search-no-results>
798
+ {I18n.get("Enter a search query to start.")}
799
+ </Text>
800
+ ) : null}
801
+ <PoweredBy variation={variation} />
802
+ </Stack>
803
+ </Paper>
804
+ </AiFeatureBorder>
805
+ </BodyComponent>
806
+ </ContentComponent>
807
+ </RootComponent>
808
+ )}
809
+ </>
749
810
  );
750
811
  };
751
812