@djangocfg/ui-tools 2.1.382 → 2.1.383
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/DictationField-U25MEYAL.mjs +4 -0
- package/dist/{DictationField-2ZLQWLYV.mjs.map → DictationField-U25MEYAL.mjs.map} +1 -1
- package/dist/DictationField-XWR5VOID.cjs +13 -0
- package/dist/{DictationField-IPPJ54CU.cjs.map → DictationField-XWR5VOID.cjs.map} +1 -1
- package/dist/{chunk-KMSBGNVC.cjs → chunk-4PFW7MIJ.cjs} +4 -2
- package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
- package/dist/{chunk-4LXG3NBV.mjs → chunk-C2YN6WEO.mjs} +3 -3
- package/dist/chunk-C2YN6WEO.mjs.map +1 -0
- package/dist/index.cjs +139 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +68 -1
- package/dist/index.d.ts +68 -1
- package/dist/index.mjs +141 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -13
- package/src/tools/Chat/index.ts +15 -0
- package/dist/DictationField-2ZLQWLYV.mjs +0 -4
- package/dist/DictationField-IPPJ54CU.cjs +0 -13
- package/dist/chunk-4LXG3NBV.mjs.map +0 -1
- package/dist/chunk-KMSBGNVC.cjs.map +0 -1
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
- package/src/stories/index.ts +0 -63
- package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
- package/src/tools/Chat/stories/01-basic.story.tsx +0 -64
- package/src/tools/Chat/stories/02-bubbles.story.tsx +0 -21
- package/src/tools/Chat/stories/03-tool-calls.story.tsx +0 -59
- package/src/tools/Chat/stories/04-personas.story.tsx +0 -78
- package/src/tools/Chat/stories/05-launcher.story.tsx +0 -321
- package/src/tools/Chat/stories/06-header.story.tsx +0 -147
- package/src/tools/Chat/stories/07-audio-actions.story.tsx +0 -112
- package/src/tools/Chat/stories/shared/Frame.tsx +0 -21
- package/src/tools/Chat/stories/shared/index.ts +0 -5
- package/src/tools/Chat/stories/shared/messages.ts +0 -39
- package/src/tools/Chat/stories/shared/personas.ts +0 -13
- package/src/tools/Chat/stories/shared/seeds.ts +0 -92
- package/src/tools/Chat/stories/shared/transports.ts +0 -36
- package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
- package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
- package/src/tools/Gallery/Gallery.story.tsx +0 -237
- package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
- package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
- package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
- package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
- package/src/tools/Map/Map.story.tsx +0 -458
- package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
- package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
- package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
- package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
- package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +0 -32
- package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +0 -32
- package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +0 -27
- package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +0 -35
- package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +0 -40
- package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +0 -48
- package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +0 -57
- package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +0 -25
- package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +0 -90
- package/src/tools/SpeechRecognition/stories/shared.tsx +0 -123
- package/src/tools/Tour/Tour.story.tsx +0 -279
- package/src/tools/Tree/Tree.story.tsx +0 -620
- package/src/tools/Uploader/Uploader.story.tsx +0 -415
- package/src/tools/VideoPlayer/VideoPlayer.story.tsx +0 -87
|
@@ -1,771 +0,0 @@
|
|
|
1
|
-
import type React from 'react';
|
|
2
|
-
import { defineStory } from '@djangocfg/playground';
|
|
3
|
-
import { MarkdownMessage } from './MarkdownMessage';
|
|
4
|
-
import { ActionRow } from './ActionRow';
|
|
5
|
-
import { ChatMessageRow } from './ChatMessageRow';
|
|
6
|
-
import type { LinkRule } from './types';
|
|
7
|
-
|
|
8
|
-
export default defineStory({
|
|
9
|
-
title: 'Components/Markdown Message',
|
|
10
|
-
component: MarkdownMessage,
|
|
11
|
-
description:
|
|
12
|
-
'Chat markdown renderer. Stories cover the declarative `linkRules` API for handling custom URL schemes (e.g. cmdop://) without hand-writing a custom `a` renderer.',
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
// Canonical mention shape: `[label](cmdop://machine/<uuid>)`. The
|
|
16
|
-
// `@` prefix you sometimes see in composers (`@[label](href)`) is
|
|
17
|
-
// decorative; the chip itself reads as the mention indicator. The
|
|
18
|
-
// rule below strips the leading `@` in `preprocess` so the page
|
|
19
|
-
// doesn't show "@<chip>".
|
|
20
|
-
const CMDOP_CONTENT = `Talk to @[Vps-audi](cmdop://machine/abc-123) about deployment.
|
|
21
|
-
|
|
22
|
-
Inline reference to [Server-2](cmdop://machine/def-456) inside a sentence.
|
|
23
|
-
|
|
24
|
-
Plain links survive: [docs](https://example.com).
|
|
25
|
-
Local file: [file](cmdop://local?path=/tmp/x.md).`;
|
|
26
|
-
|
|
27
|
-
// Chip stand-in. Two variants — `subtle` for chips on neutral cards,
|
|
28
|
-
// `onPrimary` for chips inside a saturated `bg-primary` bubble.
|
|
29
|
-
function MentionChip({
|
|
30
|
-
id,
|
|
31
|
-
label,
|
|
32
|
-
variant = 'subtle',
|
|
33
|
-
}: {
|
|
34
|
-
id: string;
|
|
35
|
-
label: string;
|
|
36
|
-
variant?: 'subtle' | 'onPrimary';
|
|
37
|
-
}) {
|
|
38
|
-
const onClick = () => {
|
|
39
|
-
// eslint-disable-next-line no-console
|
|
40
|
-
console.log('[story] MentionChip clicked', { id, label });
|
|
41
|
-
if (typeof window !== 'undefined') alert(`Chip clicked: ${label} (${id})`);
|
|
42
|
-
};
|
|
43
|
-
const palette =
|
|
44
|
-
variant === 'onPrimary'
|
|
45
|
-
? 'bg-primary-foreground/15 text-primary-foreground hover:bg-primary-foreground/25'
|
|
46
|
-
: 'bg-primary/10 text-primary hover:bg-primary/20';
|
|
47
|
-
return (
|
|
48
|
-
<button
|
|
49
|
-
type="button"
|
|
50
|
-
onClick={onClick}
|
|
51
|
-
className={`inline-flex items-center gap-1 rounded-md px-1.5 py-0.5 text-xs font-sans font-medium align-baseline transition-colors ${palette}`}
|
|
52
|
-
>
|
|
53
|
-
<span
|
|
54
|
-
className={
|
|
55
|
-
variant === 'onPrimary'
|
|
56
|
-
? 'h-1.5 w-1.5 rounded-full bg-primary-foreground/70'
|
|
57
|
-
: 'h-1.5 w-1.5 rounded-full bg-emerald-500'
|
|
58
|
-
}
|
|
59
|
-
aria-hidden
|
|
60
|
-
/>
|
|
61
|
-
<span>{label}</span>
|
|
62
|
-
</button>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Helper used by the chip rule: pull a clean string label from
|
|
67
|
-
// react-markdown's children (string, number, array, or React element).
|
|
68
|
-
function extractLabel(node: React.ReactNode): string {
|
|
69
|
-
if (typeof node === 'string') return node;
|
|
70
|
-
if (typeof node === 'number') return String(node);
|
|
71
|
-
if (Array.isArray(node)) return node.map(extractLabel).join('');
|
|
72
|
-
return '';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function makeRules(chipVariant: 'subtle' | 'onPrimary'): LinkRule[] {
|
|
76
|
-
return [
|
|
77
|
-
{
|
|
78
|
-
name: 'cmdop-machine-mention',
|
|
79
|
-
protocols: ['cmdop'],
|
|
80
|
-
// Strip the decorative leading `@` from `@[label](cmdop://machine/<id>)`
|
|
81
|
-
// so the chip alone reads as the mention indicator.
|
|
82
|
-
preprocess: (s) =>
|
|
83
|
-
s.replace(/(^|[^A-Za-z0-9_])@(\[[^\]]+\]\(cmdop:\/\/machine\/[^)\s]+\))/g, '$1$2'),
|
|
84
|
-
match: (href) => href.startsWith('cmdop://machine/'),
|
|
85
|
-
render: ({ href, children }) => {
|
|
86
|
-
const id = href.slice('cmdop://machine/'.length).trim();
|
|
87
|
-
const label = extractLabel(children) || id;
|
|
88
|
-
return <MentionChip id={id} label={label} variant={chipVariant} />;
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: 'cmdop-local-file',
|
|
93
|
-
protocols: ['cmdop'],
|
|
94
|
-
match: (href) => href.startsWith('cmdop://local'),
|
|
95
|
-
render: ({ href, children }) => (
|
|
96
|
-
<a
|
|
97
|
-
href={href}
|
|
98
|
-
onClick={(e) => {
|
|
99
|
-
e.preventDefault();
|
|
100
|
-
// eslint-disable-next-line no-console
|
|
101
|
-
console.log('[story] local file click', { href });
|
|
102
|
-
if (typeof window !== 'undefined') alert(`Local link: ${href}`);
|
|
103
|
-
}}
|
|
104
|
-
className={
|
|
105
|
-
chipVariant === 'onPrimary'
|
|
106
|
-
? 'text-primary-foreground/90 underline hover:text-primary-foreground'
|
|
107
|
-
: 'text-primary underline hover:text-primary/80'
|
|
108
|
-
}
|
|
109
|
-
>
|
|
110
|
-
{children}
|
|
111
|
-
</a>
|
|
112
|
-
),
|
|
113
|
-
},
|
|
114
|
-
];
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const subtleRules = makeRules('subtle');
|
|
118
|
-
const onPrimaryRules = makeRules('onPrimary');
|
|
119
|
-
|
|
120
|
-
export const LinkRules = () => (
|
|
121
|
-
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
122
|
-
<div>
|
|
123
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
124
|
-
Declarative rules — preferred API
|
|
125
|
-
</h3>
|
|
126
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
127
|
-
Each rule declares its protocol, an optional preprocess pass,
|
|
128
|
-
a match predicate, and a render. <code>MarkdownMessage</code>
|
|
129
|
-
composes them: per-rule sanitize whitelist, custom <code>a</code>
|
|
130
|
-
renderer, and source rewrites all happen behind one prop.
|
|
131
|
-
</p>
|
|
132
|
-
<div className="rounded-lg border border-border bg-card p-4 text-card-foreground">
|
|
133
|
-
<MarkdownMessage
|
|
134
|
-
content={CMDOP_CONTENT}
|
|
135
|
-
isUser={false}
|
|
136
|
-
linkRules={subtleRules}
|
|
137
|
-
/>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
<div>
|
|
142
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
143
|
-
Inside a saturated bubble (<code>bg-primary</code>)
|
|
144
|
-
</h3>
|
|
145
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
146
|
-
Same content, rules carrying the on-primary palette. Theme
|
|
147
|
-
tokens (<code>primary-foreground</code>) keep contrast in light
|
|
148
|
-
and dark themes.
|
|
149
|
-
</p>
|
|
150
|
-
<div className="rounded-xl bg-primary p-4 text-primary-foreground">
|
|
151
|
-
<MarkdownMessage
|
|
152
|
-
content={CMDOP_CONTENT}
|
|
153
|
-
isUser
|
|
154
|
-
linkRules={onPrimaryRules}
|
|
155
|
-
/>
|
|
156
|
-
</div>
|
|
157
|
-
</div>
|
|
158
|
-
|
|
159
|
-
<div>
|
|
160
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
161
|
-
Without rules (control)
|
|
162
|
-
</h3>
|
|
163
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
164
|
-
No rules → react-markdown's default urlTransform strips
|
|
165
|
-
<code> cmdop://</code> hrefs before render. Plain text falls
|
|
166
|
-
through; cmdop links degrade to label-only anchors.
|
|
167
|
-
</p>
|
|
168
|
-
<div className="rounded-lg border border-border bg-card p-4 text-card-foreground">
|
|
169
|
-
<MarkdownMessage content={CMDOP_CONTENT} isUser={false} />
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Chat-style stories — exercise the plain-text fast path so short
|
|
176
|
-
// one-liners and prose paragraphs don't get prose vertical margins
|
|
177
|
-
// inside a chat bubble. Compare against `LongAssistantReply` below
|
|
178
|
-
// to see when the heavier ReactMarkdown pipeline kicks in.
|
|
179
|
-
const SHORT_USER_LINES = [
|
|
180
|
-
'Hi',
|
|
181
|
-
'А?',
|
|
182
|
-
'How do I install cmdop?',
|
|
183
|
-
'Can you ssh into vps-audi and check uptime?',
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
const SHORT_ASSISTANT_LINES = [
|
|
187
|
-
'Sure — what do you need?',
|
|
188
|
-
'Готово, запросил статус.',
|
|
189
|
-
'Done.',
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
// Crafted to make the plain-vs-markdown contrast obvious: contains
|
|
193
|
-
// `*pair*` (italic in markdown), a `#hash` token (lone hash with no
|
|
194
|
-
// space — NOT a heading even in markdown, kept here to show the
|
|
195
|
-
// asymmetry), and embedded newlines that should survive in plain mode.
|
|
196
|
-
const CONTRAST_CONTENT = `Multi
|
|
197
|
-
line
|
|
198
|
-
user message
|
|
199
|
-
with *asterisks*
|
|
200
|
-
and #hash that should NOT format`;
|
|
201
|
-
|
|
202
|
-
const LONG_ASSISTANT_REPLY = `## Установка CMDOP
|
|
203
|
-
|
|
204
|
-
Запусти этот скрипт:
|
|
205
|
-
|
|
206
|
-
\`\`\`bash
|
|
207
|
-
curl -sSL cmdop.com/install.sh | bash
|
|
208
|
-
\`\`\`
|
|
209
|
-
|
|
210
|
-
Дальше:
|
|
211
|
-
|
|
212
|
-
1. **Войти:** \`cmdop login\`
|
|
213
|
-
2. **Подключить машину:** \`cmdop connect\`
|
|
214
|
-
3. **Проверить:** \`cmdop doctor\`
|
|
215
|
-
|
|
216
|
-
Если что-то не работает — скажи.`;
|
|
217
|
-
|
|
218
|
-
// Mermaid payload — exercises the `mermaid` fence branch in
|
|
219
|
-
// components.tsx (lines 101–106). MarkdownMessage hands the code body
|
|
220
|
-
// to <Mermaid> instead of PrettyCode when the fence language is
|
|
221
|
-
// `mermaid`. We pick a small flowchart + a sequence diagram so the
|
|
222
|
-
// story reflects what an LLM typically emits.
|
|
223
|
-
const MERMAID_CONTENT = `Here's the chat session lifecycle:
|
|
224
|
-
|
|
225
|
-
\`\`\`mermaid
|
|
226
|
-
flowchart LR
|
|
227
|
-
A[User types] --> B[ChatProvider]
|
|
228
|
-
B --> C{streaming?}
|
|
229
|
-
C -->|yes| D[CancelMessage]
|
|
230
|
-
C -->|no| E[SendMessage]
|
|
231
|
-
D --> E
|
|
232
|
-
E --> F[Agent stream]
|
|
233
|
-
F --> G[Tokens arrive]
|
|
234
|
-
G --> H[Reducer dispatch]
|
|
235
|
-
H --> I[Bubble re-renders]
|
|
236
|
-
\`\`\`
|
|
237
|
-
|
|
238
|
-
And the cancel flow as a sequence:
|
|
239
|
-
|
|
240
|
-
\`\`\`mermaid
|
|
241
|
-
sequenceDiagram
|
|
242
|
-
participant U as User
|
|
243
|
-
participant C as ChatInput
|
|
244
|
-
participant P as ChatProvider
|
|
245
|
-
participant S as ChatService
|
|
246
|
-
U->>C: Press Enter (mid-stream)
|
|
247
|
-
C->>P: sendMessage(text)
|
|
248
|
-
P->>S: CancelMessage
|
|
249
|
-
S-->>P: STREAM_CANCELLED
|
|
250
|
-
P->>S: SendMessage(text)
|
|
251
|
-
S-->>P: STREAM_START
|
|
252
|
-
\`\`\`
|
|
253
|
-
|
|
254
|
-
The renderer routes \`mermaid\` fences to the diagram component;
|
|
255
|
-
non-mermaid fences (above) keep going through PrettyCode.`;
|
|
256
|
-
|
|
257
|
-
// Kitchen-sink: every block element the renderer can produce, in one
|
|
258
|
-
// payload, so any spacing regression is visible at a glance. Pairs
|
|
259
|
-
// neighbouring elements that are easy to break — `p → hr → p`,
|
|
260
|
-
// `code → list`, `list → table → list`, blockquote-with-paragraphs.
|
|
261
|
-
const KITCHEN_SINK = `# Top-level heading
|
|
262
|
-
|
|
263
|
-
Short intro paragraph that sits right under the heading. The next
|
|
264
|
-
paragraph should have a clear gap from this one, not be glued to it.
|
|
265
|
-
|
|
266
|
-
Second paragraph — verifies \`p + p\` spacing.
|
|
267
|
-
|
|
268
|
-
---
|
|
269
|
-
|
|
270
|
-
After the divider, the next paragraph should still breathe. The
|
|
271
|
-
divider itself is a soft hairline, not a heavy 1px gray bar.
|
|
272
|
-
|
|
273
|
-
## Subheading h2
|
|
274
|
-
|
|
275
|
-
Plain *italic*, **bold**, and \`inline code\` mid-sentence. Inline
|
|
276
|
-
code should sit on the line, not push it taller.
|
|
277
|
-
|
|
278
|
-
### Subheading h3
|
|
279
|
-
|
|
280
|
-
A short list:
|
|
281
|
-
|
|
282
|
-
- Bullet one with a [link](https://example.com)
|
|
283
|
-
- Bullet two with \`inline code\`
|
|
284
|
-
- Bullet three — short
|
|
285
|
-
|
|
286
|
-
A numbered list:
|
|
287
|
-
|
|
288
|
-
1. First step
|
|
289
|
-
2. Second step with **bold** inside
|
|
290
|
-
3. Third step
|
|
291
|
-
|
|
292
|
-
A loose list (paragraph-wrapped \`<li>\`):
|
|
293
|
-
|
|
294
|
-
- First item
|
|
295
|
-
|
|
296
|
-
with a follow-up paragraph that should NOT explode the list.
|
|
297
|
-
|
|
298
|
-
- Second item, also loose.
|
|
299
|
-
|
|
300
|
-
#### Heading h4
|
|
301
|
-
|
|
302
|
-
> Blockquote without italic, with a left border. Lighter colour for
|
|
303
|
-
> de-emphasis. Multi-line quotes wrap cleanly.
|
|
304
|
-
>
|
|
305
|
-
> Second paragraph inside the same blockquote.
|
|
306
|
-
|
|
307
|
-
A code fence:
|
|
308
|
-
|
|
309
|
-
\`\`\`go
|
|
310
|
-
func add(a, b int) int {
|
|
311
|
-
// verifies code block spacing, rounded corners, dark bg
|
|
312
|
-
return a + b
|
|
313
|
-
}
|
|
314
|
-
\`\`\`
|
|
315
|
-
|
|
316
|
-
A table — wraps inside a horizontally scrollable container:
|
|
317
|
-
|
|
318
|
-
| Field | Type | Notes |
|
|
319
|
-
|---|---|---|
|
|
320
|
-
| id | string | UUID |
|
|
321
|
-
| name | string | display name |
|
|
322
|
-
| online | bool | last-heartbeat ≤ 60s |
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
|
-
Closing paragraph after a second divider, to verify spacing holds at
|
|
327
|
-
the very end too.`;
|
|
328
|
-
|
|
329
|
-
function ChatBubble({
|
|
330
|
-
role,
|
|
331
|
-
content,
|
|
332
|
-
rules,
|
|
333
|
-
plainText,
|
|
334
|
-
actions,
|
|
335
|
-
}: {
|
|
336
|
-
role: 'user' | 'assistant';
|
|
337
|
-
content: string;
|
|
338
|
-
rules?: LinkRule[];
|
|
339
|
-
plainText?: boolean;
|
|
340
|
-
/** Show the copy-on-hover ActionRow under the bubble. */
|
|
341
|
-
actions?: 'copy';
|
|
342
|
-
}) {
|
|
343
|
-
const isUser = role === 'user';
|
|
344
|
-
return (
|
|
345
|
-
<ChatMessageRow
|
|
346
|
-
isUser={isUser}
|
|
347
|
-
actions={
|
|
348
|
-
actions === 'copy'
|
|
349
|
-
? (visible) => <ActionRow value={content} isUser={isUser} visible={visible} />
|
|
350
|
-
: undefined
|
|
351
|
-
}
|
|
352
|
-
>
|
|
353
|
-
<div
|
|
354
|
-
className={`max-w-[85%] rounded-xl px-3 py-2 ${
|
|
355
|
-
isUser
|
|
356
|
-
? 'bg-primary text-primary-foreground'
|
|
357
|
-
: 'bg-card text-card-foreground border border-border'
|
|
358
|
-
}`}
|
|
359
|
-
>
|
|
360
|
-
<MarkdownMessage
|
|
361
|
-
content={content}
|
|
362
|
-
isUser={isUser}
|
|
363
|
-
linkRules={rules}
|
|
364
|
-
plainText={plainText}
|
|
365
|
-
/>
|
|
366
|
-
</div>
|
|
367
|
-
</ChatMessageRow>
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
export const ChatBubbles = () => (
|
|
372
|
-
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
373
|
-
<div>
|
|
374
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
375
|
-
User messages — explicit <code>plainText</code>
|
|
376
|
-
</h3>
|
|
377
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
378
|
-
ChatGPT/Claude.ai split: user-typed content stays as-typed.
|
|
379
|
-
<code> *stars* </code> aren't bold; <code># headers </code>
|
|
380
|
-
aren't headings. Newlines preserved via{' '}
|
|
381
|
-
<code>whitespace-pre-wrap</code>.
|
|
382
|
-
</p>
|
|
383
|
-
<div className="rounded-lg border border-border bg-card p-4 space-y-2">
|
|
384
|
-
{SHORT_USER_LINES.map((line, i) => (
|
|
385
|
-
<ChatBubble
|
|
386
|
-
key={`u-${i}`}
|
|
387
|
-
role="user"
|
|
388
|
-
content={line}
|
|
389
|
-
plainText
|
|
390
|
-
rules={onPrimaryRules}
|
|
391
|
-
/>
|
|
392
|
-
))}
|
|
393
|
-
<ChatBubble
|
|
394
|
-
role="user"
|
|
395
|
-
content={'Multi\nline\nuser message\nwith *asterisks*\nand #hash that should NOT format'}
|
|
396
|
-
plainText
|
|
397
|
-
rules={onPrimaryRules}
|
|
398
|
-
/>
|
|
399
|
-
</div>
|
|
400
|
-
</div>
|
|
401
|
-
|
|
402
|
-
<div>
|
|
403
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
404
|
-
Heuristic auto-detect (no <code>plainText</code> prop)
|
|
405
|
-
</h3>
|
|
406
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
407
|
-
Without <code>plainText</code>, <code>looksLikePlainProse</code>
|
|
408
|
-
decides: short / single-paragraph / no markers → flat render;
|
|
409
|
-
long / multi-paragraph / structural → full markdown. Same
|
|
410
|
-
renderer for both branches; only margins differ.
|
|
411
|
-
</p>
|
|
412
|
-
<div className="rounded-lg border border-border bg-card p-4 space-y-2">
|
|
413
|
-
{SHORT_ASSISTANT_LINES.map((line, i) => (
|
|
414
|
-
<ChatBubble
|
|
415
|
-
key={`a-${i}`}
|
|
416
|
-
role="assistant"
|
|
417
|
-
content={line}
|
|
418
|
-
rules={subtleRules}
|
|
419
|
-
/>
|
|
420
|
-
))}
|
|
421
|
-
<ChatBubble
|
|
422
|
-
role="assistant"
|
|
423
|
-
content="Sure — let me check uptime and disk on the machine and report back. Should only take a couple of seconds."
|
|
424
|
-
rules={subtleRules}
|
|
425
|
-
/>
|
|
426
|
-
</div>
|
|
427
|
-
</div>
|
|
428
|
-
|
|
429
|
-
<div>
|
|
430
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
431
|
-
Bubble with mention chips
|
|
432
|
-
</h3>
|
|
433
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
434
|
-
Content with <code>[label](cmdop://...)</code> always goes
|
|
435
|
-
through ReactMarkdown — that's how MentionChip renders.
|
|
436
|
-
</p>
|
|
437
|
-
<div className="rounded-lg border border-border bg-card p-4 space-y-2">
|
|
438
|
-
<ChatBubble
|
|
439
|
-
role="user"
|
|
440
|
-
content="Спроси у @[Vps-audi](cmdop://machine/abc-123) сколько свободного места."
|
|
441
|
-
rules={onPrimaryRules}
|
|
442
|
-
/>
|
|
443
|
-
<ChatBubble
|
|
444
|
-
role="assistant"
|
|
445
|
-
content="Готово — спросил [Vps-audi](cmdop://machine/abc-123)."
|
|
446
|
-
rules={subtleRules}
|
|
447
|
-
/>
|
|
448
|
-
</div>
|
|
449
|
-
</div>
|
|
450
|
-
|
|
451
|
-
<div>
|
|
452
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
453
|
-
Same content, two modes — side-by-side contrast
|
|
454
|
-
</h3>
|
|
455
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
456
|
-
Identical input string. Left bubble forces{' '}
|
|
457
|
-
<code>plainText</code> (user-side default in cmdop):{' '}
|
|
458
|
-
<code>*asterisks*</code> stay literal, <code>#hash</code> stays
|
|
459
|
-
literal, newlines preserved verbatim. Right bubble omits{' '}
|
|
460
|
-
<code>plainText</code> so the heuristic kicks in — short
|
|
461
|
-
content but with <code>*pair*</code> markers, so it routes
|
|
462
|
-
through ReactMarkdown and renders <em>italic</em>.
|
|
463
|
-
</p>
|
|
464
|
-
<div className="grid grid-cols-2 gap-3">
|
|
465
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
466
|
-
<div className="mb-2 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
467
|
-
plainText = true
|
|
468
|
-
</div>
|
|
469
|
-
<ChatBubble
|
|
470
|
-
role="user"
|
|
471
|
-
content={CONTRAST_CONTENT}
|
|
472
|
-
plainText
|
|
473
|
-
rules={onPrimaryRules}
|
|
474
|
-
/>
|
|
475
|
-
</div>
|
|
476
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
477
|
-
<div className="mb-2 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
478
|
-
plainText omitted → heuristic
|
|
479
|
-
</div>
|
|
480
|
-
<ChatBubble
|
|
481
|
-
role="assistant"
|
|
482
|
-
content={CONTRAST_CONTENT}
|
|
483
|
-
rules={subtleRules}
|
|
484
|
-
/>
|
|
485
|
-
</div>
|
|
486
|
-
</div>
|
|
487
|
-
</div>
|
|
488
|
-
|
|
489
|
-
<div>
|
|
490
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
491
|
-
Long assistant reply (multi-paragraph + code + headings)
|
|
492
|
-
</h3>
|
|
493
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
494
|
-
Full markdown pipeline. Tight CSS overrides
|
|
495
|
-
(<code>p+p mt-2</code>, <code>p my-0</code>) keep the bubble
|
|
496
|
-
from looking airy — defaults from Tailwind's prose are too
|
|
497
|
-
loose for chat density.
|
|
498
|
-
</p>
|
|
499
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
500
|
-
<ChatBubble
|
|
501
|
-
role="assistant"
|
|
502
|
-
content={LONG_ASSISTANT_REPLY}
|
|
503
|
-
rules={subtleRules}
|
|
504
|
-
/>
|
|
505
|
-
</div>
|
|
506
|
-
</div>
|
|
507
|
-
</div>
|
|
508
|
-
);
|
|
509
|
-
|
|
510
|
-
// Kitchen-sink: stress-test every element the renderer can emit in a
|
|
511
|
-
// single bubble. If something regresses (e.g. paragraphs collapse, hr
|
|
512
|
-
// shows a heavy gray bar, table loses borders, list items glue to
|
|
513
|
-
// each other), it's visible at a glance.
|
|
514
|
-
//
|
|
515
|
-
// Numbers in MarkdownMessage.tsx come from research on ChatGPT /
|
|
516
|
-
// Claude.ai / Cursor 2025 — see KitchenSink output as the visual
|
|
517
|
-
// contract. Don't tune values by feel; pair-check against this view.
|
|
518
|
-
export const KitchenSink = () => (
|
|
519
|
-
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
520
|
-
<div>
|
|
521
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
522
|
-
Every block element in one bubble
|
|
523
|
-
</h3>
|
|
524
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
525
|
-
Visual contract for the renderer. Heading sizes downscaled
|
|
526
|
-
chat-style (h1 ≈ body), <code>p</code>/<code>ul</code>/
|
|
527
|
-
<code>ol</code>/<code>pre</code>/<code>blockquote</code>/
|
|
528
|
-
<code>hr</code>/<code>table</code> all share a coherent
|
|
529
|
-
rhythm. Loose lists keep their density — no exploded gap on
|
|
530
|
-
paragraph-wrapped <code><li></code>.
|
|
531
|
-
</p>
|
|
532
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
533
|
-
<ChatBubble
|
|
534
|
-
role="assistant"
|
|
535
|
-
content={KITCHEN_SINK}
|
|
536
|
-
rules={subtleRules}
|
|
537
|
-
/>
|
|
538
|
-
</div>
|
|
539
|
-
</div>
|
|
540
|
-
|
|
541
|
-
<div>
|
|
542
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
543
|
-
Same payload on the user-side palette
|
|
544
|
-
</h3>
|
|
545
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
546
|
-
Verifies that <code>prose-invert</code> doesn't break any of
|
|
547
|
-
the new rules — saturated bubbles need the same spacing.
|
|
548
|
-
</p>
|
|
549
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
550
|
-
<ChatBubble
|
|
551
|
-
role="user"
|
|
552
|
-
content={KITCHEN_SINK}
|
|
553
|
-
rules={onPrimaryRules}
|
|
554
|
-
/>
|
|
555
|
-
</div>
|
|
556
|
-
</div>
|
|
557
|
-
</div>
|
|
558
|
-
);
|
|
559
|
-
|
|
560
|
-
// Soft line breaks — `remark-breaks` enabled. LLMs (Claude / GPT) and
|
|
561
|
-
// chat users routinely write "joke punchline / poem stanza / dialogue"
|
|
562
|
-
// blocks separated by a single `\n` rather than a blank line. CommonMark
|
|
563
|
-
// would collapse those into one run-on paragraph; the chat convention
|
|
564
|
-
// (ChatGPT, Slack, Discord, Linear) preserves them as `<br>`. This
|
|
565
|
-
// story is the regression case for that.
|
|
566
|
-
const SOFT_BREAK_JOKE = `Приходит программист к врачу:
|
|
567
|
-
— Доктор, я со всеми общаюсь только через мессенджер. Даже с женой.
|
|
568
|
-
— И что жена говорит?
|
|
569
|
-
— Подождите, сейчас напишу и спрошу… 😄`;
|
|
570
|
-
|
|
571
|
-
const SOFT_BREAK_POEM = `Roses are red,
|
|
572
|
-
Violets are blue,
|
|
573
|
-
This line is short,
|
|
574
|
-
And so are you.`;
|
|
575
|
-
|
|
576
|
-
const SOFT_BREAK_DIALOGUE = `Customer: Why is my code not working?
|
|
577
|
-
Support: Have you tried turning it off and on again?
|
|
578
|
-
Customer: I'm a software engineer.
|
|
579
|
-
Support: Have you tried turning it off and on again?`;
|
|
580
|
-
|
|
581
|
-
export const SoftLineBreaks = () => (
|
|
582
|
-
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
583
|
-
<div>
|
|
584
|
-
<h3 className="mb-2 text-sm font-semibold">Joke (assistant bubble)</h3>
|
|
585
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
586
|
-
Single <code>\n</code> between lines should render as line
|
|
587
|
-
breaks, not collapse onto one row. This was the user-reported
|
|
588
|
-
regression — LLM punchlines surfaced as a wall of text.
|
|
589
|
-
</p>
|
|
590
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
591
|
-
<ChatBubble role="assistant" content={SOFT_BREAK_JOKE} rules={subtleRules} />
|
|
592
|
-
</div>
|
|
593
|
-
</div>
|
|
594
|
-
|
|
595
|
-
<div>
|
|
596
|
-
<h3 className="mb-2 text-sm font-semibold">Poem (user bubble)</h3>
|
|
597
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
598
|
-
Same payload on the saturated user palette. Each line stays on
|
|
599
|
-
its own row.
|
|
600
|
-
</p>
|
|
601
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
602
|
-
<ChatBubble role="user" content={SOFT_BREAK_POEM} rules={onPrimaryRules} />
|
|
603
|
-
</div>
|
|
604
|
-
</div>
|
|
605
|
-
|
|
606
|
-
<div>
|
|
607
|
-
<h3 className="mb-2 text-sm font-semibold">Dialogue (assistant bubble)</h3>
|
|
608
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
609
|
-
Speaker turns separated by single newlines.
|
|
610
|
-
</p>
|
|
611
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
612
|
-
<ChatBubble role="assistant" content={SOFT_BREAK_DIALOGUE} rules={subtleRules} />
|
|
613
|
-
</div>
|
|
614
|
-
</div>
|
|
615
|
-
</div>
|
|
616
|
-
);
|
|
617
|
-
|
|
618
|
-
// Plugin sampler — emoji shortcodes, smart typography, external link
|
|
619
|
-
// hardening. Verifies the three "lightweight chat" plugins added next
|
|
620
|
-
// to remark-breaks: remark-emoji, remark-smartypants, rehype-
|
|
621
|
-
// external-links. Each plugin contributes one transform; this story
|
|
622
|
-
// makes them visible in isolation.
|
|
623
|
-
const PLUGIN_SAMPLER = `### Plugin sampler
|
|
624
|
-
|
|
625
|
-
Emoji shortcodes work inline: :rocket: :tada: :+1: — alongside Unicode 🎉.
|
|
626
|
-
|
|
627
|
-
Smart typography: "double quotes", 'single quotes', em-dash --, ellipsis...
|
|
628
|
-
|
|
629
|
-
External link should open in a new tab: [Anthropic](https://anthropic.com).
|
|
630
|
-
Internal-style link stays in place: [#section](#section).
|
|
631
|
-
|
|
632
|
-
A line above —
|
|
633
|
-
soft break here — should still wrap (one \\n only).
|
|
634
|
-
And then a normal paragraph below.`;
|
|
635
|
-
|
|
636
|
-
export const ChatPlugins = () => (
|
|
637
|
-
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
638
|
-
<div>
|
|
639
|
-
<h3 className="mb-2 text-sm font-semibold">Assistant bubble</h3>
|
|
640
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
641
|
-
Three plugins firing at once: <code>remark-emoji</code>,
|
|
642
|
-
<code>remark-smartypants</code>, <code>rehype-external-links</code>.
|
|
643
|
-
Hover the Anthropic link — it should carry <code>target=_blank</code>
|
|
644
|
-
and <code>rel=noopener noreferrer</code> automatically.
|
|
645
|
-
</p>
|
|
646
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
647
|
-
<ChatBubble role="assistant" content={PLUGIN_SAMPLER} rules={subtleRules} />
|
|
648
|
-
</div>
|
|
649
|
-
</div>
|
|
650
|
-
|
|
651
|
-
<div>
|
|
652
|
-
<h3 className="mb-2 text-sm font-semibold">User bubble</h3>
|
|
653
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
654
|
-
Same payload on the saturated palette.
|
|
655
|
-
</p>
|
|
656
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
657
|
-
<ChatBubble role="user" content={PLUGIN_SAMPLER} rules={onPrimaryRules} />
|
|
658
|
-
</div>
|
|
659
|
-
</div>
|
|
660
|
-
</div>
|
|
661
|
-
);
|
|
662
|
-
|
|
663
|
-
// Mermaid diagrams. Live-renders the fence body via the <Mermaid>
|
|
664
|
-
// tool (lazy-loaded). Useful as a manual smoke test for: (1) the
|
|
665
|
-
// `language === 'mermaid'` branch in components.tsx, (2) the SVG
|
|
666
|
-
// scaling inside a chat-density bubble, (3) interaction with the
|
|
667
|
-
// surrounding markdown spacing rules (paragraph above/below the
|
|
668
|
-
// diagram should keep the same rhythm as around a code fence).
|
|
669
|
-
export const MermaidDiagrams = () => (
|
|
670
|
-
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
671
|
-
<div>
|
|
672
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
673
|
-
Mermaid in an assistant bubble
|
|
674
|
-
</h3>
|
|
675
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
676
|
-
Two diagrams (flowchart + sequence) wrapped in surrounding
|
|
677
|
-
prose. Verifies that <code>```mermaid</code> fences route to{' '}
|
|
678
|
-
<code><Mermaid></code> and that the SVG fits the
|
|
679
|
-
bubble's max-width without breaking the layout.
|
|
680
|
-
</p>
|
|
681
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
682
|
-
<ChatBubble
|
|
683
|
-
role="assistant"
|
|
684
|
-
content={MERMAID_CONTENT}
|
|
685
|
-
rules={subtleRules}
|
|
686
|
-
/>
|
|
687
|
-
</div>
|
|
688
|
-
</div>
|
|
689
|
-
|
|
690
|
-
<div>
|
|
691
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
692
|
-
Same payload on the user-side palette
|
|
693
|
-
</h3>
|
|
694
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
695
|
-
On a saturated <code>bg-primary</code> bubble the diagram
|
|
696
|
-
keeps its own surface — Mermaid renders SVG with its own
|
|
697
|
-
background, independent of the bubble palette.
|
|
698
|
-
</p>
|
|
699
|
-
<div className="rounded-lg border border-border bg-card p-4">
|
|
700
|
-
<ChatBubble
|
|
701
|
-
role="user"
|
|
702
|
-
content={MERMAID_CONTENT}
|
|
703
|
-
rules={onPrimaryRules}
|
|
704
|
-
/>
|
|
705
|
-
</div>
|
|
706
|
-
</div>
|
|
707
|
-
</div>
|
|
708
|
-
);
|
|
709
|
-
|
|
710
|
-
// CopyActions — opt-in action row under the bubble. The `<ActionRow>`
|
|
711
|
-
// component lives outside the bubble (so it doesn't fight the
|
|
712
|
-
// saturated `bg-primary` palette on user messages); the `group` class
|
|
713
|
-
// on the parent flex container makes it appear on hover. On touch
|
|
714
|
-
// devices it stays at 50% opacity so the affordance is discoverable
|
|
715
|
-
// without hover.
|
|
716
|
-
export const CopyActions = () => (
|
|
717
|
-
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
718
|
-
<div>
|
|
719
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
720
|
-
Hover any message to reveal the copy button
|
|
721
|
-
</h3>
|
|
722
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
723
|
-
<code>ActionRow</code> is exported separately from{' '}
|
|
724
|
-
<code>MarkdownMessage</code> on purpose — placement is a
|
|
725
|
-
layout concern of the bubble, not of the renderer. Wrap your
|
|
726
|
-
bubble + <code><ActionRow></code> in a{' '}
|
|
727
|
-
<code>group</code> div, and the row toggles via{' '}
|
|
728
|
-
<code>group-hover</code>. On <code>@media (hover: none)</code>{' '}
|
|
729
|
-
it stays at 50% opacity instead of hiding.
|
|
730
|
-
</p>
|
|
731
|
-
<div className="rounded-lg border border-border bg-card p-4 space-y-8">
|
|
732
|
-
<ChatBubble
|
|
733
|
-
role="user"
|
|
734
|
-
content="How do I install cmdop?"
|
|
735
|
-
plainText
|
|
736
|
-
actions="copy"
|
|
737
|
-
/>
|
|
738
|
-
<ChatBubble
|
|
739
|
-
role="assistant"
|
|
740
|
-
content={`Run this from your terminal:
|
|
741
|
-
|
|
742
|
-
\`\`\`bash
|
|
743
|
-
curl -sSL cmdop.com/install.sh | bash
|
|
744
|
-
\`\`\`
|
|
745
|
-
|
|
746
|
-
It auto-detects your shell and adds \`cmdop\` to PATH.`}
|
|
747
|
-
actions="copy"
|
|
748
|
-
/>
|
|
749
|
-
<ChatBubble
|
|
750
|
-
role="user"
|
|
751
|
-
content={'Multi\nline\nuser message\nto verify copy preserves all newlines'}
|
|
752
|
-
plainText
|
|
753
|
-
actions="copy"
|
|
754
|
-
/>
|
|
755
|
-
</div>
|
|
756
|
-
</div>
|
|
757
|
-
|
|
758
|
-
<div>
|
|
759
|
-
<h3 className="mb-2 text-sm font-semibold">
|
|
760
|
-
Without <code>actions</code> — no extra row, no overhead
|
|
761
|
-
</h3>
|
|
762
|
-
<p className="mb-3 text-xs text-muted-foreground">
|
|
763
|
-
Default behaviour. Existing consumers see no visual change.
|
|
764
|
-
</p>
|
|
765
|
-
<div className="rounded-lg border border-border bg-card p-4 space-y-3">
|
|
766
|
-
<ChatBubble role="user" content="Plain user bubble." plainText />
|
|
767
|
-
<ChatBubble role="assistant" content="Plain assistant bubble." />
|
|
768
|
-
</div>
|
|
769
|
-
</div>
|
|
770
|
-
</div>
|
|
771
|
-
);
|