vocs 2.5.2 → 2.6.0
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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/internal/llms.d.ts +5 -0
- package/dist/internal/llms.d.ts.map +1 -1
- package/dist/internal/llms.js +22 -2
- package/dist/internal/llms.js.map +1 -1
- package/dist/internal/mcp.d.ts.map +1 -1
- package/dist/internal/mcp.js +49 -0
- package/dist/internal/mcp.js.map +1 -1
- package/dist/internal/mdx.d.ts +9 -1
- package/dist/internal/mdx.d.ts.map +1 -1
- package/dist/internal/mdx.js +85 -1
- package/dist/internal/mdx.js.map +1 -1
- package/dist/internal/openapi/openapi.d.ts +3 -3
- package/dist/internal/openapi/sidebar.d.ts +2 -2
- package/dist/internal/openapi/sidebar.d.ts.map +1 -1
- package/dist/internal/openapi/sidebar.js +5 -6
- package/dist/internal/openapi/sidebar.js.map +1 -1
- package/dist/internal/search.d.ts.map +1 -1
- package/dist/internal/search.js +6 -2
- package/dist/internal/search.js.map +1 -1
- package/dist/internal/vite-plugins.d.ts.map +1 -1
- package/dist/internal/vite-plugins.js +1 -0
- package/dist/internal/vite-plugins.js.map +1 -1
- package/dist/mdx.d.ts.map +1 -1
- package/dist/mdx.js +4 -1
- package/dist/mdx.js.map +1 -1
- package/dist/react/Prompt.d.ts +8 -0
- package/dist/react/Prompt.d.ts.map +1 -0
- package/dist/react/Prompt.js +16 -0
- package/dist/react/Prompt.js.map +1 -0
- package/dist/react/internal/Prompt.client.d.ts +10 -0
- package/dist/react/internal/Prompt.client.d.ts.map +1 -0
- package/dist/react/internal/Prompt.client.js +50 -0
- package/dist/react/internal/Prompt.client.js.map +1 -0
- package/dist/react/internal/prompt.d.ts +9 -0
- package/dist/react/internal/prompt.d.ts.map +1 -0
- package/dist/react/internal/prompt.js +63 -0
- package/dist/react/internal/prompt.js.map +1 -0
- package/dist/server/openapi/assets.generated.js +3 -3
- package/dist/server/openapi/assets.generated.js.map +1 -1
- package/dist/styles/markdown.css +132 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +2 -0
- package/dist/vite.js.map +1 -1
- package/dist/waku/internal/middleware/md-router.d.ts.map +1 -1
- package/dist/waku/internal/middleware/md-router.js +1 -0
- package/dist/waku/internal/middleware/md-router.js.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -0
- package/src/internal/llms.test.ts +43 -1
- package/src/internal/llms.ts +31 -3
- package/src/internal/mcp.test.ts +75 -1
- package/src/internal/mcp.ts +54 -0
- package/src/internal/mdx.test.ts +55 -2
- package/src/internal/mdx.ts +102 -2
- package/src/internal/openapi/openapi.ts +3 -3
- package/src/internal/openapi/sidebar.test.ts +3 -5
- package/src/internal/openapi/sidebar.ts +7 -8
- package/src/internal/search.test.ts +38 -0
- package/src/internal/search.ts +12 -3
- package/src/internal/vite-plugins.ts +1 -0
- package/src/mdx.tsx +9 -1
- package/src/react/Prompt.test.tsx +27 -0
- package/src/react/Prompt.tsx +47 -0
- package/src/react/internal/Prompt.client.test.tsx +115 -0
- package/src/react/internal/Prompt.client.tsx +160 -0
- package/src/react/internal/prompt.test.ts +45 -0
- package/src/react/internal/prompt.ts +78 -0
- package/src/server/openapi/assets.generated.ts +3 -3
- package/src/styles/markdown.css +132 -0
- package/src/vite.ts +2 -0
- package/src/waku/internal/middleware/md-router.ts +1 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Tooltip } from '@base-ui/react/tooltip'
|
|
4
|
+
import * as React from 'react'
|
|
5
|
+
import LucideScanText from '~icons/lucide/scan-text'
|
|
6
|
+
import RiSparkling2Fill from '~icons/ri/sparkling-2-fill'
|
|
7
|
+
import SimpleIconsClaude from '~icons/simple-icons/claude'
|
|
8
|
+
import SimpleIconsOpenai from '~icons/simple-icons/openai'
|
|
9
|
+
|
|
10
|
+
type State = 'copied' | 'error' | 'idle'
|
|
11
|
+
|
|
12
|
+
export function PromptFrame(props: PromptFrame.Props) {
|
|
13
|
+
const { children, className, value } = props
|
|
14
|
+
const contentId = React.useId()
|
|
15
|
+
const encodedValue = encodeURIComponent(value)
|
|
16
|
+
const [expanded, setExpanded] = React.useState(false)
|
|
17
|
+
const [state, setState] = React.useState<State>('idle')
|
|
18
|
+
|
|
19
|
+
React.useEffect(() => {
|
|
20
|
+
if (state !== 'copied' && state !== 'error') return
|
|
21
|
+
const timeout = setTimeout(() => setState('idle'), 2_000)
|
|
22
|
+
return () => clearTimeout(timeout)
|
|
23
|
+
}, [state])
|
|
24
|
+
|
|
25
|
+
const copy = React.useCallback(() => {
|
|
26
|
+
try {
|
|
27
|
+
navigator.clipboard.writeText(value).then(
|
|
28
|
+
() => setState('copied'),
|
|
29
|
+
() => setState('error'),
|
|
30
|
+
)
|
|
31
|
+
} catch {
|
|
32
|
+
setState('error')
|
|
33
|
+
}
|
|
34
|
+
}, [value])
|
|
35
|
+
|
|
36
|
+
const copyFromFrame = React.useCallback(
|
|
37
|
+
(event: React.MouseEvent<HTMLElement>) => {
|
|
38
|
+
if (event.defaultPrevented) return
|
|
39
|
+
if (event.target instanceof Element && event.target.closest('a, button')) return
|
|
40
|
+
if (window.getSelection()?.isCollapsed === false) return
|
|
41
|
+
copy()
|
|
42
|
+
},
|
|
43
|
+
[copy],
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const label = {
|
|
47
|
+
copied: 'Copied instructions',
|
|
48
|
+
error: 'Copy failed',
|
|
49
|
+
idle: 'Copy instructions for agent',
|
|
50
|
+
}[state]
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
// biome-ignore lint/a11y/useKeyWithClickEvents: _
|
|
54
|
+
<figure
|
|
55
|
+
className={className}
|
|
56
|
+
data-expanded={expanded}
|
|
57
|
+
data-state={state}
|
|
58
|
+
data-v-prompt
|
|
59
|
+
onClick={copyFromFrame}
|
|
60
|
+
>
|
|
61
|
+
<figcaption data-v-prompt-header>
|
|
62
|
+
<button
|
|
63
|
+
aria-label={label}
|
|
64
|
+
data-state={state}
|
|
65
|
+
data-v-prompt-copy
|
|
66
|
+
onClick={copy}
|
|
67
|
+
type="button"
|
|
68
|
+
>
|
|
69
|
+
<RiSparkling2Fill aria-hidden data-v-prompt-icon />
|
|
70
|
+
<span aria-live="polite">{label}</span>
|
|
71
|
+
</button>
|
|
72
|
+
<Tooltip.Provider delay={300}>
|
|
73
|
+
<span data-v-prompt-actions>
|
|
74
|
+
<PromptAction
|
|
75
|
+
label="Open in ChatGPT"
|
|
76
|
+
render={
|
|
77
|
+
// biome-ignore lint/a11y/useAnchorContent: _
|
|
78
|
+
<a
|
|
79
|
+
aria-label="Open in ChatGPT"
|
|
80
|
+
href={`https://chatgpt.com?hints=search&q=${encodedValue}`}
|
|
81
|
+
rel="noopener noreferrer"
|
|
82
|
+
target="_blank"
|
|
83
|
+
/>
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
<SimpleIconsOpenai aria-hidden />
|
|
87
|
+
</PromptAction>
|
|
88
|
+
<PromptAction
|
|
89
|
+
label="Open in Claude"
|
|
90
|
+
render={
|
|
91
|
+
// biome-ignore lint/a11y/useAnchorContent: _
|
|
92
|
+
<a
|
|
93
|
+
aria-label="Open in Claude"
|
|
94
|
+
href={`https://claude.ai/new?q=${encodedValue}`}
|
|
95
|
+
rel="noopener noreferrer"
|
|
96
|
+
target="_blank"
|
|
97
|
+
/>
|
|
98
|
+
}
|
|
99
|
+
>
|
|
100
|
+
<SimpleIconsClaude aria-hidden />
|
|
101
|
+
</PromptAction>
|
|
102
|
+
<PromptAction
|
|
103
|
+
label={expanded ? 'Hide Prompt' : 'View Prompt'}
|
|
104
|
+
render={
|
|
105
|
+
<button
|
|
106
|
+
aria-controls={contentId}
|
|
107
|
+
aria-expanded={expanded}
|
|
108
|
+
aria-label={expanded ? 'Hide Prompt' : 'View Prompt'}
|
|
109
|
+
data-v-prompt-toggle
|
|
110
|
+
onClick={() => setExpanded((expanded) => !expanded)}
|
|
111
|
+
type="button"
|
|
112
|
+
/>
|
|
113
|
+
}
|
|
114
|
+
>
|
|
115
|
+
<LucideScanText aria-hidden />
|
|
116
|
+
</PromptAction>
|
|
117
|
+
</span>
|
|
118
|
+
</Tooltip.Provider>
|
|
119
|
+
</figcaption>
|
|
120
|
+
<div data-v-prompt-content hidden={!expanded} id={contentId}>
|
|
121
|
+
<pre data-v-prompt-body>
|
|
122
|
+
<code>{children}</code>
|
|
123
|
+
</pre>
|
|
124
|
+
</div>
|
|
125
|
+
</figure>
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export declare namespace PromptFrame {
|
|
130
|
+
type Props = {
|
|
131
|
+
children: React.ReactNode
|
|
132
|
+
className?: string | undefined
|
|
133
|
+
value: string
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// biome-ignore lint/correctness/noUnusedVariables: _
|
|
138
|
+
function PromptAction(props: PromptAction.Props) {
|
|
139
|
+
const { children, label, render } = props
|
|
140
|
+
return (
|
|
141
|
+
<Tooltip.Root>
|
|
142
|
+
<Tooltip.Trigger data-v-prompt-action render={render}>
|
|
143
|
+
{children}
|
|
144
|
+
</Tooltip.Trigger>
|
|
145
|
+
<Tooltip.Portal>
|
|
146
|
+
<Tooltip.Positioner className="vocs:z-50" side="top" sideOffset={6}>
|
|
147
|
+
<Tooltip.Popup data-v-prompt-tooltip>{label}</Tooltip.Popup>
|
|
148
|
+
</Tooltip.Positioner>
|
|
149
|
+
</Tooltip.Portal>
|
|
150
|
+
</Tooltip.Root>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
declare namespace PromptAction {
|
|
155
|
+
type Props = {
|
|
156
|
+
children: React.ReactNode
|
|
157
|
+
label: string
|
|
158
|
+
render: React.ReactElement
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { tokenize } from './prompt.js'
|
|
3
|
+
|
|
4
|
+
describe('tokenize', () => {
|
|
5
|
+
it('detects prompt structure without changing its text', () => {
|
|
6
|
+
const value = [
|
|
7
|
+
'Read https://vocs.dev/introduction/getting-started and update `vocs.config.ts`.',
|
|
8
|
+
'',
|
|
9
|
+
'Requirements:',
|
|
10
|
+
'- Preserve existing navigation',
|
|
11
|
+
'- Replace <PROJECT_NAME> with {{package}} and run $COMMAND.',
|
|
12
|
+
].join('\n')
|
|
13
|
+
const tokens = tokenize(value)
|
|
14
|
+
|
|
15
|
+
expect(tokens.map((token) => token.value).join('')).toBe(value)
|
|
16
|
+
expect(tokens.filter((token) => token.type !== 'text')).toEqual([
|
|
17
|
+
{ type: 'url', value: 'https://vocs.dev/introduction/getting-started' },
|
|
18
|
+
{ type: 'code', value: '`vocs.config.ts`' },
|
|
19
|
+
{ type: 'label', value: 'Requirements:' },
|
|
20
|
+
{ type: 'marker', value: '-' },
|
|
21
|
+
{ type: 'marker', value: '-' },
|
|
22
|
+
{ type: 'placeholder', value: '<PROJECT_NAME>' },
|
|
23
|
+
{ type: 'placeholder', value: '{{package}}' },
|
|
24
|
+
{ type: 'placeholder', value: '$COMMAND' },
|
|
25
|
+
])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('keeps trailing URL punctuation outside the link', () => {
|
|
29
|
+
const value = 'Read (https://vocs.dev/docs), then continue.'
|
|
30
|
+
const tokens = tokenize(value)
|
|
31
|
+
|
|
32
|
+
expect(tokens.map((token) => token.value).join('')).toBe(value)
|
|
33
|
+
expect(tokens.find((token) => token.type === 'url')).toEqual({
|
|
34
|
+
type: 'url',
|
|
35
|
+
value: 'https://vocs.dev/docs',
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('does not tokenize prompt syntax inside inline code', () => {
|
|
40
|
+
const value = 'Keep `https://example.com/<PROJECT>` literal.'
|
|
41
|
+
expect(tokenize(value).filter((token) => token.type !== 'text')).toEqual([
|
|
42
|
+
{ type: 'code', value: '`https://example.com/<PROJECT>`' },
|
|
43
|
+
])
|
|
44
|
+
})
|
|
45
|
+
})
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type Token = {
|
|
2
|
+
type: 'code' | 'label' | 'marker' | 'placeholder' | 'text' | 'url'
|
|
3
|
+
value: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const labelRegex = /^(\s*)([A-Z][A-Za-z0-9 /_-]{0,31}:)(?=\s|$)/
|
|
7
|
+
const listMarkerRegex = /^(\s*)([-*+]|\d+[.)])(?=\s)/
|
|
8
|
+
const trailingUrlPunctuationRegex = /[\]})>,.;:!?]+$/
|
|
9
|
+
|
|
10
|
+
export function tokenize(value: string): tokenize.ReturnType {
|
|
11
|
+
const tokens: Token[] = []
|
|
12
|
+
const lines = value.split('\n')
|
|
13
|
+
|
|
14
|
+
for (const [index, line] of lines.entries()) {
|
|
15
|
+
tokenizeLine(line, tokens)
|
|
16
|
+
if (index < lines.length - 1) push(tokens, 'text', '\n')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return tokens
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export declare namespace tokenize {
|
|
23
|
+
type ReturnType = Token[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function tokenizeLine(line: string, tokens: Token[]) {
|
|
27
|
+
const listMarker = line.match(listMarkerRegex)
|
|
28
|
+
if (listMarker) {
|
|
29
|
+
push(tokens, 'text', listMarker[1] ?? '')
|
|
30
|
+
push(tokens, 'marker', listMarker[2] ?? '')
|
|
31
|
+
tokenizeInline(line.slice(listMarker[0].length), tokens)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const label = line.match(labelRegex)
|
|
36
|
+
if (label) {
|
|
37
|
+
push(tokens, 'text', label[1] ?? '')
|
|
38
|
+
push(tokens, 'label', label[2] ?? '')
|
|
39
|
+
tokenizeInline(line.slice(label[0].length), tokens)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
tokenizeInline(line, tokens)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function tokenizeInline(value: string, tokens: Token[]) {
|
|
47
|
+
const tokenRegex =
|
|
48
|
+
/`[^`\n]+`|https?:\/\/[^\s`]+|\{\{[^{}\n]+\}\}|\$\{[^{}\n]+\}|<[A-Z][A-Z0-9_-]*>|\$[A-Z][A-Z0-9_]*/g
|
|
49
|
+
let cursor = 0
|
|
50
|
+
|
|
51
|
+
for (const match of value.matchAll(tokenRegex)) {
|
|
52
|
+
const index = match.index
|
|
53
|
+
const raw = match[0]
|
|
54
|
+
push(tokens, 'text', value.slice(cursor, index))
|
|
55
|
+
|
|
56
|
+
if (raw.startsWith('`')) push(tokens, 'code', raw)
|
|
57
|
+
else if (raw.startsWith('http')) pushUrl(tokens, raw)
|
|
58
|
+
else push(tokens, 'placeholder', raw)
|
|
59
|
+
|
|
60
|
+
cursor = index + raw.length
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
push(tokens, 'text', value.slice(cursor))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function pushUrl(tokens: Token[], value: string) {
|
|
67
|
+
const trailing = value.match(trailingUrlPunctuationRegex)?.[0] ?? ''
|
|
68
|
+
const url = trailing ? value.slice(0, -trailing.length) : value
|
|
69
|
+
push(tokens, 'url', url)
|
|
70
|
+
push(tokens, 'text', trailing)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function push(tokens: Token[], type: Token['type'], value: string) {
|
|
74
|
+
if (!value) return
|
|
75
|
+
const previous = tokens.at(-1)
|
|
76
|
+
if (type === 'text' && previous?.type === 'text') previous.value += value
|
|
77
|
+
else tokens.push({ type, value })
|
|
78
|
+
}
|