@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.
- package/CHANGELOG.md +12 -0
- package/README.md +22 -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/editor/editor-peers.js +1 -21
- package/dist/files/file-card.d.ts +30 -0
- package/dist/files/file-card.js +162 -0
- package/dist/files/file-list.d.ts +25 -0
- package/dist/files/file-list.js +88 -0
- package/dist/files/file-preview.js +16 -62
- package/dist/files/pdf-loader.d.ts +22 -0
- package/dist/files/pdf-loader.js +82 -0
- package/dist/files/pdf-viewer.d.ts +32 -0
- package/dist/files/pdf-viewer.js +567 -0
- package/dist/files.d.ts +5 -1
- package/dist/files.js +5 -1
- package/dist/index.d.ts +9 -4
- package/dist/index.js +6 -1
- package/dist/lib/optional-peer.js +29 -0
- package/dist/primitives.d.ts +2 -2
- package/dist/types/message.d.ts +19 -0
- package/dist/types.d.ts +2 -2
- package/package.json +7 -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/editor/editor-peers.ts +3 -25
- package/src/files/file-artifact-pane.test.tsx +12 -2
- package/src/files/file-card.test.tsx +62 -0
- package/src/files/file-card.tsx +210 -0
- package/src/files/file-list.stories.tsx +121 -0
- package/src/files/file-list.test.tsx +81 -0
- package/src/files/file-list.tsx +167 -0
- package/src/files/file-preview.tsx +25 -94
- package/src/files/index.ts +9 -0
- package/src/files/pdf-loader.test.ts +130 -0
- package/src/files/pdf-loader.ts +169 -0
- package/src/files/pdf-viewer.stories.tsx +81 -0
- package/src/files/pdf-viewer.test.tsx +245 -0
- package/src/files/pdf-viewer.tsx +677 -0
- package/src/files/pdfjs-worker.d.ts +3 -0
- package/src/lib/optional-peer.ts +30 -0
- package/src/markdown/code-block.test.tsx +2 -1
- package/src/types/message.ts +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @tangle-network/ui
|
|
2
2
|
|
|
3
|
+
## 11.26.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 950b7ca: `./files` gains document primitives. `PdfViewer` renders a PDF's pages to canvas with pdf.js, so a file reads the same on Android Chrome and iOS Safari as on a desktop: a scrolling column of pages with page navigation, zoom, fit-to-width and Download, one toolbar row at 360px, and only the pages near the screen holding a canvas. `FilePreview` and `FileArtifactPane` now show PDFs through it. `pdfjs-dist` (6.x) is a new optional peer, reached only through a caught dynamic import; without it, or when a file will not open, the viewer says why and falls back to the browser's `<object>` viewer and then a download card. `configurePdfViewer({ workerSrc, assetsUrl })` sets the page-wide worker and the decoder assets that scanned pages need; with neither, pdf.js runs on the main thread and no CDN is contacted. `FileCard` describes one file with its kind, size, date, uploader and actions, and replaces FilePreview's private download card. `FileList` is a flat document list whose rows open, download or delete a file and hand the host's own record back.
|
|
8
|
+
|
|
9
|
+
## 11.25.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- cf29d7e: Chat transcripts can carry more than one person beside the agent, such as a client and an attorney. `ChatMessage`, `UserMessage` and `AgentTimeline` message items take an optional `author` (`ChatAuthor`: `id`, `name`, optional `role` and `avatarUrl`), and `ChatMessage`, `UserMessage` and `AgentTimeline` take the reader's `viewerId`. The reader's own messages keep the end-aligned inverse bubble. Anyone else's sit at the start on a card under a `MessageAuthor` line: avatar (image, or initials on a tone keyed to the author's id), name, and role tag. An agent's author line opens its turn in `AgentTimeline` and is not repeated after each tool row. Without `author`, every component renders exactly the markup it did before. `MessageAuthor`, `isViewerMessage` and `ChatAuthor` are exported from `./chat`, and `ChatAuthor` from `./types`.
|
|
14
|
+
|
|
3
15
|
## 11.24.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -131,6 +131,28 @@ A TypeScript consumer resolves these entries through the emitted declarations, a
|
|
|
131
131
|
|
|
132
132
|
Because the peers now resolve at first render rather than at build time, a missing one surfaces as a thrown error while React renders. Wrap the editors in an error boundary, so the install list reaches a surface you control instead of unmounting the tree.
|
|
133
133
|
|
|
134
|
+
### PDF pages
|
|
135
|
+
|
|
136
|
+
`pdfjs-dist` backs `PdfViewer` in `./files`, which `FilePreview` and `FileArtifactPane` use for PDFs. It follows the editor's rule: the entry reaches it only through a caught dynamic `import()`, so a consumer without it builds under Vite and esbuild. Without it, `PdfViewer` says so and shows the browser's own viewer through `<object>`, which Android Chrome does not have, so a reader there gets a download card instead of pages.
|
|
137
|
+
|
|
138
|
+
```sh
|
|
139
|
+
pnpm add pdfjs-dist
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
`PdfViewer` imports pdf.js's legacy build, which carries the polyfills that phone browsers a year or two old need. Two settings are page-wide, because pdf.js's own worker setting is. Set them once, before the first viewer renders:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import { configurePdfViewer } from "@tangle-network/ui/files";
|
|
146
|
+
import workerSrc from "pdfjs-dist/legacy/build/pdf.worker.min.mjs?url"; // Vite
|
|
147
|
+
|
|
148
|
+
configurePdfViewer({ workerSrc, assetsUrl: "/pdfjs/" });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `workerSrc` moves parsing into a worker. Without it, the viewer runs pdf.js's worker code on the main thread, which needs no bundler setup and no CDN, at the cost of scrolling while a large file parses.
|
|
152
|
+
- `assetsUrl` is where the app serves the installed pdfjs-dist's `wasm/`, `cmaps/`, `standard_fonts/` and `iccs/` directories. pdf.js 6 decodes JBIG2, CCITT fax and JPEG 2000 images with the wasm modules, and black-and-white scans use the first two, so without it a scanned page renders blank. The viewer warns once in the console when it is missing. Copy the directories at build time; they must come from the same pdfjs-dist version as the worker.
|
|
153
|
+
|
|
154
|
+
An http(s) `src` on another origin needs CORS, as any `fetch` does. When it fails, the viewer falls back to `<object>`, which does not.
|
|
155
|
+
|
|
134
156
|
`nanostores` and `@nanostores/react` back `./stores`, and `react-router` backs `./nav`.
|
|
135
157
|
Those entries create values at module scope, so consumers must install their peers before importing them.
|
|
136
158
|
The package root also re-exports `./stores`, so a root import requires both nanostores peers.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChatAuthor } from "../types/message.js";
|
|
1
2
|
import { ToolPart } from "../types/parts.js";
|
|
2
3
|
import { MessageRole } from "./chat-message.js";
|
|
3
4
|
import { ToolCallData } from "../run/tool-call-feed.js";
|
|
@@ -15,6 +16,12 @@ export interface AgentTimelineMessageItem {
|
|
|
15
16
|
* timestamp of its own — the turn's user message dates the exchange. */
|
|
16
17
|
timestamp?: Date;
|
|
17
18
|
after?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Who wrote the message, when more than one person shares the transcript.
|
|
21
|
+
* A person's message is compared with the timeline's `viewerId`; an agent's
|
|
22
|
+
* author line opens its turn and is not repeated after each tool row.
|
|
23
|
+
*/
|
|
24
|
+
author?: ChatAuthor;
|
|
18
25
|
}
|
|
19
26
|
export interface AgentTimelineToolItem {
|
|
20
27
|
id: string;
|
|
@@ -72,6 +79,11 @@ export interface AgentTimelineProps {
|
|
|
72
79
|
* except user messages) behind a "Show N more steps" toggle. Omit to always
|
|
73
80
|
* show every row. */
|
|
74
81
|
collapseAfter?: number;
|
|
82
|
+
/**
|
|
83
|
+
* The reader's participant id. A user message whose `author` has another
|
|
84
|
+
* id is someone else's: it sits at the start, on a card, under its author.
|
|
85
|
+
*/
|
|
86
|
+
viewerId?: string;
|
|
75
87
|
}
|
|
76
88
|
/**
|
|
77
89
|
* AgentTimeline — unified mixed-content transcript for agent-backed sandbox
|
|
@@ -79,5 +91,5 @@ export interface AgentTimelineProps {
|
|
|
79
91
|
* as one plain column: prose at the reading size, tool calls as quiet
|
|
80
92
|
* one-liners between the paragraphs, no connector chrome.
|
|
81
93
|
*/
|
|
82
|
-
export declare function AgentTimeline({ items, isThinking, emptyState, className, renderToolActions, collapseAfter }: AgentTimelineProps): import("react").JSX.Element | null;
|
|
94
|
+
export declare function AgentTimeline({ items, isThinking, emptyState, className, renderToolActions, collapseAfter, viewerId }: AgentTimelineProps): import("react").JSX.Element | null;
|
|
83
95
|
//#endregion
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { focusRing } from "../lib/focus.js";
|
|
2
2
|
import { cn } from "../lib/utils.js";
|
|
3
3
|
import { Markdown } from "../markdown/markdown.js";
|
|
4
|
+
import { MessageAuthor } from "./message-author.js";
|
|
4
5
|
import { UserMessage } from "./user-message.js";
|
|
5
6
|
import { ThinkingIndicator } from "./thinking-indicator.js";
|
|
6
7
|
import { ToolCallGroup, ToolCallStep } from "../run/tool-call-step.js";
|
|
@@ -54,8 +55,12 @@ function spacingBefore(prev, next) {
|
|
|
54
55
|
}
|
|
55
56
|
const ROW_BLEED = "-mx-3";
|
|
56
57
|
const STREAMING_CARET = "[&>: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-[''] [&>: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-['']";
|
|
57
|
-
function AssistantMessage({ item }) {
|
|
58
|
+
function AssistantMessage({ item, author }) {
|
|
58
59
|
return /* @__PURE__ */ jsxs("div", { children: [
|
|
60
|
+
author && /* @__PURE__ */ jsx(MessageAuthor, {
|
|
61
|
+
author,
|
|
62
|
+
className: "mb-1.5"
|
|
63
|
+
}),
|
|
59
64
|
item.content && /* @__PURE__ */ jsx(Markdown, {
|
|
60
65
|
className: cn("tangle-prose text-[length:var(--font-size-base)] leading-[1.5]", item.isStreaming && STREAMING_CARET),
|
|
61
66
|
children: item.content
|
|
@@ -151,7 +156,7 @@ function ArtifactCard({ item }) {
|
|
|
151
156
|
* as one plain column: prose at the reading size, tool calls as quiet
|
|
152
157
|
* one-liners between the paragraphs, no connector chrome.
|
|
153
158
|
*/
|
|
154
|
-
function AgentTimeline({ items, isThinking, emptyState, className, renderToolActions, collapseAfter }) {
|
|
159
|
+
function AgentTimeline({ items, isThinking, emptyState, className, renderToolActions, collapseAfter, viewerId }) {
|
|
155
160
|
const [expanded, setExpanded] = useState(false);
|
|
156
161
|
if (items.length === 0 && !isThinking) return emptyState ? /* @__PURE__ */ jsx("div", {
|
|
157
162
|
className: cn("flex h-full items-center justify-center p-4", className),
|
|
@@ -180,11 +185,27 @@ function AgentTimeline({ items, isThinking, emptyState, className, renderToolAct
|
|
|
180
185
|
renderList = visible;
|
|
181
186
|
hiddenCount = stepItems.length - limit;
|
|
182
187
|
}
|
|
188
|
+
const headedAgentMessages = /* @__PURE__ */ new Set();
|
|
189
|
+
let speakingAgent;
|
|
190
|
+
for (const item of renderList) {
|
|
191
|
+
if (item.kind !== "message") continue;
|
|
192
|
+
if (item.role === "user") {
|
|
193
|
+
speakingAgent = void 0;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (item.author && item.author.id !== speakingAgent) headedAgentMessages.add(item.id);
|
|
197
|
+
speakingAgent = item.author?.id;
|
|
198
|
+
}
|
|
183
199
|
const renderItem = (item) => {
|
|
184
200
|
if (item.kind === "message") return item.role === "user" ? /* @__PURE__ */ jsx(UserMessage, {
|
|
185
201
|
content: item.content,
|
|
186
|
-
timestamp: item.timestamp
|
|
187
|
-
|
|
202
|
+
timestamp: item.timestamp,
|
|
203
|
+
author: item.author,
|
|
204
|
+
viewerId
|
|
205
|
+
}) : /* @__PURE__ */ jsx(AssistantMessage, {
|
|
206
|
+
item,
|
|
207
|
+
author: headedAgentMessages.has(item.id) ? item.author : void 0
|
|
208
|
+
});
|
|
188
209
|
if (item.kind === "tool") return /* @__PURE__ */ jsx(ToolCallStep, {
|
|
189
210
|
type: item.call.type,
|
|
190
211
|
label: item.call.label,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ChatAuthor } from "../types/message.js";
|
|
1
2
|
import { ReactNode } from "react";
|
|
2
3
|
//#region src/chat/chat-message.d.ts
|
|
3
4
|
export type MessageRole = "user" | "assistant" | "system";
|
|
@@ -17,6 +18,17 @@ export interface ChatMessageProps {
|
|
|
17
18
|
assistantLabel?: string;
|
|
18
19
|
/** Hide the role label row entirely */
|
|
19
20
|
hideRoleLabel?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Who wrote the message, when more than one person shares the transcript.
|
|
23
|
+
* It replaces the role label with the author's avatar, name and role,
|
|
24
|
+
* except on the reader's own messages, which keep `userLabel`.
|
|
25
|
+
*/
|
|
26
|
+
author?: ChatAuthor;
|
|
27
|
+
/**
|
|
28
|
+
* The reader's participant id. A user message whose `author` has another
|
|
29
|
+
* id is someone else's: it sits at the start, on a card, under its author.
|
|
30
|
+
*/
|
|
31
|
+
viewerId?: string;
|
|
20
32
|
}
|
|
21
|
-
export declare function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, className, userLabel, assistantLabel, hideRoleLabel }: ChatMessageProps): import("react").JSX.Element;
|
|
33
|
+
export declare function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, className, userLabel, assistantLabel, hideRoleLabel, author, viewerId }: ChatMessageProps): import("react").JSX.Element;
|
|
22
34
|
//#endregion
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { cn } from "../lib/utils.js";
|
|
2
2
|
import { Markdown } from "../markdown/markdown.js";
|
|
3
|
+
import { MessageAuthor, isViewerMessage } from "./message-author.js";
|
|
3
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/chat/chat-message.tsx
|
|
5
|
-
function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, className, userLabel = "You", assistantLabel = "Agent", hideRoleLabel }) {
|
|
6
|
+
function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, className, userLabel = "You", assistantLabel = "Agent", hideRoleLabel, author, viewerId }) {
|
|
6
7
|
const isUser = role === "user";
|
|
7
8
|
const isAssistant = role === "assistant";
|
|
9
|
+
const fromViewer = isUser && isViewerMessage(author, viewerId);
|
|
10
|
+
const fromOtherPerson = isUser && !fromViewer;
|
|
11
|
+
const shownAuthor = fromViewer ? void 0 : author;
|
|
8
12
|
return /* @__PURE__ */ jsxs("div", {
|
|
9
|
-
className: cn("flex flex-col gap-1",
|
|
13
|
+
className: cn("flex flex-col gap-1", fromViewer ? "items-end" : "items-start", className),
|
|
10
14
|
children: [!hideRoleLabel && /* @__PURE__ */ jsxs("div", {
|
|
11
|
-
className: cn("flex items-center gap-2 px-1",
|
|
12
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
15
|
+
className: cn("flex items-center gap-2 px-1", fromViewer && "flex-row-reverse"),
|
|
16
|
+
children: [shownAuthor ? /* @__PURE__ */ jsx(MessageAuthor, { author: shownAuthor }) : /* @__PURE__ */ jsx("span", {
|
|
13
17
|
className: "font-medium text-foreground text-xs",
|
|
14
18
|
children: isUser ? userLabel : assistantLabel
|
|
15
19
|
}), timestamp && /* @__PURE__ */ jsx("span", {
|
|
@@ -17,7 +21,7 @@ function ChatMessage({ role, content, toolCalls, isStreaming, timestamp, classNa
|
|
|
17
21
|
children: formatTime(timestamp)
|
|
18
22
|
})]
|
|
19
23
|
}), /* @__PURE__ */ jsxs("div", {
|
|
20
|
-
className: cn("min-w-0 max-w-[85%] space-y-1",
|
|
24
|
+
className: cn("min-w-0 max-w-[85%] space-y-1", fromViewer ? "rounded-[var(--radius-lg)] bg-foreground px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-background" : fromOtherPerson ? "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)] text-foreground" : isAssistant ? "text-foreground" : "rounded-[var(--radius-lg)] border border-border bg-card px-[var(--chat-message-px)] py-[var(--chat-message-py)]"),
|
|
21
25
|
children: [isUser ? /* @__PURE__ */ jsx("div", {
|
|
22
26
|
className: "whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[var(--line-height-base)]",
|
|
23
27
|
children: content
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ChatAuthor } from "../types/message.js";
|
|
2
|
+
import { HTMLAttributes } from "react";
|
|
3
|
+
//#region src/chat/message-author.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Whether the reader wrote a human message. A message without an author is
|
|
6
|
+
* the reader's, which keeps a two-party transcript as it was. A message with
|
|
7
|
+
* an author is the reader's only when that author is `viewerId`; with no
|
|
8
|
+
* `viewerId`, every authored message belongs to someone else.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isViewerMessage(author: ChatAuthor | undefined, viewerId: string | undefined): boolean;
|
|
11
|
+
export interface MessageAuthorProps extends Omit<HTMLAttributes<HTMLDivElement>, "children"> {
|
|
12
|
+
author: ChatAuthor;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The line above a message in a transcript with several participants: the
|
|
16
|
+
* author's avatar, name and, when given, their role as a tag.
|
|
17
|
+
*
|
|
18
|
+
* The avatar's tone comes from the author's id, so a participant keeps one
|
|
19
|
+
* colour for the whole thread and in every product. Tones can repeat between
|
|
20
|
+
* participants, so the name is always shown beside it. The avatar repeats the
|
|
21
|
+
* visible name and is hidden from assistive technology.
|
|
22
|
+
*/
|
|
23
|
+
export declare function MessageAuthor({ author, className, ...props }: MessageAuthorProps): import("react").JSX.Element;
|
|
24
|
+
//#endregion
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { toneFor } from "../primitives/tone.js";
|
|
3
|
+
import { Tag } from "../primitives/tag.js";
|
|
4
|
+
import { IconTile } from "../primitives/icon-tile.js";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
//#region src/chat/message-author.tsx
|
|
7
|
+
/**
|
|
8
|
+
* Whether the reader wrote a human message. A message without an author is
|
|
9
|
+
* the reader's, which keeps a two-party transcript as it was. A message with
|
|
10
|
+
* an author is the reader's only when that author is `viewerId`; with no
|
|
11
|
+
* `viewerId`, every authored message belongs to someone else.
|
|
12
|
+
*/
|
|
13
|
+
function isViewerMessage(author, viewerId) {
|
|
14
|
+
return author === void 0 || author.id === viewerId;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The line above a message in a transcript with several participants: the
|
|
18
|
+
* author's avatar, name and, when given, their role as a tag.
|
|
19
|
+
*
|
|
20
|
+
* The avatar's tone comes from the author's id, so a participant keeps one
|
|
21
|
+
* colour for the whole thread and in every product. Tones can repeat between
|
|
22
|
+
* participants, so the name is always shown beside it. The avatar repeats the
|
|
23
|
+
* visible name and is hidden from assistive technology.
|
|
24
|
+
*/
|
|
25
|
+
function MessageAuthor({ author, className, ...props }) {
|
|
26
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
27
|
+
"data-message-author": "",
|
|
28
|
+
className: cn("flex min-w-0 max-w-full items-center gap-2", className),
|
|
29
|
+
...props,
|
|
30
|
+
children: [
|
|
31
|
+
/* @__PURE__ */ jsx(IconTile, {
|
|
32
|
+
size: "xs",
|
|
33
|
+
src: author.avatarUrl,
|
|
34
|
+
name: author.name,
|
|
35
|
+
tone: toneFor(author.id),
|
|
36
|
+
className: "rounded-full"
|
|
37
|
+
}),
|
|
38
|
+
/* @__PURE__ */ jsx("span", {
|
|
39
|
+
className: "min-w-0 truncate font-medium text-foreground text-sm",
|
|
40
|
+
title: author.name,
|
|
41
|
+
children: author.name
|
|
42
|
+
}),
|
|
43
|
+
author.role ? /* @__PURE__ */ jsx(Tag, {
|
|
44
|
+
size: "sm",
|
|
45
|
+
emphasis: "outline",
|
|
46
|
+
className: "max-w-[50%]",
|
|
47
|
+
children: author.role
|
|
48
|
+
}) : null
|
|
49
|
+
]
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { MessageAuthor, isViewerMessage };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionMessage } from "../types/message.js";
|
|
1
|
+
import { ChatAuthor, SessionMessage } from "../types/message.js";
|
|
2
2
|
import { SessionPart } from "../types/parts.js";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
4
|
//#region src/chat/user-message.d.ts
|
|
@@ -10,10 +10,20 @@ export interface UserMessageProps {
|
|
|
10
10
|
content?: string;
|
|
11
11
|
timestamp?: Date;
|
|
12
12
|
actions?: ReactNode;
|
|
13
|
+
/**
|
|
14
|
+
* Who wrote the message, when more than one person shares the transcript.
|
|
15
|
+
* Omit it in a two-party chat: the message renders as the reader's.
|
|
16
|
+
*/
|
|
17
|
+
author?: ChatAuthor;
|
|
18
|
+
/**
|
|
19
|
+
* The reader's participant id. A message whose `author` has another id is
|
|
20
|
+
* someone else's: it sits at the start, on a card, under its author.
|
|
21
|
+
*/
|
|
22
|
+
viewerId?: string;
|
|
13
23
|
}
|
|
14
24
|
/**
|
|
15
25
|
* User speech retains the inverse-theme bubble and direct-content contract.
|
|
16
26
|
* Application notes interrupt the bubble in source order, outside its attribution.
|
|
17
27
|
*/
|
|
18
|
-
export declare const UserMessage: import("react").MemoExoticComponent<({ message: _message, parts, content, timestamp, actions }: UserMessageProps) => import("react").JSX.Element | null>;
|
|
28
|
+
export declare const UserMessage: import("react").MemoExoticComponent<({ message: _message, parts, content, timestamp, actions, author, viewerId }: UserMessageProps) => import("react").JSX.Element | null>;
|
|
19
29
|
//#endregion
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SyntheticText, userTextSegments } from "./synthetic-text.js";
|
|
2
|
+
import { MessageAuthor, isViewerMessage } from "./message-author.js";
|
|
2
3
|
import { memo } from "react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/chat/user-message.tsx
|
|
@@ -8,9 +9,9 @@ function formatTime(date) {
|
|
|
8
9
|
minute: "2-digit"
|
|
9
10
|
});
|
|
10
11
|
}
|
|
11
|
-
function MessageActions({ children }) {
|
|
12
|
+
function MessageActions({ children, align = "end" }) {
|
|
12
13
|
return /* @__PURE__ */ jsx("div", {
|
|
13
|
-
className:
|
|
14
|
+
className: `flex flex-wrap items-center ${align === "end" ? "justify-end" : "justify-start"} gap-1.5 text-xs text-muted-foreground`,
|
|
14
15
|
children
|
|
15
16
|
});
|
|
16
17
|
}
|
|
@@ -34,28 +35,63 @@ function UserBubble({ text, timestamp, actions }) {
|
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
/**
|
|
38
|
+
* Another person's speech: start-aligned under its author, on a card rather
|
|
39
|
+
* than the reader's inverse fill, so "mine" and "theirs" differ before the
|
|
40
|
+
* names are read. The timestamp is revealed on the bubble's far side.
|
|
41
|
+
*/
|
|
42
|
+
function OtherPersonBubble({ text, timestamp, actions, author }) {
|
|
43
|
+
return /* @__PURE__ */ jsx("div", {
|
|
44
|
+
className: "flex justify-start",
|
|
45
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
46
|
+
className: "group flex min-w-0 max-w-[78%] flex-col items-start gap-1.5",
|
|
47
|
+
children: [
|
|
48
|
+
author ? /* @__PURE__ */ jsx(MessageAuthor, { author }) : null,
|
|
49
|
+
/* @__PURE__ */ jsxs("div", {
|
|
50
|
+
className: "relative min-w-0 max-w-full rounded-2xl border border-border bg-card px-4 py-3 text-foreground",
|
|
51
|
+
children: [timestamp ? /* @__PURE__ */ jsx("span", {
|
|
52
|
+
"data-user-message-time": "",
|
|
53
|
+
className: "pointer-events-none absolute left-full top-1/2 ml-3 -translate-y-1/2 whitespace-nowrap text-[length:var(--font-size-xs)] text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100",
|
|
54
|
+
children: formatTime(timestamp)
|
|
55
|
+
}) : null, /* @__PURE__ */ jsx("div", {
|
|
56
|
+
className: "break-words whitespace-pre-wrap text-[length:var(--font-size-base)] leading-[1.5]",
|
|
57
|
+
children: text
|
|
58
|
+
})]
|
|
59
|
+
}),
|
|
60
|
+
actions ? /* @__PURE__ */ jsx(MessageActions, {
|
|
61
|
+
align: "start",
|
|
62
|
+
children: actions
|
|
63
|
+
}) : null
|
|
64
|
+
]
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
37
69
|
* User speech retains the inverse-theme bubble and direct-content contract.
|
|
38
70
|
* Application notes interrupt the bubble in source order, outside its attribution.
|
|
39
71
|
*/
|
|
40
|
-
const UserMessage = memo(({ message: _message, parts, content, timestamp, actions }) => {
|
|
41
|
-
|
|
42
|
-
|
|
72
|
+
const UserMessage = memo(({ message: _message, parts, content, timestamp, actions, author, viewerId }) => {
|
|
73
|
+
const other = isViewerMessage(author, viewerId) ? void 0 : author;
|
|
74
|
+
const bubble = (text, bubbleActions, headed = true, key) => other ? /* @__PURE__ */ jsx(OtherPersonBubble, {
|
|
75
|
+
text,
|
|
76
|
+
timestamp,
|
|
77
|
+
actions: bubbleActions,
|
|
78
|
+
author: headed ? other : void 0
|
|
79
|
+
}, key) : /* @__PURE__ */ jsx(UserBubble, {
|
|
80
|
+
text,
|
|
43
81
|
timestamp,
|
|
44
|
-
actions
|
|
45
|
-
})
|
|
82
|
+
actions: bubbleActions
|
|
83
|
+
}, key);
|
|
84
|
+
if (content != null) return content.trim() ? bubble(content, actions) : null;
|
|
46
85
|
const segments = userTextSegments(parts ?? []);
|
|
47
86
|
if (segments.length === 0) return null;
|
|
48
|
-
if (segments.length === 1 && !segments[0].synthetic) return
|
|
49
|
-
|
|
50
|
-
timestamp,
|
|
51
|
-
actions
|
|
52
|
-
});
|
|
87
|
+
if (segments.length === 1 && !segments[0].synthetic) return bubble(segments[0].text, actions);
|
|
88
|
+
const firstSpoken = segments.find((segment) => !segment.synthetic);
|
|
53
89
|
return /* @__PURE__ */ jsxs("div", {
|
|
54
90
|
className: "space-y-2",
|
|
55
|
-
children: [segments.map((segment) => segment.synthetic ? /* @__PURE__ */ jsx(SyntheticText, { text: segment.text }, segment.index) : /* @__PURE__ */ jsx(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
91
|
+
children: [segments.map((segment) => segment.synthetic ? /* @__PURE__ */ jsx(SyntheticText, { text: segment.text }, segment.index) : bubble(segment.text, void 0, segment === firstSpoken, segment.index)), actions ? /* @__PURE__ */ jsx(MessageActions, {
|
|
92
|
+
align: other ? "start" : "end",
|
|
93
|
+
children: actions
|
|
94
|
+
}) : null]
|
|
59
95
|
});
|
|
60
96
|
});
|
|
61
97
|
UserMessage.displayName = "UserMessage";
|
package/dist/chat.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { ChatAuthor } from "./types/message.js";
|
|
1
2
|
import { ChatContainer, ChatContainerProps } from "./chat/chat-container.js";
|
|
2
3
|
import { MessageList, MessageListProps } from "./chat/message-list.js";
|
|
3
4
|
import { UserMessage, UserMessageProps } from "./chat/user-message.js";
|
|
4
5
|
import { ChatMessage, ChatMessageProps, MessageRole } from "./chat/chat-message.js";
|
|
6
|
+
import { MessageAuthor, MessageAuthorProps, isViewerMessage } from "./chat/message-author.js";
|
|
5
7
|
import { ThinkingIndicator, ThinkingIndicatorProps } from "./chat/thinking-indicator.js";
|
|
6
8
|
import { AgentTimeline, AgentTimelineArtifactItem, AgentTimelineCustomItem, AgentTimelineItem, AgentTimelineMessageItem, AgentTimelineProps, AgentTimelineStatusItem, AgentTimelineTone, AgentTimelineToolGroupItem, AgentTimelineToolItem } from "./chat/agent-timeline.js";
|
|
7
|
-
export { AgentTimeline, type AgentTimelineArtifactItem, type AgentTimelineCustomItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, MessageList, type MessageListProps, type MessageRole, ThinkingIndicator, type ThinkingIndicatorProps, UserMessage, type UserMessageProps };
|
|
9
|
+
export { AgentTimeline, type AgentTimelineArtifactItem, type AgentTimelineCustomItem, type AgentTimelineItem, type AgentTimelineMessageItem, type AgentTimelineProps, type AgentTimelineStatusItem, type AgentTimelineTone, type AgentTimelineToolGroupItem, type AgentTimelineToolItem, type ChatAuthor, ChatContainer, type ChatContainerProps, ChatMessage, type ChatMessageProps, MessageAuthor, type MessageAuthorProps, MessageList, type MessageListProps, type MessageRole, ThinkingIndicator, type ThinkingIndicatorProps, UserMessage, type UserMessageProps, isViewerMessage };
|
package/dist/chat.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { MessageAuthor, isViewerMessage } from "./chat/message-author.js";
|
|
1
2
|
import { UserMessage } from "./chat/user-message.js";
|
|
2
3
|
import { MessageList } from "./chat/message-list.js";
|
|
3
4
|
import { ThinkingIndicator } from "./chat/thinking-indicator.js";
|
|
4
5
|
import { AgentTimeline } from "./chat/agent-timeline.js";
|
|
5
6
|
import { ChatContainer } from "./chat/chat-container.js";
|
|
6
7
|
import { ChatMessage } from "./chat/chat-message.js";
|
|
7
|
-
export { AgentTimeline, ChatContainer, ChatMessage, MessageList, ThinkingIndicator, UserMessage };
|
|
8
|
+
export { AgentTimeline, ChatContainer, ChatMessage, MessageAuthor, MessageList, ThinkingIndicator, UserMessage, isViewerMessage };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isMissingPeerError, rethrow } from "../lib/optional-peer.js";
|
|
1
2
|
//#region src/editor/editor-peers.ts
|
|
2
3
|
const DOCUMENT_PEERS_MISSING = "@tangle-network/ui/editor needs its optional editor peers. Install @tiptap/react and @tiptap/starter-kit.";
|
|
3
4
|
const PROVIDER_PEERS_MISSING = "@tangle-network/ui/editor needs its optional collaboration transport peers. Install @hocuspocus/provider and yjs.";
|
|
@@ -20,27 +21,6 @@ var MissingEditorPeersError = class extends Error {
|
|
|
20
21
|
function isMissingEditorPeersError(error) {
|
|
21
22
|
return error instanceof Error && error.name === "MissingEditorPeersError";
|
|
22
23
|
}
|
|
23
|
-
/** The messages a bundler or a runtime gives for a module it cannot resolve. */
|
|
24
|
-
const RESOLUTION_FAILURE = /could not resolve|cannot find (?:module|package)|can't resolve|failed to resolve|module not found/i;
|
|
25
|
-
/**
|
|
26
|
-
* True when the rejection says the package is not installed. Only such an
|
|
27
|
-
* error gets the install list: a transient chunk-fetch failure that reads as
|
|
28
|
-
* "install the peers" sends the reader to the wrong fix. An error this
|
|
29
|
-
* predicate does not match keeps its own message, so it can only
|
|
30
|
-
* under-report.
|
|
31
|
-
*/
|
|
32
|
-
function isMissingPeerError(error) {
|
|
33
|
-
return error instanceof Error && RESOLUTION_FAILURE.test(error.message);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Hands an `import()` rejection on unchanged. esbuild reports an unresolvable
|
|
37
|
-
* literal `import()` as a build error and defers it to run time only when the
|
|
38
|
-
* call carries a `.catch()`, so every peer import attaches this handler. It
|
|
39
|
-
* changes nothing at run time.
|
|
40
|
-
*/
|
|
41
|
-
function rethrow(error) {
|
|
42
|
-
throw error;
|
|
43
|
-
}
|
|
44
24
|
/**
|
|
45
25
|
* Every optional peer the `./editor` entry needs at run time. The loaders
|
|
46
26
|
* import all but `@tiptap/core`, which the tiptap packages need in turn: a
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import "../primitives/icon-tile.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
//#region src/files/file-card.d.ts
|
|
4
|
+
/** Who added a file, as a reader of the list should see them. */
|
|
5
|
+
export interface FileUploader {
|
|
6
|
+
name: string;
|
|
7
|
+
/** Their relationship to the work, e.g. "Client" or "Attorney". */
|
|
8
|
+
role?: string;
|
|
9
|
+
}
|
|
10
|
+
/** What FileCard and FileList show about one file. */
|
|
11
|
+
export interface FileDetails {
|
|
12
|
+
filename: string;
|
|
13
|
+
/** Wins over the extension when choosing the kind, as in FilePreview. */
|
|
14
|
+
mimeType?: string;
|
|
15
|
+
/** Size in bytes. */
|
|
16
|
+
size?: number;
|
|
17
|
+
/** When the file was added or last changed. */
|
|
18
|
+
date?: Date | string | number;
|
|
19
|
+
uploader?: FileUploader;
|
|
20
|
+
}
|
|
21
|
+
export interface FileCardProps extends FileDetails, Omit<React.HTMLAttributes<HTMLDivElement>, "children"> {
|
|
22
|
+
/** One sentence under the details, such as why there is no preview. */
|
|
23
|
+
description?: React.ReactNode;
|
|
24
|
+
/** Adds a Download button. */
|
|
25
|
+
onDownload?: () => void;
|
|
26
|
+
/** Further buttons after Download, such as Open or Delete. */
|
|
27
|
+
actions?: React.ReactNode;
|
|
28
|
+
}
|
|
29
|
+
export declare const FileCard: React.ForwardRefExoticComponent<FileCardProps & React.RefAttributes<HTMLDivElement>>;
|
|
30
|
+
//#endregion
|