@djangocfg/ui-tools 2.1.301 → 2.1.303
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/{DocsLayout-MWRKNFXR.mjs → DocsLayout-6ECALRLD.mjs} +3 -3
- package/dist/{DocsLayout-MWRKNFXR.mjs.map → DocsLayout-6ECALRLD.mjs.map} +1 -1
- package/dist/{DocsLayout-NWJUF42A.cjs → DocsLayout-ASPSECYR.cjs} +48 -48
- package/dist/{DocsLayout-NWJUF42A.cjs.map → DocsLayout-ASPSECYR.cjs.map} +1 -1
- package/dist/{Mermaid.client-XFQ74OYN.mjs → Mermaid.client-SXRRI2YW.mjs} +43 -6
- package/dist/Mermaid.client-SXRRI2YW.mjs.map +1 -0
- package/dist/{Mermaid.client-RSWUUHIL.cjs → Mermaid.client-W76R5AKJ.cjs} +43 -6
- package/dist/Mermaid.client-W76R5AKJ.cjs.map +1 -0
- package/dist/{chunk-CKD7GNE5.mjs → chunk-K35OF7OB.mjs} +88 -52
- package/dist/chunk-K35OF7OB.mjs.map +1 -0
- package/dist/{chunk-SEXWBCLX.cjs → chunk-PFKR6ZPZ.cjs} +88 -52
- package/dist/chunk-PFKR6ZPZ.cjs.map +1 -0
- package/dist/index.cjs +11 -11
- package/dist/index.d.cts +20 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.mjs +5 -5
- package/package.json +6 -6
- package/src/components/markdown/MarkdownMessage/CodeBlock.tsx +53 -42
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.story.tsx +422 -0
- package/src/components/markdown/MarkdownMessage/MarkdownMessage.tsx +38 -11
- package/src/components/markdown/MarkdownMessage/components.tsx +69 -14
- package/src/components/markdown/MarkdownMessage/plainText.ts +33 -0
- package/src/components/markdown/MarkdownMessage/types.ts +13 -0
- package/src/tools/Mermaid/Mermaid.client.tsx +10 -1
- package/src/tools/Mermaid/components/MermaidFullscreenModal.tsx +76 -3
- package/src/tools/Mermaid/index.tsx +7 -0
- package/dist/Mermaid.client-RSWUUHIL.cjs.map +0 -1
- package/dist/Mermaid.client-XFQ74OYN.mjs.map +0 -1
- package/dist/chunk-CKD7GNE5.mjs.map +0 -1
- package/dist/chunk-SEXWBCLX.cjs.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/ui-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.303",
|
|
4
4
|
"description": "Heavy React tools with lazy loading - for Electron, Vite, CRA, Next.js apps",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ui-tools",
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"check": "tsc --noEmit"
|
|
92
92
|
},
|
|
93
93
|
"peerDependencies": {
|
|
94
|
-
"@djangocfg/i18n": "^2.1.
|
|
95
|
-
"@djangocfg/ui-core": "^2.1.
|
|
94
|
+
"@djangocfg/i18n": "^2.1.303",
|
|
95
|
+
"@djangocfg/ui-core": "^2.1.303",
|
|
96
96
|
"consola": "^3.4.2",
|
|
97
97
|
"lodash-es": "^4.18.1",
|
|
98
98
|
"lucide-react": "^0.545.0",
|
|
@@ -140,10 +140,10 @@
|
|
|
140
140
|
"@maplibre/maplibre-gl-geocoder": "^1.7.0"
|
|
141
141
|
},
|
|
142
142
|
"devDependencies": {
|
|
143
|
-
"@djangocfg/i18n": "^2.1.
|
|
143
|
+
"@djangocfg/i18n": "^2.1.303",
|
|
144
144
|
"@djangocfg/playground": "workspace:*",
|
|
145
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
146
|
-
"@djangocfg/ui-core": "^2.1.
|
|
145
|
+
"@djangocfg/typescript-config": "^2.1.303",
|
|
146
|
+
"@djangocfg/ui-core": "^2.1.303",
|
|
147
147
|
"@types/lodash-es": "^4.17.12",
|
|
148
148
|
"@types/mapbox__mapbox-gl-draw": "^1.4.8",
|
|
149
149
|
"@types/node": "^24.7.2",
|
|
@@ -10,33 +10,43 @@ interface CodeBlockProps {
|
|
|
10
10
|
isCompact?: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Markdown code block. Renders <PrettyCode> directly — its own
|
|
15
|
+
* FloatingToolbar already carries the language tag and Copy action.
|
|
16
|
+
*
|
|
17
|
+
* Earlier versions stacked an extra <CopyButton> here too, which
|
|
18
|
+
* surfaced as two copy buttons on every code fence. The fallback
|
|
19
|
+
* branch below still ships its own button because the plain <pre>
|
|
20
|
+
* has no toolbar of its own.
|
|
21
|
+
*/
|
|
22
|
+
export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language, isCompact = false }) => {
|
|
15
23
|
const theme = useResolvedTheme();
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
${isUser
|
|
26
|
-
? 'hover:bg-white/20 text-white'
|
|
27
|
-
: 'hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground'
|
|
28
|
-
}
|
|
29
|
-
`}
|
|
30
|
-
title="Copy code"
|
|
31
|
-
/>
|
|
25
|
+
// Hoist derived values out of JSX (COMPONENTS.md
|
|
26
|
+
// "Data Preparation Before Render"):
|
|
27
|
+
// `textSizeClass` — chat-density font tier, so the JSX line
|
|
28
|
+
// stays a one-liner.
|
|
29
|
+
// The `--code` token (ui-core/styles/theme/tokens.css) handles the
|
|
30
|
+
// palette in both themes / both bubble roles; we don't branch on
|
|
31
|
+
// `isUser` here on purpose.
|
|
32
|
+
const textSizeClass = isCompact ? 'text-xs' : 'text-sm';
|
|
32
33
|
|
|
34
|
+
return (
|
|
35
|
+
<div className="my-3">
|
|
33
36
|
<PrettyCode
|
|
34
37
|
data={code}
|
|
35
38
|
language={language}
|
|
36
|
-
className={
|
|
37
|
-
customBg=
|
|
39
|
+
className={textSizeClass}
|
|
40
|
+
customBg="bg-code"
|
|
38
41
|
mode={theme}
|
|
39
42
|
isCompact={isCompact}
|
|
43
|
+
// Disable click-to-scroll-isolation in chat markdown: code
|
|
44
|
+
// fences here are part of an assistant reply, not a docs
|
|
45
|
+
// viewer. Forcing the user to click into a small block to
|
|
46
|
+
// scroll past it interrupts the reading flow. The standalone
|
|
47
|
+
// PrettyCode use cases (docs, diff viewers, long code panes)
|
|
48
|
+
// keep the default `true`.
|
|
49
|
+
scrollIsolation={false}
|
|
40
50
|
/>
|
|
41
51
|
</div>
|
|
42
52
|
);
|
|
@@ -44,26 +54,27 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language, isUser, is
|
|
|
44
54
|
|
|
45
55
|
/** Simple `<pre>` fallback used when CodeBlock throws (lazy module
|
|
46
56
|
* failure, missing PrettyCode peer, etc). */
|
|
47
|
-
export const CodeBlockFallback: React.FC<CodeBlockProps> = ({ code, isUser }) =>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
);
|
|
57
|
+
export const CodeBlockFallback: React.FC<CodeBlockProps> = ({ code, isUser }) => {
|
|
58
|
+
// See CodeBlock above for the spirit of this layout — palette
|
|
59
|
+
// pre-computed before render.
|
|
60
|
+
const copyHoverClass = isUser
|
|
61
|
+
? 'hover:bg-white/20 text-white'
|
|
62
|
+
: 'hover:bg-muted-foreground/20 text-muted-foreground hover:text-foreground';
|
|
63
|
+
const copyButtonClass =
|
|
64
|
+
`absolute top-2 right-2 z-10 opacity-0 group-hover:opacity-100 transition-opacity ` +
|
|
65
|
+
`h-8 w-8 ${copyHoverClass}`;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="relative group my-3">
|
|
69
|
+
<CopyButton
|
|
70
|
+
value={code}
|
|
71
|
+
variant="ghost"
|
|
72
|
+
className={copyButtonClass}
|
|
73
|
+
title="Copy code"
|
|
74
|
+
/>
|
|
75
|
+
<pre className="p-3 rounded text-xs font-mono overflow-x-auto bg-code text-code-foreground border border-code-border">
|
|
76
|
+
<code>{code}</code>
|
|
77
|
+
</pre>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
};
|
|
@@ -169,3 +169,425 @@ export const LinkRules = () => (
|
|
|
169
169
|
</div>
|
|
170
170
|
</div>
|
|
171
171
|
);
|
|
172
|
+
|
|
173
|
+
// Chat-style stories — exercise the plain-text fast path so short
|
|
174
|
+
// one-liners and prose paragraphs don't get prose vertical margins
|
|
175
|
+
// inside a chat bubble. Compare against `LongAssistantReply` below
|
|
176
|
+
// to see when the heavier ReactMarkdown pipeline kicks in.
|
|
177
|
+
const SHORT_USER_LINES = [
|
|
178
|
+
'Hi',
|
|
179
|
+
'А?',
|
|
180
|
+
'How do I install cmdop?',
|
|
181
|
+
'Can you ssh into vps-audi and check uptime?',
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
const SHORT_ASSISTANT_LINES = [
|
|
185
|
+
'Sure — what do you need?',
|
|
186
|
+
'Готово, запросил статус.',
|
|
187
|
+
'Done.',
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
// Crafted to make the plain-vs-markdown contrast obvious: contains
|
|
191
|
+
// `*pair*` (italic in markdown), a `#hash` token (lone hash with no
|
|
192
|
+
// space — NOT a heading even in markdown, kept here to show the
|
|
193
|
+
// asymmetry), and embedded newlines that should survive in plain mode.
|
|
194
|
+
const CONTRAST_CONTENT = `Multi
|
|
195
|
+
line
|
|
196
|
+
user message
|
|
197
|
+
with *asterisks*
|
|
198
|
+
and #hash that should NOT format`;
|
|
199
|
+
|
|
200
|
+
const LONG_ASSISTANT_REPLY = `## Установка CMDOP
|
|
201
|
+
|
|
202
|
+
Запусти этот скрипт:
|
|
203
|
+
|
|
204
|
+
\`\`\`bash
|
|
205
|
+
curl -sSL cmdop.com/install.sh | bash
|
|
206
|
+
\`\`\`
|
|
207
|
+
|
|
208
|
+
Дальше:
|
|
209
|
+
|
|
210
|
+
1. **Войти:** \`cmdop login\`
|
|
211
|
+
2. **Подключить машину:** \`cmdop connect\`
|
|
212
|
+
3. **Проверить:** \`cmdop doctor\`
|
|
213
|
+
|
|
214
|
+
Если что-то не работает — скажи.`;
|
|
215
|
+
|
|
216
|
+
// Mermaid payload — exercises the `mermaid` fence branch in
|
|
217
|
+
// components.tsx (lines 101–106). MarkdownMessage hands the code body
|
|
218
|
+
// to <Mermaid> instead of PrettyCode when the fence language is
|
|
219
|
+
// `mermaid`. We pick a small flowchart + a sequence diagram so the
|
|
220
|
+
// story reflects what an LLM typically emits.
|
|
221
|
+
const MERMAID_CONTENT = `Here's the chat session lifecycle:
|
|
222
|
+
|
|
223
|
+
\`\`\`mermaid
|
|
224
|
+
flowchart LR
|
|
225
|
+
A[User types] --> B[ChatProvider]
|
|
226
|
+
B --> C{streaming?}
|
|
227
|
+
C -->|yes| D[CancelMessage]
|
|
228
|
+
C -->|no| E[SendMessage]
|
|
229
|
+
D --> E
|
|
230
|
+
E --> F[Agent stream]
|
|
231
|
+
F --> G[Tokens arrive]
|
|
232
|
+
G --> H[Reducer dispatch]
|
|
233
|
+
H --> I[Bubble re-renders]
|
|
234
|
+
\`\`\`
|
|
235
|
+
|
|
236
|
+
And the cancel flow as a sequence:
|
|
237
|
+
|
|
238
|
+
\`\`\`mermaid
|
|
239
|
+
sequenceDiagram
|
|
240
|
+
participant U as User
|
|
241
|
+
participant C as ChatInput
|
|
242
|
+
participant P as ChatProvider
|
|
243
|
+
participant S as ChatService
|
|
244
|
+
U->>C: Press Enter (mid-stream)
|
|
245
|
+
C->>P: sendMessage(text)
|
|
246
|
+
P->>S: CancelMessage
|
|
247
|
+
S-->>P: STREAM_CANCELLED
|
|
248
|
+
P->>S: SendMessage(text)
|
|
249
|
+
S-->>P: STREAM_START
|
|
250
|
+
\`\`\`
|
|
251
|
+
|
|
252
|
+
The renderer routes \`mermaid\` fences to the diagram component;
|
|
253
|
+
non-mermaid fences (above) keep going through PrettyCode.`;
|
|
254
|
+
|
|
255
|
+
// Kitchen-sink: every block element the renderer can produce, in one
|
|
256
|
+
// payload, so any spacing regression is visible at a glance. Pairs
|
|
257
|
+
// neighbouring elements that are easy to break — `p → hr → p`,
|
|
258
|
+
// `code → list`, `list → table → list`, blockquote-with-paragraphs.
|
|
259
|
+
const KITCHEN_SINK = `# Top-level heading
|
|
260
|
+
|
|
261
|
+
Short intro paragraph that sits right under the heading. The next
|
|
262
|
+
paragraph should have a clear gap from this one, not be glued to it.
|
|
263
|
+
|
|
264
|
+
Second paragraph — verifies \`p + p\` spacing.
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
After the divider, the next paragraph should still breathe. The
|
|
269
|
+
divider itself is a soft hairline, not a heavy 1px gray bar.
|
|
270
|
+
|
|
271
|
+
## Subheading h2
|
|
272
|
+
|
|
273
|
+
Plain *italic*, **bold**, and \`inline code\` mid-sentence. Inline
|
|
274
|
+
code should sit on the line, not push it taller.
|
|
275
|
+
|
|
276
|
+
### Subheading h3
|
|
277
|
+
|
|
278
|
+
A short list:
|
|
279
|
+
|
|
280
|
+
- Bullet one with a [link](https://example.com)
|
|
281
|
+
- Bullet two with \`inline code\`
|
|
282
|
+
- Bullet three — short
|
|
283
|
+
|
|
284
|
+
A numbered list:
|
|
285
|
+
|
|
286
|
+
1. First step
|
|
287
|
+
2. Second step with **bold** inside
|
|
288
|
+
3. Third step
|
|
289
|
+
|
|
290
|
+
A loose list (paragraph-wrapped \`<li>\`):
|
|
291
|
+
|
|
292
|
+
- First item
|
|
293
|
+
|
|
294
|
+
with a follow-up paragraph that should NOT explode the list.
|
|
295
|
+
|
|
296
|
+
- Second item, also loose.
|
|
297
|
+
|
|
298
|
+
#### Heading h4
|
|
299
|
+
|
|
300
|
+
> Blockquote without italic, with a left border. Lighter colour for
|
|
301
|
+
> de-emphasis. Multi-line quotes wrap cleanly.
|
|
302
|
+
>
|
|
303
|
+
> Second paragraph inside the same blockquote.
|
|
304
|
+
|
|
305
|
+
A code fence:
|
|
306
|
+
|
|
307
|
+
\`\`\`go
|
|
308
|
+
func add(a, b int) int {
|
|
309
|
+
// verifies code block spacing, rounded corners, dark bg
|
|
310
|
+
return a + b
|
|
311
|
+
}
|
|
312
|
+
\`\`\`
|
|
313
|
+
|
|
314
|
+
A table — wraps inside a horizontally scrollable container:
|
|
315
|
+
|
|
316
|
+
| Field | Type | Notes |
|
|
317
|
+
|---|---|---|
|
|
318
|
+
| id | string | UUID |
|
|
319
|
+
| name | string | display name |
|
|
320
|
+
| online | bool | last-heartbeat ≤ 60s |
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
Closing paragraph after a second divider, to verify spacing holds at
|
|
325
|
+
the very end too.`;
|
|
326
|
+
|
|
327
|
+
function ChatBubble({
|
|
328
|
+
role,
|
|
329
|
+
content,
|
|
330
|
+
rules,
|
|
331
|
+
plainText,
|
|
332
|
+
}: {
|
|
333
|
+
role: 'user' | 'assistant';
|
|
334
|
+
content: string;
|
|
335
|
+
rules?: LinkRule[];
|
|
336
|
+
plainText?: boolean;
|
|
337
|
+
}) {
|
|
338
|
+
const isUser = role === 'user';
|
|
339
|
+
return (
|
|
340
|
+
<div className={`flex ${isUser ? 'justify-end' : 'justify-start'}`}>
|
|
341
|
+
<div
|
|
342
|
+
className={`max-w-[85%] rounded-xl px-3 py-2 ${
|
|
343
|
+
isUser
|
|
344
|
+
? 'bg-primary text-primary-foreground'
|
|
345
|
+
: 'bg-card text-card-foreground border border-border'
|
|
346
|
+
}`}
|
|
347
|
+
>
|
|
348
|
+
<MarkdownMessage
|
|
349
|
+
content={content}
|
|
350
|
+
isUser={isUser}
|
|
351
|
+
linkRules={rules}
|
|
352
|
+
plainText={plainText}
|
|
353
|
+
/>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export const ChatBubbles = () => (
|
|
360
|
+
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
361
|
+
<div>
|
|
362
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
363
|
+
User messages — explicit <code>plainText</code>
|
|
364
|
+
</h3>
|
|
365
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
366
|
+
ChatGPT/Claude.ai split: user-typed content stays as-typed.
|
|
367
|
+
<code> *stars* </code> aren't bold; <code># headers </code>
|
|
368
|
+
aren't headings. Newlines preserved via{' '}
|
|
369
|
+
<code>whitespace-pre-wrap</code>.
|
|
370
|
+
</p>
|
|
371
|
+
<div className="rounded-lg border border-border bg-card p-4 space-y-2">
|
|
372
|
+
{SHORT_USER_LINES.map((line, i) => (
|
|
373
|
+
<ChatBubble
|
|
374
|
+
key={`u-${i}`}
|
|
375
|
+
role="user"
|
|
376
|
+
content={line}
|
|
377
|
+
plainText
|
|
378
|
+
rules={onPrimaryRules}
|
|
379
|
+
/>
|
|
380
|
+
))}
|
|
381
|
+
<ChatBubble
|
|
382
|
+
role="user"
|
|
383
|
+
content={'Multi\nline\nuser message\nwith *asterisks*\nand #hash that should NOT format'}
|
|
384
|
+
plainText
|
|
385
|
+
rules={onPrimaryRules}
|
|
386
|
+
/>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div>
|
|
391
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
392
|
+
Heuristic auto-detect (no <code>plainText</code> prop)
|
|
393
|
+
</h3>
|
|
394
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
395
|
+
Without <code>plainText</code>, <code>looksLikePlainProse</code>
|
|
396
|
+
decides: short / single-paragraph / no markers → flat render;
|
|
397
|
+
long / multi-paragraph / structural → full markdown. Same
|
|
398
|
+
renderer for both branches; only margins differ.
|
|
399
|
+
</p>
|
|
400
|
+
<div className="rounded-lg border border-border bg-card p-4 space-y-2">
|
|
401
|
+
{SHORT_ASSISTANT_LINES.map((line, i) => (
|
|
402
|
+
<ChatBubble
|
|
403
|
+
key={`a-${i}`}
|
|
404
|
+
role="assistant"
|
|
405
|
+
content={line}
|
|
406
|
+
rules={subtleRules}
|
|
407
|
+
/>
|
|
408
|
+
))}
|
|
409
|
+
<ChatBubble
|
|
410
|
+
role="assistant"
|
|
411
|
+
content="Sure — let me check uptime and disk on the machine and report back. Should only take a couple of seconds."
|
|
412
|
+
rules={subtleRules}
|
|
413
|
+
/>
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
|
|
417
|
+
<div>
|
|
418
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
419
|
+
Bubble with mention chips
|
|
420
|
+
</h3>
|
|
421
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
422
|
+
Content with <code>[label](cmdop://...)</code> always goes
|
|
423
|
+
through ReactMarkdown — that's how MentionChip renders.
|
|
424
|
+
</p>
|
|
425
|
+
<div className="rounded-lg border border-border bg-card p-4 space-y-2">
|
|
426
|
+
<ChatBubble
|
|
427
|
+
role="user"
|
|
428
|
+
content="Спроси у @[Vps-audi](cmdop://machine/abc-123) сколько свободного места."
|
|
429
|
+
rules={onPrimaryRules}
|
|
430
|
+
/>
|
|
431
|
+
<ChatBubble
|
|
432
|
+
role="assistant"
|
|
433
|
+
content="Готово — спросил [Vps-audi](cmdop://machine/abc-123)."
|
|
434
|
+
rules={subtleRules}
|
|
435
|
+
/>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<div>
|
|
440
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
441
|
+
Same content, two modes — side-by-side contrast
|
|
442
|
+
</h3>
|
|
443
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
444
|
+
Identical input string. Left bubble forces{' '}
|
|
445
|
+
<code>plainText</code> (user-side default in cmdop):{' '}
|
|
446
|
+
<code>*asterisks*</code> stay literal, <code>#hash</code> stays
|
|
447
|
+
literal, newlines preserved verbatim. Right bubble omits{' '}
|
|
448
|
+
<code>plainText</code> so the heuristic kicks in — short
|
|
449
|
+
content but with <code>*pair*</code> markers, so it routes
|
|
450
|
+
through ReactMarkdown and renders <em>italic</em>.
|
|
451
|
+
</p>
|
|
452
|
+
<div className="grid grid-cols-2 gap-3">
|
|
453
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
454
|
+
<div className="mb-2 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
455
|
+
plainText = true
|
|
456
|
+
</div>
|
|
457
|
+
<ChatBubble
|
|
458
|
+
role="user"
|
|
459
|
+
content={CONTRAST_CONTENT}
|
|
460
|
+
plainText
|
|
461
|
+
rules={onPrimaryRules}
|
|
462
|
+
/>
|
|
463
|
+
</div>
|
|
464
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
465
|
+
<div className="mb-2 text-[10px] uppercase tracking-wide text-muted-foreground">
|
|
466
|
+
plainText omitted → heuristic
|
|
467
|
+
</div>
|
|
468
|
+
<ChatBubble
|
|
469
|
+
role="assistant"
|
|
470
|
+
content={CONTRAST_CONTENT}
|
|
471
|
+
rules={subtleRules}
|
|
472
|
+
/>
|
|
473
|
+
</div>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
|
|
477
|
+
<div>
|
|
478
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
479
|
+
Long assistant reply (multi-paragraph + code + headings)
|
|
480
|
+
</h3>
|
|
481
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
482
|
+
Full markdown pipeline. Tight CSS overrides
|
|
483
|
+
(<code>p+p mt-2</code>, <code>p my-0</code>) keep the bubble
|
|
484
|
+
from looking airy — defaults from Tailwind's prose are too
|
|
485
|
+
loose for chat density.
|
|
486
|
+
</p>
|
|
487
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
488
|
+
<ChatBubble
|
|
489
|
+
role="assistant"
|
|
490
|
+
content={LONG_ASSISTANT_REPLY}
|
|
491
|
+
rules={subtleRules}
|
|
492
|
+
/>
|
|
493
|
+
</div>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
// Kitchen-sink: stress-test every element the renderer can emit in a
|
|
499
|
+
// single bubble. If something regresses (e.g. paragraphs collapse, hr
|
|
500
|
+
// shows a heavy gray bar, table loses borders, list items glue to
|
|
501
|
+
// each other), it's visible at a glance.
|
|
502
|
+
//
|
|
503
|
+
// Numbers in MarkdownMessage.tsx come from research on ChatGPT /
|
|
504
|
+
// Claude.ai / Cursor 2025 — see KitchenSink output as the visual
|
|
505
|
+
// contract. Don't tune values by feel; pair-check against this view.
|
|
506
|
+
export const KitchenSink = () => (
|
|
507
|
+
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
508
|
+
<div>
|
|
509
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
510
|
+
Every block element in one bubble
|
|
511
|
+
</h3>
|
|
512
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
513
|
+
Visual contract for the renderer. Heading sizes downscaled
|
|
514
|
+
chat-style (h1 ≈ body), <code>p</code>/<code>ul</code>/
|
|
515
|
+
<code>ol</code>/<code>pre</code>/<code>blockquote</code>/
|
|
516
|
+
<code>hr</code>/<code>table</code> all share a coherent
|
|
517
|
+
rhythm. Loose lists keep their density — no exploded gap on
|
|
518
|
+
paragraph-wrapped <code><li></code>.
|
|
519
|
+
</p>
|
|
520
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
521
|
+
<ChatBubble
|
|
522
|
+
role="assistant"
|
|
523
|
+
content={KITCHEN_SINK}
|
|
524
|
+
rules={subtleRules}
|
|
525
|
+
/>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<div>
|
|
530
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
531
|
+
Same payload on the user-side palette
|
|
532
|
+
</h3>
|
|
533
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
534
|
+
Verifies that <code>prose-invert</code> doesn't break any of
|
|
535
|
+
the new rules — saturated bubbles need the same spacing.
|
|
536
|
+
</p>
|
|
537
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
538
|
+
<ChatBubble
|
|
539
|
+
role="user"
|
|
540
|
+
content={KITCHEN_SINK}
|
|
541
|
+
rules={onPrimaryRules}
|
|
542
|
+
/>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
</div>
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
// Mermaid diagrams. Live-renders the fence body via the <Mermaid>
|
|
549
|
+
// tool (lazy-loaded). Useful as a manual smoke test for: (1) the
|
|
550
|
+
// `language === 'mermaid'` branch in components.tsx, (2) the SVG
|
|
551
|
+
// scaling inside a chat-density bubble, (3) interaction with the
|
|
552
|
+
// surrounding markdown spacing rules (paragraph above/below the
|
|
553
|
+
// diagram should keep the same rhythm as around a code fence).
|
|
554
|
+
export const MermaidDiagrams = () => (
|
|
555
|
+
<div className="mx-auto max-w-2xl space-y-6 p-6">
|
|
556
|
+
<div>
|
|
557
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
558
|
+
Mermaid in an assistant bubble
|
|
559
|
+
</h3>
|
|
560
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
561
|
+
Two diagrams (flowchart + sequence) wrapped in surrounding
|
|
562
|
+
prose. Verifies that <code>```mermaid</code> fences route to{' '}
|
|
563
|
+
<code><Mermaid></code> and that the SVG fits the
|
|
564
|
+
bubble's max-width without breaking the layout.
|
|
565
|
+
</p>
|
|
566
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
567
|
+
<ChatBubble
|
|
568
|
+
role="assistant"
|
|
569
|
+
content={MERMAID_CONTENT}
|
|
570
|
+
rules={subtleRules}
|
|
571
|
+
/>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<div>
|
|
576
|
+
<h3 className="mb-2 text-sm font-semibold">
|
|
577
|
+
Same payload on the user-side palette
|
|
578
|
+
</h3>
|
|
579
|
+
<p className="mb-3 text-xs text-muted-foreground">
|
|
580
|
+
On a saturated <code>bg-primary</code> bubble the diagram
|
|
581
|
+
keeps its own surface — Mermaid renders SVG with its own
|
|
582
|
+
background, independent of the bubble palette.
|
|
583
|
+
</p>
|
|
584
|
+
<div className="rounded-lg border border-border bg-card p-4">
|
|
585
|
+
<ChatBubble
|
|
586
|
+
role="user"
|
|
587
|
+
content={MERMAID_CONTENT}
|
|
588
|
+
rules={onPrimaryRules}
|
|
589
|
+
/>
|
|
590
|
+
</div>
|
|
591
|
+
</div>
|
|
592
|
+
</div>
|
|
593
|
+
);
|
|
@@ -10,7 +10,7 @@ import type { Components } from 'react-markdown';
|
|
|
10
10
|
import { useCollapsibleContent } from '../useCollapsibleContent';
|
|
11
11
|
import type { MarkdownMessageProps } from './types';
|
|
12
12
|
import { buildSchema, buildUrlTransform } from './sanitize';
|
|
13
|
-
import {
|
|
13
|
+
import { looksLikePlainProse } from './plainText';
|
|
14
14
|
import { createMarkdownComponents } from './components';
|
|
15
15
|
import { CollapseToggle } from './CollapseToggle';
|
|
16
16
|
import { applyPreprocess, buildLinkRulesComponent, collectProtocols } from './linkRules';
|
|
@@ -51,6 +51,7 @@ export const MarkdownMessage: React.FC<MarkdownMessageProps> = ({
|
|
|
51
51
|
className = '',
|
|
52
52
|
isUser = false,
|
|
53
53
|
isCompact = false,
|
|
54
|
+
plainText,
|
|
54
55
|
customComponents,
|
|
55
56
|
extraHrefProtocols,
|
|
56
57
|
linkRules,
|
|
@@ -121,17 +122,34 @@ export const MarkdownMessage: React.FC<MarkdownMessageProps> = ({
|
|
|
121
122
|
const textSizeClass = isCompact ? 'text-xs' : 'text-sm';
|
|
122
123
|
const proseClass = isCompact ? 'prose-xs' : 'prose-sm';
|
|
123
124
|
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
// Resolve plain-vs-markdown branch:
|
|
126
|
+
// 1. Caller-passed `plainText` wins outright (explicit beats clever).
|
|
127
|
+
// 2. Caller-supplied non-`a` customComponents force the markdown
|
|
128
|
+
// pipeline — those overrides only matter on real markdown nodes.
|
|
129
|
+
// 3. Otherwise auto-detect via `looksLikePlainProse` (short,
|
|
130
|
+
// single-paragraph, no markdown markers). This is the chat-bubble
|
|
131
|
+
// WhatsApp/Telegram heuristic — "would the human have written
|
|
132
|
+
// this in one keystroke?".
|
|
133
|
+
const customComponentsBeyondLinks = React.useMemo(() => {
|
|
134
|
+
if (!customComponents) return false;
|
|
135
|
+
return Object.keys(customComponents).some((k) => k !== 'a');
|
|
136
|
+
}, [customComponents]);
|
|
137
|
+
const isPlainText = plainText !== undefined
|
|
138
|
+
? plainText
|
|
139
|
+
: !customComponentsBeyondLinks && looksLikePlainProse(displayContent);
|
|
130
140
|
|
|
131
141
|
if (isPlainText) {
|
|
142
|
+
// <div> + whitespace-pre-wrap: respects newlines AND collapses
|
|
143
|
+
// double spaces, which is what users mean when they hit Enter
|
|
144
|
+
// twice. <span> would break flow inside a flex bubble.
|
|
145
|
+
//
|
|
146
|
+
// leading-snug (1.375), not leading-relaxed (1.625) — `pre-wrap`
|
|
147
|
+
// makes every `\n` a hard line break, so the relaxed leading
|
|
148
|
+
// turns multi-line user messages into airy ladders. snug matches
|
|
149
|
+
// WhatsApp/Telegram bubble density.
|
|
132
150
|
return (
|
|
133
|
-
<
|
|
134
|
-
className={`${textSizeClass} leading-
|
|
151
|
+
<div
|
|
152
|
+
className={`${textSizeClass} leading-snug break-words whitespace-pre-wrap ${className}`}
|
|
135
153
|
>
|
|
136
154
|
{displayContent}
|
|
137
155
|
{collapsible && shouldCollapse && (
|
|
@@ -147,7 +165,7 @@ export const MarkdownMessage: React.FC<MarkdownMessageProps> = ({
|
|
|
147
165
|
/>
|
|
148
166
|
</>
|
|
149
167
|
)}
|
|
150
|
-
</
|
|
168
|
+
</div>
|
|
151
169
|
);
|
|
152
170
|
}
|
|
153
171
|
|
|
@@ -157,7 +175,16 @@ export const MarkdownMessage: React.FC<MarkdownMessageProps> = ({
|
|
|
157
175
|
className={`
|
|
158
176
|
prose ${proseClass} max-w-none break-words overflow-hidden ${textSizeClass}
|
|
159
177
|
${isUser ? 'prose-invert' : 'dark:prose-invert'}
|
|
160
|
-
[&>*]:leading-
|
|
178
|
+
[&>*]:leading-relaxed
|
|
179
|
+
[&>*:first-child]:mt-0 [&>*:last-child]:mb-0
|
|
180
|
+
[&_p]:my-2
|
|
181
|
+
[&_ul]:my-2 [&_ol]:my-2 [&_ul]:pl-5 [&_ol]:pl-5
|
|
182
|
+
[&_li]:my-1 [&_li>p]:my-0
|
|
183
|
+
[&_pre]:my-3
|
|
184
|
+
[&_h1]:mt-4 [&_h1]:mb-2 [&_h1]:text-base [&_h1]:font-semibold
|
|
185
|
+
[&_h2]:mt-3.5 [&_h2]:mb-1.5 [&_h2]:text-[15px] [&_h2]:font-semibold
|
|
186
|
+
[&_h3]:mt-3 [&_h3]:mb-1 [&_h3]:text-sm [&_h3]:font-medium
|
|
187
|
+
[&_h4]:mt-3 [&_h4]:mb-1 [&_h4]:text-sm [&_h4]:font-medium
|
|
161
188
|
`}
|
|
162
189
|
style={{
|
|
163
190
|
// Inherit colors from parent — fixes issues with external
|