@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.
- package/CHANGELOG.md +401 -0
- package/ambient.d.ts +6 -0
- package/eslint-suppressions.json +22 -6
- package/{eslint.config.js → eslint.config.ts} +1 -7
- package/package.json +62 -40
- package/plugin/buildAlgoliaIndex.ts +6 -12
- package/plugin/components/SDKSelect.astro +0 -6
- package/plugin/components/SnippetCode.tsx +6 -37
- package/plugin/components/search/SearchAlgolia.astro +1 -1
- package/plugin/components/search/SearchIsland.tsx +19 -13
- package/plugin/generateAPIReferenceLink.ts +0 -40
- package/plugin/globalJs/ai-dropdown-options.ts +22 -9
- package/plugin/globalJs/code-snippets.ts +5 -5
- package/plugin/globalJs/copy.ts +20 -91
- package/plugin/globalJs/navigation.ts +13 -13
- package/plugin/globalJs/summary-selection-tweak.ts +29 -0
- package/plugin/index.ts +107 -163
- package/plugin/loadPluginConfig.ts +49 -151
- package/plugin/markdown/highlighter.ts +100 -0
- package/plugin/markdown/index.ts +39 -0
- package/plugin/middlewareBuilder/stainlessMiddleware.d.ts +2 -0
- package/plugin/react/Routing.tsx +10 -244
- package/plugin/referencePlaceholderUtils.ts +1 -1
- package/plugin/replaceSidebarPlaceholderMiddleware.ts +1 -1
- package/plugin/routes/Docs.astro +3 -1
- package/plugin/routes/Overview.astro +14 -7
- package/plugin/routes/llms.ts +186 -0
- package/plugin/routes/markdown.ts +62 -13
- package/plugin/sidebar-utils/sidebar-builder.ts +38 -12
- package/plugin/specs/defaultSpecLoader.ts +192 -0
- package/plugin/specs/fetchSpecSSR.ts +1 -1
- package/plugin/specs/utils.ts +86 -0
- package/shared/conditionalIntegration.ts +28 -0
- package/shared/getProsePages.ts +6 -7
- package/shared/virtualModule.ts +1 -26
- package/stl-docs/aiChatExamples.ts +31 -0
- package/stl-docs/chat/docs-chat-handler.ts +17 -0
- package/stl-docs/chat/hook.ts +225 -0
- package/stl-docs/chat/schemas.ts +27 -0
- package/stl-docs/chat/ui/AiChat.module.css +591 -0
- package/stl-docs/chat/ui/AiChat.tsx +175 -0
- package/stl-docs/chat/ui/Trigger.tsx +154 -0
- package/stl-docs/chat/ui/components/ChatControls.tsx +51 -0
- package/stl-docs/chat/ui/components/ChatEmpty.tsx +42 -0
- package/stl-docs/chat/ui/components/ChatLog.tsx +93 -0
- package/stl-docs/chat/ui/components/ChatMessage.tsx +47 -0
- package/stl-docs/chat/ui/components/CodeBlock.tsx +33 -0
- package/stl-docs/chat/ui/components/MessageFeedback.tsx +106 -0
- package/stl-docs/chat/ui/components/Table.tsx +15 -0
- package/stl-docs/chat/ui/components/ToolCall.tsx +34 -0
- package/stl-docs/chat/ui/components/hljs-github.css +81 -0
- package/stl-docs/chat/ui/scroll-manager.ts +86 -0
- package/stl-docs/chat/ui/types.ts +45 -0
- package/stl-docs/components/AiChatIsland.tsx +10 -12
- package/stl-docs/components/ContentPanel.astro +9 -0
- package/stl-docs/components/Footer.astro +89 -0
- package/stl-docs/components/Header.astro +0 -5
- package/stl-docs/components/PageFrame.astro +23 -8
- package/stl-docs/components/PageSidebar.astro +11 -0
- package/stl-docs/components/StainlessLogo.svg +4 -0
- package/stl-docs/components/TwoColumnContent.astro +2 -0
- package/stl-docs/components/headers/DefaultHeader.astro +6 -8
- package/stl-docs/components/headers/StackedHeader.astro +5 -53
- package/stl-docs/components/mintlify-compat/Accordion.astro +2 -2
- package/stl-docs/components/mintlify-compat/AccordionGroup.astro +0 -4
- package/stl-docs/components/mintlify-compat/Columns.astro +2 -2
- package/stl-docs/components/mintlify-compat/Frame.astro +2 -2
- package/stl-docs/components/mintlify-compat/Tab.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Callout.astro +2 -2
- package/stl-docs/components/mintlify-compat/callouts/Check.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Danger.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Info.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Note.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Tip.astro +0 -4
- package/stl-docs/components/mintlify-compat/callouts/Warning.astro +0 -4
- package/stl-docs/components/nav-tabs/NavDropdown.astro +12 -7
- package/stl-docs/components/nav-tabs/NavTabs.astro +5 -3
- package/stl-docs/components/nav-tabs/buildNavLinks.ts +2 -0
- package/stl-docs/components/pagination/Pagination.astro +4 -2
- package/stl-docs/components/pagination/PaginationLinkEmphasized.astro +2 -2
- package/stl-docs/components/pagination/PaginationLinkQuiet.astro +2 -2
- package/stl-docs/components/pagination/util.ts +3 -3
- package/stl-docs/components/sidebars/BaseSidebar.astro +72 -1
- package/stl-docs/disableCalloutSyntax.ts +1 -1
- package/stl-docs/fonts.ts +5 -5
- package/stl-docs/index.ts +76 -53
- package/stl-docs/loadStlDocsConfig.ts +38 -8
- package/stl-docs/og-image/components/OpenGraphFunctionSignature.tsx +64 -0
- package/stl-docs/og-image/components/OpenGraphImage.tsx +126 -0
- package/stl-docs/og-image/config.ts +56 -0
- package/stl-docs/og-image/image-gen/generate-api-reference-og-image.tsx +188 -0
- package/stl-docs/og-image/image-gen/generate-og-image.tsx +119 -0
- package/stl-docs/og-image/image-gen/get-logo-url.ts +47 -0
- package/stl-docs/og-image/index.ts +135 -0
- package/stl-docs/og-image/routes/add-og-image.ts +45 -0
- package/stl-docs/og-image/routes/get-api-reference-og-image.ts +36 -0
- package/stl-docs/og-image/routes/get-og-image.ts +28 -0
- package/stl-docs/og-image/theme.ts +43 -0
- package/stl-docs/og-image/utils.ts +14 -0
- package/stl-docs/proseDocSync.test.ts +74 -0
- package/stl-docs/proseDocSync.ts +344 -0
- package/stl-docs/proseMarkdown/proseMarkdownIntegration.ts +4 -12
- package/stl-docs/schema-extension.ts +12 -0
- package/stl-docs/tabsMiddleware.ts +1 -1
- package/styles/overrides.css +2 -14
- package/styles/page.css +210 -71
- package/styles/sidebar.css +30 -17
- package/styles/sl-variables.css +3 -8
- package/styles/stldocs-variables.css +2 -2
- package/styles/toc.css +8 -0
- package/tsconfig.json +1 -1
- package/virtual-module.d.ts +35 -11
- package/playground-virtual-modules.d.ts +0 -96
- package/plugin/globalJs/create-playground.shim.ts +0 -3
- package/plugin/globalJs/playground-data.shim.ts +0 -1
- package/plugin/globalJs/playground-data.ts +0 -14
- package/plugin/specs/FileCache.ts +0 -99
- package/plugin/specs/generateSpec.ts +0 -112
- package/plugin/specs/index.ts +0 -132
- package/plugin/specs/inputResolver.ts +0 -146
- package/plugin/specs/worker.ts +0 -199
- package/plugin/vendor/preview.worker.docs.js +0 -26108
- package/plugin/vendor/templates/cli.md +0 -1
- package/plugin/vendor/templates/go.md +0 -316
- package/plugin/vendor/templates/java.md +0 -89
- package/plugin/vendor/templates/kotlin.md +0 -89
- package/plugin/vendor/templates/node.md +0 -235
- package/plugin/vendor/templates/python.md +0 -251
- package/plugin/vendor/templates/ruby.md +0 -147
- package/plugin/vendor/templates/terraform.md +0 -60
- package/plugin/vendor/templates/typescript.md +0 -319
- package/scripts/vendor_deps.ts +0 -50
- package/stl-docs/components/ClientRouterHead.astro +0 -41
- package/stl-docs/components/content-panel/ContentPanel.astro +0 -42
- package/stl-docs/components/headers/SplashMobileMenuToggle.astro +0 -65
- 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;
|