@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.
Files changed (47) hide show
  1. package/client/components/assistant-chat.ts +5 -3
  2. package/client/components/entry-review.ts +56 -0
  3. package/client/utils/entry-proposal.ts +20 -5
  4. package/dist-client/components/assistant-chat.js +5 -3
  5. package/dist-client/components/assistant-chat.js.map +1 -1
  6. package/dist-client/components/entry-review.d.ts +5 -0
  7. package/dist-client/components/entry-review.js +55 -0
  8. package/dist-client/components/entry-review.js.map +1 -1
  9. package/dist-client/tsconfig.tsbuildinfo +1 -1
  10. package/dist-client/utils/entry-proposal.d.ts +5 -0
  11. package/dist-client/utils/entry-proposal.js +16 -5
  12. package/dist-client/utils/entry-proposal.js.map +1 -1
  13. package/dist-server/service/data-entry/entry-apply.d.ts +3 -3
  14. package/dist-server/service/data-entry/entry-apply.js +55 -2
  15. package/dist-server/service/data-entry/entry-apply.js.map +1 -1
  16. package/dist-server/service/data-entry/entry-resolver.js +3 -7
  17. package/dist-server/service/data-entry/entry-resolver.js.map +1 -1
  18. package/dist-server/service/data-entry/entry-target.d.ts +51 -0
  19. package/dist-server/service/data-entry/entry-target.js +77 -34
  20. package/dist-server/service/data-entry/entry-target.js.map +1 -1
  21. package/dist-server/service/data-entry/entry-tools.d.ts +7 -1
  22. package/dist-server/service/data-entry/entry-tools.js +79 -5
  23. package/dist-server/service/data-entry/entry-tools.js.map +1 -1
  24. package/dist-server/service/data-entry/entry-update.d.ts +99 -0
  25. package/dist-server/service/data-entry/entry-update.js +196 -0
  26. package/dist-server/service/data-entry/entry-update.js.map +1 -0
  27. package/dist-server/service/workspace/workspace-blocks.js +9 -1
  28. package/dist-server/service/workspace/workspace-blocks.js.map +1 -1
  29. package/dist-server/service/workspace/workspace-tools.js +43 -1
  30. package/dist-server/service/workspace/workspace-tools.js.map +1 -1
  31. package/dist-server/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +2 -2
  33. package/server/service/data-entry/data-entry-update.test.ts +207 -0
  34. package/server/service/data-entry/entry-apply.ts +63 -6
  35. package/server/service/data-entry/entry-resolver.ts +4 -9
  36. package/server/service/data-entry/entry-target.ts +97 -31
  37. package/server/service/data-entry/entry-tools.ts +83 -5
  38. package/server/service/data-entry/entry-update.ts +209 -0
  39. package/server/service/workspace/workspace-blocks.ts +8 -1
  40. package/server/service/workspace/workspace-tools.ts +43 -1
  41. package/server/service/workspace/workspace.test.ts +16 -0
  42. package/test/entry-proposal.test.ts +15 -2
  43. package/translations/en.json +16 -1
  44. package/translations/ja.json +16 -1
  45. package/translations/ko.json +16 -1
  46. package/translations/ms.json +16 -1
  47. package/translations/zh.json +16 -1
@@ -64,7 +64,7 @@ import { isDifferentConversation, resolveConversation, type AssistantMode, type
64
64
  import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState, type ParsedProposalOutcome } from './proposal-outcome-line.js'
65
65
  import { availableTriggers, type TriggerChip } from './chat-triggers.js'
66
66
  import './entry-review.js'
67
- import { entryReadyCount, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js'
67
+ import { entryGroupOf, entryReadyCount, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js'
68
68
  import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js'
69
69
 
70
70
  interface ChatLine {
@@ -3958,7 +3958,7 @@ export class OxAssistantChat extends LitElement {
3958
3958
  if (isWorkspaceProposal(p)) return this.renderWorkspaceCard(p, key)
3959
3959
  const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) })
3960
3960
  const entry = isEntryProposal(p)
3961
- const replaced = entry && latestEntry?.get(p.target) !== key && card.state === 'ready'
3961
+ const replaced = entry && latestEntry?.get(entryGroupOf(p)) !== key && card.state === 'ready'
3962
3962
  const ready = entry ? entryReadyCount(p) : 0
3963
3963
  const sent = card.state !== 'ready' || replaced || (entry && ready === 0)
3964
3964
  /*
@@ -4000,7 +4000,9 @@ export class OxAssistantChat extends LitElement {
4000
4000
  ? this.proposalStateText(key)
4001
4001
  : entry
4002
4002
  ? ready
4003
- ? i18next.t('ai-assistant.entry.enter', { defaultValue: 'enter {count} rows', count: ready })
4003
+ ? isChangeProposal(p)
4004
+ ? i18next.t('ai-assistant.entry.change', { defaultValue: 'change {count} records', count: ready })
4005
+ : i18next.t('ai-assistant.entry.enter', { defaultValue: 'enter {count} rows', count: ready })
4004
4006
  : i18next.t('ai-assistant.entry.nothing-to-enter', { defaultValue: 'nothing to enter' })
4005
4007
  : i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
4006
4008
  </button>
@@ -103,6 +103,11 @@ export class AssistantEntryReview extends LitElement {
103
103
  text-underline-offset: 3px;
104
104
  }
105
105
 
106
+ .before {
107
+ color: var(--md-sys-color-on-surface-variant);
108
+ text-decoration: line-through;
109
+ }
110
+
106
111
  .missing {
107
112
  color: var(--md-sys-color-error);
108
113
  font-style: italic;
@@ -182,8 +187,59 @@ export class AssistantEntryReview extends LitElement {
182
187
  </div>`
183
188
  }
184
189
 
190
+ /**
191
+ * A 수정안 - each record by its name, each changing field as what it holds now and what it becomes, and how the
192
+ * record was picked. The "now" is the server's reading, never the model's memory.
193
+ */
194
+ private renderChanges(p: any) {
195
+ const columns: { name: string; type: string; labelKey?: string }[] = p.columns ?? []
196
+ const rows: any[] = p.rows ?? []
197
+ const counts = p.counts ?? {}
198
+ const via = (row: any) =>
199
+ row.via === 'current' ? t('via-current', 'the record on screen') : row.via === 'selected' ? t('via-selected', 'selected on screen') : t('via-id', 'found in the conversation')
200
+ return html`
201
+ <div class="summary" role="status">
202
+ <span>${t('changes', '{count} records', { count: counts.rows ?? rows.length })}</span>
203
+ <span>${t('ready', '{count} ready', { count: counts.ready ?? 0 })}</span>
204
+ ${counts.blocked ? html`<span class="blocked">${t('not-changed', '{count} will not be changed', { count: counts.blocked })}</span>` : nothing}
205
+ </div>
206
+ <div class="scroll">
207
+ <table>
208
+ <thead>
209
+ <tr>
210
+ <th>#</th>
211
+ <th>${t('record', 'record')}</th>
212
+ ${columns.map(column => html`<th>${column.labelKey ? i18next.t(column.labelKey) : column.name}</th>`)}
213
+ <th>${t('picked', 'picked')}</th>
214
+ </tr>
215
+ </thead>
216
+ <tbody>
217
+ ${rows.slice(0, SHOWN).map(row => {
218
+ const blocked = (row.issues ?? []).some((issue: EntryIssueView) => issue.blocking)
219
+ const whole = entryCellIssues(row)
220
+ return html`<tr class=${blocked ? 'blocked' : ''}>
221
+ <td class="num">${row.index + 1}</td>
222
+ <td>${row.label || row.id}${whole.map(issue => html`<span class="row-issue ${issue.blocking ? 'blocking' : ''}">${issueText(issue)}</span>`)}</td>
223
+ ${columns.map(column => {
224
+ const issues = entryCellIssues(row, column.name)
225
+ const bad = issues.some(issue => issue.blocking)
226
+ const changes = column.name in (row.values ?? {})
227
+ return html`<td class="${column.type === 'number' || column.type === 'integer' ? 'num' : ''} ${bad ? 'bad' : issues.length ? 'note' : ''}" title=${issues.map(issueText).join('\n')}>
228
+ ${changes ? html`<span class="before">${shown(row.before?.[column.name]) || '—'}</span> → <strong>${shown(row.values[column.name]) || '—'}</strong>` : ''}
229
+ </td>`
230
+ })}
231
+ <td class="source">${via(row)}</td>
232
+ </tr>`
233
+ })}
234
+ </tbody>
235
+ </table>
236
+ </div>
237
+ `
238
+ }
239
+
185
240
  render() {
186
241
  const p = this.proposal ?? {}
242
+ if (p.op === 'update') return this.renderChanges(p)
187
243
  const columns: { name: string; type: string; required?: boolean; labelKey?: string }[] = p.columns ?? []
188
244
  const rows: EntryRowView[] = p.rows ?? []
189
245
  const counts = p.counts ?? {}
@@ -43,6 +43,8 @@ export interface EntryRowResult {
43
43
 
44
44
  export interface EntryApplyAnswer extends ProposalOutcome {
45
45
  rows?: EntryRowResult[]
46
+ /** Set by the handler from the card - which words the conversation line uses. */
47
+ op?: string
46
48
  }
47
49
 
48
50
  export function isEntryProposal(proposal: any): boolean {
@@ -66,9 +68,13 @@ export function entryCellIssues(row: EntryRowView, field?: string): EntryIssueVi
66
68
  /** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */
67
69
  export function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string {
68
70
  const refused = (answer.rows ?? []).filter(row => !row.ok)
69
- const head = answer.applied
70
- ? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })
71
- : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' })
71
+ const head = answer.op === 'update'
72
+ ? answer.applied
73
+ ? t('ai-assistant.entry.changed', { defaultValue: 'changed {count} records.', count: answer.applied })
74
+ : t('ai-assistant.entry.changed-none', { defaultValue: 'no record was changed.' })
75
+ : answer.applied
76
+ ? t('ai-assistant.entry.entered', { defaultValue: 'entered {count} rows.', count: answer.applied })
77
+ : t('ai-assistant.entry.entered-none', { defaultValue: 'no row was entered.' })
72
78
  const lines = refused.slice(0, 20).map(row => t('ai-assistant.entry.row-refused', { defaultValue: 'row {n}: {reason}', n: row.index + 1, reason: row.error || '' }))
73
79
  const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : []
74
80
  return [head, ...lines, ...more].join('\n')
@@ -81,7 +87,7 @@ export function entryProposalHandler(
81
87
  ) {
82
88
  return async (proposal: any, context: ProposalContext): Promise<ProposalOutcome> => {
83
89
  if (!context.sessionId || !proposal?.key) return { verdict: 'unavailable', errorCode: 'no-conversation' }
84
- const answer = await apply(context.sessionId, proposal.key)
90
+ const answer = { ...(await apply(context.sessionId, proposal.key)), op: proposal.op }
85
91
  return {
86
92
  verdict: answer.verdict,
87
93
  ...(answer.errorCode ? { errorCode: answer.errorCode } : {}),
@@ -98,6 +104,15 @@ export function entryProposalHandler(
98
104
  */
99
105
  export function latestEntryKeys(proposalsInOrder: any[][]): Map<string, string> {
100
106
  const latest = new Map<string, string>()
101
- for (const list of proposalsInOrder) for (const one of list ?? []) if (isEntryProposal(one) && one.key) latest.set(one.target, one.key)
107
+ for (const list of proposalsInOrder) for (const one of list ?? []) if (isEntryProposal(one) && one.key) latest.set(entryGroupOf(one), one.key)
102
108
  return latest
103
109
  }
110
+
111
+ /** A 입력안 and a 수정안 of one record are counted apart - a newer one of the same kind replaces the older. */
112
+ export function entryGroupOf(proposal: any): string {
113
+ return `${proposal?.target}:${proposal?.op ?? 'create'}`
114
+ }
115
+
116
+ export function isChangeProposal(proposal: any): boolean {
117
+ return isEntryProposal(proposal) && proposal.op === 'update'
118
+ }
@@ -48,7 +48,7 @@ import { isDifferentConversation, resolveConversation, pickInitialMode } from '.
48
48
  import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState } from './proposal-outcome-line.js';
49
49
  import { availableTriggers } from './chat-triggers.js';
50
50
  import './entry-review.js';
51
- import { entryReadyCount, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
51
+ import { entryGroupOf, entryReadyCount, isChangeProposal, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
52
52
  import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js';
53
53
  function formatFileSize(bytes) {
54
54
  if (!bytes || bytes < 1024)
@@ -3514,7 +3514,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3514
3514
  return this.renderWorkspaceCard(p, key);
3515
3515
  const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) });
3516
3516
  const entry = isEntryProposal(p);
3517
- const replaced = entry && latestEntry?.get(p.target) !== key && card.state === 'ready';
3517
+ const replaced = entry && latestEntry?.get(entryGroupOf(p)) !== key && card.state === 'ready';
3518
3518
  const ready = entry ? entryReadyCount(p) : 0;
3519
3519
  const sent = card.state !== 'ready' || replaced || (entry && ready === 0);
3520
3520
  /*
@@ -3555,7 +3555,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3555
3555
  ? this.proposalStateText(key)
3556
3556
  : entry
3557
3557
  ? ready
3558
- ? i18next.t('ai-assistant.entry.enter', { defaultValue: 'enter {count} rows', count: ready })
3558
+ ? isChangeProposal(p)
3559
+ ? i18next.t('ai-assistant.entry.change', { defaultValue: 'change {count} records', count: ready })
3560
+ : i18next.t('ai-assistant.entry.enter', { defaultValue: 'enter {count} rows', count: ready })
3559
3561
  : i18next.t('ai-assistant.entry.nothing-to-enter', { defaultValue: 'nothing to enter' })
3560
3562
  : i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
3561
3563
  </button>