@inferencesh/ui 0.0.0-stage → 0.1.1
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/LICENSE +22 -0
- package/README.md +68 -2
- package/components.json +22 -0
- package/package.json +231 -4
- package/src/components/audio-player.tsx +549 -0
- package/src/components/button.tsx +13 -0
- package/src/components/chart.tsx +403 -0
- package/src/components/circuit.tsx +276 -0
- package/src/components/code-block/code-block.tsx +260 -0
- package/src/components/code-block/index.ts +18 -0
- package/src/components/code-block/languages.ts +311 -0
- package/src/components/code-block/styles.ts +39 -0
- package/src/components/code-block/tokenizer.ts +290 -0
- package/src/components/code-block/types.ts +38 -0
- package/src/components/code-block/utils.ts +39 -0
- package/src/components/code.tsx +48 -0
- package/src/components/cookie-consent.tsx +137 -0
- package/src/components/copy-button.tsx +44 -0
- package/src/components/country-dropdown.tsx +188 -0
- package/src/components/glow-loader-fast.tsx +32 -0
- package/src/components/glow-loader-mono.tsx +33 -0
- package/src/components/glow-loader-sm-fast.tsx +32 -0
- package/src/components/glow-loader-sm-mono.tsx +34 -0
- package/src/components/glow-loader-sm.tsx +32 -0
- package/src/components/glow-loader.tsx +32 -0
- package/src/components/glow-logo.tsx +32 -0
- package/src/components/gradient.tsx +80 -0
- package/src/components/index.ts +45 -0
- package/src/components/inline-sidebar.tsx +351 -0
- package/src/components/input-color.tsx +349 -0
- package/src/components/logo-base.tsx +213 -0
- package/src/components/logo-color.tsx +23 -0
- package/src/components/logo-full.tsx +71 -0
- package/src/components/logo-svg.tsx +23 -0
- package/src/components/menu-navigation.tsx +359 -0
- package/src/components/multi-select.tsx +721 -0
- package/src/components/pastel-gradient.tsx +25 -0
- package/src/components/pixel-symmetry.tsx +151 -0
- package/src/components/progress.tsx +28 -0
- package/src/components/scroll-more.tsx +131 -0
- package/src/components/select.tsx +85 -0
- package/src/components/shimmering-text.tsx +108 -0
- package/src/components/sidebar-light.tsx +102 -0
- package/src/components/sonner.tsx +38 -0
- package/src/components/theme-switcher-sidebar.tsx +56 -0
- package/src/components/theme-switcher-simple.tsx +110 -0
- package/src/components/theme-switcher.tsx +47 -0
- package/src/components/toggle-group.tsx +55 -0
- package/src/components/tooltip-button.tsx +91 -0
- package/src/components/tooltip.tsx +71 -0
- package/src/components/tree-view.tsx +499 -0
- package/src/components/unglow-loader.tsx +33 -0
- package/src/components/waveform.tsx +1632 -0
- package/src/components/zoomable-image.tsx +76 -0
- package/src/docs/api-docs.tsx +763 -0
- package/src/docs/callout.tsx +160 -0
- package/src/docs/code-group.tsx +215 -0
- package/src/docs/copy-menu.tsx +143 -0
- package/src/docs/doc-tabs.tsx +84 -0
- package/src/docs/index.ts +16 -0
- package/src/docs/steps.tsx +105 -0
- package/src/docs/table-of-contents.tsx +174 -0
- package/src/examples/guide-markdown.ts +129 -0
- package/src/examples/index.ts +1153 -0
- package/src/global.d.ts +1 -0
- package/src/hooks/use-copy-to-clipboard.ts +36 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/lib/display-name.ts +15 -0
- package/src/lib/example-input.ts +130 -0
- package/src/lib/plans.ts +44 -0
- package/src/lib/safe-content.ts +137 -0
- package/src/lib/schema.ts +320 -0
- package/src/lib/storage.ts +108 -0
- package/src/markdown/docs-markdown-renderer.tsx +518 -0
- package/src/markdown/helpers.ts +95 -0
- package/src/markdown/index.ts +24 -0
- package/src/markdown/markdown-renderer.tsx +232 -0
- package/src/markdown/youtube-embed.tsx +43 -0
- package/src/og/image.ts +312 -0
- package/src/og/index.tsx +430 -0
- package/src/og/query.ts +26 -0
- package/src/shadcn/accordion.tsx +71 -0
- package/src/shadcn/alert-dialog.tsx +187 -0
- package/src/shadcn/alert.tsx +75 -0
- package/src/shadcn/aspect-ratio.tsx +22 -0
- package/src/shadcn/attachment.tsx +207 -0
- package/src/shadcn/avatar.tsx +106 -0
- package/src/shadcn/badge.tsx +51 -0
- package/src/shadcn/breadcrumb.tsx +124 -0
- package/src/shadcn/bubble.tsx +127 -0
- package/src/shadcn/button-group.tsx +87 -0
- package/src/shadcn/button.tsx +57 -0
- package/src/shadcn/calendar.tsx +219 -0
- package/src/shadcn/card.tsx +102 -0
- package/src/shadcn/carousel.tsx +242 -0
- package/src/shadcn/chart.tsx +372 -0
- package/src/shadcn/checkbox.tsx +28 -0
- package/src/shadcn/collapsible.tsx +19 -0
- package/src/shadcn/combobox.tsx +297 -0
- package/src/shadcn/command.tsx +194 -0
- package/src/shadcn/context-menu.tsx +270 -0
- package/src/shadcn/dialog.tsx +158 -0
- package/src/shadcn/direction.tsx +4 -0
- package/src/shadcn/drawer.tsx +227 -0
- package/src/shadcn/dropdown-menu.tsx +265 -0
- package/src/shadcn/empty.tsx +103 -0
- package/src/shadcn/field.tsx +238 -0
- package/src/shadcn/form.tsx +173 -0
- package/src/shadcn/hover-card.tsx +50 -0
- package/src/shadcn/index.ts +53 -0
- package/src/shadcn/input-group.tsx +156 -0
- package/src/shadcn/input-otp.tsx +84 -0
- package/src/shadcn/input.tsx +19 -0
- package/src/shadcn/item.tsx +201 -0
- package/src/shadcn/kbd.tsx +26 -0
- package/src/shadcn/label.tsx +19 -0
- package/src/shadcn/marker.tsx +70 -0
- package/src/shadcn/menubar.tsx +278 -0
- package/src/shadcn/message-scroller.tsx +131 -0
- package/src/shadcn/message.tsx +91 -0
- package/src/shadcn/native-select.tsx +60 -0
- package/src/shadcn/navigation-menu.tsx +167 -0
- package/src/shadcn/pagination.tsx +130 -0
- package/src/shadcn/popover.tsx +87 -0
- package/src/shadcn/progress.tsx +82 -0
- package/src/shadcn/radio-group.tsx +35 -0
- package/src/shadcn/resizable.tsx +49 -0
- package/src/shadcn/scroll-area.tsx +52 -0
- package/src/shadcn/select.tsx +200 -0
- package/src/shadcn/separator.tsx +22 -0
- package/src/shadcn/sheet.tsx +136 -0
- package/src/shadcn/sidebar.tsx +721 -0
- package/src/shadcn/skeleton.tsx +13 -0
- package/src/shadcn/slider.tsx +51 -0
- package/src/shadcn/sonner.tsx +49 -0
- package/src/shadcn/spinner.tsx +10 -0
- package/src/shadcn/switch.tsx +29 -0
- package/src/shadcn/table.tsx +115 -0
- package/src/shadcn/tabs.tsx +79 -0
- package/src/shadcn/textarea.tsx +17 -0
- package/src/shadcn/toggle-group.tsx +89 -0
- package/src/shadcn/toggle.tsx +44 -0
- package/src/shadcn/tooltip.tsx +63 -0
- package/src/theme/config.ts +48 -0
- package/src/theme/css/base.css +92 -0
- package/src/theme/css/bitboy.css +75 -0
- package/src/theme/css/dark.css +47 -0
- package/src/theme/css/designer.css +122 -0
- package/src/theme/css/fonts.css +33 -0
- package/src/theme/css/globals.css +14 -0
- package/src/theme/css/hackintosh.css +57 -0
- package/src/theme/css/llama.css +103 -0
- package/src/theme/css/safelist.css +39 -0
- package/src/theme/css/shadcn.css +77 -0
- package/src/theme/css/theme.css +216 -0
- package/src/theme/embed-theme.ts +146 -0
- package/src/theme/fonts/FiraCode-VF.woff2 +0 -0
- package/src/theme/fonts/Hack-Regular.woff2 +0 -0
- package/src/theme/fonts/LICENSE-FiraCode.txt +93 -0
- package/src/theme/fonts/LICENSE-Hack.txt +45 -0
- package/src/theme/fonts/LICENSE-MonaSans.txt +93 -0
- package/src/theme/fonts/Mona-Sans.woff2 +0 -0
- package/src/theme/index.ts +15 -0
- package/src/utils/cn.ts +3 -0
- package/src/utils/index.ts +2 -0
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from "cn"
|
|
4
|
+
import ZoomableImage from "../components/zoomable-image"
|
|
5
|
+
import { Hash, Link as LinkIcon } from 'lucide-react'
|
|
6
|
+
import React, { memo, useMemo } from 'react'
|
|
7
|
+
import Markdown from 'react-markdown'
|
|
8
|
+
import { safeUrlTransform } from '../lib/safe-content'
|
|
9
|
+
import remarkGfm from 'remark-gfm'
|
|
10
|
+
import rehypeRaw from 'rehype-raw'
|
|
11
|
+
import rehypeSanitize, { defaultSchema, type Options as SanitizeSchema } from 'rehype-sanitize'
|
|
12
|
+
import type { Root, Code } from 'mdast'
|
|
13
|
+
|
|
14
|
+
/** What raw HTML in markdown may keep. This renderer shows user-written
|
|
15
|
+
* content (skills, knowledge, agent output) on the app's own origin, so raw
|
|
16
|
+
* HTML goes through GitHub's allowlist (defaultSchema: no script, iframe,
|
|
17
|
+
* object, embed, style, event handlers or javascript: URLs), plus the
|
|
18
|
+
* attribute the docs components switch on (div data-type; title is already
|
|
19
|
+
* allowed everywhere) and the code-fence meta string.
|
|
20
|
+
*
|
|
21
|
+
* Ids: remark-rehype already prefixes the ids it generates (footnotes) with
|
|
22
|
+
* user-content-, so the schema does not clobber ids again (that would make
|
|
23
|
+
* user-content-user-content-fn-1 and break footnote links). Instead an id
|
|
24
|
+
* is kept only when it carries that prefix, which still stops raw HTML from
|
|
25
|
+
* naming elements after document or window properties (DOM clobbering). */
|
|
26
|
+
export const markdownSanitizeSchema: SanitizeSchema = {
|
|
27
|
+
...defaultSchema,
|
|
28
|
+
clobber: ['name'],
|
|
29
|
+
attributes: {
|
|
30
|
+
...defaultSchema.attributes,
|
|
31
|
+
'*': [...(defaultSchema.attributes?.['*'] ?? []).filter((a) => a !== 'id'), ['id', /^user-content-/]],
|
|
32
|
+
div: [...(defaultSchema.attributes?.div ?? []), 'dataType'],
|
|
33
|
+
code: [...(defaultSchema.attributes?.code ?? []), 'dataMeta'],
|
|
34
|
+
},
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Remark plugin that preserves code-fence meta strings (e.g. ```text Python)
|
|
38
|
+
* as data-meta attributes so they survive the remark→rehype conversion. */
|
|
39
|
+
function remarkCodeMeta() {
|
|
40
|
+
return (tree: Root) => {
|
|
41
|
+
function walk(node: Root | Root['children'][number]) {
|
|
42
|
+
if (node.type === 'code') {
|
|
43
|
+
const code = node as Code
|
|
44
|
+
if (code.meta) {
|
|
45
|
+
;(code as any).data = (code as any).data || {}
|
|
46
|
+
;(code as any).data.hProperties = (code as any).data.hProperties || {}
|
|
47
|
+
;(code as any).data.hProperties['data-meta'] = code.meta
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if ('children' in node && Array.isArray(node.children)) {
|
|
51
|
+
for (const child of node.children) walk(child)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
walk(tree)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
import { Callout } from '../docs/callout'
|
|
58
|
+
import { CodeGroup } from '../docs/code-group'
|
|
59
|
+
import { DocsTabs, DocsTab } from '../docs/doc-tabs'
|
|
60
|
+
import { Steps, Step } from '../docs/steps'
|
|
61
|
+
import { isCloudInferenceUrl, isVideoUrl, getYouTubeVideoId, stripHtmlComments } from './helpers'
|
|
62
|
+
import { YouTubeEmbed } from './youtube-embed'
|
|
63
|
+
import { CodeBlock as MarkdownCodeBlock } from '../components/code-block'
|
|
64
|
+
|
|
65
|
+
// Heading with anchor link
|
|
66
|
+
function HeadingWithAnchor({
|
|
67
|
+
level,
|
|
68
|
+
children,
|
|
69
|
+
id,
|
|
70
|
+
}: {
|
|
71
|
+
level: 1 | 2 | 3 | 4 | 5 | 6
|
|
72
|
+
children: React.ReactNode
|
|
73
|
+
id?: string
|
|
74
|
+
}) {
|
|
75
|
+
const getTextContent = (node: React.ReactNode): string => {
|
|
76
|
+
if (typeof node === 'string') return node
|
|
77
|
+
if (typeof node === 'number') return String(node)
|
|
78
|
+
if (!node) return ''
|
|
79
|
+
if (Array.isArray(node)) return node.map(getTextContent).join('')
|
|
80
|
+
if (React.isValidElement(node)) {
|
|
81
|
+
return getTextContent((node.props as { children?: React.ReactNode }).children)
|
|
82
|
+
}
|
|
83
|
+
return ''
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const text = getTextContent(children)
|
|
87
|
+
const headingId =
|
|
88
|
+
id ||
|
|
89
|
+
text
|
|
90
|
+
.toLowerCase()
|
|
91
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
92
|
+
.replace(/(^-|-$)/g, '')
|
|
93
|
+
|
|
94
|
+
const Tag = `h${level}` as 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
95
|
+
const sizeClasses = {
|
|
96
|
+
1: 'text-3xl font-bold mt-0 mb-6',
|
|
97
|
+
2: 'text-2xl font-bold mt-12 mb-4 pb-2 border-b border-border',
|
|
98
|
+
3: 'text-xl font-semibold mt-10 mb-3',
|
|
99
|
+
4: 'text-lg font-semibold mt-8 mb-2',
|
|
100
|
+
5: 'text-base font-semibold mt-6 mb-2',
|
|
101
|
+
6: 'text-base font-medium mt-4 mb-2',
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<Tag
|
|
106
|
+
id={headingId}
|
|
107
|
+
className={cn(
|
|
108
|
+
sizeClasses[level],
|
|
109
|
+
'group scroll-mt-20 flex items-center gap-2 lowercase',
|
|
110
|
+
'[&:first-child]:mt-0'
|
|
111
|
+
)}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
<a
|
|
115
|
+
href={`#${headingId}`}
|
|
116
|
+
className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-brand-pink"
|
|
117
|
+
aria-label={`Link to ${text}`}
|
|
118
|
+
>
|
|
119
|
+
<Hash className="h-4 w-4" />
|
|
120
|
+
</a>
|
|
121
|
+
</Tag>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Parse callout blocks from blockquotes
|
|
126
|
+
function parseCalloutFromBlockquote(children: React.ReactNode): {
|
|
127
|
+
type: 'note' | 'tip' | 'info' | 'warning' | 'danger' | 'example' | 'terminal'
|
|
128
|
+
title?: string
|
|
129
|
+
content: React.ReactNode
|
|
130
|
+
} | null {
|
|
131
|
+
const childArray = React.Children.toArray(children)
|
|
132
|
+
if (childArray.length === 0) return null
|
|
133
|
+
|
|
134
|
+
const firstChild = childArray[0]
|
|
135
|
+
if (!React.isValidElement(firstChild)) return null
|
|
136
|
+
|
|
137
|
+
const props = firstChild.props as { children?: React.ReactNode }
|
|
138
|
+
if (!props.children) return null
|
|
139
|
+
|
|
140
|
+
const innerChildren = React.Children.toArray(props.children)
|
|
141
|
+
if (innerChildren.length === 0) return null
|
|
142
|
+
|
|
143
|
+
const firstText = innerChildren[0]
|
|
144
|
+
if (typeof firstText !== 'string') return null
|
|
145
|
+
|
|
146
|
+
const match = firstText.match(
|
|
147
|
+
/^\[!(NOTE|TIP|INFO|WARNING|DANGER|EXAMPLE|TERMINAL)\](?:\s*(.*))?/
|
|
148
|
+
)
|
|
149
|
+
if (!match) return null
|
|
150
|
+
|
|
151
|
+
const type = match[1].toLowerCase() as
|
|
152
|
+
| 'note'
|
|
153
|
+
| 'tip'
|
|
154
|
+
| 'info'
|
|
155
|
+
| 'warning'
|
|
156
|
+
| 'danger'
|
|
157
|
+
| 'example'
|
|
158
|
+
| 'terminal'
|
|
159
|
+
const title = match[2]?.trim() || undefined
|
|
160
|
+
|
|
161
|
+
const restOfFirstText = firstText
|
|
162
|
+
.replace(
|
|
163
|
+
/^\[!(NOTE|TIP|INFO|WARNING|DANGER|EXAMPLE|TERMINAL)\](?:\s*(.*))?/,
|
|
164
|
+
''
|
|
165
|
+
)
|
|
166
|
+
.trim()
|
|
167
|
+
const restOfInnerChildren = innerChildren.slice(1)
|
|
168
|
+
const restOfChildren = childArray.slice(1)
|
|
169
|
+
|
|
170
|
+
const content = (
|
|
171
|
+
<>
|
|
172
|
+
{restOfFirstText && <p>{restOfFirstText}</p>}
|
|
173
|
+
{restOfInnerChildren}
|
|
174
|
+
{restOfChildren}
|
|
175
|
+
</>
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
return { type, title, content }
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Parse YAML frontmatter from markdown content.
|
|
182
|
+
// Returns [frontmatter entries, body without frontmatter].
|
|
183
|
+
function parseFrontmatter(content: string): [Record<string, string>, string] {
|
|
184
|
+
if (!content.startsWith('---')) return [{}, content]
|
|
185
|
+
const lines = content.split('\n')
|
|
186
|
+
let endIndex = -1
|
|
187
|
+
for (let i = 1; i < lines.length; i++) {
|
|
188
|
+
if (lines[i].trim() === '---') {
|
|
189
|
+
endIndex = i
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (endIndex === -1) return [{}, content]
|
|
194
|
+
|
|
195
|
+
const entries: Record<string, string> = {}
|
|
196
|
+
let currentKey = ''
|
|
197
|
+
let currentValue = ''
|
|
198
|
+
|
|
199
|
+
for (let i = 1; i < endIndex; i++) {
|
|
200
|
+
const line = lines[i]
|
|
201
|
+
const match = line.match(/^(\w[\w-]*):\s*(.*)/)
|
|
202
|
+
if (match) {
|
|
203
|
+
if (currentKey) entries[currentKey] = currentValue.trim()
|
|
204
|
+
currentKey = match[1]
|
|
205
|
+
currentValue = match[2]
|
|
206
|
+
} else if (currentKey && (line.startsWith(' ') || line.startsWith('\t'))) {
|
|
207
|
+
currentValue += ' ' + line.trim()
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (currentKey) entries[currentKey] = currentValue.trim()
|
|
211
|
+
|
|
212
|
+
const body = lines.slice(endIndex + 1).join('\n').replace(/^\n/, '')
|
|
213
|
+
return [entries, body]
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Render frontmatter as a GitHub-style metadata table
|
|
217
|
+
function FrontmatterTable({ entries }: { entries: Record<string, string> }) {
|
|
218
|
+
const keys = Object.keys(entries).filter(k => entries[k])
|
|
219
|
+
if (keys.length === 0) return null
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
<div className="my-4 w-full overflow-x-auto rounded-xl border border-border">
|
|
223
|
+
<table className="w-full text-sm">
|
|
224
|
+
<tbody>
|
|
225
|
+
{keys.map(key => (
|
|
226
|
+
<tr key={key} className="border-b border-border last:border-0 hover:bg-muted/30 transition-colors">
|
|
227
|
+
<td className="px-4 py-2 font-medium text-muted-foreground w-1/4 bg-muted/30">{key}</td>
|
|
228
|
+
<td className="px-4 py-2 text-foreground/90">{entries[key]}</td>
|
|
229
|
+
</tr>
|
|
230
|
+
))}
|
|
231
|
+
</tbody>
|
|
232
|
+
</table>
|
|
233
|
+
</div>
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface DocsMarkdownRendererProps {
|
|
238
|
+
content: string
|
|
239
|
+
className?: string
|
|
240
|
+
/** Custom renderer for cloud.inference.sh file links */
|
|
241
|
+
renderFilePreview?: (props: { uri: string; filename: string }) => React.ReactNode
|
|
242
|
+
/** Custom renderer for cloud.inference.sh images */
|
|
243
|
+
renderCloudImage?: (props: { src: string; alt?: string }) => React.ReactNode
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export const DocsMarkdownRenderer = memo(function DocsMarkdownRenderer({
|
|
247
|
+
content,
|
|
248
|
+
className,
|
|
249
|
+
renderFilePreview,
|
|
250
|
+
renderCloudImage,
|
|
251
|
+
}: DocsMarkdownRendererProps) {
|
|
252
|
+
const [frontmatter, bodyContent] = useMemo(
|
|
253
|
+
() => parseFrontmatter(content),
|
|
254
|
+
[content]
|
|
255
|
+
)
|
|
256
|
+
const processedContent = useMemo(
|
|
257
|
+
() => stripHtmlComments(bodyContent),
|
|
258
|
+
[bodyContent]
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div className={cn('docs-content', className)}>
|
|
263
|
+
{Object.keys(frontmatter).length > 0 && (
|
|
264
|
+
<FrontmatterTable entries={frontmatter} />
|
|
265
|
+
)}
|
|
266
|
+
<Markdown
|
|
267
|
+
urlTransform={safeUrlTransform}
|
|
268
|
+
remarkPlugins={[remarkCodeMeta, [remarkGfm, { singleTilde: false }]]}
|
|
269
|
+
rehypePlugins={[rehypeRaw, [rehypeSanitize, markdownSanitizeSchema]]}
|
|
270
|
+
components={{
|
|
271
|
+
// Headings with anchors
|
|
272
|
+
h1: ({ children }) => <HeadingWithAnchor level={1}>{children}</HeadingWithAnchor>,
|
|
273
|
+
h2: ({ children }) => <HeadingWithAnchor level={2}>{children}</HeadingWithAnchor>,
|
|
274
|
+
h3: ({ children }) => <HeadingWithAnchor level={3}>{children}</HeadingWithAnchor>,
|
|
275
|
+
h4: ({ children }) => <HeadingWithAnchor level={4}>{children}</HeadingWithAnchor>,
|
|
276
|
+
h5: ({ children }) => <HeadingWithAnchor level={5}>{children}</HeadingWithAnchor>,
|
|
277
|
+
h6: ({ children }) => <HeadingWithAnchor level={6}>{children}</HeadingWithAnchor>,
|
|
278
|
+
|
|
279
|
+
// Paragraphs
|
|
280
|
+
p: ({ children }) => {
|
|
281
|
+
const childArray = React.Children.toArray(children)
|
|
282
|
+
const hasBlockContent = childArray.some(
|
|
283
|
+
(child) => {
|
|
284
|
+
if (!React.isValidElement(child)) return false
|
|
285
|
+
const props = child.props as { href?: string; src?: string; children?: React.ReactNode }
|
|
286
|
+
if (props.src) return true
|
|
287
|
+
if (props.href && isCloudInferenceUrl(props.href)) return true
|
|
288
|
+
// YouTube embed only when it's a bare link (link text = URL)
|
|
289
|
+
if (props.href && getYouTubeVideoId(props.href)) {
|
|
290
|
+
const linkChildren = React.Children.toArray(props.children)
|
|
291
|
+
return linkChildren.length === 1 && typeof linkChildren[0] === 'string' && !!getYouTubeVideoId(linkChildren[0])
|
|
292
|
+
}
|
|
293
|
+
return false
|
|
294
|
+
}
|
|
295
|
+
)
|
|
296
|
+
if (hasBlockContent) {
|
|
297
|
+
return <div className="my-4">{children}</div>
|
|
298
|
+
}
|
|
299
|
+
return (
|
|
300
|
+
<p className="text-base leading-7 text-foreground/90 my-4 [&:first-child]:mt-0 [&:last-child]:mb-0">
|
|
301
|
+
{children}
|
|
302
|
+
</p>
|
|
303
|
+
)
|
|
304
|
+
},
|
|
305
|
+
|
|
306
|
+
// Blockquotes - with callout detection
|
|
307
|
+
blockquote: ({ children }) => {
|
|
308
|
+
const callout = parseCalloutFromBlockquote(children)
|
|
309
|
+
if (callout) {
|
|
310
|
+
return (
|
|
311
|
+
<Callout type={callout.type} title={callout.title}>
|
|
312
|
+
{callout.content}
|
|
313
|
+
</Callout>
|
|
314
|
+
)
|
|
315
|
+
}
|
|
316
|
+
return (
|
|
317
|
+
<blockquote className="my-6 border-l-4 border-muted-foreground/20 pl-4 text-muted-foreground italic">
|
|
318
|
+
{children}
|
|
319
|
+
</blockquote>
|
|
320
|
+
)
|
|
321
|
+
},
|
|
322
|
+
|
|
323
|
+
// Lists
|
|
324
|
+
ul: ({ children }) => (
|
|
325
|
+
<ul className="my-4 ml-6 list-disc space-y-2 marker:text-muted-foreground/50">
|
|
326
|
+
{children}
|
|
327
|
+
</ul>
|
|
328
|
+
),
|
|
329
|
+
ol: ({ children }) => (
|
|
330
|
+
<ol className="my-4 ml-6 list-decimal space-y-2 marker:text-muted-foreground/50">
|
|
331
|
+
{children}
|
|
332
|
+
</ol>
|
|
333
|
+
),
|
|
334
|
+
// id: footnote items (user-content-fn-N) are the targets of the
|
|
335
|
+
// footnote references.
|
|
336
|
+
li: ({ children, id }) => (
|
|
337
|
+
<li id={id} className="text-base leading-7 text-foreground/90 pl-1">
|
|
338
|
+
{children}
|
|
339
|
+
</li>
|
|
340
|
+
),
|
|
341
|
+
|
|
342
|
+
// Inline elements
|
|
343
|
+
strong: ({ children }) => (
|
|
344
|
+
<strong className="font-semibold text-foreground">{children}</strong>
|
|
345
|
+
),
|
|
346
|
+
em: ({ children }) => <em className="italic">{children}</em>,
|
|
347
|
+
|
|
348
|
+
// Code
|
|
349
|
+
code(props) {
|
|
350
|
+
const { children, className, node } = props as any
|
|
351
|
+
const match = /language-(\w+)/.exec(className || '')
|
|
352
|
+
const hasNewlines = String(children).includes('\n')
|
|
353
|
+
const isBlock = match || hasNewlines
|
|
354
|
+
const meta = node?.properties?.['dataMeta'] || undefined
|
|
355
|
+
|
|
356
|
+
if (isBlock) {
|
|
357
|
+
return (
|
|
358
|
+
<code className={cn('font-mono', className)} data-meta={meta}>{children}</code>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
// Inline code
|
|
362
|
+
return (
|
|
363
|
+
<code className="relative rounded-md bg-muted px-[0.4rem] py-[0.2rem] font-mono text-sm text-brand-pink">
|
|
364
|
+
{children}
|
|
365
|
+
</code>
|
|
366
|
+
)
|
|
367
|
+
},
|
|
368
|
+
|
|
369
|
+
pre: ({ children }) => {
|
|
370
|
+
const codeChild = React.Children.toArray(children)[0]
|
|
371
|
+
let language: string | undefined
|
|
372
|
+
let meta: string | undefined
|
|
373
|
+
if (React.isValidElement(codeChild)) {
|
|
374
|
+
const codeProps = codeChild.props as { className?: string; 'data-meta'?: string }
|
|
375
|
+
const className = codeProps.className || ''
|
|
376
|
+
const match = /language-(\w+)/.exec(className)
|
|
377
|
+
if (match) {
|
|
378
|
+
language = match[1]
|
|
379
|
+
}
|
|
380
|
+
meta = codeProps['data-meta']
|
|
381
|
+
}
|
|
382
|
+
// textSize is explicit because the shared CodeBlock defaults to
|
|
383
|
+
// text-xs for the app's dense panels; docs prose wants text-sm.
|
|
384
|
+
return <MarkdownCodeBlock language={language} data-meta={meta} textSize="text-sm">{children}</MarkdownCodeBlock>
|
|
385
|
+
},
|
|
386
|
+
|
|
387
|
+
// Links
|
|
388
|
+
// id: footnote references (user-content-fnref-N) are the targets
|
|
389
|
+
// of the footnotes' back links.
|
|
390
|
+
a: ({ href, children, id }) => {
|
|
391
|
+
if (href) {
|
|
392
|
+
// YouTube videos — only embed when the link text is the URL itself
|
|
393
|
+
// (bare URL on its own line), not when it has a custom label like [text](url)
|
|
394
|
+
const videoId = getYouTubeVideoId(href)
|
|
395
|
+
const childArray = React.Children.toArray(children)
|
|
396
|
+
const isBareLink = childArray.length === 1 && typeof childArray[0] === 'string' && getYouTubeVideoId(childArray[0])
|
|
397
|
+
if (videoId && isBareLink) {
|
|
398
|
+
return <YouTubeEmbed videoId={videoId} />
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Cloud video URLs — embed as <video> player
|
|
402
|
+
if (isCloudInferenceUrl(href) && isVideoUrl(href)) {
|
|
403
|
+
return (
|
|
404
|
+
<video
|
|
405
|
+
src={href}
|
|
406
|
+
controls
|
|
407
|
+
playsInline
|
|
408
|
+
className="my-6 rounded-xl w-full max-w-2xl"
|
|
409
|
+
/>
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Cloud file previews (custom renderer)
|
|
414
|
+
if (isCloudInferenceUrl(href) && renderFilePreview) {
|
|
415
|
+
return renderFilePreview({
|
|
416
|
+
uri: href,
|
|
417
|
+
filename: typeof children === 'string' ? children : 'file',
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// External vs internal links
|
|
422
|
+
const isExternal = href.startsWith('http')
|
|
423
|
+
return (
|
|
424
|
+
<a
|
|
425
|
+
id={id}
|
|
426
|
+
href={href}
|
|
427
|
+
className="text-brand-pink font-medium hover:underline underline-offset-4 inline-flex items-center gap-1"
|
|
428
|
+
target={isExternal ? '_blank' : undefined}
|
|
429
|
+
rel={isExternal ? 'noopener noreferrer' : undefined}
|
|
430
|
+
>
|
|
431
|
+
{children}
|
|
432
|
+
{isExternal && <LinkIcon className="h-3 w-3" />}
|
|
433
|
+
</a>
|
|
434
|
+
)
|
|
435
|
+
}
|
|
436
|
+
return <>{children}</>
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
// Images
|
|
440
|
+
img: ({ src, alt }) => {
|
|
441
|
+
if (!src || typeof src !== 'string') return null
|
|
442
|
+
|
|
443
|
+
// Cloud images (custom renderer)
|
|
444
|
+
if (isCloudInferenceUrl(src) && renderCloudImage) {
|
|
445
|
+
return renderCloudImage({ src, alt })
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return (
|
|
449
|
+
<ZoomableImage
|
|
450
|
+
src={src}
|
|
451
|
+
alt={alt || ''}
|
|
452
|
+
className="my-6 rounded-xl border border-border max-w-full h-auto"
|
|
453
|
+
/>
|
|
454
|
+
)
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
// Tables
|
|
458
|
+
table: ({ children }) => (
|
|
459
|
+
<div className="my-6 w-full overflow-x-auto rounded-xl border border-border">
|
|
460
|
+
<table className="w-full text-sm">{children}</table>
|
|
461
|
+
</div>
|
|
462
|
+
),
|
|
463
|
+
thead: ({ children }) => (
|
|
464
|
+
<thead className="bg-muted/50 border-b border-border">
|
|
465
|
+
{children}
|
|
466
|
+
</thead>
|
|
467
|
+
),
|
|
468
|
+
tbody: ({ children }) => <tbody>{children}</tbody>,
|
|
469
|
+
tr: ({ children }) => (
|
|
470
|
+
<tr className="border-b border-border last:border-0 hover:bg-muted/30 transition-colors">
|
|
471
|
+
{children}
|
|
472
|
+
</tr>
|
|
473
|
+
),
|
|
474
|
+
th: ({ children }) => (
|
|
475
|
+
<th className="px-4 py-3 text-left font-semibold text-foreground">
|
|
476
|
+
{children}
|
|
477
|
+
</th>
|
|
478
|
+
),
|
|
479
|
+
td: ({ children }) => (
|
|
480
|
+
<td className="px-4 py-3 text-foreground/90">{children}</td>
|
|
481
|
+
),
|
|
482
|
+
|
|
483
|
+
// Horizontal rule
|
|
484
|
+
hr: () => <hr className="my-8 border-t border-border" />,
|
|
485
|
+
|
|
486
|
+
// Strikethrough
|
|
487
|
+
s: ({ children }) => (
|
|
488
|
+
<s className="line-through text-muted-foreground">{children}</s>
|
|
489
|
+
),
|
|
490
|
+
|
|
491
|
+
// Custom components via data-type attribute
|
|
492
|
+
div: ({ children, node, ...props }) => {
|
|
493
|
+
const dataType = node?.properties?.['dataType'] as string | undefined
|
|
494
|
+
|
|
495
|
+
switch (dataType) {
|
|
496
|
+
case 'codegroup':
|
|
497
|
+
return <CodeGroup>{children}</CodeGroup>
|
|
498
|
+
case 'tabs':
|
|
499
|
+
return <DocsTabs>{children}</DocsTabs>
|
|
500
|
+
case 'tab':
|
|
501
|
+
const tabTitle = node?.properties?.['title'] as string | undefined
|
|
502
|
+
return <DocsTab title={tabTitle || 'Tab'}>{children}</DocsTab>
|
|
503
|
+
case 'steps':
|
|
504
|
+
return <Steps>{children}</Steps>
|
|
505
|
+
case 'step':
|
|
506
|
+
const stepTitle = node?.properties?.['title'] as string | undefined
|
|
507
|
+
return <Step title={stepTitle || ''}>{children}</Step>
|
|
508
|
+
default:
|
|
509
|
+
return <div {...props}>{children}</div>
|
|
510
|
+
}
|
|
511
|
+
},
|
|
512
|
+
}}
|
|
513
|
+
>
|
|
514
|
+
{processedContent}
|
|
515
|
+
</Markdown>
|
|
516
|
+
</div>
|
|
517
|
+
)
|
|
518
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Shared markdown utilities
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Detect if URL is from cloud.inference.sh
|
|
5
|
+
*/
|
|
6
|
+
export function isCloudInferenceUrl(url: string): boolean {
|
|
7
|
+
try {
|
|
8
|
+
const urlObj = new URL(url)
|
|
9
|
+
return urlObj.hostname === 'cloud.inference.sh'
|
|
10
|
+
} catch {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract YouTube video ID from various URL formats
|
|
17
|
+
*/
|
|
18
|
+
export function getYouTubeVideoId(url: string): string | null {
|
|
19
|
+
try {
|
|
20
|
+
if (!url) return null
|
|
21
|
+
const urlObj = new URL(url)
|
|
22
|
+
|
|
23
|
+
// youtu.be format
|
|
24
|
+
if (urlObj.hostname === 'youtu.be') {
|
|
25
|
+
return urlObj.pathname.slice(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// youtube.com formats
|
|
29
|
+
if (urlObj.hostname === 'youtube.com' || urlObj.hostname === 'www.youtube.com') {
|
|
30
|
+
// Watch URLs: youtube.com/watch?v=VIDEO_ID
|
|
31
|
+
if (urlObj.searchParams.has('v')) {
|
|
32
|
+
return urlObj.searchParams.get('v')
|
|
33
|
+
}
|
|
34
|
+
// Embed URLs: youtube.com/embed/VIDEO_ID
|
|
35
|
+
if (urlObj.pathname.startsWith('/embed/')) {
|
|
36
|
+
return urlObj.pathname.split('/')[2]
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return null
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.ogg']
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a URL points to a video file
|
|
50
|
+
*/
|
|
51
|
+
export function isVideoUrl(url: string): boolean {
|
|
52
|
+
try {
|
|
53
|
+
const pathname = new URL(url).pathname.toLowerCase()
|
|
54
|
+
return VIDEO_EXTENSIONS.some(ext => pathname.endsWith(ext))
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Strip HTML comments from markdown content
|
|
62
|
+
*/
|
|
63
|
+
export function stripHtmlComments(content: string): string {
|
|
64
|
+
return content.replace(/<!--[\s\S]*?-->/g, '')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Heuristic: determine if a string is likely Markdown rather than plain text
|
|
69
|
+
*/
|
|
70
|
+
export function isLikelyMarkdown(input: string): boolean {
|
|
71
|
+
if (!input) return false
|
|
72
|
+
let score = 0
|
|
73
|
+
|
|
74
|
+
// Headings (# ...)
|
|
75
|
+
if (/(?:^|\n)\s{0,3}#{1,6} \S/.test(input)) score += 2
|
|
76
|
+
// Bold text
|
|
77
|
+
if (/\*\*[\s\S]*?\*\*/.test(input)) score += 1
|
|
78
|
+
// Italic text
|
|
79
|
+
if (/(?:^|[^*])\*[^*\s][\s\S]*?\*(?:[^*]|$)/.test(input)) score += 1
|
|
80
|
+
// Lists (-, *, +, or ordered)
|
|
81
|
+
if (/(?:^|\n)\s{0,3}(?:[-*+] |\d+\. )\S/.test(input)) score += 1
|
|
82
|
+
// Links/images
|
|
83
|
+
if (/!\[[^\]]*\]\([^)]+\)|\[[^\]]+\]\([^)]+\)/.test(input)) score += 2
|
|
84
|
+
// Code blocks or inline code
|
|
85
|
+
if (/(?:^|\n)```[\s\S]*?```/.test(input) || /`[^`\n]+`/.test(input)) score += 2
|
|
86
|
+
// Blockquote
|
|
87
|
+
if (/(?:^|\n)\s{0,3}>\s?\S/.test(input)) score += 1
|
|
88
|
+
// Tables (GFM)
|
|
89
|
+
if (/(?:^|\n)\|[^\n]*\|\s*(?:\n)\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?/.test(input)) score += 2
|
|
90
|
+
// Horizontal rule
|
|
91
|
+
if (/(?:^|\n)\s{0,3}(?:-{3,}|_{3,}|\*{3,})\s*(?:\n|$)/.test(input)) score += 1
|
|
92
|
+
|
|
93
|
+
return score > 0
|
|
94
|
+
}
|
|
95
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Markdown rendering components
|
|
2
|
+
// Shared between app and web
|
|
3
|
+
|
|
4
|
+
// Renderers
|
|
5
|
+
export { MarkdownRenderer } from './markdown-renderer'
|
|
6
|
+
export { DocsMarkdownRenderer } from './docs-markdown-renderer'
|
|
7
|
+
export type { MarkdownRendererProps } from './markdown-renderer'
|
|
8
|
+
export type { DocsMarkdownRendererProps } from './docs-markdown-renderer'
|
|
9
|
+
|
|
10
|
+
// Components
|
|
11
|
+
export { YouTubeEmbed } from './youtube-embed'
|
|
12
|
+
|
|
13
|
+
// Code blocks - re-export from the code-block module with backward-compatible names
|
|
14
|
+
export { CodeBlock as MarkdownCodeBlock, CompactCodeBlock } from '../components/code-block'
|
|
15
|
+
export type { CodeBlockProps as MarkdownCodeBlockProps } from '../components/code-block'
|
|
16
|
+
|
|
17
|
+
// Helpers
|
|
18
|
+
export {
|
|
19
|
+
isCloudInferenceUrl,
|
|
20
|
+
getYouTubeVideoId,
|
|
21
|
+
stripHtmlComments,
|
|
22
|
+
isLikelyMarkdown,
|
|
23
|
+
} from './helpers'
|
|
24
|
+
|