@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,450 @@
1
+ /*
2
+ * One card on the dashboard - its name and what it reads, the block its definition draws as the viewer, when it was
3
+ * read and how much of its source it was drawn from (ADR-0160 decisions 4-6, 8). Drawing it asks the server to run
4
+ * the definition; no model is asked.
5
+ */
6
+ import '@material/web/icon/icon.js'
7
+ import '@things-factory/ai-assistant/dist-client/index.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
+
14
+ import { cardApi, type CardItem, type CardRun } from '../utils/card-api.js'
15
+ import { drawsNothing, issueLine, ownershipLine, partialLine, readLine, viewIcon } from '../utils/card-words.js'
16
+
17
+ const t = (key: string, options: Record<string, unknown> = {}) => String(i18next.t(key, options))
18
+
19
+ export type CardAction = 'refresh' | 'ai' | 'rename' | 'share' | 'versions' | 'copy' | 'unplace' | 'left' | 'right' | 'wider' | 'narrower' | 'taller' | 'shorter'
20
+
21
+ @customElement('ai-dashboard-card')
22
+ export class AiDashboardCard extends localize(i18next)(LitElement) {
23
+ static styles = css`
24
+ :host {
25
+ display: block;
26
+ min-width: 0;
27
+ min-height: 0;
28
+ }
29
+
30
+ article {
31
+ position: relative;
32
+ display: flex;
33
+ flex-direction: column;
34
+ height: 100%;
35
+ box-sizing: border-box;
36
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
37
+ border-radius: var(--ui-r, 12px);
38
+ background: var(--ui-panel, var(--md-sys-color-surface-container-lowest));
39
+ overflow: hidden;
40
+ transition:
41
+ box-shadow 0.18s ease,
42
+ border-color 0.18s ease,
43
+ transform 0.18s ease;
44
+ }
45
+
46
+ article:hover {
47
+ border-color: color-mix(in srgb, var(--ui-accent, var(--md-sys-color-primary)) 35%, var(--ui-line, transparent));
48
+ box-shadow: 0 6px 20px -12px rgb(0 0 0 / 35%);
49
+ }
50
+
51
+ :host([editing]) article {
52
+ border-style: dashed;
53
+ border-color: var(--ui-accent, var(--md-sys-color-primary));
54
+ }
55
+
56
+ header {
57
+ display: flex;
58
+ align-items: flex-start;
59
+ gap: 10px;
60
+ padding: 14px 14px 6px 16px;
61
+ }
62
+
63
+ .kind {
64
+ flex: none;
65
+ display: grid;
66
+ place-items: center;
67
+ width: 30px;
68
+ height: 30px;
69
+ border-radius: 8px;
70
+ background: var(--ui-accent-wash, color-mix(in srgb, var(--md-sys-color-primary) 12%, transparent));
71
+ color: var(--ui-accent, var(--md-sys-color-primary));
72
+ --md-icon-size: 18px;
73
+ }
74
+
75
+ .titles {
76
+ flex: 1;
77
+ min-width: 0;
78
+ }
79
+
80
+ h4 {
81
+ margin: 0;
82
+ font-size: var(--ui-t-title, 15px);
83
+ font-weight: 600;
84
+ line-height: 1.3;
85
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
86
+ overflow: hidden;
87
+ text-overflow: ellipsis;
88
+ white-space: nowrap;
89
+ }
90
+
91
+ .sub {
92
+ display: block;
93
+ margin-top: 2px;
94
+ font-size: var(--ui-t-small, 12px);
95
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
96
+ overflow: hidden;
97
+ text-overflow: ellipsis;
98
+ white-space: nowrap;
99
+ }
100
+
101
+ .tools {
102
+ flex: none;
103
+ display: flex;
104
+ gap: 2px;
105
+ opacity: 0.55;
106
+ transition: opacity 0.15s ease;
107
+ }
108
+
109
+ article:hover .tools,
110
+ article:focus-within .tools,
111
+ :host([editing]) .tools {
112
+ opacity: 1;
113
+ }
114
+
115
+ button.icon {
116
+ display: grid;
117
+ place-items: center;
118
+ width: 30px;
119
+ height: 30px;
120
+ padding: 0;
121
+ border: 0;
122
+ border-radius: 8px;
123
+ background: transparent;
124
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
125
+ cursor: pointer;
126
+ --md-icon-size: 18px;
127
+ }
128
+
129
+ button.icon:hover,
130
+ button.icon:focus-visible {
131
+ background: var(--ui-hover, var(--md-sys-color-surface-container-high));
132
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
133
+ outline: none;
134
+ }
135
+
136
+ button.icon[disabled] {
137
+ opacity: 0.35;
138
+ cursor: default;
139
+ }
140
+
141
+ button.icon.spin md-icon {
142
+ animation: spin 0.9s linear infinite;
143
+ }
144
+
145
+ @keyframes spin {
146
+ to {
147
+ transform: rotate(360deg);
148
+ }
149
+ }
150
+
151
+ .menu {
152
+ position: absolute;
153
+ top: 44px;
154
+ right: 10px;
155
+ z-index: 2;
156
+ display: flex;
157
+ flex-direction: column;
158
+ min-width: 180px;
159
+ padding: 6px;
160
+ border: 1px solid var(--ui-line, var(--md-sys-color-outline-variant));
161
+ border-radius: 10px;
162
+ background: var(--ui-panel, var(--md-sys-color-surface-container-lowest));
163
+ box-shadow: 0 12px 32px -12px rgb(0 0 0 / 40%);
164
+ }
165
+
166
+ .menu button {
167
+ display: flex;
168
+ align-items: center;
169
+ gap: 10px;
170
+ padding: 8px 10px;
171
+ border: 0;
172
+ border-radius: 6px;
173
+ background: transparent;
174
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
175
+ font: inherit;
176
+ font-size: var(--ui-t-body, 13px);
177
+ text-align: start;
178
+ cursor: pointer;
179
+ --md-icon-size: 18px;
180
+ }
181
+
182
+ .menu button:hover,
183
+ .menu button:focus-visible {
184
+ background: var(--ui-hover, var(--md-sys-color-surface-container-high));
185
+ outline: none;
186
+ }
187
+
188
+ .menu hr {
189
+ width: 100%;
190
+ margin: 4px 0;
191
+ border: 0;
192
+ border-top: 1px solid var(--ui-line-soft, var(--md-sys-color-outline-variant));
193
+ }
194
+
195
+ .body {
196
+ flex: 1;
197
+ min-height: 0;
198
+ padding: 4px 16px 8px;
199
+ overflow: auto;
200
+ }
201
+
202
+ .skeleton {
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 10px;
206
+ padding-top: 8px;
207
+ }
208
+
209
+ .skeleton span {
210
+ display: block;
211
+ height: 14px;
212
+ border-radius: 6px;
213
+ background: linear-gradient(90deg, var(--ui-line-soft, #eee) 0%, var(--ui-panel-2, #f6f6f6) 50%, var(--ui-line-soft, #eee) 100%);
214
+ background-size: 200% 100%;
215
+ animation: shimmer 1.4s ease-in-out infinite;
216
+ }
217
+
218
+ .skeleton span:nth-child(1) {
219
+ width: 40%;
220
+ height: 28px;
221
+ }
222
+
223
+ .skeleton span:nth-child(2) {
224
+ width: 85%;
225
+ }
226
+
227
+ .skeleton span:nth-child(3) {
228
+ width: 65%;
229
+ }
230
+
231
+ @keyframes shimmer {
232
+ from {
233
+ background-position: 200% 0;
234
+ }
235
+ to {
236
+ background-position: -200% 0;
237
+ }
238
+ }
239
+
240
+ .issue {
241
+ display: flex;
242
+ gap: 10px;
243
+ align-items: flex-start;
244
+ margin-top: 8px;
245
+ padding: 12px;
246
+ border-radius: 8px;
247
+ background: color-mix(in srgb, var(--ui-crit, #c62828) 8%, transparent);
248
+ color: var(--ui-ink, var(--md-sys-color-on-surface));
249
+ font-size: var(--ui-t-body, 13px);
250
+ --md-icon-size: 18px;
251
+ }
252
+
253
+ .issue md-icon {
254
+ flex: none;
255
+ color: var(--ui-crit, #c62828);
256
+ }
257
+
258
+ .issue.access {
259
+ background: var(--ui-panel-2, var(--md-sys-color-surface-container));
260
+ }
261
+
262
+ .issue.access md-icon {
263
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
264
+ }
265
+
266
+ .nothing {
267
+ display: flex;
268
+ flex-direction: column;
269
+ align-items: center;
270
+ justify-content: center;
271
+ gap: 6px;
272
+ height: 100%;
273
+ min-height: 120px;
274
+ color: var(--ui-faint, var(--md-sys-color-on-surface-variant));
275
+ font-size: var(--ui-t-body, 13px);
276
+ text-align: center;
277
+ --md-icon-size: 28px;
278
+ }
279
+
280
+ footer {
281
+ display: flex;
282
+ flex-wrap: wrap;
283
+ align-items: center;
284
+ gap: 4px 10px;
285
+ padding: 6px 16px 12px;
286
+ font-size: var(--ui-t-micro, 11px);
287
+ color: var(--ui-faint, var(--md-sys-color-on-surface-variant));
288
+ }
289
+
290
+ footer .partial {
291
+ display: inline-flex;
292
+ align-items: center;
293
+ gap: 4px;
294
+ color: var(--ui-warn, #b26a00);
295
+ --md-icon-size: 14px;
296
+ }
297
+
298
+ .edit {
299
+ display: flex;
300
+ flex-wrap: wrap;
301
+ align-items: center;
302
+ justify-content: center;
303
+ gap: 4px;
304
+ padding: 8px;
305
+ border-top: 1px dashed var(--ui-line, var(--md-sys-color-outline-variant));
306
+ background: var(--ui-panel-2, var(--md-sys-color-surface-container-low));
307
+ }
308
+
309
+ .edit .size {
310
+ margin: 0 6px;
311
+ font-size: var(--ui-t-small, 12px);
312
+ color: var(--ui-muted, var(--md-sys-color-on-surface-variant));
313
+ font-variant-numeric: tabular-nums;
314
+ }
315
+ `
316
+
317
+ @property({ attribute: false }) card?: CardItem
318
+ /** The place's size, when it is on the dashboard. */
319
+ @property({ type: Number }) width = 2
320
+ @property({ type: Number }) height = 1
321
+ @property({ type: Boolean, reflect: true }) editing = false
322
+ @property({ type: Boolean }) first = false
323
+ @property({ type: Boolean }) last = false
324
+
325
+ @state() private run?: CardRun
326
+ @state() private failed?: string
327
+ @state() private loading = false
328
+ @state() private menu = false
329
+ @state() private readAt?: Date
330
+
331
+ private readingFor?: string
332
+ private closeMenu = (event: Event) => {
333
+ if (!event.composedPath().includes(this)) this.menu = false
334
+ }
335
+
336
+ connectedCallback() {
337
+ super.connectedCallback()
338
+ document.addEventListener('click', this.closeMenu, true)
339
+ }
340
+
341
+ disconnectedCallback() {
342
+ document.removeEventListener('click', this.closeMenu, true)
343
+ super.disconnectedCallback()
344
+ }
345
+
346
+ updated(changes: Map<string, unknown>) {
347
+ /* A new card, or a new version of it, is read again; nothing else is. */
348
+ const key = this.card ? `${this.card.id}@${this.card.version}` : undefined
349
+ if (changes.has('card') && key && key !== this.readingFor) this.refresh()
350
+ }
351
+
352
+ /** Draws the card again from the data - the server runs its definition as the viewer. */
353
+ async refresh() {
354
+ if (!this.card) return
355
+ const key = `${this.card.id}@${this.card.version}`
356
+ this.readingFor = key
357
+ this.loading = true
358
+ this.failed = undefined
359
+ try {
360
+ const run = await cardApi.run(this.card.id)
361
+ if (this.readingFor !== key) return
362
+ this.run = run
363
+ this.readAt = new Date()
364
+ } catch (error: any) {
365
+ if (this.readingFor === key) this.failed = error?.message ?? String(error)
366
+ } finally {
367
+ if (this.readingFor === key) this.loading = false
368
+ }
369
+ }
370
+
371
+ private act(action: CardAction) {
372
+ this.menu = false
373
+ if (action === 'refresh') return this.refresh()
374
+ this.dispatchEvent(new CustomEvent('card-action', { detail: { action, card: this.card }, bubbles: true, composed: true }))
375
+ }
376
+
377
+ render() {
378
+ const card = this.card
379
+ if (!card) return nothing
380
+ const owner = card.access === 'owner'
381
+ const source = this.run?.source?.labelKey ? t(this.run.source.labelKey) : ''
382
+ const ownership = ownershipLine(card, t)
383
+ const sub = [card.description || source, ownership].filter(Boolean).join(' · ')
384
+ return html`<article aria-busy=${this.loading ? 'true' : 'false'}>
385
+ <header>
386
+ <span class="kind"><md-icon>${viewIcon(card.definition)}</md-icon></span>
387
+ <div class="titles">
388
+ <h4 title=${card.name}>${card.name}</h4>
389
+ ${sub ? html`<span class="sub" title=${sub}>${sub}</span>` : nothing}
390
+ </div>
391
+ <div class="tools">
392
+ <button class="icon ${this.loading ? 'spin' : ''}" title=${t('ai-dashboard.card.refresh')} aria-label=${t('ai-dashboard.card.refresh')} ?disabled=${this.loading} @click=${() => this.act('refresh')}>
393
+ <md-icon>refresh</md-icon>
394
+ </button>
395
+ ${owner
396
+ ? html`<button class="icon" title=${t('ai-dashboard.card.ai')} aria-label=${t('ai-dashboard.card.ai')} @click=${() => this.act('ai')}><md-icon>auto_awesome</md-icon></button>`
397
+ : nothing}
398
+ <button class="icon" title=${t('ai-dashboard.card.more')} aria-label=${t('ai-dashboard.card.more')} aria-expanded=${this.menu ? 'true' : 'false'} @click=${() => (this.menu = !this.menu)}>
399
+ <md-icon>more_vert</md-icon>
400
+ </button>
401
+ </div>
402
+ </header>
403
+ ${this.menu ? this.renderMenu(owner) : nothing}
404
+ <div class="body">${this.renderBody()}</div>
405
+ ${this.run && !this.run.issue ? this.renderFooter() : nothing}
406
+ ${this.editing ? this.renderEdit() : nothing}
407
+ </article>`
408
+ }
409
+
410
+ private renderMenu(owner: boolean) {
411
+ const item = (action: CardAction, icon: string, key: string) => html`<button role="menuitem" @click=${() => this.act(action)}><md-icon>${icon}</md-icon>${t(key)}</button>`
412
+ return html`<div class="menu" role="menu">
413
+ ${owner ? item('ai', 'auto_awesome', 'ai-dashboard.card.ai') : nothing} ${owner ? item('rename', 'edit', 'ai-dashboard.card.rename') : nothing}
414
+ ${owner ? item('share', 'share', 'ai-dashboard.card.share') : nothing} ${owner ? item('versions', 'history', 'ai-dashboard.card.versions') : nothing}
415
+ ${item('copy', 'content_copy', owner ? 'ai-dashboard.card.duplicate' : 'ai-dashboard.card.copy')}
416
+ <hr />
417
+ ${item('unplace', 'remove_circle_outline', 'ai-dashboard.card.unplace')}
418
+ </div>`
419
+ }
420
+
421
+ private renderBody() {
422
+ if (this.failed) return html`<div class="issue"><md-icon>error</md-icon><span>${this.failed}</span></div>`
423
+ if (!this.run) return html`<div class="skeleton" aria-hidden="true"><span></span><span></span><span></span></div>`
424
+ const issue = issueLine(this.run, t)
425
+ if (issue) return html`<div class="issue ${issue.access ? 'access' : ''}"><md-icon>${issue.access ? 'lock' : 'error'}</md-icon><span>${issue.text}</span></div>`
426
+ if (drawsNothing(this.run)) return html`<div class="nothing"><md-icon>inbox</md-icon><span>${t('ai-dashboard.card.nothing')}</span></div>`
427
+ return html`<ox-assistant-workspace-view compact .workspace=${{ name: this.card!.id, title: this.card!.name, blocks: [this.run.block] }}></ox-assistant-workspace-view>`
428
+ }
429
+
430
+ private renderFooter() {
431
+ const partial = partialLine(this.run!, t)
432
+ return html`<footer>
433
+ <span>${readLine(this.run!, this.readAt ?? new Date(), t, i18next.language)}</span>
434
+ ${partial ? html`<span class="partial"><md-icon>warning</md-icon>${partial}</span>` : nothing}
435
+ </footer>`
436
+ }
437
+
438
+ private renderEdit() {
439
+ const button = (action: CardAction, icon: string, key: string, disabled = false) =>
440
+ html`<button class="icon" title=${t(key)} aria-label=${t(key)} ?disabled=${disabled} @click=${() => this.act(action)}><md-icon>${icon}</md-icon></button>`
441
+ return html`<div class="edit">
442
+ ${button('left', 'arrow_back', 'ai-dashboard.edit.left', this.first)} ${button('right', 'arrow_forward', 'ai-dashboard.edit.right', this.last)}
443
+ ${button('narrower', 'remove', 'ai-dashboard.edit.narrower', this.width <= 1)}
444
+ <span class="size">${this.width} × ${this.height}</span>
445
+ ${button('wider', 'add', 'ai-dashboard.edit.wider', this.width >= 4)}
446
+ ${button(this.height > 1 ? 'shorter' : 'taller', this.height > 1 ? 'unfold_less' : 'unfold_more', this.height > 1 ? 'ai-dashboard.edit.shorter' : 'ai-dashboard.edit.taller')}
447
+ ${button('unplace', 'delete_outline', 'ai-dashboard.card.unplace')}
448
+ </div>`
449
+ }
450
+ }
@@ -0,0 +1,96 @@
1
+ /*
2
+ * The preview under a card the conversation offers (ADR-0160 decisions 6, 9). The proposal holds the definition only;
3
+ * the preview asks the server to run it as whoever is looking at the conversation, so a value is never kept in the
4
+ * conversation and nobody sees data their own doors do not open.
5
+ */
6
+ import '@material/web/icon/icon.js'
7
+ import '@things-factory/ai-assistant/dist-client/index.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
+
14
+ import { cardApi, type CardRun } from '../utils/card-api.js'
15
+ import { drawsNothing, issueLine, partialLine } from '../utils/card-words.js'
16
+
17
+ const t = (key: string, options: Record<string, unknown> = {}) => String(i18next.t(key, options))
18
+
19
+ @customElement('ai-dashboard-proposal-preview')
20
+ export class ProposalPreview extends localize(i18next)(LitElement) {
21
+ static styles = css`
22
+ :host {
23
+ display: block;
24
+ margin: 4px 0 8px;
25
+ }
26
+
27
+ .frame {
28
+ padding: 10px 12px 8px;
29
+ border: 1px solid var(--md-sys-color-outline-variant);
30
+ border-radius: 10px;
31
+ background: var(--md-sys-color-surface-container-lowest);
32
+ }
33
+
34
+ .waiting,
35
+ .note {
36
+ font-size: 12px;
37
+ color: var(--md-sys-color-on-surface-variant);
38
+ }
39
+
40
+ .note {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 4px;
44
+ margin-top: 6px;
45
+ --md-icon-size: 14px;
46
+ }
47
+
48
+ .issue {
49
+ font-size: 12px;
50
+ color: var(--md-sys-color-error);
51
+ }
52
+
53
+ .partial {
54
+ color: var(--ui-warn, #b26a00);
55
+ }
56
+ `
57
+
58
+ @property({ attribute: false }) proposal: any
59
+ @property({ attribute: false }) sessionId?: string
60
+ @property({ attribute: false }) proposalKey?: string
61
+
62
+ @state() private run?: CardRun
63
+ @state() private failed?: string
64
+ private readFor?: string
65
+
66
+ updated() {
67
+ const key = this.sessionId && this.proposalKey ? `${this.sessionId}/${this.proposalKey}` : undefined
68
+ if (key && key !== this.readFor) this.read(key)
69
+ }
70
+
71
+ private async read(key: string) {
72
+ this.readFor = key
73
+ try {
74
+ const run = await cardApi.preview(this.sessionId!, this.proposalKey!)
75
+ if (this.readFor === key) this.run = run
76
+ } catch (error: any) {
77
+ if (this.readFor === key) this.failed = error?.message ?? String(error)
78
+ }
79
+ }
80
+
81
+ render() {
82
+ if (this.failed) return html`<div class="frame issue">${this.failed}</div>`
83
+ if (!this.run) return html`<div class="frame waiting">${t('ai-dashboard.preview.reading')}</div>`
84
+ const issue = issueLine(this.run, t)
85
+ const partial = partialLine(this.run, t)
86
+ return html`<div class="frame">
87
+ ${issue
88
+ ? html`<div class="issue">${issue.text}</div>`
89
+ : drawsNothing(this.run)
90
+ ? html`<div class="waiting">${t('ai-dashboard.card.nothing')}</div>`
91
+ : html`<ox-assistant-workspace-view compact .workspace=${{ name: this.proposalKey, title: this.proposal?.card?.name ?? '', blocks: [this.run.block] }}></ox-assistant-workspace-view>`}
92
+ ${partial ? html`<div class="note partial"><md-icon>warning</md-icon>${partial}</div>` : nothing}
93
+ <div class="note"><md-icon>bolt</md-icon>${t('ai-dashboard.preview.note', { day: this.run.asOf })}</div>
94
+ </div>`
95
+ }
96
+ }
@@ -0,0 +1,3 @@
1
+ export * from './utils/card-api.js'
2
+ export * from './utils/card-words.js'
3
+ export * from './components/dashboard-card.js'