@things-factory/ai-assistant 10.1.91 → 10.1.98
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/client/components/assistant-chat.ts +5 -3
- package/client/components/entry-review.ts +56 -0
- package/client/utils/entry-proposal.ts +20 -5
- package/dist-client/components/assistant-chat.js +5 -3
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/entry-review.d.ts +5 -0
- package/dist-client/components/entry-review.js +55 -0
- package/dist-client/components/entry-review.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/entry-proposal.d.ts +5 -0
- package/dist-client/utils/entry-proposal.js +16 -5
- package/dist-client/utils/entry-proposal.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.d.ts +3 -3
- package/dist-server/service/data-entry/entry-apply.js +55 -2
- package/dist-server/service/data-entry/entry-apply.js.map +1 -1
- package/dist-server/service/data-entry/entry-resolver.js +3 -7
- package/dist-server/service/data-entry/entry-resolver.js.map +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +51 -0
- package/dist-server/service/data-entry/entry-target.js +77 -34
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.d.ts +7 -1
- package/dist-server/service/data-entry/entry-tools.js +79 -5
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.d.ts +99 -0
- package/dist-server/service/data-entry/entry-update.js +196 -0
- package/dist-server/service/data-entry/entry-update.js.map +1 -0
- package/dist-server/service/workspace/workspace-blocks.js +9 -1
- package/dist-server/service/workspace/workspace-blocks.js.map +1 -1
- package/dist-server/service/workspace/workspace-tools.js +43 -1
- package/dist-server/service/workspace/workspace-tools.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/data-entry/data-entry-update.test.ts +207 -0
- package/server/service/data-entry/entry-apply.ts +63 -6
- package/server/service/data-entry/entry-resolver.ts +4 -9
- package/server/service/data-entry/entry-target.ts +97 -31
- package/server/service/data-entry/entry-tools.ts +83 -5
- package/server/service/data-entry/entry-update.ts +209 -0
- package/server/service/workspace/workspace-blocks.ts +8 -1
- package/server/service/workspace/workspace-tools.ts +43 -1
- package/server/service/workspace/workspace.test.ts +16 -0
- package/test/entry-proposal.test.ts +15 -2
- package/translations/en.json +16 -1
- package/translations/ja.json +16 -1
- package/translations/ko.json +16 -1
- package/translations/ms.json +16 -1
- package/translations/zh.json +16 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Changing records that exist - the 수정안 (operato-application/design/ai/data-entry.md §5-1).
|
|
3
|
+
*
|
|
4
|
+
* The most expensive mistake is changing the wrong record, so which record is meant is settled here, by the
|
|
5
|
+
* server, never by the model's memory. A record is picked by the screen (the one being looked at, the rows
|
|
6
|
+
* selected) or by an id the find tool handed back; whichever way, it is read again through the application's
|
|
7
|
+
* read query as the person, and a record they cannot see is not found. The card shows what is there now and
|
|
8
|
+
* what it becomes; pressing reads it once more, and a field that moved in between keeps its row out.
|
|
9
|
+
*
|
|
10
|
+
* Pure but for the GraphQL runner, which comes in as an argument - the rules are tested without a server.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
changeToolName,
|
|
14
|
+
findToolName,
|
|
15
|
+
readFieldValue,
|
|
16
|
+
sameFieldValue,
|
|
17
|
+
type EntryField,
|
|
18
|
+
type EntryIssue,
|
|
19
|
+
type EntryRecord,
|
|
20
|
+
type EntryTarget
|
|
21
|
+
} from './entry-target.js'
|
|
22
|
+
|
|
23
|
+
/** Runs one of the application's queries or mutations as the person. */
|
|
24
|
+
export type GraphqlRunner = (document: string, variables: Record<string, unknown>) => Promise<{ data?: any; errors?: readonly { message: string }[] }>
|
|
25
|
+
|
|
26
|
+
/** How the record of a change was picked - said on the card, so the person sees the ground of it. */
|
|
27
|
+
export type ChangeVia = 'current' | 'selected' | 'id'
|
|
28
|
+
|
|
29
|
+
export interface ChangeRow {
|
|
30
|
+
index: number
|
|
31
|
+
id: string
|
|
32
|
+
via: ChangeVia
|
|
33
|
+
label?: string
|
|
34
|
+
/** What the record holds now, for the fields that change - read by the server. */
|
|
35
|
+
before: Record<string, unknown>
|
|
36
|
+
values: Record<string, unknown>
|
|
37
|
+
issues: EntryIssue[]
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const MAX_CHANGES = 100
|
|
41
|
+
|
|
42
|
+
const blockedChange = (row: { issues: EntryIssue[] }) => row.issues.some(issue => issue.blocking)
|
|
43
|
+
|
|
44
|
+
/** The fields a target lets change. */
|
|
45
|
+
export function changeableFields(target: EntryTarget): EntryField[] {
|
|
46
|
+
const names = target.update?.fields
|
|
47
|
+
return names ? target.fields.filter(field => names.includes(field.name)) : target.fields
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The records the changes name. `current` is the record on screen, `selected` every row selected there -
|
|
52
|
+
* both from the surface's host context, which is **not trusted**: an id from it is only a candidate, read
|
|
53
|
+
* through the application before anything is shown. Any other ref is an id, from the find tool.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveChangeRefs(
|
|
56
|
+
targetKey: string,
|
|
57
|
+
changes: unknown,
|
|
58
|
+
host: Record<string, any> | undefined
|
|
59
|
+
): { picked: { id: string; via: ChangeVia; values: Record<string, unknown> }[]; unresolved: string[] } {
|
|
60
|
+
const picked: { id: string; via: ChangeVia; values: Record<string, unknown> }[] = []
|
|
61
|
+
const unresolved: string[] = []
|
|
62
|
+
const mine = (one: any) => !one?.target || one.target === targetKey
|
|
63
|
+
for (const change of Array.isArray(changes) ? changes : []) {
|
|
64
|
+
const values = change?.values && typeof change.values === 'object' ? change.values : {}
|
|
65
|
+
const ref = typeof change?.ref === 'string' ? change.ref.trim() : ''
|
|
66
|
+
if (ref === 'current') {
|
|
67
|
+
const id = mine(host?.record) ? host?.record?.id : undefined
|
|
68
|
+
if (typeof id === 'string' && id) picked.push({ id, via: 'current', values })
|
|
69
|
+
else unresolved.push('current')
|
|
70
|
+
} else if (ref === 'selected') {
|
|
71
|
+
const ids = mine(host?.selection) && Array.isArray(host?.selection?.ids) ? host!.selection.ids.filter((id: unknown) => typeof id === 'string' && id) : []
|
|
72
|
+
if (ids.length) for (const id of ids) picked.push({ id, via: 'selected', values })
|
|
73
|
+
else unresolved.push('selected')
|
|
74
|
+
} else if (ref) {
|
|
75
|
+
picked.push({ id: ref, via: 'id', values })
|
|
76
|
+
} else {
|
|
77
|
+
unresolved.push('no-ref')
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { picked: picked.slice(0, MAX_CHANGES), unresolved }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Reads the records by id through the application's read query, as the person. */
|
|
84
|
+
export async function readRecords(target: EntryTarget, ids: string[], run: GraphqlRunner): Promise<Map<string, EntryRecord>> {
|
|
85
|
+
const update = target.update!
|
|
86
|
+
const answer = await run(update.read.query, update.read.variables([...new Set(ids)]))
|
|
87
|
+
if (answer.errors?.length) throw new Error(answer.errors.map(one => one.message).join('; '))
|
|
88
|
+
return new Map((update.read.recordsOf(answer.data) ?? []).filter(record => record && typeof record.id === 'string').map(record => [record.id, record]))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The change rows against what the records hold now. */
|
|
92
|
+
export function changeRows(target: EntryTarget, picked: { id: string; via: ChangeVia; values: Record<string, unknown> }[], records: Map<string, EntryRecord>): ChangeRow[] {
|
|
93
|
+
const fields = new Map(changeableFields(target).map(field => [field.name, field]))
|
|
94
|
+
return picked.map((one, index): ChangeRow => {
|
|
95
|
+
const record = records.get(one.id)
|
|
96
|
+
const issues: EntryIssue[] = []
|
|
97
|
+
const values: Record<string, unknown> = {}
|
|
98
|
+
const before: Record<string, unknown> = {}
|
|
99
|
+
for (const [name, value] of Object.entries(one.values)) {
|
|
100
|
+
const field = fields.get(name)
|
|
101
|
+
if (!field) {
|
|
102
|
+
issues.push({ field: name, code: 'not-changeable', blocking: false })
|
|
103
|
+
continue
|
|
104
|
+
}
|
|
105
|
+
const { read, code } = value === null || value === '' ? { read: null, code: undefined } : readFieldValue(field, value)
|
|
106
|
+
if (read === undefined) {
|
|
107
|
+
values[name] = value
|
|
108
|
+
issues.push({ field: name, code: code!, params: code === 'not-in-list' ? { values: field.values } : undefined, blocking: true })
|
|
109
|
+
} else {
|
|
110
|
+
if (read === null && field.required) issues.push({ field: name, code: 'required', blocking: true })
|
|
111
|
+
values[name] = read
|
|
112
|
+
}
|
|
113
|
+
if (record) before[name] = record[name] ?? null
|
|
114
|
+
}
|
|
115
|
+
if (!record) issues.push({ code: 'record-not-found', blocking: true })
|
|
116
|
+
else if (Object.keys(values).length && Object.keys(values).every(name => sameFieldValue(fields.get(name)!, before[name], values[name])))
|
|
117
|
+
issues.push({ code: 'no-change', blocking: true })
|
|
118
|
+
if (!Object.keys(values).length && !issues.some(issue => issue.blocking)) issues.push({ code: 'nothing-to-change', blocking: true })
|
|
119
|
+
return { index, id: one.id, via: one.via, ...(record && target.update?.labelOf ? { label: target.update.labelOf(record) } : {}), before, values, issues }
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** What the card needs besides the rows - the fields that change, in the target's order. */
|
|
124
|
+
function changedColumns(target: EntryTarget, rows: ChangeRow[]) {
|
|
125
|
+
const used = new Set(rows.flatMap(row => Object.keys(row.values)))
|
|
126
|
+
return changeableFields(target)
|
|
127
|
+
.filter(field => used.has(field.name))
|
|
128
|
+
.map(field => ({ name: field.name, type: field.type, ...(field.labelKey ? { labelKey: field.labelKey } : {}) }))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function proposeChanges(target: EntryTarget, args: any, context: any, run: GraphqlRunner) {
|
|
132
|
+
const { picked, unresolved } = resolveChangeRefs(target.key, args?.changes, context?.host)
|
|
133
|
+
if (!picked.length) {
|
|
134
|
+
return {
|
|
135
|
+
rejected: true,
|
|
136
|
+
error: unresolved.length ? `unresolved-${unresolved[0]}` : 'no-changes',
|
|
137
|
+
message:
|
|
138
|
+
unresolved[0] === 'current' || unresolved[0] === 'selected'
|
|
139
|
+
? `the screen names no ${unresolved[0] === 'current' ? 'record' : 'selected rows'} of this kind. Ask the person which record, or call ${findToolName(target.key)}.`
|
|
140
|
+
: `name each record: ref "current", "selected", or an id from ${findToolName(target.key)}. Never guess an id.`
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
let records: Map<string, EntryRecord>
|
|
144
|
+
try {
|
|
145
|
+
records = await readRecords(target, picked.map(one => one.id), run)
|
|
146
|
+
} catch (error: any) {
|
|
147
|
+
return { rejected: true, error: 'read-failed', message: `could not read the records: ${error?.message ?? error}` }
|
|
148
|
+
}
|
|
149
|
+
const rows = changeRows(target, picked, records)
|
|
150
|
+
const blocked = rows.filter(blockedChange).length
|
|
151
|
+
return {
|
|
152
|
+
proposed: true,
|
|
153
|
+
kind: 'entry',
|
|
154
|
+
op: 'update',
|
|
155
|
+
target: target.key,
|
|
156
|
+
labelKey: target.labelKey,
|
|
157
|
+
label: `${rows.length} ${target.key} changes`,
|
|
158
|
+
icon: 'edit_note',
|
|
159
|
+
columns: changedColumns(target, rows),
|
|
160
|
+
rows,
|
|
161
|
+
counts: { rows: rows.length, ready: rows.length - blocked, blocked, cut: 0 },
|
|
162
|
+
...(typeof args?.note === 'string' && args.note.trim() ? { reason: args.note.trim().slice(0, 300) } : {}),
|
|
163
|
+
message:
|
|
164
|
+
`proposed ${rows.length} changes (${rows.length - blocked} ready, ${blocked} blocked). Nothing changes until the person presses the card. ` +
|
|
165
|
+
'Tell them which records are not found or would not change.'
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const MAX_CANDIDATES = 20
|
|
170
|
+
|
|
171
|
+
/** Candidates for what the person described - the model lists them and asks when there is more than one. */
|
|
172
|
+
export async function findRecords(target: EntryTarget, args: any, run: GraphqlRunner) {
|
|
173
|
+
const find = target.update?.find
|
|
174
|
+
if (!find) return { rejected: true, error: 'no-find', message: 'this record cannot be searched.' }
|
|
175
|
+
const criteria: Record<string, unknown> = {}
|
|
176
|
+
for (const field of find.criteria) {
|
|
177
|
+
const value = args?.[field.name]
|
|
178
|
+
if (value === undefined || value === null || value === '') continue
|
|
179
|
+
const { read } = readFieldValue(field, value)
|
|
180
|
+
if (read !== undefined) criteria[field.name] = read
|
|
181
|
+
}
|
|
182
|
+
const answer = await run(find.query, find.variables(criteria))
|
|
183
|
+
if (answer.errors?.length) return { rejected: true, error: 'find-failed', message: answer.errors.map(one => one.message).join('; ') }
|
|
184
|
+
const found = (find.recordsOf(answer.data) ?? []).filter(record => record && typeof record.id === 'string')
|
|
185
|
+
return {
|
|
186
|
+
total: found.length,
|
|
187
|
+
candidates: found.slice(0, MAX_CANDIDATES).map(record => ({ id: record.id, ...(target.update?.labelOf ? { label: target.update.labelOf(record) } : {}), record })),
|
|
188
|
+
message:
|
|
189
|
+
found.length === 1
|
|
190
|
+
? `one record matches; pass its id to ${changeToolName(target.key)}.`
|
|
191
|
+
: found.length
|
|
192
|
+
? `${found.length} records match${found.length > MAX_CANDIDATES ? ` (first ${MAX_CANDIDATES} shown)` : ''}. List them to the person and ask which - never pick one yourself.`
|
|
193
|
+
: 'no record matches. Tell the person and ask for more to go on.'
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The rows of a pressed 수정안, against what the records hold now. A row whose field moved since the card was
|
|
199
|
+
* made is kept out as `changed-since` - it is not overwritten unseen.
|
|
200
|
+
*/
|
|
201
|
+
export function stillAsProposed(target: EntryTarget, rows: ChangeRow[], now: Map<string, EntryRecord>): (ChangeRow & { now?: EntryRecord })[] {
|
|
202
|
+
const fields = new Map(target.fields.map(field => [field.name, field]))
|
|
203
|
+
return rows.map(row => {
|
|
204
|
+
const record = now.get(row.id)
|
|
205
|
+
if (!record) return { ...row, issues: [...row.issues, { code: 'record-not-found', blocking: true }] }
|
|
206
|
+
const moved = Object.keys(row.values).find(name => fields.get(name) && !sameFieldValue(fields.get(name)!, record[name] ?? null, row.before[name]))
|
|
207
|
+
return moved ? { ...row, now: record, issues: [...row.issues, { field: moved, code: 'changed-since', blocking: true }] } : { ...row, now: record }
|
|
208
|
+
})
|
|
209
|
+
}
|
|
@@ -75,8 +75,15 @@ function block(raw: any): WorkspaceBlock {
|
|
|
75
75
|
if (!columns.length) return unreadable(kind, 'no-columns')
|
|
76
76
|
const given = Array.isArray(raw.rows) ? raw.rows : []
|
|
77
77
|
/* A row is an object by column name, or an array in column order - models write both. */
|
|
78
|
+
/* A number column reads "12,400" as 12400 - cells come as text from a model told one type per field. */
|
|
79
|
+
const cell = (column: any, value: unknown) => {
|
|
80
|
+
const read = scalar(value)
|
|
81
|
+
if (column.type !== 'number' || typeof read !== 'string') return read
|
|
82
|
+
const number = Number(read.replace(/[,\s₩$€]/g, ''))
|
|
83
|
+
return read.trim() && Number.isFinite(number) ? number : read
|
|
84
|
+
}
|
|
78
85
|
const rows = given.slice(0, MAX_TABLE_ROWS).map((row: any) =>
|
|
79
|
-
Object.fromEntries(columns.map((column: any, i: number) => [column.name,
|
|
86
|
+
Object.fromEntries(columns.map((column: any, i: number) => [column.name, cell(column, Array.isArray(row) ? row[i] : row?.[column.name])]))
|
|
80
87
|
)
|
|
81
88
|
return { kind, columns, rows, ...(text(raw.caption, 300) ? { caption: text(raw.caption, 300) } : {}), ...(given.length > MAX_TABLE_ROWS ? { cut: given.length - MAX_TABLE_ROWS } : {}) }
|
|
82
89
|
}
|
|
@@ -41,9 +41,51 @@ const spec = {
|
|
|
41
41
|
name: { type: 'string', description: 'A short key, lowercase with dashes ("contracts-by-customer"). The same name again makes a new version of the same workspace.' },
|
|
42
42
|
title: { type: 'string' },
|
|
43
43
|
summary: { type: 'string', description: 'One line under the title.' },
|
|
44
|
+
/*
|
|
45
|
+
* Every field of every block is declared, each with one type: Gemini's function schema (an OpenAPI 3.0
|
|
46
|
+
* subset) refuses a list of types and may leave out a field it was not told of (2026-10-01).
|
|
47
|
+
*/
|
|
44
48
|
blocks: {
|
|
45
49
|
type: 'array',
|
|
46
|
-
items: {
|
|
50
|
+
items: {
|
|
51
|
+
type: 'object',
|
|
52
|
+
properties: {
|
|
53
|
+
kind: { type: 'string', enum: [...WORKSPACE_BLOCK_KINDS] },
|
|
54
|
+
text: { type: 'string', description: 'heading' },
|
|
55
|
+
level: { type: 'integer', description: 'heading: 1-3' },
|
|
56
|
+
markdown: { type: 'string', description: 'text' },
|
|
57
|
+
caption: { type: 'string', description: 'table, chart' },
|
|
58
|
+
unit: { type: 'string', description: 'chart' },
|
|
59
|
+
chart: { type: 'string', enum: ['bar', 'line'], description: 'chart' },
|
|
60
|
+
items: {
|
|
61
|
+
type: 'array',
|
|
62
|
+
description: 'metrics: {label,value,unit,note}; steps: {label,state,note}',
|
|
63
|
+
items: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
label: { type: 'string' },
|
|
67
|
+
value: { type: 'string' },
|
|
68
|
+
unit: { type: 'string' },
|
|
69
|
+
note: { type: 'string' },
|
|
70
|
+
state: { type: 'string', enum: ['done', 'doing', 'todo', 'failed'] }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
columns: {
|
|
75
|
+
type: 'array',
|
|
76
|
+
description: 'table',
|
|
77
|
+
items: { type: 'object', properties: { name: { type: 'string' }, label: { type: 'string' }, type: { type: 'string', enum: ['number', 'date', 'string'] } } }
|
|
78
|
+
},
|
|
79
|
+
rows: { type: 'array', description: 'table: each row is its cells in column order', items: { type: 'array', items: { type: 'string' } } },
|
|
80
|
+
x: { type: 'array', description: 'chart: the labels along the bottom', items: { type: 'string' } },
|
|
81
|
+
series: {
|
|
82
|
+
type: 'array',
|
|
83
|
+
description: 'chart',
|
|
84
|
+
items: { type: 'object', properties: { name: { type: 'string' }, values: { type: 'array', items: { type: 'number' } } } }
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
required: ['kind']
|
|
88
|
+
}
|
|
47
89
|
}
|
|
48
90
|
},
|
|
49
91
|
required: ['name', 'title', 'blocks']
|
|
@@ -55,4 +55,20 @@ describe('the workspace', () => {
|
|
|
55
55
|
expect(showInWorkspace({ blocks: [{ kind: 'text', markdown: 'x' }] })).toMatchObject({ rejected: true, error: 'no-title' })
|
|
56
56
|
expect(showInWorkspace({ title: 'x', blocks: [] })).toMatchObject({ rejected: true, error: 'no-blocks' })
|
|
57
57
|
})
|
|
58
|
+
|
|
59
|
+
it('a number column reads the text a model writes - "12,400" is 12400, a word stays a word', () => {
|
|
60
|
+
const table: any = normalizeWorkspace({ title: 't', blocks: [{ kind: 'table', columns: [{ name: 'a', type: 'number' }], rows: [['12,400'], ['n/a']] }] }).workspace!.blocks[0]
|
|
61
|
+
expect(table.rows).toEqual([{ a: 12400 }, { a: 'n/a' }])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('★ every field of the tool has one type, and every object says its fields - what Gemini accepts', () => {
|
|
65
|
+
const walk = (schema: any, path: string, out: string[]) => {
|
|
66
|
+
if (!schema || typeof schema !== 'object') return out
|
|
67
|
+
if (Array.isArray(schema.type)) out.push(`${path}: a list of types`)
|
|
68
|
+
if (schema.type === 'object' && !Object.keys(schema.properties ?? {}).length) out.push(`${path}: an object with no fields`)
|
|
69
|
+
for (const [key, value] of Object.entries(schema)) if (value && typeof value === 'object') walk(value, `${path}/${key}`, out)
|
|
70
|
+
return out
|
|
71
|
+
}
|
|
72
|
+
expect(walk((workspaceCategory.specs[0] as any).schema, '', [])).toEqual([])
|
|
73
|
+
})
|
|
58
74
|
})
|
|
@@ -7,6 +7,7 @@ import { join } from 'path'
|
|
|
7
7
|
|
|
8
8
|
import { runProposal, setEntryProposalHandler } from '../client/utils/assistant-proposals'
|
|
9
9
|
import {
|
|
10
|
+
entryGroupOf,
|
|
10
11
|
entryOutcomeNote,
|
|
11
12
|
entryProposalHandler,
|
|
12
13
|
entryReadyCount,
|
|
@@ -31,8 +32,8 @@ describe('the entry card', () => {
|
|
|
31
32
|
|
|
32
33
|
it('★ the newest card per record is the current one - an older one is replaced', () => {
|
|
33
34
|
const latest = latestEntryKeys([[card('k1')], [{ key: 'x', tool: 'proposeAction' }], [card('k2'), card('k3', 'office.leave')]])
|
|
34
|
-
expect(latest.get('office.expense-line')).toBe('k2')
|
|
35
|
-
expect(latest.get('office.leave')).toBe('k3')
|
|
35
|
+
expect(latest.get('office.expense-line:create')).toBe('k2')
|
|
36
|
+
expect(latest.get('office.leave:create')).toBe('k3')
|
|
36
37
|
})
|
|
37
38
|
|
|
38
39
|
it('leaves in the conversation what went in and each row that did not, by its place', () => {
|
|
@@ -64,4 +65,16 @@ describe('the entry card', () => {
|
|
|
64
65
|
expect(chat).toContain("'ai-assistant.entry.enter'")
|
|
65
66
|
expect(chat).toContain("'ai-assistant.entry.replaced'")
|
|
66
67
|
})
|
|
68
|
+
|
|
69
|
+
it('★ a 입력안 and a 수정안 of one record are counted apart - a newer 수정안 replaces only the older 수정안', () => {
|
|
70
|
+
const latest = latestEntryKeys([[card('e1')], [{ ...card('u1'), op: 'update' }], [card('e2')]])
|
|
71
|
+
expect(latest.get(entryGroupOf(card('x')))).toBe('e2')
|
|
72
|
+
expect(latest.get(entryGroupOf({ ...card('x'), op: 'update' }))).toBe('u1')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('a 수정안 says in the conversation what was changed', () => {
|
|
76
|
+
expect(entryOutcomeNote({ verdict: 'accepted', applied: 1, missed: 1, op: 'update', rows: [{ index: 0, ok: true }, { index: 1, ok: false, error: 'amount: changed-since' }] }, t)).toBe(
|
|
77
|
+
'changed 1 records.\nrow 2: amount: changed-since'
|
|
78
|
+
)
|
|
79
|
+
})
|
|
67
80
|
})
|
package/translations/en.json
CHANGED
|
@@ -252,5 +252,20 @@
|
|
|
252
252
|
"ai-assistant.workspace.contains-table": "{count} tables",
|
|
253
253
|
"ai-assistant.workspace.contains-chart": "{count} charts",
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "{count} figures",
|
|
255
|
-
"ai-assistant.workspace.contains-steps": "{count} progress"
|
|
255
|
+
"ai-assistant.workspace.contains-steps": "{count} progress",
|
|
256
|
+
"ai-assistant.entry.change": "change {count} records",
|
|
257
|
+
"ai-assistant.entry.changed": "changed {count} records.",
|
|
258
|
+
"ai-assistant.entry.changed-none": "no record was changed.",
|
|
259
|
+
"ai-assistant.entry.changes": "{count} records",
|
|
260
|
+
"ai-assistant.entry.not-changed": "{count} will not be changed",
|
|
261
|
+
"ai-assistant.entry.record": "record",
|
|
262
|
+
"ai-assistant.entry.picked": "picked",
|
|
263
|
+
"ai-assistant.entry.via-current": "the record on screen",
|
|
264
|
+
"ai-assistant.entry.via-selected": "selected on screen",
|
|
265
|
+
"ai-assistant.entry.via-id": "found in the conversation",
|
|
266
|
+
"ai-assistant.entry.issue.record-not-found": "not a record you can see",
|
|
267
|
+
"ai-assistant.entry.issue.changed-since": "changed since the card was made",
|
|
268
|
+
"ai-assistant.entry.issue.no-change": "already that value",
|
|
269
|
+
"ai-assistant.entry.issue.nothing-to-change": "nothing to change",
|
|
270
|
+
"ai-assistant.entry.issue.not-changeable": "this field cannot be changed - left out"
|
|
256
271
|
}
|
package/translations/ja.json
CHANGED
|
@@ -252,5 +252,20 @@
|
|
|
252
252
|
"ai-assistant.workspace.contains-table": "表 {count}",
|
|
253
253
|
"ai-assistant.workspace.contains-chart": "チャート {count}",
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "指標 {count}",
|
|
255
|
-
"ai-assistant.workspace.contains-steps": "経過 {count}"
|
|
255
|
+
"ai-assistant.workspace.contains-steps": "経過 {count}",
|
|
256
|
+
"ai-assistant.entry.change": "{count}件を修正",
|
|
257
|
+
"ai-assistant.entry.changed": "{count}件を修正しました。",
|
|
258
|
+
"ai-assistant.entry.changed-none": "修正された記録はありません。",
|
|
259
|
+
"ai-assistant.entry.changes": "{count}件",
|
|
260
|
+
"ai-assistant.entry.not-changed": "{count}件は修正しません",
|
|
261
|
+
"ai-assistant.entry.record": "記録",
|
|
262
|
+
"ai-assistant.entry.picked": "選び方",
|
|
263
|
+
"ai-assistant.entry.via-current": "表示中の記録",
|
|
264
|
+
"ai-assistant.entry.via-selected": "選択した行",
|
|
265
|
+
"ai-assistant.entry.via-id": "会話で見つけた記録",
|
|
266
|
+
"ai-assistant.entry.issue.record-not-found": "閲覧できる記録ではありません",
|
|
267
|
+
"ai-assistant.entry.issue.changed-since": "その間に変更されました",
|
|
268
|
+
"ai-assistant.entry.issue.no-change": "すでにその値です",
|
|
269
|
+
"ai-assistant.entry.issue.nothing-to-change": "修正する項目がありません",
|
|
270
|
+
"ai-assistant.entry.issue.not-changeable": "修正できない項目のため除外しました"
|
|
256
271
|
}
|
package/translations/ko.json
CHANGED
|
@@ -252,5 +252,20 @@
|
|
|
252
252
|
"ai-assistant.workspace.contains-table": "표 {count}",
|
|
253
253
|
"ai-assistant.workspace.contains-chart": "차트 {count}",
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "수치 {count}",
|
|
255
|
-
"ai-assistant.workspace.contains-steps": "경과 {count}"
|
|
255
|
+
"ai-assistant.workspace.contains-steps": "경과 {count}",
|
|
256
|
+
"ai-assistant.entry.change": "{count}건 고치기",
|
|
257
|
+
"ai-assistant.entry.changed": "{count}건을 고쳤습니다.",
|
|
258
|
+
"ai-assistant.entry.changed-none": "고친 기록이 없습니다.",
|
|
259
|
+
"ai-assistant.entry.changes": "{count}건",
|
|
260
|
+
"ai-assistant.entry.not-changed": "{count}건은 고치지 않습니다",
|
|
261
|
+
"ai-assistant.entry.record": "기록",
|
|
262
|
+
"ai-assistant.entry.picked": "고른 길",
|
|
263
|
+
"ai-assistant.entry.via-current": "보고 있는 기록",
|
|
264
|
+
"ai-assistant.entry.via-selected": "선택한 행",
|
|
265
|
+
"ai-assistant.entry.via-id": "대화에서 찾은 기록",
|
|
266
|
+
"ai-assistant.entry.issue.record-not-found": "볼 수 있는 기록이 아닙니다",
|
|
267
|
+
"ai-assistant.entry.issue.changed-since": "그사이 바뀌었습니다",
|
|
268
|
+
"ai-assistant.entry.issue.no-change": "이미 그 값입니다",
|
|
269
|
+
"ai-assistant.entry.issue.nothing-to-change": "고칠 칸이 없습니다",
|
|
270
|
+
"ai-assistant.entry.issue.not-changeable": "고칠 수 없는 칸이라 뺐습니다"
|
|
256
271
|
}
|
package/translations/ms.json
CHANGED
|
@@ -252,5 +252,20 @@
|
|
|
252
252
|
"ai-assistant.workspace.contains-table": "{count} jadual",
|
|
253
253
|
"ai-assistant.workspace.contains-chart": "{count} carta",
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "{count} angka",
|
|
255
|
-
"ai-assistant.workspace.contains-steps": "{count} kemajuan"
|
|
255
|
+
"ai-assistant.workspace.contains-steps": "{count} kemajuan",
|
|
256
|
+
"ai-assistant.entry.change": "ubah {count} rekod",
|
|
257
|
+
"ai-assistant.entry.changed": "{count} rekod diubah.",
|
|
258
|
+
"ai-assistant.entry.changed-none": "tiada rekod diubah.",
|
|
259
|
+
"ai-assistant.entry.changes": "{count} rekod",
|
|
260
|
+
"ai-assistant.entry.not-changed": "{count} tidak akan diubah",
|
|
261
|
+
"ai-assistant.entry.record": "rekod",
|
|
262
|
+
"ai-assistant.entry.picked": "dipilih",
|
|
263
|
+
"ai-assistant.entry.via-current": "rekod pada skrin",
|
|
264
|
+
"ai-assistant.entry.via-selected": "dipilih pada skrin",
|
|
265
|
+
"ai-assistant.entry.via-id": "ditemui dalam perbualan",
|
|
266
|
+
"ai-assistant.entry.issue.record-not-found": "bukan rekod yang boleh anda lihat",
|
|
267
|
+
"ai-assistant.entry.issue.changed-since": "telah berubah sejak kad dibuat",
|
|
268
|
+
"ai-assistant.entry.issue.no-change": "sudah nilai itu",
|
|
269
|
+
"ai-assistant.entry.issue.nothing-to-change": "tiada yang perlu diubah",
|
|
270
|
+
"ai-assistant.entry.issue.not-changeable": "medan ini tidak boleh diubah - ditinggalkan"
|
|
256
271
|
}
|
package/translations/zh.json
CHANGED
|
@@ -252,5 +252,20 @@
|
|
|
252
252
|
"ai-assistant.workspace.contains-table": "表格 {count}",
|
|
253
253
|
"ai-assistant.workspace.contains-chart": "图表 {count}",
|
|
254
254
|
"ai-assistant.workspace.contains-metrics": "指标 {count}",
|
|
255
|
-
"ai-assistant.workspace.contains-steps": "进度 {count}"
|
|
255
|
+
"ai-assistant.workspace.contains-steps": "进度 {count}",
|
|
256
|
+
"ai-assistant.entry.change": "修改{count}条",
|
|
257
|
+
"ai-assistant.entry.changed": "已修改{count}条。",
|
|
258
|
+
"ai-assistant.entry.changed-none": "未修改任何记录。",
|
|
259
|
+
"ai-assistant.entry.changes": "{count}条",
|
|
260
|
+
"ai-assistant.entry.not-changed": "{count}条不会修改",
|
|
261
|
+
"ai-assistant.entry.record": "记录",
|
|
262
|
+
"ai-assistant.entry.picked": "选取方式",
|
|
263
|
+
"ai-assistant.entry.via-current": "当前查看的记录",
|
|
264
|
+
"ai-assistant.entry.via-selected": "已选中的行",
|
|
265
|
+
"ai-assistant.entry.via-id": "在对话中找到",
|
|
266
|
+
"ai-assistant.entry.issue.record-not-found": "不是您可以查看的记录",
|
|
267
|
+
"ai-assistant.entry.issue.changed-since": "在此期间已被修改",
|
|
268
|
+
"ai-assistant.entry.issue.no-change": "已是该值",
|
|
269
|
+
"ai-assistant.entry.issue.nothing-to-change": "没有要修改的字段",
|
|
270
|
+
"ai-assistant.entry.issue.not-changeable": "此字段不可修改,已忽略"
|
|
256
271
|
}
|