radiant-docs 0.1.61 → 0.1.63

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 (39) hide show
  1. package/package.json +1 -1
  2. package/template/astro.config.mjs +38 -27
  3. package/template/package-lock.json +2858 -1140
  4. package/template/package.json +18 -13
  5. package/template/scripts/generate-proxy-allowed-origins.mjs +10 -179
  6. package/template/scripts/publish-shiki-platform-assets.mjs +1177 -0
  7. package/template/src/components/Header.astro +6 -1
  8. package/template/src/components/NavigationTabList.astro +65 -0
  9. package/template/src/components/NavigationTabs.astro +109 -0
  10. package/template/src/components/OpenApiPage.astro +17 -1
  11. package/template/src/components/Sidebar.astro +2 -2
  12. package/template/src/components/SidebarDropdown.astro +105 -44
  13. package/template/src/components/SidebarMenu.astro +3 -0
  14. package/template/src/components/SidebarSegmented.astro +87 -52
  15. package/template/src/components/SidebarTabs.astro +86 -0
  16. package/template/src/components/chat/AssistantDocsWidget.tsx +127 -2
  17. package/template/src/components/chat/AssistantEmbedPanel.tsx +401 -283
  18. package/template/src/components/endpoint/PlaygroundForm.astro +69 -55
  19. package/template/src/components/endpoint/ResponseDisplay.astro +2 -2
  20. package/template/src/components/user/Accordion.astro +1 -1
  21. package/template/src/components/user/Callout.astro +2 -2
  22. package/template/src/components/user/CodeBlock.astro +58 -7
  23. package/template/src/components/user/CodeGroup.astro +52 -1
  24. package/template/src/components/user/Column.astro +1 -1
  25. package/template/src/components/user/Step.astro +1 -1
  26. package/template/src/components/user/Tabs.astro +1 -1
  27. package/template/src/generated/shiki-platform-assets.json +24 -0
  28. package/template/src/layouts/Layout.astro +111 -8
  29. package/template/src/lib/assistant-panel-config.ts +4 -0
  30. package/template/src/lib/assistant-shiki-client.ts +522 -0
  31. package/template/src/lib/client-shiki-config.ts +60 -0
  32. package/template/src/lib/dev-playground-proxy.mjs +597 -0
  33. package/template/src/lib/mdx/remark-resolve-internal-links.ts +334 -17
  34. package/template/src/lib/proxy-allowed-origins.mjs +189 -0
  35. package/template/src/lib/routes.ts +66 -24
  36. package/template/src/styles/global.css +16 -4
  37. package/template/src/components/ui/demo/CodeDemo.astro +0 -15
  38. package/template/src/components/ui/demo/Demo.astro +0 -3
  39. package/template/src/components/ui/demo/UiDisplay.astro +0 -13
@@ -1,34 +1,18 @@
1
1
  import type { JSX } from "preact";
2
2
  import { useEffect, useRef, useState } from "preact/hooks";
3
3
  import { Icon } from "@iconify/react";
4
- import Prism from "prismjs";
5
- import "prismjs/components/prism-markup.js";
6
- import "prismjs/components/prism-clike.js";
7
- import "prismjs/components/prism-javascript.js";
8
- import "prismjs/components/prism-typescript.js";
9
- import "prismjs/components/prism-jsx.js";
10
- import "prismjs/components/prism-tsx.js";
11
- import "prismjs/components/prism-json.js";
12
- import "prismjs/components/prism-markdown.js";
13
- import "prismjs/components/prism-bash.js";
14
- import "prismjs/components/prism-python.js";
15
- import "prismjs/components/prism-yaml.js";
16
- import "prismjs/components/prism-sql.js";
17
- import "prismjs/components/prism-rust.js";
18
- import "prismjs/components/prism-go.js";
19
- import "prismjs/components/prism-java.js";
20
- import "prismjs/components/prism-markup-templating.js";
21
- import "prismjs/components/prism-php.js";
22
- import "prismjs/components/prism-ruby.js";
23
- import "prismjs/components/prism-css.js";
24
- import "prismjs/components/prism-diff.js";
25
- import "prism-themes/themes/prism-one-light.css";
26
4
  import { type Plugin, unified } from "unified";
27
5
  import remarkParse from "remark-parse";
28
6
  import remarkGfm from "remark-gfm";
29
7
  import remarkRehype from "remark-rehype";
30
8
  import rehypeStringify from "rehype-stringify";
31
9
  import { getDocsBasePath, withBasePath } from "../../lib/base-path";
10
+ import {
11
+ highlightAssistantCodeToHtml,
12
+ normalizeAssistantCodeLanguage,
13
+ warmAssistantShikiRuntime,
14
+ type AssistantShikiRuntimeConfig,
15
+ } from "../../lib/assistant-shiki-client";
32
16
 
33
17
  type AssistantLinkTarget = "current" | "blank";
34
18
  export type AssistantPanelSize = "default" | "expanded";
@@ -59,10 +43,13 @@ type AssistantEmbedPanelProps = {
59
43
  allowApiPathQueryOverride?: boolean;
60
44
  openSignal?: number;
61
45
  panelSize?: AssistantPanelSize;
46
+ mobileBreakpoint?: string;
47
+ panelFullscreenHeightBreakpoint?: string;
62
48
  onRequestOpen?: () => void;
63
49
  onRequestClose?: () => void;
64
50
  onRequestPanelSizeToggle?: (size: AssistantPanelSize) => void;
65
51
  onCurrentLinkNavigate?: (href: string, sourceElement?: Element) => void;
52
+ shiki?: AssistantShikiRuntimeConfig;
66
53
  };
67
54
 
68
55
  type AssistantColorByMode = {
@@ -70,15 +57,22 @@ type AssistantColorByMode = {
70
57
  dark: string;
71
58
  };
72
59
 
60
+ type AssistantMessageSource = {
61
+ href: string;
62
+ label: string;
63
+ };
64
+
73
65
  type ChatMessage = {
74
66
  id: string;
75
67
  role: "user" | "assistant";
76
68
  content: string;
69
+ sources?: AssistantMessageSource[];
77
70
  };
78
71
 
79
72
  type AssistantStreamEvent = {
80
73
  type?: string;
81
74
  delta?: string;
75
+ sources?: unknown;
82
76
  };
83
77
 
84
78
  type ChatInputKeyEvent = JSX.TargetedKeyboardEvent<HTMLTextAreaElement>;
@@ -120,41 +114,50 @@ const HANDOFF_ACK_TYPE = "assistant-handoff:ack";
120
114
  const IN_FLIGHT_STALE_MS = 10000;
121
115
  const IN_FLIGHT_HEARTBEAT_MS = 3000;
122
116
  const MARKDOWN_HTML_CACHE_LIMIT = 300;
117
+ const DEFAULT_ASSISTANT_MOBILE_BREAKPOINT = "640px";
118
+ const DEFAULT_ASSISTANT_PANEL_FULLSCREEN_HEIGHT_BREAKPOINT = "560px";
123
119
  const markdownHtmlCache = new Map<string, string>();
124
- const PRISM_LANGUAGE_ALIAS: Record<string, string> = {
125
- js: "javascript",
126
- ts: "typescript",
127
- yml: "yaml",
128
- sh: "bash",
129
- shell: "bash",
130
- shellscript: "bash",
131
- md: "markdown",
132
- mdx: "markdown",
133
- };
134
-
135
- const PRISM_LANGUAGE_LABEL: Record<string, string> = {
120
+ const markdownHtmlPromiseCache = new Map<string, Promise<string>>();
121
+ const ASSISTANT_LANGUAGE_LABEL: Record<string, string> = {
122
+ bash: "Bash",
123
+ css: "CSS",
124
+ diff: "Diff",
125
+ go: "Go",
126
+ html: "HTML",
127
+ java: "Java",
136
128
  javascript: "JavaScript",
137
- typescript: "TypeScript",
138
129
  jsx: "JSX",
139
- tsx: "TSX",
140
130
  json: "JSON",
141
131
  markdown: "Markdown",
142
- bash: "Bash",
143
- shell: "Shell",
132
+ mdx: "MDX",
133
+ php: "PHP",
134
+ plaintext: "Text",
144
135
  python: "Python",
145
- yaml: "YAML",
146
- sql: "SQL",
136
+ ruby: "Ruby",
147
137
  rust: "Rust",
148
- go: "Go",
149
- java: "Java",
150
- css: "CSS",
151
- html: "HTML",
152
- diff: "Diff",
153
- mdx: "MDX",
138
+ shell: "Shell",
139
+ sql: "SQL",
140
+ tsx: "TSX",
141
+ typescript: "TypeScript",
142
+ yaml: "YAML",
154
143
  };
155
144
 
156
145
  const EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
157
146
 
147
+ function buildMarkdownCacheKey(
148
+ normalizedMarkdown: string,
149
+ linkTarget: AssistantLinkTarget,
150
+ shiki: AssistantShikiRuntimeConfig | undefined,
151
+ ): string {
152
+ return [
153
+ linkTarget,
154
+ shiki?.assetBaseUrl ?? "",
155
+ shiki?.syntaxThemes.light ?? "",
156
+ shiki?.syntaxThemes.dark ?? "",
157
+ normalizedMarkdown,
158
+ ].join("\0");
159
+ }
160
+
158
161
  function normalizeRelTokens(value: unknown): string[] {
159
162
  if (Array.isArray(value)) {
160
163
  return value
@@ -356,6 +359,33 @@ function createMessageId(): string {
356
359
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
357
360
  }
358
361
 
362
+ function normalizeAssistantMessageSources(
363
+ rawSources: unknown,
364
+ ): AssistantMessageSource[] {
365
+ if (!Array.isArray(rawSources)) {
366
+ return [];
367
+ }
368
+
369
+ return rawSources.flatMap((rawSource) => {
370
+ if (!rawSource || typeof rawSource !== "object") {
371
+ return [];
372
+ }
373
+
374
+ const source = rawSource as Partial<AssistantMessageSource>;
375
+ const href = typeof source.href === "string" ? source.href.trim() : "";
376
+ const label =
377
+ typeof source.label === "string" && source.label.trim()
378
+ ? source.label.trim()
379
+ : href;
380
+
381
+ if (!href || !label) {
382
+ return [];
383
+ }
384
+
385
+ return [{ href, label }];
386
+ });
387
+ }
388
+
359
389
  function normalizePersistedMessages(rawMessages: unknown): ChatMessage[] {
360
390
  if (!Array.isArray(rawMessages)) {
361
391
  return [];
@@ -377,6 +407,7 @@ function normalizePersistedMessages(rawMessages: unknown): ChatMessage[] {
377
407
  id: message.id,
378
408
  role: message.role,
379
409
  content: message.content,
410
+ sources: normalizeAssistantMessageSources(message.sources),
380
411
  }));
381
412
  }
382
413
 
@@ -395,6 +426,13 @@ function normalizePanelSize(value: unknown): AssistantPanelSize {
395
426
  return value === "expanded" ? "expanded" : "default";
396
427
  }
397
428
 
429
+ function getPanelFullscreenMediaQuery(
430
+ mobileBreakpoint: string,
431
+ panelFullscreenHeightBreakpoint: string,
432
+ ): string {
433
+ return `(max-width: ${mobileBreakpoint}), (max-height: ${panelFullscreenHeightBreakpoint})`;
434
+ }
435
+
398
436
  function normalizePersistedPanelState(rawState: unknown): PersistedPanelState {
399
437
  if (!rawState || typeof rawState !== "object") {
400
438
  return createEmptyPersistedPanelState();
@@ -574,32 +612,20 @@ function escapeHtml(value: string): string {
574
612
  .replaceAll("'", "&#39;");
575
613
  }
576
614
 
577
- function resolvePrismLanguage(rawLanguage: string): string {
578
- const normalized = rawLanguage.trim().toLowerCase();
579
- return PRISM_LANGUAGE_ALIAS[normalized] ?? normalized;
580
- }
581
-
582
- function resolvePrismLanguageLabel(language: string): string {
615
+ function resolveAssistantLanguageLabel(language: string): string {
583
616
  const normalized = language.trim().toLowerCase();
584
617
  if (!normalized) {
585
618
  return "";
586
619
  }
587
620
 
588
621
  return (
589
- PRISM_LANGUAGE_LABEL[normalized] ??
622
+ ASSISTANT_LANGUAGE_LABEL[normalized] ??
590
623
  normalized
591
624
  .replace(/[-_]+/g, " ")
592
625
  .replace(/\b\w/g, (char) => char.toUpperCase())
593
626
  );
594
627
  }
595
628
 
596
- function resolvePrismGrammar(language: string) {
597
- return (
598
- Prism.languages[language] ??
599
- Prism.languages[PRISM_LANGUAGE_ALIAS[language] ?? ""]
600
- );
601
- }
602
-
603
629
  function ensureCodeBlockCopyButton(preElement: HTMLPreElement): void {
604
630
  const existingButton = Array.from(preElement.children).find(
605
631
  (child) =>
@@ -697,12 +723,23 @@ function unwrapEscapedFenceCodeBlock(rawCode: string): {
697
723
  }
698
724
 
699
725
  return {
700
- language: resolvePrismLanguage(languageToken),
726
+ language: normalizeAssistantCodeLanguage(languageToken),
701
727
  code: lines.slice(1, -1).join("\n"),
702
728
  };
703
729
  }
704
730
 
705
- function highlightCodeBlocksInHtml(html: string): string {
731
+ function stripSerializedCodeBlockTrailingNewline(rawCode: string): string {
732
+ if (rawCode.endsWith("\n")) {
733
+ return rawCode.slice(0, -1);
734
+ }
735
+
736
+ return rawCode;
737
+ }
738
+
739
+ async function highlightCodeBlocksInHtml(
740
+ html: string,
741
+ shiki: AssistantShikiRuntimeConfig | undefined,
742
+ ): Promise<string> {
706
743
  if (typeof document === "undefined" || !html.trim()) {
707
744
  return html;
708
745
  }
@@ -711,8 +748,8 @@ function highlightCodeBlocksInHtml(html: string): string {
711
748
  const container = document.createElement("div");
712
749
  container.innerHTML = html;
713
750
 
714
- const codeNodes = container.querySelectorAll("pre > code");
715
- codeNodes.forEach((codeNode) => {
751
+ const codeNodes = Array.from(container.querySelectorAll("pre > code"));
752
+ for (const codeNode of codeNodes) {
716
753
  const getLanguageClassName = (element: Element | null) =>
717
754
  Array.from(element?.classList ?? []).find(
718
755
  (className) =>
@@ -741,13 +778,14 @@ function highlightCodeBlocksInHtml(html: string): string {
741
778
  }
742
779
  });
743
780
  preParent.classList.add(`language-${language}`);
744
- const languageLabel = resolvePrismLanguageLabel(language);
781
+ const languageLabel = resolveAssistantLanguageLabel(language);
745
782
  if (languageLabel) {
746
783
  preParent.setAttribute("data-language", languageLabel);
747
784
  } else {
748
785
  preParent.removeAttribute("data-language");
749
786
  }
750
787
 
788
+ codeNode.setAttribute("data-rd-code-theme", "");
751
789
  ensureCodeBlockCopyButton(preParent);
752
790
  }
753
791
  };
@@ -759,7 +797,9 @@ function highlightCodeBlocksInHtml(html: string): string {
759
797
  preLanguageClassName?.replace(/^(language-|lang-)/, "") ??
760
798
  "";
761
799
 
762
- let rawCode = codeNode.textContent ?? "";
800
+ let rawCode = stripSerializedCodeBlockTrailingNewline(
801
+ codeNode.textContent ?? "",
802
+ );
763
803
  if (!rawLanguage) {
764
804
  const unwrappedFence = unwrapEscapedFenceCodeBlock(rawCode);
765
805
  if (unwrappedFence) {
@@ -769,28 +809,37 @@ function highlightCodeBlocksInHtml(html: string): string {
769
809
  }
770
810
 
771
811
  if (!rawLanguage) {
772
- return;
812
+ const preParent = codeNode.parentElement;
813
+ if (preParent instanceof HTMLPreElement) {
814
+ ensureCodeBlockCopyButton(preParent);
815
+ }
816
+ continue;
773
817
  }
774
818
 
775
- const resolvedLanguage = resolvePrismLanguage(rawLanguage);
776
- const grammar =
777
- resolvePrismGrammar(resolvedLanguage) ??
778
- resolvePrismGrammar(rawLanguage);
779
- if (!grammar) {
780
- return;
819
+ const resolvedLanguage = normalizeAssistantCodeLanguage(rawLanguage);
820
+ setLanguageClass(resolvedLanguage);
821
+
822
+ if (!shiki) {
823
+ codeNode.textContent = rawCode;
824
+ continue;
781
825
  }
782
826
 
783
- setLanguageClass(resolvedLanguage);
784
827
  try {
785
- const highlighted = Prism.highlight(rawCode, grammar, resolvedLanguage);
786
- codeNode.innerHTML = highlighted;
828
+ const highlighted = await highlightAssistantCodeToHtml({
829
+ code: rawCode,
830
+ config: shiki,
831
+ language: resolvedLanguage,
832
+ });
833
+ setLanguageClass(highlighted.language);
834
+ codeNode.innerHTML = highlighted.html;
787
835
  } catch (error) {
788
836
  console.error("Assistant embed code highlighting failed", {
789
837
  language: resolvedLanguage,
790
838
  error,
791
839
  });
840
+ codeNode.textContent = rawCode;
792
841
  }
793
- });
842
+ }
794
843
 
795
844
  return container.innerHTML;
796
845
  } catch (error) {
@@ -805,45 +854,176 @@ function highlightCodeBlocksInHtml(html: string): string {
805
854
  function renderMarkdownToHtml(
806
855
  markdown: string,
807
856
  linkTarget: AssistantLinkTarget,
808
- ): string {
857
+ shiki: AssistantShikiRuntimeConfig | undefined,
858
+ ): Promise<string> {
809
859
  const normalizedMarkdown = markdown.replaceAll("\r\n", "\n");
810
- const cacheKey = `${linkTarget}\0${normalizedMarkdown}`;
860
+ const cacheKey = buildMarkdownCacheKey(normalizedMarkdown, linkTarget, shiki);
811
861
  const cached = markdownHtmlCache.get(cacheKey);
812
862
  if (cached !== undefined) {
813
- return cached;
863
+ return Promise.resolve(cached);
864
+ }
865
+
866
+ const cachedPromise = markdownHtmlPromiseCache.get(cacheKey);
867
+ if (cachedPromise) {
868
+ return cachedPromise;
814
869
  }
815
870
 
816
- let html = "";
871
+ const renderPromise = (async () => {
872
+ let html = "";
817
873
 
818
- try {
819
- const processor = unified()
820
- .use(remarkParse)
821
- .use(remarkGfm)
822
- .use(remarkRehype, { allowDangerousHtml: false })
823
- .use(rehypeRebaseInternalLinks);
824
-
825
- if (linkTarget === "blank") {
826
- processor.use(rehypeOpenLinksInNewTab);
874
+ try {
875
+ const processor = unified()
876
+ .use(remarkParse)
877
+ .use(remarkGfm)
878
+ .use(remarkRehype, { allowDangerousHtml: false })
879
+ .use(rehypeRebaseInternalLinks);
880
+
881
+ if (linkTarget === "blank") {
882
+ processor.use(rehypeOpenLinksInNewTab);
883
+ }
884
+
885
+ html = String(
886
+ processor.use(rehypeStringify).processSync(normalizedMarkdown),
887
+ );
888
+ } catch {
889
+ html = escapeHtml(normalizedMarkdown).replaceAll("\n", "<br/>");
827
890
  }
828
891
 
829
- html = String(
830
- processor.use(rehypeStringify).processSync(normalizedMarkdown),
831
- );
832
- } catch {
833
- html = escapeHtml(normalizedMarkdown).replaceAll("\n", "<br/>");
834
- }
892
+ html = await highlightCodeBlocksInHtml(html, shiki);
835
893
 
836
- html = highlightCodeBlocksInHtml(html);
894
+ markdownHtmlCache.set(cacheKey, html);
895
+ if (markdownHtmlCache.size > MARKDOWN_HTML_CACHE_LIMIT) {
896
+ const oldestKey = markdownHtmlCache.keys().next().value;
897
+ if (oldestKey) {
898
+ markdownHtmlCache.delete(oldestKey);
899
+ }
900
+ }
837
901
 
838
- markdownHtmlCache.set(cacheKey, html);
839
- if (markdownHtmlCache.size > MARKDOWN_HTML_CACHE_LIMIT) {
840
- const oldestKey = markdownHtmlCache.keys().next().value;
841
- if (oldestKey) {
842
- markdownHtmlCache.delete(oldestKey);
902
+ markdownHtmlPromiseCache.delete(cacheKey);
903
+ return html;
904
+ })();
905
+
906
+ markdownHtmlPromiseCache.set(cacheKey, renderPromise);
907
+ return renderPromise;
908
+ }
909
+
910
+ function getCachedMarkdownHtml(
911
+ markdown: string,
912
+ linkTarget: AssistantLinkTarget,
913
+ shiki: AssistantShikiRuntimeConfig | undefined,
914
+ ): string | undefined {
915
+ return markdownHtmlCache.get(
916
+ buildMarkdownCacheKey(markdown.replaceAll("\r\n", "\n"), linkTarget, shiki),
917
+ );
918
+ }
919
+
920
+ function AssistantMarkdownBlock({
921
+ className,
922
+ linkTarget,
923
+ markdown,
924
+ onClick,
925
+ onRendered,
926
+ shiki,
927
+ }: {
928
+ className: string;
929
+ linkTarget: AssistantLinkTarget;
930
+ markdown: string;
931
+ onClick: (event: JSX.TargetedMouseEvent<HTMLDivElement>) => void;
932
+ onRendered?: () => void;
933
+ shiki?: AssistantShikiRuntimeConfig;
934
+ }) {
935
+ const onRenderedRef = useRef(onRendered);
936
+ const [html, setHtml] = useState(
937
+ () => getCachedMarkdownHtml(markdown, linkTarget, shiki) ?? "",
938
+ );
939
+
940
+ useEffect(() => {
941
+ onRenderedRef.current = onRendered;
942
+ }, [onRendered]);
943
+
944
+ useEffect(() => {
945
+ let isCancelled = false;
946
+ const cached = getCachedMarkdownHtml(markdown, linkTarget, shiki);
947
+ if (cached !== undefined) {
948
+ setHtml(cached);
949
+ onRenderedRef.current?.();
950
+ return () => {
951
+ isCancelled = true;
952
+ };
843
953
  }
954
+
955
+ void renderMarkdownToHtml(markdown, linkTarget, shiki)
956
+ .then((nextHtml) => {
957
+ if (isCancelled) return;
958
+ setHtml((previousHtml) =>
959
+ previousHtml === nextHtml ? previousHtml : nextHtml,
960
+ );
961
+ onRenderedRef.current?.();
962
+ })
963
+ .catch((error) => {
964
+ if (isCancelled) return;
965
+ console.error("Assistant embed markdown rendering failed", error);
966
+ setHtml(escapeHtml(markdown).replaceAll("\n", "<br/>"));
967
+ onRenderedRef.current?.();
968
+ });
969
+
970
+ return () => {
971
+ isCancelled = true;
972
+ };
973
+ }, [
974
+ linkTarget,
975
+ markdown,
976
+ shiki?.assetBaseUrl,
977
+ shiki?.syntaxThemes.dark,
978
+ shiki?.syntaxThemes.light,
979
+ ]);
980
+
981
+ return (
982
+ <div
983
+ className={className}
984
+ onClick={onClick}
985
+ dangerouslySetInnerHTML={{
986
+ __html: html,
987
+ }}
988
+ />
989
+ );
990
+ }
991
+
992
+ function AssistantSourcesBlock({
993
+ linkTarget,
994
+ onClick,
995
+ sources,
996
+ }: {
997
+ linkTarget: AssistantLinkTarget;
998
+ onClick: (event: JSX.TargetedMouseEvent<HTMLDivElement>) => void;
999
+ sources: AssistantMessageSource[] | undefined;
1000
+ }) {
1001
+ const visibleSources = normalizeAssistantMessageSources(sources);
1002
+ if (visibleSources.length === 0) {
1003
+ return null;
844
1004
  }
845
1005
 
846
- return html;
1006
+ return (
1007
+ <div
1008
+ className="ask-ai-sources ask-ai-markdown prose-rules mt-3 max-w-full min-w-0 wrap-break-word text-[15px]!"
1009
+ onClick={onClick}
1010
+ >
1011
+ <p>Sources:</p>
1012
+ <ul>
1013
+ {visibleSources.map((source) => (
1014
+ <li key={`${source.href}\0${source.label}`}>
1015
+ <a
1016
+ href={source.href}
1017
+ target={linkTarget === "blank" ? "_blank" : undefined}
1018
+ rel={linkTarget === "blank" ? "noopener noreferrer" : undefined}
1019
+ >
1020
+ {source.label}
1021
+ </a>
1022
+ </li>
1023
+ ))}
1024
+ </ul>
1025
+ </div>
1026
+ );
847
1027
  }
848
1028
 
849
1029
  function extractErrorMessage(rawBody: string): string {
@@ -944,11 +1124,18 @@ export default function AssistantEmbedPanel({
944
1124
  allowApiPathQueryOverride = true,
945
1125
  openSignal = 0,
946
1126
  panelSize,
1127
+ mobileBreakpoint = DEFAULT_ASSISTANT_MOBILE_BREAKPOINT,
1128
+ panelFullscreenHeightBreakpoint = DEFAULT_ASSISTANT_PANEL_FULLSCREEN_HEIGHT_BREAKPOINT,
947
1129
  onRequestOpen,
948
1130
  onRequestClose,
949
1131
  onRequestPanelSizeToggle,
950
1132
  onCurrentLinkNavigate,
1133
+ shiki,
951
1134
  }: AssistantEmbedPanelProps) {
1135
+ const panelFullscreenMediaQuery = getPanelFullscreenMediaQuery(
1136
+ mobileBreakpoint,
1137
+ panelFullscreenHeightBreakpoint,
1138
+ );
952
1139
  const [initialPanelState] = useState<PersistedPanelState>(() =>
953
1140
  canSendChatRequest
954
1141
  ? readPersistedPanelState()
@@ -969,7 +1156,17 @@ export default function AssistantEmbedPanel({
969
1156
  const [localPanelSize, setLocalPanelSize] = useState<AssistantPanelSize>(
970
1157
  panelSize ?? initialPanelState.panelSize,
971
1158
  );
972
- const [isShellFullscreen, setIsShellFullscreen] = useState(false);
1159
+ const [isShellFullscreen, setIsShellFullscreen] = useState(() => {
1160
+ if (
1161
+ panelSurface !== "inline" ||
1162
+ typeof window === "undefined" ||
1163
+ typeof window.matchMedia !== "function"
1164
+ ) {
1165
+ return false;
1166
+ }
1167
+
1168
+ return window.matchMedia(panelFullscreenMediaQuery).matches;
1169
+ });
973
1170
  const activeRequestAbortRef = useRef<AbortController | null>(null);
974
1171
  const scrollViewportRef = useRef<HTMLDivElement | null>(null);
975
1172
  const savedScrollTopRef = useRef(initialPanelState.scrollTop);
@@ -986,6 +1183,7 @@ export default function AssistantEmbedPanel({
986
1183
  const panelSizeRef = useRef<AssistantPanelSize>(
987
1184
  panelSize ?? initialPanelState.panelSize,
988
1185
  );
1186
+ const hasPrewarmedShikiRef = useRef(false);
989
1187
  const skipNextMessagesPersistRef = useRef(false);
990
1188
  const inputRef = useRef<HTMLTextAreaElement | null>(null);
991
1189
  const resolvedApiPathRef = useRef(
@@ -1541,6 +1739,27 @@ export default function AssistantEmbedPanel({
1541
1739
  notifyPanelSizeChange(resolvedPanelSize);
1542
1740
  }, [resolvedPanelSize]);
1543
1741
 
1742
+ useEffect(() => {
1743
+ if (
1744
+ panelSurface !== "inline" ||
1745
+ typeof window === "undefined" ||
1746
+ typeof window.matchMedia !== "function"
1747
+ ) {
1748
+ return;
1749
+ }
1750
+
1751
+ const mediaQuery = window.matchMedia(panelFullscreenMediaQuery);
1752
+ const updateShellLayout = () => {
1753
+ setIsShellFullscreen(mediaQuery.matches);
1754
+ };
1755
+
1756
+ updateShellLayout();
1757
+ mediaQuery.addEventListener("change", updateShellLayout);
1758
+ return () => {
1759
+ mediaQuery.removeEventListener("change", updateShellLayout);
1760
+ };
1761
+ }, [panelFullscreenMediaQuery, panelSurface]);
1762
+
1544
1763
  useEffect(() => {
1545
1764
  if (typeof window === "undefined") {
1546
1765
  return;
@@ -1654,6 +1873,13 @@ export default function AssistantEmbedPanel({
1654
1873
  setMessages(nextConversation);
1655
1874
  queueThreadScrollToBottom(nextConversation);
1656
1875
 
1876
+ if (shiki && !hasPrewarmedShikiRef.current) {
1877
+ hasPrewarmedShikiRef.current = true;
1878
+ void warmAssistantShikiRuntime(shiki).catch((error) => {
1879
+ console.warn("Assistant embed Shiki runtime prewarm failed", error);
1880
+ });
1881
+ }
1882
+
1657
1883
  activeRequestAbortRef.current?.abort();
1658
1884
  const abortController = new AbortController();
1659
1885
  activeRequestAbortRef.current = abortController;
@@ -1771,6 +1997,49 @@ export default function AssistantEmbedPanel({
1771
1997
  : message,
1772
1998
  );
1773
1999
  });
2000
+ continue;
2001
+ }
2002
+
2003
+ if (parsed.type === "sources") {
2004
+ const nextSources = normalizeAssistantMessageSources(
2005
+ parsed.sources,
2006
+ );
2007
+ if (nextSources.length === 0) {
2008
+ continue;
2009
+ }
2010
+
2011
+ if (!hasReceivedFirstTextDelta) {
2012
+ hasReceivedFirstTextDelta = true;
2013
+ setSharedAwaitingFirstToken(false);
2014
+ }
2015
+
2016
+ setMessages((previous) => {
2017
+ const existingAssistantMessage = previous.find(
2018
+ (message) => message.id === assistantId,
2019
+ );
2020
+
2021
+ const nextMessages = existingAssistantMessage
2022
+ ? previous.map((message) =>
2023
+ message.id === assistantId
2024
+ ? {
2025
+ ...message,
2026
+ sources: nextSources,
2027
+ }
2028
+ : message,
2029
+ )
2030
+ : [
2031
+ ...previous,
2032
+ {
2033
+ id: assistantId,
2034
+ role: "assistant" as const,
2035
+ content: "",
2036
+ sources: nextSources,
2037
+ },
2038
+ ];
2039
+
2040
+ queueThreadScrollToBottom(nextMessages);
2041
+ return nextMessages;
2042
+ });
1774
2043
  }
1775
2044
  }
1776
2045
  }
@@ -1949,6 +2218,12 @@ export default function AssistantEmbedPanel({
1949
2218
  });
1950
2219
  };
1951
2220
 
2221
+ const handleMarkdownRendered = () => {
2222
+ if (isBusyRef.current) {
2223
+ queueThreadScrollToBottom(messagesRef.current);
2224
+ }
2225
+ };
2226
+
1952
2227
  const panelClassName = [
1953
2228
  "relative flex min-h-0 flex-col overflow-hidden text-neutral-900 shadow-2xl dark:text-neutral-50",
1954
2229
  panelSurface === "inline"
@@ -2041,7 +2316,7 @@ export default function AssistantEmbedPanel({
2041
2316
  {messages.length === 0 ? (
2042
2317
  <div
2043
2318
  key={emptyStateAnimationKey}
2044
- className="flex min-h-full flex-col items-center justify-center px-1. pb-28. pt-8. text-center"
2319
+ className="flex min-h-full flex-col items-center justify-center py-8 text-center"
2045
2320
  >
2046
2321
  <AssistantPanelIcon
2047
2322
  color={launcherIconColor}
@@ -2069,7 +2344,7 @@ export default function AssistantEmbedPanel({
2069
2344
  <button
2070
2345
  key={question}
2071
2346
  type="button"
2072
- className="assistant-empty-state-item w-fit rounded-full border-neutral-900/8 bg-white/90 px-3.5 py-2 text-left text-[13px] leading-5 text-neutral-700 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.04),0px_0px_0px_1px_rgba(0,0,0,0.06)_inset,0px_-1px_0px_0px_rgba(0,0,0,0.06)_inset] transition hover:bg-white dark:bg-white/4 dark:text-neutral-200 dark:hover:bg-white/8 cursor-pointer dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.04),inset_0_1px_0_0_rgba(255,255,255,0.04),inset_0_0_0_1px_rgba(0,0,0,0.06),inset_0_-1px_0_0_rgba(0,0,0,0.06),inset_0_0_0_1px_rgba(196,196,196,0.07)]"
2347
+ className="assistant-empty-state-item w-fit rounded-full border-neutral-900/8 bg-white/90 px-3.5 py-2 text-center text-[13px] leading-5 text-neutral-700 shadow-[0px_1px_3px_0px_rgba(0,0,0,0.04),0px_0px_0px_1px_rgba(0,0,0,0.06)_inset,0px_-1px_0px_0px_rgba(0,0,0,0.06)_inset] transition hover:bg-white dark:bg-white/4 dark:text-neutral-200 dark:hover:bg-white/8 cursor-pointer dark:shadow-[0_1px_3px_0_rgba(0,0,0,0.04),inset_0_1px_0_0_rgba(255,255,255,0.04),inset_0_0_0_1px_rgba(0,0,0,0.06),inset_0_-1px_0_0_rgba(0,0,0,0.06),inset_0_0_0_1px_rgba(196,196,196,0.07)]"
2073
2348
  style={
2074
2349
  {
2075
2350
  "--assistant-empty-state-delay": `${400 + index * 100}ms`,
@@ -2091,7 +2366,8 @@ export default function AssistantEmbedPanel({
2091
2366
  {messages.map((message) => {
2092
2367
  if (
2093
2368
  message.role === "assistant" &&
2094
- message.content.length === 0
2369
+ message.content.length === 0 &&
2370
+ normalizeAssistantMessageSources(message.sources).length === 0
2095
2371
  ) {
2096
2372
  return null;
2097
2373
  }
@@ -2101,18 +2377,26 @@ export default function AssistantEmbedPanel({
2101
2377
  key={message.id}
2102
2378
  className={isUser ? "text-right" : "text-left"}
2103
2379
  >
2104
- <div
2380
+ <AssistantMarkdownBlock
2105
2381
  className={[
2106
2382
  "ask-ai-markdown prose-rules text-[15px]! max-w-full min-w-0 wrap-break-word prose-code:text-neutral-700 dark:prose-code:text-neutral-200 prose-pre:shadow-xs prose-pre:text-neutral-700! dark:prose-pre:text-neutral-100! prose-pre:border prose-pre:border-neutral-200 dark:prose-pre:border-neutral-800 prose-pre:rounded-xl!",
2107
2383
  isUser
2108
2384
  ? "inline-block ml-2 px-3 py-1.5 rounded-2xl rounded-br-sm bg-neutral-900/5 text-neutral-700/85 dark:bg-neutral-800 dark:text-neutral-100 *:text-left"
2109
2385
  : "block w-full bg-transparent text-neutral-900 dark:text-neutral-100",
2110
2386
  ].join(" ")}
2387
+ linkTarget={linkTarget}
2388
+ markdown={message.content}
2111
2389
  onClick={handleRenderedMarkdownClick}
2112
- dangerouslySetInnerHTML={{
2113
- __html: renderMarkdownToHtml(message.content, linkTarget),
2114
- }}
2390
+ onRendered={handleMarkdownRendered}
2391
+ shiki={shiki}
2115
2392
  />
2393
+ {!isUser ? (
2394
+ <AssistantSourcesBlock
2395
+ linkTarget={linkTarget}
2396
+ onClick={handleRenderedMarkdownClick}
2397
+ sources={message.sources}
2398
+ />
2399
+ ) : null}
2116
2400
  </div>
2117
2401
  );
2118
2402
  })}
@@ -2152,7 +2436,10 @@ export default function AssistantEmbedPanel({
2152
2436
  }}
2153
2437
  onKeyDown={handleChatInputKeyDown}
2154
2438
  placeholder="Ask a question..."
2155
- className="text-sm my-auto min-w-0 flex-1 bg-transparent pl-4 py-2.5 text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-400 leading-5 resize-none [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
2439
+ className={[
2440
+ "assistant-embed-input my-auto min-w-0 flex-1 bg-transparent pl-4 py-2.5 text-neutral-900 placeholder:text-neutral-400 focus:outline-none dark:text-white dark:placeholder:text-neutral-400 leading-5 resize-none [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
2441
+ isShellFullscreen ? "text-base" : "text-sm",
2442
+ ].join(" ")}
2156
2443
  disabled={isBusy}
2157
2444
  rows={1}
2158
2445
  />
@@ -2261,7 +2548,7 @@ export default function AssistantEmbedPanel({
2261
2548
  max-width: 100%;
2262
2549
  min-width: 0;
2263
2550
  box-sizing: border-box;
2264
- padding-top: 2.25rem;
2551
+ padding: 2.25rem 0 0 !important;
2265
2552
  background: var(--color-neutral-50) !important;
2266
2553
  overflow-x: hidden;
2267
2554
  overflow-y: hidden;
@@ -2326,10 +2613,6 @@ export default function AssistantEmbedPanel({
2326
2613
  justify-content: center;
2327
2614
  width: 1.75rem;
2328
2615
  height: 1.75rem;
2329
- border: 1px solid
2330
- color-mix(in oklab, var(--color-neutral-200) 80%, transparent);
2331
- background: color-mix(in oklab, #fff 80%, transparent);
2332
- backdrop-filter: blur(4px);
2333
2616
  color: color-mix(
2334
2617
  in oklab,
2335
2618
  var(--color-neutral-500) 80%,
@@ -2344,22 +2627,14 @@ export default function AssistantEmbedPanel({
2344
2627
  }
2345
2628
 
2346
2629
  .dark .ask-ai-markdown .ask-ai-copy-code-button {
2347
- border-color: color-mix(
2348
- in oklab,
2349
- var(--color-neutral-700) 50%,
2350
- transparent
2351
- );
2352
- background: var(--rd-code-surface);
2353
2630
  color: var(--color-neutral-400);
2354
2631
  }
2355
2632
 
2356
2633
  .ask-ai-markdown .ask-ai-copy-code-button:hover {
2357
- background: var(--color-neutral-50);
2358
2634
  color: var(--color-neutral-600);
2359
2635
  }
2360
2636
 
2361
2637
  .dark .ask-ai-markdown .ask-ai-copy-code-button:hover {
2362
- background: var(--color-neutral-800);
2363
2638
  color: var(--color-neutral-200);
2364
2639
  }
2365
2640
 
@@ -2437,6 +2712,7 @@ export default function AssistantEmbedPanel({
2437
2712
  display: block;
2438
2713
  width: 100%;
2439
2714
  min-width: 100%;
2715
+ padding-block: 0.625rem;
2440
2716
  background: transparent !important;
2441
2717
  white-space: inherit;
2442
2718
  word-break: normal;
@@ -2482,164 +2758,6 @@ export default function AssistantEmbedPanel({
2482
2758
  background: var(--color-neutral-800);
2483
2759
  }
2484
2760
 
2485
- .dark .ask-ai-markdown .token.comment,
2486
- .dark .ask-ai-markdown .token.prolog,
2487
- .dark .ask-ai-markdown .token.doctype,
2488
- .dark .ask-ai-markdown .token.cdata {
2489
- color: #7f848e;
2490
- }
2491
-
2492
- .dark .ask-ai-markdown .token.punctuation {
2493
- color: #abb2bf;
2494
- }
2495
-
2496
- .dark .ask-ai-markdown .token.property,
2497
- .dark .ask-ai-markdown .token.tag,
2498
- .dark .ask-ai-markdown .token.boolean,
2499
- .dark .ask-ai-markdown .token.number,
2500
- .dark .ask-ai-markdown .token.constant,
2501
- .dark .ask-ai-markdown .token.symbol,
2502
- .dark .ask-ai-markdown .token.deleted {
2503
- color: #d19a66;
2504
- }
2505
-
2506
- .dark .ask-ai-markdown .token.selector,
2507
- .dark .ask-ai-markdown .token.attr-name,
2508
- .dark .ask-ai-markdown .token.string,
2509
- .dark .ask-ai-markdown .token.char,
2510
- .dark .ask-ai-markdown .token.builtin,
2511
- .dark .ask-ai-markdown .token.inserted {
2512
- color: #98c379;
2513
- }
2514
-
2515
- .dark .ask-ai-markdown .token.operator,
2516
- .dark .ask-ai-markdown .token.entity,
2517
- .dark .ask-ai-markdown .token.url,
2518
- .dark .ask-ai-markdown .language-css .token.string,
2519
- .dark .ask-ai-markdown .style .token.string {
2520
- color: #56b6c2;
2521
- }
2522
-
2523
- .dark .ask-ai-markdown .token.atrule,
2524
- .dark .ask-ai-markdown .token.attr-value,
2525
- .dark .ask-ai-markdown .token.keyword {
2526
- color: #c678dd;
2527
- }
2528
-
2529
- .dark .ask-ai-markdown .token.function,
2530
- .dark .ask-ai-markdown .token.class-name {
2531
- color: #e5c07b;
2532
- }
2533
-
2534
- .dark .ask-ai-markdown .token.regex,
2535
- .dark .ask-ai-markdown .token.important,
2536
- .dark .ask-ai-markdown .token.variable {
2537
- color: #e06c75;
2538
- }
2539
-
2540
- .dark .ask-ai-markdown .token.attr-value > .token.punctuation.attr-equals,
2541
- .dark
2542
- .ask-ai-markdown
2543
- .token.special-attr
2544
- > .token.attr-value
2545
- > .token.value.css {
2546
- color: #abb2bf;
2547
- }
2548
-
2549
- .dark .ask-ai-markdown .language-css .token.selector {
2550
- color: #d19a66;
2551
- }
2552
-
2553
- .dark .ask-ai-markdown .language-css .token.property {
2554
- color: #abb2bf;
2555
- }
2556
-
2557
- .dark .ask-ai-markdown .language-css .token.function,
2558
- .dark .ask-ai-markdown .language-css .token.url > .token.function {
2559
- color: #56b6c2;
2560
- }
2561
-
2562
- .dark .ask-ai-markdown .language-css .token.url > .token.string.url {
2563
- color: #98c379;
2564
- }
2565
-
2566
- .dark .ask-ai-markdown .language-css .token.important,
2567
- .dark .ask-ai-markdown .language-css .token.atrule .token.rule {
2568
- color: #c678dd;
2569
- }
2570
-
2571
- .dark .ask-ai-markdown .language-javascript .token.operator {
2572
- color: #c678dd;
2573
- }
2574
-
2575
- .dark
2576
- .ask-ai-markdown
2577
- .language-javascript
2578
- .token.template-string
2579
- > .token.interpolation
2580
- > .token.interpolation-punctuation.punctuation {
2581
- color: #e06c75;
2582
- }
2583
-
2584
- .dark .ask-ai-markdown .language-json .token.operator {
2585
- color: #abb2bf;
2586
- }
2587
-
2588
- .dark .ask-ai-markdown .language-json .token.null.keyword {
2589
- color: #d19a66;
2590
- }
2591
-
2592
- .dark .ask-ai-markdown .language-markdown .token.url,
2593
- .dark .ask-ai-markdown .language-markdown .token.url > .token.operator,
2594
- .dark
2595
- .ask-ai-markdown
2596
- .language-markdown
2597
- .token.url-reference.url
2598
- > .token.string {
2599
- color: #abb2bf;
2600
- }
2601
-
2602
- .dark .ask-ai-markdown .language-markdown .token.url > .token.content {
2603
- color: #56b6c2;
2604
- }
2605
-
2606
- .dark .ask-ai-markdown .language-markdown .token.url > .token.url,
2607
- .dark .ask-ai-markdown .language-markdown .token.url-reference.url {
2608
- color: #98c379;
2609
- }
2610
-
2611
- .dark .ask-ai-markdown .language-markdown .token.blockquote.punctuation,
2612
- .dark .ask-ai-markdown .language-markdown .token.hr.punctuation {
2613
- color: #7f848e;
2614
- }
2615
-
2616
- .dark .ask-ai-markdown .language-markdown .token.code-snippet {
2617
- color: #98c379;
2618
- }
2619
-
2620
- .dark .ask-ai-markdown .language-markdown .token.bold .token.content {
2621
- color: #e5c07b;
2622
- }
2623
-
2624
- .dark .ask-ai-markdown .language-markdown .token.italic .token.content {
2625
- color: #c678dd;
2626
- }
2627
-
2628
- .dark .ask-ai-markdown .language-markdown .token.strike .token.content,
2629
- .dark
2630
- .ask-ai-markdown
2631
- .language-markdown
2632
- .token.strike
2633
- .token.punctuation,
2634
- .dark .ask-ai-markdown .language-markdown .token.list.punctuation,
2635
- .dark
2636
- .ask-ai-markdown
2637
- .language-markdown
2638
- .token.title.important
2639
- > .token.punctuation {
2640
- color: #e06c75;
2641
- }
2642
-
2643
2761
  .ask-ai-markdown pre > code::-webkit-scrollbar {
2644
2762
  width: 0;
2645
2763
  height: 0;