@parallelworks/ui 0.58.0 → 0.59.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/README.md +48 -9
- package/dist/ai/agent/diff.d.ts +1 -0
- package/dist/ai/agent/diff.js +50 -3
- package/dist/ai/components/AttachmentUpload.d.ts +3 -0
- package/dist/ai/components/AttachmentUpload.js +51 -43
- package/dist/ai/components/agent/AgentMessageParts.js +33 -6
- package/dist/ai/strings.d.ts +2 -0
- package/dist/ai/strings.js +2 -1
- package/dist/components/AppLink.js +8 -3
- package/dist/components/Avatar.js +9 -3
- package/dist/components/ExternalLink.d.ts +3 -2
- package/dist/components/ExternalLink.js +5 -2
- package/dist/components/Field.d.ts +7 -1
- package/dist/components/Field.js +83 -56
- package/dist/components/HoverCard.js +12 -0
- package/dist/components/Provider.d.ts +6 -1
- package/dist/components/Provider.js +8 -3
- package/dist/components/StickyBar.d.ts +14 -0
- package/dist/components/StickyBar.js +22 -0
- package/dist/components/Table.js +3 -3
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +5 -2
- package/dist/components/stickyTop.d.ts +7 -3
- package/dist/components/stickyTop.js +11 -6
- package/dist/file-explorer/FileExplorer.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/list/ActionToolbar.d.ts +2 -3
- package/dist/list/ActionToolbar.js +3 -3
- package/dist/list/index.d.ts +1 -1
- package/dist/list/index.js +2 -2
- package/dist/styles/theme.css +11 -0
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,6 +41,12 @@ package navigates and never re-renders anything. Define slot components and
|
|
|
41
41
|
string functions outside a component: one made each render is a new value, and
|
|
42
42
|
a new slot component remounts what it renders.
|
|
43
43
|
|
|
44
|
+
Under `UIProvider` the components render no inline `<style>`, so they run
|
|
45
|
+
under a Content Security Policy whose `style-src` is `'self'` alone; the one
|
|
46
|
+
rule a select or scroll area would inject ships in `theme.css`. A host that
|
|
47
|
+
renders on the server passes its policy's `nonce`, which the few components
|
|
48
|
+
that emit a script to place themselves before hydration put on it.
|
|
49
|
+
|
|
44
50
|
## Which component?
|
|
45
51
|
|
|
46
52
|
Each need has one component. Reach for it before styling markup by hand.
|
|
@@ -60,7 +66,7 @@ Each need has one component. Reach for it before styling markup by hand.
|
|
|
60
66
|
| A record's details, such as in a drawer or a card: names and values on their own | `DescriptionList` of `DescriptionListItem`s; `copy` on an ID, `mono` on a path, `variant="card"` to frame them; titled lists in a `DescriptionListGroup` to share one label column |
|
|
61
67
|
| An error beside a control outside a `Field` | `ErrorText` |
|
|
62
68
|
| A dense field in a toolbar or a table cell | any field control with `size="sm"`; a width in `className` wins, and `cx` merges classes the same way |
|
|
63
|
-
| On or off | `Switch`; `Checkbox` in a list of things to tick, several of which may be on |
|
|
69
|
+
| On or off | `Switch`; `Checkbox` in a list of things to tick, several of which may be on; either in a `Field` with `controlFirst` to read control, label, then its help and description |
|
|
64
70
|
| One of a few choices, all shown | `RadioGroup` in a form; `SegmentedControl` in a toolbar, or as a field's control in a `Field group` |
|
|
65
71
|
| Narrowing a list by values, such as statuses: several at once, or one or none | `ToggleGroup` |
|
|
66
72
|
| Narrowing what is shown by typing | `SearchInput`; `ListSearchControl` on a list page's toolbar |
|
|
@@ -72,6 +78,7 @@ Each need has one component. Reach for it before styling markup by hand.
|
|
|
72
78
|
| A titled part of a page or form | `Section` |
|
|
73
79
|
| More detail under a line | `Disclosure` |
|
|
74
80
|
| Views of one thing | `Tabs` |
|
|
81
|
+
| Chrome that stays in view as the page scrolls | `sticky` on `ActionToolbar` or `Tabs`; `StickyBar` around controls of your own. They stack under the page header by themselves (see [Host chrome](#host-chrome)) |
|
|
75
82
|
| A list of actions | `ActionMenu`; `useContextMenu` for a right-click or a row's menu |
|
|
76
83
|
| A panel of controls anchored to a trigger | `Popover` |
|
|
77
84
|
| A hint for a control | `Tooltip`; a `Field`'s `help` for more about a field, which its control reads and which opens as Tab reaches it; `InfoTip` for help about anything else |
|
|
@@ -353,9 +360,20 @@ leaves unset follows the `color-scheme` it sets:
|
|
|
353
360
|
```css
|
|
354
361
|
/* In a Tailwind v4 build: */
|
|
355
362
|
@import '@parallelworks/ui/theme.css';
|
|
356
|
-
@source '../node_modules/@parallelworks/ui/
|
|
363
|
+
@source '../node_modules/@parallelworks/ui/dist/components';
|
|
364
|
+
@source '../node_modules/@parallelworks/ui/dist/list';
|
|
357
365
|
```
|
|
358
366
|
|
|
367
|
+
The package publishes only `dist`, built one JavaScript module per source
|
|
368
|
+
module under the same folders as its subpaths (`dist/components`, `dist/list`,
|
|
369
|
+
`dist/form`, `dist/ai` and so on), so `@source` points at those modules. List
|
|
370
|
+
the folders of the subpaths the host imports: each one adds the utilities of
|
|
371
|
+
every component in it. Scanning all of `dist` works too, but also reads the
|
|
372
|
+
editor's bundled workers in `dist/assets`. A host that wants only a few
|
|
373
|
+
components can name their modules one by one, such as
|
|
374
|
+
`dist/components/Avatar.js`, as long as it also names the modules they style
|
|
375
|
+
through, such as `dist/components/floating.js` for a popup's panel.
|
|
376
|
+
|
|
359
377
|
Consumers without a Tailwind build can import the compiled standalone
|
|
360
378
|
stylesheet instead:
|
|
361
379
|
|
|
@@ -582,13 +600,34 @@ document.documentElement.style.setProperty('--app-inset-top', '56px')
|
|
|
582
600
|
document.documentElement.style.setProperty('--app-inset-start', '220px')
|
|
583
601
|
```
|
|
584
602
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
or
|
|
603
|
+
Sticky chrome stacks by itself, with no offsets to set. A page's pinned pieces,
|
|
604
|
+
`ActionToolbar sticky`, `Tabs sticky`, a `StickyBar` around controls of the
|
|
605
|
+
page's own, and a table's header row (`Table stickyHeader`, which list tables
|
|
606
|
+
use), each stop just below whatever their scrolling box already pins at the top.
|
|
607
|
+
Each finds the elements there with `position: sticky` and a `top` whose parent
|
|
608
|
+
also holds it, so they stay pinned while it passes, and follows them as they
|
|
609
|
+
wrap to a new height or come and go, before the page paints. In a box that
|
|
610
|
+
scrolls on its own, such as a fixed-height panel or `ListPage`'s rows, they pin
|
|
611
|
+
at the box's top.
|
|
612
|
+
|
|
613
|
+
The stack starts from the host's page header. Put it inside the page's
|
|
614
|
+
scroller, ahead of the page, with `position: sticky` and a `top`, such as
|
|
615
|
+
`sticky top-0`: as a child of the scroller or of a wrapper the page also sits
|
|
616
|
+
in. A header above the scroller, outside it, needs nothing: the page's chrome
|
|
617
|
+
pins at the scroller's top, just under it.
|
|
618
|
+
|
|
619
|
+
```tsx
|
|
620
|
+
<div className="overflow-auto">
|
|
621
|
+
<header className="sticky top-0 z-(--layer-header)">…</header>
|
|
622
|
+
<main>
|
|
623
|
+
<StickyBar className="px-4 py-1.5">
|
|
624
|
+
<ActionToolbar groups={actions} />
|
|
625
|
+
</StickyBar>
|
|
626
|
+
<Tabs label="Cluster" sticky items={tabs} />
|
|
627
|
+
<Table stickyHeader>…</Table>
|
|
628
|
+
</main>
|
|
629
|
+
</div>
|
|
630
|
+
```
|
|
592
631
|
|
|
593
632
|
Everything the package lays over the page stacks on one scale, set on the
|
|
594
633
|
document root by `base.css`. Give the host's own fixed elements a place on it,
|
package/dist/ai/agent/diff.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export interface EditToolArgs {
|
|
|
10
10
|
}
|
|
11
11
|
export declare function isEditDiffTool(name: string): boolean;
|
|
12
12
|
export declare function parseEditArgs(argsJSON: string): EditToolArgs | null;
|
|
13
|
+
export declare const MAX_DIFF_CELLS = 1000000;
|
|
13
14
|
export declare function wordEmphasisRanges(oldLine: string, newLine: string): Array<[number, number]> | null;
|
|
14
15
|
export declare function lineDelta(oldLines: string[], newLines: string[]): [number, number];
|
|
15
16
|
export interface DiffRow {
|
package/dist/ai/agent/diff.js
CHANGED
|
@@ -19,7 +19,51 @@ function splitLines(s) {
|
|
|
19
19
|
const trimmed = s.endsWith("\n") ? s.slice(0, -1) : s;
|
|
20
20
|
return trimmed === "" ? [] : trimmed.split("\n");
|
|
21
21
|
}
|
|
22
|
+
var MAX_DIFF_CELLS = 1e6;
|
|
22
23
|
function getOpCodes(a, b) {
|
|
24
|
+
if (a.length * b.length <= 1e6) return lcsOpCodes(a, b);
|
|
25
|
+
let start = 0;
|
|
26
|
+
while (start < a.length && start < b.length && a[start] === b[start]) start++;
|
|
27
|
+
let endA = a.length;
|
|
28
|
+
let endB = b.length;
|
|
29
|
+
while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
|
|
30
|
+
endA--;
|
|
31
|
+
endB--;
|
|
32
|
+
}
|
|
33
|
+
const midA = a.slice(start, endA);
|
|
34
|
+
const midB = b.slice(start, endB);
|
|
35
|
+
const middle = midA.length * midB.length <= 1e6 ? lcsOpCodes(midA, midB) : [{
|
|
36
|
+
tag: "r",
|
|
37
|
+
i1: 0,
|
|
38
|
+
i2: midA.length,
|
|
39
|
+
j1: 0,
|
|
40
|
+
j2: midB.length
|
|
41
|
+
}];
|
|
42
|
+
const ops = [];
|
|
43
|
+
if (start > 0) ops.push({
|
|
44
|
+
tag: "e",
|
|
45
|
+
i1: 0,
|
|
46
|
+
i2: start,
|
|
47
|
+
j1: 0,
|
|
48
|
+
j2: start
|
|
49
|
+
});
|
|
50
|
+
for (const op of middle) ops.push({
|
|
51
|
+
tag: op.tag,
|
|
52
|
+
i1: op.i1 + start,
|
|
53
|
+
i2: op.i2 + start,
|
|
54
|
+
j1: op.j1 + start,
|
|
55
|
+
j2: op.j2 + start
|
|
56
|
+
});
|
|
57
|
+
if (endA < a.length) ops.push({
|
|
58
|
+
tag: "e",
|
|
59
|
+
i1: endA,
|
|
60
|
+
i2: a.length,
|
|
61
|
+
j1: endB,
|
|
62
|
+
j2: b.length
|
|
63
|
+
});
|
|
64
|
+
return ops;
|
|
65
|
+
}
|
|
66
|
+
function lcsOpCodes(a, b) {
|
|
23
67
|
const n = a.length;
|
|
24
68
|
const m = b.length;
|
|
25
69
|
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
@@ -106,9 +150,12 @@ function wordEmphasisRanges(oldLine, newLine) {
|
|
|
106
150
|
return ranges.length > 0 ? ranges : null;
|
|
107
151
|
}
|
|
108
152
|
function lineDelta(oldLines, newLines) {
|
|
153
|
+
return countLines(getOpCodes(oldLines, newLines));
|
|
154
|
+
}
|
|
155
|
+
function countLines(ops) {
|
|
109
156
|
let added = 0;
|
|
110
157
|
let removed = 0;
|
|
111
|
-
for (const op of
|
|
158
|
+
for (const op of ops) {
|
|
112
159
|
if (op.tag === "d" || op.tag === "r") removed += op.i2 - op.i1;
|
|
113
160
|
if (op.tag === "i" || op.tag === "r") added += op.j2 - op.j1;
|
|
114
161
|
}
|
|
@@ -117,8 +164,8 @@ function lineDelta(oldLines, newLines) {
|
|
|
117
164
|
function computeEditDiff(oldString, newString, startLine = 0) {
|
|
118
165
|
const oldLines = splitLines(oldString);
|
|
119
166
|
const newLines = splitLines(newString);
|
|
120
|
-
const [added, removed] = lineDelta(oldLines, newLines);
|
|
121
167
|
const ops = getOpCodes(oldLines, newLines);
|
|
168
|
+
const [added, removed] = countLines(ops);
|
|
122
169
|
const numbered = startLine > 0;
|
|
123
170
|
const rows = [];
|
|
124
171
|
let truncated = false;
|
|
@@ -213,4 +260,4 @@ function editSummary(toolName, argsJSON, t = defaultToolStrings) {
|
|
|
213
260
|
return "";
|
|
214
261
|
}
|
|
215
262
|
//#endregion
|
|
216
|
-
export { MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, computeEditDiff, computeWriteRows, editSummary, isEditDiffTool, lineDelta, parseEditArgs, wordEmphasisRanges };
|
|
263
|
+
export { MAX_DIFF_CELLS, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, computeEditDiff, computeWriteRows, editSummary, isEditDiffTool, lineDelta, parseEditArgs, wordEmphasisRanges };
|
|
@@ -16,6 +16,9 @@ export interface Attachment {
|
|
|
16
16
|
interface AttachmentUploadProps {
|
|
17
17
|
conversationId?: string | undefined;
|
|
18
18
|
attachments: Attachment[];
|
|
19
|
+
/** The list with a change. Keep its entries as given: an upload's result
|
|
20
|
+
* replaces its pending entry by identity, on the list as it is when the
|
|
21
|
+
* upload finishes. */
|
|
19
22
|
onAttachmentsChange: (attachments: Attachment[]) => void;
|
|
20
23
|
disabled?: boolean;
|
|
21
24
|
maxFiles?: number;
|
|
@@ -7,7 +7,7 @@ import { useChatConfig } from "../core/config.js";
|
|
|
7
7
|
import { useChat } from "../core/ChatProvider.js";
|
|
8
8
|
import { ATTACHMENTS_INFINITE_KEY } from "./attachmentKeys.js";
|
|
9
9
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
10
|
-
import { useCallback, useImperativeHandle, useRef, useState } from "react";
|
|
10
|
+
import { useCallback, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
11
11
|
import { mutate } from "swr";
|
|
12
12
|
//#region src/ai/components/AttachmentUpload.tsx
|
|
13
13
|
var DEFAULT_MAX_FILE_SIZE = 26214400;
|
|
@@ -24,18 +24,23 @@ function AttachmentUpload({ conversationId, attachments, onAttachmentsChange, di
|
|
|
24
24
|
const { common } = useStrings();
|
|
25
25
|
const [isDragging, setIsDragging] = useState(false);
|
|
26
26
|
const fileInputRef = useRef(null);
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
t
|
|
36
|
-
]);
|
|
27
|
+
const latest = useRef(attachments);
|
|
28
|
+
useLayoutEffect(() => {
|
|
29
|
+
latest.current = attachments;
|
|
30
|
+
}, [attachments]);
|
|
31
|
+
const commit = useCallback((next) => {
|
|
32
|
+
latest.current = next;
|
|
33
|
+
onAttachmentsChange(next);
|
|
34
|
+
}, [onAttachmentsChange]);
|
|
37
35
|
const uploadFile = useCallback(async (file) => {
|
|
38
|
-
|
|
36
|
+
const failed = (error) => ({
|
|
37
|
+
file,
|
|
38
|
+
filename: file.name,
|
|
39
|
+
mimeType: file.type,
|
|
40
|
+
size: file.size,
|
|
41
|
+
error
|
|
42
|
+
});
|
|
43
|
+
if (!adapter.attachments) return failed(t.uploadUnavailable);
|
|
39
44
|
try {
|
|
40
45
|
const uploaded = await adapter.attachments.upload(file, conversationId);
|
|
41
46
|
await mutate(ATTACHMENTS_INFINITE_KEY);
|
|
@@ -47,44 +52,47 @@ function AttachmentUpload({ conversationId, attachments, onAttachmentsChange, di
|
|
|
47
52
|
uploaded: true
|
|
48
53
|
};
|
|
49
54
|
} catch (error) {
|
|
50
|
-
return
|
|
51
|
-
file,
|
|
52
|
-
filename: file.name,
|
|
53
|
-
mimeType: file.type,
|
|
54
|
-
size: file.size,
|
|
55
|
-
error: error instanceof Error ? error.message : String(error)
|
|
56
|
-
};
|
|
55
|
+
return failed(error instanceof Error ? error.message : String(error));
|
|
57
56
|
}
|
|
58
|
-
}, [
|
|
57
|
+
}, [
|
|
58
|
+
adapter,
|
|
59
|
+
conversationId,
|
|
60
|
+
t
|
|
61
|
+
]);
|
|
59
62
|
const handleFiles = useCallback(async (files) => {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
toast.error(
|
|
63
|
+
const pending = [];
|
|
64
|
+
let room = maxFiles - latest.current.length;
|
|
65
|
+
let overLimit = false;
|
|
66
|
+
for (const file of Array.from(files)) {
|
|
67
|
+
if (file.size > maxFileSize) {
|
|
68
|
+
toast.error(t.fileTooLarge(formatBytes(maxFileSize)));
|
|
66
69
|
continue;
|
|
67
70
|
}
|
|
68
|
-
|
|
71
|
+
if (room <= 0) {
|
|
72
|
+
overLimit = true;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
room--;
|
|
76
|
+
pending.push({
|
|
69
77
|
file,
|
|
70
78
|
filename: file.name,
|
|
71
79
|
mimeType: file.type,
|
|
72
80
|
size: file.size,
|
|
73
81
|
uploading: true
|
|
74
|
-
};
|
|
75
|
-
newAttachments.push(pendingAttachment);
|
|
82
|
+
});
|
|
76
83
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
if (overLimit) toast.error(t.maxFilesAllowed(maxFiles));
|
|
85
|
+
if (pending.length === 0) return;
|
|
86
|
+
commit([...latest.current, ...pending]);
|
|
87
|
+
const results = await Promise.all(pending.map((a) => uploadFile(a.file)));
|
|
88
|
+
const resultOf = new Map(pending.map((a, i) => [a, results[i]]));
|
|
89
|
+
commit(latest.current.map((a) => resultOf.get(a) ?? a));
|
|
83
90
|
}, [
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
91
|
+
commit,
|
|
92
|
+
maxFileSize,
|
|
93
|
+
maxFiles,
|
|
94
|
+
t,
|
|
95
|
+
uploadFile
|
|
88
96
|
]);
|
|
89
97
|
const handleDragOver = useCallback((e) => {
|
|
90
98
|
e.preventDefault();
|
|
@@ -103,9 +111,9 @@ function AttachmentUpload({ conversationId, attachments, onAttachmentsChange, di
|
|
|
103
111
|
if (e.target.files && e.target.files.length > 0) handleFiles(e.target.files);
|
|
104
112
|
e.target.value = "";
|
|
105
113
|
}, [handleFiles]);
|
|
106
|
-
const handleRemove = useCallback((
|
|
107
|
-
|
|
108
|
-
}, [
|
|
114
|
+
const handleRemove = useCallback((attachment) => {
|
|
115
|
+
commit(latest.current.filter((a) => a !== attachment));
|
|
116
|
+
}, [commit]);
|
|
109
117
|
useImperativeHandle(ref, () => ({ addFiles: handleFiles }), [handleFiles]);
|
|
110
118
|
const handleClick = () => {
|
|
111
119
|
fileInputRef.current?.click();
|
|
@@ -148,7 +156,7 @@ function AttachmentUpload({ conversationId, attachments, onAttachmentsChange, di
|
|
|
148
156
|
}),
|
|
149
157
|
/* @__PURE__ */ jsx("button", {
|
|
150
158
|
type: "button",
|
|
151
|
-
onClick: () => handleRemove(
|
|
159
|
+
onClick: () => handleRemove(attachment),
|
|
152
160
|
className: "p-0.5 rounded hover:bg-(--theme-error-muted)",
|
|
153
161
|
title: common.remove,
|
|
154
162
|
disabled,
|
|
@@ -92,10 +92,30 @@ function DiffRows({ rows, lang }) {
|
|
|
92
92
|
function EditDiffBlock({ part }) {
|
|
93
93
|
const { strings } = useChatConfig();
|
|
94
94
|
const t = strings.agentTranscript;
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
95
|
+
const diff = useMemo(() => {
|
|
96
|
+
const a = parseEditArgs(part.args);
|
|
97
|
+
if (!a) return null;
|
|
98
|
+
if (part.name === "EditFile") {
|
|
99
|
+
const model = computeEditDiff(a.old_string ?? "", a.new_string ?? "", part.startLine);
|
|
100
|
+
return {
|
|
101
|
+
a,
|
|
102
|
+
model,
|
|
103
|
+
outcome: strings.tools.edited(model.added, model.removed)
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
a,
|
|
108
|
+
model: computeWriteRows(a.content ?? ""),
|
|
109
|
+
outcome: editSummary(part.name, part.args, strings.tools)
|
|
110
|
+
};
|
|
111
|
+
}, [
|
|
112
|
+
part.name,
|
|
113
|
+
part.args,
|
|
114
|
+
part.startLine,
|
|
115
|
+
strings.tools
|
|
116
|
+
]);
|
|
117
|
+
if (!diff) return null;
|
|
118
|
+
const { a, model, outcome } = diff;
|
|
99
119
|
return /* @__PURE__ */ jsxs("div", {
|
|
100
120
|
className: "mt-1.5 space-y-1",
|
|
101
121
|
children: [
|
|
@@ -210,10 +230,17 @@ function ToolCallBlock({ part }) {
|
|
|
210
230
|
const header = toolHeaderFor(part.name);
|
|
211
231
|
const argSummary = summarizeToolArgs(header.arg, part.args);
|
|
212
232
|
const isDiff = isEditDiffTool(part.name);
|
|
213
|
-
const summary = part.status === "running" ? t.running : toolSummary(part.name, part.args, part.result ?? "", part.status === "error", {
|
|
233
|
+
const summary = useMemo(() => part.status === "running" ? t.running : toolSummary(part.name, part.args, part.result ?? "", part.status === "error", {
|
|
214
234
|
editSummary,
|
|
215
235
|
strings: strings.tools
|
|
216
|
-
})
|
|
236
|
+
}), [
|
|
237
|
+
part.status,
|
|
238
|
+
part.name,
|
|
239
|
+
part.args,
|
|
240
|
+
part.result,
|
|
241
|
+
t.running,
|
|
242
|
+
strings.tools
|
|
243
|
+
]);
|
|
217
244
|
return /* @__PURE__ */ jsxs("div", {
|
|
218
245
|
className: "py-1.5 text-sm",
|
|
219
246
|
"data-testid": "tool-call-part",
|
package/dist/ai/strings.d.ts
CHANGED
|
@@ -139,6 +139,8 @@ export interface ChatStrings {
|
|
|
139
139
|
addFiles: string;
|
|
140
140
|
maxSizePerFile: (size: string) => string;
|
|
141
141
|
uploading: string;
|
|
142
|
+
/** On a file added where the chat has nowhere to upload it. */
|
|
143
|
+
uploadUnavailable: string;
|
|
142
144
|
};
|
|
143
145
|
attachmentManager: {
|
|
144
146
|
title: string;
|
package/dist/ai/strings.js
CHANGED
|
@@ -174,7 +174,8 @@ var defaultChatStrings = {
|
|
|
174
174
|
releaseToUpload: "Release to upload",
|
|
175
175
|
addFiles: "Add files (drag or click)",
|
|
176
176
|
maxSizePerFile: (size) => `Max ${size} per file`,
|
|
177
|
-
uploading: "Uploading…"
|
|
177
|
+
uploading: "Uploading…",
|
|
178
|
+
uploadUnavailable: "Attachments can’t be uploaded here"
|
|
178
179
|
},
|
|
179
180
|
attachmentManager: {
|
|
180
181
|
title: "Attachments",
|
|
@@ -3,9 +3,14 @@ import { useLink, useStrings } from "./Provider.js";
|
|
|
3
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
//#region src/components/AppLink.tsx
|
|
5
5
|
function leavesApp(url) {
|
|
6
|
-
if (
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
if (typeof window === "undefined") return /^([a-z][a-z\d+.-]*:|[/\\]{2})/i.test(url.replace(/[\t\n\r]/g, "").trimStart());
|
|
7
|
+
const { origin, protocol } = window.location;
|
|
8
|
+
try {
|
|
9
|
+
const resolved = new URL(url, window.location.href);
|
|
10
|
+
return resolved.origin !== origin || resolved.protocol !== protocol;
|
|
11
|
+
} catch {
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
9
14
|
}
|
|
10
15
|
/** True for a click the browser would open in place: the primary button with
|
|
11
16
|
* no modifier. A link's own onClick work, such as closing a drawer or clearing
|
|
@@ -18,11 +18,17 @@ var STATUS_CLASSES = {
|
|
|
18
18
|
busy: "bg-(--theme-error)",
|
|
19
19
|
away: "bg-(--theme-warning)"
|
|
20
20
|
};
|
|
21
|
+
var graphemes = typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(void 0, { granularity: "grapheme" }) : void 0;
|
|
22
|
+
function firstCharacter(text) {
|
|
23
|
+
if (!text) return;
|
|
24
|
+
if (graphemes) return graphemes.segment(text)[Symbol.iterator]().next().value?.segment;
|
|
25
|
+
return Array.from(text)[0];
|
|
26
|
+
}
|
|
21
27
|
/** Up to two initials: the first and last word of a name split on spaces and dots. */
|
|
22
28
|
function avatarInitials(name) {
|
|
23
29
|
const parts = (name ?? "").split(/[\s.]+/).filter(Boolean);
|
|
24
|
-
const first = parts[0]
|
|
25
|
-
const last = parts.at(-1)
|
|
30
|
+
const first = firstCharacter(parts[0]);
|
|
31
|
+
const last = firstCharacter(parts.at(-1));
|
|
26
32
|
if (parts.length >= 2 && first && last) return (first + last).toUpperCase();
|
|
27
33
|
return first?.toUpperCase() || "?";
|
|
28
34
|
}
|
|
@@ -48,7 +54,7 @@ function Avatar$1({ src, name, colorKey, size = "md", status, className }) {
|
|
|
48
54
|
}, src), /* @__PURE__ */ jsx(Avatar.Fallback, {
|
|
49
55
|
"aria-hidden": "true",
|
|
50
56
|
className: cx("flex size-full items-center justify-center", colorBy ? categoryClasses(colorBy) : "bg-gradient-to-br from-[var(--gradient-start,var(--theme-accent))] to-[var(--gradient-mid,var(--theme-link))] text-(--theme-accent-text)"),
|
|
51
|
-
children: size === "xs" ? avatarInitials(name)
|
|
57
|
+
children: size === "xs" ? firstCharacter(avatarInitials(name)) : avatarInitials(name)
|
|
52
58
|
})]
|
|
53
59
|
}), status && /* @__PURE__ */ jsx("span", {
|
|
54
60
|
"data-testid": "online-indicator",
|
|
@@ -4,5 +4,6 @@ export type ExternalLinkProps = Omit<ComponentPropsWithRef<'a'>, 'target' | 'rel
|
|
|
4
4
|
icon?: boolean;
|
|
5
5
|
};
|
|
6
6
|
/** A link that opens in a new tab, says so to screen readers, and gives the
|
|
7
|
-
* page it opens no hold on this one.
|
|
8
|
-
|
|
7
|
+
* page it opens no hold on this one. Its href passes `safeUrl`, as every
|
|
8
|
+
* component's does, and one that doesn't is dropped. */
|
|
9
|
+
export declare function ExternalLink({ href, children, icon, className, ...props }: ExternalLinkProps): import("react").JSX.Element;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
2
|
import { ExternalLinkIcon } from "../icons.js";
|
|
3
|
+
import { safeUrl } from "../safeUrl.js";
|
|
3
4
|
import { useStrings } from "./Provider.js";
|
|
4
5
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
6
|
//#region src/components/ExternalLink.tsx
|
|
6
7
|
/** A link that opens in a new tab, says so to screen readers, and gives the
|
|
7
|
-
* page it opens no hold on this one.
|
|
8
|
-
|
|
8
|
+
* page it opens no hold on this one. Its href passes `safeUrl`, as every
|
|
9
|
+
* component's does, and one that doesn't is dropped. */
|
|
10
|
+
function ExternalLink({ href, children, icon = false, className, ...props }) {
|
|
9
11
|
const { common } = useStrings();
|
|
10
12
|
return /* @__PURE__ */ jsxs("a", {
|
|
11
13
|
...props,
|
|
14
|
+
href: safeUrl(href),
|
|
12
15
|
target: "_blank",
|
|
13
16
|
rel: "noopener noreferrer",
|
|
14
17
|
className: cx(icon && "inline-flex items-center gap-1", className),
|
|
@@ -59,6 +59,12 @@ export interface FieldProps {
|
|
|
59
59
|
size?: FieldBoxSize | undefined;
|
|
60
60
|
/** A set of controls, such as radios or checkboxes, that a legend names instead of a label. */
|
|
61
61
|
group?: boolean | undefined;
|
|
62
|
+
/** Puts a checkbox, a switch or a lone radio before its label, as such a control reads: the
|
|
63
|
+
* control, then its label and ?, with the description and error under the label. A click on
|
|
64
|
+
* the label sets the control. Inline, or in a row of fields, it sits in the control column,
|
|
65
|
+
* its first line on the line of the labels beside it. Not for a `group`, whose options are
|
|
66
|
+
* each a `FieldOption`. */
|
|
67
|
+
controlFirst?: boolean | undefined;
|
|
62
68
|
/** The control's id; one is made up when left out. */
|
|
63
69
|
id?: string | undefined;
|
|
64
70
|
className?: string | undefined;
|
|
@@ -77,7 +83,7 @@ export declare function ErrorText({ id, children }: {
|
|
|
77
83
|
}): import("react").JSX.Element | null;
|
|
78
84
|
/** A control with its label, help and error, tied to it for assistive technology. One frame
|
|
79
85
|
* for every field, in a form or standing alone. */
|
|
80
|
-
export declare function Field({ label, hint, help, actions, description, error, invalid, required, disabled, layout, compact, size, group, id: ownId, className, children, }: FieldProps): import("react").JSX.Element;
|
|
86
|
+
export declare function Field({ label, hint, help, actions, description, error, invalid, required, disabled, layout, compact, size, group, controlFirst, id: ownId, className, children, }: FieldProps): import("react").JSX.Element;
|
|
81
87
|
/** One checkbox or radio and its label inside a Field, such as an option of a group. A bare
|
|
82
88
|
* `<label>` there would take the Field's label as its own, so every option read as the field. */
|
|
83
89
|
export declare function FieldOption({ disabled, className, children, }: {
|