@stainless-api/docs 0.1.0-beta.99 → 1.0.0-beta.141

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 (136) hide show
  1. package/CHANGELOG.md +401 -0
  2. package/ambient.d.ts +6 -0
  3. package/eslint-suppressions.json +22 -6
  4. package/{eslint.config.js → eslint.config.ts} +1 -7
  5. package/package.json +62 -40
  6. package/plugin/buildAlgoliaIndex.ts +6 -12
  7. package/plugin/components/SDKSelect.astro +0 -6
  8. package/plugin/components/SnippetCode.tsx +6 -37
  9. package/plugin/components/search/SearchAlgolia.astro +1 -1
  10. package/plugin/components/search/SearchIsland.tsx +19 -13
  11. package/plugin/generateAPIReferenceLink.ts +0 -40
  12. package/plugin/globalJs/ai-dropdown-options.ts +22 -9
  13. package/plugin/globalJs/code-snippets.ts +5 -5
  14. package/plugin/globalJs/copy.ts +20 -91
  15. package/plugin/globalJs/navigation.ts +13 -13
  16. package/plugin/globalJs/summary-selection-tweak.ts +29 -0
  17. package/plugin/index.ts +107 -163
  18. package/plugin/loadPluginConfig.ts +49 -151
  19. package/plugin/markdown/highlighter.ts +100 -0
  20. package/plugin/markdown/index.ts +39 -0
  21. package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +2 -0
  22. package/plugin/react/Routing.tsx +10 -244
  23. package/plugin/referencePlaceholderUtils.ts +1 -1
  24. package/plugin/replaceSidebarPlaceholderMiddleware.ts +1 -1
  25. package/plugin/routes/Docs.astro +3 -1
  26. package/plugin/routes/Overview.astro +14 -7
  27. package/plugin/routes/llms.ts +186 -0
  28. package/plugin/routes/markdown.ts +62 -13
  29. package/plugin/sidebar-utils/sidebar-builder.ts +38 -12
  30. package/plugin/specs/defaultSpecLoader.ts +192 -0
  31. package/plugin/specs/fetchSpecSSR.ts +1 -1
  32. package/plugin/specs/utils.ts +86 -0
  33. package/shared/conditionalIntegration.ts +28 -0
  34. package/shared/getProsePages.ts +6 -7
  35. package/shared/virtualModule.ts +1 -26
  36. package/stl-docs/aiChatExamples.ts +31 -0
  37. package/stl-docs/chat/docs-chat-handler.ts +17 -0
  38. package/stl-docs/chat/hook.ts +225 -0
  39. package/stl-docs/chat/schemas.ts +27 -0
  40. package/stl-docs/chat/ui/AiChat.module.css +591 -0
  41. package/stl-docs/chat/ui/AiChat.tsx +175 -0
  42. package/stl-docs/chat/ui/Trigger.tsx +154 -0
  43. package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
  44. package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
  45. package/stl-docs/chat/ui/components/ChatLog.tsx +93 -0
  46. package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
  47. package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
  48. package/stl-docs/chat/ui/components/MessageFeedback.tsx +106 -0
  49. package/stl-docs/chat/ui/components/Table.tsx +15 -0
  50. package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
  51. package/stl-docs/chat/ui/components/hljs-github.css +81 -0
  52. package/stl-docs/chat/ui/scroll-manager.ts +86 -0
  53. package/stl-docs/chat/ui/types.ts +45 -0
  54. package/stl-docs/components/AiChatIsland.tsx +10 -12
  55. package/stl-docs/components/ContentPanel.astro +9 -0
  56. package/stl-docs/components/Footer.astro +89 -0
  57. package/stl-docs/components/Header.astro +0 -5
  58. package/stl-docs/components/PageFrame.astro +23 -8
  59. package/stl-docs/components/PageSidebar.astro +11 -0
  60. package/stl-docs/components/StainlessLogo.svg +4 -0
  61. package/stl-docs/components/TwoColumnContent.astro +2 -0
  62. package/stl-docs/components/headers/DefaultHeader.astro +6 -8
  63. package/stl-docs/components/headers/StackedHeader.astro +5 -53
  64. package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
  65. package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
  66. package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
  67. package/stl-docs/components/mintlify-compat/Frame.astro +2 -2
  68. package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
  69. package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
  70. package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
  71. package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
  72. package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
  73. package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
  74. package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
  75. package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
  76. package/stl-docs/components/nav-tabs/NavDropdown.astro +12 -7
  77. package/stl-docs/components/nav-tabs/NavTabs.astro +5 -3
  78. package/stl-docs/components/nav-tabs/buildNavLinks.ts +2 -0
  79. package/stl-docs/components/pagination/Pagination.astro +4 -2
  80. package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +2 -2
  81. package/stl-docs/components/pagination/PaginationLinkQuiet.astro +2 -2
  82. package/stl-docs/components/pagination/util.ts +3 -3
  83. package/stl-docs/components/sidebars/BaseSidebar.astro +72 -1
  84. package/stl-docs/disableCalloutSyntax.ts +1 -1
  85. package/stl-docs/fonts.ts +5 -5
  86. package/stl-docs/index.ts +76 -53
  87. package/stl-docs/loadStlDocsConfig.ts +38 -8
  88. package/stl-docs/og-image/components/OpenGraphFunctionSignature.tsx +64 -0
  89. package/stl-docs/og-image/components/OpenGraphImage.tsx +126 -0
  90. package/stl-docs/og-image/config.ts +56 -0
  91. package/stl-docs/og-image/image-gen/generate-api-reference-og-image.tsx +188 -0
  92. package/stl-docs/og-image/image-gen/generate-og-image.tsx +119 -0
  93. package/stl-docs/og-image/image-gen/get-logo-url.ts +47 -0
  94. package/stl-docs/og-image/index.ts +135 -0
  95. package/stl-docs/og-image/routes/add-og-image.ts +45 -0
  96. package/stl-docs/og-image/routes/get-api-reference-og-image.ts +36 -0
  97. package/stl-docs/og-image/routes/get-og-image.ts +28 -0
  98. package/stl-docs/og-image/theme.ts +43 -0
  99. package/stl-docs/og-image/utils.ts +14 -0
  100. package/stl-docs/proseDocSync.test.ts +74 -0
  101. package/stl-docs/proseDocSync.ts +344 -0
  102. package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +4 -12
  103. package/stl-docs/schema-extension.ts +12 -0
  104. package/stl-docs/tabsMiddleware.ts +1 -1
  105. package/styles/overrides.css +2 -14
  106. package/styles/page.css +210 -71
  107. package/styles/sidebar.css +30 -17
  108. package/styles/sl-variables.css +3 -8
  109. package/styles/stldocs-variables.css +2 -2
  110. package/styles/toc.css +8 -0
  111. package/tsconfig.json +1 -1
  112. package/virtual-module.d.ts +35 -11
  113. package/playground-virtual-modules.d.ts +0 -96
  114. package/plugin/globalJs/create-playground.shim.ts +0 -3
  115. package/plugin/globalJs/playground-data.shim.ts +0 -1
  116. package/plugin/globalJs/playground-data.ts +0 -14
  117. package/plugin/specs/FileCache.ts +0 -99
  118. package/plugin/specs/generateSpec.ts +0 -112
  119. package/plugin/specs/index.ts +0 -132
  120. package/plugin/specs/inputResolver.ts +0 -146
  121. package/plugin/specs/worker.ts +0 -199
  122. package/plugin/vendor/preview.worker.docs.js +0 -26108
  123. package/plugin/vendor/templates/cli.md +0 -1
  124. package/plugin/vendor/templates/go.md +0 -316
  125. package/plugin/vendor/templates/java.md +0 -89
  126. package/plugin/vendor/templates/kotlin.md +0 -89
  127. package/plugin/vendor/templates/node.md +0 -235
  128. package/plugin/vendor/templates/python.md +0 -251
  129. package/plugin/vendor/templates/ruby.md +0 -147
  130. package/plugin/vendor/templates/terraform.md +0 -60
  131. package/plugin/vendor/templates/typescript.md +0 -319
  132. package/scripts/vendor_deps.ts +0 -50
  133. package/stl-docs/components/ClientRouterHead.astro +0 -41
  134. package/stl-docs/components/content-panel/ContentPanel.astro +0 -42
  135. package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -65
  136. package/stl-docs/proseSearchIndexing.ts +0 -606
@@ -0,0 +1,175 @@
1
+ import { motion } from 'motion/react';
2
+ import { Suspense, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
3
+ import { useScrollToBottom } from './scroll-manager';
4
+ import { useChat } from '../hook';
5
+ import type { DocsChatHandler } from '../docs-chat-handler';
6
+
7
+ import ChatLog from './components/ChatLog';
8
+ import AiChatTrigger from './Trigger';
9
+ import ChatEmpty from './components/ChatEmpty';
10
+ import ChatControls from './components/ChatControls';
11
+ import { AI_CHAT_HANDLER } from 'virtual:stl-docs-ai-chat';
12
+
13
+ import styles from './AiChat.module.css';
14
+ import clsx from 'clsx';
15
+
16
+ const borderRadius = 16;
17
+
18
+ /** Remembers user preference but returns 'floating' if panel is not supported */
19
+ function usePresentation(supportsPanel: boolean) {
20
+ const [presentation, setPresentation] = useState<'floating' | 'panel'>('floating');
21
+ const appliedPresentation = supportsPanel ? presentation : 'floating';
22
+ return [appliedPresentation, setPresentation] as const;
23
+ }
24
+
25
+ const examplesPromise = import('virtual:stl-docs-ai-chat-examples')
26
+ .then((mod) => mod.examples)
27
+ .catch(() => undefined);
28
+
29
+ export default function AiChat({ siteTitle }: { siteTitle?: string }) {
30
+ if (!AI_CHAT_HANDLER) {
31
+ return null;
32
+ }
33
+ return <AiChatInner siteTitle={siteTitle} handler={AI_CHAT_HANDLER} />;
34
+ }
35
+
36
+ function AiChatInner({ siteTitle, handler }: { siteTitle?: string; handler: DocsChatHandler }) {
37
+ const { chatMessages, sendMessage, rateMessage } = useChat({
38
+ handler,
39
+ });
40
+
41
+ // panel mode is supported only on larger viewports
42
+ const supportsPanel = useSyncExternalStore(
43
+ (cb) => {
44
+ window.addEventListener('resize', cb);
45
+ return () => window.removeEventListener('resize', cb);
46
+ },
47
+ () => window.innerWidth >= 968,
48
+ () => false,
49
+ );
50
+
51
+ const baseRef = useRef<HTMLDivElement>(null);
52
+ const inputRef = useRef<HTMLTextAreaElement>(null);
53
+ const [focused, setFocused] = useState(false);
54
+
55
+ const [presentation, setPresentation] = usePresentation(supportsPanel);
56
+ const expanded = focused || presentation === 'panel';
57
+
58
+ // Manage “focus” state
59
+ // prettier-ignore
60
+ useEffect(() => {
61
+ const ac = new AbortController();
62
+ // “focus” in/out with click
63
+ window.addEventListener('click', (e) => {
64
+ if (!(e.target instanceof Element) || !baseRef.current || !document.contains(e.target)) return;
65
+ // clicks on elements with this attribute shouldn’t change focus state
66
+ // e.g. clicking minimize button shouldn’t cause the chat to _become_ focused
67
+ if (e.target.closest('[data-ai-chat-hit-ignore]')) return;
68
+ const hit = baseRef.current.contains(e.target);
69
+ setFocused(hit);
70
+ }, { signal: ac.signal });
71
+ // leave with escape
72
+ document.addEventListener('keydown', (e) => {
73
+ if (e.key === 'Escape') {
74
+ setFocused(false);
75
+ setPresentation('floating');
76
+ inputRef.current?.blur(); // this is the one case where the input won’t have already lost focus
77
+ }
78
+ }, { signal: ac.signal });
79
+
80
+ // record focus state when our chat elements receive focus
81
+ // unfocus when another element outside of our component gets focus (incl. by keyboard)
82
+ document.addEventListener('focusin', (e) => {
83
+ if (!(e.target instanceof Element) || !baseRef.current || !document.contains(e.target)) return;
84
+ setFocused(baseRef.current.contains(e.target));
85
+ }, { signal: ac.signal });
86
+ return () => ac.abort();
87
+ }, [setPresentation]);
88
+
89
+ const [pendingResponses, setPendingResponses] = useState(0);
90
+ const handleSendMessage = useCallback(
91
+ (q: string) => {
92
+ setPendingResponses((p) => p + 1);
93
+ sendMessage(q)
94
+ .catch(() => {})
95
+ .finally(() => {
96
+ setPendingResponses((p) => p - 1);
97
+ });
98
+ },
99
+ [sendMessage],
100
+ );
101
+
102
+ // scroll to bottom when new messages come in
103
+ const scrollAreaRef = useRef<HTMLDivElement>(null);
104
+ const scrollContentsRef = useRef<HTMLDivElement>(null);
105
+ useScrollToBottom(
106
+ scrollAreaRef,
107
+ scrollContentsRef,
108
+ // deps to re-run scroll
109
+ useMemo(() => [chatMessages, presentation], [chatMessages, presentation]),
110
+ );
111
+
112
+ return (
113
+ <div className={clsx(styles['outer-wrapper'], styles[`presentation-${presentation}`])} ref={baseRef}>
114
+ <AiChatTrigger
115
+ expanded={expanded}
116
+ updateFocused={setFocused}
117
+ sendMessage={handleSendMessage}
118
+ inputRef={inputRef}
119
+ borderRadius={borderRadius}
120
+ />
121
+
122
+ <motion.div
123
+ layout
124
+ className={styles['chat-area-container']}
125
+ variants={{
126
+ floating: { borderRadius: borderRadius + 1, '--shadow-color': 'var(--base-shadow-color)' },
127
+ panel: { borderRadius: 0, '--shadow-color': 'transparent' },
128
+ }}
129
+ animate={presentation}
130
+ style={{
131
+ display: expanded ? 'flex' : 'none',
132
+ boxShadow: '0 8px 20px -8px var(--shadow-color)',
133
+ }}
134
+ >
135
+ <motion.div
136
+ layout
137
+ className={clsx(styles['chat-area'], 'scrolls-up')}
138
+ variants={{ floating: { borderRadius }, panel: { borderRadius: 0 } }}
139
+ animate={presentation}
140
+ ref={scrollAreaRef}
141
+ >
142
+ <div className={styles['chat-scroll-contents']} ref={scrollContentsRef}>
143
+ <ChatControls
144
+ presentation={presentation}
145
+ setPresentation={(p) => {
146
+ setPresentation(p);
147
+ inputRef.current?.focus();
148
+ }}
149
+ supportsPanel={supportsPanel}
150
+ setClosed={() => {
151
+ setFocused(false);
152
+ setPresentation('floating');
153
+ }}
154
+ />
155
+ {chatMessages.length > 0 ? (
156
+ <ChatLog
157
+ messages={chatMessages}
158
+ rateMessage={rateMessage}
159
+ responsePending={pendingResponses > 0}
160
+ />
161
+ ) : (
162
+ <Suspense fallback={null}>
163
+ <ChatEmpty
164
+ siteTitle={siteTitle}
165
+ promptExamples={examplesPromise}
166
+ sendMessage={handleSendMessage}
167
+ />
168
+ </Suspense>
169
+ )}
170
+ </div>
171
+ </motion.div>
172
+ </motion.div>
173
+ </div>
174
+ );
175
+ }
@@ -0,0 +1,154 @@
1
+ // TODO: move to components
2
+ import React, { useState } from 'react';
3
+ import { ArrowUpIcon, BotMessageSquareIcon } from 'lucide-react';
4
+ import clsx from 'clsx';
5
+ import { Transition } from 'motion';
6
+ import styles from './AiChat.module.css';
7
+ import { motion } from 'motion/react';
8
+
9
+ const MotionBotIcon = motion.create(BotMessageSquareIcon);
10
+
11
+ export default function AiChatTrigger({
12
+ expanded,
13
+ updateFocused,
14
+ sendMessage,
15
+ inputRef,
16
+ borderRadius,
17
+ }: {
18
+ expanded: boolean;
19
+ updateFocused: (focused: boolean) => void;
20
+ sendMessage: (question: string) => void;
21
+ inputRef: React.RefObject<HTMLTextAreaElement | null>;
22
+ borderRadius: number;
23
+ }) {
24
+ const [empty, setEmpty] = useState(true);
25
+ const [resetKey, setResetKey] = useState(0);
26
+
27
+ const layoutTransition = {
28
+ type: 'spring',
29
+ mass: 0.7,
30
+ stiffness: 275,
31
+ damping: 20,
32
+ } satisfies Transition;
33
+
34
+ const crossBlurTransition = {
35
+ delay: expanded ? 0.07 : 0,
36
+ duration: 0.1,
37
+ ease: 'easeInOut',
38
+ } satisfies Transition;
39
+
40
+ const [willChange, setWillChange] = useState(false);
41
+ const willChangeStyle = willChange ? { willChange: 'transform' as const } : {};
42
+
43
+ return (
44
+ <form
45
+ style={{ display: 'contents' }}
46
+ action={(formData) => {
47
+ const question = formData.get('question');
48
+ if (typeof question === 'string' && question.trim().length) {
49
+ sendMessage(question);
50
+ setResetKey((k) => k + 1);
51
+ setEmpty(true);
52
+ }
53
+ }}
54
+ >
55
+ <motion.label
56
+ layout
57
+ transition={layoutTransition}
58
+ className={styles['trigger-outer']}
59
+ style={{
60
+ borderRadius: borderRadius + 1,
61
+ boxShadow: '0 4px 12px -4px var(--shadow-color)',
62
+ ...willChangeStyle,
63
+ }}
64
+ // set will-change when hovering the collapsed trigger
65
+ onMouseEnter={() => {
66
+ setWillChange(!expanded);
67
+ }}
68
+ onMouseLeave={() => setWillChange(false)}
69
+ >
70
+ <motion.div
71
+ layout
72
+ transition={layoutTransition}
73
+ className={clsx(styles.trigger, expanded && styles.expanded)}
74
+ style={{ borderRadius: borderRadius, '--border-radius': `${borderRadius}px`, ...willChangeStyle }}
75
+ >
76
+ {/* Bot icon is visible while closed */}
77
+ <MotionBotIcon
78
+ layout
79
+ className={styles['bot-icon']}
80
+ animate={{
81
+ opacity: expanded ? 0 : 1,
82
+ scale: expanded ? 0.75 : 1,
83
+ filter: expanded ? 'blur(4px)' : 'blur(0px)',
84
+ }}
85
+ style={willChange ? { willChange: 'filter, transform' } : {}}
86
+ transition={crossBlurTransition}
87
+ aria-label="AI chat"
88
+ />
89
+
90
+ {/* Input & send button are visible while open */}
91
+ <motion.div
92
+ layout
93
+ className={styles['expanded-contents']}
94
+ initial={{ opacity: 0 }}
95
+ animate={{
96
+ opacity: expanded ? 1 : 0,
97
+ filter: expanded ? 'blur(0px)' : 'blur(4px)',
98
+ }}
99
+ style={willChange ? { willChange: 'filter, transform' } : {}}
100
+ transition={crossBlurTransition}
101
+ >
102
+ <motion.textarea
103
+ layout
104
+ transition={layoutTransition}
105
+ name="question"
106
+ style={willChangeStyle}
107
+ rows={1}
108
+ placeholder="Ask a question"
109
+ // Keep track of whether the question is submittable
110
+ onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
111
+ setEmpty(e.target.value.trim().length === 0);
112
+ }}
113
+ // Submit on Cmd+Enter
114
+ onKeyDown={(e: React.KeyboardEvent<HTMLTextAreaElement>) => {
115
+ if (e.key === 'Enter' && !e.shiftKey) {
116
+ e.preventDefault();
117
+ e.currentTarget.form?.requestSubmit();
118
+ }
119
+ }}
120
+ // Update textarea height to fit content as user types
121
+ ref={(el: HTMLTextAreaElement | null) => {
122
+ inputRef.current = el;
123
+ if (!el) return;
124
+ const updateSize = () => {
125
+ el.style.height = 'auto';
126
+ el.style.height = `${el.scrollHeight}px`;
127
+ };
128
+ const ac = new AbortController();
129
+ el.addEventListener('input', updateSize, { signal: ac.signal });
130
+ updateSize();
131
+ // in case the user focused it before we mounted
132
+ if (document.activeElement === el) updateFocused(true);
133
+ // Re-focus after remount (e.g., after form submission resets the key)
134
+ if (expanded) el.focus();
135
+ return () => ac.abort();
136
+ }}
137
+ // make the ref re-mount so we get a fresh height measurement after reset
138
+ key={resetKey}
139
+ />
140
+ <motion.button
141
+ layout
142
+ type="submit"
143
+ disabled={empty}
144
+ transition={layoutTransition}
145
+ style={willChangeStyle}
146
+ >
147
+ <ArrowUpIcon aria-label="Send" />
148
+ </motion.button>
149
+ </motion.div>
150
+ </motion.div>
151
+ </motion.label>
152
+ </form>
153
+ );
154
+ }
@@ -0,0 +1,51 @@
1
+ import type { Dispatch, SetStateAction } from 'react';
2
+ import { motion } from 'motion/react';
3
+
4
+ import { Button } from '@stainless-api/ui-primitives';
5
+ import { PictureInPictureIcon, PanelRightCloseIcon as PanelRightIcon, MinusIcon } from 'lucide-react';
6
+
7
+ import styles from '../AiChat.module.css';
8
+
9
+ export default function ChatControls({
10
+ presentation,
11
+ setPresentation,
12
+ supportsPanel,
13
+ setClosed,
14
+ }: {
15
+ presentation: 'floating' | 'panel';
16
+ setPresentation: Dispatch<SetStateAction<'floating' | 'panel'>>;
17
+ supportsPanel: boolean;
18
+ setClosed: (e: React.MouseEvent<HTMLElement>) => void;
19
+ }) {
20
+ return (
21
+ <motion.div layout className={styles['controls']}>
22
+ {supportsPanel && (
23
+ <Button
24
+ type="button"
25
+ variant="ghost"
26
+ size="sm"
27
+ aria-label={{ panel: 'Switch to floating chat', floating: 'Switch to panel chat' }[presentation]}
28
+ onClick={() => {
29
+ setPresentation(presentation === 'floating' ? 'panel' : 'floating');
30
+ }}
31
+ >
32
+ <Button.Icon
33
+ icon={{ panel: PictureInPictureIcon, floating: PanelRightIcon }[presentation]}
34
+ size={15}
35
+ />
36
+ </Button>
37
+ )}
38
+
39
+ <Button
40
+ type="button"
41
+ variant="ghost"
42
+ size="sm"
43
+ aria-label="Close chat"
44
+ onClick={setClosed}
45
+ data-ai-chat-hit-ignore
46
+ >
47
+ <Button.Icon icon={MinusIcon} size={15} />
48
+ </Button>
49
+ </motion.div>
50
+ );
51
+ }
@@ -0,0 +1,42 @@
1
+ import { use } from 'react';
2
+ import { motion } from 'motion/react';
3
+ import { BotMessageSquareIcon as BotIcon } from 'lucide-react';
4
+ import type { ExamplePrompt } from '../types';
5
+ import styles from '../AiChat.module.css';
6
+
7
+ export default function ChatEmpty({
8
+ siteTitle,
9
+ promptExamples: promptExamplesPromise,
10
+ sendMessage,
11
+ }: {
12
+ siteTitle?: string;
13
+ promptExamples?: Promise<ExamplePrompt[] | undefined>;
14
+ sendMessage: (question: string) => void;
15
+ }) {
16
+ const promptExamples = promptExamplesPromise && use(promptExamplesPromise);
17
+
18
+ return (
19
+ <motion.div layout className={styles['chat-empty-state']}>
20
+ <BotIcon />
21
+ <h2>What can I help you with?</h2>
22
+
23
+ {promptExamples?.length ? (
24
+ <>
25
+ <h3>Suggestions</h3>
26
+ <ul>
27
+ {promptExamples.map(({ shortPrompt, longPrompt, icon: Icon }) => (
28
+ <li key={shortPrompt} className={styles['chat-example']}>
29
+ <button type="button" onClick={() => sendMessage(longPrompt)}>
30
+ <Icon />
31
+ {shortPrompt}
32
+ </button>
33
+ </li>
34
+ ))}
35
+ </ul>
36
+ </>
37
+ ) : (
38
+ <p>I can help answer questions about {siteTitle ?? 'this API'}. What do you want to build?</p>
39
+ )}
40
+ </motion.div>
41
+ );
42
+ }
@@ -0,0 +1,93 @@
1
+ import type { ChatMessage } from '../types';
2
+ import Message from './ChatMessage';
3
+ import MessageFeedbackButtons from './MessageFeedback';
4
+ import ToolCall from './ToolCall';
5
+
6
+ import { motion } from 'motion/react';
7
+
8
+ import styles from '../AiChat.module.css';
9
+ import { LoaderCircleIcon } from 'lucide-react';
10
+
11
+ export default function ChatLog({
12
+ messages,
13
+ rateMessage,
14
+ responsePending = false,
15
+ }: {
16
+ messages: ChatMessage[];
17
+ rateMessage?: (spanId: string, rating: 'up' | 'down') => Promise<boolean>;
18
+ responsePending?: boolean;
19
+ }) {
20
+ const lastMessage = messages.at(-1);
21
+
22
+ return (
23
+ <motion.ul
24
+ layout
25
+ role="log"
26
+ aria-live="polite"
27
+ className={styles['message-log']}
28
+ initial={{ opacity: 0, filter: `blur(4px)` }}
29
+ animate={{ opacity: 1, filter: `blur(0px)` }}
30
+ >
31
+ {messages.map((msg) => {
32
+ if (msg.role === 'user') {
33
+ return (
34
+ <Message key={msg.id} role="user">
35
+ {msg.content}
36
+ </Message>
37
+ );
38
+ }
39
+
40
+ if (msg.role === 'assistant' && msg.messageType === 'text') {
41
+ return (
42
+ <Message key={msg.id} role={msg.role} isMarkdown isStreaming={!msg.isComplete}>
43
+ {msg.content}
44
+ </Message>
45
+ );
46
+ }
47
+
48
+ if (msg.role === 'assistant' && msg.messageType === 'tool_use') {
49
+ return <ToolCall key={msg.id} message={msg} />;
50
+ }
51
+
52
+ if (msg.role === 'assistant' && msg.messageType === 'done') {
53
+ return (
54
+ <MessageFeedbackButtons
55
+ key={msg.id}
56
+ spanId={msg.spanId}
57
+ rateMessage={rateMessage}
58
+ // all "text" responses to the given message
59
+ messages={messages.flatMap((msg2) =>
60
+ msg2.role === 'assistant' &&
61
+ msg2.respondingTo === msg.respondingTo &&
62
+ msg2.messageType === 'text'
63
+ ? msg2
64
+ : [],
65
+ )}
66
+ />
67
+ );
68
+ }
69
+
70
+ if (msg.role === 'assistant' && msg.messageType === 'error') {
71
+ return (
72
+ <Message key={msg.id} role="error">
73
+ {msg.errorMessage}
74
+ </Message>
75
+ );
76
+ }
77
+
78
+ return null;
79
+ })}
80
+
81
+ {lastMessage?.role === 'user' && responsePending && (
82
+ <Message key={`${lastMessage.id}-response`} role="assistant">
83
+ {'Thinking'.split('').map((char, i) => (
84
+ <span key={i} className={styles['shimmer-letter']} style={{ '--i': i }}>
85
+ {char}
86
+ </span>
87
+ ))}
88
+ <LoaderCircleIcon className={styles['message-loader']} />
89
+ </Message>
90
+ )}
91
+ </motion.ul>
92
+ );
93
+ }
@@ -0,0 +1,47 @@
1
+ import remend from 'remend';
2
+ import Markdown from 'react-markdown';
3
+ import { motion } from 'motion/react';
4
+
5
+ import highlightCodeComponent from './CodeBlock';
6
+ import tableComponent from './Table';
7
+
8
+ import clsx from 'clsx';
9
+ import styles from '../AiChat.module.css';
10
+ import remarkGfm from 'remark-gfm';
11
+
12
+ export default function ChatMessage({
13
+ children,
14
+ role,
15
+ isStreaming = false,
16
+ isMarkdown = false,
17
+ }: {
18
+ children: React.ReactNode;
19
+ role: 'user' | 'assistant' | 'error';
20
+ isStreaming?: boolean;
21
+ isMarkdown?: boolean;
22
+ }) {
23
+ return (
24
+ <motion.li
25
+ layout="position"
26
+ data-message-role={role}
27
+ className={clsx(styles['chat-message'], 'stl-ui-prose', 'smaller-headings')}
28
+ style={{ borderRadius: 16 }}
29
+ >
30
+ {/* inner div provides scale correction while outer container transforms */}
31
+ {isMarkdown && typeof children === 'string' ? (
32
+ <Markdown
33
+ remarkPlugins={[remarkGfm]}
34
+ components={{
35
+ ...highlightCodeComponent,
36
+ ...tableComponent,
37
+ }}
38
+ >
39
+ {/* repair incomplete markdown syntax during streaming to ensure proper rendering */}
40
+ {isStreaming ? remend(children) : children}
41
+ </Markdown>
42
+ ) : (
43
+ <p>{children}</p>
44
+ )}
45
+ </motion.li>
46
+ );
47
+ }
@@ -0,0 +1,33 @@
1
+ import { Fragment } from 'react';
2
+
3
+ import { LightAsync as SyntaxHighlighter } from 'react-syntax-highlighter';
4
+
5
+ import type { Components } from 'react-markdown';
6
+ import clsx from 'clsx';
7
+ import './hljs-github.css';
8
+
9
+ function NoPropsFragment({ children }: { children: React.ReactNode }) {
10
+ return <Fragment>{children}</Fragment>;
11
+ }
12
+
13
+ export default {
14
+ code(props) {
15
+ const { children, className, ref, style, ...rest } = props;
16
+ const match = /language-(\w+)/.exec(className || '');
17
+ return match && typeof children === 'string' ? (
18
+ <SyntaxHighlighter
19
+ {...rest}
20
+ PreTag={NoPropsFragment}
21
+ language={match[1]}
22
+ useInlineStyles
23
+ codeTagProps={{ className: clsx(className, 'hljs-github') }}
24
+ >
25
+ {children.replace(/\n$/, '')}
26
+ </SyntaxHighlighter>
27
+ ) : (
28
+ <code {...rest} className={className}>
29
+ {children}
30
+ </code>
31
+ );
32
+ },
33
+ } satisfies Components;