@tangle-network/ui 11.23.0 → 11.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/chat/agent-timeline.d.ts +13 -1
- package/dist/chat/agent-timeline.js +25 -4
- package/dist/chat/chat-message.d.ts +13 -1
- package/dist/chat/chat-message.js +9 -5
- package/dist/chat/message-author.d.ts +24 -0
- package/dist/chat/message-author.js +53 -0
- package/dist/chat/user-message.d.ts +12 -2
- package/dist/chat/user-message.js +52 -16
- package/dist/chat.d.ts +3 -1
- package/dist/chat.js +2 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/primitives/pagination.d.ts +47 -9
- package/dist/primitives/pagination.js +50 -20
- package/dist/types/message.d.ts +19 -0
- package/dist/types.d.ts +2 -2
- package/package.json +2 -2
- package/src/chat/agent-timeline.tsx +33 -3
- package/src/chat/chat-message.tsx +37 -10
- package/src/chat/index.ts +2 -0
- package/src/chat/message-author.stories.tsx +170 -0
- package/src/chat/message-author.test.tsx +189 -0
- package/src/chat/message-author.tsx +52 -0
- package/src/chat/user-message.tsx +63 -8
- package/src/markdown/code-block.test.tsx +2 -1
- package/src/primitives/pagination-alert.stories.tsx +35 -0
- package/src/primitives/pagination-alert.test.tsx +90 -0
- package/src/primitives/pagination.tsx +141 -40
- package/src/types/message.ts +20 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import { buttonVariants } from "./button.js";
|
|
3
3
|
import "react";
|
|
4
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
6
6
|
//#region src/primitives/pagination.tsx
|
|
7
7
|
/**
|
|
@@ -24,13 +24,46 @@ const pageButton = cn(buttonVariants({
|
|
|
24
24
|
variant: "ghost",
|
|
25
25
|
size: "sm"
|
|
26
26
|
}), "min-w-8 px-2 tabular-nums");
|
|
27
|
-
|
|
27
|
+
/** A click that opens the link in another tab or window leaves this page where it is. */
|
|
28
|
+
function opensElsewhere(event) {
|
|
29
|
+
return event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
|
|
30
|
+
}
|
|
31
|
+
function Pagination({ page, pageCount, onPageChange, hrefFor, linkComponent: LinkComponent = "a", siblings = 1, className, "aria-label": ariaLabel = "Pagination", ...rest }) {
|
|
28
32
|
if (pageCount <= 1) return null;
|
|
29
33
|
const last = pageCount - 1;
|
|
30
34
|
const current = Math.min(Math.max(page, 0), last);
|
|
31
35
|
const go = (next) => {
|
|
32
36
|
const target = Math.min(Math.max(next, 0), last);
|
|
33
|
-
if (target !== current) onPageChange(target);
|
|
37
|
+
if (target !== current) onPageChange?.(target);
|
|
38
|
+
};
|
|
39
|
+
const control = ({ target, label, className: controlClass, disabled, current: isCurrent, children }) => {
|
|
40
|
+
const ariaCurrent = isCurrent ? "page" : void 0;
|
|
41
|
+
if (!hrefFor) return /* @__PURE__ */ jsx("button", {
|
|
42
|
+
type: "button",
|
|
43
|
+
"aria-label": label,
|
|
44
|
+
"aria-current": ariaCurrent,
|
|
45
|
+
disabled,
|
|
46
|
+
onClick: () => go(target),
|
|
47
|
+
className: controlClass,
|
|
48
|
+
children
|
|
49
|
+
});
|
|
50
|
+
if (disabled) return /* @__PURE__ */ jsx("span", {
|
|
51
|
+
role: "link",
|
|
52
|
+
"aria-label": label,
|
|
53
|
+
"aria-disabled": "true",
|
|
54
|
+
className: cn(controlClass, "pointer-events-none"),
|
|
55
|
+
children
|
|
56
|
+
});
|
|
57
|
+
return /* @__PURE__ */ jsx(LinkComponent, {
|
|
58
|
+
href: hrefFor(target),
|
|
59
|
+
"aria-label": label,
|
|
60
|
+
"aria-current": ariaCurrent,
|
|
61
|
+
onClick: onPageChange ? (event) => {
|
|
62
|
+
if (!opensElsewhere(event)) go(target);
|
|
63
|
+
} : void 0,
|
|
64
|
+
className: controlClass,
|
|
65
|
+
children
|
|
66
|
+
});
|
|
34
67
|
};
|
|
35
68
|
return /* @__PURE__ */ jsx("nav", {
|
|
36
69
|
"aria-label": ariaLabel,
|
|
@@ -39,22 +72,20 @@ function Pagination({ page, pageCount, onPageChange, siblings = 1, className, "a
|
|
|
39
72
|
children: /* @__PURE__ */ jsxs("ul", {
|
|
40
73
|
className: "flex flex-wrap items-center gap-1",
|
|
41
74
|
children: [
|
|
42
|
-
/* @__PURE__ */ jsx("li", { children:
|
|
43
|
-
|
|
44
|
-
|
|
75
|
+
/* @__PURE__ */ jsx("li", { children: control({
|
|
76
|
+
target: current - 1,
|
|
77
|
+
label: "Previous page",
|
|
45
78
|
disabled: current === 0,
|
|
46
|
-
onClick: () => go(current - 1),
|
|
47
79
|
className: cn(pageButton, "gap-1 pl-1.5"),
|
|
48
|
-
children: [/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" }), /* @__PURE__ */ jsx("span", {
|
|
80
|
+
children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ChevronLeft, { "aria-hidden": "true" }), /* @__PURE__ */ jsx("span", {
|
|
49
81
|
className: "hidden sm:inline",
|
|
50
82
|
children: "Previous"
|
|
51
|
-
})]
|
|
83
|
+
})] })
|
|
52
84
|
}) }),
|
|
53
|
-
pageSlots(current, pageCount, siblings).map((slot) => typeof slot === "number" ? /* @__PURE__ */ jsx("li", { children:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
onClick: () => go(slot),
|
|
85
|
+
pageSlots(current, pageCount, siblings).map((slot) => typeof slot === "number" ? /* @__PURE__ */ jsx("li", { children: control({
|
|
86
|
+
target: slot,
|
|
87
|
+
label: `Page ${slot + 1}`,
|
|
88
|
+
current: slot === current,
|
|
58
89
|
className: cn(pageButton, slot === current && "border-border bg-card font-semibold text-foreground hover:bg-card"),
|
|
59
90
|
children: slot + 1
|
|
60
91
|
}) }, slot) : /* @__PURE__ */ jsx("li", {
|
|
@@ -62,16 +93,15 @@ function Pagination({ page, pageCount, onPageChange, siblings = 1, className, "a
|
|
|
62
93
|
className: "flex min-w-8 justify-center text-sm text-muted-foreground",
|
|
63
94
|
children: "…"
|
|
64
95
|
}, slot)),
|
|
65
|
-
/* @__PURE__ */ jsx("li", { children:
|
|
66
|
-
|
|
67
|
-
|
|
96
|
+
/* @__PURE__ */ jsx("li", { children: control({
|
|
97
|
+
target: current + 1,
|
|
98
|
+
label: "Next page",
|
|
68
99
|
disabled: current === last,
|
|
69
|
-
onClick: () => go(current + 1),
|
|
70
100
|
className: cn(pageButton, "gap-1 pr-1.5"),
|
|
71
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
101
|
+
children: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
72
102
|
className: "hidden sm:inline",
|
|
73
103
|
children: "Next"
|
|
74
|
-
}), /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })]
|
|
104
|
+
}), /* @__PURE__ */ jsx(ChevronRight, { "aria-hidden": "true" })] })
|
|
75
105
|
}) })
|
|
76
106
|
]
|
|
77
107
|
})
|
package/dist/types/message.d.ts
CHANGED
|
@@ -12,4 +12,23 @@ export interface SessionMessage {
|
|
|
12
12
|
/** Monotonically increasing insertion index for stable ordering. */
|
|
13
13
|
_insertionIndex?: number;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Who wrote a chat message, for a transcript with more than one person beside
|
|
17
|
+
* the agent: a client, an attorney and the agent in one thread.
|
|
18
|
+
*/
|
|
19
|
+
export interface ChatAuthor {
|
|
20
|
+
/**
|
|
21
|
+
* Stable participant id. A transcript's `viewerId` is compared with it to
|
|
22
|
+
* tell the reader's own messages apart, and it keys the avatar's tone.
|
|
23
|
+
*/
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
/**
|
|
27
|
+
* The participant's part in the conversation, shown as a tag beside the
|
|
28
|
+
* name, such as "Attorney". Unrelated to the message's `role`.
|
|
29
|
+
*/
|
|
30
|
+
role?: string;
|
|
31
|
+
/** Falls back to initials when absent or when the image fails to load. */
|
|
32
|
+
avatarUrl?: string;
|
|
33
|
+
}
|
|
15
34
|
//#endregion
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { SessionMessage } from "./types/message.js";
|
|
1
|
+
import { ChatAuthor, SessionMessage } from "./types/message.js";
|
|
2
2
|
import { ReasoningPart, SessionPart, TextPart, ToolPart, ToolState, ToolStatus, ToolTime } from "./types/parts.js";
|
|
3
3
|
import { AgentBranding } from "./types/branding.js";
|
|
4
4
|
import { CustomToolRenderer, DisplayVariant, ToolDisplayMetadata } from "./types/tool-display.js";
|
|
5
5
|
import { FinalTextPart, GroupedMessage, MessageRun, MessageUser, Run, RunStats, ToolCategory } from "./types/run.js";
|
|
6
|
-
export { AgentBranding, CustomToolRenderer, DisplayVariant, FinalTextPart, GroupedMessage, MessageRun, MessageUser, ReasoningPart, Run, RunStats, SessionMessage, SessionPart, TextPart, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime };
|
|
6
|
+
export { AgentBranding, ChatAuthor, CustomToolRenderer, DisplayVariant, FinalTextPart, GroupedMessage, MessageRun, MessageUser, ReasoningPart, Run, RunStats, SessionMessage, SessionPart, TextPart, ToolCategory, ToolDisplayMetadata, ToolPart, ToolState, ToolStatus, ToolTime };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/ui",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.25.0",
|
|
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,7 +149,7 @@
|
|
|
149
149
|
"peerDependencies": {
|
|
150
150
|
"@hocuspocus/provider": ">=2.15.0 <5.0.0",
|
|
151
151
|
"@nanostores/react": ">=0.8.4 <3.0.0",
|
|
152
|
-
"@tangle-network/brand": "^1.
|
|
152
|
+
"@tangle-network/brand": "^1.17.0",
|
|
153
153
|
"@tiptap/core": ">=3.20.4 <4.0.0",
|
|
154
154
|
"@tiptap/extension-collaboration": ">=3.20.4 <4.0.0",
|
|
155
155
|
"@tiptap/extension-collaboration-caret": ">=3.7.0 <4.0.0",
|
|
@@ -9,11 +9,13 @@ import {
|
|
|
9
9
|
import { focusRing } from "../lib/focus";
|
|
10
10
|
import { cn } from "../lib/utils";
|
|
11
11
|
import { type MessageRole } from "./chat-message";
|
|
12
|
+
import { MessageAuthor } from "./message-author";
|
|
12
13
|
import { UserMessage } from "./user-message";
|
|
13
14
|
import { Markdown } from "../markdown/markdown";
|
|
14
15
|
import { ThinkingIndicator } from "./thinking-indicator";
|
|
15
16
|
import { type ToolCallData } from "../run/tool-call-feed";
|
|
16
17
|
import { ToolCallGroup, ToolCallStep } from "../run/tool-call-step";
|
|
18
|
+
import type { ChatAuthor } from "../types/message";
|
|
17
19
|
import type { ToolPart } from "../types/parts";
|
|
18
20
|
|
|
19
21
|
export type AgentTimelineTone = "default" | "info" | "success" | "warning" | "error";
|
|
@@ -29,6 +31,12 @@ export interface AgentTimelineMessageItem {
|
|
|
29
31
|
* timestamp of its own — the turn's user message dates the exchange. */
|
|
30
32
|
timestamp?: Date;
|
|
31
33
|
after?: ReactNode;
|
|
34
|
+
/**
|
|
35
|
+
* Who wrote the message, when more than one person shares the transcript.
|
|
36
|
+
* A person's message is compared with the timeline's `viewerId`; an agent's
|
|
37
|
+
* author line opens its turn and is not repeated after each tool row.
|
|
38
|
+
*/
|
|
39
|
+
author?: ChatAuthor;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
export interface AgentTimelineToolItem {
|
|
@@ -99,6 +107,11 @@ export interface AgentTimelineProps {
|
|
|
99
107
|
* except user messages) behind a "Show N more steps" toggle. Omit to always
|
|
100
108
|
* show every row. */
|
|
101
109
|
collapseAfter?: number;
|
|
110
|
+
/**
|
|
111
|
+
* The reader's participant id. A user message whose `author` has another
|
|
112
|
+
* id is someone else's: it sits at the start, on a card, under its author.
|
|
113
|
+
*/
|
|
114
|
+
viewerId?: string;
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
const TONE_STYLES: Record<AgentTimelineTone, { card: string; text: string; supportingText: string; icon: typeof Info }> = {
|
|
@@ -169,9 +182,10 @@ const STREAMING_CARET =
|
|
|
169
182
|
"[&>:last-child:not(ul,ol)]:after:ml-0.5 [&>:last-child:not(ul,ol)]:after:inline-block [&>:last-child:not(ul,ol)]:after:h-[1em] [&>:last-child:not(ul,ol)]:after:w-0.5 [&>:last-child:not(ul,ol)]:after:animate-pulse [&>:last-child:not(ul,ol)]:after:rounded-full [&>:last-child:not(ul,ol)]:after:bg-muted-foreground [&>:last-child:not(ul,ol)]:after:align-text-bottom [&>:last-child:not(ul,ol)]:after:content-[''] " +
|
|
170
183
|
"[&>:is(ul,ol):last-child>li:last-child]:after:ml-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:inline-block [&>:is(ul,ol):last-child>li:last-child]:after:h-[1em] [&>:is(ul,ol):last-child>li:last-child]:after:w-0.5 [&>:is(ul,ol):last-child>li:last-child]:after:animate-pulse [&>:is(ul,ol):last-child>li:last-child]:after:rounded-full [&>:is(ul,ol):last-child>li:last-child]:after:bg-muted-foreground [&>:is(ul,ol):last-child>li:last-child]:after:align-text-bottom [&>:is(ul,ol):last-child>li:last-child]:after:content-['']";
|
|
171
184
|
|
|
172
|
-
function AssistantMessage({ item }: { item: AgentTimelineMessageItem }) {
|
|
185
|
+
function AssistantMessage({ item, author }: { item: AgentTimelineMessageItem; author?: ChatAuthor }) {
|
|
173
186
|
return (
|
|
174
187
|
<div>
|
|
188
|
+
{author && <MessageAuthor author={author} className="mb-1.5" />}
|
|
175
189
|
{item.content && (
|
|
176
190
|
<Markdown
|
|
177
191
|
// `length:` types the token: a bare `text-[var(--x)]` compiles to a
|
|
@@ -281,6 +295,7 @@ export function AgentTimeline({
|
|
|
281
295
|
className,
|
|
282
296
|
renderToolActions,
|
|
283
297
|
collapseAfter,
|
|
298
|
+
viewerId,
|
|
284
299
|
}: AgentTimelineProps) {
|
|
285
300
|
const [expanded, setExpanded] = useState(false);
|
|
286
301
|
|
|
@@ -322,12 +337,27 @@ export function AgentTimeline({
|
|
|
322
337
|
hiddenCount = stepItems.length - limit;
|
|
323
338
|
}
|
|
324
339
|
|
|
340
|
+
// An agent's turn is often several prose items split by tool rows. Its
|
|
341
|
+
// author line goes on the first of them, and again only when a person
|
|
342
|
+
// speaks or a different agent takes over.
|
|
343
|
+
const headedAgentMessages = new Set<string>();
|
|
344
|
+
let speakingAgent: string | undefined;
|
|
345
|
+
for (const item of renderList) {
|
|
346
|
+
if (item.kind !== "message") continue;
|
|
347
|
+
if (item.role === "user") {
|
|
348
|
+
speakingAgent = undefined;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
if (item.author && item.author.id !== speakingAgent) headedAgentMessages.add(item.id);
|
|
352
|
+
speakingAgent = item.author?.id;
|
|
353
|
+
}
|
|
354
|
+
|
|
325
355
|
const renderItem = (item: AgentTimelineItem): ReactNode => {
|
|
326
356
|
if (item.kind === "message") {
|
|
327
357
|
return item.role === "user" ? (
|
|
328
|
-
<UserMessage content={item.content} timestamp={item.timestamp} />
|
|
358
|
+
<UserMessage content={item.content} timestamp={item.timestamp} author={item.author} viewerId={viewerId} />
|
|
329
359
|
) : (
|
|
330
|
-
<AssistantMessage item={item} />
|
|
360
|
+
<AssistantMessage item={item} author={headedAgentMessages.has(item.id) ? item.author : undefined} />
|
|
331
361
|
);
|
|
332
362
|
}
|
|
333
363
|
|
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
* ChatMessage — one message in the conversation.
|
|
3
3
|
*
|
|
4
4
|
* Supports user messages (plain text) and assistant messages
|
|
5
|
-
* (rich markdown with inline tool call activity).
|
|
5
|
+
* (rich markdown with inline tool call activity). With `author` and
|
|
6
|
+
* `viewerId` it renders a transcript shared by several people: the reader's
|
|
7
|
+
* own messages stay at the end, everyone else's sit at the start under
|
|
8
|
+
* their author.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { type ReactNode } from "react";
|
|
9
12
|
import { cn } from "../lib/utils";
|
|
10
13
|
import { Markdown } from "../markdown/markdown";
|
|
14
|
+
import type { ChatAuthor } from "../types/message";
|
|
15
|
+
import { isViewerMessage, MessageAuthor } from "./message-author";
|
|
11
16
|
|
|
12
17
|
export type MessageRole = "user" | "assistant" | "system";
|
|
13
18
|
|
|
@@ -27,6 +32,17 @@ export interface ChatMessageProps {
|
|
|
27
32
|
assistantLabel?: string;
|
|
28
33
|
/** Hide the role label row entirely */
|
|
29
34
|
hideRoleLabel?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Who wrote the message, when more than one person shares the transcript.
|
|
37
|
+
* It replaces the role label with the author's avatar, name and role,
|
|
38
|
+
* except on the reader's own messages, which keep `userLabel`.
|
|
39
|
+
*/
|
|
40
|
+
author?: ChatAuthor;
|
|
41
|
+
/**
|
|
42
|
+
* The reader's participant id. A user message whose `author` has another
|
|
43
|
+
* id is someone else's: it sits at the start, on a card, under its author.
|
|
44
|
+
*/
|
|
45
|
+
viewerId?: string;
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
export function ChatMessage({
|
|
@@ -39,23 +55,32 @@ export function ChatMessage({
|
|
|
39
55
|
userLabel = "You",
|
|
40
56
|
assistantLabel = "Agent",
|
|
41
57
|
hideRoleLabel,
|
|
58
|
+
author,
|
|
59
|
+
viewerId,
|
|
42
60
|
}: ChatMessageProps) {
|
|
43
61
|
const isUser = role === "user";
|
|
44
62
|
const isAssistant = role === "assistant";
|
|
63
|
+
const fromViewer = isUser && isViewerMessage(author, viewerId);
|
|
64
|
+
const fromOtherPerson = isUser && !fromViewer;
|
|
65
|
+
const shownAuthor = fromViewer ? undefined : author;
|
|
45
66
|
|
|
46
67
|
return (
|
|
47
68
|
<div
|
|
48
69
|
className={cn(
|
|
49
70
|
"flex flex-col gap-1",
|
|
50
|
-
|
|
71
|
+
fromViewer ? "items-end" : "items-start",
|
|
51
72
|
className,
|
|
52
73
|
)}
|
|
53
74
|
>
|
|
54
75
|
{!hideRoleLabel && (
|
|
55
|
-
<div className={cn("flex items-center gap-2 px-1",
|
|
56
|
-
|
|
57
|
-
{
|
|
58
|
-
|
|
76
|
+
<div className={cn("flex items-center gap-2 px-1", fromViewer && "flex-row-reverse")}>
|
|
77
|
+
{shownAuthor ? (
|
|
78
|
+
<MessageAuthor author={shownAuthor} />
|
|
79
|
+
) : (
|
|
80
|
+
<span className="font-medium text-foreground text-xs">
|
|
81
|
+
{isUser ? userLabel : assistantLabel}
|
|
82
|
+
</span>
|
|
83
|
+
)}
|
|
59
84
|
{timestamp && (
|
|
60
85
|
<span className="text-muted-foreground text-xs">
|
|
61
86
|
{formatTime(timestamp)}
|
|
@@ -67,11 +92,13 @@ export function ChatMessage({
|
|
|
67
92
|
<div
|
|
68
93
|
className={cn(
|
|
69
94
|
"min-w-0 max-w-[85%] space-y-1",
|
|
70
|
-
|
|
95
|
+
fromViewer
|
|
71
96
|
? "rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background"
|
|
72
|
-
:
|
|
73
|
-
? "text-foreground"
|
|
74
|
-
:
|
|
97
|
+
: fromOtherPerson
|
|
98
|
+
? "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-foreground"
|
|
99
|
+
: isAssistant
|
|
100
|
+
? "text-foreground"
|
|
101
|
+
: "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]",
|
|
75
102
|
)}
|
|
76
103
|
>
|
|
77
104
|
{isUser ? (
|
package/src/chat/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ export { ChatContainer, type ChatContainerProps } from "./chat-container";
|
|
|
2
2
|
export { MessageList, type MessageListProps } from "./message-list";
|
|
3
3
|
export { UserMessage, type UserMessageProps } from "./user-message";
|
|
4
4
|
export { ChatMessage, type ChatMessageProps, type MessageRole } from "./chat-message";
|
|
5
|
+
export { MessageAuthor, isViewerMessage, type MessageAuthorProps } from "./message-author";
|
|
6
|
+
export type { ChatAuthor } from "../types/message";
|
|
5
7
|
export { ThinkingIndicator, type ThinkingIndicatorProps } from "./thinking-indicator";
|
|
6
8
|
export {
|
|
7
9
|
AgentTimeline,
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react'
|
|
2
|
+
import { AgentTimeline, type AgentTimelineItem } from './agent-timeline'
|
|
3
|
+
import { ChatMessage } from './chat-message'
|
|
4
|
+
import { MessageAuthor } from './message-author'
|
|
5
|
+
import type { ChatAuthor } from '../types/message'
|
|
6
|
+
|
|
7
|
+
const client: ChatAuthor = { id: 'client-maria-lopez', name: 'Maria Lopez', role: 'Client' }
|
|
8
|
+
const attorney: ChatAuthor = { id: 'attorney-jane-doe', name: 'Jane Doe', role: 'Attorney' }
|
|
9
|
+
const agent: ChatAuthor = { id: 'agent-intake', name: 'Intake assistant', role: 'AI' }
|
|
10
|
+
|
|
11
|
+
const NOW = Date.now()
|
|
12
|
+
const t = (offsetMinutes: number) => new Date(NOW - offsetMinutes * 60 * 1000)
|
|
13
|
+
|
|
14
|
+
const intakeThread: AgentTimelineItem[] = [
|
|
15
|
+
{
|
|
16
|
+
id: 'm1',
|
|
17
|
+
kind: 'message',
|
|
18
|
+
role: 'user',
|
|
19
|
+
author: client,
|
|
20
|
+
timestamp: t(42),
|
|
21
|
+
content:
|
|
22
|
+
'My landlord kept my $2,400 security deposit and never sent an itemized list. I moved out on August 31. What can I do?',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'm2',
|
|
26
|
+
kind: 'message',
|
|
27
|
+
role: 'assistant',
|
|
28
|
+
author: agent,
|
|
29
|
+
content:
|
|
30
|
+
'In California a landlord has 21 days after move-out to return the deposit or send an itemized statement of deductions. That window closed on September 21.\n\nThis looks like a case an attorney should review, so I have shared your summary with Jane Doe.',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'tool-1',
|
|
34
|
+
kind: 'tool',
|
|
35
|
+
call: {
|
|
36
|
+
id: 'tc-1',
|
|
37
|
+
type: 'write',
|
|
38
|
+
label: 'Write',
|
|
39
|
+
status: 'success',
|
|
40
|
+
detail: 'intake/deposit-dispute-summary.md',
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'm3',
|
|
45
|
+
kind: 'message',
|
|
46
|
+
role: 'assistant',
|
|
47
|
+
author: agent,
|
|
48
|
+
content: 'The summary includes your lease dates and the deposit amount.',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'm4',
|
|
52
|
+
kind: 'message',
|
|
53
|
+
role: 'user',
|
|
54
|
+
author: attorney,
|
|
55
|
+
timestamp: t(30),
|
|
56
|
+
content: 'Hi Maria, I am Jane. Did you give the landlord a forwarding address in writing?',
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'm5',
|
|
60
|
+
kind: 'message',
|
|
61
|
+
role: 'user',
|
|
62
|
+
author: client,
|
|
63
|
+
timestamp: t(27),
|
|
64
|
+
content: 'Yes, I emailed it to the property manager on August 28.',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'm6',
|
|
68
|
+
kind: 'message',
|
|
69
|
+
role: 'assistant',
|
|
70
|
+
author: agent,
|
|
71
|
+
content: 'I added the August 28 email to the case file.',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'm7',
|
|
75
|
+
kind: 'message',
|
|
76
|
+
role: 'user',
|
|
77
|
+
author: attorney,
|
|
78
|
+
timestamp: t(20),
|
|
79
|
+
content: 'Thanks. I will draft a demand letter today, and you will have it to review by 5pm.',
|
|
80
|
+
},
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
const meta: Meta<typeof AgentTimeline> = {
|
|
84
|
+
title: 'Chat/Multi-party transcript',
|
|
85
|
+
component: AgentTimeline,
|
|
86
|
+
parameters: {
|
|
87
|
+
layout: 'fullscreen',
|
|
88
|
+
},
|
|
89
|
+
decorators: [
|
|
90
|
+
(Story) => (
|
|
91
|
+
<div className="min-h-screen bg-[var(--bg-root)]">
|
|
92
|
+
<div className="mx-auto max-w-3xl">
|
|
93
|
+
<Story />
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
),
|
|
97
|
+
],
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default meta
|
|
101
|
+
type Story = StoryObj<typeof AgentTimeline>
|
|
102
|
+
|
|
103
|
+
/** The client's screen: their own messages at the end, the attorney and the agent at the start. */
|
|
104
|
+
export const ClientView: Story = {
|
|
105
|
+
args: { items: intakeThread, viewerId: client.id },
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The same thread on the attorney's screen. */
|
|
109
|
+
export const AttorneyView: Story = {
|
|
110
|
+
args: { items: intakeThread, viewerId: attorney.id },
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** No viewer, as in an audit log: every person is someone else, so every message is named. */
|
|
114
|
+
export const ObserverView: Story = {
|
|
115
|
+
args: { items: intakeThread },
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** `ChatMessage` with always-on labels: the reader keeps "You", everyone else is named. */
|
|
119
|
+
export const LabeledMessages: Story = {
|
|
120
|
+
render: () => (
|
|
121
|
+
<div className="space-y-4 p-4">
|
|
122
|
+
<ChatMessage role="user" author={client} viewerId={client.id} timestamp={t(42)} content="My landlord kept my $2,400 security deposit and never sent an itemized list." />
|
|
123
|
+
<ChatMessage role="assistant" author={agent} content="That window closed on September 21. I have shared your summary with Jane Doe." />
|
|
124
|
+
<ChatMessage role="user" author={attorney} viewerId={client.id} timestamp={t(30)} content="Hi Maria, I am Jane. Did you give the landlord a forwarding address in writing?" />
|
|
125
|
+
</div>
|
|
126
|
+
),
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const longName: ChatAuthor = {
|
|
130
|
+
id: 'attorney-long',
|
|
131
|
+
name: 'Alexandria Montgomery-Vandenberghe Castellanos',
|
|
132
|
+
role: 'Senior supervising attorney, tenant rights',
|
|
133
|
+
}
|
|
134
|
+
const noRole: ChatAuthor = { id: 'guest-1', name: 'Q' }
|
|
135
|
+
const withImage: ChatAuthor = {
|
|
136
|
+
id: 'paralegal-sam',
|
|
137
|
+
name: 'Sam Okafor',
|
|
138
|
+
role: 'Paralegal',
|
|
139
|
+
avatarUrl:
|
|
140
|
+
"data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><rect width='20' height='20' fill='%236366f1'/><circle cx='10' cy='8' r='4' fill='white'/><rect x='4' y='13' width='12' height='7' rx='3' fill='white'/></svg>",
|
|
141
|
+
}
|
|
142
|
+
const brokenImage: ChatAuthor = { id: 'client-broken', name: 'Lee Park', role: 'Client', avatarUrl: 'https://invalid.example/avatar.png' }
|
|
143
|
+
|
|
144
|
+
/** Worst cases: a long name and role, a one-letter name with no role, an image, a failing image. */
|
|
145
|
+
export const EdgeCases: Story = {
|
|
146
|
+
render: () => (
|
|
147
|
+
<div className="space-y-6 p-4">
|
|
148
|
+
<div className="space-y-2">
|
|
149
|
+
<MessageAuthor author={longName} />
|
|
150
|
+
<MessageAuthor author={noRole} />
|
|
151
|
+
<MessageAuthor author={withImage} />
|
|
152
|
+
<MessageAuthor author={brokenImage} />
|
|
153
|
+
</div>
|
|
154
|
+
<AgentTimeline
|
|
155
|
+
className="px-0"
|
|
156
|
+
viewerId={client.id}
|
|
157
|
+
items={[
|
|
158
|
+
{
|
|
159
|
+
id: 'e1',
|
|
160
|
+
kind: 'message',
|
|
161
|
+
role: 'user',
|
|
162
|
+
author: longName,
|
|
163
|
+
content: 'Superlongunbrokenidentifierwithoutanyspaces_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
164
|
+
},
|
|
165
|
+
{ id: 'e2', kind: 'message', role: 'user', author: noRole, content: 'ok' },
|
|
166
|
+
]}
|
|
167
|
+
/>
|
|
168
|
+
</div>
|
|
169
|
+
),
|
|
170
|
+
}
|