@parallelworks/ui 0.57.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 +60 -4
- package/dist/access/PrincipalPicker.js +2 -1
- package/dist/ai/agent/diff.d.ts +1 -0
- package/dist/ai/agent/diff.js +50 -3
- package/dist/ai/ai.css +38 -0
- 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.d.ts +5 -1
- package/dist/components/Avatar.js +13 -5
- 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/Markdown.d.ts +3 -0
- package/dist/components/Markdown.js +2 -1
- package/dist/components/MarkdownImpl.d.ts +1 -1
- package/dist/components/MarkdownImpl.js +5 -3
- package/dist/components/Provider.d.ts +16 -1
- package/dist/components/Provider.js +17 -3
- package/dist/components/StickyBar.d.ts +14 -0
- package/dist/components/StickyBar.js +22 -0
- package/dist/components/Table.d.ts +7 -5
- package/dist/components/Table.js +6 -2
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +5 -2
- package/dist/components/category.d.ts +9 -0
- package/dist/components/category.js +30 -0
- package/dist/components/httpsImages.d.ts +12 -0
- package/dist/components/httpsImages.js +27 -0
- package/dist/components/stickyTop.d.ts +14 -0
- package/dist/components/stickyTop.js +93 -0
- package/dist/file-explorer/FileExplorer.js +21 -15
- package/dist/graph/DependencyGraph.d.ts +14 -1
- package/dist/graph/DependencyGraph.js +169 -77
- package/dist/graph/JobSummary.js +13 -3
- package/dist/graph/StepSummary.d.ts +4 -0
- package/dist/graph/StepSummary.js +31 -0
- package/dist/graph/index.d.ts +1 -0
- package/dist/graph/index.js +2 -1
- package/dist/graph/types.d.ts +3 -0
- package/dist/graph/util.d.ts +1 -1
- package/dist/icons.d.ts +1 -0
- package/dist/icons.js +3 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -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/logviewer/LogViewer.js +3 -3
- package/dist/styles/theme.css +27 -0
- package/dist/styles.css +1 -1
- package/dist/theme/derive.d.ts +2 -1
- package/dist/theme/derive.js +45 -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 |
|
|
@@ -84,7 +91,7 @@ Each need has one component. Reach for it before styling markup by hand.
|
|
|
84
91
|
| How full something is, how far a task has come | `Meter`, `ProgressBar` |
|
|
85
92
|
| Pages of a long list | `Pagination` |
|
|
86
93
|
| A grouped page of settings | `SettingsCard` with `SettingRow`s |
|
|
87
|
-
| A person | `Avatar
|
|
94
|
+
| A person | `Avatar`, colored by name without a picture |
|
|
88
95
|
| A status in words | `StatusBadge`, or `StatusDot` beside text that says it |
|
|
89
96
|
| A run's, job's or step's status beside its name | `RunStatusIcon`; `StatusBadge` with `runStatusTone` to say it in words |
|
|
90
97
|
| A copy action | `CopyButton`; `CopyCodeBlock` for a command or snippet; `useCopy` for a menu row or a control of your own |
|
|
@@ -312,6 +319,16 @@ icon beside an error message may keep the message's text shade through
|
|
|
312
319
|
from the accent's hue when they come within 30° of it, so a keyword never looks
|
|
313
320
|
like a link.
|
|
314
321
|
|
|
322
|
+
Eight category colors tell things apart without ranking them:
|
|
323
|
+
`--theme-category-1` to `--theme-category-8` are soft fills at hues 45° apart,
|
|
324
|
+
all at one lightness and chroma so none reads louder, and each
|
|
325
|
+
`--theme-category-N-text` is solved to secondary text's targets on its fill. An
|
|
326
|
+
`Avatar` without a picture takes one, picked by an FNV-1a hash of its name (or
|
|
327
|
+
its `colorKey`), so the same name has the same color on every page; a nameless
|
|
328
|
+
one keeps the accent. `categoryOf(key)` gives the same pick, 1–8, and
|
|
329
|
+
`categoryClasses(key)` its fill and text classes, for anything else drawn
|
|
330
|
+
beside a name, such as a group's icon.
|
|
331
|
+
|
|
315
332
|
Every color is mapped into sRGB with the CSS Color 4 method, which lowers
|
|
316
333
|
chroma at fixed lightness and hue. It is mapped into display-p3 too: where the
|
|
317
334
|
wider color differs from sRGB by more than one just-noticeable difference
|
|
@@ -343,9 +360,20 @@ leaves unset follows the `color-scheme` it sets:
|
|
|
343
360
|
```css
|
|
344
361
|
/* In a Tailwind v4 build: */
|
|
345
362
|
@import '@parallelworks/ui/theme.css';
|
|
346
|
-
@source '../node_modules/@parallelworks/ui/
|
|
363
|
+
@source '../node_modules/@parallelworks/ui/dist/components';
|
|
364
|
+
@source '../node_modules/@parallelworks/ui/dist/list';
|
|
347
365
|
```
|
|
348
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
|
+
|
|
349
377
|
Consumers without a Tailwind build can import the compiled standalone
|
|
350
378
|
stylesheet instead:
|
|
351
379
|
|
|
@@ -566,13 +594,41 @@ root:
|
|
|
566
594
|
| ------------------- | ----------------------------------------------------------------------- |
|
|
567
595
|
| `--app-inset-top` | The height of a fixed top bar. Drawers and toasts start below it. |
|
|
568
596
|
| `--app-inset-start` | The width of a fixed sidebar on the start side. Toasts center over the page beside it, and a drawer at its widest leaves that strip, and never less than 2.5rem. Update it when the sidebar collapses. |
|
|
569
|
-
| `--table-header-top` | How far below the top of its scrolling box a table's sticky header row stops (`Table stickyHeader`, which list tables use). Set it on the scrolling box, or an ancestor, when something else sticks at that box's top, such as a page header in a page that scrolls as a whole. Inside `ListPage` the rows scroll in their own box below the header and toolbar, so it stays 0. |
|
|
570
597
|
|
|
571
598
|
```ts
|
|
572
599
|
document.documentElement.style.setProperty('--app-inset-top', '56px')
|
|
573
600
|
document.documentElement.style.setProperty('--app-inset-start', '220px')
|
|
574
601
|
```
|
|
575
602
|
|
|
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
|
+
```
|
|
631
|
+
|
|
576
632
|
Everything the package lays over the page stacks on one scale, set on the
|
|
577
633
|
document root by `base.css`. Give the host's own fixed elements a place on it,
|
|
578
634
|
such as `z-index: 100` for a top bar, rather than a z-index chosen to win:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
2
|
import { OrganizationIcon, RefreshIcon, UsersIcon } from "../icons.js";
|
|
3
3
|
import { useQueryMatch } from "../components/search.js";
|
|
4
|
+
import { categoryClasses } from "../components/category.js";
|
|
4
5
|
import { Avatar } from "../components/Avatar.js";
|
|
5
6
|
import { Callout } from "../components/Callout.js";
|
|
6
7
|
import { Combobox } from "../components/Combobox.js";
|
|
@@ -35,7 +36,7 @@ function PrincipalTile({ principal, compact = false, className }) {
|
|
|
35
36
|
const Icon = everyone ? OrganizationIcon : UsersIcon;
|
|
36
37
|
return /* @__PURE__ */ jsx("span", {
|
|
37
38
|
"aria-hidden": "true",
|
|
38
|
-
className: cx("flex size-8 shrink-0 items-center justify-center rounded-md [&_svg]:size-4", compact && "size-6 [&_svg]:size-3.5", everyone ? "bg-(--theme-info-muted) text-(--theme-info-text)" :
|
|
39
|
+
className: cx("flex size-8 shrink-0 items-center justify-center rounded-md [&_svg]:size-4", compact && "size-6 [&_svg]:size-3.5", everyone ? "bg-(--theme-info-muted) text-(--theme-info-text)" : categoryClasses(principal.name), className),
|
|
39
40
|
children: /* @__PURE__ */ jsx(Icon, {})
|
|
40
41
|
});
|
|
41
42
|
}
|
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 };
|
package/dist/ai/ai.css
CHANGED
|
@@ -182,6 +182,44 @@
|
|
|
182
182
|
.markdown [data-streamdown="table"] tbody tr:last-child {
|
|
183
183
|
border-bottom: 0;
|
|
184
184
|
}
|
|
185
|
+
/* Markdown set inside other UI, such as a step's summary, reads at the page's
|
|
186
|
+
* text size, its headings a step above it rather than at a document's sizes. */
|
|
187
|
+
.markdown-compact {
|
|
188
|
+
font-size: 0.875rem;
|
|
189
|
+
line-height: 1.5;
|
|
190
|
+
}
|
|
191
|
+
.markdown-compact :is(h1, h2, h3, h4, h5, h6) {
|
|
192
|
+
font-size: 0.875rem;
|
|
193
|
+
line-height: 1.25rem;
|
|
194
|
+
font-weight: 600;
|
|
195
|
+
}
|
|
196
|
+
.markdown-compact :is(h1, h2) {
|
|
197
|
+
font-size: 1rem;
|
|
198
|
+
line-height: 1.5rem;
|
|
199
|
+
}
|
|
200
|
+
.markdown-compact [data-streamdown="table-wrapper"] table,
|
|
201
|
+
.markdown-compact [data-streamdown="table"] :is(th, td) {
|
|
202
|
+
font-size: 0.875rem;
|
|
203
|
+
line-height: 1.25rem;
|
|
204
|
+
}
|
|
205
|
+
/* A summary's tables are mostly a few short columns of counts: they hug their
|
|
206
|
+
* content so each number sits by its label, and a phone fits them unscrolled. */
|
|
207
|
+
.markdown-compact table[data-streamdown="table"] {
|
|
208
|
+
width: auto;
|
|
209
|
+
min-width: min(100%, 20rem);
|
|
210
|
+
}
|
|
211
|
+
.markdown-compact [data-streamdown="table"] :is(th, td) {
|
|
212
|
+
min-width: 0;
|
|
213
|
+
padding-inline-end: 2rem;
|
|
214
|
+
font-variant-numeric: tabular-nums;
|
|
215
|
+
}
|
|
216
|
+
.markdown-compact [data-streamdown="table"] th {
|
|
217
|
+
font-weight: 500;
|
|
218
|
+
color: var(--theme-muted-text-color);
|
|
219
|
+
}
|
|
220
|
+
.markdown-compact :not(pre) > code {
|
|
221
|
+
font-size: 0.8125rem;
|
|
222
|
+
}
|
|
185
223
|
@media (prefers-reduced-motion: reduce) {
|
|
186
224
|
.markdown [data-streamdown="table-wrapper"] > div:first-child {
|
|
187
225
|
transition: none;
|
|
@@ -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
|
|
@@ -4,6 +4,10 @@ export interface AvatarProps {
|
|
|
4
4
|
/** Image URL. Until it loads, or when it fails, the initials of `name` show. */
|
|
5
5
|
src?: string | null | undefined;
|
|
6
6
|
name?: string | undefined;
|
|
7
|
+
/** Picks the fallback's color from the theme's categorical palette, so the same key gets the
|
|
8
|
+
* same color everywhere. Defaults to `name`; pass a stable id when names can repeat or
|
|
9
|
+
* change, or `null` for the accent fill whatever the name. Without either, the accent fills it. */
|
|
10
|
+
colorKey?: string | null | undefined;
|
|
7
11
|
size?: AvatarSize | undefined;
|
|
8
12
|
status?: AvatarStatus | undefined;
|
|
9
13
|
className?: string | undefined;
|
|
@@ -13,4 +17,4 @@ export declare function avatarInitials(name: string | undefined): string;
|
|
|
13
17
|
/** A person's picture, or their initials while it loads or when it fails. It is
|
|
14
18
|
* one image to assistive technology, named for the person, whichever shows,
|
|
15
19
|
* and their presence when it has a status dot. */
|
|
16
|
-
export declare function Avatar({ src, name, size, status, className }: AvatarProps): import("react").JSX.Element;
|
|
20
|
+
export declare function Avatar({ src, name, colorKey, size, status, className }: AvatarProps): import("react").JSX.Element;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { cx } from "../cx.js";
|
|
2
2
|
import { useStrings } from "./Provider.js";
|
|
3
|
+
import { categoryClasses } from "./category.js";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
import { Avatar } from "@base-ui/react/avatar";
|
|
5
6
|
//#region src/components/Avatar.tsx
|
|
@@ -17,20 +18,27 @@ var STATUS_CLASSES = {
|
|
|
17
18
|
busy: "bg-(--theme-error)",
|
|
18
19
|
away: "bg-(--theme-warning)"
|
|
19
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
|
+
}
|
|
20
27
|
/** Up to two initials: the first and last word of a name split on spaces and dots. */
|
|
21
28
|
function avatarInitials(name) {
|
|
22
29
|
const parts = (name ?? "").split(/[\s.]+/).filter(Boolean);
|
|
23
|
-
const first = parts[0]
|
|
24
|
-
const last = parts.at(-1)
|
|
30
|
+
const first = firstCharacter(parts[0]);
|
|
31
|
+
const last = firstCharacter(parts.at(-1));
|
|
25
32
|
if (parts.length >= 2 && first && last) return (first + last).toUpperCase();
|
|
26
33
|
return first?.toUpperCase() || "?";
|
|
27
34
|
}
|
|
28
35
|
/** A person's picture, or their initials while it loads or when it fails. It is
|
|
29
36
|
* one image to assistive technology, named for the person, whichever shows,
|
|
30
37
|
* and their presence when it has a status dot. */
|
|
31
|
-
function Avatar$1({ src, name, size = "md", status, className }) {
|
|
38
|
+
function Avatar$1({ src, name, colorKey, size = "md", status, className }) {
|
|
32
39
|
const { common } = useStrings();
|
|
33
40
|
const who = name || common.userAvatar;
|
|
41
|
+
const colorBy = colorKey === void 0 ? name?.trim() : colorKey;
|
|
34
42
|
return /* @__PURE__ */ jsxs("span", {
|
|
35
43
|
role: "img",
|
|
36
44
|
"aria-label": status ? common.withPresence(who, common.presence[status]) : who,
|
|
@@ -45,8 +53,8 @@ function Avatar$1({ src, name, size = "md", status, className }) {
|
|
|
45
53
|
className: "absolute inset-0 size-full object-cover data-error:hidden data-loading:invisible"
|
|
46
54
|
}, src), /* @__PURE__ */ jsx(Avatar.Fallback, {
|
|
47
55
|
"aria-hidden": "true",
|
|
48
|
-
className: "flex size-full items-center justify-center bg-gradient-to-br from-[var(--gradient-start,var(--theme-accent))] to-[var(--gradient-mid,var(--theme-link))] text-(--theme-accent-text)",
|
|
49
|
-
children: size === "xs" ? avatarInitials(name)
|
|
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)"),
|
|
57
|
+
children: size === "xs" ? firstCharacter(avatarInitials(name)) : avatarInitials(name)
|
|
50
58
|
})]
|
|
51
59
|
}), status && /* @__PURE__ */ jsx("span", {
|
|
52
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, }: {
|