@lotics/ui 13.9.0 → 14.0.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/docs/ai_patterns.md +57 -37
- package/docs/catalog.md +27 -9
- package/examples/tpl_dieline.tsx +21 -18
- package/examples/tpl_documents.tsx +19 -12
- package/examples/tpl_lookup.tsx +18 -13
- package/examples/tpl_record.tsx +21 -16
- package/examples/tpl_task_board.tsx +13 -8
- package/package.json +7 -2
- package/src/agent_progress.tsx +11 -12
- package/src/agent_run.tsx +82 -121
- package/src/agent_transform.ts +102 -0
- package/src/choice_list.tsx +103 -14
- package/src/clarify.tsx +31 -15
- package/src/clarify_wizard.tsx +84 -0
- package/src/json_panel.tsx +25 -13
- package/src/locale.tsx +4 -0
- package/src/primitives_purity.test.ts +15 -1
- package/src/table.tsx +5 -1
package/docs/ai_patterns.md
CHANGED
|
@@ -93,26 +93,27 @@ transition — never leave a live button waiting on a network round-trip:
|
|
|
93
93
|
## Show the work — `AgentRun`
|
|
94
94
|
|
|
95
95
|
`AgentRun` (`@lotics/ui/agent_run`) is a live feed of the agent's work as a TIMELINE. The prop is
|
|
96
|
-
|
|
96
|
+
the agent message's **ai-sdk `parts`** (`UIMessagePart[]`) — the same wire shape chat and app
|
|
97
|
+
agents emit; `@lotics/ui` owns the transform from parts to the render timeline, so there is no
|
|
98
|
+
bespoke transcript type. It renders three part kinds:
|
|
97
99
|
|
|
98
|
-
|
|
|
99
|
-
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
text, NOT all text on top of a flat step list.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
`
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
**Activity grouping.** Consecutive `step` items fold into ONE group. While the agent is mid-tools
|
|
100
|
+
| Part | Renders as |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `text` | Answer prose (`Markdown`), grows as it streams |
|
|
103
|
+
| `reasoning` | Thinking — COLLAPSED, a muted "Thinking" row; press to reveal the Markdown |
|
|
104
|
+
| a tool part — `dynamic-tool` (app agents) or `tool-<name>` (chat) | An activity row with a status dot |
|
|
105
|
+
|
|
106
|
+
Parts appear in the order they happened — a real run is think → text → a burst of calls → more
|
|
107
|
+
text, NOT all text on top of a flat step list. Content parts with no feed representation
|
|
108
|
+
(`source-*`, `file`, `data-*`, `step-start`, `custom`) are skipped. Transparent work, NEVER a bare spinner.
|
|
109
|
+
|
|
110
|
+
**Progressive disclosure.** The feed shows the label + state; the detail is revealed on demand. A
|
|
111
|
+
tool part's `input`/`output` are hidden in the row and **expand in place** on press — auto-built
|
|
112
|
+
Input / Output `JsonPanel`s roll out under the row (the same disclosure pattern as "Thinking",
|
|
113
|
+
never a popover). A tool in an `output-error` state flows like any other call — the dot goes
|
|
114
|
+
amber and its `errorText` shows in the expanded Error panel.
|
|
115
|
+
|
|
116
|
+
**Activity grouping.** Consecutive tool parts fold into ONE group. While the agent is mid-tools
|
|
116
117
|
the tail group is a SINGLE pulsing row whose label swaps in place as each call fires (no growing
|
|
117
118
|
stack of dots); once prose resumes the group settles: a single call becomes one done row, and a
|
|
118
119
|
multi-call group becomes a persistent "{final action} · {n} steps" HEADER (a `complete` terminal
|
|
@@ -120,17 +121,28 @@ dot — distinct from the filled `done` step dots; amber `warning` if any call e
|
|
|
120
121
|
PUT and rolls the calls out BELOW it on press. The run ALWAYS ends on the agent's text — there is
|
|
121
122
|
no global terminal node.
|
|
122
123
|
|
|
123
|
-
**Tool labels.** A
|
|
124
|
-
resolved via a built-in map of the platform record/document
|
|
125
|
-
override (localize THERE — the kit stays English); unknown
|
|
126
|
-
`stepsLabel` localizes the "{n} steps" suffix.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
124
|
+
**Tool labels.** A tool part carries the RAW tool name (`update_records` from `tool-update_records`,
|
|
125
|
+
or a `dynamic-tool`'s `toolName`), resolved via a built-in map of the platform record/document
|
|
126
|
+
tools + an optional `labelForTool` override (localize THERE — the kit stays English); unknown
|
|
127
|
+
names fall back to a prettified form. `stepsLabel` localizes the "{n} steps" suffix. The tool
|
|
128
|
+
`state` collapses ai's lifecycle to running / done / error (an approval-pending state reads as
|
|
129
|
+
running — a read-only feed has no approval control). The whole-run `state` prop
|
|
130
|
+
(`"streaming" | "done" | "error"`) defaults to `streaming` while any tool is running, else `done`.
|
|
131
|
+
|
|
132
|
+
**Errors — two kinds, both surfaced, neither shouting.** An INTERMEDIARY tool failure rides in
|
|
133
|
+
that tool's `output-error` part and flows like a normal call — the amber dot marks it (a settled
|
|
134
|
+
group's header goes amber too), the reason waits in the expanded Error panel; real runs retry and
|
|
135
|
+
move on, so the feed doesn't dramatize it. A BREAKING error that terminates the whole run lives
|
|
136
|
+
OUTSIDE `parts` (chat persists it in the message's `errors`; `useAgentRun().error` carries it) —
|
|
137
|
+
pass it as the `error` prop and it renders as a terminal danger row under the transcript.
|
|
138
|
+
|
|
139
|
+
Fed natively by `@lotics/app-sdk`'s `useAgentRun().parts` (reasoning + per-tool I/O + state come
|
|
140
|
+
for free — no hand-assembly): `<AgentRun parts={run.parts} state={…} error={run.error} />` — see
|
|
141
|
+
[the SDK doc](../../app-sdk/docs/ai.md) for the hook. Requires `ai` as an optional (type-only) peer.
|
|
142
|
+
**Reasoning only shows when the model produces it** — the declared model must support thinking
|
|
143
|
+
(Sonnet 4.6 / Opus 4.8, not Haiku); the renderer shows every reasoning part it's handed.
|
|
144
|
+
|
|
145
|
+
**Limitation:** the collapsed reasoning row's "Thinking" label and the expanded detail's
|
|
134
146
|
"Input"/"Output"/"Error" panel titles are currently English-only (not on the locale provider and
|
|
135
147
|
not prop-overridable); `labelForTool`/`stepsLabel` localize everything else in the feed.
|
|
136
148
|
|
|
@@ -243,15 +255,23 @@ plain `useState` (controlled `status` everywhere); the family owns the mechanics
|
|
|
243
255
|
Undo, the counter, Keep-all, apply gating). Never apply a field with no value — gate unresolved
|
|
244
256
|
conflicts (`keepDisabled`, `applyDisabled`). Editing IS the review.
|
|
245
257
|
|
|
246
|
-
## Ask back — `Clarify`
|
|
258
|
+
## Ask back — `Clarify` / `ClarifyWizard`
|
|
247
259
|
|
|
248
260
|
`Clarify` (`@lotics/ui/clarify`): when the agent is unsure, it asks a question with quick-reply
|
|
249
|
-
options and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run: `question
|
|
250
|
-
`options` (`{ label, value, description
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
options and PAUSES, instead of guessing wrong. Human-in-the-loop input mid-run: `question` (the
|
|
262
|
+
prompt, rendered as-is — no eyebrow chrome), `options` (`{ label, value, description }` — every
|
|
263
|
+
answer carries a REQUIRED one-line description so the human picks without re-deriving it from the
|
|
264
|
+
question; a `ChoiceList`), `onAnswer(value)` resumes the run, the controlled `answer` keeps the pick
|
|
265
|
+
switchable until committed. **`allowCustom`** adds an always-visible, borderless, auto-growing multiline field below the
|
|
266
|
+
options — typing it IS the selection; its text becomes the value (any value not matching an option
|
|
267
|
+
reads as the custom answer).
|
|
268
|
+
|
|
269
|
+
`ClarifyWizard` (`@lotics/ui/clarify_wizard`): a SEQUENCE of clarify questions worked one at a time —
|
|
270
|
+
Back / Next / Cancel / Submit (Next/Submit primary), the step position an eyebrow ("1 / 3") above the
|
|
271
|
+
question — no progress bar. Each step is a `Clarify`; advance only once the
|
|
272
|
+
current question is answered; `onSubmit` gives one `{ value, custom }` per question (aligned by index,
|
|
273
|
+
`custom` true when the answer came from "Other…"), `onCancel` dismisses. All navigation chrome is
|
|
274
|
+
locale-wired (the `clarify` slice). The multi-question form of the ask-back.
|
|
255
275
|
|
|
256
276
|
## Provenance — `Sources`
|
|
257
277
|
|
package/docs/catalog.md
CHANGED
|
@@ -962,11 +962,20 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
962
962
|
- **`composer`** — `Composer`: the adaptive command/chat composer — a compact pill that
|
|
963
963
|
expands for long text + attachments (`pills` + attachment slots, controlled or
|
|
964
964
|
uncontrolled `value`, `onSend`/`onStop`, `sendDisabled` override).
|
|
965
|
-
- **`agent_run`** — `AgentRun` + `
|
|
966
|
-
|
|
967
|
-
`
|
|
968
|
-
|
|
969
|
-
|
|
965
|
+
- **`agent_run`** — `AgentRun` + `resolveToolMeta`: the live streaming work feed. Its prop is the
|
|
966
|
+
agent message's ai-sdk `parts` (`UIMessagePart[]` — text, reasoning, and tool parts), fed straight
|
|
967
|
+
from `useAgentRun().parts`; renders prose, a collapsed "Thinking" disclosure, and grouped tool
|
|
968
|
+
steps whose I/O expands in place on press (no popover). A per-tool failure shows amber with the
|
|
969
|
+
reason in its expanded Error panel; a run-level
|
|
970
|
+
breaking `error` (outside `parts`) renders as a terminal danger row. Needs `ai` as an optional
|
|
971
|
+
type-only peer. AI work ONLY —
|
|
972
|
+
deterministic work (template fill, an export) shows a `loading` state on its trigger and delivers
|
|
973
|
+
at once; dressing it as an agent run is theater.
|
|
974
|
+
- **`agent_transform`** — internal (no component): the one place that folds an ai-sdk `UIMessage`'s
|
|
975
|
+
`parts` into the render timeline (`AgentUIPart`, `toSegments`, `lastRunningStep`), shared by
|
|
976
|
+
`AgentRun` and `AgentProgress`. The sole file permitted to `import type` from `ai`.
|
|
977
|
+
- **`agent_progress`** — `AgentProgress`: `AgentRun`'s compact, floating, expandable form (same
|
|
978
|
+
`parts` prop) — a composer's working state.
|
|
970
979
|
- **`confidence`** — `Confidence` + `ConfidenceLevel` + `levelFromScore`: calibrated
|
|
971
980
|
high/med/low; localized via the provider.
|
|
972
981
|
- **`change_review`** — the COMPOUND review family — frame: `ChangeReview` provider/stack ·
|
|
@@ -980,11 +989,19 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
980
989
|
`ChangeRecord` (THE item card: registers like a Change; tone wash + localized op word;
|
|
981
990
|
verb level follows the decision level) · `ChangeBand` (the raw ± band) ·
|
|
982
991
|
`ChangeValueInput` (the diff-at-rest editor); the `changeReview` locale slice.
|
|
983
|
-
- **`clarify`** — `Clarify`: the agent asks back — a borderless block
|
|
984
|
-
`ChoiceList
|
|
992
|
+
- **`clarify`** — `Clarify` + `ClarifyOption`: the agent asks back — a borderless block
|
|
993
|
+
(the question text + a `ChoiceList`, no card wrapper; an optional muted `eyebrow` sits tight above
|
|
994
|
+
the question — e.g. a wizard's "1 / 3"). `ClarifyOption` requires
|
|
995
|
+
`{ value, label, description }` — the description is MANDATORY (every answer explains itself).
|
|
996
|
+
`allowCustom` adds an always-visible borderless multiline custom-answer field below the options.
|
|
997
|
+
- **`clarify_wizard`** — `ClarifyWizard` + `ClarifyWizardQuestion`/`ClarifyWizardAnswer`: a SEQUENCE
|
|
998
|
+
of `Clarify` questions with Back/Next/Cancel/Submit + a `n / total` position indicator; advance
|
|
999
|
+
only when the current is answered, `onSubmit` returns one `{ value, custom }` per question.
|
|
1000
|
+
Navigation chrome is locale-wired (the `clarify` locale slice).
|
|
985
1001
|
- **`choice_list`** — `ChoiceList` + `ChoiceOption`: selectable answer options as
|
|
986
1002
|
divider-separated rows (no bordered cards) with a per-row focus ring + hover wash; the
|
|
987
|
-
agent's quick-reply surface.
|
|
1003
|
+
agent's quick-reply surface. `allowCustom` appends an always-visible borderless multiline field whose
|
|
1004
|
+
text IS the value (a value not in `options` reads as the custom answer).
|
|
988
1005
|
- **`sources`** — `Sources` + `SourceRef`/`SourceKind` (record | document | table | web |
|
|
989
1006
|
knowledge): provenance chips, per-kind glyphs.
|
|
990
1007
|
- **`finding`** — `Finding` + `FindingComparison` + `FindingSeverity`/`FindingLabels`: one
|
|
@@ -1023,4 +1040,5 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1023
1040
|
a programmatic focus restore inside the suppression window doesn't re-open the editor it
|
|
1024
1041
|
just closed.
|
|
1025
1042
|
- **`json_panel`** — `JsonPanel` (`{title, value}`) + `stringifyData`: a labeled monospace
|
|
1026
|
-
panel for raw/JSON payloads (debug & developer surfaces).
|
|
1043
|
+
panel for raw/JSON payloads (debug & developer surfaces). Height-capped (240) and scrolls
|
|
1044
|
+
inside, so a huge dump (a streamed tool input) can never blow the surface open.
|
package/examples/tpl_dieline.tsx
CHANGED
|
@@ -10,7 +10,9 @@ import { InlineTextInput } from "@lotics/ui/inline_text_input";
|
|
|
10
10
|
import { AgentProgress } from "@lotics/ui/agent_progress";
|
|
11
11
|
import { Composer } from "@lotics/ui/composer";
|
|
12
12
|
import { FileThumbnail, type DisplayFile } from "@lotics/ui/file_thumbnail";
|
|
13
|
-
import {
|
|
13
|
+
import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
14
|
+
|
|
15
|
+
type Part = UIMessagePart<UIDataTypes, UITools>;
|
|
14
16
|
|
|
15
17
|
// A real app fills the attachment from `useFileUpload` (the uploaded file's URL);
|
|
16
18
|
// this mock uses an inline placeholder so the example needs no network image.
|
|
@@ -34,20 +36,20 @@ const MOCK_PHOTO: DisplayFile = {
|
|
|
34
36
|
// carries the single Download. All mock — the recipe, not a CAD engine.
|
|
35
37
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
38
|
|
|
37
|
-
type ScriptStep =
|
|
39
|
+
type ScriptStep = { id: string; label: string };
|
|
38
40
|
type Dims = { L: number; W: number; H: number };
|
|
39
41
|
|
|
40
42
|
const RECOGNIZE: ScriptStep[] = [
|
|
41
|
-
{ id: "r1", label: "Reading the photo"
|
|
42
|
-
{ id: "r2", label: "reference_dielines.search"
|
|
43
|
-
{ id: "r3", label: "Identifying the box style"
|
|
44
|
-
{ id: "r4", label: "Estimating dimensions"
|
|
45
|
-
{ id: "r5", label: "Drafting the dieline"
|
|
43
|
+
{ id: "r1", label: "Reading the photo" },
|
|
44
|
+
{ id: "r2", label: "reference_dielines.search" },
|
|
45
|
+
{ id: "r3", label: "Identifying the box style" },
|
|
46
|
+
{ id: "r4", label: "Estimating dimensions" },
|
|
47
|
+
{ id: "r5", label: "Drafting the dieline" },
|
|
46
48
|
];
|
|
47
49
|
|
|
48
|
-
const
|
|
49
|
-
{ id: "e1", label: "Reading the request"
|
|
50
|
-
{ id: "e2", label: "Recomputing the dieline"
|
|
50
|
+
const EDIT_SCRIPT: ScriptStep[] = [
|
|
51
|
+
{ id: "e1", label: "Reading the request" },
|
|
52
|
+
{ id: "e2", label: "Recomputing the dieline" },
|
|
51
53
|
];
|
|
52
54
|
|
|
53
55
|
const ZOOM_MIN = 0.4;
|
|
@@ -105,12 +107,13 @@ export function TplDieline() {
|
|
|
105
107
|
return () => clearTimeout(t);
|
|
106
108
|
}, [run, revealed]);
|
|
107
109
|
|
|
108
|
-
const liveItems:
|
|
109
|
-
? run.script.slice(0, revealed).map((s, i) =>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
const liveItems: Part[] = run
|
|
111
|
+
? run.script.slice(0, revealed).map((s, i): Part => {
|
|
112
|
+
const running = revealed < run.script.length && i === revealed - 1;
|
|
113
|
+
return running
|
|
114
|
+
? { type: "dynamic-tool", toolName: s.label, toolCallId: s.id, state: "input-available", input: undefined }
|
|
115
|
+
: { type: "dynamic-tool", toolName: s.label, toolCallId: s.id, state: "output-available", input: undefined, output: undefined };
|
|
116
|
+
})
|
|
114
117
|
: [];
|
|
115
118
|
|
|
116
119
|
const startRecognize = () => {
|
|
@@ -135,7 +138,7 @@ export function TplDieline() {
|
|
|
135
138
|
setPhase("working");
|
|
136
139
|
setRevealed(0);
|
|
137
140
|
setRun({
|
|
138
|
-
script:
|
|
141
|
+
script: EDIT_SCRIPT,
|
|
139
142
|
onDone: () => {
|
|
140
143
|
const m = /(\d+)\s*mm/i.exec(text);
|
|
141
144
|
const delta = m ? Number(m[1]) : 5;
|
|
@@ -209,7 +212,7 @@ export function TplDieline() {
|
|
|
209
212
|
<View style={{ position: "absolute", left: 0, right: 0, bottom: 24, alignItems: "center", paddingHorizontal: 16, pointerEvents: "none" }}>
|
|
210
213
|
<View style={{ width: "100%", maxWidth: 620, pointerEvents: "auto" }}>
|
|
211
214
|
{phase === "working" ? (
|
|
212
|
-
<AgentProgress
|
|
215
|
+
<AgentProgress parts={liveItems} state="streaming" />
|
|
213
216
|
) : (
|
|
214
217
|
<Composer
|
|
215
218
|
value={prompt}
|
|
@@ -22,11 +22,14 @@ import { useSelection } from "@lotics/ui/use_selection";
|
|
|
22
22
|
import { FloatingActionBar } from "@lotics/ui/floating_action_bar";
|
|
23
23
|
import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
|
|
24
24
|
import { CardSelectItem } from "@lotics/ui/card_select_item";
|
|
25
|
-
import { AgentRun
|
|
25
|
+
import { AgentRun } from "@lotics/ui/agent_run";
|
|
26
26
|
import { type SourceRef } from "@lotics/ui/sources";
|
|
27
27
|
import { ChangeValueInput, Change, ChangeField, ChangeFields, ChangeReasoning, ChangeRecord, ChangeReview, ChangeReviewActions, ChangeReviewHeader, type ChangeStatus } from "@lotics/ui/change_review";
|
|
28
28
|
|
|
29
29
|
import { CompletionState } from "@lotics/ui/completion_state";
|
|
30
|
+
import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
31
|
+
|
|
32
|
+
type Part = UIMessagePart<UIDataTypes, UITools>;
|
|
30
33
|
|
|
31
34
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
35
|
// Template · Document desk — THE worked example for everything document-driven
|
|
@@ -50,7 +53,7 @@ import { CompletionState } from "@lotics/ui/completion_state";
|
|
|
50
53
|
// split) and drops the toolbar button.
|
|
51
54
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
52
55
|
|
|
53
|
-
type ScriptStep =
|
|
56
|
+
type ScriptStep = { id: string; label: string; detail?: string; kind?: "tool" };
|
|
54
57
|
type Task = "extract" | "check";
|
|
55
58
|
type Phase = "fork" | "running" | "review" | "done";
|
|
56
59
|
|
|
@@ -276,14 +279,16 @@ export function TplDocuments() {
|
|
|
276
279
|
return () => clearTimeout(t);
|
|
277
280
|
}, [phase, revealed, script.length]);
|
|
278
281
|
|
|
279
|
-
const runItems:
|
|
280
|
-
{ type: "text",
|
|
281
|
-
...script.slice(0, Math.min(revealed + 1, script.length)).map((s, i):
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
const runItems: Part[] = [
|
|
283
|
+
{ type: "text", text: (task === "check" ? "Comparing the documents against the record and each other." + (brief.trim() ? ` Also: ${brief.trim()}` : "") : "Reading the documents and comparing them with the record.") },
|
|
284
|
+
...script.slice(0, Math.min(revealed + 1, script.length)).map((s, i): Part =>
|
|
285
|
+
i < revealed
|
|
286
|
+
? { type: "dynamic-tool", toolName: s.label, toolCallId: s.id, state: "output-available", input: undefined, output: undefined }
|
|
287
|
+
: { type: "dynamic-tool", toolName: s.label, toolCallId: s.id, state: "input-available", input: undefined },
|
|
288
|
+
),
|
|
284
289
|
];
|
|
285
290
|
if (revealed >= script.length) {
|
|
286
|
-
runItems.push({ type: "text",
|
|
291
|
+
runItems.push({ type: "text", text: task === "check" ? "Checked 18 fields — 2 disagree, 1 worth noting. The findings are below." : "3 fields already match; 3 need a decision, and the invoice carries 2 lines the order doesn't have yet." });
|
|
287
292
|
}
|
|
288
293
|
|
|
289
294
|
// Apply counts what actually commits: the kept fields.
|
|
@@ -498,7 +503,7 @@ export function TplDocuments() {
|
|
|
498
503
|
</View>
|
|
499
504
|
) : null}
|
|
500
505
|
|
|
501
|
-
{phase === "running" ? <AgentRun
|
|
506
|
+
{phase === "running" ? <AgentRun parts={runItems} state={revealed >= script.length ? "done" : "streaming"} /> : null}
|
|
502
507
|
|
|
503
508
|
{phase === "review" && task === "extract" ? (
|
|
504
509
|
<View style={{ gap: 16 }}>
|
|
@@ -695,9 +700,11 @@ export function TplDocuments() {
|
|
|
695
700
|
</View>
|
|
696
701
|
) : genRunning ? (
|
|
697
702
|
<AgentRun
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
703
|
+
parts={genSteps.slice(0, Math.min(genRevealed + 1, genSteps.length)).map((st, i): Part =>
|
|
704
|
+
i < genRevealed
|
|
705
|
+
? { type: "dynamic-tool", toolName: st.label, toolCallId: st.id, state: "output-available", input: undefined, output: undefined }
|
|
706
|
+
: { type: "dynamic-tool", toolName: st.label, toolCallId: st.id, state: "input-available", input: undefined },
|
|
707
|
+
)}
|
|
701
708
|
state={genRevealed >= genSteps.length ? "done" : "streaming"}
|
|
702
709
|
/>
|
|
703
710
|
) : !genDone ? (
|
package/examples/tpl_lookup.tsx
CHANGED
|
@@ -6,10 +6,13 @@ import { Divider } from "@lotics/ui/divider";
|
|
|
6
6
|
import { Callout, CalloutText, CalloutTitle } from "@lotics/ui/callout";
|
|
7
7
|
import { PressableHighlight } from "@lotics/ui/pressable_highlight";
|
|
8
8
|
import { Composer } from "@lotics/ui/composer";
|
|
9
|
-
import { AgentRun
|
|
9
|
+
import { AgentRun } from "@lotics/ui/agent_run";
|
|
10
10
|
import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
|
|
11
11
|
import { Badge } from "@lotics/ui/badge";
|
|
12
12
|
import { Sources, type SourceRef } from "@lotics/ui/sources";
|
|
13
|
+
import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
14
|
+
|
|
15
|
+
type Part = UIMessagePart<UIDataTypes, UITools>;
|
|
13
16
|
|
|
14
17
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
15
18
|
// Template · Answer desk — describe the goods on the LEFT, the agent RANKS the
|
|
@@ -134,13 +137,13 @@ const CANDIDATES: Candidate[] = [
|
|
|
134
137
|
},
|
|
135
138
|
];
|
|
136
139
|
|
|
137
|
-
type ScriptStep =
|
|
140
|
+
type ScriptStep = { id: string; toolName: string; input?: unknown; output?: unknown };
|
|
138
141
|
const SCRIPT: ScriptStep[] = [
|
|
139
|
-
{ id: "r1",
|
|
140
|
-
{ id: "r2",
|
|
141
|
-
{ id: "r3",
|
|
142
|
-
{ id: "r4",
|
|
143
|
-
{ id: "r5",
|
|
142
|
+
{ id: "r1", toolName: "Reading the description", input: undefined },
|
|
143
|
+
{ id: "r2", toolName: "tariff_schedule.search" },
|
|
144
|
+
{ id: "r3", toolName: "Narrowing the heading", input: undefined },
|
|
145
|
+
{ id: "r4", toolName: "duty_rates.read" },
|
|
146
|
+
{ id: "r5", toolName: "Checking import policies", input: undefined },
|
|
144
147
|
];
|
|
145
148
|
|
|
146
149
|
export function TplLookup() {
|
|
@@ -182,11 +185,13 @@ export function TplLookup() {
|
|
|
182
185
|
};
|
|
183
186
|
|
|
184
187
|
const streaming = phase === "running" && revealed < SCRIPT.length;
|
|
185
|
-
const items:
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
const items: Part[] = SCRIPT.slice(0, revealed).map((s, i): Part => {
|
|
189
|
+
const running = streaming && i === revealed - 1;
|
|
190
|
+
if (running) {
|
|
191
|
+
return { type: "dynamic-tool", toolName: s.toolName, toolCallId: s.id, state: "input-available", input: s.input };
|
|
192
|
+
}
|
|
193
|
+
return { type: "dynamic-tool", toolName: s.toolName, toolCallId: s.id, state: "output-available", input: s.input, output: s.output };
|
|
194
|
+
});
|
|
190
195
|
|
|
191
196
|
return (
|
|
192
197
|
<View style={{ flex: 1, flexDirection: "row", backgroundColor: colors.white }}>
|
|
@@ -200,7 +205,7 @@ export function TplLookup() {
|
|
|
200
205
|
|
|
201
206
|
<ScrollView style={{ flex: 1 }} contentContainerStyle={{ padding: 16, gap: 16 }}>
|
|
202
207
|
{/* current run */}
|
|
203
|
-
<AgentRun
|
|
208
|
+
<AgentRun parts={items} state={streaming ? "streaming" : "done"} />
|
|
204
209
|
|
|
205
210
|
{/* Matches surface only once the run settles — the agent's output,
|
|
206
211
|
never a pre-filled list; they sit at the bottom, under the run. */}
|
package/examples/tpl_record.tsx
CHANGED
|
@@ -65,10 +65,13 @@ import { SearchInput } from "@lotics/ui/search_input";
|
|
|
65
65
|
import { useSelection } from "@lotics/ui/use_selection";
|
|
66
66
|
import { FloatingActionBar } from "@lotics/ui/floating_action_bar";
|
|
67
67
|
import { CardSelectItem } from "@lotics/ui/card_select_item";
|
|
68
|
-
import { AgentRun
|
|
68
|
+
import { AgentRun } from "@lotics/ui/agent_run";
|
|
69
69
|
import { type SourceRef } from "@lotics/ui/sources";
|
|
70
70
|
import { ChangeValueInput, Change, ChangeField, ChangeFields, ChangeReasoning, ChangeRecord, ChangeReview, ChangeReviewActions, ChangeReviewHeader, type ChangeStatus } from "@lotics/ui/change_review";
|
|
71
71
|
import { CompletionState } from "@lotics/ui/completion_state";
|
|
72
|
+
import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
73
|
+
|
|
74
|
+
type Part = UIMessagePart<UIDataTypes, UITools>;
|
|
72
75
|
|
|
73
76
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
74
77
|
// Template · Record — THE record surface: the surface IS the editor, every
|
|
@@ -431,7 +434,7 @@ const CONTENT_MAX = 720;
|
|
|
431
434
|
// that forks into extract / cross-check / edit-with-AI. The two templates
|
|
432
435
|
// share this desk — change it in BOTH (the one divergence: generation lives
|
|
433
436
|
// in this page's output sections, in tpl_documents' toolbar dialog).
|
|
434
|
-
type ScriptStep =
|
|
437
|
+
type ScriptStep = { id: string; toolName: string; input?: unknown; output?: unknown };
|
|
435
438
|
type Task = "extract" | "check";
|
|
436
439
|
type Phase = "fork" | "running" | "review" | "done";
|
|
437
440
|
|
|
@@ -455,15 +458,15 @@ const DOCS: Doc[] = [
|
|
|
455
458
|
];
|
|
456
459
|
|
|
457
460
|
const EXTRACT_STEPS: ScriptStep[] = [
|
|
458
|
-
{ id: "e1",
|
|
459
|
-
{ id: "e2",
|
|
460
|
-
{ id: "e3",
|
|
461
|
-
{ id: "e4",
|
|
461
|
+
{ id: "e1", toolName: "Reading the selected documents" },
|
|
462
|
+
{ id: "e2", toolName: "get_record" },
|
|
463
|
+
{ id: "e3", toolName: "Extracting field values" },
|
|
464
|
+
{ id: "e4", toolName: "Comparing against the record" },
|
|
462
465
|
];
|
|
463
466
|
const CHECK_STEPS: ScriptStep[] = [
|
|
464
|
-
{ id: "c1",
|
|
465
|
-
{ id: "c2",
|
|
466
|
-
{ id: "c3",
|
|
467
|
+
{ id: "c1", toolName: "Reading the selected documents" },
|
|
468
|
+
{ id: "c2", toolName: "get_record" },
|
|
469
|
+
{ id: "c3", toolName: "Cross-checking documents and record" },
|
|
467
470
|
];
|
|
468
471
|
|
|
469
472
|
// The shapes of an extract decision — an ADD, an UPDATE, a REMOVAL, a source
|
|
@@ -959,14 +962,16 @@ export function TplRecord() {
|
|
|
959
962
|
return () => clearTimeout(t);
|
|
960
963
|
}, [phase, revealed, script.length]);
|
|
961
964
|
|
|
962
|
-
const runItems:
|
|
963
|
-
{ type: "text",
|
|
964
|
-
...script.slice(0, Math.min(revealed + 1, script.length)).map((s, i):
|
|
965
|
-
|
|
966
|
-
|
|
965
|
+
const runItems: Part[] = [
|
|
966
|
+
{ type: "text", text: (task === "check" ? "Comparing the documents against the record and each other." + (brief.trim() ? ` Also: ${brief.trim()}` : "") : "Reading the documents and comparing them with the record.") },
|
|
967
|
+
...script.slice(0, Math.min(revealed + 1, script.length)).map((s, i): Part => (
|
|
968
|
+
i < revealed
|
|
969
|
+
? { type: "dynamic-tool", toolName: s.toolName, toolCallId: s.id, state: "output-available", input: s.input, output: s.output }
|
|
970
|
+
: { type: "dynamic-tool", toolName: s.toolName, toolCallId: s.id, state: "input-available", input: s.input }
|
|
971
|
+
)),
|
|
967
972
|
];
|
|
968
973
|
if (revealed >= script.length) {
|
|
969
|
-
runItems.push({ type: "text",
|
|
974
|
+
runItems.push({ type: "text", text: task === "check" ? "Checked 18 fields — 2 disagree, 1 worth noting. The findings are below." : "3 fields already match; 3 need a decision, and the invoice carries 2 lines the order doesn't have yet." });
|
|
970
975
|
}
|
|
971
976
|
|
|
972
977
|
// Apply counts what actually commits: the kept fields.
|
|
@@ -2771,7 +2776,7 @@ export function TplRecord() {
|
|
|
2771
2776
|
</View>
|
|
2772
2777
|
) : null}
|
|
2773
2778
|
|
|
2774
|
-
{phase === "running" ? <AgentRun
|
|
2779
|
+
{phase === "running" ? <AgentRun parts={runItems} state={revealed >= script.length ? "done" : "streaming"} /> : null}
|
|
2775
2780
|
|
|
2776
2781
|
{phase === "review" && task === "extract" ? (
|
|
2777
2782
|
<View style={{ gap: 16 }}>
|
|
@@ -27,12 +27,15 @@ import { DataGrid, gridRowStyle, type DataGridColumn, type DataGridGroup } from
|
|
|
27
27
|
import { CONTROL_RADIUS } from "@lotics/ui/control_surface";
|
|
28
28
|
import { cycleSort, sortBy, type SortState } from "@lotics/ui/sort_header";
|
|
29
29
|
import { Composer } from "@lotics/ui/composer";
|
|
30
|
-
import { AgentRun
|
|
30
|
+
import { AgentRun } from "@lotics/ui/agent_run";
|
|
31
31
|
import { Change, ChangeReview, ChangeReviewActions, ChangeReviewHeader, ChangeSummary, type ChangeStatus } from "@lotics/ui/change_review";
|
|
32
32
|
import { Dialog, DialogHeader, DialogHeaderTitle, DialogScrollArea, DialogFooter } from "@lotics/ui/dialog";
|
|
33
33
|
import { CompletionState } from "@lotics/ui/completion_state";
|
|
34
34
|
import { Confidence, type ConfidenceLevel } from "@lotics/ui/confidence";
|
|
35
35
|
import { toISODate } from "@lotics/ui/format_date";
|
|
36
|
+
import type { UIMessagePart, UIDataTypes, UITools } from "ai";
|
|
37
|
+
|
|
38
|
+
type Part = UIMessagePart<UIDataTypes, UITools>;
|
|
36
39
|
|
|
37
40
|
type TagOption = { value: string; label: string };
|
|
38
41
|
|
|
@@ -253,14 +256,16 @@ export function TplTaskBoard() {
|
|
|
253
256
|
return () => clearTimeout(t);
|
|
254
257
|
}, [phase, revealed]);
|
|
255
258
|
|
|
256
|
-
const runItems:
|
|
257
|
-
{ type: "text",
|
|
258
|
-
...DRAFT_STEPS.slice(0, Math.min(revealed + 1, DRAFT_STEPS.length)).map((s, i):
|
|
259
|
-
|
|
260
|
-
|
|
259
|
+
const runItems: Part[] = [
|
|
260
|
+
{ type: "text", text: "I read your notes and pulled out the work." },
|
|
261
|
+
...DRAFT_STEPS.slice(0, Math.min(revealed + 1, DRAFT_STEPS.length)).map((s, i): Part =>
|
|
262
|
+
i < revealed
|
|
263
|
+
? { type: "dynamic-tool", toolName: s.label, toolCallId: `s${i}`, state: "output-available", input: undefined, output: undefined }
|
|
264
|
+
: { type: "dynamic-tool", toolName: s.label, toolCallId: `s${i}`, state: "input-available", input: undefined },
|
|
265
|
+
),
|
|
261
266
|
];
|
|
262
267
|
if (revealed >= DRAFT_STEPS.length) {
|
|
263
|
-
runItems.push({ type: "text",
|
|
268
|
+
runItems.push({ type: "text", text: `Drafted ${DRAFTED.length} tasks — keep the ones that look right, edit any before adding.` });
|
|
264
269
|
}
|
|
265
270
|
|
|
266
271
|
const propertyCols: DataGridColumn<Task>[] = [
|
|
@@ -384,7 +389,7 @@ export function TplTaskBoard() {
|
|
|
384
389
|
<Composer onSend={startDraft} placeholder="Paste your notes…" autoFocus sendLabel="Draft tasks" />
|
|
385
390
|
</View>
|
|
386
391
|
) : null}
|
|
387
|
-
{phase === "reading" ? <AgentRun
|
|
392
|
+
{phase === "reading" ? <AgentRun parts={runItems} state={revealed >= DRAFT_STEPS.length ? "done" : "streaming"} /> : null}
|
|
388
393
|
{phase === "review" ? (
|
|
389
394
|
<View style={{ gap: 8 }}>
|
|
390
395
|
<ChangeReviewHeader title="Drafted tasks" />
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lotics/ui",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "14.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./vite": {
|
|
@@ -89,6 +89,7 @@
|
|
|
89
89
|
"./change_review": "./src/change_review.tsx",
|
|
90
90
|
"./finding": "./src/finding.tsx",
|
|
91
91
|
"./clarify": "./src/clarify.tsx",
|
|
92
|
+
"./clarify_wizard": "./src/clarify_wizard.tsx",
|
|
92
93
|
"./choice_list": "./src/choice_list.tsx",
|
|
93
94
|
"./sources": "./src/sources.tsx",
|
|
94
95
|
"./icon": "./src/icon.tsx",
|
|
@@ -263,9 +264,13 @@
|
|
|
263
264
|
"react-dom": "^19.2.0",
|
|
264
265
|
"react-native": ">=0.85.0",
|
|
265
266
|
"react-native-svg": ">=15.0.0",
|
|
266
|
-
"react-native-web": ">=0.20.0"
|
|
267
|
+
"react-native-web": ">=0.20.0",
|
|
268
|
+
"ai": ">=7.0.0"
|
|
267
269
|
},
|
|
268
270
|
"peerDependenciesMeta": {
|
|
271
|
+
"ai": {
|
|
272
|
+
"optional": true
|
|
273
|
+
},
|
|
269
274
|
"expo-image": {
|
|
270
275
|
"optional": true
|
|
271
276
|
},
|
package/src/agent_progress.tsx
CHANGED
|
@@ -4,12 +4,16 @@ import { colors } from "./colors";
|
|
|
4
4
|
import { Text } from "./text";
|
|
5
5
|
import { WaveAvatar } from "./wave_avatar";
|
|
6
6
|
import { PressableHighlight } from "./pressable_highlight";
|
|
7
|
-
import { AgentRun, resolveToolMeta
|
|
7
|
+
import { AgentRun, resolveToolMeta } from "./agent_run";
|
|
8
|
+
import { toSegments, lastRunningStep, type AgentUIPart } from "./agent_transform";
|
|
8
9
|
|
|
9
10
|
export interface AgentProgressProps {
|
|
10
|
-
/** The
|
|
11
|
-
|
|
11
|
+
/** The agent message's ai-sdk `parts` — same shape `AgentRun` takes. */
|
|
12
|
+
parts: readonly AgentUIPart[];
|
|
12
13
|
state?: "streaming" | "done" | "error";
|
|
14
|
+
/** A run-level breaking error, forwarded to the expanded `AgentRun` (a terminal
|
|
15
|
+
* danger row). Lives outside `parts` — `useAgentRun().error`. */
|
|
16
|
+
error?: string;
|
|
13
17
|
/** The collapsed-pill label. Defaults to the running step's label (or
|
|
14
18
|
* "Working…" / "Done"). */
|
|
15
19
|
label?: string;
|
|
@@ -30,16 +34,11 @@ export interface AgentProgressProps {
|
|
|
30
34
|
* label, so the surface stays calm until you ask to see the steps.
|
|
31
35
|
*/
|
|
32
36
|
export function AgentProgress(props: AgentProgressProps) {
|
|
33
|
-
const {
|
|
37
|
+
const { parts, state = "streaming", error, label, labelForTool, stepsLabel, defaultExpanded } = props;
|
|
34
38
|
const [expanded, setExpanded] = useState(defaultExpanded ?? false);
|
|
35
39
|
|
|
36
|
-
const running =
|
|
37
|
-
const runningLabel =
|
|
38
|
-
running && running.type === "step"
|
|
39
|
-
? running.kind === "tool"
|
|
40
|
-
? resolveToolMeta(running.label, labelForTool).label
|
|
41
|
-
: running.label
|
|
42
|
-
: undefined;
|
|
40
|
+
const running = lastRunningStep(toSegments(parts));
|
|
41
|
+
const runningLabel = running ? resolveToolMeta(running.toolName, labelForTool).label : undefined;
|
|
43
42
|
const compact = label ?? runningLabel ?? (state === "done" ? "Done" : state === "error" ? "Stopped" : "Working…");
|
|
44
43
|
const streaming = state === "streaming";
|
|
45
44
|
|
|
@@ -48,7 +47,7 @@ export function AgentProgress(props: AgentProgressProps) {
|
|
|
48
47
|
{expanded ? (
|
|
49
48
|
<View style={styles.panel}>
|
|
50
49
|
<ScrollView style={{ maxHeight: 260 }} contentContainerStyle={{ padding: 16 }}>
|
|
51
|
-
<AgentRun
|
|
50
|
+
<AgentRun parts={parts} state={state} error={error} labelForTool={labelForTool} stepsLabel={stepsLabel} />
|
|
52
51
|
</ScrollView>
|
|
53
52
|
</View>
|
|
54
53
|
) : null}
|