@things-factory/ai-dashboard 0.0.0-stage → 10.1.213

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 (108) hide show
  1. package/client/bootstrap.ts +25 -0
  2. package/client/components/card-dialogs.ts +422 -0
  3. package/client/components/card-library.ts +272 -0
  4. package/client/components/dashboard-card.ts +450 -0
  5. package/client/components/proposal-preview.ts +96 -0
  6. package/client/index.ts +3 -0
  7. package/client/pages/my-dashboard-page.ts +371 -0
  8. package/client/route.ts +8 -0
  9. package/client/tsconfig.json +13 -0
  10. package/client/utils/card-api.ts +110 -0
  11. package/client/utils/card-words.ts +80 -0
  12. package/dist-client/bootstrap.d.ts +2 -0
  13. package/dist-client/bootstrap.js +24 -0
  14. package/dist-client/bootstrap.js.map +1 -0
  15. package/dist-client/components/card-dialogs.d.ts +59 -0
  16. package/dist-client/components/card-dialogs.js +457 -0
  17. package/dist-client/components/card-dialogs.js.map +1 -0
  18. package/dist-client/components/card-library.d.ts +23 -0
  19. package/dist-client/components/card-library.js +276 -0
  20. package/dist-client/components/card-library.js.map +1 -0
  21. package/dist-client/components/dashboard-card.d.ts +35 -0
  22. package/dist-client/components/dashboard-card.js +490 -0
  23. package/dist-client/components/dashboard-card.js.map +1 -0
  24. package/dist-client/components/proposal-preview.d.ts +17 -0
  25. package/dist-client/components/proposal-preview.js +111 -0
  26. package/dist-client/components/proposal-preview.js.map +1 -0
  27. package/dist-client/index.d.ts +3 -0
  28. package/dist-client/index.js +4 -0
  29. package/dist-client/index.js.map +1 -0
  30. package/dist-client/pages/my-dashboard-page.d.ts +46 -0
  31. package/dist-client/pages/my-dashboard-page.js +376 -0
  32. package/dist-client/pages/my-dashboard-page.js.map +1 -0
  33. package/dist-client/route.d.ts +1 -0
  34. package/dist-client/route.js +9 -0
  35. package/dist-client/route.js.map +1 -0
  36. package/dist-client/tsconfig.tsbuildinfo +1 -0
  37. package/dist-client/utils/card-api.d.ts +95 -0
  38. package/dist-client/utils/card-api.js +39 -0
  39. package/dist-client/utils/card-api.js.map +1 -0
  40. package/dist-client/utils/card-words.d.ts +24 -0
  41. package/dist-client/utils/card-words.js +72 -0
  42. package/dist-client/utils/card-words.js.map +1 -0
  43. package/dist-server/index.d.ts +11 -0
  44. package/dist-server/index.js +19 -0
  45. package/dist-server/index.js.map +1 -0
  46. package/dist-server/role-tasks.d.ts +2 -0
  47. package/dist-server/role-tasks.js +13 -0
  48. package/dist-server/role-tasks.js.map +1 -0
  49. package/dist-server/service/card/card-access.d.ts +32 -0
  50. package/dist-server/service/card/card-access.js +45 -0
  51. package/dist-server/service/card/card-access.js.map +1 -0
  52. package/dist-server/service/card/card-definition.d.ts +100 -0
  53. package/dist-server/service/card/card-definition.js +237 -0
  54. package/dist-server/service/card/card-definition.js.map +1 -0
  55. package/dist-server/service/card/card-names.d.ts +5 -0
  56. package/dist-server/service/card/card-names.js +9 -0
  57. package/dist-server/service/card/card-names.js.map +1 -0
  58. package/dist-server/service/card/card-proposal.d.ts +21 -0
  59. package/dist-server/service/card/card-proposal.js +28 -0
  60. package/dist-server/service/card/card-proposal.js.map +1 -0
  61. package/dist-server/service/card/card-resolver.d.ts +29 -0
  62. package/dist-server/service/card/card-resolver.js +541 -0
  63. package/dist-server/service/card/card-resolver.js.map +1 -0
  64. package/dist-server/service/card/card-run.d.ts +48 -0
  65. package/dist-server/service/card/card-run.js +239 -0
  66. package/dist-server/service/card/card-run.js.map +1 -0
  67. package/dist-server/service/card/card-tool.d.ts +218 -0
  68. package/dist-server/service/card/card-tool.js +313 -0
  69. package/dist-server/service/card/card-tool.js.map +1 -0
  70. package/dist-server/service/card/card-types.d.ts +56 -0
  71. package/dist-server/service/card/card-types.js +212 -0
  72. package/dist-server/service/card/card-types.js.map +1 -0
  73. package/dist-server/service/card/dashboard-card.d.ts +87 -0
  74. package/dist-server/service/card/dashboard-card.js +388 -0
  75. package/dist-server/service/card/dashboard-card.js.map +1 -0
  76. package/dist-server/service/card/long-text.d.ts +6 -0
  77. package/dist-server/service/card/long-text.js +28 -0
  78. package/dist-server/service/card/long-text.js.map +1 -0
  79. package/dist-server/service/index.d.ts +12 -0
  80. package/dist-server/service/index.js +24 -0
  81. package/dist-server/service/index.js.map +1 -0
  82. package/dist-server/tsconfig.tsbuildinfo +1 -0
  83. package/package.json +47 -4
  84. package/server/index.ts +16 -0
  85. package/server/role-tasks.ts +12 -0
  86. package/server/service/card/card-access.test.ts +138 -0
  87. package/server/service/card/card-access.ts +53 -0
  88. package/server/service/card/card-definition.ts +298 -0
  89. package/server/service/card/card-names.ts +5 -0
  90. package/server/service/card/card-proposal.ts +37 -0
  91. package/server/service/card/card-resolver.ts +434 -0
  92. package/server/service/card/card-run.test.ts +237 -0
  93. package/server/service/card/card-run.ts +288 -0
  94. package/server/service/card/card-tool.ts +299 -0
  95. package/server/service/card/card-types.ts +145 -0
  96. package/server/service/card/dashboard-card.ts +301 -0
  97. package/server/service/card/long-text.ts +25 -0
  98. package/server/service/index.ts +16 -0
  99. package/test/translations.test.ts +42 -0
  100. package/things-factory.config.js +8 -0
  101. package/translations/en.json +126 -0
  102. package/translations/es.json +126 -0
  103. package/translations/ja.json +126 -0
  104. package/translations/ko.json +126 -0
  105. package/translations/ms.json +126 -0
  106. package/translations/zh.json +126 -0
  107. package/tsconfig.json +9 -0
  108. package/README.md +0 -3
@@ -0,0 +1,25 @@
1
+ /*
2
+ * The conversation's dashboard card (ADR-0160 decision 9): its preview under the card, and pressing it saves the stored
3
+ * proposal to the person's dashboard. Registered here, by the package that declares the tool - the dock knows no kind.
4
+ */
5
+ import { i18next } from '@operato/i18n'
6
+ import { registerProposalHandler, registerProposalView } from '@things-factory/ai-assistant/dist-client/index.js'
7
+
8
+ import './components/proposal-preview.js'
9
+ import { cardApi, dashboardChanged } from './utils/card-api.js'
10
+
11
+ const TOOL = 'proposeDashboardCard'
12
+
13
+ export default function bootstrap() {
14
+ registerProposalView(TOOL, { previewTag: 'ai-dashboard-proposal-preview', runLabelKey: 'ai-dashboard.preview.save', runLabel: 'Save to my dashboard' })
15
+ registerProposalHandler(TOOL, async (proposal, context) => {
16
+ if (!context.sessionId || !proposal?.key) return { verdict: 'unavailable', errorCode: 'no-conversation' }
17
+ const saved = await cardApi.saveProposal(context.sessionId, proposal.key)
18
+ dashboardChanged()
19
+ return {
20
+ verdict: 'accepted',
21
+ applied: 1,
22
+ note: String(i18next.t(saved.version > 1 ? 'ai-dashboard.preview.saved-version' : 'ai-dashboard.preview.saved', { name: proposal.card?.name ?? '', version: saved.version }))
23
+ }
24
+ })
25
+ }
@@ -0,0 +1,422 @@
1
+ /*
2
+ * The small forms a card opens - what to change with the assistant, a new name, whom to share it with, and its
3
+ * versions (ADR-0160 decisions 7, 9). Each changes one card through its own door and tells an open dashboard.
4
+ */
5
+ import '@material/web/icon/icon.js'
6
+ import '@material/web/button/filled-button.js'
7
+ import '@material/web/button/text-button.js'
8
+
9
+ import { LitElement, css, html, nothing } from 'lit'
10
+ import { customElement, property, state } from 'lit/decorators.js'
11
+
12
+ import { i18next, localize } from '@operato/i18n'
13
+ import { notify, openPopup } from '@operato/layout'
14
+ import { askAssistant } from '@things-factory/ai-assistant/dist-client/index.js'
15
+
16
+ import { cardApi, dashboardChanged, type CardItem, type CardShare } from '../utils/card-api.js'
17
+
18
+ const t = (key: string, options: Record<string, unknown> = {}) => String(i18next.t(key, options))
19
+
20
+ const FormStyles = css`
21
+ :host {
22
+ display: flex;
23
+ flex-direction: column;
24
+ gap: 14px;
25
+ padding: 16px 20px 20px;
26
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
27
+ font-size: var(--ui-t-body, 13px);
28
+ line-height: 1.5;
29
+ }
30
+
31
+ p.lead {
32
+ margin: 0;
33
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
34
+ }
35
+
36
+ textarea,
37
+ input[type='text'],
38
+ input[type='search'] {
39
+ box-sizing: border-box;
40
+ width: 100%;
41
+ padding: 9px 12px;
42
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
43
+ border-radius: 8px;
44
+ background: var(--ui-panel, var(--md-sys-color-surface-container-lowest));
45
+ color: inherit;
46
+ font: inherit;
47
+ }
48
+
49
+ textarea {
50
+ min-height: 88px;
51
+ resize: vertical;
52
+ }
53
+
54
+ textarea:focus,
55
+ input:focus {
56
+ outline: 2px solid color-mix(in srgb, var(--ui-accent, var(--md-sys-color-primary)) 40%, transparent);
57
+ border-color: var(--ui-accent, var(--md-sys-color-primary));
58
+ }
59
+
60
+ .hints {
61
+ display: flex;
62
+ flex-wrap: wrap;
63
+ gap: 6px;
64
+ }
65
+
66
+ .hints button {
67
+ padding: 4px 10px;
68
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
69
+ border-radius: 999px;
70
+ background: transparent;
71
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
72
+ font: inherit;
73
+ font-size: var(--ui-t-small, 12px);
74
+ cursor: pointer;
75
+ }
76
+
77
+ .hints button:hover {
78
+ border-color: var(--ui-accent, var(--md-sys-color-primary));
79
+ color: var(--ui-accent, var(--md-sys-color-primary));
80
+ }
81
+
82
+ .actions {
83
+ display: flex;
84
+ justify-content: flex-end;
85
+ gap: 8px;
86
+ }
87
+
88
+ h5 {
89
+ margin: 6px 0 0;
90
+ font-size: var(--ui-t-small, 12px);
91
+ font-weight: 600;
92
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
93
+ text-transform: none;
94
+ }
95
+
96
+ label.option {
97
+ display: flex;
98
+ align-items: center;
99
+ gap: 10px;
100
+ padding: 8px 10px;
101
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
102
+ border-radius: 8px;
103
+ cursor: pointer;
104
+ }
105
+
106
+ label.option small {
107
+ margin-left: auto;
108
+ color: var(--ui-faint, var(--md-sys-color-on-surface-variant));
109
+ }
110
+
111
+ .people {
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: 6px;
115
+ }
116
+
117
+ .person {
118
+ display: flex;
119
+ align-items: center;
120
+ gap: 8px;
121
+ padding: 6px 10px;
122
+ border-radius: 8px;
123
+ background: var(--ui-panel-2, var(--md-sys-color-surface-container-low));
124
+ --md-icon-size: 16px;
125
+ }
126
+
127
+ .person small {
128
+ color: var(--ui-faint, var(--md-sys-color-on-surface-variant));
129
+ }
130
+
131
+ .person button,
132
+ .found button {
133
+ margin-left: auto;
134
+ border: 0;
135
+ background: transparent;
136
+ color: var(--ui-accent, var(--md-sys-color-primary));
137
+ font: inherit;
138
+ cursor: pointer;
139
+ }
140
+
141
+ .found {
142
+ display: flex;
143
+ flex-direction: column;
144
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
145
+ border-radius: 8px;
146
+ }
147
+
148
+ .found div {
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 8px;
152
+ padding: 6px 10px;
153
+ }
154
+
155
+ .found div + div {
156
+ border-top: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
157
+ }
158
+
159
+ .note {
160
+ margin: 0;
161
+ font-size: var(--ui-t-small, 12px);
162
+ color: var(--ui-faint, var(--md-sys-color-on-surface-variant));
163
+ }
164
+
165
+ ol.versions {
166
+ list-style: none;
167
+ margin: 0;
168
+ padding: 0;
169
+ display: flex;
170
+ flex-direction: column;
171
+ gap: 6px;
172
+ }
173
+
174
+ ol.versions li {
175
+ display: flex;
176
+ align-items: center;
177
+ gap: 12px;
178
+ padding: 10px 12px;
179
+ border: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
180
+ border-radius: 8px;
181
+ }
182
+
183
+ ol.versions .v {
184
+ flex: none;
185
+ min-width: 36px;
186
+ font-weight: 650;
187
+ font-variant-numeric: tabular-nums;
188
+ color: var(--ui-accent, var(--md-sys-color-primary));
189
+ }
190
+
191
+ ol.versions .what {
192
+ flex: 1;
193
+ min-width: 0;
194
+ }
195
+
196
+ ol.versions small {
197
+ display: block;
198
+ color: var(--ui-faint, var(--md-sys-color-on-surface-variant));
199
+ }
200
+ `
201
+
202
+ /** What to change in a card - sent to the conversation beside the page with the card picked, as a screen request. */
203
+ @customElement('ai-dashboard-card-ai-form')
204
+ export class CardAiForm extends localize(i18next)(LitElement) {
205
+ static styles = FormStyles
206
+ @property({ attribute: false }) card!: CardItem
207
+ @state() private text = ''
208
+
209
+ render() {
210
+ const hints = ['ai-dashboard.ai.hint-period', 'ai-dashboard.ai.hint-top', 'ai-dashboard.ai.hint-line', 'ai-dashboard.ai.hint-table']
211
+ return html`<p class="lead">${t('ai-dashboard.ai.lead', { name: this.card.name })}</p>
212
+ <textarea .value=${this.text} placeholder=${t('ai-dashboard.ai.placeholder')} @input=${(e: any) => (this.text = e.target.value)} @keydown=${(e: KeyboardEvent) => e.key === 'Enter' && (e.metaKey || e.ctrlKey) && this.send()}></textarea>
213
+ <div class="hints">${hints.map(key => html`<button @click=${() => (this.text = t(key))}>${t(key)}</button>`)}</div>
214
+ <div class="actions">
215
+ <md-text-button @click=${() => this.close()}>${t('ai-dashboard.button.cancel')}</md-text-button>
216
+ <md-filled-button ?disabled=${!this.text.trim()} @click=${() => this.send()}>${t('ai-dashboard.ai.send')}</md-filled-button>
217
+ </div>`
218
+ }
219
+
220
+ private close() {
221
+ this.dispatchEvent(new CustomEvent('done', { bubbles: true, composed: true }))
222
+ }
223
+
224
+ private send() {
225
+ if (!this.text.trim()) return
226
+ /* Closed first: the dock opens over the page, and a popup that is no longer the top one refuses to close. */
227
+ this.close()
228
+ askAssistant({
229
+ ask: t('ai-dashboard.ai.ask', { name: this.card.name, version: this.card.version, change: this.text.trim() }),
230
+ selection: { title: t('ai-dashboard.title'), list: 'dashboardCards', records: [{ id: this.card.id, label: this.card.name }] }
231
+ })
232
+ }
233
+ }
234
+
235
+ /** A card's new name - a new version. */
236
+ @customElement('ai-dashboard-card-rename-form')
237
+ export class CardRenameForm extends localize(i18next)(LitElement) {
238
+ static styles = FormStyles
239
+ @property({ attribute: false }) card!: CardItem
240
+ @state() private name = ''
241
+
242
+ connectedCallback() {
243
+ super.connectedCallback()
244
+ this.name = this.card?.name ?? ''
245
+ }
246
+
247
+ render() {
248
+ return html`<input type="text" .value=${this.name} maxlength="100" @input=${(e: any) => (this.name = e.target.value)} @keydown=${(e: KeyboardEvent) => e.key === 'Enter' && this.save()} />
249
+ <div class="actions">
250
+ <md-text-button @click=${() => this.close()}>${t('ai-dashboard.button.cancel')}</md-text-button>
251
+ <md-filled-button ?disabled=${!this.name.trim() || this.name.trim() === this.card.name} @click=${() => this.save()}>${t('ai-dashboard.button.save')}</md-filled-button>
252
+ </div>`
253
+ }
254
+
255
+ private close() {
256
+ this.dispatchEvent(new CustomEvent('done', { bubbles: true, composed: true }))
257
+ }
258
+
259
+ private async save() {
260
+ try {
261
+ await cardApi.rename(this.card.id, this.name.trim())
262
+ dashboardChanged()
263
+ this.close()
264
+ } catch (error: any) {
265
+ notify({ level: 'error', message: error?.message ?? String(error) })
266
+ }
267
+ }
268
+ }
269
+
270
+ /** Whom one's own card is shared with - the whole domain, one's own departments, people found by name. */
271
+ @customElement('ai-dashboard-card-share-form')
272
+ export class CardShareForm extends localize(i18next)(LitElement) {
273
+ static styles = FormStyles
274
+ @property({ attribute: false }) card!: CardItem
275
+ @state() private options?: Awaited<ReturnType<typeof cardApi.shareOptions>>
276
+ @state() private shares: CardShare[] = []
277
+ @state() private found: { id: string; name: string; note?: string }[] = []
278
+ @state() private search = ''
279
+ private searching?: ReturnType<typeof setTimeout>
280
+
281
+ async connectedCallback() {
282
+ super.connectedCallback()
283
+ this.shares = [...(this.card?.shares ?? [])].map(({ scope, targetId, targetName }) => ({ scope, targetId, targetName }))
284
+ try {
285
+ this.options = await cardApi.shareOptions()
286
+ } catch (error: any) {
287
+ notify({ level: 'error', message: error?.message ?? String(error) })
288
+ }
289
+ }
290
+
291
+ private has(scope: string, id: string) {
292
+ return this.shares.some(one => one.scope === scope && one.targetId === id)
293
+ }
294
+
295
+ private toggle(share: CardShare, on: boolean) {
296
+ this.shares = on ? [...this.shares.filter(one => !(one.scope === share.scope && one.targetId === share.targetId)), share] : this.shares.filter(one => !(one.scope === share.scope && one.targetId === share.targetId))
297
+ }
298
+
299
+ private find(text: string) {
300
+ this.search = text
301
+ clearTimeout(this.searching)
302
+ this.searching = setTimeout(async () => {
303
+ try {
304
+ this.found = text.trim() ? await cardApi.people(text.trim()) : []
305
+ } catch {
306
+ this.found = []
307
+ }
308
+ }, 250)
309
+ }
310
+
311
+ render() {
312
+ const options = this.options
313
+ const people = this.shares.filter(one => one.scope === 'USER')
314
+ return html`<p class="lead">${t('ai-dashboard.share.lead')}</p>
315
+ ${options
316
+ ? html`<h5>${t('ai-dashboard.share.everyone-title')}</h5>
317
+ <label class="option">
318
+ <input type="checkbox" .checked=${this.has('DOMAIN', options.domainId)} @change=${(e: any) => this.toggle({ scope: 'DOMAIN', targetId: options.domainId, targetName: options.domainName }, e.target.checked)} />
319
+ <md-icon>domain</md-icon>${t('ai-dashboard.share.everyone-in', { name: options.domainName })}
320
+ </label>
321
+ ${options.departmentsAvailable && options.departments.length
322
+ ? html`<h5>${t('ai-dashboard.share.departments')}</h5>
323
+ ${options.departments.map(
324
+ one => html`<label class="option">
325
+ <input type="checkbox" .checked=${this.has('DEPARTMENT', one.id)} @change=${(e: any) => this.toggle({ scope: 'DEPARTMENT', targetId: one.id, targetName: one.name }, e.target.checked)} />
326
+ <md-icon>groups</md-icon>${one.name}<small>${t('ai-dashboard.share.department-members')}</small>
327
+ </label>`
328
+ )}`
329
+ : nothing}
330
+ <h5>${t('ai-dashboard.share.people')}</h5>
331
+ ${people.length
332
+ ? html`<div class="people">
333
+ ${people.map(one => html`<div class="person"><md-icon>person</md-icon>${one.targetName ?? one.targetId}<button @click=${() => this.toggle(one, false)}>${t('ai-dashboard.share.remove')}</button></div>`)}
334
+ </div>`
335
+ : nothing}
336
+ ${options.peopleAvailable
337
+ ? html`<input type="search" .value=${this.search} placeholder=${t('ai-dashboard.share.find-person')} @input=${(e: any) => this.find(e.target.value)} />
338
+ ${this.found.length
339
+ ? html`<div class="found">
340
+ ${this.found
341
+ .filter(one => !this.has('USER', one.id))
342
+ .map(one => html`<div><md-icon>person_add</md-icon>${one.name} ${one.note ? html`<small>${one.note}</small>` : nothing}<button @click=${() => this.toggle({ scope: 'USER', targetId: one.id, targetName: one.name }, true)}>${t('ai-dashboard.share.add')}</button></div>`)}
343
+ </div>`
344
+ : nothing}`
345
+ : html`<p class="note">${t('ai-dashboard.share.people-unavailable')}</p>`}
346
+ <p class="note">${t('ai-dashboard.share.rule')}</p>`
347
+ : nothing}
348
+ <div class="actions">
349
+ <md-text-button @click=${() => this.close()}>${t('ai-dashboard.button.cancel')}</md-text-button>
350
+ <md-filled-button ?disabled=${!options} @click=${() => this.save()}>${t('ai-dashboard.button.save')}</md-filled-button>
351
+ </div>`
352
+ }
353
+
354
+ private close() {
355
+ this.dispatchEvent(new CustomEvent('done', { bubbles: true, composed: true }))
356
+ }
357
+
358
+ private async save() {
359
+ try {
360
+ await cardApi.share(this.card.id, this.shares)
361
+ notify({ level: 'info', message: t(this.shares.length ? 'ai-dashboard.text.shared' : 'ai-dashboard.text.private', { name: this.card.name }) })
362
+ dashboardChanged()
363
+ this.close()
364
+ } catch (error: any) {
365
+ notify({ level: 'error', message: error?.message ?? String(error) })
366
+ }
367
+ }
368
+ }
369
+
370
+ /** One's own card's versions, newest first - an earlier one comes back as a new version. */
371
+ @customElement('ai-dashboard-card-versions')
372
+ export class CardVersions extends localize(i18next)(LitElement) {
373
+ static styles = FormStyles
374
+ @property({ attribute: false }) card!: CardItem
375
+ @state() private versions?: Awaited<ReturnType<typeof cardApi.versions>>
376
+
377
+ async connectedCallback() {
378
+ super.connectedCallback()
379
+ try {
380
+ this.versions = await cardApi.versions(this.card.id)
381
+ } catch (error: any) {
382
+ notify({ level: 'error', message: error?.message ?? String(error) })
383
+ }
384
+ }
385
+
386
+ render() {
387
+ const view = (definition: any) => t(`ai-dashboard.view.${definition?.view?.kind ?? 'table'}`)
388
+ return html`<p class="lead">${t('ai-dashboard.versions.lead')}</p>
389
+ <ol class="versions">
390
+ ${(this.versions ?? []).map(
391
+ one => html`<li>
392
+ <span class="v">v${one.version}</span>
393
+ <span class="what">${one.name}<small>${view(one.definition)} · ${one.creatorName ?? ''} · ${one.createdAt ? new Date(one.createdAt).toLocaleString(i18next.language) : ''}</small></span>
394
+ ${one.version === this.card.version
395
+ ? html`<small>${t('ai-dashboard.versions.current')}</small>`
396
+ : html`<md-text-button @click=${() => this.restore(one.version)}>${t('ai-dashboard.versions.restore')}</md-text-button>`}
397
+ </li>`
398
+ )}
399
+ </ol>`
400
+ }
401
+
402
+ private async restore(version: number) {
403
+ try {
404
+ await cardApi.restore(this.card.id, version)
405
+ notify({ level: 'info', message: t('ai-dashboard.text.restored', { version }) })
406
+ dashboardChanged()
407
+ this.dispatchEvent(new CustomEvent('done', { bubbles: true, composed: true }))
408
+ } catch (error: any) {
409
+ notify({ level: 'error', message: error?.message ?? String(error) })
410
+ }
411
+ }
412
+ }
413
+
414
+ function open(template: unknown, title: string, size: 'small' | 'medium' = 'small') {
415
+ const popup: any = openPopup(html`<div @done=${() => popup?.close?.()}>${template}</div>`, { backdrop: true, size, title })
416
+ return popup
417
+ }
418
+
419
+ export const openCardAiPrompt = (card: CardItem) => open(html`<ai-dashboard-card-ai-form .card=${card}></ai-dashboard-card-ai-form>`, t('ai-dashboard.card.ai'), 'medium')
420
+ export const openCardRename = (card: CardItem) => open(html`<ai-dashboard-card-rename-form .card=${card}></ai-dashboard-card-rename-form>`, t('ai-dashboard.card.rename'))
421
+ export const openCardShare = (card: CardItem) => open(html`<ai-dashboard-card-share-form .card=${card}></ai-dashboard-card-share-form>`, t('ai-dashboard.share.title', { name: card.name }), 'medium')
422
+ export const openCardVersions = (card: CardItem) => open(html`<ai-dashboard-card-versions .card=${card}></ai-dashboard-card-versions>`, t('ai-dashboard.card.versions'), 'medium')