radiant-docs 0.1.60 → 0.1.62

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 (41) hide show
  1. package/package.json +1 -1
  2. package/template/package-lock.json +10 -4
  3. package/template/package.json +11 -2
  4. package/template/scripts/generate-proxy-allowed-origins.mjs +14 -6
  5. package/template/scripts/publish-shiki-platform-assets.mjs +1151 -0
  6. package/template/src/components/Header.astro +6 -1
  7. package/template/src/components/NavigationTabList.astro +65 -0
  8. package/template/src/components/NavigationTabs.astro +109 -0
  9. package/template/src/components/OpenApiPage.astro +178 -14
  10. package/template/src/components/Sidebar.astro +2 -2
  11. package/template/src/components/SidebarDropdown.astro +105 -44
  12. package/template/src/components/SidebarMenu.astro +3 -0
  13. package/template/src/components/SidebarSegmented.astro +87 -52
  14. package/template/src/components/SidebarTabs.astro +86 -0
  15. package/template/src/components/chat/AssistantDocsWidget.tsx +127 -2
  16. package/template/src/components/chat/AssistantEmbedPanel.tsx +287 -290
  17. package/template/src/components/endpoint/PlaygroundBar.astro +54 -9
  18. package/template/src/components/endpoint/PlaygroundForm.astro +1 -1
  19. package/template/src/components/endpoint/RequestSnippets.astro +6 -1
  20. package/template/src/components/endpoint/ResponseFieldTree.astro +17 -13
  21. package/template/src/components/endpoint/ResponseFields.astro +4 -6
  22. package/template/src/components/endpoint/ResponseSnippets.astro +6 -1
  23. package/template/src/components/sidebar/SidebarEndpointLink.astro +9 -12
  24. package/template/src/components/sidebar/SidebarOpenApi.astro +3 -9
  25. package/template/src/components/ui/Field.astro +18 -15
  26. package/template/src/components/ui/Tag.astro +16 -2
  27. package/template/src/components/user/Accordion.astro +1 -1
  28. package/template/src/components/user/Callout.astro +2 -2
  29. package/template/src/components/user/CodeBlock.astro +58 -7
  30. package/template/src/components/user/CodeGroup.astro +52 -1
  31. package/template/src/components/user/Column.astro +1 -1
  32. package/template/src/components/user/Step.astro +1 -1
  33. package/template/src/components/user/Tabs.astro +1 -1
  34. package/template/src/generated/shiki-platform-assets.json +24 -0
  35. package/template/src/layouts/Layout.astro +111 -8
  36. package/template/src/lib/assistant-panel-config.ts +59 -0
  37. package/template/src/lib/assistant-shiki-client.ts +506 -0
  38. package/template/src/lib/mdx/remark-resolve-internal-links.ts +334 -17
  39. package/template/src/lib/routes.ts +66 -24
  40. package/template/src/lib/utils.ts +11 -0
  41. package/template/src/styles/global.css +12 -0
@@ -1,34 +1,17 @@
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
+ type AssistantShikiRuntimeConfig,
14
+ } from "../../lib/assistant-shiki-client";
32
15
 
33
16
  type AssistantLinkTarget = "current" | "blank";
34
17
  export type AssistantPanelSize = "default" | "expanded";
@@ -59,10 +42,13 @@ type AssistantEmbedPanelProps = {
59
42
  allowApiPathQueryOverride?: boolean;
60
43
  openSignal?: number;
61
44
  panelSize?: AssistantPanelSize;
45
+ mobileBreakpoint?: string;
46
+ panelFullscreenHeightBreakpoint?: string;
62
47
  onRequestOpen?: () => void;
63
48
  onRequestClose?: () => void;
64
49
  onRequestPanelSizeToggle?: (size: AssistantPanelSize) => void;
65
50
  onCurrentLinkNavigate?: (href: string, sourceElement?: Element) => void;
51
+ shiki?: AssistantShikiRuntimeConfig;
66
52
  };
67
53
 
68
54
  type AssistantColorByMode = {
@@ -120,41 +106,50 @@ const HANDOFF_ACK_TYPE = "assistant-handoff:ack";
120
106
  const IN_FLIGHT_STALE_MS = 10000;
121
107
  const IN_FLIGHT_HEARTBEAT_MS = 3000;
122
108
  const MARKDOWN_HTML_CACHE_LIMIT = 300;
109
+ const DEFAULT_ASSISTANT_MOBILE_BREAKPOINT = "640px";
110
+ const DEFAULT_ASSISTANT_PANEL_FULLSCREEN_HEIGHT_BREAKPOINT = "560px";
123
111
  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> = {
112
+ const markdownHtmlPromiseCache = new Map<string, Promise<string>>();
113
+ const ASSISTANT_LANGUAGE_LABEL: Record<string, string> = {
114
+ bash: "Bash",
115
+ css: "CSS",
116
+ diff: "Diff",
117
+ go: "Go",
118
+ html: "HTML",
119
+ java: "Java",
136
120
  javascript: "JavaScript",
137
- typescript: "TypeScript",
138
121
  jsx: "JSX",
139
- tsx: "TSX",
140
122
  json: "JSON",
141
123
  markdown: "Markdown",
142
- bash: "Bash",
143
- shell: "Shell",
124
+ mdx: "MDX",
125
+ php: "PHP",
126
+ plaintext: "Text",
144
127
  python: "Python",
145
- yaml: "YAML",
146
- sql: "SQL",
128
+ ruby: "Ruby",
147
129
  rust: "Rust",
148
- go: "Go",
149
- java: "Java",
150
- css: "CSS",
151
- html: "HTML",
152
- diff: "Diff",
153
- mdx: "MDX",
130
+ shell: "Shell",
131
+ sql: "SQL",
132
+ tsx: "TSX",
133
+ typescript: "TypeScript",
134
+ yaml: "YAML",
154
135
  };
155
136
 
156
137
  const EXTERNAL_PROTOCOL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*:/;
157
138
 
139
+ function buildMarkdownCacheKey(
140
+ normalizedMarkdown: string,
141
+ linkTarget: AssistantLinkTarget,
142
+ shiki: AssistantShikiRuntimeConfig | undefined,
143
+ ): string {
144
+ return [
145
+ linkTarget,
146
+ shiki?.assetBaseUrl ?? "",
147
+ shiki?.syntaxThemes.light ?? "",
148
+ shiki?.syntaxThemes.dark ?? "",
149
+ normalizedMarkdown,
150
+ ].join("\0");
151
+ }
152
+
158
153
  function normalizeRelTokens(value: unknown): string[] {
159
154
  if (Array.isArray(value)) {
160
155
  return value
@@ -395,6 +390,13 @@ function normalizePanelSize(value: unknown): AssistantPanelSize {
395
390
  return value === "expanded" ? "expanded" : "default";
396
391
  }
397
392
 
393
+ function getPanelFullscreenMediaQuery(
394
+ mobileBreakpoint: string,
395
+ panelFullscreenHeightBreakpoint: string,
396
+ ): string {
397
+ return `(max-width: ${mobileBreakpoint}), (max-height: ${panelFullscreenHeightBreakpoint})`;
398
+ }
399
+
398
400
  function normalizePersistedPanelState(rawState: unknown): PersistedPanelState {
399
401
  if (!rawState || typeof rawState !== "object") {
400
402
  return createEmptyPersistedPanelState();
@@ -574,32 +576,20 @@ function escapeHtml(value: string): string {
574
576
  .replaceAll("'", "&#39;");
575
577
  }
576
578
 
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 {
579
+ function resolveAssistantLanguageLabel(language: string): string {
583
580
  const normalized = language.trim().toLowerCase();
584
581
  if (!normalized) {
585
582
  return "";
586
583
  }
587
584
 
588
585
  return (
589
- PRISM_LANGUAGE_LABEL[normalized] ??
586
+ ASSISTANT_LANGUAGE_LABEL[normalized] ??
590
587
  normalized
591
588
  .replace(/[-_]+/g, " ")
592
589
  .replace(/\b\w/g, (char) => char.toUpperCase())
593
590
  );
594
591
  }
595
592
 
596
- function resolvePrismGrammar(language: string) {
597
- return (
598
- Prism.languages[language] ??
599
- Prism.languages[PRISM_LANGUAGE_ALIAS[language] ?? ""]
600
- );
601
- }
602
-
603
593
  function ensureCodeBlockCopyButton(preElement: HTMLPreElement): void {
604
594
  const existingButton = Array.from(preElement.children).find(
605
595
  (child) =>
@@ -697,12 +687,23 @@ function unwrapEscapedFenceCodeBlock(rawCode: string): {
697
687
  }
698
688
 
699
689
  return {
700
- language: resolvePrismLanguage(languageToken),
690
+ language: normalizeAssistantCodeLanguage(languageToken),
701
691
  code: lines.slice(1, -1).join("\n"),
702
692
  };
703
693
  }
704
694
 
705
- function highlightCodeBlocksInHtml(html: string): string {
695
+ function stripSerializedCodeBlockTrailingNewline(rawCode: string): string {
696
+ if (rawCode.endsWith("\n")) {
697
+ return rawCode.slice(0, -1);
698
+ }
699
+
700
+ return rawCode;
701
+ }
702
+
703
+ async function highlightCodeBlocksInHtml(
704
+ html: string,
705
+ shiki: AssistantShikiRuntimeConfig | undefined,
706
+ ): Promise<string> {
706
707
  if (typeof document === "undefined" || !html.trim()) {
707
708
  return html;
708
709
  }
@@ -711,8 +712,8 @@ function highlightCodeBlocksInHtml(html: string): string {
711
712
  const container = document.createElement("div");
712
713
  container.innerHTML = html;
713
714
 
714
- const codeNodes = container.querySelectorAll("pre > code");
715
- codeNodes.forEach((codeNode) => {
715
+ const codeNodes = Array.from(container.querySelectorAll("pre > code"));
716
+ for (const codeNode of codeNodes) {
716
717
  const getLanguageClassName = (element: Element | null) =>
717
718
  Array.from(element?.classList ?? []).find(
718
719
  (className) =>
@@ -741,13 +742,14 @@ function highlightCodeBlocksInHtml(html: string): string {
741
742
  }
742
743
  });
743
744
  preParent.classList.add(`language-${language}`);
744
- const languageLabel = resolvePrismLanguageLabel(language);
745
+ const languageLabel = resolveAssistantLanguageLabel(language);
745
746
  if (languageLabel) {
746
747
  preParent.setAttribute("data-language", languageLabel);
747
748
  } else {
748
749
  preParent.removeAttribute("data-language");
749
750
  }
750
751
 
752
+ codeNode.setAttribute("data-rd-code-theme", "");
751
753
  ensureCodeBlockCopyButton(preParent);
752
754
  }
753
755
  };
@@ -759,7 +761,9 @@ function highlightCodeBlocksInHtml(html: string): string {
759
761
  preLanguageClassName?.replace(/^(language-|lang-)/, "") ??
760
762
  "";
761
763
 
762
- let rawCode = codeNode.textContent ?? "";
764
+ let rawCode = stripSerializedCodeBlockTrailingNewline(
765
+ codeNode.textContent ?? "",
766
+ );
763
767
  if (!rawLanguage) {
764
768
  const unwrappedFence = unwrapEscapedFenceCodeBlock(rawCode);
765
769
  if (unwrappedFence) {
@@ -769,28 +773,37 @@ function highlightCodeBlocksInHtml(html: string): string {
769
773
  }
770
774
 
771
775
  if (!rawLanguage) {
772
- return;
776
+ const preParent = codeNode.parentElement;
777
+ if (preParent instanceof HTMLPreElement) {
778
+ ensureCodeBlockCopyButton(preParent);
779
+ }
780
+ continue;
773
781
  }
774
782
 
775
- const resolvedLanguage = resolvePrismLanguage(rawLanguage);
776
- const grammar =
777
- resolvePrismGrammar(resolvedLanguage) ??
778
- resolvePrismGrammar(rawLanguage);
779
- if (!grammar) {
780
- return;
783
+ const resolvedLanguage = normalizeAssistantCodeLanguage(rawLanguage);
784
+ setLanguageClass(resolvedLanguage);
785
+
786
+ if (!shiki) {
787
+ codeNode.textContent = rawCode;
788
+ continue;
781
789
  }
782
790
 
783
- setLanguageClass(resolvedLanguage);
784
791
  try {
785
- const highlighted = Prism.highlight(rawCode, grammar, resolvedLanguage);
786
- codeNode.innerHTML = highlighted;
792
+ const highlighted = await highlightAssistantCodeToHtml({
793
+ code: rawCode,
794
+ config: shiki,
795
+ language: resolvedLanguage,
796
+ });
797
+ setLanguageClass(highlighted.language);
798
+ codeNode.innerHTML = highlighted.html;
787
799
  } catch (error) {
788
800
  console.error("Assistant embed code highlighting failed", {
789
801
  language: resolvedLanguage,
790
802
  error,
791
803
  });
804
+ codeNode.textContent = rawCode;
792
805
  }
793
- });
806
+ }
794
807
 
795
808
  return container.innerHTML;
796
809
  } catch (error) {
@@ -805,45 +818,139 @@ function highlightCodeBlocksInHtml(html: string): string {
805
818
  function renderMarkdownToHtml(
806
819
  markdown: string,
807
820
  linkTarget: AssistantLinkTarget,
808
- ): string {
821
+ shiki: AssistantShikiRuntimeConfig | undefined,
822
+ ): Promise<string> {
809
823
  const normalizedMarkdown = markdown.replaceAll("\r\n", "\n");
810
- const cacheKey = `${linkTarget}\0${normalizedMarkdown}`;
824
+ const cacheKey = buildMarkdownCacheKey(normalizedMarkdown, linkTarget, shiki);
811
825
  const cached = markdownHtmlCache.get(cacheKey);
812
826
  if (cached !== undefined) {
813
- return cached;
827
+ return Promise.resolve(cached);
814
828
  }
815
829
 
816
- let html = "";
830
+ const cachedPromise = markdownHtmlPromiseCache.get(cacheKey);
831
+ if (cachedPromise) {
832
+ return cachedPromise;
833
+ }
817
834
 
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);
835
+ const renderPromise = (async () => {
836
+ let html = "";
837
+
838
+ try {
839
+ const processor = unified()
840
+ .use(remarkParse)
841
+ .use(remarkGfm)
842
+ .use(remarkRehype, { allowDangerousHtml: false })
843
+ .use(rehypeRebaseInternalLinks);
844
+
845
+ if (linkTarget === "blank") {
846
+ processor.use(rehypeOpenLinksInNewTab);
847
+ }
848
+
849
+ html = String(
850
+ processor.use(rehypeStringify).processSync(normalizedMarkdown),
851
+ );
852
+ } catch {
853
+ html = escapeHtml(normalizedMarkdown).replaceAll("\n", "<br/>");
827
854
  }
828
855
 
829
- html = String(
830
- processor.use(rehypeStringify).processSync(normalizedMarkdown),
831
- );
832
- } catch {
833
- html = escapeHtml(normalizedMarkdown).replaceAll("\n", "<br/>");
834
- }
856
+ html = await highlightCodeBlocksInHtml(html, shiki);
835
857
 
836
- html = highlightCodeBlocksInHtml(html);
858
+ markdownHtmlCache.set(cacheKey, html);
859
+ if (markdownHtmlCache.size > MARKDOWN_HTML_CACHE_LIMIT) {
860
+ const oldestKey = markdownHtmlCache.keys().next().value;
861
+ if (oldestKey) {
862
+ markdownHtmlCache.delete(oldestKey);
863
+ }
864
+ }
865
+
866
+ markdownHtmlPromiseCache.delete(cacheKey);
867
+ return html;
868
+ })();
869
+
870
+ markdownHtmlPromiseCache.set(cacheKey, renderPromise);
871
+ return renderPromise;
872
+ }
837
873
 
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);
874
+ function getCachedMarkdownHtml(
875
+ markdown: string,
876
+ linkTarget: AssistantLinkTarget,
877
+ shiki: AssistantShikiRuntimeConfig | undefined,
878
+ ): string | undefined {
879
+ return markdownHtmlCache.get(
880
+ buildMarkdownCacheKey(markdown.replaceAll("\r\n", "\n"), linkTarget, shiki),
881
+ );
882
+ }
883
+
884
+ function AssistantMarkdownBlock({
885
+ className,
886
+ linkTarget,
887
+ markdown,
888
+ onClick,
889
+ onRendered,
890
+ shiki,
891
+ }: {
892
+ className: string;
893
+ linkTarget: AssistantLinkTarget;
894
+ markdown: string;
895
+ onClick: (event: JSX.TargetedMouseEvent<HTMLDivElement>) => void;
896
+ onRendered?: () => void;
897
+ shiki?: AssistantShikiRuntimeConfig;
898
+ }) {
899
+ const onRenderedRef = useRef(onRendered);
900
+ const [html, setHtml] = useState(
901
+ () => getCachedMarkdownHtml(markdown, linkTarget, shiki) ?? "",
902
+ );
903
+
904
+ useEffect(() => {
905
+ onRenderedRef.current = onRendered;
906
+ }, [onRendered]);
907
+
908
+ useEffect(() => {
909
+ let isCancelled = false;
910
+ const cached = getCachedMarkdownHtml(markdown, linkTarget, shiki);
911
+ if (cached !== undefined) {
912
+ setHtml(cached);
913
+ onRenderedRef.current?.();
914
+ return () => {
915
+ isCancelled = true;
916
+ };
843
917
  }
844
- }
845
918
 
846
- return html;
919
+ void renderMarkdownToHtml(markdown, linkTarget, shiki)
920
+ .then((nextHtml) => {
921
+ if (isCancelled) return;
922
+ setHtml((previousHtml) =>
923
+ previousHtml === nextHtml ? previousHtml : nextHtml,
924
+ );
925
+ onRenderedRef.current?.();
926
+ })
927
+ .catch((error) => {
928
+ if (isCancelled) return;
929
+ console.error("Assistant embed markdown rendering failed", error);
930
+ setHtml(escapeHtml(markdown).replaceAll("\n", "<br/>"));
931
+ onRenderedRef.current?.();
932
+ });
933
+
934
+ return () => {
935
+ isCancelled = true;
936
+ };
937
+ }, [
938
+ linkTarget,
939
+ markdown,
940
+ shiki?.assetBaseUrl,
941
+ shiki?.syntaxThemes.dark,
942
+ shiki?.syntaxThemes.light,
943
+ ]);
944
+
945
+ return (
946
+ <div
947
+ className={className}
948
+ onClick={onClick}
949
+ dangerouslySetInnerHTML={{
950
+ __html: html,
951
+ }}
952
+ />
953
+ );
847
954
  }
848
955
 
849
956
  function extractErrorMessage(rawBody: string): string {
@@ -944,11 +1051,18 @@ export default function AssistantEmbedPanel({
944
1051
  allowApiPathQueryOverride = true,
945
1052
  openSignal = 0,
946
1053
  panelSize,
1054
+ mobileBreakpoint = DEFAULT_ASSISTANT_MOBILE_BREAKPOINT,
1055
+ panelFullscreenHeightBreakpoint = DEFAULT_ASSISTANT_PANEL_FULLSCREEN_HEIGHT_BREAKPOINT,
947
1056
  onRequestOpen,
948
1057
  onRequestClose,
949
1058
  onRequestPanelSizeToggle,
950
1059
  onCurrentLinkNavigate,
1060
+ shiki,
951
1061
  }: AssistantEmbedPanelProps) {
1062
+ const panelFullscreenMediaQuery = getPanelFullscreenMediaQuery(
1063
+ mobileBreakpoint,
1064
+ panelFullscreenHeightBreakpoint,
1065
+ );
952
1066
  const [initialPanelState] = useState<PersistedPanelState>(() =>
953
1067
  canSendChatRequest
954
1068
  ? readPersistedPanelState()
@@ -969,7 +1083,17 @@ export default function AssistantEmbedPanel({
969
1083
  const [localPanelSize, setLocalPanelSize] = useState<AssistantPanelSize>(
970
1084
  panelSize ?? initialPanelState.panelSize,
971
1085
  );
972
- const [isShellFullscreen, setIsShellFullscreen] = useState(false);
1086
+ const [isShellFullscreen, setIsShellFullscreen] = useState(() => {
1087
+ if (
1088
+ panelSurface !== "inline" ||
1089
+ typeof window === "undefined" ||
1090
+ typeof window.matchMedia !== "function"
1091
+ ) {
1092
+ return false;
1093
+ }
1094
+
1095
+ return window.matchMedia(panelFullscreenMediaQuery).matches;
1096
+ });
973
1097
  const activeRequestAbortRef = useRef<AbortController | null>(null);
974
1098
  const scrollViewportRef = useRef<HTMLDivElement | null>(null);
975
1099
  const savedScrollTopRef = useRef(initialPanelState.scrollTop);
@@ -1541,6 +1665,27 @@ export default function AssistantEmbedPanel({
1541
1665
  notifyPanelSizeChange(resolvedPanelSize);
1542
1666
  }, [resolvedPanelSize]);
1543
1667
 
1668
+ useEffect(() => {
1669
+ if (
1670
+ panelSurface !== "inline" ||
1671
+ typeof window === "undefined" ||
1672
+ typeof window.matchMedia !== "function"
1673
+ ) {
1674
+ return;
1675
+ }
1676
+
1677
+ const mediaQuery = window.matchMedia(panelFullscreenMediaQuery);
1678
+ const updateShellLayout = () => {
1679
+ setIsShellFullscreen(mediaQuery.matches);
1680
+ };
1681
+
1682
+ updateShellLayout();
1683
+ mediaQuery.addEventListener("change", updateShellLayout);
1684
+ return () => {
1685
+ mediaQuery.removeEventListener("change", updateShellLayout);
1686
+ };
1687
+ }, [panelFullscreenMediaQuery, panelSurface]);
1688
+
1544
1689
  useEffect(() => {
1545
1690
  if (typeof window === "undefined") {
1546
1691
  return;
@@ -1949,6 +2094,12 @@ export default function AssistantEmbedPanel({
1949
2094
  });
1950
2095
  };
1951
2096
 
2097
+ const handleMarkdownRendered = () => {
2098
+ if (isBusyRef.current) {
2099
+ queueThreadScrollToBottom(messagesRef.current);
2100
+ }
2101
+ };
2102
+
1952
2103
  const panelClassName = [
1953
2104
  "relative flex min-h-0 flex-col overflow-hidden text-neutral-900 shadow-2xl dark:text-neutral-50",
1954
2105
  panelSurface === "inline"
@@ -1963,7 +2114,7 @@ export default function AssistantEmbedPanel({
1963
2114
  <AssistantPanelIcon
1964
2115
  color={launcherIconColor}
1965
2116
  imageSrc={launcherIconImageSrc}
1966
- className="assistant-embed-header-icon inline-flex size-9 shrink-0 items-center justify-center rounded-md dark:border-[0.5px] border-neutral-900/7 bg-radial from-neutral-700/5 dark:from-white/7 to-neutral-900/7 dark:to-white/5 to-60% text-neutral-900 dark:border-white/7 dark:text-neutral-50 [&_img]:block [&_img]:size-5.5 [&_img]:object-contain [&_svg]:static [&_svg]:block [&_svg]:size-5 [&_svg]:transform-none [&_svg]:opacity-100"
2117
+ className="assistant-embed-header-icon inline-flex size-9 shrink-0 items-center justify-center rounded-md dark:border-[0.5px] border-neutral-900/7 bg-neutral-700/7 dark:bg-white/6 text-neutral-900 dark:border-white/7 dark:text-neutral-50 [&_img]:block [&_img]:size-5.5 [&_img]:object-contain [&_svg]:static [&_svg]:block [&_svg]:size-5 [&_svg]:transform-none [&_svg]:opacity-100"
1967
2118
  />
1968
2119
  <div className="min-w-0 space-y-px">
1969
2120
  <p className="truncate text-sm font-medium leading-3.5 text-neutral-900 dark:text-neutral-50">
@@ -2041,7 +2192,7 @@ export default function AssistantEmbedPanel({
2041
2192
  {messages.length === 0 ? (
2042
2193
  <div
2043
2194
  key={emptyStateAnimationKey}
2044
- className="flex min-h-full flex-col items-center justify-center px-1. pb-28. pt-8. text-center"
2195
+ className="flex min-h-full flex-col items-center justify-center py-8 text-center"
2045
2196
  >
2046
2197
  <AssistantPanelIcon
2047
2198
  color={launcherIconColor}
@@ -2069,7 +2220,7 @@ export default function AssistantEmbedPanel({
2069
2220
  <button
2070
2221
  key={question}
2071
2222
  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)]"
2223
+ 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
2224
  style={
2074
2225
  {
2075
2226
  "--assistant-empty-state-delay": `${400 + index * 100}ms`,
@@ -2101,17 +2252,18 @@ export default function AssistantEmbedPanel({
2101
2252
  key={message.id}
2102
2253
  className={isUser ? "text-right" : "text-left"}
2103
2254
  >
2104
- <div
2255
+ <AssistantMarkdownBlock
2105
2256
  className={[
2106
2257
  "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
2258
  isUser
2108
2259
  ? "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
2260
  : "block w-full bg-transparent text-neutral-900 dark:text-neutral-100",
2110
2261
  ].join(" ")}
2262
+ linkTarget={linkTarget}
2263
+ markdown={message.content}
2111
2264
  onClick={handleRenderedMarkdownClick}
2112
- dangerouslySetInnerHTML={{
2113
- __html: renderMarkdownToHtml(message.content, linkTarget),
2114
- }}
2265
+ onRendered={handleMarkdownRendered}
2266
+ shiki={shiki}
2115
2267
  />
2116
2268
  </div>
2117
2269
  );
@@ -2152,7 +2304,10 @@ export default function AssistantEmbedPanel({
2152
2304
  }}
2153
2305
  onKeyDown={handleChatInputKeyDown}
2154
2306
  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"
2307
+ className={[
2308
+ "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",
2309
+ isShellFullscreen ? "text-base" : "text-sm",
2310
+ ].join(" ")}
2156
2311
  disabled={isBusy}
2157
2312
  rows={1}
2158
2313
  />
@@ -2198,14 +2353,25 @@ export default function AssistantEmbedPanel({
2198
2353
  </p>
2199
2354
  <button
2200
2355
  type="button"
2201
- className="mt-1 inline-flex items-center gap-1.5 rounded-md border border-neutral-900/8 bg-white px-2.5 py-1.5 text-[13px] font-medium text-neutral-700 shadow-xs transition hover:bg-neutral-50 dark:border-white/10 dark:bg-white/5 dark:text-neutral-200 dark:hover:bg-white/10 cursor-pointer"
2356
+ className="mt-1 inline-flex items-center gap-1.5 rounded-md border border-neutral-900/8 bg-white px-2.5 py-1.5 text-[13px] font-medium text-neutral-700 shadow-xs transition hover:bg-neutral-50/80 dark:border-white/10 dark:bg-white/5 dark:text-neutral-200 dark:hover:bg-white/10 cursor-pointer"
2202
2357
  onClick={handleUnavailableBack}
2203
2358
  >
2204
- <Icon
2205
- icon="lucide:arrow-left"
2206
- className="size-3.5"
2207
- aria-hidden="true"
2208
- />
2359
+ <svg
2360
+ xmlns="http://www.w3.org/2000/svg"
2361
+ width="14px"
2362
+ height="14px"
2363
+ viewBox="0 0 24 24"
2364
+ >
2365
+ <path d="M0 0h24v24H0z" fill="none" />
2366
+ <path
2367
+ fill="none"
2368
+ stroke="currentColor"
2369
+ stroke-linecap="round"
2370
+ stroke-linejoin="round"
2371
+ stroke-width="2"
2372
+ d="m12 19l-7-7l7-7m7 7H5"
2373
+ />
2374
+ </svg>
2209
2375
  Back
2210
2376
  </button>
2211
2377
  </div>
@@ -2250,7 +2416,7 @@ export default function AssistantEmbedPanel({
2250
2416
  max-width: 100%;
2251
2417
  min-width: 0;
2252
2418
  box-sizing: border-box;
2253
- padding-top: 2.25rem;
2419
+ padding: 2.25rem 0 0 !important;
2254
2420
  background: var(--color-neutral-50) !important;
2255
2421
  overflow-x: hidden;
2256
2422
  overflow-y: hidden;
@@ -2315,10 +2481,6 @@ export default function AssistantEmbedPanel({
2315
2481
  justify-content: center;
2316
2482
  width: 1.75rem;
2317
2483
  height: 1.75rem;
2318
- border: 1px solid
2319
- color-mix(in oklab, var(--color-neutral-200) 80%, transparent);
2320
- background: color-mix(in oklab, #fff 80%, transparent);
2321
- backdrop-filter: blur(4px);
2322
2484
  color: color-mix(
2323
2485
  in oklab,
2324
2486
  var(--color-neutral-500) 80%,
@@ -2333,22 +2495,14 @@ export default function AssistantEmbedPanel({
2333
2495
  }
2334
2496
 
2335
2497
  .dark .ask-ai-markdown .ask-ai-copy-code-button {
2336
- border-color: color-mix(
2337
- in oklab,
2338
- var(--color-neutral-700) 50%,
2339
- transparent
2340
- );
2341
- background: var(--rd-code-surface);
2342
2498
  color: var(--color-neutral-400);
2343
2499
  }
2344
2500
 
2345
2501
  .ask-ai-markdown .ask-ai-copy-code-button:hover {
2346
- background: var(--color-neutral-50);
2347
2502
  color: var(--color-neutral-600);
2348
2503
  }
2349
2504
 
2350
2505
  .dark .ask-ai-markdown .ask-ai-copy-code-button:hover {
2351
- background: var(--color-neutral-800);
2352
2506
  color: var(--color-neutral-200);
2353
2507
  }
2354
2508
 
@@ -2426,6 +2580,7 @@ export default function AssistantEmbedPanel({
2426
2580
  display: block;
2427
2581
  width: 100%;
2428
2582
  min-width: 100%;
2583
+ padding-block: 0.625rem;
2429
2584
  background: transparent !important;
2430
2585
  white-space: inherit;
2431
2586
  word-break: normal;
@@ -2471,164 +2626,6 @@ export default function AssistantEmbedPanel({
2471
2626
  background: var(--color-neutral-800);
2472
2627
  }
2473
2628
 
2474
- .dark .ask-ai-markdown .token.comment,
2475
- .dark .ask-ai-markdown .token.prolog,
2476
- .dark .ask-ai-markdown .token.doctype,
2477
- .dark .ask-ai-markdown .token.cdata {
2478
- color: #7f848e;
2479
- }
2480
-
2481
- .dark .ask-ai-markdown .token.punctuation {
2482
- color: #abb2bf;
2483
- }
2484
-
2485
- .dark .ask-ai-markdown .token.property,
2486
- .dark .ask-ai-markdown .token.tag,
2487
- .dark .ask-ai-markdown .token.boolean,
2488
- .dark .ask-ai-markdown .token.number,
2489
- .dark .ask-ai-markdown .token.constant,
2490
- .dark .ask-ai-markdown .token.symbol,
2491
- .dark .ask-ai-markdown .token.deleted {
2492
- color: #d19a66;
2493
- }
2494
-
2495
- .dark .ask-ai-markdown .token.selector,
2496
- .dark .ask-ai-markdown .token.attr-name,
2497
- .dark .ask-ai-markdown .token.string,
2498
- .dark .ask-ai-markdown .token.char,
2499
- .dark .ask-ai-markdown .token.builtin,
2500
- .dark .ask-ai-markdown .token.inserted {
2501
- color: #98c379;
2502
- }
2503
-
2504
- .dark .ask-ai-markdown .token.operator,
2505
- .dark .ask-ai-markdown .token.entity,
2506
- .dark .ask-ai-markdown .token.url,
2507
- .dark .ask-ai-markdown .language-css .token.string,
2508
- .dark .ask-ai-markdown .style .token.string {
2509
- color: #56b6c2;
2510
- }
2511
-
2512
- .dark .ask-ai-markdown .token.atrule,
2513
- .dark .ask-ai-markdown .token.attr-value,
2514
- .dark .ask-ai-markdown .token.keyword {
2515
- color: #c678dd;
2516
- }
2517
-
2518
- .dark .ask-ai-markdown .token.function,
2519
- .dark .ask-ai-markdown .token.class-name {
2520
- color: #e5c07b;
2521
- }
2522
-
2523
- .dark .ask-ai-markdown .token.regex,
2524
- .dark .ask-ai-markdown .token.important,
2525
- .dark .ask-ai-markdown .token.variable {
2526
- color: #e06c75;
2527
- }
2528
-
2529
- .dark .ask-ai-markdown .token.attr-value > .token.punctuation.attr-equals,
2530
- .dark
2531
- .ask-ai-markdown
2532
- .token.special-attr
2533
- > .token.attr-value
2534
- > .token.value.css {
2535
- color: #abb2bf;
2536
- }
2537
-
2538
- .dark .ask-ai-markdown .language-css .token.selector {
2539
- color: #d19a66;
2540
- }
2541
-
2542
- .dark .ask-ai-markdown .language-css .token.property {
2543
- color: #abb2bf;
2544
- }
2545
-
2546
- .dark .ask-ai-markdown .language-css .token.function,
2547
- .dark .ask-ai-markdown .language-css .token.url > .token.function {
2548
- color: #56b6c2;
2549
- }
2550
-
2551
- .dark .ask-ai-markdown .language-css .token.url > .token.string.url {
2552
- color: #98c379;
2553
- }
2554
-
2555
- .dark .ask-ai-markdown .language-css .token.important,
2556
- .dark .ask-ai-markdown .language-css .token.atrule .token.rule {
2557
- color: #c678dd;
2558
- }
2559
-
2560
- .dark .ask-ai-markdown .language-javascript .token.operator {
2561
- color: #c678dd;
2562
- }
2563
-
2564
- .dark
2565
- .ask-ai-markdown
2566
- .language-javascript
2567
- .token.template-string
2568
- > .token.interpolation
2569
- > .token.interpolation-punctuation.punctuation {
2570
- color: #e06c75;
2571
- }
2572
-
2573
- .dark .ask-ai-markdown .language-json .token.operator {
2574
- color: #abb2bf;
2575
- }
2576
-
2577
- .dark .ask-ai-markdown .language-json .token.null.keyword {
2578
- color: #d19a66;
2579
- }
2580
-
2581
- .dark .ask-ai-markdown .language-markdown .token.url,
2582
- .dark .ask-ai-markdown .language-markdown .token.url > .token.operator,
2583
- .dark
2584
- .ask-ai-markdown
2585
- .language-markdown
2586
- .token.url-reference.url
2587
- > .token.string {
2588
- color: #abb2bf;
2589
- }
2590
-
2591
- .dark .ask-ai-markdown .language-markdown .token.url > .token.content {
2592
- color: #56b6c2;
2593
- }
2594
-
2595
- .dark .ask-ai-markdown .language-markdown .token.url > .token.url,
2596
- .dark .ask-ai-markdown .language-markdown .token.url-reference.url {
2597
- color: #98c379;
2598
- }
2599
-
2600
- .dark .ask-ai-markdown .language-markdown .token.blockquote.punctuation,
2601
- .dark .ask-ai-markdown .language-markdown .token.hr.punctuation {
2602
- color: #7f848e;
2603
- }
2604
-
2605
- .dark .ask-ai-markdown .language-markdown .token.code-snippet {
2606
- color: #98c379;
2607
- }
2608
-
2609
- .dark .ask-ai-markdown .language-markdown .token.bold .token.content {
2610
- color: #e5c07b;
2611
- }
2612
-
2613
- .dark .ask-ai-markdown .language-markdown .token.italic .token.content {
2614
- color: #c678dd;
2615
- }
2616
-
2617
- .dark .ask-ai-markdown .language-markdown .token.strike .token.content,
2618
- .dark
2619
- .ask-ai-markdown
2620
- .language-markdown
2621
- .token.strike
2622
- .token.punctuation,
2623
- .dark .ask-ai-markdown .language-markdown .token.list.punctuation,
2624
- .dark
2625
- .ask-ai-markdown
2626
- .language-markdown
2627
- .token.title.important
2628
- > .token.punctuation {
2629
- color: #e06c75;
2630
- }
2631
-
2632
2629
  .ask-ai-markdown pre > code::-webkit-scrollbar {
2633
2630
  width: 0;
2634
2631
  height: 0;