@things-factory/ai-assistant 10.1.88 → 10.1.91

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 (93) hide show
  1. package/client/components/assistant-chat.ts +45 -5
  2. package/client/components/assistant-dock.ts +30 -2
  3. package/client/components/entry-review.ts +246 -0
  4. package/client/components/workspace-view.ts +428 -0
  5. package/client/pages/assistant-workspace-page.ts +237 -0
  6. package/client/route.ts +9 -0
  7. package/client/utils/assistant-proposals.ts +11 -1
  8. package/client/utils/entry-proposal.ts +103 -0
  9. package/client/utils/workspace.ts +89 -0
  10. package/dist-client/components/assistant-chat.d.ts +3 -0
  11. package/dist-client/components/assistant-chat.js +46 -5
  12. package/dist-client/components/assistant-chat.js.map +1 -1
  13. package/dist-client/components/assistant-dock.d.ts +1 -0
  14. package/dist-client/components/assistant-dock.js +25 -2
  15. package/dist-client/components/assistant-dock.js.map +1 -1
  16. package/dist-client/components/entry-review.d.ts +9 -0
  17. package/dist-client/components/entry-review.js +247 -0
  18. package/dist-client/components/entry-review.js.map +1 -0
  19. package/dist-client/components/workspace-view.d.ts +19 -0
  20. package/dist-client/components/workspace-view.js +416 -0
  21. package/dist-client/components/workspace-view.js.map +1 -0
  22. package/dist-client/pages/assistant-workspace-page.d.ts +27 -0
  23. package/dist-client/pages/assistant-workspace-page.js +263 -0
  24. package/dist-client/pages/assistant-workspace-page.js.map +1 -0
  25. package/dist-client/route.d.ts +1 -0
  26. package/dist-client/route.js +10 -0
  27. package/dist-client/route.js.map +1 -0
  28. package/dist-client/tsconfig.tsbuildinfo +1 -1
  29. package/dist-client/utils/assistant-proposals.d.ts +1 -0
  30. package/dist-client/utils/assistant-proposals.js +9 -1
  31. package/dist-client/utils/assistant-proposals.js.map +1 -1
  32. package/dist-client/utils/entry-proposal.d.ts +55 -0
  33. package/dist-client/utils/entry-proposal.js +52 -0
  34. package/dist-client/utils/entry-proposal.js.map +1 -0
  35. package/dist-client/utils/workspace.d.ts +40 -0
  36. package/dist-client/utils/workspace.js +68 -0
  37. package/dist-client/utils/workspace.js.map +1 -0
  38. package/dist-server/service/assistant-mode/registry.js +13 -0
  39. package/dist-server/service/assistant-mode/registry.js.map +1 -1
  40. package/dist-server/service/assistant-mode/resolve.d.ts +11 -0
  41. package/dist-server/service/assistant-mode/resolve.js +15 -3
  42. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  43. package/dist-server/service/assistant-mode/types.d.ts +7 -1
  44. package/dist-server/service/assistant-mode/types.js.map +1 -1
  45. package/dist-server/service/data-entry/entry-apply.d.ts +46 -0
  46. package/dist-server/service/data-entry/entry-apply.js +102 -0
  47. package/dist-server/service/data-entry/entry-apply.js.map +1 -0
  48. package/dist-server/service/data-entry/entry-resolver.d.ts +4 -0
  49. package/dist-server/service/data-entry/entry-resolver.js +80 -0
  50. package/dist-server/service/data-entry/entry-resolver.js.map +1 -0
  51. package/dist-server/service/data-entry/entry-sources.d.ts +1 -0
  52. package/dist-server/service/data-entry/entry-sources.js +41 -0
  53. package/dist-server/service/data-entry/entry-sources.js.map +1 -0
  54. package/dist-server/service/data-entry/entry-target.d.ts +157 -0
  55. package/dist-server/service/data-entry/entry-target.js +343 -0
  56. package/dist-server/service/data-entry/entry-target.js.map +1 -0
  57. package/dist-server/service/data-entry/entry-tools.d.ts +43 -0
  58. package/dist-server/service/data-entry/entry-tools.js +185 -0
  59. package/dist-server/service/data-entry/entry-tools.js.map +1 -0
  60. package/dist-server/service/index.d.ts +6 -1
  61. package/dist-server/service/index.js +13 -2
  62. package/dist-server/service/index.js.map +1 -1
  63. package/dist-server/service/workspace/workspace-blocks.d.ts +24 -0
  64. package/dist-server/service/workspace/workspace-blocks.js +118 -0
  65. package/dist-server/service/workspace/workspace-blocks.js.map +1 -0
  66. package/dist-server/service/workspace/workspace-tools.d.ts +24 -0
  67. package/dist-server/service/workspace/workspace-tools.js +65 -0
  68. package/dist-server/service/workspace/workspace-tools.js.map +1 -0
  69. package/dist-server/tsconfig.tsbuildinfo +1 -1
  70. package/package.json +2 -2
  71. package/server/service/assistant-mode/registry.ts +13 -0
  72. package/server/service/assistant-mode/resolve.test.ts +2 -1
  73. package/server/service/assistant-mode/resolve.ts +15 -3
  74. package/server/service/assistant-mode/types.ts +7 -1
  75. package/server/service/data-entry/data-entry.test.ts +311 -0
  76. package/server/service/data-entry/entry-apply.ts +134 -0
  77. package/server/service/data-entry/entry-resolver.ts +74 -0
  78. package/server/service/data-entry/entry-sources.ts +35 -0
  79. package/server/service/data-entry/entry-target.ts +442 -0
  80. package/server/service/data-entry/entry-tools.ts +174 -0
  81. package/server/service/index.ts +10 -1
  82. package/server/service/workspace/workspace-blocks.ts +127 -0
  83. package/server/service/workspace/workspace-tools.ts +66 -0
  84. package/server/service/workspace/workspace.test.ts +58 -0
  85. package/test/entry-proposal.test.ts +67 -0
  86. package/test/open-doors.test.ts +2 -1
  87. package/test/workspace.test.ts +54 -0
  88. package/things-factory.config.js +6 -1
  89. package/translations/en.json +56 -1
  90. package/translations/ja.json +56 -1
  91. package/translations/ko.json +56 -1
  92. package/translations/ms.json +56 -1
  93. package/translations/zh.json +56 -1
@@ -0,0 +1,103 @@
1
+ /*
2
+ * The data entry card's rules and its handler (operato-application/design/ai/data-entry.md §5).
3
+ *
4
+ * The card is a review, not an editor: a correction is said in the conversation and the model proposes
5
+ * again - the newer card replaces the older one. Pressing asks the server to enter the stored rows; the
6
+ * screen sends no rows of its own.
7
+ */
8
+ import type { ProposalContext, ProposalOutcome } from './assistant-proposals.js'
9
+
10
+ export interface EntryIssueView {
11
+ field?: string
12
+ code: string
13
+ messageKey?: string
14
+ params?: Record<string, unknown>
15
+ blocking: boolean
16
+ }
17
+
18
+ export interface EntryRowView {
19
+ index: number
20
+ values: Record<string, unknown>
21
+ sources: { attachmentId?: string; where?: string }[]
22
+ issues: EntryIssueView[]
23
+ /** The server found this row's numbers and dates together on this line of the cited file. */
24
+ evidence?: { attachmentId: string; line: number }
25
+ }
26
+
27
+ export interface EntryTotalView {
28
+ field: string
29
+ all: number
30
+ ready: number
31
+ declared?: number
32
+ declaredFound?: boolean
33
+ difference?: number
34
+ }
35
+
36
+ export interface EntryRowResult {
37
+ index: number
38
+ ok: boolean
39
+ id?: string
40
+ label?: string
41
+ error?: string
42
+ }
43
+
44
+ export interface EntryApplyAnswer extends ProposalOutcome {
45
+ rows?: EntryRowResult[]
46
+ }
47
+
48
+ export function isEntryProposal(proposal: any): boolean {
49
+ return proposal?.kind === 'entry' && Array.isArray(proposal?.rows)
50
+ }
51
+
52
+ export function entryReadyCount(proposal: any): number {
53
+ return (proposal?.rows ?? []).filter((row: EntryRowView) => !row.issues?.some(issue => issue.blocking)).length
54
+ }
55
+
56
+ /** The i18n key of an issue - the target's own, or the framework's by code. */
57
+ export function entryIssueKey(issue: EntryIssueView): string {
58
+ return issue.messageKey || `ai-assistant.entry.issue.${issue.code}`
59
+ }
60
+
61
+ /** The issues of one cell, and of the row as a whole (no field). */
62
+ export function entryCellIssues(row: EntryRowView, field?: string): EntryIssueView[] {
63
+ return (row.issues ?? []).filter(issue => (field ? issue.field === field : !issue.field))
64
+ }
65
+
66
+ /** What stays in the conversation - the model reads it on its next turn, and the person sees what went in. */
67
+ export function entryOutcomeNote(answer: EntryApplyAnswer, t: (key: string, options?: any) => string): string {
68
+ 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.' })
72
+ 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
+ const more = refused.length > 20 ? [t('ai-assistant.entry.more-refused', { defaultValue: '{count} more rows were not entered.', count: refused.length - 20 })] : []
74
+ return [head, ...lines, ...more].join('\n')
75
+ }
76
+
77
+ /** The handler every entry card runs - built with the mutation so the rules are tested without a server. */
78
+ export function entryProposalHandler(
79
+ apply: (sessionId: string, proposalKey: string) => Promise<EntryApplyAnswer>,
80
+ t: (key: string, options?: any) => string
81
+ ) {
82
+ return async (proposal: any, context: ProposalContext): Promise<ProposalOutcome> => {
83
+ if (!context.sessionId || !proposal?.key) return { verdict: 'unavailable', errorCode: 'no-conversation' }
84
+ const answer = await apply(context.sessionId, proposal.key)
85
+ return {
86
+ verdict: answer.verdict,
87
+ ...(answer.errorCode ? { errorCode: answer.errorCode } : {}),
88
+ applied: answer.applied ?? 0,
89
+ missed: answer.missed ?? 0,
90
+ note: entryOutcomeNote(answer, t)
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * The newest card per record, over the conversation's proposals in order - an older one is replaced and
97
+ * cannot be entered (the server refuses it as `superseded`; the card says so before anyone presses).
98
+ */
99
+ export function latestEntryKeys(proposalsInOrder: any[][]): Map<string, string> {
100
+ 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)
102
+ return latest
103
+ }
@@ -0,0 +1,89 @@
1
+ /*
2
+ * The AI 작업판 on the screen - which versions a workspace has, and the table's sort and CSV
3
+ * (operato-application/design/ai/workspace.md). Pure - tested without a browser.
4
+ */
5
+
6
+ export interface WorkspaceBlockView {
7
+ kind: string
8
+ [key: string]: any
9
+ }
10
+
11
+ export interface WorkspaceView {
12
+ name: string
13
+ title: string
14
+ summary?: string
15
+ blocks: WorkspaceBlockView[]
16
+ }
17
+
18
+ export interface WorkspaceVersion {
19
+ /** 1-based, in the order the conversation made them. */
20
+ version: number
21
+ messageId: string
22
+ createdAt?: string
23
+ workspace: WorkspaceView
24
+ }
25
+
26
+ export function isWorkspaceProposal(proposal: any): boolean {
27
+ return proposal?.kind === 'workspace' && !!proposal?.workspace?.name
28
+ }
29
+
30
+ /**
31
+ * Every version of the named workspace in a conversation, oldest first. A message the reader may not see
32
+ * comes back redacted without its proposals, so a version behind a door is simply not among them.
33
+ */
34
+ export function workspaceVersions(messages: { id: string; createdAt?: string; proposalsJson?: any[] | null }[], name: string): WorkspaceVersion[] {
35
+ const versions: WorkspaceVersion[] = []
36
+ for (const message of messages) {
37
+ for (const proposal of message.proposalsJson ?? []) {
38
+ if (isWorkspaceProposal(proposal) && proposal.workspace.name === name) {
39
+ versions.push({ version: versions.length + 1, messageId: message.id, createdAt: message.createdAt, workspace: proposal.workspace })
40
+ }
41
+ }
42
+ }
43
+ return versions
44
+ }
45
+
46
+ /** The version of a card within its conversation - "2 of 3" on the card and on the page. */
47
+ export function versionOfCard(proposalsInOrder: any[][], name: string, key: string): { version: number; of: number } {
48
+ let version = 0
49
+ let of = 0
50
+ for (const list of proposalsInOrder)
51
+ for (const one of list ?? []) {
52
+ if (!isWorkspaceProposal(one) || one.workspace.name !== name) continue
53
+ of += 1
54
+ if (one.key === key) version = of
55
+ }
56
+ return { version, of }
57
+ }
58
+
59
+ export function workspaceAddress(sessionId: string, name: string, version?: number): string {
60
+ const query = new URLSearchParams({ session: sessionId, name })
61
+ if (version) query.set('version', String(version))
62
+ return `assistant-workspace?${query.toString()}`
63
+ }
64
+
65
+ /** Sorts by one column - numbers as numbers, empty last, the rest by the reader's collation. */
66
+ export function sortRows(rows: Record<string, any>[], column: string, direction: 'asc' | 'desc', locale?: string): Record<string, any>[] {
67
+ const factor = direction === 'asc' ? 1 : -1
68
+ const collator = new Intl.Collator(locale, { numeric: true })
69
+ return [...rows].sort((a, b) => {
70
+ const x = a[column]
71
+ const y = b[column]
72
+ const emptyX = x === null || x === undefined || x === ''
73
+ const emptyY = y === null || y === undefined || y === ''
74
+ if (emptyX || emptyY) return emptyX === emptyY ? 0 : emptyX ? 1 : -1
75
+ if (typeof x === 'number' && typeof y === 'number') return (x - y) * factor
76
+ return collator.compare(String(x), String(y)) * factor
77
+ })
78
+ }
79
+
80
+ const csvCell = (value: unknown) => {
81
+ const text = value === null || value === undefined ? '' : String(value)
82
+ return /[",\n\r]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text
83
+ }
84
+
85
+ /** The table as CSV - the heading row in the column labels, a BOM so a spreadsheet reads Korean. */
86
+ export function tableCsv(columns: { name: string; label?: string }[], rows: Record<string, any>[]): string {
87
+ const lines = [columns.map(column => csvCell(column.label || column.name)).join(','), ...rows.map(row => columns.map(column => csvCell(row[column.name])).join(','))]
88
+ return '' + lines.join('\r\n')
89
+ }
@@ -20,6 +20,7 @@ import { LitElement } from 'lit';
20
20
  import './mention-popup.js';
21
21
  import { type FilterResult } from './mention-popup.js';
22
22
  import { type AssistantMode } from './assistant-mode.js';
23
+ import './entry-review.js';
23
24
  export interface ChatAttachment {
24
25
  id: string;
25
26
  file: File;
@@ -539,6 +540,8 @@ export declare class OxAssistantChat extends LitElement {
539
540
  * 실행 결과는 호스트가 **대화에 기록**한다 — 누가 무엇을 실행했는지가 협의 이력에 남아야 한다.
540
541
  */
541
542
  private renderProposals;
543
+ /** The AI 작업판 card - what it holds, which version, and the way to open it. The person opens it; the model does not. */
544
+ private renderWorkspaceCard;
542
545
  /**
543
546
  * 눌린 카드가 말하는 것 — **호스트가 한 일**이고, 아직 모르면 모른다고 말한다.
544
547
  *
@@ -47,6 +47,9 @@ import { chatDefaults } from './chat-defaults.js';
47
47
  import { isDifferentConversation, resolveConversation, pickInitialMode } from './assistant-mode.js';
48
48
  import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState } from './proposal-outcome-line.js';
49
49
  import { availableTriggers } from './chat-triggers.js';
50
+ import './entry-review.js';
51
+ import { entryReadyCount, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
52
+ import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js';
50
53
  function formatFileSize(bytes) {
51
54
  if (!bytes || bytes < 1024)
52
55
  return `${bytes || 0} B`;
@@ -3500,12 +3503,20 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3500
3503
  /* The card's state is the conversation's rows - read once for every card of this line. */
3501
3504
  const outcomes = this.proposalOutcomes;
3502
3505
  const requested = this.proposalRequests;
3506
+ /* A data entry card replaced by a newer one for the same record cannot be entered (design/ai/data-entry.md §5). */
3507
+ const latestEntry = items.some(isEntryProposal) ? latestEntryKeys(this.lines.map(one => one.proposals ?? [])) : undefined;
3503
3508
  return html `
3504
3509
  <div class="proposals">
3505
3510
  ${items.map(p => {
3506
3511
  const key = this.proposalKey(p);
3512
+ /* A workspace card opens the page; it runs nothing (design/ai/workspace.md §4). */
3513
+ if (isWorkspaceProposal(p))
3514
+ return this.renderWorkspaceCard(p, key);
3507
3515
  const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) });
3508
- const sent = card.state !== 'ready';
3516
+ const entry = isEntryProposal(p);
3517
+ const replaced = entry && latestEntry?.get(p.target) !== key && card.state === 'ready';
3518
+ const ready = entry ? entryReadyCount(p) : 0;
3519
+ const sent = card.state !== 'ready' || replaced || (entry && ready === 0);
3509
3520
  /*
3510
3521
  Follow-up chips are the host's words, never this component's. The fallback that used
3511
3522
  to live here named a 3D modeller's operations in hardcoded Korean, and it reached the
@@ -3516,7 +3527,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3516
3527
  <div class="proposal ${sent ? 'sent' : ''}">
3517
3528
  <md-icon>${p.icon || (p.source ? 'view_in_ar' : 'bolt')}</md-icon>
3518
3529
  <span class="pbody">
3519
- <span class="plabel">${p.label || p.command}</span>
3530
+ <span class="plabel">${entry && p.labelKey ? i18next.t(p.labelKey) : p.label || p.command}</span>
3520
3531
  ${p.reason ? html `<span class="preason">${p.reason}</span>` : nothing}
3521
3532
  </span>
3522
3533
  ${ /*
@@ -3538,9 +3549,15 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3538
3549
  class="run"
3539
3550
  ?disabled=${sent}
3540
3551
  @click=${() => this.executeProposal(p, key)}>
3541
- ${sent
3542
- ? this.proposalStateText(key)
3543
- : i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
3552
+ ${replaced
3553
+ ? i18next.t('ai-assistant.entry.replaced', { defaultValue: 'replaced by a newer card' })
3554
+ : card.state !== 'ready'
3555
+ ? this.proposalStateText(key)
3556
+ : entry
3557
+ ? ready
3558
+ ? i18next.t('ai-assistant.entry.enter', { defaultValue: 'enter {count} rows', count: ready })
3559
+ : i18next.t('ai-assistant.entry.nothing-to-enter', { defaultValue: 'nothing to enter' })
3560
+ : i18next.t('ai-assistant.button.run-proposal', { defaultValue: 'run' })}
3544
3561
  </button>
3545
3562
  `}
3546
3563
  ${ /*
@@ -3556,6 +3573,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3556
3573
  </span>
3557
3574
  ` : nothing}
3558
3575
  </div>
3576
+ ${entry ? html `<ox-assistant-entry-review .proposal=${p}></ox-assistant-entry-review>` : nothing}
3559
3577
  ${rawChoices.length ? html `
3560
3578
  <div class="proposal-choices" role="group" aria-label=${i18next.t('ai-assistant.text.proposal-choices', { defaultValue: 'follow-up choices' })}>
3561
3579
  ${rawChoices.map((c) => html `
@@ -3574,6 +3592,29 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3574
3592
  </div>
3575
3593
  `;
3576
3594
  }
3595
+ /** The AI 작업판 card - what it holds, which version, and the way to open it. The person opens it; the model does not. */
3596
+ renderWorkspaceCard(p, key) {
3597
+ const { version, of } = versionOfCard(this.lines.map(one => one.proposals ?? []), p.workspace.name, key);
3598
+ const contents = Object.entries(p.contents ?? {})
3599
+ .map(([kind, count]) => String(i18next.t(`ai-assistant.workspace.contains-${kind}`, { defaultValue: `${kind} {count}`, count: Number(count) })))
3600
+ .join(' · ');
3601
+ const open = () => {
3602
+ if (!this.sessionId)
3603
+ return;
3604
+ /* The page is the application's; navigate is the shell's, reached by an event so this component stays standalone. */
3605
+ this.dispatchEvent(new CustomEvent('assistant-open-page', { detail: { href: workspaceAddress(this.sessionId, p.workspace.name, version) }, bubbles: true, composed: true }));
3606
+ };
3607
+ return html `
3608
+ <div class="proposal workspace">
3609
+ <md-icon>${p.icon || 'dashboard'}</md-icon>
3610
+ <span class="pbody">
3611
+ <span class="plabel">${p.workspace.title}</span>
3612
+ <span class="preason">${[contents, of > 1 ? i18next.t('ai-assistant.workspace.version', { defaultValue: 'version {version} of {of}', version, of }) : ''].filter(Boolean).join(' · ')}</span>
3613
+ </span>
3614
+ <button class="run" ?disabled=${!this.sessionId} @click=${open}>${i18next.t('ai-assistant.workspace.open', { defaultValue: 'open in workspace' })}</button>
3615
+ </div>
3616
+ `;
3617
+ }
3577
3618
  /**
3578
3619
  * 눌린 카드가 말하는 것 — **호스트가 한 일**이고, 아직 모르면 모른다고 말한다.
3579
3620
  *