@tangle-network/ui 11.24.0 → 11.26.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +22 -0
  3. package/dist/chat/agent-timeline.d.ts +13 -1
  4. package/dist/chat/agent-timeline.js +25 -4
  5. package/dist/chat/chat-message.d.ts +13 -1
  6. package/dist/chat/chat-message.js +9 -5
  7. package/dist/chat/message-author.d.ts +24 -0
  8. package/dist/chat/message-author.js +53 -0
  9. package/dist/chat/user-message.d.ts +12 -2
  10. package/dist/chat/user-message.js +52 -16
  11. package/dist/chat.d.ts +3 -1
  12. package/dist/chat.js +2 -1
  13. package/dist/editor/editor-peers.js +1 -21
  14. package/dist/files/file-card.d.ts +30 -0
  15. package/dist/files/file-card.js +162 -0
  16. package/dist/files/file-list.d.ts +25 -0
  17. package/dist/files/file-list.js +88 -0
  18. package/dist/files/file-preview.js +16 -62
  19. package/dist/files/pdf-loader.d.ts +22 -0
  20. package/dist/files/pdf-loader.js +82 -0
  21. package/dist/files/pdf-viewer.d.ts +32 -0
  22. package/dist/files/pdf-viewer.js +567 -0
  23. package/dist/files.d.ts +5 -1
  24. package/dist/files.js +5 -1
  25. package/dist/index.d.ts +9 -4
  26. package/dist/index.js +6 -1
  27. package/dist/lib/optional-peer.js +29 -0
  28. package/dist/primitives.d.ts +2 -2
  29. package/dist/types/message.d.ts +19 -0
  30. package/dist/types.d.ts +2 -2
  31. package/package.json +7 -2
  32. package/src/chat/agent-timeline.tsx +33 -3
  33. package/src/chat/chat-message.tsx +37 -10
  34. package/src/chat/index.ts +2 -0
  35. package/src/chat/message-author.stories.tsx +170 -0
  36. package/src/chat/message-author.test.tsx +189 -0
  37. package/src/chat/message-author.tsx +52 -0
  38. package/src/chat/user-message.tsx +63 -8
  39. package/src/editor/editor-peers.ts +3 -25
  40. package/src/files/file-artifact-pane.test.tsx +12 -2
  41. package/src/files/file-card.test.tsx +62 -0
  42. package/src/files/file-card.tsx +210 -0
  43. package/src/files/file-list.stories.tsx +121 -0
  44. package/src/files/file-list.test.tsx +81 -0
  45. package/src/files/file-list.tsx +167 -0
  46. package/src/files/file-preview.tsx +25 -94
  47. package/src/files/index.ts +9 -0
  48. package/src/files/pdf-loader.test.ts +130 -0
  49. package/src/files/pdf-loader.ts +169 -0
  50. package/src/files/pdf-viewer.stories.tsx +81 -0
  51. package/src/files/pdf-viewer.test.tsx +245 -0
  52. package/src/files/pdf-viewer.tsx +677 -0
  53. package/src/files/pdfjs-worker.d.ts +3 -0
  54. package/src/lib/optional-peer.ts +30 -0
  55. package/src/markdown/code-block.test.tsx +2 -1
  56. package/src/types/message.ts +20 -0
package/dist/index.js CHANGED
@@ -71,6 +71,7 @@ import { OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES } from "./openui/schema-data
71
71
  import { findUnsupportedOpenUINode, validateOpenUIJsonArtifact, validateOpenUIJsonNode } from "./openui-schema.js";
72
72
  import { OpenUIArtifactRenderer } from "./openui/openui-artifact-renderer.js";
73
73
  import { RunGroup } from "./run/run-group.js";
74
+ import { MessageAuthor, isViewerMessage } from "./chat/message-author.js";
74
75
  import { UserMessage } from "./chat/user-message.js";
75
76
  import { MessageList } from "./chat/message-list.js";
76
77
  import { ThinkingIndicator } from "./chat/thinking-indicator.js";
@@ -81,7 +82,11 @@ import { LiveDuration } from "./run/run-item-primitives.js";
81
82
  import { ToolCallFeed, parseToolEvent } from "./run/tool-call-feed.js";
82
83
  import { FileTree, filterFileTree } from "./files/file-tree.js";
83
84
  import { RichFileTree } from "./files/rich-file-tree.js";
85
+ import { FileCard } from "./files/file-card.js";
86
+ import { configurePdfViewer } from "./files/pdf-loader.js";
87
+ import { PdfViewer } from "./files/pdf-viewer.js";
84
88
  import { CSV_PREVIEW_ROW_LIMIT, FilePreview, parseCsv } from "./files/file-preview.js";
89
+ import { FileList } from "./files/file-list.js";
85
90
  import { FileTabs } from "./files/file-tabs.js";
86
91
  import { FileArtifactPane } from "./files/file-artifact-pane.js";
87
92
  import { AuthHeader, GitHubLoginButton, UserMenu } from "./auth/auth.js";
@@ -98,4 +103,4 @@ import { addMessage, addParts, clearChat, isStreamingAtom, messagesAtom, partMap
98
103
  import { copyText } from "./utils/copy-text.js";
99
104
  import { timeAgo } from "./utils/time-ago.js";
100
105
  import { RedactedDocument } from "./redaction/redacted-document.js";
101
- export { AgentTimeline, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatContainer, ChatMessage, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandedToolDetail, FileArtifactPane, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageList, Metric, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunRowShell, RunRowStatusDot, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, WriteFilePreview, activeSessionsAtom, addMessage, addParts, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, copyText, createAuthFetcher, detectFileFormat, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, messagesAtom, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
106
+ export { AgentTimeline, Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, AuthHeader, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, CSV_PREVIEW_ROW_LIMIT, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, ChatContainer, ChatMessage, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, CommandPreview, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DiffPreview, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, ExpandedToolDetail, FileArtifactPane, FileCard, FileList, FilePreview, FileTabs, FileTree, FilterField, GitHubLoginButton, GlobResultsPreview, GrepResultsPreview, Heading, IconTile, InlineCode, InlineThinkingItem, InlineToolItem, Input, Label, LiveDuration, LoginLayout, Logo, Markdown, MessageAuthor, MessageList, Metric, MetricStrip, OPENUI_NODE_JSON_SCHEMA, OPENUI_NODE_TYPES, OpenUIArtifactRenderer, PageHeader, PageShell, Pagination, PdfViewer, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, QuestionPreview, RealtimeSessionRegistry, RedactedDocument, RichFileTree, RunGroup, RunRowShell, RunRowStatusDot, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, TOOL_CATEGORY_ICONS, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, ThinkingIndicator, ToastContainer, ToastProvider, ToolCallFeed, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, UserMenu, UserMessage, WebSearchPreview, WriteFilePreview, activeSessionsAtom, addMessage, addParts, badgeVariants, bumpActiveSessionActivity, buttonVariants, clearChat, cn, configurePdfViewer, copyText, createAuthFetcher, detectFileFormat, fileExtension, filterFileTree, findUnsupportedOpenUINode, focusField, focusFieldInvalid, focusFieldWithin, focusRing, focusRingInset, formatBytes, formatDuration, formatUptime, getActiveSession, getAllActiveSessions, getAllProjectActivity, getCodeLanguage, getFormatLabel, getSanitizedMarkdownHeadingIdFromRawFragment, getSessionsByActivity, getSessionsForNavbar, getSessionsForProject, getSyntaxLanguage, getToolCategory, getToolDisplayMetadata, getToolErrorText, getTotalRunningSessionCount, hasBackgroundRunningSessions, initialsOf, isStreamingAtom, isViewerMessage, messagesAtom, parseCsv, parseToolEvent, partMapAtom, registerActiveSession, resetActiveSessions, resolveFilePreviewKind, setActiveSessionAttention, setActiveSessionConnection, setActiveSessionError, setActiveSessionRunning, setForegroundActiveSession, timeAgo, toneFor, truncateText, unregisterActiveSession, updateActiveSessionMeta, updatePart, useActiveSession, useActiveSessions, useActiveSessionsState, useApiKey, useAuth, useAutoScroll, useDropdownMenu, useHasBackgroundRunningSessions, useLiveTime, useNavbarSessions, useProjectActivity, useProjectSessions, useRealtimeSession, useRunCollapseState, useRunGroups, useSSEStream, useSdkSession, useSessionsByActivity, useTheme, useToast, useToolCallStream, useTotalRunningSessions, validateOpenUIJsonArtifact, validateOpenUIJsonNode };
@@ -0,0 +1,29 @@
1
+ //#region src/lib/optional-peer.ts
2
+ /**
3
+ * Shared handling for optional peers that an entry reaches through a dynamic
4
+ * `import()`: `./editor` for tiptap, yjs and Hocuspocus, `./files` for
5
+ * pdfjs-dist. `scripts/validate-dist.mjs` holds the rules these helpers serve.
6
+ */
7
+ /** The messages a bundler or a runtime gives for a module it cannot resolve. */
8
+ const RESOLUTION_FAILURE = /could not resolve|cannot find (?:module|package)|can't resolve|failed to resolve|module not found/i;
9
+ /**
10
+ * True when the rejection says the package is not installed. Only such an
11
+ * error gets the install list: a transient chunk-fetch failure that reads as
12
+ * "install the peers" sends the reader to the wrong fix. An error this
13
+ * predicate does not match keeps its own message, so it can only
14
+ * under-report.
15
+ */
16
+ function isMissingPeerError(error) {
17
+ return error instanceof Error && RESOLUTION_FAILURE.test(error.message);
18
+ }
19
+ /**
20
+ * Hands an `import()` rejection on unchanged. esbuild reports an unresolvable
21
+ * literal `import()` as a build error and defers it to run time only when the
22
+ * call carries a `.catch()`, so every peer import attaches this handler. It
23
+ * changes nothing at run time.
24
+ */
25
+ function rethrow(error) {
26
+ throw error;
27
+ }
28
+ //#endregion
29
+ export { isMissingPeerError, rethrow };
@@ -1,5 +1,7 @@
1
1
  import { Button, ButtonProps, buttonVariants } from "./primitives/button.js";
2
2
  import { ArtifactPane, ArtifactPaneProps } from "./primitives/artifact-pane.js";
3
+ import { CATEGORY_TONES, CategoryTone, TONE_CLASSES, Tone, ToneClasses, toneFor } from "./primitives/tone.js";
4
+ import { IconTile, IconTileProps, initialsOf } from "./primitives/icon-tile.js";
3
5
  import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CardTitleProps } from "./primitives/card.js";
4
6
  import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "./primitives/dialog.js";
5
7
  import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger } from "./primitives/alert-dialog.js";
@@ -40,10 +42,8 @@ import { PageHeader, PageHeaderProps } from "./primitives/page-header.js";
40
42
  import { StatusPill, StatusPillProps, StatusTone } from "./primitives/status-pill.js";
41
43
  import { Alert, AlertDescription, AlertProps, AlertTitle, AlertTone } from "./primitives/alert.js";
42
44
  import { Pagination, PaginationProps } from "./primitives/pagination.js";
43
- import { CATEGORY_TONES, CategoryTone, TONE_CLASSES, Tone, ToneClasses, toneFor } from "./primitives/tone.js";
44
45
  import { Tag, TagProps } from "./primitives/tag.js";
45
46
  import { Chip, ChipProps } from "./primitives/chip.js";
46
- import { IconTile, IconTileProps, initialsOf } from "./primitives/icon-tile.js";
47
47
  import { Metric, MetricProps, MetricStrip, MetricStripProps } from "./primitives/metric-strip.js";
48
48
  import { FilterField, FilterFieldProps, Toolbar, ToolbarProps } from "./primitives/toolbar.js";
49
49
  import { Logo, LogoProps, TangleKnot } from "@tangle-network/brand";
@@ -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.24.0",
3
+ "version": "11.26.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,13 +149,14 @@
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.15.6",
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",
156
156
  "@tiptap/react": ">=3.20.4 <4.0.0",
157
157
  "@tiptap/starter-kit": ">=3.20.4 <4.0.0",
158
158
  "nanostores": ">=0.11.3 <2.0.0",
159
+ "pdfjs-dist": ">=6.0.0 <7.0.0",
159
160
  "react": "^18 || ^19",
160
161
  "react-dom": "^18 || ^19",
161
162
  "react-router": "^7 || ^8",
@@ -191,6 +192,9 @@
191
192
  },
192
193
  "react-router": {
193
194
  "optional": true
195
+ },
196
+ "pdfjs-dist": {
197
+ "optional": true
194
198
  }
195
199
  },
196
200
  "devDependencies": {
@@ -208,6 +212,7 @@
208
212
  "@types/turndown": "^5.0.6",
209
213
  "ajv": "8.17.1",
210
214
  "nanostores": "^1.5.4",
215
+ "pdfjs-dist": "^6.4.299",
211
216
  "react": "^19.3.0",
212
217
  "react-dom": "^19.3.0",
213
218
  "react-router": "^8.4.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
- isUser ? "items-end" : "items-start",
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", isUser && "flex-row-reverse")}>
56
- <span className="font-medium text-foreground text-xs">
57
- {isUser ? userLabel : assistantLabel}
58
- </span>
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
- isUser
95
+ fromViewer
71
96
  ? "rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background"
72
- : isAssistant
73
- ? "text-foreground"
74
- : "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]",
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
+ }