@lotics/ui 15.1.1 → 15.2.1
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/AGENTS.md +1 -1
- package/docs/ai_patterns.md +56 -13
- package/docs/catalog.md +5 -1
- package/docs/templates.md +8 -3
- package/examples/tpl_item_list.tsx +73 -59
- package/package.json +1 -1
- package/src/agent_run.tsx +36 -2
- package/src/clarify_wizard.tsx +99 -10
- package/src/locale.tsx +3 -3
package/AGENTS.md
CHANGED
|
@@ -16,7 +16,7 @@ CURRENT major only — upgrading an app across majors is `MIGRATION.md`.
|
|
|
16
16
|
|---|---|
|
|
17
17
|
| [docs/catalog.md](./docs/catalog.md) | **The complete inventory** — Reach-by-role (each data role → the ONE canonical component) + every `@lotics/ui/<module>` entry point (incl. `@lotics/ui/vite`'s `loticsOptimizeDeps` for a custom-code app's `vite.config.ts`). Read before building any screen; reuse first. |
|
|
18
18
|
| [docs/data_entry.md](./docs/data_entry.md) | Which editing pattern for which job — inline edit, fieldset forms, find-or-create (`Combobox`), line items, handoffs, phased records, billing, tags, dispositions, attachments (incl. the three-way file INTAKE: CTA + `FileDropTarget` + `usePasteFiles`), stage gates. |
|
|
19
|
-
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI
|
|
19
|
+
| [docs/ai_patterns.md](./docs/ai_patterns.md) | AI acts, the human stays in charge — composer, live run feed (`AgentRun`), the one law's split (modify → review-before-apply; create → save-direct + result list), findings, provenance, confidence; the UI half of the SDK's [ai doc](../app-sdk/docs/ai.md). |
|
|
20
20
|
| [docs/composition.md](./docs/composition.md) | The design-language contract — canvas + content column, heading altitude, banded cards, register vs inset rows, master-detail `Drawer`, view controls, color discipline, typography, whitespace. |
|
|
21
21
|
| [docs/templates.md](./docs/templates.md) | The map of `examples/tpl_*.tsx` — what shape each template solves and which to start from (copy + adapt, never import) — plus the record-surface composition rules (pipeline order, static shape, decision budget). |
|
|
22
22
|
|
package/docs/ai_patterns.md
CHANGED
|
@@ -11,19 +11,40 @@ file). Every component here is in the [catalog](./catalog.md) inventory.
|
|
|
11
11
|
|
|
12
12
|
## The one law
|
|
13
13
|
|
|
14
|
-
The AI surfaces share ONE law: the agent never commits
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
The AI surfaces share ONE law: the agent never commits **invisibly**. The agent owns judgment
|
|
15
|
+
(recognition, estimation, intent→parameters); the app owns geometry, math, and the write. How
|
|
16
|
+
the human stays in charge follows the stakes — split by whether a before-state exists:
|
|
17
|
+
|
|
18
|
+
- **Modifying an existing record** — a diff exists, and a wrong value can clobber a right one →
|
|
19
|
+
review BEFORE apply: the `ChangeReview` family, Keep/Drop per change, nothing auto-applies.
|
|
20
|
+
- **Creating new records** (intake, import, drafting rows) — there is no before-state, so there
|
|
21
|
+
is nothing to diff → **apply directly, then show the result**. The run ends on a per-item
|
|
22
|
+
result list — the receipt, not a gate. A result row is a plain composition (no component —
|
|
23
|
+
hand-roll it): the outcome mark (`success` = `circle-check` green-600 · `error` =
|
|
24
|
+
`circle-alert` red-600 · needs-attention = `circle-alert` amber-600 · skipped = `minus`
|
|
25
|
+
zinc-400, size 18) · outcome-first title (sm semibold) · the key figures / the reason (xs,
|
|
26
|
+
danger ink on error) · one-line provenance (xs muted) · a right-slot `Button` (an
|
|
27
|
+
open-record jump, a retry); rows stack with `gap: 12`. A batch-level failure lands on
|
|
28
|
+
EVERY row as its reason — never ALSO paint it as a banner above the list (a top `Callout`
|
|
29
|
+
is only for information the rows don't carry). The closing footer button is
|
|
30
|
+
dismissal (secondary "Done"), never primary. Correction uses
|
|
31
|
+
the record's ORDINARY verbs — open it, edit or delete (make the delete workflow CASCADE the
|
|
32
|
+
record's children, so deletion is always safe) — never a special undo affordance duplicating
|
|
33
|
+
the delete verb in a second place. A review gate on a create degenerates into proofreading a
|
|
34
|
+
wall of cards the user blind-confirms; the register shows the same data better. Fewer steps,
|
|
35
|
+
same control.
|
|
36
|
+
|
|
37
|
+
Either way the deterministic app applies the write. Compose the surfaces as a loop, and reach
|
|
38
|
+
for the right one by job.
|
|
18
39
|
|
|
19
40
|
## Which AI surface — decide by the OUTCOME's shape, not the task's topic
|
|
20
41
|
|
|
21
42
|
Three shapes:
|
|
22
43
|
|
|
23
|
-
1. **Field writes** (extract, match, rank, classify →
|
|
24
|
-
(`useAgentRun`)
|
|
25
|
-
change, app-workflow writes, bounded app authority)
|
|
26
|
-
a
|
|
44
|
+
1. **Field writes** (extract, match, rank, classify → records change) → **in-app agent**
|
|
45
|
+
(`useAgentRun`), then split by the one law: modifying an existing record → **`ChangeReview`**
|
|
46
|
+
(Keep/Drop per change, app-workflow writes, bounded app authority); creating new records →
|
|
47
|
+
**save direct + a result list** (correction = the record's ordinary verbs). Either way one button, never a prompt box.
|
|
27
48
|
2. **Evidence for an in-app decision** (cross-check, audit, tie-out → nothing is written; the
|
|
28
49
|
operator acts on what was found) → **in-app agent** + **`Finding`**. Still bounded and
|
|
29
50
|
prompt-free (an optional instructions brief at most): the brief is fixed, the output is
|
|
@@ -161,6 +182,13 @@ for free — no hand-assembly): `<AgentRun parts={run.parts} state={…} error={
|
|
|
161
182
|
[the SDK doc](../../app-sdk/docs/ai.md) for the hook. The `ai` package is a regular dependency of
|
|
162
183
|
`@lotics/ui` — its part TYPES resolve transitively in any consumer's typecheck with nothing to
|
|
163
184
|
install; the imports are type-only (purity-enforced), so no `ai` runtime ever enters a bundle.
|
|
185
|
+
**A run with no parts yet renders the breathing "Starting…" row** (streaming state only) — the
|
|
186
|
+
live dot plus a label that pulses, covering the gap between the CTA press and the first streamed
|
|
187
|
+
part (upload + run creation + first token); a silent feed reads as broken. **The law: pass
|
|
188
|
+
`state="streaming"` from the moment the paid CTA fires — uploads included — and render
|
|
189
|
+
`AgentRun` even with zero parts. Never hand-roll a text/skeleton placeholder in front of the
|
|
190
|
+
feed**; the starting row IS the placeholder, consistent and localized.
|
|
191
|
+
|
|
164
192
|
**Reasoning only shows when the model produces it** — the declared model must support thinking
|
|
165
193
|
(Sonnet / Opus, not Haiku); the renderer shows every reasoning part it's handed.
|
|
166
194
|
|
|
@@ -197,6 +225,13 @@ into it while running and reveals again when done.
|
|
|
197
225
|
|
|
198
226
|
## Review before apply — ONE surface, the compound `ChangeReview` family
|
|
199
227
|
|
|
228
|
+
**When to reach for it — the one law's boundary.** `ChangeReview` is for MODIFYING an existing
|
|
229
|
+
record: a diff exists (the `−` band has something real to show) and a wrong extraction can
|
|
230
|
+
overwrite a right value — the record page's document desk is the home ground. Do NOT put it in
|
|
231
|
+
front of a creation flow (intake, import, bulk drafting): there is no before-state to diff, so
|
|
232
|
+
the "review" is a proofread the user blind-confirms — save direct and end on a result list
|
|
233
|
+
instead (see [the one law](#the-one-law) and `tpl_item_list`'s intake).
|
|
234
|
+
|
|
200
235
|
Every "the agent proposes → the human accepts / edits / dismisses → nothing auto-applies" surface
|
|
201
236
|
composes from twelve pieces in three groups (all in `@lotics/ui/change_review`; worked example:
|
|
202
237
|
[`tpl_record`](../examples/tpl_record.tsx)'s Use-AI extract flow).
|
|
@@ -306,9 +341,13 @@ Back / Next / Cancel / Submit (Next/Submit primary), the step position an eyebro
|
|
|
306
341
|
question — no progress bar, and no eyebrow at all for a single question. Each step is a `Clarify`; advance only once the
|
|
307
342
|
current question is answered; `onSubmit` gives one `{ value, custom }` per question (aligned by index,
|
|
308
343
|
`custom` true when the answer came from "Other…"), `onCancel` dismisses. All navigation chrome is
|
|
309
|
-
locale-wired (the `clarify` slice). The multi-question form of the ask-back.
|
|
344
|
+
locale-wired (the `clarify` slice). The multi-question form of the ask-back. **Placement follows
|
|
345
|
+
the dialog grammar**: standalone (riding a parked run's stream, where no footer exists) the wizard
|
|
346
|
+
renders its own inline action row; as a DIALOG phase, wrap the `Dialog` with `ClarifyWizardScope`
|
|
347
|
+
and put `ClarifyWizardActions` in the `DialogFooter` — the wizard suppresses the inline row and
|
|
348
|
+
drives the footer bar (same verbs, same answered-gating). Worked example:
|
|
310
349
|
[`tpl_item_list`](../examples/tpl_item_list.tsx)'s intake fork (the wizard between the analyze
|
|
311
|
-
and import runs).
|
|
350
|
+
and import runs, its actions in the footer).
|
|
312
351
|
|
|
313
352
|
## Provenance — `Sources`
|
|
314
353
|
|
|
@@ -408,9 +447,13 @@ ranked set, don't cram it into chat prose:
|
|
|
408
447
|
the two AI tasks into one mixed output — the fork is the design.
|
|
409
448
|
- **Intake desk** (in [`tpl_item_list`](../examples/tpl_item_list.tsx)'s "Enter data" fork) — the
|
|
410
449
|
register's New CTA leads with AI: a `FileDropzone` hero → a short ANALYZE run reads the files →
|
|
411
|
-
`ClarifyWizard` asks the ambiguities the analysis surfaced
|
|
412
|
-
`
|
|
413
|
-
|
|
450
|
+
`ClarifyWizard` asks the ambiguities the analysis surfaced (its Cancel/Back/Next/Submit riding
|
|
451
|
+
the `DialogFooter` via `ClarifyWizardScope` + `ClarifyWizardActions`) → the IMPORT run
|
|
452
|
+
**creates the records and the dialog ends on the RESULT LIST** — one result row per record
|
|
453
|
+
(what landed, the key figures — the hand-rolled anatomy in the one law above) — per the one
|
|
454
|
+
law's creation branch: no review gate, the register IS the review, correction is the
|
|
455
|
+
record's ordinary verbs. The manual VARIANTS sit below the dropzone
|
|
456
|
+
as `CardSelectItem`s, each opening the classic create-then-refine form. The template mocks the
|
|
414
457
|
clarify step as TWO app-orchestrated phases (analyze-run → wizard → import-run); a real app
|
|
415
458
|
can do it in ONE run — every app agent carries `ask_user_choice`, so the run PARKS on the
|
|
416
459
|
agent's own question and `useAgentRun().pendingChoice`/`answerChoice` drive the same
|
package/docs/catalog.md
CHANGED
|
@@ -1087,7 +1087,11 @@ source (`src/<module>.tsx`/`.ts`) is the API reference.
|
|
|
1087
1087
|
of `Clarify` questions with Back/Next/Cancel/Submit + a `n / total` position indicator (omitted
|
|
1088
1088
|
for a single question); advance
|
|
1089
1089
|
only when the current is answered, `onSubmit` returns one `{ value, custom }` per question.
|
|
1090
|
-
Navigation chrome is locale-wired (the `clarify` locale slice).
|
|
1090
|
+
Navigation chrome is locale-wired (the `clarify` locale slice). Standalone it renders its own
|
|
1091
|
+
inline action row (a parked run's stream has no footer); inside a dialog, wrap the `Dialog`
|
|
1092
|
+
with **`ClarifyWizardScope`** and put **`ClarifyWizardActions`** in the `DialogFooter` — the
|
|
1093
|
+
wizard suppresses the inline row and drives the footer bar (same verbs, same gating), per the
|
|
1094
|
+
dialog grammar's footer-owns-actions law (worked example: `tpl_item_list`'s clarify phase).
|
|
1091
1095
|
- **`choice_list`** — `ChoiceList` + `ChoiceOption`: selectable answer options as
|
|
1092
1096
|
divider-separated rows (no bordered cards) with a per-row focus ring + hover wash; the
|
|
1093
1097
|
agent's quick-reply surface. `allowCustom` appends an always-visible borderless multiline field whose
|
package/docs/templates.md
CHANGED
|
@@ -218,9 +218,14 @@ screens — register, per-row action, gated selection, and act-on-many in one. T
|
|
|
218
218
|
button). The dialog is CONTROLLED (`open`/`onOpenChange` + `seedNames`) so both the button and
|
|
219
219
|
the top-level drop drive it. The dialog itself: a `FileDropzone` hero (drop the customer's files) → a short ANALYZE stream
|
|
220
220
|
(`AgentRun` in a `FollowScroll`) reads them → `ClarifyWizard` asks the ambiguities the
|
|
221
|
-
analysis surfaced (described options, one custom-answer slot
|
|
222
|
-
|
|
223
|
-
|
|
221
|
+
analysis surfaced (described options, one custom-answer slot; its Cancel/Back/Next/Submit
|
|
222
|
+
live in the `DialogFooter` — `ClarifyWizardScope` wraps the Dialog, `ClarifyWizardActions`
|
|
223
|
+
rides the footer) → the IMPORT stream CREATES the records and the dialog ends on the RESULT
|
|
224
|
+
LIST — one hand-rolled result row per record (outcome mark · what landed · key figures —
|
|
225
|
+
the anatomy in ai_patterns §the one law; secondary "Done" to dismiss) — the one law's
|
|
226
|
+
creation branch
|
|
227
|
+
(ai_patterns.md): no review gate on a create, the register IS the review, correction uses
|
|
228
|
+
the record's ordinary verbs (no drawer on a bulk landing either). BELOW the
|
|
224
229
|
dropzone, "or enter manually" → the record VARIANTS as `CardSelectItem`s, each opening the
|
|
225
230
|
same THREE-field create-then-refine form (a single manual create still opens its drawer).
|
|
226
231
|
The template mocks the clarify step as two app-orchestrated phases; a real app can do it in
|
|
@@ -54,11 +54,10 @@ import { ProgressBar } from "@lotics/ui/progress_bar";
|
|
|
54
54
|
import { Dialog, DialogFooter, DialogHeader, DialogHeaderTitle, DialogScrollArea } from "@lotics/ui/dialog";
|
|
55
55
|
import { AgentRun } from "@lotics/ui/agent_run";
|
|
56
56
|
import { FollowScroll } from "@lotics/ui/follow_scroll";
|
|
57
|
-
import { ClarifyWizard, type ClarifyWizardAnswer, type ClarifyWizardQuestion } from "@lotics/ui/clarify_wizard";
|
|
57
|
+
import { ClarifyWizard, ClarifyWizardActions, ClarifyWizardScope, type ClarifyWizardAnswer, type ClarifyWizardQuestion } from "@lotics/ui/clarify_wizard";
|
|
58
58
|
import { CardSelectItem } from "@lotics/ui/card_select_item";
|
|
59
59
|
import { FileDropzone } from "@lotics/ui/file_dropzone";
|
|
60
60
|
import { FileDropTarget } from "@lotics/ui/file_drop_target";
|
|
61
|
-
import { ChangeField, ChangeRecord, ChangeReview, ChangeReviewActions, ChangeReviewHeader, ChangeValueInput, type ChangeStatus } from "@lotics/ui/change_review";
|
|
62
61
|
import { useScreenSize } from "@lotics/ui/use_screen_size";
|
|
63
62
|
import { MemberSelect } from "@lotics/ui/member_select";
|
|
64
63
|
import { Callout, CalloutText } from "@lotics/ui/callout";
|
|
@@ -1049,8 +1048,13 @@ function LinkedRecordScreen({ ma }: { ma: string }) {
|
|
|
1049
1048
|
// ─── Enter data — the INTAKE fork ────────────────────────────────────────────
|
|
1050
1049
|
// AI FIRST, form as fallback. The one "Enter data" CTA opens a phased dialog:
|
|
1051
1050
|
// drop files (the hero) → a short ANALYZE stream reads them → `ClarifyWizard`
|
|
1052
|
-
// asks the genuine ambiguities the analysis surfaced → the IMPORT stream
|
|
1053
|
-
// the records
|
|
1051
|
+
// asks the genuine ambiguities the analysis surfaced → the IMPORT stream
|
|
1052
|
+
// CREATES the records and the dialog ends on the RESULT LIST — one row per
|
|
1053
|
+
// record with its key figures. This is the one law's creation branch: no
|
|
1054
|
+
// review gate on a create (nothing to diff — the register IS the review);
|
|
1055
|
+
// correction uses the record's ORDINARY verbs (open it, edit or delete);
|
|
1056
|
+
// `ChangeReview` belongs to the record page, where a SINGLE existing record
|
|
1057
|
+
// is being modified.
|
|
1054
1058
|
// Below the dropzone, the MANUAL variants (each flavors the same 3-field
|
|
1055
1059
|
// create-then-refine gate). The mock plays the clarify step as two phases; a
|
|
1056
1060
|
// real app can run it as ONE agent run — every app agent carries
|
|
@@ -1058,7 +1062,7 @@ function LinkedRecordScreen({ ma }: { ma: string }) {
|
|
|
1058
1062
|
// `useAgentRun().pendingChoice`/`answerChoice` drive this same wizard.
|
|
1059
1063
|
|
|
1060
1064
|
type Part = UIMessagePart<UIDataTypes, UITools>;
|
|
1061
|
-
type IntakePhase = "intake" | "analyze" | "clarify" | "running" | "
|
|
1065
|
+
type IntakePhase = "intake" | "analyze" | "clarify" | "running" | "done" | "form";
|
|
1062
1066
|
type ManualVariant = "export" | "import";
|
|
1063
1067
|
|
|
1064
1068
|
type Proposal = { id: string; khach: string; dienThoai: string; phi: number; orders: string };
|
|
@@ -1107,8 +1111,8 @@ const importScript = (grouping: "customer" | "order"): Part[] => [
|
|
|
1107
1111
|
{ type: "reasoning", text: grouping === "customer" ? "Group the six orders under their three customers, one record each, fees summed per customer." : "One record per order — six records, fees split from the order totals." },
|
|
1108
1112
|
doneTool("i1", "Extract order lines"),
|
|
1109
1113
|
doneTool("i2", "Match customers"),
|
|
1110
|
-
doneTool("i3", grouping === "customer" ? "
|
|
1111
|
-
{ type: "text", text: `
|
|
1114
|
+
doneTool("i3", grouping === "customer" ? "Create 3 records" : "Create 6 records"),
|
|
1115
|
+
{ type: "text", text: `Created **${grouping === "customer" ? 3 : 6} records** — they are in the register.` },
|
|
1112
1116
|
];
|
|
1113
1117
|
|
|
1114
1118
|
/** The manual gate stays the THREE-field create-then-refine form — past the
|
|
@@ -1125,6 +1129,7 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1125
1129
|
* auto-analyze), ready for the deliberate "Read with AI". Empty from the button. */
|
|
1126
1130
|
seedDocs: DisplayFile[];
|
|
1127
1131
|
onCreate: (khach: string, dienThoai: string, phi: number) => void;
|
|
1132
|
+
/** The import's save — rows land in the register as the stream finishes. */
|
|
1128
1133
|
onCreateMany: (records: { khach: string; dienThoai: string; phi: number }[]) => void;
|
|
1129
1134
|
}) {
|
|
1130
1135
|
const { small } = useScreenSize();
|
|
@@ -1133,12 +1138,11 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1133
1138
|
const [variant, setVariant] = useState<ManualVariant>("export");
|
|
1134
1139
|
const [grouping, setGrouping] = useState<"customer" | "order">("customer");
|
|
1135
1140
|
// Q2's answer is load-bearing too: it decides how the unknown customer lands
|
|
1136
|
-
// on the
|
|
1141
|
+
// on the created records (create + link, leave unlinked, or the user's own
|
|
1137
1142
|
// instruction via the custom slot).
|
|
1138
1143
|
const [customerPlan, setCustomerPlan] = useState("New customer created and linked");
|
|
1139
|
-
//
|
|
1140
|
-
const [
|
|
1141
|
-
const [fees, setFees] = useState<Record<string, string>>({});
|
|
1144
|
+
// the result list — what the import CREATED, one entry per record
|
|
1145
|
+
const [landed, setLanded] = useState<{ title: string; detail: string }[]>([]);
|
|
1142
1146
|
// form state (the manual gate)
|
|
1143
1147
|
const [khach, setKhach] = useState("");
|
|
1144
1148
|
const [dienThoai, setDienThoai] = useState("");
|
|
@@ -1169,8 +1173,25 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1169
1173
|
setRevealed(n);
|
|
1170
1174
|
if (n >= total) {
|
|
1171
1175
|
clearInterval(t);
|
|
1172
|
-
|
|
1173
|
-
|
|
1176
|
+
if (phase === "analyze") {
|
|
1177
|
+
// Auto-advance: analysis flows into the wizard.
|
|
1178
|
+
setTimeout(() => setPhase("clarify"), 500);
|
|
1179
|
+
} else {
|
|
1180
|
+
// The import SAVES as it lands — the records are created here, and the
|
|
1181
|
+
// dialog advances to the result list where each row carries Undo.
|
|
1182
|
+
setTimeout(() => {
|
|
1183
|
+
const proposals = grouping === "customer" ? PROPOSED_BY_CUSTOMER : PROPOSED_BY_ORDER;
|
|
1184
|
+
onCreateMany(proposals.map((p) => ({ khach: p.khach, dienThoai: p.dienThoai, phi: p.phi })));
|
|
1185
|
+
setLanded(proposals.map((p) => ({
|
|
1186
|
+
title: grouping === "customer" ? p.khach : `${p.orders} — ${p.khach}`,
|
|
1187
|
+
// The wizard's answers are load-bearing: the unknown customer's row
|
|
1188
|
+
// states how it landed (created + linked / left unassigned / the
|
|
1189
|
+
// user's own instruction).
|
|
1190
|
+
detail: [p.orders, p.dienThoai, formatMoney(p.phi), p.khach === "Blue Harbor Foods" ? customerPlan : ""].filter(Boolean).join(" · "),
|
|
1191
|
+
})));
|
|
1192
|
+
setPhase("done");
|
|
1193
|
+
}, 500);
|
|
1194
|
+
}
|
|
1174
1195
|
}
|
|
1175
1196
|
}, 650);
|
|
1176
1197
|
return () => clearInterval(t);
|
|
@@ -1183,16 +1204,10 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1183
1204
|
// dropzone, and the compact add-more dropzone all call this.
|
|
1184
1205
|
const addDocs = (fs: File[]) => setDocs((prev) => [...prev, ...toDisplayFiles(fs)]);
|
|
1185
1206
|
|
|
1186
|
-
const proposals = grouping === "customer" ? PROPOSED_BY_CUSTOMER : PROPOSED_BY_ORDER;
|
|
1187
|
-
// Kept = explicitly ACCEPTED (the kit's review contract): Apply stays disabled
|
|
1188
|
-
// at 0 kept, and the bar's own Keep-all presses every pending card's Keep.
|
|
1189
|
-
const keptCount = proposals.filter((p) => cardStatus[p.id] === "accepted").length;
|
|
1190
|
-
|
|
1191
1207
|
const reset = () => {
|
|
1192
1208
|
setPhase("intake");
|
|
1193
1209
|
setDocs([]);
|
|
1194
|
-
|
|
1195
|
-
setFees({});
|
|
1210
|
+
setLanded([]);
|
|
1196
1211
|
setCustomerPlan("New customer created and linked");
|
|
1197
1212
|
setKhach("");
|
|
1198
1213
|
setDienThoai("");
|
|
@@ -1200,12 +1215,6 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1200
1215
|
};
|
|
1201
1216
|
const close = () => { onOpenChange(false); reset(); };
|
|
1202
1217
|
|
|
1203
|
-
const applyProposals = () => {
|
|
1204
|
-
const kept = proposals.filter((p) => cardStatus[p.id] === "accepted");
|
|
1205
|
-
onCreateMany(kept.map((p) => ({ khach: p.khach, dienThoai: p.dienThoai, phi: Number(fees[p.id] ?? p.phi) || 0 })));
|
|
1206
|
-
close();
|
|
1207
|
-
};
|
|
1208
|
-
|
|
1209
1218
|
const createManual = () => {
|
|
1210
1219
|
if (khach.trim() === "") return;
|
|
1211
1220
|
onCreate(khach.trim(), dienThoai.trim(), phi ?? 0);
|
|
@@ -1215,14 +1224,18 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1215
1224
|
const title =
|
|
1216
1225
|
phase === "form" ? (variant === "export" ? "New export case" : "New import case")
|
|
1217
1226
|
: phase === "intake" ? "Enter data"
|
|
1227
|
+
: phase === "done" ? "Saved"
|
|
1218
1228
|
: "Import from files";
|
|
1219
1229
|
|
|
1220
1230
|
return (
|
|
1221
|
-
<
|
|
1222
|
-
{/* the file/stream phases (intake/analyze/clarify/running/
|
|
1223
|
-
WIDE — room for the thumbnail hero + the AgentRun/ClarifyWizard/
|
|
1224
|
-
|
|
1225
|
-
760 sits just under the kit Dialog's default 786 maxWidth cap.
|
|
1231
|
+
<ClarifyWizardScope>
|
|
1232
|
+
{/* the file/stream phases (intake/analyze/clarify/running/done) run
|
|
1233
|
+
WIDE — room for the thumbnail hero + the AgentRun/ClarifyWizard/result
|
|
1234
|
+
panes; only the manual `form` stays a narrow single-column pane.
|
|
1235
|
+
760 sits just under the kit Dialog's default 786 maxWidth cap.
|
|
1236
|
+
ClarifyWizardScope wraps the Dialog from OUTSIDE so the wizard's
|
|
1237
|
+
Cancel/Back/Next/Submit render in the DialogFooter — the dialog
|
|
1238
|
+
grammar's home for action bars — via ClarifyWizardActions. */}
|
|
1226
1239
|
<Dialog width={phase === "form" ? 480 : 760} open={open} onOpenChange={(o) => { if (!o) close(); }}>
|
|
1227
1240
|
<DialogHeader>
|
|
1228
1241
|
<DialogHeaderTitle>{title}</DialogHeaderTitle>
|
|
@@ -1324,31 +1337,27 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1324
1337
|
</View>
|
|
1325
1338
|
) : null}
|
|
1326
1339
|
|
|
1327
|
-
{phase === "
|
|
1340
|
+
{phase === "done" ? (
|
|
1341
|
+
// The RESULT LIST — one row per created record: mark · what landed ·
|
|
1342
|
+
// the key figures. The receipt, not a gate: correcting a record uses
|
|
1343
|
+
// its ORDINARY verbs (open it in the register, edit or delete — the
|
|
1344
|
+
// app's cascade delete keeps that safe), never a special undo here.
|
|
1345
|
+
// The outcome-mark vocabulary (ai_patterns §the one law): success =
|
|
1346
|
+
// circle-check green-600 · error = circle-alert red-600 · attention =
|
|
1347
|
+
// circle-alert amber-600 · skipped = minus zinc-400. A real app adds
|
|
1348
|
+
// an open-record jump / retry Button on the row's right.
|
|
1328
1349
|
<DialogScrollArea>
|
|
1329
|
-
<View style={{ gap:
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
onReject={() => setCardStatus((s) => ({ ...s, [p.id]: "rejected" }))}
|
|
1341
|
-
onUndo={() => setCardStatus((s) => ({ ...s, [p.id]: "pending" }))}
|
|
1342
|
-
>
|
|
1343
|
-
<ChangeField label="Customer" value={p.khach} summary={p.khach} />
|
|
1344
|
-
{p.khach === "Blue Harbor Foods" ? (
|
|
1345
|
-
<ChangeField label="Customer link" value={customerPlan} summary={customerPlan} />
|
|
1346
|
-
) : null}
|
|
1347
|
-
{p.dienThoai ? <ChangeField label="Phone" value={p.dienThoai} summary={p.dienThoai} /> : null}
|
|
1348
|
-
<ChangeField label="Service fee" value={fees[p.id] ?? String(p.phi)} summary={formatMoney(Number(fees[p.id] ?? p.phi) || 0)}>
|
|
1349
|
-
<ChangeValueInput value={fees[p.id] ?? String(p.phi)} onChangeText={(v) => setFees((s) => ({ ...s, [p.id]: v }))} accessibilityLabel={`Service fee for ${p.khach}`} />
|
|
1350
|
-
</ChangeField>
|
|
1351
|
-
</ChangeRecord>
|
|
1350
|
+
<View style={{ gap: 12 }}>
|
|
1351
|
+
{landed.map((l, i) => (
|
|
1352
|
+
<View key={i} style={{ flexDirection: "row", alignItems: "flex-start", gap: 12 }}>
|
|
1353
|
+
<View style={{ paddingTop: 2 }}>
|
|
1354
|
+
<Icon name="circle-check" size={18} color={colors.green[600]} />
|
|
1355
|
+
</View>
|
|
1356
|
+
<View style={{ flex: 1, gap: 2 }}>
|
|
1357
|
+
<Text size="sm" weight="semibold">{`Created ${l.title}`}</Text>
|
|
1358
|
+
<Text size="xs" color="muted" numberOfLines={1}>{l.detail}</Text>
|
|
1359
|
+
</View>
|
|
1360
|
+
</View>
|
|
1352
1361
|
))}
|
|
1353
1362
|
</View>
|
|
1354
1363
|
</DialogScrollArea>
|
|
@@ -1402,13 +1411,18 @@ function EnterDataDialog({ open, onOpenChange, seedDocs, onCreate, onCreateMany
|
|
|
1402
1411
|
<Button title="Back" color="secondary" onPress={() => { setKhach(""); setDienThoai(""); setPhi(null); setPhase("intake"); }} />
|
|
1403
1412
|
<Button title="Create record" color="primary" disabled={khach.trim() === ""} onPress={createManual} />
|
|
1404
1413
|
</DialogFooter>
|
|
1405
|
-
) : phase === "
|
|
1414
|
+
) : phase === "clarify" ? (
|
|
1415
|
+
<DialogFooter>
|
|
1416
|
+
<ClarifyWizardActions />
|
|
1417
|
+
</DialogFooter>
|
|
1418
|
+
) : phase === "done" ? (
|
|
1406
1419
|
<DialogFooter>
|
|
1407
|
-
|
|
1420
|
+
{/* Dismissal, not an action — the records already landed. */}
|
|
1421
|
+
<Button title="Done" color="secondary" onPress={close} />
|
|
1408
1422
|
</DialogFooter>
|
|
1409
1423
|
) : null}
|
|
1410
1424
|
</Dialog>
|
|
1411
|
-
</
|
|
1425
|
+
</ClarifyWizardScope>
|
|
1412
1426
|
);
|
|
1413
1427
|
}
|
|
1414
1428
|
|
|
@@ -1470,7 +1484,7 @@ export function TplItemList() {
|
|
|
1470
1484
|
// A single manual create opens its drawer for refinement (create-then-refine).
|
|
1471
1485
|
setOpenMa(ma);
|
|
1472
1486
|
};
|
|
1473
|
-
// The AI-import
|
|
1487
|
+
// The AI-import save — rows land at the top of the register; no drawer (a
|
|
1474
1488
|
// bulk landing is reviewed IN the register, refined record by record later).
|
|
1475
1489
|
const createRecordsBulk = (records: { khach: string; dienThoai: string; phi: number }[]) => {
|
|
1476
1490
|
records.forEach((r) => addRow(r.khach, r.dienThoai, r.phi));
|
package/package.json
CHANGED
package/src/agent_run.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useState, type ReactNode } from "react";
|
|
2
|
-
import { StyleSheet, View } from "react-native";
|
|
1
|
+
import { useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
|
+
import { Animated, StyleSheet, View } from "react-native";
|
|
3
3
|
import { colors } from "./colors";
|
|
4
4
|
import { Text } from "./text";
|
|
5
5
|
import { Button } from "./button";
|
|
@@ -136,6 +136,13 @@ export function AgentRun(props: AgentRunProps) {
|
|
|
136
136
|
|
|
137
137
|
return (
|
|
138
138
|
<View accessibilityLabel={accessibilityLabel} style={{ gap: 10 }}>
|
|
139
|
+
{/* The run has started but nothing has streamed yet (upload, run creation,
|
|
140
|
+
the model's first token) — a silent feed reads as broken, so the pulsing
|
|
141
|
+
starting row gives instant feedback until the first real part. This IS
|
|
142
|
+
the canonical empty-live state: hosts pass state="streaming" from the
|
|
143
|
+
CTA press (uploads included) and never hand-roll a text/skeleton
|
|
144
|
+
placeholder in front of the feed. */}
|
|
145
|
+
{streaming && segments.length === 0 ? <StartingRow label={locale.agentRun.starting} /> : null}
|
|
139
146
|
{segments.map((seg, i) => {
|
|
140
147
|
if (seg.kind === "text") {
|
|
141
148
|
return (
|
|
@@ -284,6 +291,33 @@ function StepRow({ s, label, renderToolOutput }: { s: AgentStep; label: string;
|
|
|
284
291
|
|
|
285
292
|
// Thinking — a muted, COLLAPSED "Thinking" disclosure (revealed on press), so the
|
|
286
293
|
// agent's reasoning never crowds the answer. Pulses while it's still streaming.
|
|
294
|
+
/** The empty-live feed row: the live dot's halo plus a BREATHING label — the
|
|
295
|
+
* message itself pulses (Skeleton's opacity rhythm), so "work has started"
|
|
296
|
+
* reads at a glance even before the first token. */
|
|
297
|
+
function StartingRow({ label }: { label: string }) {
|
|
298
|
+
const v = useRef(new Animated.Value(0.45)).current;
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
const loop = Animated.loop(
|
|
301
|
+
Animated.sequence([
|
|
302
|
+
Animated.timing(v, { toValue: 1, duration: 700, useNativeDriver: false }),
|
|
303
|
+
Animated.timing(v, { toValue: 0.45, duration: 700, useNativeDriver: false }),
|
|
304
|
+
]),
|
|
305
|
+
);
|
|
306
|
+
loop.start();
|
|
307
|
+
return () => loop.stop();
|
|
308
|
+
}, [v]);
|
|
309
|
+
return (
|
|
310
|
+
<View style={styles.row}>
|
|
311
|
+
<View style={styles.dotCol}>
|
|
312
|
+
<Marker status="current" color={colors.zinc[400]} live />
|
|
313
|
+
</View>
|
|
314
|
+
<Animated.View style={[styles.rowBody, { opacity: v }]}>
|
|
315
|
+
<Text size="sm" color="muted">{label}</Text>
|
|
316
|
+
</Animated.View>
|
|
317
|
+
</View>
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
287
321
|
function ReasoningDisclosure(props: { text: string; streaming?: boolean; expanded: boolean; onToggle: () => void }) {
|
|
288
322
|
const { text, streaming, expanded, onToggle } = props;
|
|
289
323
|
const locale = useLoticsLocale();
|
package/src/clarify_wizard.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from "react";
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Button } from "./button";
|
|
4
4
|
import { Clarify, type ClarifyOption } from "./clarify";
|
|
@@ -26,16 +26,52 @@ export interface ClarifyWizardProps {
|
|
|
26
26
|
onCancel: () => void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/** What the wizard's action bar needs, published by the wizard body into the
|
|
30
|
+
* scope so `ClarifyWizardActions` can render Cancel/Back/Next/Submit from a
|
|
31
|
+
* `DialogFooter` — outside the wizard's own subtree. */
|
|
32
|
+
interface ClarifyWizardHandle {
|
|
33
|
+
index: number;
|
|
34
|
+
total: number;
|
|
35
|
+
answered: boolean;
|
|
36
|
+
back: () => void;
|
|
37
|
+
next: () => void;
|
|
38
|
+
submit: () => void;
|
|
39
|
+
cancel: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface ClarifyWizardScopeValue {
|
|
43
|
+
handle: ClarifyWizardHandle | null;
|
|
44
|
+
setHandle: (h: ClarifyWizardHandle | null) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ScopeContext = createContext<ClarifyWizardScopeValue | null>(null);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The compound frame for a wizard whose actions live in the dialog's footer —
|
|
51
|
+
* the dialog grammar's home for action bars. Wrap the `Dialog` from OUTSIDE
|
|
52
|
+
* (the `ChangeReview` position), put the `ClarifyWizard` in the content pane
|
|
53
|
+
* and `ClarifyWizardActions` in the `DialogFooter`; the wizard then suppresses
|
|
54
|
+
* its inline action row and drives the bar through this scope. One wizard per
|
|
55
|
+
* scope. Standalone `ClarifyWizard` (no scope — e.g. riding a parked run's
|
|
56
|
+
* stream, where no footer exists) keeps its inline actions.
|
|
57
|
+
*/
|
|
58
|
+
export function ClarifyWizardScope({ children }: { children: ReactNode }) {
|
|
59
|
+
const [handle, setHandle] = useState<ClarifyWizardHandle | null>(null);
|
|
60
|
+
return <ScopeContext.Provider value={{ handle, setHandle }}>{children}</ScopeContext.Provider>;
|
|
61
|
+
}
|
|
62
|
+
|
|
29
63
|
/**
|
|
30
64
|
* A SEQUENCE of `Clarify` questions the human works through one at a time —
|
|
31
65
|
* Back / Next / Cancel / Submit with a position indicator. Each step reuses
|
|
32
66
|
* `Clarify` (question + described answers + optional "Other…"). You can only
|
|
33
67
|
* advance once the current question is answered; Submit fires when the last is.
|
|
34
68
|
* The multi-question form of the agent's ask-back; single questions use `Clarify`.
|
|
69
|
+
* Inside a `ClarifyWizardScope`, the action row moves to `ClarifyWizardActions`.
|
|
35
70
|
*/
|
|
36
71
|
export function ClarifyWizard(props: ClarifyWizardProps) {
|
|
37
72
|
const { questions, onSubmit, onCancel } = props;
|
|
38
73
|
const labels = useLoticsLocale().clarify;
|
|
74
|
+
const scope = useContext(ScopeContext);
|
|
39
75
|
const [index, setIndex] = useState(0);
|
|
40
76
|
const [answers, setAnswers] = useState<(string | undefined)[]>(() => questions.map(() => undefined));
|
|
41
77
|
|
|
@@ -62,6 +98,28 @@ export function ClarifyWizard(props: ClarifyWizardProps) {
|
|
|
62
98
|
}),
|
|
63
99
|
);
|
|
64
100
|
|
|
101
|
+
// Publish the action-bar handle into the scope (and clear it on unmount) so
|
|
102
|
+
// the footer bar always reflects the CURRENT step's position and validity.
|
|
103
|
+
// The handle's callbacks read the latest submit/cancel through a ref — the
|
|
104
|
+
// host may recreate them every render, and a handle republished only on
|
|
105
|
+
// wizard-state changes would otherwise call a stale closure.
|
|
106
|
+
const latest = useRef({ submit, onCancel });
|
|
107
|
+
latest.current = { submit, onCancel };
|
|
108
|
+
const setHandle = scope?.setHandle;
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!setHandle) return;
|
|
111
|
+
setHandle({
|
|
112
|
+
index,
|
|
113
|
+
total,
|
|
114
|
+
answered,
|
|
115
|
+
back: () => setIndex((i) => Math.max(0, i - 1)),
|
|
116
|
+
next: () => setIndex((i) => Math.min(total - 1, i + 1)),
|
|
117
|
+
submit: () => latest.current.submit(),
|
|
118
|
+
cancel: () => latest.current.onCancel(),
|
|
119
|
+
});
|
|
120
|
+
return () => setHandle(null);
|
|
121
|
+
}, [setHandle, index, total, answered]);
|
|
122
|
+
|
|
65
123
|
return (
|
|
66
124
|
<View style={{ gap: 16 }}>
|
|
67
125
|
{/* The step position is an eyebrow above the question (no progress bar — and
|
|
@@ -69,16 +127,47 @@ export function ClarifyWizard(props: ClarifyWizardProps) {
|
|
|
69
127
|
as the question and answers. Keyed by index so each step's custom-answer
|
|
70
128
|
draft is its own (no bleed across steps). */}
|
|
71
129
|
<Clarify key={index} eyebrow={total > 1 ? `${index + 1} / ${total}` : undefined} question={q.question} options={q.answers} answer={current} onAnswer={setAnswer} allowCustom={q.allowCustom} />
|
|
72
|
-
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
130
|
+
{scope == null ? (
|
|
131
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8, paddingHorizontal: 8 }}>
|
|
132
|
+
<Button title={labels.cancel} color="muted" onPress={onCancel} />
|
|
133
|
+
<View style={{ flexDirection: "row", gap: 8 }}>
|
|
134
|
+
{index > 0 ? <Button title={labels.back} color="secondary" onPress={() => setIndex(index - 1)} /> : null}
|
|
135
|
+
{isLast ? (
|
|
136
|
+
<Button title={labels.submit} color="primary" onPress={submit} disabled={!answered} />
|
|
137
|
+
) : (
|
|
138
|
+
<Button title={labels.next} color="primary" onPress={() => setIndex(index + 1)} disabled={!answered} />
|
|
139
|
+
)}
|
|
140
|
+
</View>
|
|
81
141
|
</View>
|
|
142
|
+
) : null}
|
|
143
|
+
</View>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The wizard's action bar for a `DialogFooter` — Cancel bottom-left,
|
|
149
|
+
* Back + Next/Submit right, the same verbs and gating as the inline row.
|
|
150
|
+
* Requires a `ClarifyWizardScope` ancestor; renders nothing until its
|
|
151
|
+
* `ClarifyWizard` mounts (so a multi-phase dialog can keep the footer JSX
|
|
152
|
+
* conditional on the clarify phase alone).
|
|
153
|
+
*/
|
|
154
|
+
export function ClarifyWizardActions() {
|
|
155
|
+
const labels = useLoticsLocale().clarify;
|
|
156
|
+
const scope = useContext(ScopeContext);
|
|
157
|
+
if (scope == null) throw new Error("ClarifyWizardActions requires a ClarifyWizardScope ancestor.");
|
|
158
|
+
const h = scope.handle;
|
|
159
|
+
if (h == null) return null;
|
|
160
|
+
const isLast = h.index === h.total - 1;
|
|
161
|
+
return (
|
|
162
|
+
<View style={{ flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 8, flex: 1 }}>
|
|
163
|
+
<Button title={labels.cancel} color="muted" onPress={h.cancel} />
|
|
164
|
+
<View style={{ flexDirection: "row", gap: 8 }}>
|
|
165
|
+
{h.index > 0 ? <Button title={labels.back} color="secondary" onPress={h.back} /> : null}
|
|
166
|
+
{isLast ? (
|
|
167
|
+
<Button title={labels.submit} color="primary" onPress={h.submit} disabled={!h.answered} />
|
|
168
|
+
) : (
|
|
169
|
+
<Button title={labels.next} color="primary" onPress={h.next} disabled={!h.answered} />
|
|
170
|
+
)}
|
|
82
171
|
</View>
|
|
83
172
|
</View>
|
|
84
173
|
);
|
package/src/locale.tsx
CHANGED
|
@@ -134,7 +134,7 @@ export interface LoticsLocale {
|
|
|
134
134
|
* annotation on a call parked on a human decision, and the terminal error's
|
|
135
135
|
* `retry` action. (Tool-step labels and the "{n} steps" suffix stay
|
|
136
136
|
* prop-localized — `labelForCall` / `stepsLabel`.) */
|
|
137
|
-
agentRun: { thinking: string; thinkingStreaming: string; input: string; error: string; output: string; awaiting: string; retry: string };
|
|
137
|
+
agentRun: { starting: string; thinking: string; thinkingStreaming: string; input: string; error: string; output: string; awaiting: string; retry: string };
|
|
138
138
|
/** `ApprovalPrompt`: the default prompt line (overridable per instance) and
|
|
139
139
|
* the Approve / Deny button labels — the surface that ANSWERS `AgentRun`'s
|
|
140
140
|
* read-only `awaiting` row (approve/deny = the ai-sdk approval vocabulary). */
|
|
@@ -237,7 +237,7 @@ export const en: LoticsLocale = {
|
|
|
237
237
|
matrix: { total: "Total", less: "Less", more: "More" },
|
|
238
238
|
scrollToBottom: { tooltip: "Scroll to bottom" },
|
|
239
239
|
textInputField: { clear: "Clear" },
|
|
240
|
-
agentRun: { thinking: "Thinking", thinkingStreaming: "Thinking…", input: "Input", error: "Error", output: "Output", awaiting: "Awaiting", retry: "Retry" },
|
|
240
|
+
agentRun: { starting: "Starting…", thinking: "Thinking", thinkingStreaming: "Thinking…", input: "Input", error: "Error", output: "Output", awaiting: "Awaiting", retry: "Retry" },
|
|
241
241
|
approvalPrompt: { message: "The assistant wants to perform an action that needs your approval.", approve: "Approve", deny: "Deny" },
|
|
242
242
|
messageActions: { copy: "Copy", copied: "Copied", regenerate: "Regenerate", edit: "Edit", previousVersion: "Previous version", nextVersion: "Next version" },
|
|
243
243
|
};
|
|
@@ -335,7 +335,7 @@ export const vi: LoticsLocale = {
|
|
|
335
335
|
matrix: { total: "Tổng", less: "Ít", more: "Nhiều" },
|
|
336
336
|
scrollToBottom: { tooltip: "Cuộn xuống cuối" },
|
|
337
337
|
textInputField: { clear: "Xóa" },
|
|
338
|
-
agentRun: { thinking: "Suy nghĩ", thinkingStreaming: "Đang suy nghĩ…", input: "Đầu vào", error: "Lỗi", output: "Kết quả", awaiting: "Chờ duyệt", retry: "Thử lại" },
|
|
338
|
+
agentRun: { starting: "Đang bắt đầu…", thinking: "Suy nghĩ", thinkingStreaming: "Đang suy nghĩ…", input: "Đầu vào", error: "Lỗi", output: "Kết quả", awaiting: "Chờ duyệt", retry: "Thử lại" },
|
|
339
339
|
approvalPrompt: { message: "Trợ lý muốn thực hiện thao tác cần bạn duyệt.", approve: "Cho phép", deny: "Từ chối" },
|
|
340
340
|
messageActions: { copy: "Sao chép", copied: "Đã sao chép", regenerate: "Tạo lại", edit: "Chỉnh sửa", previousVersion: "Phiên bản trước", nextVersion: "Phiên bản sau" },
|
|
341
341
|
};
|