@things-factory/ai-assistant 10.1.213 → 10.1.214
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-selection-bar.test.ts +42 -0
- package/client/components/assistant-selection-bar.ts +129 -0
- package/client/index.ts +2 -0
- package/client/utils/assistant-selection.test.ts +25 -1
- package/client/utils/assistant-selection.ts +27 -0
- package/dist-client/components/assistant-selection-bar.d.ts +44 -0
- package/dist-client/components/assistant-selection-bar.js +149 -0
- package/dist-client/components/assistant-selection-bar.js.map +1 -0
- package/dist-client/components/assistant-selection-bar.test.d.ts +1 -0
- package/dist-client/components/assistant-selection-bar.test.js +38 -0
- package/dist-client/components/assistant-selection-bar.test.js.map +1 -0
- package/dist-client/index.d.ts +1 -0
- package/dist-client/index.js +2 -0
- package/dist-client/index.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-selection.d.ts +24 -0
- package/dist-client/utils/assistant-selection.js +24 -0
- package/dist-client/utils/assistant-selection.js.map +1 -1
- package/dist-client/utils/assistant-selection.test.js +21 -1
- package/dist-client/utils/assistant-selection.test.js.map +1 -1
- package/dist-server/service/assistant-chat-resolver.js +5 -3
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.d.ts +15 -0
- package/dist-server/service/assistant-mode/mode-categories.js +28 -1
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/assistant-chat-resolver.ts +6 -4
- package/server/service/assistant-mode/mode-categories.ts +31 -1
- package/server/service/assistant-mode/mode-front.test.ts +27 -1
- package/translations/en.json +8 -1
- package/translations/es.json +8 -1
- package/translations/ja.json +8 -1
- package/translations/ko.json +8 -1
- package/translations/ms.json +8 -1
- package/translations/zh.json +8 -1
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 「AI와 작업」 over a list (ox-assistant-selection-bar) - one bar for every application's lists. What it is held to here is
|
|
3
|
+
* its source and its words; its behaviour on a real grid is held by the applications' browser tests (operato-office
|
|
4
|
+
* test/browser/page-ai-input.test.js).
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'node:fs'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
const bare = readFileSync(join(__dirname, 'assistant-selection-bar.ts'), 'utf-8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
10
|
+
|
|
11
|
+
describe('the selection bar', () => {
|
|
12
|
+
it('★ counts every way a grid picks - a row, the header\'s select-all, a new page of rows', () => {
|
|
13
|
+
expect(bare).toMatch(/GRID_EVENTS = \['select-record-change', 'select-all-change', 'list-adopted'\]/)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('★ asks for its grid when it needs it - a page finds its grid only after its first render', () => {
|
|
17
|
+
expect(bare).toMatch(/gridOf\?: \(\) => SelectionGrid \| undefined/)
|
|
18
|
+
expect(bare).not.toMatch(/@property\(\{ attribute: false \}\) grist/)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('★ hands the rows as keys of the list, to this screen\'s conversation, sent at once', () => {
|
|
22
|
+
expect(bare).toMatch(/selection: \{ title: this\.name, list: this\.list, records: selectedRecords\(rows, \{ axis: this\.axis, labelOf: this\.labelOf \}\) \}/)
|
|
23
|
+
expect(bare).toMatch(/ask: request\?\.trim\(\) \|\| i18next\.t\('ai-assistant\.selection\.ask'/)
|
|
24
|
+
expect(bare).not.toMatch(/draft|continueCurrent|transcript/)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('keeps the words unless the request was sent', () => {
|
|
28
|
+
expect(bare).toMatch(/if \(outcome\?\.sent && this\.request === request\) this\.request = ''/)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('every word it shows reads in every language', () => {
|
|
32
|
+
const keys = [...new Set([...bare.matchAll(/'(ai-assistant\.selection\.[\w-]+)'/g)].map(([, key]) => key))]
|
|
33
|
+
expect(keys.length).toBe(7)
|
|
34
|
+
for (const locale of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {
|
|
35
|
+
const words = JSON.parse(readFileSync(join(__dirname, `../../translations/${locale}.json`), 'utf-8'))
|
|
36
|
+
for (const key of keys) expect([locale, key, typeof words[key]]).toEqual([locale, key, 'string'])
|
|
37
|
+
expect(words['ai-assistant.selection.ask']).toMatch(/\{n\}/)
|
|
38
|
+
expect(words['ai-assistant.selection.ask']).toMatch(/\{title\}/)
|
|
39
|
+
expect(words['ai-assistant.selection.count']).toMatch(/\{n\}/)
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { css, html, LitElement, nothing } from 'lit'
|
|
2
|
+
import { customElement, property, state } from 'lit/decorators.js'
|
|
3
|
+
|
|
4
|
+
import { i18next } from '@operato/i18n'
|
|
5
|
+
import { notify } from '@operato/layout'
|
|
6
|
+
import { ButtonStyles, FormFieldStyles } from '@operato/styles'
|
|
7
|
+
|
|
8
|
+
import { askAssistant } from '../utils/assistant-ask.js'
|
|
9
|
+
import { selectedRecords } from '../utils/assistant-selection.js'
|
|
10
|
+
|
|
11
|
+
/** What a grid says when its picks or its rows change - a row's picker, the header's select-all, a new page of rows. */
|
|
12
|
+
const GRID_EVENTS = ['select-record-change', 'select-all-change', 'list-adopted']
|
|
13
|
+
|
|
14
|
+
/** The part of a grid (data-grist `ox-grist`) the bar reads. */
|
|
15
|
+
export interface SelectionGrid extends HTMLElement {
|
|
16
|
+
selected?: any[]
|
|
17
|
+
updateComplete?: Promise<unknown>
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 「AI와 작업」 over a list - one bar for every application's lists (design/ai/shared-dock.md §2-11-1). While rows are
|
|
22
|
+
* picked it shows how many, what the list itself does with them, and a request sent to this screen's conversation with
|
|
23
|
+
* the rows as the selection. Begun on the Office ledgers; the user, 2026-10-25: 「'AI와 작업' 기능을 사용하려면, 그리드
|
|
24
|
+
* 선택기능이 필요한데, 주요 리스트 화면에서 선택기능이 없는 경우가 꽤 있다」, and it is to serve plant, warehouse and twin.
|
|
25
|
+
*
|
|
26
|
+
* A screen gives the grid (asked for when needed - a page finds its grid only after its first render), the list as the
|
|
27
|
+
* person knows it, the list query and its axis (the last part of the list's read key, so a row goes as `<axis>:<uuid>`),
|
|
28
|
+
* and, if it likes, its own actions and words for a row. The grid shows `SELECTION_ROW_PICKER`.
|
|
29
|
+
*/
|
|
30
|
+
@customElement('ox-assistant-selection-bar')
|
|
31
|
+
export class OxAssistantSelectionBar extends LitElement {
|
|
32
|
+
static styles = [
|
|
33
|
+
ButtonStyles,
|
|
34
|
+
FormFieldStyles,
|
|
35
|
+
css`
|
|
36
|
+
:host { display:block; }
|
|
37
|
+
.selection-work { display:flex; flex-wrap:wrap; align-items:center; gap:var(--spacing-medium); padding:var(--spacing-medium) var(--spacing-large); border-bottom:1px solid var(--ui-line); background:var(--ui-panel); }
|
|
38
|
+
.scope { font-size:var(--ui-t-body); font-weight:600; white-space:nowrap; }
|
|
39
|
+
form { display:flex; flex:1 1 320px; min-width:0; gap:var(--spacing-small); }
|
|
40
|
+
input { flex:1; min-width:0; width:100%; }
|
|
41
|
+
.tools { display:flex; flex-wrap:wrap; gap:var(--spacing-small); }
|
|
42
|
+
`
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/** The grid whose picked rows these are, asked for when needed. */
|
|
46
|
+
@property({ attribute: false }) gridOf?: () => SelectionGrid | undefined
|
|
47
|
+
/** The list as the person knows it (「계약 대장」). */
|
|
48
|
+
@property() name = ''
|
|
49
|
+
/** The list query the rows came from (`contracts`) - one the assistant reads. */
|
|
50
|
+
@property() list = ''
|
|
51
|
+
/** The last part of that list's read key (`office.contract` → `contract`): each row goes as `<axis>:<uuid>`. */
|
|
52
|
+
@property() axis = ''
|
|
53
|
+
/** What the list itself does with the picked rows, before 「AI와 작업」. */
|
|
54
|
+
@property({ attribute: false }) actions: { title: string; action: () => any }[] = []
|
|
55
|
+
/** A row in the person's words, when the code and name do not say it (`selectionLabel`). */
|
|
56
|
+
@property({ attribute: false }) labelOf?: (row: any) => string
|
|
57
|
+
|
|
58
|
+
@state() count = 0
|
|
59
|
+
@state() request = ''
|
|
60
|
+
|
|
61
|
+
private get grid() {
|
|
62
|
+
return this.gridOf?.()
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* The grid's own events reach the page's root (bubbling, composed); the bar counts those that come from its grid. */
|
|
66
|
+
private root?: Node
|
|
67
|
+
private recount = async (event?: Event) => {
|
|
68
|
+
const grid = this.grid
|
|
69
|
+
if (event && (!grid || !event.composedPath().includes(grid))) return
|
|
70
|
+
await grid?.updateComplete
|
|
71
|
+
this.count = grid?.selected?.length ?? 0
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
connectedCallback() {
|
|
75
|
+
super.connectedCallback()
|
|
76
|
+
this.root = this.getRootNode()
|
|
77
|
+
for (const event of GRID_EVENTS) this.root.addEventListener(event, this.recount)
|
|
78
|
+
void this.recount()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
disconnectedCallback() {
|
|
82
|
+
super.disconnectedCallback()
|
|
83
|
+
for (const event of GRID_EVENTS) this.root?.removeEventListener(event, this.recount)
|
|
84
|
+
this.root = undefined
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
willUpdate(changes: Map<string, unknown>) {
|
|
88
|
+
if (changes.has('gridOf')) void this.recount()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/*
|
|
92
|
+
* A screen request (design/ai/shared-dock.md §2-11-1, the user: 「"AI와 작업" 기능이 unstable 하다 … 새로운 세션을 여는지,
|
|
93
|
+
* 기존 세션을 활용하는지 등등을 명확하게 하라」): it goes to this screen's conversation with the rows picked now, in
|
|
94
|
+
* screen order, and is sent at once - what was written on the bar, or the set request. The bar is cleared only when it
|
|
95
|
+
* was sent; otherwise the dock has said why and the words stay to press again.
|
|
96
|
+
*/
|
|
97
|
+
async ask(request: string = this.request) {
|
|
98
|
+
const rows = this.grid?.selected ?? []
|
|
99
|
+
if (!rows.length) {
|
|
100
|
+
notify({ message: i18next.t('ai-assistant.selection.pick-rows'), level: 'info' })
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
if (rows.some(row => !row.id || row.__dirty__)) {
|
|
104
|
+
notify({ message: i18next.t('ai-assistant.selection.save-first'), level: 'warn' })
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
const outcome = await askAssistant({
|
|
108
|
+
selection: { title: this.name, list: this.list, records: selectedRecords(rows, { axis: this.axis, labelOf: this.labelOf }) },
|
|
109
|
+
ask: request?.trim() || i18next.t('ai-assistant.selection.ask', { n: rows.length, title: this.name })
|
|
110
|
+
})
|
|
111
|
+
if (outcome?.sent && this.request === request) this.request = ''
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render() {
|
|
115
|
+
return this.count
|
|
116
|
+
? html`<div class="selection-work" role="region" aria-label=${i18next.t('ai-assistant.selection.region')}>
|
|
117
|
+
<span class="scope" role="status">${i18next.t('ai-assistant.selection.count', { n: this.count })}</span>
|
|
118
|
+
${this.actions.length
|
|
119
|
+
? html`<div class="tools">${this.actions.map(action => html`<button quiet small @click=${action.action}>${action.title}</button>`)}</div>`
|
|
120
|
+
: nothing}
|
|
121
|
+
<form @submit=${(event: Event) => { event.preventDefault(); this.ask() }}>
|
|
122
|
+
<input aria-label=${i18next.t('ai-assistant.selection.request')} placeholder=${i18next.t('ai-assistant.selection.request')}
|
|
123
|
+
.value=${this.request} @input=${(event: any) => (this.request = event.target.value)} />
|
|
124
|
+
<button quiet small type="submit">${i18next.t('ai-assistant.selection.work-with-ai')}</button>
|
|
125
|
+
</form>
|
|
126
|
+
</div>`
|
|
127
|
+
: nothing
|
|
128
|
+
}
|
|
129
|
+
}
|
package/client/index.ts
CHANGED
|
@@ -24,6 +24,8 @@ export * from './utils/assistant-proposals.js'
|
|
|
24
24
|
export * from './components/workspace-view.js'
|
|
25
25
|
export * from './utils/assistant-ask.js'
|
|
26
26
|
export * from './utils/assistant-selection.js'
|
|
27
|
+
/* 「AI와 작업」 over a list - every application's lists hand their picked rows by it (shared-dock §2-11-1). */
|
|
28
|
+
export * from './components/assistant-selection-bar.js'
|
|
27
29
|
export * from './utils/assistant-activity.js'
|
|
28
30
|
export * from './utils/assistant-request-context.js'
|
|
29
31
|
export * from './utils/assistant-capability.js'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { noteForDisplay, selectionNote, SELECTION_KEYS_PREFIX } from './assistant-selection'
|
|
1
|
+
import { noteForDisplay, selectedRecords, selectionLabel, selectionNote, SELECTION_KEYS_PREFIX, SELECTION_ROW_PICKER } from './assistant-selection'
|
|
2
2
|
|
|
3
3
|
const t = (_key: string, defaultValue: string, options: any = {}) => defaultValue.replace(/\{(\w+)\}/g, (_, name) => String(options[name]))
|
|
4
4
|
const contract = (id: string, label: string) => ({ id, label })
|
|
@@ -28,3 +28,27 @@ describe('the records picked on a screen, as a note', () => {
|
|
|
28
28
|
expect(selectionNote({ title: 'L', records: [] }, t)).toBe('')
|
|
29
29
|
})
|
|
30
30
|
})
|
|
31
|
+
|
|
32
|
+
describe('the rows a list hands over (ox-assistant-selection-bar)', () => {
|
|
33
|
+
it('★ each row goes as the key its list\'s read tool takes - `<axis>:<uuid>`', () => {
|
|
34
|
+
expect(selectedRecords([{ id: 'u1', code: 'H1', name: 'A' }], { axis: 'contract' })).toEqual([{ id: 'contract:u1', label: 'H1 · A' }])
|
|
35
|
+
expect(selectedRecords([{ id: 'u1', name: 'A' }])).toEqual([{ id: 'u1', label: 'A' }])
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('saved rows only, each once, in screen order; never the whole row', () => {
|
|
39
|
+
const row = { id: 'a', number: 'INV-1', title: 'T', bankDetails: 'secret' }
|
|
40
|
+
const records = selectedRecords([row, row, { id: 'b', __dirty__: 'M', name: 'B' }, { name: 'no id' }, { id: 'c', name: 'C' }])
|
|
41
|
+
expect(records).toEqual([{ id: 'a', label: 'INV-1 · T' }, { id: 'c', label: 'C' }])
|
|
42
|
+
expect(JSON.stringify(records)).not.toMatch(/secret/)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('a screen may say a row in its own words', () => {
|
|
46
|
+
expect(selectedRecords([{ id: 'q', number: 'Q-7', version: 2 }], { labelOf: row => `${row.number} v${row.version}` })).toEqual([{ id: 'q', label: 'Q-7 v2' }])
|
|
47
|
+
expect(selectionLabel({ code: 'C', description: 'D' })).toBe('C · D')
|
|
48
|
+
expect(selectionLabel({})).toBe('')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('the row picker is the grid\'s own, many at once', () => {
|
|
52
|
+
expect(SELECTION_ROW_PICKER).toEqual({ type: 'gutter', gutterName: 'row-selector', multiple: true })
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -7,6 +7,33 @@
|
|
|
7
7
|
* opened on `{"source":"explicit-page-selection",…}`.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The grid's own row picker - a list that hands its rows to the assistant shows it beside `ox-assistant-selection-bar`
|
|
12
|
+
* (data-grist gutter `row-selector`, many at once).
|
|
13
|
+
*/
|
|
14
|
+
export const SELECTION_ROW_PICKER = { type: 'gutter', gutterName: 'row-selector', multiple: true }
|
|
15
|
+
|
|
16
|
+
/** A picked row as the person reads it, when the screen gives no words of its own - its code or number, then its name. */
|
|
17
|
+
export function selectionLabel(row: any): string {
|
|
18
|
+
return [row?.code || row?.number, row?.name || row?.title || row?.description]
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join(' · ')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The rows a person picked, as the assistant is handed them (`askAssistant({ selection })`): saved rows only (an id, no
|
|
25
|
+
* pending edit), each once, in screen order - never whole rows. Given the list's axis, each id is the record's key
|
|
26
|
+
* `<axis>:<uuid>`, the one that list's read tool takes (read-target.ts `recordIdOfKey`): a bare uuid names no tool, and
|
|
27
|
+
* a model read a contract's id with a dashboard's card tool (2026-10-25, Office 미수금).
|
|
28
|
+
*/
|
|
29
|
+
export function selectedRecords(rows: any[], options: { axis?: string; labelOf?: (row: any) => string } = {}): { id: string; label: string }[] {
|
|
30
|
+
const { axis, labelOf = selectionLabel } = options
|
|
31
|
+
const seen = new Set<string>()
|
|
32
|
+
return (rows ?? [])
|
|
33
|
+
.filter(row => row?.id && !row.__dirty__ && !seen.has(row.id) && !!seen.add(row.id))
|
|
34
|
+
.map(row => ({ id: axis ? `${axis}:${row.id}` : row.id, label: labelOf(row) || '' }))
|
|
35
|
+
}
|
|
36
|
+
|
|
10
37
|
/** The line the model reads begins with this; the conversation does not draw it. */
|
|
11
38
|
export const SELECTION_KEYS_PREFIX = '[keys]'
|
|
12
39
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { LitElement, nothing } from 'lit';
|
|
2
|
+
/** The part of a grid (data-grist `ox-grist`) the bar reads. */
|
|
3
|
+
export interface SelectionGrid extends HTMLElement {
|
|
4
|
+
selected?: any[];
|
|
5
|
+
updateComplete?: Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 「AI와 작업」 over a list - one bar for every application's lists (design/ai/shared-dock.md §2-11-1). While rows are
|
|
9
|
+
* picked it shows how many, what the list itself does with them, and a request sent to this screen's conversation with
|
|
10
|
+
* the rows as the selection. Begun on the Office ledgers; the user, 2026-10-25: 「'AI와 작업' 기능을 사용하려면, 그리드
|
|
11
|
+
* 선택기능이 필요한데, 주요 리스트 화면에서 선택기능이 없는 경우가 꽤 있다」, and it is to serve plant, warehouse and twin.
|
|
12
|
+
*
|
|
13
|
+
* A screen gives the grid (asked for when needed - a page finds its grid only after its first render), the list as the
|
|
14
|
+
* person knows it, the list query and its axis (the last part of the list's read key, so a row goes as `<axis>:<uuid>`),
|
|
15
|
+
* and, if it likes, its own actions and words for a row. The grid shows `SELECTION_ROW_PICKER`.
|
|
16
|
+
*/
|
|
17
|
+
export declare class OxAssistantSelectionBar extends LitElement {
|
|
18
|
+
static styles: import("lit").CSSResult[];
|
|
19
|
+
/** The grid whose picked rows these are, asked for when needed. */
|
|
20
|
+
gridOf?: () => SelectionGrid | undefined;
|
|
21
|
+
/** The list as the person knows it (「계약 대장」). */
|
|
22
|
+
name: string;
|
|
23
|
+
/** The list query the rows came from (`contracts`) - one the assistant reads. */
|
|
24
|
+
list: string;
|
|
25
|
+
/** The last part of that list's read key (`office.contract` → `contract`): each row goes as `<axis>:<uuid>`. */
|
|
26
|
+
axis: string;
|
|
27
|
+
/** What the list itself does with the picked rows, before 「AI와 작업」. */
|
|
28
|
+
actions: {
|
|
29
|
+
title: string;
|
|
30
|
+
action: () => any;
|
|
31
|
+
}[];
|
|
32
|
+
/** A row in the person's words, when the code and name do not say it (`selectionLabel`). */
|
|
33
|
+
labelOf?: (row: any) => string;
|
|
34
|
+
count: number;
|
|
35
|
+
request: string;
|
|
36
|
+
private get grid();
|
|
37
|
+
private root?;
|
|
38
|
+
private recount;
|
|
39
|
+
connectedCallback(): void;
|
|
40
|
+
disconnectedCallback(): void;
|
|
41
|
+
willUpdate(changes: Map<string, unknown>): void;
|
|
42
|
+
ask(request?: string): Promise<void>;
|
|
43
|
+
render(): import("lit-html").TemplateResult<1> | typeof nothing;
|
|
44
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
3
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
4
|
+
import { i18next } from '@operato/i18n';
|
|
5
|
+
import { notify } from '@operato/layout';
|
|
6
|
+
import { ButtonStyles, FormFieldStyles } from '@operato/styles';
|
|
7
|
+
import { askAssistant } from '../utils/assistant-ask.js';
|
|
8
|
+
import { selectedRecords } from '../utils/assistant-selection.js';
|
|
9
|
+
/** What a grid says when its picks or its rows change - a row's picker, the header's select-all, a new page of rows. */
|
|
10
|
+
const GRID_EVENTS = ['select-record-change', 'select-all-change', 'list-adopted'];
|
|
11
|
+
/**
|
|
12
|
+
* 「AI와 작업」 over a list - one bar for every application's lists (design/ai/shared-dock.md §2-11-1). While rows are
|
|
13
|
+
* picked it shows how many, what the list itself does with them, and a request sent to this screen's conversation with
|
|
14
|
+
* the rows as the selection. Begun on the Office ledgers; the user, 2026-10-25: 「'AI와 작업' 기능을 사용하려면, 그리드
|
|
15
|
+
* 선택기능이 필요한데, 주요 리스트 화면에서 선택기능이 없는 경우가 꽤 있다」, and it is to serve plant, warehouse and twin.
|
|
16
|
+
*
|
|
17
|
+
* A screen gives the grid (asked for when needed - a page finds its grid only after its first render), the list as the
|
|
18
|
+
* person knows it, the list query and its axis (the last part of the list's read key, so a row goes as `<axis>:<uuid>`),
|
|
19
|
+
* and, if it likes, its own actions and words for a row. The grid shows `SELECTION_ROW_PICKER`.
|
|
20
|
+
*/
|
|
21
|
+
let OxAssistantSelectionBar = class OxAssistantSelectionBar extends LitElement {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
/** The list as the person knows it (「계약 대장」). */
|
|
25
|
+
this.name = '';
|
|
26
|
+
/** The list query the rows came from (`contracts`) - one the assistant reads. */
|
|
27
|
+
this.list = '';
|
|
28
|
+
/** The last part of that list's read key (`office.contract` → `contract`): each row goes as `<axis>:<uuid>`. */
|
|
29
|
+
this.axis = '';
|
|
30
|
+
/** What the list itself does with the picked rows, before 「AI와 작업」. */
|
|
31
|
+
this.actions = [];
|
|
32
|
+
this.count = 0;
|
|
33
|
+
this.request = '';
|
|
34
|
+
this.recount = async (event) => {
|
|
35
|
+
const grid = this.grid;
|
|
36
|
+
if (event && (!grid || !event.composedPath().includes(grid)))
|
|
37
|
+
return;
|
|
38
|
+
await grid?.updateComplete;
|
|
39
|
+
this.count = grid?.selected?.length ?? 0;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
static { this.styles = [
|
|
43
|
+
ButtonStyles,
|
|
44
|
+
FormFieldStyles,
|
|
45
|
+
css `
|
|
46
|
+
:host { display:block; }
|
|
47
|
+
.selection-work { display:flex; flex-wrap:wrap; align-items:center; gap:var(--spacing-medium); padding:var(--spacing-medium) var(--spacing-large); border-bottom:1px solid var(--ui-line); background:var(--ui-panel); }
|
|
48
|
+
.scope { font-size:var(--ui-t-body); font-weight:600; white-space:nowrap; }
|
|
49
|
+
form { display:flex; flex:1 1 320px; min-width:0; gap:var(--spacing-small); }
|
|
50
|
+
input { flex:1; min-width:0; width:100%; }
|
|
51
|
+
.tools { display:flex; flex-wrap:wrap; gap:var(--spacing-small); }
|
|
52
|
+
`
|
|
53
|
+
]; }
|
|
54
|
+
get grid() {
|
|
55
|
+
return this.gridOf?.();
|
|
56
|
+
}
|
|
57
|
+
connectedCallback() {
|
|
58
|
+
super.connectedCallback();
|
|
59
|
+
this.root = this.getRootNode();
|
|
60
|
+
for (const event of GRID_EVENTS)
|
|
61
|
+
this.root.addEventListener(event, this.recount);
|
|
62
|
+
void this.recount();
|
|
63
|
+
}
|
|
64
|
+
disconnectedCallback() {
|
|
65
|
+
super.disconnectedCallback();
|
|
66
|
+
for (const event of GRID_EVENTS)
|
|
67
|
+
this.root?.removeEventListener(event, this.recount);
|
|
68
|
+
this.root = undefined;
|
|
69
|
+
}
|
|
70
|
+
willUpdate(changes) {
|
|
71
|
+
if (changes.has('gridOf'))
|
|
72
|
+
void this.recount();
|
|
73
|
+
}
|
|
74
|
+
/*
|
|
75
|
+
* A screen request (design/ai/shared-dock.md §2-11-1, the user: 「"AI와 작업" 기능이 unstable 하다 … 새로운 세션을 여는지,
|
|
76
|
+
* 기존 세션을 활용하는지 등등을 명확하게 하라」): it goes to this screen's conversation with the rows picked now, in
|
|
77
|
+
* screen order, and is sent at once - what was written on the bar, or the set request. The bar is cleared only when it
|
|
78
|
+
* was sent; otherwise the dock has said why and the words stay to press again.
|
|
79
|
+
*/
|
|
80
|
+
async ask(request = this.request) {
|
|
81
|
+
const rows = this.grid?.selected ?? [];
|
|
82
|
+
if (!rows.length) {
|
|
83
|
+
notify({ message: i18next.t('ai-assistant.selection.pick-rows'), level: 'info' });
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (rows.some(row => !row.id || row.__dirty__)) {
|
|
87
|
+
notify({ message: i18next.t('ai-assistant.selection.save-first'), level: 'warn' });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const outcome = await askAssistant({
|
|
91
|
+
selection: { title: this.name, list: this.list, records: selectedRecords(rows, { axis: this.axis, labelOf: this.labelOf }) },
|
|
92
|
+
ask: request?.trim() || i18next.t('ai-assistant.selection.ask', { n: rows.length, title: this.name })
|
|
93
|
+
});
|
|
94
|
+
if (outcome?.sent && this.request === request)
|
|
95
|
+
this.request = '';
|
|
96
|
+
}
|
|
97
|
+
render() {
|
|
98
|
+
return this.count
|
|
99
|
+
? html `<div class="selection-work" role="region" aria-label=${i18next.t('ai-assistant.selection.region')}>
|
|
100
|
+
<span class="scope" role="status">${i18next.t('ai-assistant.selection.count', { n: this.count })}</span>
|
|
101
|
+
${this.actions.length
|
|
102
|
+
? html `<div class="tools">${this.actions.map(action => html `<button quiet small @click=${action.action}>${action.title}</button>`)}</div>`
|
|
103
|
+
: nothing}
|
|
104
|
+
<form @submit=${(event) => { event.preventDefault(); this.ask(); }}>
|
|
105
|
+
<input aria-label=${i18next.t('ai-assistant.selection.request')} placeholder=${i18next.t('ai-assistant.selection.request')}
|
|
106
|
+
.value=${this.request} @input=${(event) => (this.request = event.target.value)} />
|
|
107
|
+
<button quiet small type="submit">${i18next.t('ai-assistant.selection.work-with-ai')}</button>
|
|
108
|
+
</form>
|
|
109
|
+
</div>`
|
|
110
|
+
: nothing;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
__decorate([
|
|
114
|
+
property({ attribute: false }),
|
|
115
|
+
__metadata("design:type", Function)
|
|
116
|
+
], OxAssistantSelectionBar.prototype, "gridOf", void 0);
|
|
117
|
+
__decorate([
|
|
118
|
+
property(),
|
|
119
|
+
__metadata("design:type", Object)
|
|
120
|
+
], OxAssistantSelectionBar.prototype, "name", void 0);
|
|
121
|
+
__decorate([
|
|
122
|
+
property(),
|
|
123
|
+
__metadata("design:type", Object)
|
|
124
|
+
], OxAssistantSelectionBar.prototype, "list", void 0);
|
|
125
|
+
__decorate([
|
|
126
|
+
property(),
|
|
127
|
+
__metadata("design:type", Object)
|
|
128
|
+
], OxAssistantSelectionBar.prototype, "axis", void 0);
|
|
129
|
+
__decorate([
|
|
130
|
+
property({ attribute: false }),
|
|
131
|
+
__metadata("design:type", Array)
|
|
132
|
+
], OxAssistantSelectionBar.prototype, "actions", void 0);
|
|
133
|
+
__decorate([
|
|
134
|
+
property({ attribute: false }),
|
|
135
|
+
__metadata("design:type", Function)
|
|
136
|
+
], OxAssistantSelectionBar.prototype, "labelOf", void 0);
|
|
137
|
+
__decorate([
|
|
138
|
+
state(),
|
|
139
|
+
__metadata("design:type", Object)
|
|
140
|
+
], OxAssistantSelectionBar.prototype, "count", void 0);
|
|
141
|
+
__decorate([
|
|
142
|
+
state(),
|
|
143
|
+
__metadata("design:type", Object)
|
|
144
|
+
], OxAssistantSelectionBar.prototype, "request", void 0);
|
|
145
|
+
OxAssistantSelectionBar = __decorate([
|
|
146
|
+
customElement('ox-assistant-selection-bar')
|
|
147
|
+
], OxAssistantSelectionBar);
|
|
148
|
+
export { OxAssistantSelectionBar };
|
|
149
|
+
//# sourceMappingURL=assistant-selection-bar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-selection-bar.js","sourceRoot":"","sources":["../../client/components/assistant-selection-bar.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, LitElement, nothing } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { i18next } from '@operato/i18n'\nimport { notify } from '@operato/layout'\nimport { ButtonStyles, FormFieldStyles } from '@operato/styles'\n\nimport { askAssistant } from '../utils/assistant-ask.js'\nimport { selectedRecords } from '../utils/assistant-selection.js'\n\n/** What a grid says when its picks or its rows change - a row's picker, the header's select-all, a new page of rows. */\nconst GRID_EVENTS = ['select-record-change', 'select-all-change', 'list-adopted']\n\n/** The part of a grid (data-grist `ox-grist`) the bar reads. */\nexport interface SelectionGrid extends HTMLElement {\n selected?: any[]\n updateComplete?: Promise<unknown>\n}\n\n/**\n * 「AI와 작업」 over a list - one bar for every application's lists (design/ai/shared-dock.md §2-11-1). While rows are\n * picked it shows how many, what the list itself does with them, and a request sent to this screen's conversation with\n * the rows as the selection. Begun on the Office ledgers; the user, 2026-10-25: 「'AI와 작업' 기능을 사용하려면, 그리드\n * 선택기능이 필요한데, 주요 리스트 화면에서 선택기능이 없는 경우가 꽤 있다」, and it is to serve plant, warehouse and twin.\n *\n * A screen gives the grid (asked for when needed - a page finds its grid only after its first render), the list as the\n * person knows it, the list query and its axis (the last part of the list's read key, so a row goes as `<axis>:<uuid>`),\n * and, if it likes, its own actions and words for a row. The grid shows `SELECTION_ROW_PICKER`.\n */\n@customElement('ox-assistant-selection-bar')\nexport class OxAssistantSelectionBar extends LitElement {\n static styles = [\n ButtonStyles,\n FormFieldStyles,\n css`\n :host { display:block; }\n .selection-work { display:flex; flex-wrap:wrap; align-items:center; gap:var(--spacing-medium); padding:var(--spacing-medium) var(--spacing-large); border-bottom:1px solid var(--ui-line); background:var(--ui-panel); }\n .scope { font-size:var(--ui-t-body); font-weight:600; white-space:nowrap; }\n form { display:flex; flex:1 1 320px; min-width:0; gap:var(--spacing-small); }\n input { flex:1; min-width:0; width:100%; }\n .tools { display:flex; flex-wrap:wrap; gap:var(--spacing-small); }\n `\n ]\n\n /** The grid whose picked rows these are, asked for when needed. */\n @property({ attribute: false }) gridOf?: () => SelectionGrid | undefined\n /** The list as the person knows it (「계약 대장」). */\n @property() name = ''\n /** The list query the rows came from (`contracts`) - one the assistant reads. */\n @property() list = ''\n /** The last part of that list's read key (`office.contract` → `contract`): each row goes as `<axis>:<uuid>`. */\n @property() axis = ''\n /** What the list itself does with the picked rows, before 「AI와 작업」. */\n @property({ attribute: false }) actions: { title: string; action: () => any }[] = []\n /** A row in the person's words, when the code and name do not say it (`selectionLabel`). */\n @property({ attribute: false }) labelOf?: (row: any) => string\n\n @state() count = 0\n @state() request = ''\n\n private get grid() {\n return this.gridOf?.()\n }\n\n /* The grid's own events reach the page's root (bubbling, composed); the bar counts those that come from its grid. */\n private root?: Node\n private recount = async (event?: Event) => {\n const grid = this.grid\n if (event && (!grid || !event.composedPath().includes(grid))) return\n await grid?.updateComplete\n this.count = grid?.selected?.length ?? 0\n }\n\n connectedCallback() {\n super.connectedCallback()\n this.root = this.getRootNode()\n for (const event of GRID_EVENTS) this.root.addEventListener(event, this.recount)\n void this.recount()\n }\n\n disconnectedCallback() {\n super.disconnectedCallback()\n for (const event of GRID_EVENTS) this.root?.removeEventListener(event, this.recount)\n this.root = undefined\n }\n\n willUpdate(changes: Map<string, unknown>) {\n if (changes.has('gridOf')) void this.recount()\n }\n\n /*\n * A screen request (design/ai/shared-dock.md §2-11-1, the user: 「\"AI와 작업\" 기능이 unstable 하다 … 새로운 세션을 여는지,\n * 기존 세션을 활용하는지 등등을 명확하게 하라」): it goes to this screen's conversation with the rows picked now, in\n * screen order, and is sent at once - what was written on the bar, or the set request. The bar is cleared only when it\n * was sent; otherwise the dock has said why and the words stay to press again.\n */\n async ask(request: string = this.request) {\n const rows = this.grid?.selected ?? []\n if (!rows.length) {\n notify({ message: i18next.t('ai-assistant.selection.pick-rows'), level: 'info' })\n return\n }\n if (rows.some(row => !row.id || row.__dirty__)) {\n notify({ message: i18next.t('ai-assistant.selection.save-first'), level: 'warn' })\n return\n }\n const outcome = await askAssistant({\n selection: { title: this.name, list: this.list, records: selectedRecords(rows, { axis: this.axis, labelOf: this.labelOf }) },\n ask: request?.trim() || i18next.t('ai-assistant.selection.ask', { n: rows.length, title: this.name })\n })\n if (outcome?.sent && this.request === request) this.request = ''\n }\n\n render() {\n return this.count\n ? html`<div class=\"selection-work\" role=\"region\" aria-label=${i18next.t('ai-assistant.selection.region')}>\n <span class=\"scope\" role=\"status\">${i18next.t('ai-assistant.selection.count', { n: this.count })}</span>\n ${this.actions.length\n ? html`<div class=\"tools\">${this.actions.map(action => html`<button quiet small @click=${action.action}>${action.title}</button>`)}</div>`\n : nothing}\n <form @submit=${(event: Event) => { event.preventDefault(); this.ask() }}>\n <input aria-label=${i18next.t('ai-assistant.selection.request')} placeholder=${i18next.t('ai-assistant.selection.request')}\n .value=${this.request} @input=${(event: any) => (this.request = event.target.value)} />\n <button quiet small type=\"submit\">${i18next.t('ai-assistant.selection.work-with-ai')}</button>\n </form>\n </div>`\n : nothing\n }\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* 「AI와 작업」 over a list (ox-assistant-selection-bar) - one bar for every application's lists. What it is held to here is
|
|
3
|
+
* its source and its words; its behaviour on a real grid is held by the applications' browser tests (operato-office
|
|
4
|
+
* test/browser/page-ai-input.test.js).
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
const bare = readFileSync(join(__dirname, 'assistant-selection-bar.ts'), 'utf-8').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
9
|
+
describe('the selection bar', () => {
|
|
10
|
+
it('★ counts every way a grid picks - a row, the header\'s select-all, a new page of rows', () => {
|
|
11
|
+
expect(bare).toMatch(/GRID_EVENTS = \['select-record-change', 'select-all-change', 'list-adopted'\]/);
|
|
12
|
+
});
|
|
13
|
+
it('★ asks for its grid when it needs it - a page finds its grid only after its first render', () => {
|
|
14
|
+
expect(bare).toMatch(/gridOf\?: \(\) => SelectionGrid \| undefined/);
|
|
15
|
+
expect(bare).not.toMatch(/@property\(\{ attribute: false \}\) grist/);
|
|
16
|
+
});
|
|
17
|
+
it('★ hands the rows as keys of the list, to this screen\'s conversation, sent at once', () => {
|
|
18
|
+
expect(bare).toMatch(/selection: \{ title: this\.name, list: this\.list, records: selectedRecords\(rows, \{ axis: this\.axis, labelOf: this\.labelOf \}\) \}/);
|
|
19
|
+
expect(bare).toMatch(/ask: request\?\.trim\(\) \|\| i18next\.t\('ai-assistant\.selection\.ask'/);
|
|
20
|
+
expect(bare).not.toMatch(/draft|continueCurrent|transcript/);
|
|
21
|
+
});
|
|
22
|
+
it('keeps the words unless the request was sent', () => {
|
|
23
|
+
expect(bare).toMatch(/if \(outcome\?\.sent && this\.request === request\) this\.request = ''/);
|
|
24
|
+
});
|
|
25
|
+
it('every word it shows reads in every language', () => {
|
|
26
|
+
const keys = [...new Set([...bare.matchAll(/'(ai-assistant\.selection\.[\w-]+)'/g)].map(([, key]) => key))];
|
|
27
|
+
expect(keys.length).toBe(7);
|
|
28
|
+
for (const locale of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {
|
|
29
|
+
const words = JSON.parse(readFileSync(join(__dirname, `../../translations/${locale}.json`), 'utf-8'));
|
|
30
|
+
for (const key of keys)
|
|
31
|
+
expect([locale, key, typeof words[key]]).toEqual([locale, key, 'string']);
|
|
32
|
+
expect(words['ai-assistant.selection.ask']).toMatch(/\{n\}/);
|
|
33
|
+
expect(words['ai-assistant.selection.ask']).toMatch(/\{title\}/);
|
|
34
|
+
expect(words['ai-assistant.selection.count']).toMatch(/\{n\}/);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=assistant-selection-bar.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"assistant-selection-bar.test.js","sourceRoot":"","sources":["../../client/components/assistant-selection-bar.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAEhC,MAAM,IAAI,GAAG,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,4BAA4B,CAAC,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAA;AAElH,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,EAAE,CAAC,uFAAuF,EAAE,GAAG,EAAE;QAC/F,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,+EAA+E,CAAC,CAAA;IACvG,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0FAA0F,EAAE,GAAG,EAAE;QAClG,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,8CAA8C,CAAC,CAAA;QACpE,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,2CAA2C,CAAC,CAAA;IACvE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oFAAoF,EAAE,GAAG,EAAE;QAC5F,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,wIAAwI,CAAC,CAAA;QAC9J,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,0EAA0E,CAAC,CAAA;QAChG,MAAM,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,kCAAkC,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,wEAAwE,CAAC,CAAA;IAChG,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6CAA6C,EAAE,GAAG,EAAE;QACrD,MAAM,IAAI,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,sCAAsC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAA;QAC3G,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAC3B,KAAK,MAAM,MAAM,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;YAC1D,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,EAAE,sBAAsB,MAAM,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAA;YACrG,KAAK,MAAM,GAAG,IAAI,IAAI;gBAAE,MAAM,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAA;YACjG,MAAM,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;YAC5D,MAAM,CAAC,KAAK,CAAC,4BAA4B,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAA;YAChE,MAAM,CAAC,KAAK,CAAC,8BAA8B,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;QAChE,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA","sourcesContent":["/*\n * 「AI와 작업」 over a list (ox-assistant-selection-bar) - one bar for every application's lists. What it is held to here is\n * its source and its words; its behaviour on a real grid is held by the applications' browser tests (operato-office\n * test/browser/page-ai-input.test.js).\n */\nimport { readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nconst bare = readFileSync(join(__dirname, 'assistant-selection-bar.ts'), 'utf-8').replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n\ndescribe('the selection bar', () => {\n it('★ counts every way a grid picks - a row, the header\\'s select-all, a new page of rows', () => {\n expect(bare).toMatch(/GRID_EVENTS = \\['select-record-change', 'select-all-change', 'list-adopted'\\]/)\n })\n\n it('★ asks for its grid when it needs it - a page finds its grid only after its first render', () => {\n expect(bare).toMatch(/gridOf\\?: \\(\\) => SelectionGrid \\| undefined/)\n expect(bare).not.toMatch(/@property\\(\\{ attribute: false \\}\\) grist/)\n })\n\n it('★ hands the rows as keys of the list, to this screen\\'s conversation, sent at once', () => {\n expect(bare).toMatch(/selection: \\{ title: this\\.name, list: this\\.list, records: selectedRecords\\(rows, \\{ axis: this\\.axis, labelOf: this\\.labelOf \\}\\) \\}/)\n expect(bare).toMatch(/ask: request\\?\\.trim\\(\\) \\|\\| i18next\\.t\\('ai-assistant\\.selection\\.ask'/)\n expect(bare).not.toMatch(/draft|continueCurrent|transcript/)\n })\n\n it('keeps the words unless the request was sent', () => {\n expect(bare).toMatch(/if \\(outcome\\?\\.sent && this\\.request === request\\) this\\.request = ''/)\n })\n\n it('every word it shows reads in every language', () => {\n const keys = [...new Set([...bare.matchAll(/'(ai-assistant\\.selection\\.[\\w-]+)'/g)].map(([, key]) => key))]\n expect(keys.length).toBe(7)\n for (const locale of ['en', 'ko', 'ja', 'zh', 'ms', 'es']) {\n const words = JSON.parse(readFileSync(join(__dirname, `../../translations/${locale}.json`), 'utf-8'))\n for (const key of keys) expect([locale, key, typeof words[key]]).toEqual([locale, key, 'string'])\n expect(words['ai-assistant.selection.ask']).toMatch(/\\{n\\}/)\n expect(words['ai-assistant.selection.ask']).toMatch(/\\{title\\}/)\n expect(words['ai-assistant.selection.count']).toMatch(/\\{n\\}/)\n }\n })\n})\n"]}
|
package/dist-client/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from './utils/assistant-proposals.js';
|
|
|
23
23
|
export * from './components/workspace-view.js';
|
|
24
24
|
export * from './utils/assistant-ask.js';
|
|
25
25
|
export * from './utils/assistant-selection.js';
|
|
26
|
+
export * from './components/assistant-selection-bar.js';
|
|
26
27
|
export * from './utils/assistant-activity.js';
|
|
27
28
|
export * from './utils/assistant-request-context.js';
|
|
28
29
|
export * from './utils/assistant-capability.js';
|
package/dist-client/index.js
CHANGED
|
@@ -24,6 +24,8 @@ export * from './utils/assistant-proposals.js';
|
|
|
24
24
|
export * from './components/workspace-view.js';
|
|
25
25
|
export * from './utils/assistant-ask.js';
|
|
26
26
|
export * from './utils/assistant-selection.js';
|
|
27
|
+
/* 「AI와 작업」 over a list - every application's lists hand their picked rows by it (shared-dock §2-11-1). */
|
|
28
|
+
export * from './components/assistant-selection-bar.js';
|
|
27
29
|
export * from './utils/assistant-activity.js';
|
|
28
30
|
export * from './utils/assistant-request-context.js';
|
|
29
31
|
export * from './utils/assistant-capability.js';
|
package/dist-client/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,oCAAoC,CAAA;AAClD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,uCAAuC,CAAA;AACrD,cAAc,yCAAyC,CAAA;AACvD,cAAc,wCAAwC,CAAA;AACtD,cAAc,2CAA2C,CAAA;AACzD,cAAc,oCAAoC,CAAA;AAClD,cAAc,2CAA2C,CAAA;AACzD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,gCAAgC,CAAA;AAC9C,iFAAiF;AACjF,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,sCAAsC,CAAA;AACpD,cAAc,iCAAiC,CAAA;AAC/C,cAAc,sCAAsC,CAAA","sourcesContent":["/**\n * `@things-factory/ai-assistant` — 대화면.\n *\n * 표준 컴포넌트는 `<ox-assistant-chat>` 이다.\n */\nexport * from './components/assistant-chat.js'\nexport * from './components/chat-defaults.js'\nexport * from './components/assistant-mode.js'\nexport * from './components/markdown.js'\nexport * from './components/chat-echo-dedup.js'\nexport * from './components/chat-input-builder.js'\nexport * from './components/mention-popup.js'\nexport * from './components/mention-popup-helpers.js'\nexport * from './utils/assistant-session-controller.js'\nexport * from './utils/assistant-session-transport.js'\nexport * from './components/assistant-session-toolbar.js'\nexport * from './utils/assistant-session-admin.js'\nexport * from './components/assistant-session-manager.js'\nexport * from './components/assistant-dock.js'\nexport * from './setup-assistant-dock.js'\nexport * from './utils/assistant-surface.js'\nexport * from './utils/assistant-proposals.js'\n/* The AI 작업판's blocks, drawn - also a dashboard card's (ADR-0160 decision 5). */\nexport * from './components/workspace-view.js'\nexport * from './utils/assistant-ask.js'\nexport * from './utils/assistant-selection.js'\nexport * from './utils/assistant-activity.js'\nexport * from './utils/assistant-request-context.js'\nexport * from './utils/assistant-capability.js'\nexport * from './utils/assistant-capability-read.js'\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../client/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,cAAc,gCAAgC,CAAA;AAC9C,cAAc,+BAA+B,CAAA;AAC7C,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,iCAAiC,CAAA;AAC/C,cAAc,oCAAoC,CAAA;AAClD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,uCAAuC,CAAA;AACrD,cAAc,yCAAyC,CAAA;AACvD,cAAc,wCAAwC,CAAA;AACtD,cAAc,2CAA2C,CAAA;AACzD,cAAc,oCAAoC,CAAA;AAClD,cAAc,2CAA2C,CAAA;AACzD,cAAc,gCAAgC,CAAA;AAC9C,cAAc,2BAA2B,CAAA;AACzC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,gCAAgC,CAAA;AAC9C,iFAAiF;AACjF,cAAc,gCAAgC,CAAA;AAC9C,cAAc,0BAA0B,CAAA;AACxC,cAAc,gCAAgC,CAAA;AAC9C,0GAA0G;AAC1G,cAAc,yCAAyC,CAAA;AACvD,cAAc,+BAA+B,CAAA;AAC7C,cAAc,sCAAsC,CAAA;AACpD,cAAc,iCAAiC,CAAA;AAC/C,cAAc,sCAAsC,CAAA","sourcesContent":["/**\n * `@things-factory/ai-assistant` — 대화면.\n *\n * 표준 컴포넌트는 `<ox-assistant-chat>` 이다.\n */\nexport * from './components/assistant-chat.js'\nexport * from './components/chat-defaults.js'\nexport * from './components/assistant-mode.js'\nexport * from './components/markdown.js'\nexport * from './components/chat-echo-dedup.js'\nexport * from './components/chat-input-builder.js'\nexport * from './components/mention-popup.js'\nexport * from './components/mention-popup-helpers.js'\nexport * from './utils/assistant-session-controller.js'\nexport * from './utils/assistant-session-transport.js'\nexport * from './components/assistant-session-toolbar.js'\nexport * from './utils/assistant-session-admin.js'\nexport * from './components/assistant-session-manager.js'\nexport * from './components/assistant-dock.js'\nexport * from './setup-assistant-dock.js'\nexport * from './utils/assistant-surface.js'\nexport * from './utils/assistant-proposals.js'\n/* The AI 작업판's blocks, drawn - also a dashboard card's (ADR-0160 decision 5). */\nexport * from './components/workspace-view.js'\nexport * from './utils/assistant-ask.js'\nexport * from './utils/assistant-selection.js'\n/* 「AI와 작업」 over a list - every application's lists hand their picked rows by it (shared-dock §2-11-1). */\nexport * from './components/assistant-selection-bar.js'\nexport * from './utils/assistant-activity.js'\nexport * from './utils/assistant-request-context.js'\nexport * from './utils/assistant-capability.js'\nexport * from './utils/assistant-capability-read.js'\n"]}
|