@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.
- package/client/components/assistant-chat.ts +45 -5
- package/client/components/assistant-dock.ts +30 -2
- package/client/components/entry-review.ts +246 -0
- 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/assistant-proposals.ts +11 -1
- package/client/utils/entry-proposal.ts +103 -0
- package/client/utils/workspace.ts +89 -0
- package/dist-client/components/assistant-chat.d.ts +3 -0
- package/dist-client/components/assistant-chat.js +46 -5
- 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 +25 -2
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/entry-review.d.ts +9 -0
- package/dist-client/components/entry-review.js +247 -0
- package/dist-client/components/entry-review.js.map +1 -0
- 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/assistant-proposals.d.ts +1 -0
- package/dist-client/utils/assistant-proposals.js +9 -1
- package/dist-client/utils/assistant-proposals.js.map +1 -1
- package/dist-client/utils/entry-proposal.d.ts +55 -0
- package/dist-client/utils/entry-proposal.js +52 -0
- package/dist-client/utils/entry-proposal.js.map +1 -0
- 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/registry.js +13 -0
- package/dist-server/service/assistant-mode/registry.js.map +1 -1
- package/dist-server/service/assistant-mode/resolve.d.ts +11 -0
- package/dist-server/service/assistant-mode/resolve.js +15 -3
- package/dist-server/service/assistant-mode/resolve.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +7 -1
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/data-entry/entry-apply.d.ts +46 -0
- package/dist-server/service/data-entry/entry-apply.js +102 -0
- package/dist-server/service/data-entry/entry-apply.js.map +1 -0
- package/dist-server/service/data-entry/entry-resolver.d.ts +4 -0
- package/dist-server/service/data-entry/entry-resolver.js +80 -0
- package/dist-server/service/data-entry/entry-resolver.js.map +1 -0
- package/dist-server/service/data-entry/entry-sources.d.ts +1 -0
- package/dist-server/service/data-entry/entry-sources.js +41 -0
- package/dist-server/service/data-entry/entry-sources.js.map +1 -0
- package/dist-server/service/data-entry/entry-target.d.ts +157 -0
- package/dist-server/service/data-entry/entry-target.js +343 -0
- package/dist-server/service/data-entry/entry-target.js.map +1 -0
- package/dist-server/service/data-entry/entry-tools.d.ts +43 -0
- package/dist-server/service/data-entry/entry-tools.js +185 -0
- package/dist-server/service/data-entry/entry-tools.js.map +1 -0
- package/dist-server/service/index.d.ts +6 -1
- package/dist-server/service/index.js +13 -2
- 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/registry.ts +13 -0
- package/server/service/assistant-mode/resolve.test.ts +2 -1
- package/server/service/assistant-mode/resolve.ts +15 -3
- package/server/service/assistant-mode/types.ts +7 -1
- package/server/service/data-entry/data-entry.test.ts +311 -0
- package/server/service/data-entry/entry-apply.ts +134 -0
- package/server/service/data-entry/entry-resolver.ts +74 -0
- package/server/service/data-entry/entry-sources.ts +35 -0
- package/server/service/data-entry/entry-target.ts +442 -0
- package/server/service/data-entry/entry-tools.ts +174 -0
- package/server/service/index.ts +10 -1
- 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/entry-proposal.test.ts +67 -0
- package/test/open-doors.test.ts +2 -1
- package/test/workspace.test.ts +54 -0
- package/things-factory.config.js +6 -1
- package/translations/en.json +56 -1
- package/translations/ja.json +56 -1
- package/translations/ko.json +56 -1
- package/translations/ms.json +56 -1
- 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
|
|
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
|
-
${
|
|
3542
|
-
?
|
|
3543
|
-
:
|
|
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
|
*
|