@djangocfg/ui-tools 2.1.382 → 2.1.384

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/dist/ChatRoot-JVR3M3H2.mjs +5 -0
  2. package/dist/{ChatRoot-6IZFM5HM.mjs.map → ChatRoot-JVR3M3H2.mjs.map} +1 -1
  3. package/dist/ChatRoot-LXIUBOXF.cjs +14 -0
  4. package/dist/{ChatRoot-LW4XNIKP.cjs.map → ChatRoot-LXIUBOXF.cjs.map} +1 -1
  5. package/dist/DictationField-U25MEYAL.mjs +4 -0
  6. package/dist/{DictationField-2ZLQWLYV.mjs.map → DictationField-U25MEYAL.mjs.map} +1 -1
  7. package/dist/DictationField-XWR5VOID.cjs +13 -0
  8. package/dist/{DictationField-IPPJ54CU.cjs.map → DictationField-XWR5VOID.cjs.map} +1 -1
  9. package/dist/{chunk-KMSBGNVC.cjs → chunk-4PFW7MIJ.cjs} +4 -2
  10. package/dist/chunk-4PFW7MIJ.cjs.map +1 -0
  11. package/dist/{chunk-4LXG3NBV.mjs → chunk-C2YN6WEO.mjs} +3 -3
  12. package/dist/chunk-C2YN6WEO.mjs.map +1 -0
  13. package/dist/{chunk-OZAU3QWD.cjs → chunk-HPK3EWBF.cjs} +8 -8
  14. package/dist/chunk-HPK3EWBF.cjs.map +1 -0
  15. package/dist/{chunk-UWVP6LCW.mjs → chunk-PEKBT75W.mjs} +8 -8
  16. package/dist/chunk-PEKBT75W.mjs.map +1 -0
  17. package/dist/index.cjs +192 -55
  18. package/dist/index.cjs.map +1 -1
  19. package/dist/index.d.cts +78 -6
  20. package/dist/index.d.ts +78 -6
  21. package/dist/index.mjs +143 -8
  22. package/dist/index.mjs.map +1 -1
  23. package/package.json +6 -13
  24. package/src/tools/Chat/core/audio/defaults.ts +16 -11
  25. package/src/tools/Chat/core/audio/sounds/error.ts +3 -0
  26. package/src/tools/Chat/core/audio/sounds/mention.ts +3 -0
  27. package/src/tools/Chat/core/audio/sounds/notification.ts +3 -0
  28. package/src/tools/Chat/core/audio/sounds/received.ts +3 -0
  29. package/src/tools/Chat/core/audio/sounds/sent.ts +3 -0
  30. package/src/tools/Chat/core/audio/sounds/start.ts +3 -0
  31. package/src/tools/Chat/index.ts +15 -0
  32. package/src/tools/SpeechRecognition/core/audio/defaults.ts +4 -4
  33. package/dist/ChatRoot-6IZFM5HM.mjs +0 -5
  34. package/dist/ChatRoot-LW4XNIKP.cjs +0 -14
  35. package/dist/DictationField-2ZLQWLYV.mjs +0 -4
  36. package/dist/DictationField-IPPJ54CU.cjs +0 -13
  37. package/dist/chunk-4LXG3NBV.mjs.map +0 -1
  38. package/dist/chunk-KMSBGNVC.cjs.map +0 -1
  39. package/dist/chunk-OZAU3QWD.cjs.map +0 -1
  40. package/dist/chunk-UWVP6LCW.mjs.map +0 -1
  41. package/src/audio-assets.d.ts +0 -8
  42. package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +0 -771
  43. package/src/stories/index.ts +0 -63
  44. package/src/tools/AudioPlayer/AudioPlayer.story.tsx +0 -481
  45. package/src/tools/Chat/core/audio/sounds/error.mp3 +0 -0
  46. package/src/tools/Chat/core/audio/sounds/mention.mp3 +0 -0
  47. package/src/tools/Chat/core/audio/sounds/notification.mp3 +0 -0
  48. package/src/tools/Chat/core/audio/sounds/received.mp3 +0 -0
  49. package/src/tools/Chat/core/audio/sounds/sent.mp3 +0 -0
  50. package/src/tools/Chat/core/audio/sounds/start.mp3 +0 -0
  51. package/src/tools/Chat/stories/01-basic.story.tsx +0 -64
  52. package/src/tools/Chat/stories/02-bubbles.story.tsx +0 -21
  53. package/src/tools/Chat/stories/03-tool-calls.story.tsx +0 -59
  54. package/src/tools/Chat/stories/04-personas.story.tsx +0 -78
  55. package/src/tools/Chat/stories/05-launcher.story.tsx +0 -321
  56. package/src/tools/Chat/stories/06-header.story.tsx +0 -147
  57. package/src/tools/Chat/stories/07-audio-actions.story.tsx +0 -112
  58. package/src/tools/Chat/stories/shared/Frame.tsx +0 -21
  59. package/src/tools/Chat/stories/shared/index.ts +0 -5
  60. package/src/tools/Chat/stories/shared/messages.ts +0 -39
  61. package/src/tools/Chat/stories/shared/personas.ts +0 -13
  62. package/src/tools/Chat/stories/shared/seeds.ts +0 -92
  63. package/src/tools/Chat/stories/shared/transports.ts +0 -36
  64. package/src/tools/CodeEditor/CodeEditor.story.tsx +0 -202
  65. package/src/tools/CronScheduler/CronScheduler.story.tsx +0 -300
  66. package/src/tools/Gallery/Gallery.story.tsx +0 -237
  67. package/src/tools/ImageViewer/ImageViewer.story.tsx +0 -85
  68. package/src/tools/JsonForm/JsonForm.story.tsx +0 -350
  69. package/src/tools/JsonTree/JsonTree.story.tsx +0 -141
  70. package/src/tools/LottiePlayer/LottiePlayer.story.tsx +0 -95
  71. package/src/tools/Map/Map.story.tsx +0 -458
  72. package/src/tools/MarkdownEditor/MarkdownEditor.story.tsx +0 -225
  73. package/src/tools/Mermaid/Mermaid.story.tsx +0 -251
  74. package/src/tools/OpenapiViewer/OpenapiViewer.story.tsx +0 -230
  75. package/src/tools/PrettyCode/PrettyCode.story.tsx +0 -304
  76. package/src/tools/SpeechRecognition/stories/01-basic.story.tsx +0 -32
  77. package/src/tools/SpeechRecognition/stories/02-dictation-field.story.tsx +0 -32
  78. package/src/tools/SpeechRecognition/stories/03-push-to-talk.story.tsx +0 -27
  79. package/src/tools/SpeechRecognition/stories/04-mic-meter.story.tsx +0 -35
  80. package/src/tools/SpeechRecognition/stories/05-custom-engine-http.story.tsx +0 -40
  81. package/src/tools/SpeechRecognition/stories/06-custom-engine-ws.story.tsx +0 -48
  82. package/src/tools/SpeechRecognition/stories/07-language-device.story.tsx +0 -57
  83. package/src/tools/SpeechRecognition/stories/08-errors-permissions.story.tsx +0 -25
  84. package/src/tools/SpeechRecognition/stories/09-chat-voice.story.tsx +0 -90
  85. package/src/tools/SpeechRecognition/stories/shared.tsx +0 -123
  86. package/src/tools/Tour/Tour.story.tsx +0 -279
  87. package/src/tools/Tree/Tree.story.tsx +0 -620
  88. package/src/tools/Uploader/Uploader.story.tsx +0 -415
  89. 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>&lt;li&gt;</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>&lt;Mermaid&gt;</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>&lt;ActionRow&gt;</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
- );