@things-factory/ai-assistant 10.1.90 → 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.
- package/client/components/assistant-chat.ts +26 -0
- package/client/components/assistant-dock.ts +9 -1
- package/client/components/workspace-view.ts +428 -0
- package/client/pages/assistant-workspace-page.ts +237 -0
- package/client/route.ts +9 -0
- package/client/utils/workspace.ts +89 -0
- package/dist-client/components/assistant-chat.d.ts +2 -0
- package/dist-client/components/assistant-chat.js +27 -0
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.d.ts +1 -0
- package/dist-client/components/assistant-dock.js +9 -1
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/workspace-view.d.ts +19 -0
- package/dist-client/components/workspace-view.js +416 -0
- package/dist-client/components/workspace-view.js.map +1 -0
- package/dist-client/pages/assistant-workspace-page.d.ts +27 -0
- package/dist-client/pages/assistant-workspace-page.js +263 -0
- package/dist-client/pages/assistant-workspace-page.js.map +1 -0
- package/dist-client/route.d.ts +1 -0
- package/dist-client/route.js +10 -0
- package/dist-client/route.js.map +1 -0
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/workspace.d.ts +40 -0
- package/dist-client/utils/workspace.js +68 -0
- package/dist-client/utils/workspace.js.map +1 -0
- package/dist-server/service/assistant-mode/resolve.d.ts +2 -2
- package/dist-server/service/assistant-mode/resolve.js +5 -3
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -0
- package/dist-server/service/index.js +3 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/workspace/workspace-blocks.d.ts +24 -0
- package/dist-server/service/workspace/workspace-blocks.js +118 -0
- package/dist-server/service/workspace/workspace-blocks.js.map +1 -0
- package/dist-server/service/workspace/workspace-tools.d.ts +24 -0
- package/dist-server/service/workspace/workspace-tools.js +65 -0
- package/dist-server/service/workspace/workspace-tools.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/server/service/assistant-mode/resolve.test.ts +2 -1
- package/server/service/assistant-mode/resolve.ts +5 -3
- package/server/service/data-entry/data-entry.test.ts +2 -2
- package/server/service/index.ts +3 -0
- package/server/service/workspace/workspace-blocks.ts +127 -0
- package/server/service/workspace/workspace-tools.ts +66 -0
- package/server/service/workspace/workspace.test.ts +58 -0
- package/test/workspace.test.ts +54 -0
- package/things-factory.config.js +6 -1
- package/translations/en.json +21 -1
- package/translations/ja.json +21 -1
- package/translations/ko.json +21 -1
- package/translations/ms.json +21 -1
- package/translations/zh.json +21 -1
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The AI 작업판 page (operato-application/design/ai/workspace.md).
|
|
3
|
+
*
|
|
4
|
+
* `assistant-workspace?session=<id>&name=<name>[&version=<n>]`. It reads the conversation through its own
|
|
5
|
+
* door (`chatMessages`), so a private conversation shows nothing to someone outside it and a version behind a
|
|
6
|
+
* door the reader cannot pass is not among the versions (ADR-0073). A new version arriving while the page is
|
|
7
|
+
* open is announced, never swapped in under the reader.
|
|
8
|
+
*/
|
|
9
|
+
import '@material/web/icon/icon.js'
|
|
10
|
+
import '../components/workspace-view.js'
|
|
11
|
+
|
|
12
|
+
import gql from 'graphql-tag'
|
|
13
|
+
import { css, html, nothing } from 'lit'
|
|
14
|
+
import { customElement, state } from 'lit/decorators.js'
|
|
15
|
+
|
|
16
|
+
import { client } from '@operato/graphql'
|
|
17
|
+
import { i18next, localize } from '@operato/i18n'
|
|
18
|
+
import { navigate, PageView } from '@operato/shell'
|
|
19
|
+
import { ScrollbarStyles } from '@operato/styles'
|
|
20
|
+
|
|
21
|
+
import { startSessionActivityFeed, subscribeSessionActivity } from '../utils/assistant-activity.js'
|
|
22
|
+
import { workspaceAddress, workspaceVersions, type WorkspaceVersion } from '../utils/workspace.js'
|
|
23
|
+
|
|
24
|
+
const MESSAGES = gql`
|
|
25
|
+
query ($sessionId: String!, $limit: Int, $offset: Int) {
|
|
26
|
+
chatMessages(sessionId: $sessionId, limit: $limit, offset: $offset) {
|
|
27
|
+
id
|
|
28
|
+
createdAt
|
|
29
|
+
proposalsJson
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
`
|
|
33
|
+
|
|
34
|
+
const PAGE = 200
|
|
35
|
+
const MOST = 5000
|
|
36
|
+
|
|
37
|
+
const t = (key: string, defaultValue: string, options: Record<string, unknown> = {}) =>
|
|
38
|
+
String(i18next.t(`ai-assistant.workspace.${key}`, { defaultValue, ...options }))
|
|
39
|
+
|
|
40
|
+
@customElement('assistant-workspace-page')
|
|
41
|
+
export class AssistantWorkspacePage extends localize(i18next)(PageView) {
|
|
42
|
+
static styles = [
|
|
43
|
+
ScrollbarStyles,
|
|
44
|
+
css`
|
|
45
|
+
:host {
|
|
46
|
+
display: block;
|
|
47
|
+
overflow: auto;
|
|
48
|
+
padding: 20px 24px 40px;
|
|
49
|
+
box-sizing: border-box;
|
|
50
|
+
background: var(--md-sys-color-surface);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.page {
|
|
54
|
+
max-width: 1100px;
|
|
55
|
+
margin: 0 auto;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
header {
|
|
59
|
+
display: flex;
|
|
60
|
+
align-items: flex-start;
|
|
61
|
+
gap: 12px;
|
|
62
|
+
margin-bottom: 16px;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
header .what {
|
|
66
|
+
flex: 1;
|
|
67
|
+
min-width: 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
header h1 {
|
|
71
|
+
margin: 0;
|
|
72
|
+
font-size: 24px;
|
|
73
|
+
line-height: 1.3;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
header .summary {
|
|
77
|
+
margin-top: 4px;
|
|
78
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
header select {
|
|
82
|
+
padding: 4px 8px;
|
|
83
|
+
border: 1px solid var(--md-sys-color-outline-variant);
|
|
84
|
+
border-radius: 6px;
|
|
85
|
+
background: var(--md-sys-color-surface-container-lowest);
|
|
86
|
+
color: inherit;
|
|
87
|
+
font: inherit;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.notice {
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
93
|
+
gap: 10px;
|
|
94
|
+
margin-bottom: 14px;
|
|
95
|
+
padding: 8px 12px;
|
|
96
|
+
border-radius: 8px;
|
|
97
|
+
background: var(--md-sys-color-secondary-container);
|
|
98
|
+
color: var(--md-sys-color-on-secondary-container);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.notice button {
|
|
102
|
+
padding: 3px 10px;
|
|
103
|
+
border: 1px solid currentColor;
|
|
104
|
+
border-radius: 6px;
|
|
105
|
+
background: transparent;
|
|
106
|
+
color: inherit;
|
|
107
|
+
font: inherit;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.empty {
|
|
112
|
+
padding: 40px 0;
|
|
113
|
+
color: var(--md-sys-color-on-surface-variant);
|
|
114
|
+
text-align: center;
|
|
115
|
+
}
|
|
116
|
+
`
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
@state() private sessionId = ''
|
|
120
|
+
@state() private name = ''
|
|
121
|
+
@state() private wanted = 0
|
|
122
|
+
@state() private versions: WorkspaceVersion[] = []
|
|
123
|
+
@state() private loading = false
|
|
124
|
+
@state() private failure = ''
|
|
125
|
+
/** A version that arrived after the page was drawn - announced, not shown until asked. */
|
|
126
|
+
@state() private newer = 0
|
|
127
|
+
|
|
128
|
+
private unsubscribe?: () => void
|
|
129
|
+
|
|
130
|
+
get context() {
|
|
131
|
+
const shown = this.shown
|
|
132
|
+
return { title: shown?.workspace.title || t('title', 'AI workspace') }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private get shown(): WorkspaceVersion | undefined {
|
|
136
|
+
if (!this.versions.length) return undefined
|
|
137
|
+
return this.versions.find(one => one.version === this.wanted) ?? this.versions[this.versions.length - 1]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
connectedCallback() {
|
|
141
|
+
super.connectedCallback()
|
|
142
|
+
startSessionActivityFeed()
|
|
143
|
+
this.unsubscribe = subscribeSessionActivity(activity => {
|
|
144
|
+
if (activity.sessionId === this.sessionId && activity.kind === 'message') void this.load(true)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
disconnectedCallback() {
|
|
149
|
+
this.unsubscribe?.()
|
|
150
|
+
super.disconnectedCallback()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async pageUpdated(_changes: any, lifecycle: any) {
|
|
154
|
+
const params = lifecycle?.params ?? {}
|
|
155
|
+
const sessionId = String(params.session || '')
|
|
156
|
+
const name = String(params.name || '')
|
|
157
|
+
const wanted = Number(params.version) || 0
|
|
158
|
+
const changed = sessionId !== this.sessionId || name !== this.name
|
|
159
|
+
this.sessionId = sessionId
|
|
160
|
+
this.name = name
|
|
161
|
+
this.wanted = wanted
|
|
162
|
+
if (changed) {
|
|
163
|
+
this.newer = 0
|
|
164
|
+
await this.load(false)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async load(arriving: boolean) {
|
|
169
|
+
if (!this.sessionId || !this.name) return
|
|
170
|
+
const before = this.versions.length
|
|
171
|
+
if (!arriving) this.loading = true
|
|
172
|
+
this.failure = ''
|
|
173
|
+
try {
|
|
174
|
+
const messages: any[] = []
|
|
175
|
+
for (let offset = 0; offset < MOST; offset += PAGE) {
|
|
176
|
+
const result = await client.query({ query: MESSAGES, variables: { sessionId: this.sessionId, limit: PAGE, offset }, fetchPolicy: 'no-cache' })
|
|
177
|
+
if (result.errors?.length) throw new Error(result.errors[0].message)
|
|
178
|
+
const page = result.data?.chatMessages ?? []
|
|
179
|
+
messages.push(...page)
|
|
180
|
+
if (page.length < PAGE) break
|
|
181
|
+
}
|
|
182
|
+
const versions = workspaceVersions(messages, this.name)
|
|
183
|
+
this.versions = versions
|
|
184
|
+
if (arriving && versions.length > before && before > 0) this.newer = versions[versions.length - 1].version
|
|
185
|
+
} catch (error: any) {
|
|
186
|
+
this.failure = error?.message || String(error)
|
|
187
|
+
} finally {
|
|
188
|
+
this.loading = false
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private go(version?: number) {
|
|
193
|
+
this.newer = 0
|
|
194
|
+
navigate(workspaceAddress(this.sessionId, this.name, version))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
render() {
|
|
198
|
+
const shown = this.shown
|
|
199
|
+
if (this.loading && !shown) return html`<div class="empty">${t('loading', 'loading the workspace…')}</div>`
|
|
200
|
+
if (this.failure) return html`<div class="empty">${t('unreadable-page', 'could not read this workspace: {reason}', { reason: this.failure })}</div>`
|
|
201
|
+
/*
|
|
202
|
+
* Nothing to show says only what is true: this reader sees no such workspace. Whether it never existed, is in
|
|
203
|
+
* a private conversation or sits behind a door is not said - saying which would itself tell.
|
|
204
|
+
*/
|
|
205
|
+
if (!shown) return html`<div class="empty">${t('not-found', 'there is no workspace here that you can see.')}</div>`
|
|
206
|
+
|
|
207
|
+
const latest = this.versions[this.versions.length - 1].version
|
|
208
|
+
return html`<div class="page">
|
|
209
|
+
${this.newer && this.newer !== shown.version
|
|
210
|
+
? html`<div class="notice" role="status">
|
|
211
|
+
<md-icon>update</md-icon>
|
|
212
|
+
<span>${t('newer', 'a newer version ({version}) is ready.', { version: this.newer })}</span>
|
|
213
|
+
<button @click=${() => this.go(this.newer)}>${t('show-newer', 'show it')}</button>
|
|
214
|
+
</div>`
|
|
215
|
+
: nothing}
|
|
216
|
+
<header>
|
|
217
|
+
<div class="what">
|
|
218
|
+
<h1>${shown.workspace.title}</h1>
|
|
219
|
+
${shown.workspace.summary ? html`<div class="summary">${shown.workspace.summary}</div>` : nothing}
|
|
220
|
+
</div>
|
|
221
|
+
${this.versions.length > 1
|
|
222
|
+
? html`<select aria-label=${t('versions', 'versions')} @change=${(e: Event) => this.go(Number((e.target as HTMLSelectElement).value))}>
|
|
223
|
+
${this.versions
|
|
224
|
+
.slice()
|
|
225
|
+
.reverse()
|
|
226
|
+
.map(
|
|
227
|
+
one => html`<option value=${one.version} ?selected=${one.version === shown.version}>
|
|
228
|
+
${t('version', 'version {version} of {of}', { version: one.version, of: latest })}${one.createdAt ? ` · ${new Date(one.createdAt).toLocaleString(i18next.language || undefined)}` : ''}
|
|
229
|
+
</option>`
|
|
230
|
+
)}
|
|
231
|
+
</select>`
|
|
232
|
+
: nothing}
|
|
233
|
+
</header>
|
|
234
|
+
<ox-assistant-workspace-view .workspace=${shown.workspace}></ox-assistant-workspace-view>
|
|
235
|
+
</div>`
|
|
236
|
+
}
|
|
237
|
+
}
|
package/client/route.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* The pages ai-assistant brings to every application that installs it. */
|
|
2
|
+
export default function route(page: string) {
|
|
3
|
+
switch (page) {
|
|
4
|
+
/* The AI 작업판 - results too large for the dock (design/ai/workspace.md). */
|
|
5
|
+
case 'assistant-workspace':
|
|
6
|
+
import('./pages/assistant-workspace-page.js')
|
|
7
|
+
return page
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -540,6 +540,8 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
540
540
|
* 실행 결과는 호스트가 **대화에 기록**한다 — 누가 무엇을 실행했는지가 협의 이력에 남아야 한다.
|
|
541
541
|
*/
|
|
542
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;
|
|
543
545
|
/**
|
|
544
546
|
* 눌린 카드가 말하는 것 — **호스트가 한 일**이고, 아직 모르면 모른다고 말한다.
|
|
545
547
|
*
|
|
@@ -49,6 +49,7 @@ import { parseProposalOutcomeLine, parseProposalRequestLine, proposalCardState }
|
|
|
49
49
|
import { availableTriggers } from './chat-triggers.js';
|
|
50
50
|
import './entry-review.js';
|
|
51
51
|
import { entryReadyCount, isEntryProposal, latestEntryKeys } from '../utils/entry-proposal.js';
|
|
52
|
+
import { isWorkspaceProposal, versionOfCard, workspaceAddress } from '../utils/workspace.js';
|
|
52
53
|
function formatFileSize(bytes) {
|
|
53
54
|
if (!bytes || bytes < 1024)
|
|
54
55
|
return `${bytes || 0} B`;
|
|
@@ -3508,6 +3509,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3508
3509
|
<div class="proposals">
|
|
3509
3510
|
${items.map(p => {
|
|
3510
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);
|
|
3511
3515
|
const card = proposalCardState(key, { outcomes, requested, sentHere: this.sentProposals.has(key) });
|
|
3512
3516
|
const entry = isEntryProposal(p);
|
|
3513
3517
|
const replaced = entry && latestEntry?.get(p.target) !== key && card.state === 'ready';
|
|
@@ -3588,6 +3592,29 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
|
|
|
3588
3592
|
</div>
|
|
3589
3593
|
`;
|
|
3590
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
|
+
}
|
|
3591
3618
|
/**
|
|
3592
3619
|
* 눌린 카드가 말하는 것 — **호스트가 한 일**이고, 아직 모르면 모른다고 말한다.
|
|
3593
3620
|
*
|