doccupine 0.0.63 → 0.0.65

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +4 -3
  2. package/dist/lib/structures.js +4 -0
  3. package/dist/templates/app/api/rag/route.d.ts +1 -1
  4. package/dist/templates/app/api/rag/route.js +11 -4
  5. package/dist/templates/app/theme.d.ts +3 -1
  6. package/dist/templates/app/theme.js +17 -15
  7. package/dist/templates/components/Chat.d.ts +1 -1
  8. package/dist/templates/components/Chat.js +358 -161
  9. package/dist/templates/components/DocsSideBar.d.ts +1 -1
  10. package/dist/templates/components/DocsSideBar.js +45 -11
  11. package/dist/templates/components/LockBodyScroll.d.ts +1 -0
  12. package/dist/templates/components/LockBodyScroll.js +17 -0
  13. package/dist/templates/components/SideBar.d.ts +1 -1
  14. package/dist/templates/components/SideBar.js +15 -2
  15. package/dist/templates/components/layout/Accordion.d.ts +1 -1
  16. package/dist/templates/components/layout/Accordion.js +1 -1
  17. package/dist/templates/components/layout/ActionBar.d.ts +1 -1
  18. package/dist/templates/components/layout/ActionBar.js +17 -87
  19. package/dist/templates/components/layout/Callout.d.ts +1 -1
  20. package/dist/templates/components/layout/Callout.js +1 -1
  21. package/dist/templates/components/layout/Card.d.ts +1 -1
  22. package/dist/templates/components/layout/Card.js +26 -7
  23. package/dist/templates/components/layout/Code.d.ts +1 -1
  24. package/dist/templates/components/layout/Code.js +1 -1
  25. package/dist/templates/components/layout/Columns.d.ts +1 -1
  26. package/dist/templates/components/layout/Columns.js +1 -1
  27. package/dist/templates/components/layout/DocsComponents.d.ts +1 -1
  28. package/dist/templates/components/layout/DocsComponents.js +40 -14
  29. package/dist/templates/components/layout/DocsNavigation.d.ts +1 -1
  30. package/dist/templates/components/layout/DocsNavigation.js +3 -2
  31. package/dist/templates/components/layout/Field.d.ts +1 -1
  32. package/dist/templates/components/layout/Field.js +1 -1
  33. package/dist/templates/components/layout/Footer.d.ts +1 -1
  34. package/dist/templates/components/layout/Footer.js +28 -6
  35. package/dist/templates/components/layout/Header.d.ts +1 -1
  36. package/dist/templates/components/layout/Header.js +10 -12
  37. package/dist/templates/components/layout/SharedStyles.d.ts +1 -1
  38. package/dist/templates/components/layout/SharedStyles.js +26 -2
  39. package/dist/templates/components/layout/StaticLinks.d.ts +1 -1
  40. package/dist/templates/components/layout/StaticLinks.js +7 -3
  41. package/dist/templates/components/layout/Steps.d.ts +1 -1
  42. package/dist/templates/components/layout/Steps.js +7 -2
  43. package/dist/templates/components/layout/Tabs.d.ts +1 -1
  44. package/dist/templates/components/layout/Tabs.js +2 -2
  45. package/dist/templates/components/layout/Update.d.ts +1 -1
  46. package/dist/templates/components/layout/Update.js +1 -1
  47. package/dist/templates/mdx/ai-assistant.mdx.d.ts +1 -1
  48. package/dist/templates/mdx/ai-assistant.mdx.js +8 -0
  49. package/dist/templates/mdx/callouts.mdx.d.ts +1 -1
  50. package/dist/templates/mdx/callouts.mdx.js +6 -2
  51. package/dist/templates/mdx/cards.mdx.d.ts +1 -1
  52. package/dist/templates/mdx/cards.mdx.js +19 -3
  53. package/dist/templates/mdx/columns.mdx.d.ts +1 -1
  54. package/dist/templates/mdx/columns.mdx.js +2 -2
  55. package/dist/templates/mdx/commands.mdx.d.ts +1 -1
  56. package/dist/templates/mdx/commands.mdx.js +10 -2
  57. package/dist/templates/mdx/components.mdx.d.ts +1 -0
  58. package/dist/templates/mdx/components.mdx.js +56 -0
  59. package/dist/templates/mdx/deployment.mdx.d.ts +1 -1
  60. package/dist/templates/mdx/deployment.mdx.js +1 -1
  61. package/dist/templates/mdx/globals.mdx.d.ts +1 -1
  62. package/dist/templates/mdx/globals.mdx.js +5 -0
  63. package/dist/templates/mdx/index.mdx.d.ts +1 -1
  64. package/dist/templates/mdx/index.mdx.js +5 -5
  65. package/dist/templates/mdx/model-context-protocol.mdx.d.ts +1 -1
  66. package/dist/templates/mdx/model-context-protocol.mdx.js +2 -2
  67. package/dist/templates/mdx/navigation.mdx.d.ts +1 -1
  68. package/dist/templates/mdx/navigation.mdx.js +1 -1
  69. package/dist/templates/mdx/platform/ai-assistant.mdx.d.ts +1 -1
  70. package/dist/templates/mdx/platform/ai-assistant.mdx.js +20 -0
  71. package/dist/templates/mdx/platform/external-links.mdx.d.ts +1 -1
  72. package/dist/templates/mdx/platform/external-links.mdx.js +2 -0
  73. package/dist/templates/mdx/platform/fonts-settings.mdx.d.ts +1 -1
  74. package/dist/templates/mdx/platform/fonts-settings.mdx.js +8 -5
  75. package/dist/templates/mdx/platform/index.mdx.d.ts +1 -1
  76. package/dist/templates/mdx/platform/index.mdx.js +10 -1
  77. package/dist/templates/mdx/platform/site-settings.mdx.d.ts +1 -1
  78. package/dist/templates/mdx/platform/site-settings.mdx.js +4 -4
  79. package/dist/templates/mdx/sections.mdx.d.ts +1 -1
  80. package/dist/templates/mdx/sections.mdx.js +2 -2
  81. package/dist/templates/mdx/steps.mdx.d.ts +1 -1
  82. package/dist/templates/mdx/steps.mdx.js +4 -0
  83. package/dist/templates/mdx/tabs.mdx.d.ts +1 -1
  84. package/dist/templates/mdx/tabs.mdx.js +1 -1
  85. package/dist/templates/package.js +5 -5
  86. package/dist/templates/services/llm/types.d.ts +1 -1
  87. package/dist/templates/services/llm/types.js +1 -1
  88. package/dist/templates/services/mcp/server.d.ts +1 -1
  89. package/dist/templates/services/mcp/server.js +1 -1
  90. package/dist/templates/services/mcp/tools.d.ts +1 -1
  91. package/dist/templates/services/mcp/tools.js +1 -5
  92. package/dist/templates/utils/config.d.ts +1 -1
  93. package/dist/templates/utils/config.js +1 -1
  94. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { CHAT_WIDTH } from "../app/theme.js";
1
2
  export const chatTemplate = `"use client";
2
3
  import React, {
3
4
  createContext,
@@ -9,17 +10,21 @@ import React, {
9
10
  import styled, { css, keyframes } from "styled-components";
10
11
  import { rgba } from "polished";
11
12
  import { Button } from "cherry-styled-components";
12
- import { ArrowUp, LoaderPinwheel, Sparkles, X } from "lucide-react";
13
+ import { ArrowUp, LoaderPinwheel, RotateCcw, Sparkles, X } from "lucide-react";
13
14
  import remarkGfm from "remark-gfm";
14
15
  import rehypeHighlight from "rehype-highlight";
15
16
  import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
16
17
  import { serialize } from "next-mdx-remote/serialize";
18
+ import Link from "next/link";
17
19
  import { mq, Theme } from "@/app/theme";
20
+ import { useLockBodyScroll } from "@/components/LockBodyScroll";
18
21
  import { useMDXComponents as getMDXComponents } from "@/components/MDXComponents";
19
22
  import {
20
23
  styledAnchor,
21
24
  styledTable,
22
25
  stylesLists,
26
+ StyledSmallButton,
27
+ interactiveStyles,
23
28
  } from "@/components/layout/SharedStyled";
24
29
 
25
30
  const mdxComponents = getMDXComponents({});
@@ -40,7 +45,7 @@ const StyledChat = styled.div<{ theme: Theme; $isVisible: boolean }>\`
40
45
  top: 0;
41
46
  right: 0;
42
47
  width: 100%;
43
- height: calc(100vh - 90px);
48
+ height: calc(100dvh - 90px);
44
49
  overflow-y: scroll;
45
50
  overflow-x: hidden;
46
51
  z-index: 1000;
@@ -49,6 +54,7 @@ const StyledChat = styled.div<{ theme: Theme; $isVisible: boolean }>\`
49
54
  transform: translateX(0);
50
55
  background: \${({ theme }) => theme.colors.light};
51
56
  -webkit-overflow-scrolling: touch;
57
+ opacity: 1;
52
58
 
53
59
  &::-webkit-scrollbar {
54
60
  display: none;
@@ -58,10 +64,11 @@ const StyledChat = styled.div<{ theme: Theme; $isVisible: boolean }>\`
58
64
  !$isVisible &&
59
65
  css\`
60
66
  transform: translateX(100%);
67
+ opacity: 0;
61
68
  \`}
62
69
 
63
70
  \${mq("lg")} {
64
- width: 420px;
71
+ width: ${CHAT_WIDTH}px;
65
72
  border-left: solid 1px \${({ theme }) => theme.colors.grayLight};
66
73
  }
67
74
  \`;
@@ -352,15 +359,17 @@ const StyledChatForm = styled.form<{ theme: Theme; $isVisible: boolean }>\`
352
359
  border-top: solid 1px \${({ theme }) => theme.colors.grayLight};
353
360
  transition: all 0.3s ease;
354
361
  transform: translateX(100%);
362
+ opacity: 0;
355
363
 
356
364
  \${mq("lg")} {
357
- width: 420px;
365
+ width: ${CHAT_WIDTH}px;
358
366
  border-left: solid 1px \${({ theme }) => theme.colors.grayLight};
359
367
  }
360
368
 
361
369
  \${({ $isVisible }) =>
362
370
  $isVisible &&
363
371
  css\`
372
+ opacity: 1;
364
373
  transform: translateX(0);
365
374
  \`}
366
375
 
@@ -369,53 +378,94 @@ const StyledChatForm = styled.form<{ theme: Theme; $isVisible: boolean }>\`
369
378
  }
370
379
  \`;
371
380
 
372
- const StyledChatFixedForm = styled.form<{
381
+ const StyledGlowSmallButton = styled(StyledSmallButton)<{
373
382
  theme: Theme;
374
- $hide: boolean;
383
+ $hasContent: boolean;
375
384
  }>\`
376
- transition: all 0.3s ease;
377
- position: fixed;
378
- bottom: 20px;
379
- left: 20px;
380
- width: calc(100% - 115px);
381
- z-index: 998;
382
-
383
- \${mq("lg")} {
384
- left: 50%;
385
- transform: translateX(-50%) translateY(0);
386
- bottom: initial;
387
- position: absolute;
388
- top: 153px;
389
- width: calc(100% - 320px * 2 - 40px);
390
- opacity: 1;
385
+ @property --gradient-angle {
386
+ syntax: "<angle>";
387
+ initial-value: 0deg;
388
+ inherits: false;
391
389
  }
392
390
 
393
- \${({ $hide }) =>
394
- $hide &&
395
- css\`
396
- transform: translateX(-100px);
391
+ position: relative;
392
+ isolation: isolate;
393
+ margin-right: 0;
394
+ background: \${({ theme }) => theme.colors.light};
395
+ padding: 0;
397
396
 
398
- \${mq("lg")} {
399
- opacity: 0;
400
- transform: translateX(-50%) translateY(-20px);
401
- }
402
- \`}
397
+ &::before {
398
+ content: "";
399
+ inset: -2px;
400
+ border-radius: 8px;
401
+ background: conic-gradient(
402
+ from var(--gradient-angle),
403
+ #cc5555,
404
+ #d9a745,
405
+ #3ab0cc,
406
+ #cc7fc2,
407
+ #4380cc,
408
+ #4c1fa3,
409
+ #cc5555
410
+ );
411
+ opacity: 0;
412
+ transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
413
+ animation: \${rotateGradient} 3s linear infinite;
414
+ z-index: -1;
415
+ position: absolute;
416
+ top: -2px;
417
+ left: -2px;
418
+ width: calc(100% + 4px);
419
+ height: calc(100% + 4px);
420
+ }
403
421
 
404
- & .loading {
405
- animation: \${loadingAnimation} 1s linear infinite;
422
+ &::after {
423
+ content: "";
424
+ position: absolute;
425
+ inset: -8px;
426
+ border-radius: 14px;
427
+ background: conic-gradient(
428
+ from var(--gradient-angle),
429
+ \${rgba("#ff6b6b", 0.4)},
430
+ \${rgba("#feca57", 0.4)},
431
+ \${rgba("#48dbfb", 0.4)},
432
+ \${rgba("#ff9ff3", 0.4)},
433
+ \${rgba("#54a0ff", 0.4)},
434
+ \${rgba("#5f27cd", 0.4)},
435
+ \${rgba("#ff6b6b", 0.4)}
436
+ );
437
+ opacity: 0;
438
+ transition: opacity 0.4s cubic-bezier(0.4, 0, 0.2, 1);
439
+ animation:
440
+ \${rotateGradient} 3s linear infinite,
441
+ \${pulseGlow} 2s ease-in-out infinite;
442
+ z-index: -2;
443
+ pointer-events: none;
406
444
  }
407
- \`;
408
445
 
409
- const StyledChatFixedInner = styled.div\`
410
- margin: auto;
411
- display: flex;
412
- gap: 10px;
413
- justify-content: center;
414
- align-items: center;
446
+ &:hover::before,
447
+ &:hover::after {
448
+ opacity: 1;
449
+ }
415
450
 
416
- \${mq("lg")} {
417
- max-width: 640px;
451
+ & span {
452
+ padding: 6px 8px;
453
+ display: flex;
454
+ background: \${({ theme }) => theme.colors.light};
455
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
456
+ gap: 6px;
418
457
  }
458
+
459
+ \${({ $hasContent }) =>
460
+ $hasContent &&
461
+ css\`
462
+ &::before {
463
+ opacity: 1;
464
+ }
465
+ &::after {
466
+ opacity: 1;
467
+ }
468
+ \`}
419
469
  \`;
420
470
 
421
471
  const StyledError = styled.div<{ theme: Theme }>\`
@@ -561,6 +611,40 @@ const StyledAnswer = styled.div<{ theme: Theme; $isAnswer: boolean }>\`
561
611
  }
562
612
  \`;
563
613
 
614
+ const StyledSources = styled.div\`
615
+ display: flex;
616
+ gap: 16px;
617
+ flex-wrap: wrap;
618
+ margin: -5px 0 20px;
619
+ \`;
620
+
621
+ const StyledSourceLink = styled(Link)<{ theme: Theme }>\`
622
+ position: relative;
623
+ text-decoration: none;
624
+ font-size: \${({ theme }) => theme.fontSizes.small.lg};
625
+ line-height: 1;
626
+ color: \${({ theme }) => theme.colors.primary};
627
+ display: flex;
628
+ gap: 6px;
629
+ transition: all 0.3s ease;
630
+ font-weight: 600;
631
+ white-space: nowrap;
632
+ min-width: fit-content;
633
+ background: \${({ theme }) => rgba(theme.colors.primaryLight, 0.1)};
634
+ padding: 6px 8px;
635
+ border-radius: \${({ theme }) => theme.spacing.radius.xs};
636
+ \${interactiveStyles};
637
+
638
+ & * {
639
+ margin: auto 0;
640
+ }
641
+
642
+ &:hover {
643
+ color: \${({ theme }) =>
644
+ theme.isDark ? theme.colors.primaryLight : theme.colors.primaryDark};
645
+ }
646
+ \`;
647
+
564
648
  const StyledChatTitle = styled.div<{ theme: Theme }>\`
565
649
  display: flex;
566
650
  flex-wrap: nowrap;
@@ -578,7 +662,7 @@ const StyledChatTitle = styled.div<{ theme: Theme }>\`
578
662
  const StyledChatTitleIconWrapper = styled.span<{ theme: Theme }>\`
579
663
  display: flex;
580
664
  align-items: center;
581
- gap: 5px;
665
+ gap: 12px;
582
666
  color: \${({ theme }) => theme.colors.dark};
583
667
  \`;
584
668
 
@@ -600,10 +684,18 @@ const StyledChatCloseButton = styled.button<{ theme: Theme }>\`
600
684
  }
601
685
  \`;
602
686
 
687
+ type Source = {
688
+ id: string;
689
+ path: string;
690
+ uri: string;
691
+ score: number;
692
+ };
693
+
603
694
  type Answer = {
604
695
  text: string;
605
696
  answer?: boolean;
606
697
  mdx?: MDXRemoteSerializeResult;
698
+ sources?: Source[];
607
699
  };
608
700
 
609
701
  const SPARKLE_COLORS = [
@@ -636,6 +728,7 @@ interface RainbowInputProps {
636
728
  placeholder?: string;
637
729
  autoComplete?: string;
638
730
  "aria-label"?: string;
731
+ inputRef?: React.Ref<HTMLInputElement>;
639
732
  }
640
733
 
641
734
  function RainbowInput({
@@ -645,6 +738,7 @@ function RainbowInput({
645
738
  placeholder,
646
739
  autoComplete,
647
740
  "aria-label": ariaLabel,
741
+ inputRef,
648
742
  }: RainbowInputProps) {
649
743
  const [isFocused, setIsFocused] = useState(false);
650
744
  const [isHovered, setIsHovered] = useState(false);
@@ -675,6 +769,7 @@ function RainbowInput({
675
769
  ))}
676
770
  </StyledSparkleContainer>
677
771
  <StyledRainbowInput
772
+ ref={inputRef}
678
773
  id={id}
679
774
  value={value}
680
775
  onChange={onChange}
@@ -688,14 +783,54 @@ function RainbowInput({
688
783
  );
689
784
  }
690
785
 
786
+ function ChatButtonCTA() {
787
+ const { setIsOpen, isOpen, answer, setAnswer, chatInputRef } =
788
+ useContext(ChatContext);
789
+
790
+ return (
791
+ <StyledGlowSmallButton
792
+ onClick={() => {
793
+ const next = !isOpen;
794
+ setIsOpen(next);
795
+ if (next) {
796
+ if (answer.length === 0) {
797
+ setAnswer([
798
+ { text: "Hey there, how can I assist you?", answer: true },
799
+ ]);
800
+ }
801
+ setTimeout(() => {
802
+ chatInputRef.current?.focus();
803
+ }, 350);
804
+ }
805
+ }}
806
+ aria-label="Ask AI Assistant"
807
+ $hasContent={isOpen}
808
+ type="button"
809
+ >
810
+ <span>
811
+ <Sparkles size={16} />
812
+ Ask AI
813
+ </span>
814
+ </StyledGlowSmallButton>
815
+ );
816
+ }
817
+
691
818
  function Chat() {
692
- const [question, setQuestion] = useState("");
693
- const [loading, setLoading] = useState(false);
694
- const [error, setError] = useState<string | null>(null);
695
- const [answer, setAnswer] = useState<Answer[]>([]);
819
+ const {
820
+ isOpen,
821
+ question,
822
+ setQuestion,
823
+ loading,
824
+ error,
825
+ answer,
826
+ ask,
827
+ closeChat,
828
+ resetChat,
829
+ chatInputRef,
830
+ } = useContext(ChatContext);
696
831
  const endRef = useRef<HTMLDivElement | null>(null);
697
- const abortRef = useRef<AbortController | null>(null);
698
- const { isOpen, setIsOpen } = useContext(ChatContext);
832
+
833
+ useLockBodyScroll(isOpen);
699
834
 
700
835
  useEffect(() => {
701
836
  endRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
@@ -703,12 +838,150 @@ function Chat() {
703
838
 
704
839
  useEffect(() => {
705
840
  if (answer?.length > 0) {
706
- const el = document.getElementById(
707
- "chat-bottom-input",
708
- ) as HTMLInputElement | null;
709
- el?.focus();
841
+ chatInputRef.current?.focus();
710
842
  }
711
- }, [answer]);
843
+ }, [answer, chatInputRef]);
844
+
845
+ return (
846
+ <>
847
+ <StyledChat $isVisible={isOpen}>
848
+ <StyledChatTitle>
849
+ <StyledChatTitleIconWrapper>
850
+ <Sparkles />
851
+ <h3>AI Assistant</h3>
852
+ </StyledChatTitleIconWrapper>
853
+ <StyledChatTitleIconWrapper>
854
+ <StyledChatCloseButton
855
+ onClick={resetChat}
856
+ aria-label="Reset chat history"
857
+ title="Reset chat history"
858
+ >
859
+ <RotateCcw size={18} />
860
+ </StyledChatCloseButton>
861
+ <StyledChatCloseButton
862
+ onClick={closeChat}
863
+ aria-label="Close chat"
864
+ title="Close chat"
865
+ >
866
+ <X />
867
+ </StyledChatCloseButton>
868
+ </StyledChatTitleIconWrapper>
869
+ </StyledChatTitle>
870
+ {answer &&
871
+ answer.map((a, i) => (
872
+ <React.Fragment key={i}>
873
+ <StyledAnswer $isAnswer={a.answer ?? false}>
874
+ {a.answer && a.mdx ? (
875
+ <MDXRemote {...a.mdx} components={mdxComponents} />
876
+ ) : (
877
+ a.text
878
+ )}
879
+ </StyledAnswer>
880
+ {a.answer && a.sources && a.sources.length > 0 && (
881
+ <StyledSources>
882
+ {a.sources.map((src) => {
883
+ const slug = src.uri
884
+ .replace("docs://", "")
885
+ .replace(/^\\/+/, "");
886
+ const href = slug ? \`/\${slug}/\` : "/";
887
+ const label = slug
888
+ ? slug
889
+ .split("/")
890
+ .pop()!
891
+ .replace(/-/g, " ")
892
+ .replace(/\\b\\w/g, (c: string) => c.toUpperCase())
893
+ : "Home";
894
+ return (
895
+ <StyledSourceLink key={src.id} href={href}>
896
+ {label}
897
+ </StyledSourceLink>
898
+ );
899
+ })}
900
+ </StyledSources>
901
+ )}
902
+ </React.Fragment>
903
+ ))}
904
+ {loading && (
905
+ <StyledLoading>
906
+ Answering<span>.</span>
907
+ <span>.</span>
908
+ <span>.</span>
909
+ </StyledLoading>
910
+ )}
911
+ {error && (
912
+ <StyledError>
913
+ <strong>Error:</strong> {error}
914
+ </StyledError>
915
+ )}
916
+ <div ref={endRef} />
917
+ </StyledChat>
918
+
919
+ <StyledChatForm onSubmit={ask} $isVisible={isOpen}>
920
+ <RainbowInput
921
+ id="chat-bottom-input"
922
+ inputRef={chatInputRef}
923
+ value={question}
924
+ onChange={(e) => setQuestion(e.target.value)}
925
+ placeholder="Ask AI Assistant..."
926
+ autoComplete="off"
927
+ aria-label="Ask a follow-up question"
928
+ />
929
+ <StyledRainbowButton
930
+ type="submit"
931
+ disabled={loading || question.trim() === ""}
932
+ $hasContent={question.trim().length > 0}
933
+ aria-label={loading ? "Loading response" : "Submit question"}
934
+ >
935
+ {loading ? <LoaderPinwheel className="loading" /> : <ArrowUp />}
936
+ </StyledRainbowButton>
937
+ </StyledChatForm>
938
+ </>
939
+ );
940
+ }
941
+
942
+ const ChatContext = createContext<{
943
+ isOpen: boolean;
944
+ setIsOpen: (isOpen: boolean) => void;
945
+ isChatActive: boolean;
946
+ question: string;
947
+ setQuestion: (q: string) => void;
948
+ loading: boolean;
949
+ error: string | null;
950
+ answer: Answer[];
951
+ setAnswer: (answers: Answer[]) => void;
952
+ ask: (e: React.FormEvent) => void;
953
+ closeChat: () => void;
954
+ resetChat: () => void;
955
+ chatInputRef: React.RefObject<HTMLInputElement | null>;
956
+ }>({
957
+ isOpen: false,
958
+ setIsOpen: () => {},
959
+ isChatActive: false,
960
+ question: "",
961
+ setQuestion: () => {},
962
+ loading: false,
963
+ error: null,
964
+ answer: [],
965
+ setAnswer: () => {},
966
+ ask: () => {},
967
+ closeChat: () => {},
968
+ resetChat: () => {},
969
+ chatInputRef: { current: null },
970
+ });
971
+
972
+ interface ChatContextProviderProps {
973
+ children: React.ReactNode;
974
+ isChatActive: boolean;
975
+ }
976
+
977
+ const ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {
978
+ const [isOpen, setIsOpen] = useState(false);
979
+ const [question, setQuestion] = useState("");
980
+ const [loading, setLoading] = useState(false);
981
+ const [error, setError] = useState<string | null>(null);
982
+ const [answer, setAnswer] = useState<Answer[]>([]);
983
+ const abortRef = useRef<AbortController | null>(null);
984
+ const chatInputRef = useRef<HTMLInputElement | null>(null);
712
985
 
713
986
  async function ask(e: React.FormEvent) {
714
987
  e.preventDefault();
@@ -730,7 +1003,6 @@ function Chat() {
730
1003
  abortRef.current = controller;
731
1004
 
732
1005
  try {
733
- // Build conversation history from previous Q&A pairs
734
1006
  const history = answer
735
1007
  .filter((a) => a.text.trim() !== "")
736
1008
  .map((a) => ({
@@ -753,11 +1025,11 @@ function Chat() {
753
1025
  const reader = res.body?.getReader();
754
1026
  const decoder = new TextDecoder();
755
1027
  const contentParts: string[] = [];
1028
+ let sources: Source[] = [];
756
1029
  if (!reader) {
757
1030
  throw new Error("Failed to get response reader");
758
1031
  }
759
1032
 
760
- // Add a placeholder for the streaming answer
761
1033
  const streamingAnswerIndex = mergedQuestions.length;
762
1034
  setAnswer([...mergedQuestions, { text: "", answer: true }]);
763
1035
 
@@ -768,7 +1040,6 @@ function Chat() {
768
1040
 
769
1041
  buffer += decoder.decode(value, { stream: true });
770
1042
  const parts = buffer.split("\\n");
771
- // Keep the last (potentially incomplete) part in the buffer
772
1043
  buffer = parts.pop() ?? "";
773
1044
 
774
1045
  for (const line of parts) {
@@ -776,7 +1047,15 @@ function Chat() {
776
1047
  try {
777
1048
  const data = JSON.parse(line.slice(6));
778
1049
 
779
- if (data.type === "content") {
1050
+ if (data.type === "metadata") {
1051
+ const allSources: Source[] = data.data?.sources ?? [];
1052
+ const seen = new Set<string>();
1053
+ sources = allSources.filter((s: Source) => {
1054
+ if (s.score < 0.4 || seen.has(s.uri)) return false;
1055
+ seen.add(s.uri);
1056
+ return true;
1057
+ });
1058
+ } else if (data.type === "content") {
780
1059
  contentParts.push(data.data);
781
1060
  const streamedContent = contentParts.join("");
782
1061
 
@@ -785,6 +1064,7 @@ function Chat() {
785
1064
  newAnswers[streamingAnswerIndex] = {
786
1065
  text: streamedContent,
787
1066
  answer: true,
1067
+ sources,
788
1068
  };
789
1069
  return newAnswers;
790
1070
  });
@@ -792,7 +1072,6 @@ function Chat() {
792
1072
  throw new Error(data.data);
793
1073
  } else if (data.type === "done") {
794
1074
  const streamedContent = contentParts.join("");
795
- // Finalize with MDX serialization
796
1075
  let mdxSource: MDXRemoteSerializeResult | null = null;
797
1076
  try {
798
1077
  mdxSource = await serialize(streamedContent, {
@@ -814,6 +1093,7 @@ function Chat() {
814
1093
  text: streamedContent,
815
1094
  answer: true,
816
1095
  mdx: mdxSource || undefined,
1096
+ sources,
817
1097
  };
818
1098
  return newAnswers;
819
1099
  });
@@ -838,109 +1118,16 @@ function Chat() {
838
1118
  }
839
1119
  }
840
1120
 
841
- return (
842
- <>
843
- <StyledChatFixedForm onSubmit={ask} $hide={answer?.length > 0}>
844
- <StyledChatFixedInner>
845
- <RainbowInput
846
- value={question}
847
- onChange={(e) => setQuestion(e.target.value)}
848
- placeholder="Ask AI Assistant..."
849
- autoComplete="off"
850
- aria-label="Ask a question about the documentation"
851
- />
852
- <StyledRainbowButton
853
- type="submit"
854
- disabled={loading}
855
- $hasContent={question.trim().length > 0}
856
- aria-label={loading ? "Loading response" : "Submit question"}
857
- >
858
- {loading ? <LoaderPinwheel className="loading" /> : <ArrowUp />}
859
- </StyledRainbowButton>
860
- </StyledChatFixedInner>
861
- </StyledChatFixedForm>
862
-
863
- <StyledChat $isVisible={isOpen}>
864
- <StyledChatTitle>
865
- <StyledChatTitleIconWrapper>
866
- <Sparkles />
867
- <h3>AI Assistant</h3>
868
- </StyledChatTitleIconWrapper>
869
- <StyledChatCloseButton
870
- onClick={() => {
871
- abortRef.current?.abort();
872
- setAnswer([]);
873
- setIsOpen(false);
874
- }}
875
- aria-label="Close chat"
876
- >
877
- <X />
878
- </StyledChatCloseButton>
879
- </StyledChatTitle>
880
- {answer &&
881
- answer.map((a, i) => (
882
- <StyledAnswer key={i} $isAnswer={a.answer ?? false}>
883
- {a.answer && a.mdx ? (
884
- <MDXRemote {...a.mdx} components={mdxComponents} />
885
- ) : (
886
- a.text
887
- )}
888
- </StyledAnswer>
889
- ))}
890
- {loading && (
891
- <StyledLoading>
892
- Answering<span>.</span>
893
- <span>.</span>
894
- <span>.</span>
895
- </StyledLoading>
896
- )}
897
- {error && (
898
- <StyledError>
899
- <strong>Error:</strong> {error}
900
- </StyledError>
901
- )}
902
- <div ref={endRef} />
903
- </StyledChat>
904
-
905
- <StyledChatForm onSubmit={ask} $isVisible={isOpen}>
906
- <RainbowInput
907
- id="chat-bottom-input"
908
- value={question}
909
- onChange={(e) => setQuestion(e.target.value)}
910
- placeholder="Ask AI Assistant..."
911
- autoComplete="off"
912
- aria-label="Ask a follow-up question"
913
- />
914
- <StyledRainbowButton
915
- type="submit"
916
- disabled={loading || question.trim() === ""}
917
- $hasContent={question.trim().length > 0}
918
- aria-label={loading ? "Loading response" : "Submit question"}
919
- >
920
- {loading ? <LoaderPinwheel className="loading" /> : <ArrowUp />}
921
- </StyledRainbowButton>
922
- </StyledChatForm>
923
- </>
924
- );
925
- }
926
-
927
- const ChatContext = createContext<{
928
- isOpen: boolean;
929
- setIsOpen: (isOpen: boolean) => void;
930
- isChatActive: boolean;
931
- }>({
932
- isOpen: false,
933
- setIsOpen: () => {},
934
- isChatActive: false,
935
- });
936
-
937
- interface ChatContextProviderProps {
938
- children: React.ReactNode;
939
- isChatActive: boolean;
940
- }
1121
+ function closeChat() {
1122
+ setIsOpen(false);
1123
+ }
941
1124
 
942
- const ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {
943
- const [isOpen, setIsOpen] = useState(false);
1125
+ function resetChat() {
1126
+ abortRef.current?.abort();
1127
+ setLoading(false);
1128
+ setError(null);
1129
+ setAnswer([{ text: "Hey there, how can I assist you?", answer: true }]);
1130
+ }
944
1131
 
945
1132
  return (
946
1133
  <ChatContext.Provider
@@ -948,6 +1135,16 @@ const ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {
948
1135
  isOpen,
949
1136
  setIsOpen,
950
1137
  isChatActive,
1138
+ question,
1139
+ setQuestion,
1140
+ loading,
1141
+ error,
1142
+ answer,
1143
+ setAnswer,
1144
+ ask,
1145
+ closeChat,
1146
+ resetChat,
1147
+ chatInputRef,
951
1148
  }}
952
1149
  >
953
1150
  {children}
@@ -955,5 +1152,5 @@ const ChtProvider = ({ children, isChatActive }: ChatContextProviderProps) => {
955
1152
  );
956
1153
  };
957
1154
 
958
- export { Chat, ChtProvider, ChatContext };
1155
+ export { Chat, ChtProvider, ChatContext, ChatButtonCTA };
959
1156
  `;