@tangle-network/ui 11.51.0 → 11.51.2
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/CHANGELOG.md +12 -0
- package/README.md +4 -0
- package/dist/chat/speaker.d.ts +3 -42
- package/dist/chat/speaker.js +1 -42
- package/dist/markdown/markdown.js +1 -1
- package/package.json +3 -2
- package/src/chat/message-author.test.tsx +0 -17
- package/src/chat/speaker.tsx +2 -65
- package/src/markdown/markdown.stories.tsx +32 -1
- package/src/markdown/markdown.tsx +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tangle-network/ui
|
|
2
2
|
|
|
3
|
+
## 11.51.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- d8990b5: Re-export canonical server-safe speaker grouping from AgentInterface; preserve every UI chat name. Record the adopted GTM heading reduction in the owning drift baseline.
|
|
8
|
+
|
|
9
|
+
## 11.51.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- dfdfc77: Keep streamed blockquotes stable under host typography styles by suppressing generated quotation marks. Explicit quotation marks in Markdown content remain visible.
|
|
14
|
+
|
|
3
15
|
## 11.51.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -26,6 +26,10 @@ export function App() {
|
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
`partitionBySpeaker` and the speaker types in `/chat` re-export the canonical
|
|
30
|
+
`@tangle-network/agent-interface/speaker` contract. Server consumers import that
|
|
31
|
+
entry directly; `SpeakerHeader` remains the UI renderer.
|
|
32
|
+
|
|
29
33
|
## Styling
|
|
30
34
|
|
|
31
35
|
This package ships no CSS. Its components are Tailwind v4 class names in source, and the build emits JavaScript only, so two pieces of consumer setup carry real weight.
|
package/dist/chat/speaker.d.ts
CHANGED
|
@@ -1,45 +1,5 @@
|
|
|
1
1
|
import { HTMLAttributes } from "react";
|
|
2
|
-
|
|
3
|
-
export type SpeakerState = "answering" | "done" | "failed";
|
|
4
|
-
/**
|
|
5
|
-
* A responder of a turn addressed to several agents. In the turn's parts it is
|
|
6
|
-
* a `{ type: "speaker", id, name, handle?, state? }` part written before the
|
|
7
|
-
* responder's answer.
|
|
8
|
-
*/
|
|
9
|
-
export interface Speaker {
|
|
10
|
-
/** The agent's stable id. It keys the avatar's tone and a live part's `speaker` tag. */
|
|
11
|
-
id: string;
|
|
12
|
-
name: string;
|
|
13
|
-
/** What a person types after `@` to address the agent. */
|
|
14
|
-
handle?: string;
|
|
15
|
-
/** Set once the responder ended; absent while it answers and on rows written before responders reported an end. */
|
|
16
|
-
state?: SpeakerState;
|
|
17
|
-
}
|
|
18
|
-
export type SpeakerPart = Speaker & {
|
|
19
|
-
type: "speaker";
|
|
20
|
-
};
|
|
21
|
-
/** One responder's share of a turn: its speaker part and the parts it wrote. */
|
|
22
|
-
export interface SpeakerSection<T> {
|
|
23
|
-
/** The latest speaker part for this responder; null for the parts no speaker owns. */
|
|
24
|
-
speaker: (T & SpeakerPart) | null;
|
|
25
|
-
parts: T[];
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Splits one assistant turn's parts by the agent that wrote them.
|
|
29
|
-
*
|
|
30
|
-
* A stored row holds a speaker part before each responder's answer, so a part
|
|
31
|
-
* belongs to the speaker part before it. A live stream announces every
|
|
32
|
-
* responder first and then interleaves their output, so a part names its owner
|
|
33
|
-
* in `speaker` (the responder's id), which wins over position. A later speaker
|
|
34
|
-
* part with an id already seen replaces that responder's speaker part, which is
|
|
35
|
-
* how its end state arrives, and never opens a second section.
|
|
36
|
-
*
|
|
37
|
-
* Sections keep the order speakers were first announced. Parts no speaker owns
|
|
38
|
-
* come first, in a section whose `speaker` is null; a turn without speaker
|
|
39
|
-
* parts is that one section. No part is dropped, copied or reordered within its
|
|
40
|
-
* section.
|
|
41
|
-
*/
|
|
42
|
-
export declare function partitionBySpeaker<T extends object>(parts: readonly T[]): SpeakerSection<T>[];
|
|
2
|
+
import { Speaker, Speaker as Speaker$1, SpeakerPart, SpeakerSection, SpeakerState, partitionBySpeaker } from "@tangle-network/agent-interface/speaker";
|
|
43
3
|
export interface SpeakerHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
44
4
|
speaker: Speaker;
|
|
45
5
|
/** The turn is still running, so a responder that has not reported an end is answering. */
|
|
@@ -54,4 +14,5 @@ export interface SpeakerHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>,
|
|
|
54
14
|
* failed glyph with the word, both from `RunStatusIcon`, and a finished one
|
|
55
15
|
* nothing, as a settled run row does. `data-speaker-state` carries all three.
|
|
56
16
|
*/
|
|
57
|
-
export declare function SpeakerHeader({ speaker, live, ...props }: SpeakerHeaderProps): import("react").JSX.Element;
|
|
17
|
+
export declare function SpeakerHeader({ speaker, live, ...props }: SpeakerHeaderProps): import("react").JSX.Element;
|
|
18
|
+
export { type Speaker$1 as Speaker, type SpeakerPart, type SpeakerSection, type SpeakerState, partitionBySpeaker };
|
package/dist/chat/speaker.js
CHANGED
|
@@ -1,49 +1,8 @@
|
|
|
1
1
|
import { RunStatusIcon } from "../run/status-meta.js";
|
|
2
2
|
import { MessageAuthor } from "./message-author.js";
|
|
3
3
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { partitionBySpeaker } from "@tangle-network/agent-interface/speaker";
|
|
4
5
|
// src/chat/speaker.tsx
|
|
5
|
-
function isSpeakerPart(part) {
|
|
6
|
-
const record = part;
|
|
7
|
-
return !!record && record.type === "speaker" && typeof record.id === "string" && typeof record.name === "string";
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Splits one assistant turn's parts by the agent that wrote them.
|
|
11
|
-
*
|
|
12
|
-
* A stored row holds a speaker part before each responder's answer, so a part
|
|
13
|
-
* belongs to the speaker part before it. A live stream announces every
|
|
14
|
-
* responder first and then interleaves their output, so a part names its owner
|
|
15
|
-
* in `speaker` (the responder's id), which wins over position. A later speaker
|
|
16
|
-
* part with an id already seen replaces that responder's speaker part, which is
|
|
17
|
-
* how its end state arrives, and never opens a second section.
|
|
18
|
-
*
|
|
19
|
-
* Sections keep the order speakers were first announced. Parts no speaker owns
|
|
20
|
-
* come first, in a section whose `speaker` is null; a turn without speaker
|
|
21
|
-
* parts is that one section. No part is dropped, copied or reordered within its
|
|
22
|
-
* section.
|
|
23
|
-
*/
|
|
24
|
-
function partitionBySpeaker(parts) {
|
|
25
|
-
const sections = /* @__PURE__ */ new Map();
|
|
26
|
-
const unowned = [];
|
|
27
|
-
let current;
|
|
28
|
-
for (const part of parts) {
|
|
29
|
-
if (isSpeakerPart(part)) {
|
|
30
|
-
current = sections.get(part.id);
|
|
31
|
-
if (current) current.speaker = part;
|
|
32
|
-
else sections.set(part.id, current = {
|
|
33
|
-
speaker: part,
|
|
34
|
-
parts: []
|
|
35
|
-
});
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
const tag = part.speaker;
|
|
39
|
-
((typeof tag === "string" ? sections.get(tag) : current)?.parts ?? unowned).push(part);
|
|
40
|
-
}
|
|
41
|
-
const owned = [...sections.values()];
|
|
42
|
-
return unowned.length > 0 || owned.length === 0 ? [{
|
|
43
|
-
speaker: null,
|
|
44
|
-
parts: unowned
|
|
45
|
-
}, ...owned] : owned;
|
|
46
|
-
}
|
|
47
6
|
/**
|
|
48
7
|
* The line above one responder's answer: the author line every participant
|
|
49
8
|
* gets, then the handle a person types to address the agent, then its state.
|
|
@@ -166,7 +166,7 @@ function MarkdownImage({ node: _node, src, alt, className, ...props }) {
|
|
|
166
166
|
*/
|
|
167
167
|
const Markdown = memo(({ children, className, urlTransform, linkChip }) => {
|
|
168
168
|
return /* @__PURE__ */ jsx("div", {
|
|
169
|
-
className: cn("tangle-prose max-w-none text-sm", className),
|
|
169
|
+
className: cn("tangle-prose max-w-none text-sm [&_blockquote_p]:before:content-none [&_blockquote_p]:after:content-none", className),
|
|
170
170
|
children: /* @__PURE__ */ jsx(LinkChipContext.Provider, {
|
|
171
171
|
value: linkChip,
|
|
172
172
|
children: /* @__PURE__ */ jsx(ReactMarkdown, {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/ui",
|
|
3
|
-
"version": "11.51.
|
|
3
|
+
"version": "11.51.2",
|
|
4
4
|
"description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -149,6 +149,7 @@
|
|
|
149
149
|
"@radix-ui/react-switch": "^1.4.1",
|
|
150
150
|
"@radix-ui/react-tabs": "^1.2.1",
|
|
151
151
|
"@radix-ui/react-tooltip": "^1.4.1",
|
|
152
|
+
"@tangle-network/agent-interface": "^5.10.3",
|
|
152
153
|
"class-variance-authority": "^0.7.1",
|
|
153
154
|
"clsx": "^2.1.1",
|
|
154
155
|
"github-slugger": "^2.0.0",
|
|
@@ -166,7 +167,7 @@
|
|
|
166
167
|
"peerDependencies": {
|
|
167
168
|
"@hocuspocus/provider": ">=2.15.0 <5.0.0",
|
|
168
169
|
"@nanostores/react": ">=0.8.4 <3.0.0",
|
|
169
|
-
"@tangle-network/brand": "^1.31.
|
|
170
|
+
"@tangle-network/brand": "^1.31.2",
|
|
170
171
|
"@tiptap/core": ">=3.20.4 <4.0.0",
|
|
171
172
|
"@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
|
|
172
173
|
"@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
|
|
@@ -21,11 +21,6 @@ const segmentedParts: SessionPart[] = [
|
|
|
21
21
|
// The snapshots below were recorded from the components before `author`
|
|
22
22
|
// existed. A transcript that names no author must keep that exact markup.
|
|
23
23
|
describe("a transcript without authors", () => {
|
|
24
|
-
it("renders ChatMessage as before", () => {
|
|
25
|
-
expect(html(<ChatMessage role="user" content="Can I break my lease early?" />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-end"><div class="flex items-center gap-2 px-1 flex-row-reverse"><span class="font-medium text-foreground text-xs">You</span></div><div class="min-w-0 max-w-[85%] space-y-1 rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background"><div class="whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Can I break my lease early?</div></div></div>"`)
|
|
26
|
-
expect(html(<ChatMessage role="assistant" content="It depends on the lease terms." />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-start"><div class="flex items-center gap-2 px-1"><span class="font-medium text-foreground text-xs">Agent</span></div><div class="min-w-0 max-w-[85%] space-y-1 text-foreground"><div class="tangle-prose max-w-none tangle-prose tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]"><p>It depends on the lease terms.</p></div></div></div>"`)
|
|
27
|
-
expect(html(<ChatMessage role="system" content="Session started." />)).toMatchInlineSnapshot(`"<div class="flex flex-col gap-1 items-start"><div class="flex items-center gap-2 px-1"><span class="font-medium text-foreground text-xs">Agent</span></div><div class="min-w-0 max-w-[85%] space-y-1 rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]"><div class="tangle-prose max-w-none tangle-prose tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]"><p>Session started.</p></div></div></div>"`)
|
|
28
|
-
})
|
|
29
24
|
|
|
30
25
|
it("renders UserMessage actions and application notes as before", () => {
|
|
31
26
|
expect(html(<UserMessage content="Can I break my lease early?" actions={<button type="button">Copy</button>} />)).toMatchInlineSnapshot(`"<div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Can I break my lease early?</div></div><div class="flex flex-wrap items-center justify-end gap-1.5 text-sm text-muted-foreground"><button type="button">Copy</button></div></div></div>"`)
|
|
@@ -36,18 +31,6 @@ describe("a transcript without authors", () => {
|
|
|
36
31
|
expect(html(<UserMessage content="Can I break my lease early?" />)).toMatchInlineSnapshot(`"<div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Can I break my lease early?</div></div></div></div>"`)
|
|
37
32
|
})
|
|
38
33
|
|
|
39
|
-
it("renders AgentTimeline messages as before", () => {
|
|
40
|
-
expect(
|
|
41
|
-
html(
|
|
42
|
-
<AgentTimeline
|
|
43
|
-
items={[
|
|
44
|
-
{ id: "u1", kind: "message", role: "user", content: "Can I break my lease early?" },
|
|
45
|
-
{ id: "a1", kind: "message", role: "assistant", content: "It depends on the lease terms." },
|
|
46
|
-
]}
|
|
47
|
-
/>,
|
|
48
|
-
),
|
|
49
|
-
).toMatchInlineSnapshot(`"<div class="mx-auto flex w-full max-w-5xl flex-col px-4 py-4"><div data-timeline-step="user" class=""><div class="flex justify-end"><div class="group flex min-w-0 max-w-[78%] flex-col items-end gap-2"><div class="relative w-full min-w-0 rounded-2xl bg-foreground px-4 py-3 text-background"><div class="break-words whitespace-pre-wrap tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]">Can I break my lease early?</div></div></div></div></div><div data-timeline-step="prose" class="mt-4"><div><div class="tangle-prose max-w-none tangle-prose tangle-transcript-text text-[length:var(--transcript-font-size)] leading-[var(--transcript-line-height)]"><p>It depends on the lease terms.</p></div></div></div></div>"`)
|
|
50
|
-
})
|
|
51
34
|
})
|
|
52
35
|
|
|
53
36
|
const client: ChatAuthor = { id: "client-maria", name: "Maria Lopez", role: "Client" }
|
package/src/chat/speaker.tsx
CHANGED
|
@@ -2,71 +2,8 @@ import type { HTMLAttributes } from "react";
|
|
|
2
2
|
import { RunStatusIcon } from "../run/status-meta";
|
|
3
3
|
import { MessageAuthor } from "./message-author";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
export type
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* A responder of a turn addressed to several agents. In the turn's parts it is
|
|
10
|
-
* a `{ type: "speaker", id, name, handle?, state? }` part written before the
|
|
11
|
-
* responder's answer.
|
|
12
|
-
*/
|
|
13
|
-
export interface Speaker {
|
|
14
|
-
/** The agent's stable id. It keys the avatar's tone and a live part's `speaker` tag. */
|
|
15
|
-
id: string;
|
|
16
|
-
name: string;
|
|
17
|
-
/** What a person types after `@` to address the agent. */
|
|
18
|
-
handle?: string;
|
|
19
|
-
/** Set once the responder ended; absent while it answers and on rows written before responders reported an end. */
|
|
20
|
-
state?: SpeakerState;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export type SpeakerPart = Speaker & { type: "speaker" };
|
|
24
|
-
|
|
25
|
-
/** One responder's share of a turn: its speaker part and the parts it wrote. */
|
|
26
|
-
export interface SpeakerSection<T> {
|
|
27
|
-
/** The latest speaker part for this responder; null for the parts no speaker owns. */
|
|
28
|
-
speaker: (T & SpeakerPart) | null;
|
|
29
|
-
parts: T[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isSpeakerPart<T>(part: T): part is T & SpeakerPart {
|
|
33
|
-
const record = part as { type?: unknown; id?: unknown; name?: unknown } | null;
|
|
34
|
-
return !!record && record.type === "speaker" && typeof record.id === "string" && typeof record.name === "string";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Splits one assistant turn's parts by the agent that wrote them.
|
|
39
|
-
*
|
|
40
|
-
* A stored row holds a speaker part before each responder's answer, so a part
|
|
41
|
-
* belongs to the speaker part before it. A live stream announces every
|
|
42
|
-
* responder first and then interleaves their output, so a part names its owner
|
|
43
|
-
* in `speaker` (the responder's id), which wins over position. A later speaker
|
|
44
|
-
* part with an id already seen replaces that responder's speaker part, which is
|
|
45
|
-
* how its end state arrives, and never opens a second section.
|
|
46
|
-
*
|
|
47
|
-
* Sections keep the order speakers were first announced. Parts no speaker owns
|
|
48
|
-
* come first, in a section whose `speaker` is null; a turn without speaker
|
|
49
|
-
* parts is that one section. No part is dropped, copied or reordered within its
|
|
50
|
-
* section.
|
|
51
|
-
*/
|
|
52
|
-
export function partitionBySpeaker<T extends object>(parts: readonly T[]): SpeakerSection<T>[] {
|
|
53
|
-
const sections = new Map<string, SpeakerSection<T>>();
|
|
54
|
-
const unowned: T[] = [];
|
|
55
|
-
let current: SpeakerSection<T> | undefined;
|
|
56
|
-
for (const part of parts) {
|
|
57
|
-
if (isSpeakerPart(part)) {
|
|
58
|
-
current = sections.get(part.id);
|
|
59
|
-
if (current) current.speaker = part;
|
|
60
|
-
else sections.set(part.id, (current = { speaker: part, parts: [] }));
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
const tag = (part as { speaker?: unknown }).speaker;
|
|
64
|
-
const owner = typeof tag === "string" ? sections.get(tag) : current;
|
|
65
|
-
(owner?.parts ?? unowned).push(part);
|
|
66
|
-
}
|
|
67
|
-
const owned = [...sections.values()];
|
|
68
|
-
return unowned.length > 0 || owned.length === 0 ? [{ speaker: null, parts: unowned }, ...owned] : owned;
|
|
69
|
-
}
|
|
5
|
+
import type { Speaker, SpeakerState } from "@tangle-network/agent-interface/speaker";
|
|
6
|
+
export { partitionBySpeaker, type Speaker, type SpeakerPart, type SpeakerSection, type SpeakerState } from "@tangle-network/agent-interface/speaker";
|
|
70
7
|
|
|
71
8
|
export interface SpeakerHeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
72
9
|
speaker: Speaker;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { useEffect, useState } from 'react'
|
|
2
3
|
import { Markdown } from './markdown'
|
|
3
4
|
import { CodeBlock, CopyButton } from './code-block'
|
|
4
5
|
import { codePresentation } from '../lib/code-presentation'
|
|
@@ -11,7 +12,7 @@ const markdownMeta: Meta<typeof Markdown> = {
|
|
|
11
12
|
parameters: { layout: 'centered', backgrounds: { default: 'dark' } },
|
|
12
13
|
decorators: [
|
|
13
14
|
(Story) => (
|
|
14
|
-
<div className="w-[680px] rounded-xl bg-card p-6 text-foreground">
|
|
15
|
+
<div className="w-[680px] max-w-full rounded-xl bg-card p-6 text-foreground">
|
|
15
16
|
<Story />
|
|
16
17
|
</div>
|
|
17
18
|
),
|
|
@@ -262,3 +263,33 @@ export const CodeBlockLight: Story = {
|
|
|
262
263
|
</div>
|
|
263
264
|
),
|
|
264
265
|
}
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
/** Host typography must not add a moving closing quote to streamed text. */
|
|
269
|
+
function StreamingBlockquoteFixture() {
|
|
270
|
+
const text = '> **A researcher collects the evidence and a writer prepares the draft.**\n> You decide when the work leaves the workspace. Each person sees the source, the proposed action and the result before approving it.'
|
|
271
|
+
const [length, setLength] = useState(0)
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
let interval: ReturnType<typeof setInterval>
|
|
274
|
+
const delay = setTimeout(() => {
|
|
275
|
+
let shown = 0
|
|
276
|
+
interval = setInterval(() => {
|
|
277
|
+
shown = Math.min(shown + 4, text.length); setLength(shown)
|
|
278
|
+
if (shown === text.length) clearInterval(interval)
|
|
279
|
+
}, 30)
|
|
280
|
+
}, 1500)
|
|
281
|
+
return () => { clearTimeout(delay); clearInterval(interval) }
|
|
282
|
+
}, [])
|
|
283
|
+
return <div className="streaming-blockquote-fixture" data-stream-length={length} data-stream-complete={length === text.length}>
|
|
284
|
+
<style>{`@layer base {
|
|
285
|
+
.streaming-blockquote-fixture blockquote p::before { content: open-quote; }
|
|
286
|
+
.streaming-blockquote-fixture blockquote p::after { content: close-quote; }
|
|
287
|
+
}`}</style>
|
|
288
|
+
<Markdown>{text.slice(0, length)}</Markdown>
|
|
289
|
+
</div>
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export const StreamingBlockquote: Story = {
|
|
293
|
+
parameters: { layout: 'fullscreen' },
|
|
294
|
+
render: () => <StreamingBlockquoteFixture />,
|
|
295
|
+
}
|
|
@@ -240,7 +240,7 @@ export interface MarkdownProps {
|
|
|
240
240
|
export const Markdown = memo(({ children, className, urlTransform, linkChip }: MarkdownProps) => {
|
|
241
241
|
return (
|
|
242
242
|
<div
|
|
243
|
-
className={cn("tangle-prose max-w-none text-sm", className)}
|
|
243
|
+
className={cn("tangle-prose max-w-none text-sm [&_blockquote_p]:before:content-none [&_blockquote_p]:after:content-none", className)}
|
|
244
244
|
>
|
|
245
245
|
<LinkChipContext.Provider value={linkChip}>
|
|
246
246
|
<ReactMarkdown
|