aimeat 1.23.2 → 1.23.3

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 (154) hide show
  1. package/dist/locales/en.json +536 -21
  2. package/dist/locales/fi.json +548 -33
  3. package/dist/public/components/ContactCard.js +28 -0
  4. package/dist/public/css/views/admin.css +12 -0
  5. package/dist/public/css/views/agents-detail.css +62 -10
  6. package/dist/public/css/views/landing.css +87 -0
  7. package/dist/public/css/views/offers.css +96 -0
  8. package/dist/public/css/views/portfolio.css +18 -0
  9. package/dist/public/css/views/profile.css +153 -8
  10. package/dist/public/css/views/start.css +96 -0
  11. package/dist/public/img/business-hero.png +0 -0
  12. package/dist/public/img/toimitus.png +0 -0
  13. package/dist/public/js/components/tag-cloud.js +46 -31
  14. package/dist/public/js/components/tag-editor.js +2 -2
  15. package/dist/public/js/recents.js +40 -0
  16. package/dist/public/js/services/admin.js +6 -0
  17. package/dist/public/js/services/agents.js +34 -0
  18. package/dist/public/js/services/offers.js +152 -0
  19. package/dist/public/js/services/packages.js +1 -0
  20. package/dist/public/spa.html +32 -2
  21. package/dist/public/views/admin/subdomains-tab.js +182 -0
  22. package/dist/public/views/admin.js +2 -0
  23. package/dist/public/views/business.js +119 -0
  24. package/dist/public/views/how-it-works.js +159 -0
  25. package/dist/public/views/landing.js +235 -0
  26. package/dist/public/views/portfolio.js +120 -23
  27. package/dist/public/views/pricing.js +75 -0
  28. package/dist/public/views/profile/access-tab.js +124 -51
  29. package/dist/public/views/profile/agents/agent-card.js +68 -37
  30. package/dist/public/views/profile/agents/shared-board.js +56 -22
  31. package/dist/public/views/profile/agents/tab-activity.js +24 -12
  32. package/dist/public/views/profile/agents/tab-agent-config.js +31 -1
  33. package/dist/public/views/profile/agents/tab-data-access.js +9 -13
  34. package/dist/public/views/profile/agents/tab-integration.js +27 -1
  35. package/dist/public/views/profile/agents/tab-messages.js +6 -11
  36. package/dist/public/views/profile/agents/tab-quality.js +45 -5
  37. package/dist/public/views/profile/agents-tab.js +41 -6
  38. package/dist/public/views/profile/apps-tab.js +7 -1
  39. package/dist/public/views/profile/knowledge-tab.js +4 -1
  40. package/dist/public/views/profile/landing-page.js +427 -123
  41. package/dist/public/views/profile/memory-tab.js +409 -211
  42. package/dist/public/views/profile/nodes-tab.js +18 -3
  43. package/dist/public/views/profile/offers-tab.js +310 -0
  44. package/dist/public/views/profile/organisms-tab.js +289 -28
  45. package/dist/public/views/profile/packages-tab.js +152 -16
  46. package/dist/public/views/profile/portfolio-tab.js +98 -31
  47. package/dist/public/views/profile.js +2 -0
  48. package/dist/public/views/start-flows.js +281 -0
  49. package/dist/public/views/start.js +258 -0
  50. package/dist/src/generated/api-types.d.ts +856 -17
  51. package/dist/src/generated/api-types.d.ts.map +1 -1
  52. package/dist/src/generated/prisma-postgres/edge.js +15 -3
  53. package/dist/src/generated/prisma-postgres/index-browser.js +12 -0
  54. package/dist/src/generated/prisma-postgres/index.d.ts +1476 -160
  55. package/dist/src/generated/prisma-postgres/index.js +15 -3
  56. package/dist/src/generated/prisma-postgres/package.json +1 -1
  57. package/dist/src/generated/prisma-postgres/schema.prisma +11 -0
  58. package/dist/src/generated/prisma-postgres/wasm.js +15 -3
  59. package/dist/src/middleware/subdomain.d.ts +28 -0
  60. package/dist/src/middleware/subdomain.d.ts.map +1 -0
  61. package/dist/src/middleware/subdomain.js +32 -0
  62. package/dist/src/middleware/subdomain.js.map +1 -0
  63. package/dist/src/models/offer-schemas.d.ts +229 -0
  64. package/dist/src/models/offer-schemas.d.ts.map +1 -0
  65. package/dist/src/models/offer-schemas.js +100 -0
  66. package/dist/src/models/offer-schemas.js.map +1 -0
  67. package/dist/src/routes/agent-tasks.d.ts.map +1 -1
  68. package/dist/src/routes/agent-tasks.js +33 -0
  69. package/dist/src/routes/agent-tasks.js.map +1 -1
  70. package/dist/src/routes/agents.d.ts.map +1 -1
  71. package/dist/src/routes/agents.js +146 -1
  72. package/dist/src/routes/agents.js.map +1 -1
  73. package/dist/src/routes/apps-backup.d.ts +20 -0
  74. package/dist/src/routes/apps-backup.d.ts.map +1 -0
  75. package/dist/src/routes/apps-backup.js +165 -0
  76. package/dist/src/routes/apps-backup.js.map +1 -0
  77. package/dist/src/routes/ghii.d.ts.map +1 -1
  78. package/dist/src/routes/ghii.js +4 -0
  79. package/dist/src/routes/ghii.js.map +1 -1
  80. package/dist/src/routes/instances.d.ts +2 -1
  81. package/dist/src/routes/instances.d.ts.map +1 -1
  82. package/dist/src/routes/instances.js +59 -1
  83. package/dist/src/routes/instances.js.map +1 -1
  84. package/dist/src/routes/packages.d.ts.map +1 -1
  85. package/dist/src/routes/packages.js +32 -0
  86. package/dist/src/routes/packages.js.map +1 -1
  87. package/dist/src/routes/permissions.d.ts +12 -0
  88. package/dist/src/routes/permissions.d.ts.map +1 -1
  89. package/dist/src/routes/permissions.js +35 -3
  90. package/dist/src/routes/permissions.js.map +1 -1
  91. package/dist/src/routes/portal.d.ts +3 -0
  92. package/dist/src/routes/portal.d.ts.map +1 -1
  93. package/dist/src/routes/portal.js +12 -0
  94. package/dist/src/routes/portal.js.map +1 -1
  95. package/dist/src/routes/public-stats.d.ts +15 -0
  96. package/dist/src/routes/public-stats.d.ts.map +1 -0
  97. package/dist/src/routes/public-stats.js +90 -0
  98. package/dist/src/routes/public-stats.js.map +1 -0
  99. package/dist/src/routes/sse.d.ts +7 -2
  100. package/dist/src/routes/sse.d.ts.map +1 -1
  101. package/dist/src/routes/sse.js +35 -5
  102. package/dist/src/routes/sse.js.map +1 -1
  103. package/dist/src/routes/subdomains.d.ts +33 -0
  104. package/dist/src/routes/subdomains.d.ts.map +1 -0
  105. package/dist/src/routes/subdomains.js +208 -0
  106. package/dist/src/routes/subdomains.js.map +1 -0
  107. package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
  108. package/dist/src/server-bootstrap/routes-loader.js +12 -1
  109. package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
  110. package/dist/src/server.d.ts.map +1 -1
  111. package/dist/src/server.js +4 -0
  112. package/dist/src/server.js.map +1 -1
  113. package/dist/src/services/apps-backup-export.d.ts +22 -0
  114. package/dist/src/services/apps-backup-export.d.ts.map +1 -0
  115. package/dist/src/services/apps-backup-export.js +136 -0
  116. package/dist/src/services/apps-backup-export.js.map +1 -0
  117. package/dist/src/services/apps-backup-import.d.ts +120 -0
  118. package/dist/src/services/apps-backup-import.d.ts.map +1 -0
  119. package/dist/src/services/apps-backup-import.js +259 -0
  120. package/dist/src/services/apps-backup-import.js.map +1 -0
  121. package/dist/src/services/component-registrar.d.ts +17 -0
  122. package/dist/src/services/component-registrar.d.ts.map +1 -1
  123. package/dist/src/services/component-registrar.js +63 -5
  124. package/dist/src/services/component-registrar.js.map +1 -1
  125. package/dist/src/services/handbooks/appdev.d.ts +1 -1
  126. package/dist/src/services/handbooks/appdev.d.ts.map +1 -1
  127. package/dist/src/services/handbooks/appdev.js +5 -2
  128. package/dist/src/services/handbooks/appdev.js.map +1 -1
  129. package/dist/src/storage/interface.d.ts +11 -1
  130. package/dist/src/storage/interface.d.ts.map +1 -1
  131. package/dist/src/storage/interface.js.map +1 -1
  132. package/dist/src/storage/providers/mongodb/index.d.ts +10 -1
  133. package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
  134. package/dist/src/storage/providers/mongodb/index.js +76 -0
  135. package/dist/src/storage/providers/mongodb/index.js.map +1 -1
  136. package/dist/src/storage/providers/sqlite/index.d.ts +8 -1
  137. package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
  138. package/dist/src/storage/providers/sqlite/index.js +61 -0
  139. package/dist/src/storage/providers/sqlite/index.js.map +1 -1
  140. package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
  141. package/dist/src/storage/providers/sqlite/schema.js +11 -0
  142. package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
  143. package/dist/src/storage/repositories/index.d.ts +1 -0
  144. package/dist/src/storage/repositories/index.d.ts.map +1 -1
  145. package/dist/src/storage/repositories/package.repository.d.ts +2 -0
  146. package/dist/src/storage/repositories/package.repository.d.ts.map +1 -1
  147. package/dist/src/storage/repositories/subdomain-site.repository.d.ts +19 -0
  148. package/dist/src/storage/repositories/subdomain-site.repository.d.ts.map +1 -0
  149. package/dist/src/storage/repositories/subdomain-site.repository.js +2 -0
  150. package/dist/src/storage/repositories/subdomain-site.repository.js.map +1 -0
  151. package/dist/static/app-catalog.html +1139 -28
  152. package/package.json +1 -1
  153. package/prisma/schema.postgres.prisma +11 -0
  154. package/prisma/schema.prisma +11 -0
@@ -29,6 +29,122 @@ import { useConfirm } from '/components/Modal.js';
29
29
  import * as pkgService from '/js/services/packages.js';
30
30
  import { importPackageToGenerator } from '/js/services/generator-packaging.js';
31
31
 
32
+ // ─── InstanceCard ───────────────────────────────────────────────────────────
33
+ // Expanded card for one installed package instance: shows every component's
34
+ // status + type, with direct action buttons (Launch an app in a new tab,
35
+ // jump to extensions/cortex tab for management, copy the registered name).
36
+ // Components produced via packages-install are stored with status:'active'
37
+ // at creation time (see component-registrar.ts), so the green "active" chip
38
+ // here matches reality without re-fetching the underlying extension/cortex.
39
+ // Profile reads ?tab=<id> on mount and routes to the matching tab; hash
40
+ // fragments don't switch tabs. The IDs here are the profile tab IDs.
41
+ const COMP_TYPE_META = {
42
+ app: { icon: '\u{1F4F1}', tabId: 'apps' },
43
+ extension: { icon: '\u{1F50C}', tabId: 'extensions' },
44
+ cortex: { icon: '\u{1F9E0}', tabId: 'extensions' },
45
+ memory: { icon: '\u{1F4BE}', tabId: 'memory' },
46
+ translation: { icon: '\u{1F310}', tabId: 'memory' },
47
+ csm: { icon: '\u{1F4D0}', tabId: 'memory' },
48
+ msm: { icon: '\u{1F4DC}', tabId: 'memory' },
49
+ };
50
+
51
+ function InstanceCard({ inst, session, onCheckUpdate, onRemove, navigate }) {
52
+ const [expanded, setExpanded] = useState(true);
53
+ const comps = inst.installedComponents || [];
54
+ const owner = session?.owner || '';
55
+
56
+ // Build a deterministic action per component type.
57
+ function actionsFor(c) {
58
+ const meta = COMP_TYPE_META[c.type] || {};
59
+ const acts = [];
60
+ if (c.type === 'app') {
61
+ const url = `/v1/apps/${encodeURIComponent(owner)}/${encodeURIComponent(c.registeredAs)}?mode=inline`;
62
+ acts.push(html`
63
+ <a class="btn-primary btn-sm pf-no-underline" href=${url} target="_blank" rel="noopener">
64
+ ${t('packages.launch') || 'Launch'}
65
+ </a>
66
+ `);
67
+ }
68
+ if (c.type === 'extension' || c.type === 'cortex') {
69
+ acts.push(html`
70
+ <button class="btn-outline btn-sm" onClick=${() => {
71
+ // Profile reads ?tab=<id> on mount; this preserves the activeTab
72
+ // state via the same path the landing-page tab cards use.
73
+ const url = `/v1/profile?tab=${meta.tabId || 'extensions'}`;
74
+ if (typeof navigate === 'function') navigate(url);
75
+ else window.location.href = url;
76
+ }}>
77
+ ${t('packages.manage') || 'Manage'}
78
+ </button>
79
+ `);
80
+ }
81
+ if (c.type === 'memory' || c.type === 'translation' || c.type === 'csm' || c.type === 'msm') {
82
+ acts.push(html`
83
+ <button class="btn-outline btn-sm" onClick=${() => {
84
+ const url = `/v1/profile?tab=${meta.tabId || 'memory'}`;
85
+ if (typeof navigate === 'function') navigate(url);
86
+ else window.location.href = url;
87
+ }}>
88
+ ${t('packages.inspect') || 'Inspect'}
89
+ </button>
90
+ `);
91
+ }
92
+ // Copy registered name — useful for debugging or wiring up custom things.
93
+ acts.push(html`
94
+ <button class="btn-ghost btn-sm" title=${c.registeredAs} onClick=${() => copyToClipboard(c.registeredAs)}>
95
+ \u{1F4CB}
96
+ </button>
97
+ `);
98
+ return acts;
99
+ }
100
+
101
+ // INSTALLED == installed AND active+running. Show a clarifying sub-label so
102
+ // the user doesn't confuse it with the two-step extension/cortex lifecycle.
103
+ const installedSubLabel = inst.status === 'installed'
104
+ ? (t('packages.statusInstalledActive') || 'active & running')
105
+ : '';
106
+
107
+ return html`
108
+ <div class="pkg-card pkg-card-expanded" key=${inst.id}>
109
+ <div class="pkg-card-header">
110
+ <span class="pkg-card-name">${escHtml(inst.label || inst.packageGroupId)}</span>
111
+ <span class="pkg-badge pkg-badge-${inst.status}" title=${installedSubLabel}>
112
+ ${inst.status}${installedSubLabel ? html` <span class="pkg-badge-sub">· ${installedSubLabel}</span>` : ''}
113
+ </span>
114
+ </div>
115
+ <div class="pkg-card-meta">
116
+ <span class="pkg-card-version">${escHtml(inst.packageVersion)}</span>
117
+ <span>· ${comps.length} ${t('packages.components') || 'components'}</span>
118
+ <button class="btn-ghost btn-xs" onClick=${() => setExpanded(v => !v)}>
119
+ ${expanded ? (t('common.hide') || 'Hide') : (t('common.show') || 'Show')}
120
+ </button>
121
+ </div>
122
+ ${expanded && comps.length > 0 && html`
123
+ <ul class="pkg-comp-list">
124
+ ${comps.map(c => html`
125
+ <li class="pkg-comp" key=${c.componentId}>
126
+ <span class="pkg-comp-icon">${COMP_TYPE_META[c.type]?.icon || '\u{1F4E6}'}</span>
127
+ <span class="pkg-comp-id">${escHtml(c.componentId)}</span>
128
+ <span class="pkg-comp-type">${c.type}</span>
129
+ <span class="pkg-comp-status">
130
+ <span class="pkg-dot pkg-dot-active" title=${t('packages.statusActive') || 'Active'}></span>
131
+ ${c.customized ? html`<span class="pkg-comp-customized" title=${t('packages.customizedHint') || 'Customized after install'}>✏</span>` : ''}
132
+ </span>
133
+ <span class="pkg-comp-actions">${actionsFor(c)}</span>
134
+ </li>
135
+ `)}
136
+ </ul>
137
+ `}
138
+ <div class="pkg-card-footer">
139
+ <div class="pkg-card-actions">
140
+ <button class="btn-outline btn-sm" onClick=${onCheckUpdate}>${t('packages.checkUpdate') || 'Check Update'}</button>
141
+ <button class="btn-danger btn-sm" onClick=${onRemove}>${t('packages.remove') || 'Remove'}</button>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ `;
146
+ }
147
+
32
148
  export default function PackagesTab({ session, showToast, navigate, locale }) {
33
149
  const { confirm, ConfirmUI } = useConfirm();
34
150
  const [instances, setInstances] = useState([]);
@@ -186,6 +302,29 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
186
302
  }
187
303
  };
188
304
 
305
+ // Archive every version of a package group (own-author only, enforced server-side).
306
+ // Already-installed instances stay intact — only the package source is removed
307
+ // from the gallery + browse listings so it can no longer be re-installed.
308
+ const handleDeletePackage = (groupId, name) => {
309
+ confirm(
310
+ (t('packages.confirmDeletePackage') || 'Delete the package "{name}" and all its versions? Installed instances will keep working but no new installs will be possible.').replace('{name}', name || groupId),
311
+ async () => {
312
+ try {
313
+ const res = await pkgService.archivePackageGroup(groupId);
314
+ if (res.ok) {
315
+ showToast(t('packages.packageDeleted') || 'Package deleted');
316
+ loadData();
317
+ } else {
318
+ showToast(res.error || (t('packages.deleteFailed') || 'Delete failed'), true);
319
+ }
320
+ } catch (e) {
321
+ showToast(e.message, true);
322
+ }
323
+ },
324
+ { danger: true },
325
+ );
326
+ };
327
+
189
328
  const handleSyncFederation = async () => {
190
329
  setSyncing(true);
191
330
  try {
@@ -257,22 +396,14 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
257
396
  ` : html`
258
397
  <div class="pkg-grid">
259
398
  ${instances.map(inst => html`
260
- <div class="pkg-card" key=${inst.id}>
261
- <div class="pkg-card-header">
262
- <span class="pkg-card-name">${escHtml(inst.label || inst.packageGroupId)}</span>
263
- <span class="pkg-badge pkg-badge-${inst.status}">${inst.status}</span>
264
- </div>
265
- <div class="pkg-card-meta">
266
- <span class="pkg-card-version">${escHtml(inst.packageVersion)}</span>
267
- <span>\u00B7 ${inst.installedComponents?.length ?? 0} ${t('packages.components') || 'components'}</span>
268
- </div>
269
- <div class="pkg-card-footer">
270
- <div class="pkg-card-actions">
271
- <button class="btn-outline btn-sm" onClick=${() => handleCheckUpdate(inst.id)}>${t('packages.checkUpdate') || 'Check Update'}</button>
272
- <button class="btn-danger btn-sm" onClick=${() => handleRemove(inst.id)}>${t('packages.remove') || 'Remove'}</button>
273
- </div>
274
- </div>
275
- </div>
399
+ <${InstanceCard}
400
+ key=${inst.id}
401
+ inst=${inst}
402
+ session=${session}
403
+ onCheckUpdate=${() => handleCheckUpdate(inst.id)}
404
+ onRemove=${() => handleRemove(inst.id)}
405
+ navigate=${navigate}
406
+ />
276
407
  `)}
277
408
  </div>
278
409
  `}
@@ -341,6 +472,11 @@ export default function PackagesTab({ session, showToast, navigate, locale }) {
341
472
  ${t('packages.proposeAsTemplate') || 'Propose as Template'}
342
473
  </button>
343
474
  `}
475
+ ${pkg.author === session?.owner && html`
476
+ <button class="btn-danger btn-sm" onClick=${() => handleDeletePackage(pkg.packageGroupId, pkg.name)}>
477
+ ${t('packages.delete') || 'Delete'}
478
+ </button>
479
+ `}
344
480
  </div>
345
481
  </div>
346
482
  </div>
@@ -1,31 +1,98 @@
1
- /**
2
- * @file portfolio-tab.js
3
- * @description Profile tab providing navigation to the portfolio builder and public view.
4
- * @version-history
5
- * v1.0.0 — 2026-03-16 — Initial portfolio tab
6
- * v1.1.0 — 2026-03-17 — Replace inline styles with CSS classes
7
- */
8
- import { h } from 'preact';
9
- import htm from 'htm';
10
- const html = htm.bind(h);
11
- import { t } from '/js/i18n.js';
12
-
13
- export default function PortfolioTab({ session, navigate }) {
14
- return html`
15
- <div class="tab-content">
16
- <div class="portfolio-center">
17
- <h3>${t('portfolio.builder.heading')}</h3>
18
- <p class="portfolio-subtitle">${t('portfolio.builder.subtitle')}</p>
19
- <button class="btn-primary" onClick=${() => navigate('/v1/portfolio')}>
20
- ${t('portfolio.builder.heading')}
21
- </button>
22
- <br/><br/>
23
- ${session && html`
24
- <a href="/v1/portfolio/${encodeURIComponent(session.owner)}" class="btn-ghost" target="_blank">
25
- ${t('portfolio.builder.viewPublic')}
26
- </a>
27
- `}
28
- </div>
29
- </div>
30
- `;
31
- }
1
+ /**
2
+ * @file portfolio-tab.js
3
+ * @description Profile tab for the portfolio. NOT a landing page: with no published
4
+ * portfolio it forwards straight to the builder (an empty two-button page was a wasted
5
+ * click); with one published it earns its place — public URL with copy, last updated,
6
+ * and Visit / Edit / Unpublish.
7
+ * @version-history
8
+ * v2.0.0 — 2026-06-10 — Replace the two-button landing: auto-forward to the builder when
9
+ * nothing is published; published state shows URL + last updated + Visit/Edit/Unpublish.
10
+ * v1.1.0 — 2026-03-17 — Replace inline styles with CSS classes
11
+ * v1.0.0 — 2026-03-16 — Initial portfolio tab
12
+ */
13
+ import { h } from 'preact';
14
+ import { useState, useEffect } from 'preact/hooks';
15
+ import htm from 'htm';
16
+ const html = htm.bind(h);
17
+ import { t, getLocale } from '/js/i18n.js';
18
+
19
+ // t() echoes the key when a translation is missing (e.g. a server still serving
20
+ // pre-update locales) — fall back to readable English instead of raw keys.
21
+ const tr = (key, fallback) => { const v = t(key); return v && v !== key ? v : fallback; };
22
+ import { apiGet, apiPut } from '/js/api.js';
23
+ import { Spinner } from './shared.js';
24
+ import { CopyButton } from '/components/CopyButton.js';
25
+ import { useConfirm } from '/components/Modal.js';
26
+
27
+ export default function PortfolioTab({ session, navigate, showToast }) {
28
+ const { confirm, ConfirmUI } = useConfirm();
29
+ // undefined = loading, null = no config, object = config
30
+ const [cfg, setCfg] = useState(undefined);
31
+
32
+ const load = () => apiGet('/v1/portfolio/config')
33
+ .then(r => setCfg(r?.data?.config || null))
34
+ .catch(() => setCfg(null));
35
+ useEffect(() => { load(); }, []);
36
+
37
+ // No published portfolio → the builder IS the page. Forward instead of showing
38
+ // two buttons on 90% whitespace. CRITICAL: clear the profile's remembered-open-tab
39
+ // first — otherwise every later /v1/profile visit restores this tab, which forwards
40
+ // again, and the user can never reach the profile home (bounce loop).
41
+ useEffect(() => {
42
+ if (cfg !== undefined && !cfg?.enabled) {
43
+ try { sessionStorage.removeItem('aimeat-profile-tab'); } catch { /* noop */ }
44
+ navigate('/v1/portfolio');
45
+ }
46
+ }, [cfg]);
47
+
48
+ if (cfg === undefined) return html`<${Spinner} text=${t('loading') || 'Loading…'} />`;
49
+ if (!cfg?.enabled) return null; // forwarding to the builder
50
+
51
+ const url = `${window.location.origin}/v1/portfolio/${encodeURIComponent(session.owner)}`;
52
+
53
+ const handleUnpublish = () => {
54
+ confirm(tr('portfolio.builder.unpublishConfirm', 'Unpublish your portfolio? The public page stops working until you publish again.'), async () => {
55
+ try {
56
+ await apiPut('/v1/portfolio/config', { ...cfg, enabled: false, tags: ['portfolio'] });
57
+ showToast?.(tr('portfolio.builder.unpublished', 'Portfolio unpublished'));
58
+ navigate('/v1/portfolio');
59
+ } catch (e) { showToast?.(e.message || 'Failed', true); }
60
+ }, { danger: true });
61
+ };
62
+
63
+ return html`
64
+ <div class="tab-content">
65
+ <div class="section-title">${t('portfolio.builder.heading')}</div>
66
+ <div class="section-desc">${t('portfolio.builder.enabled')}</div>
67
+
68
+ <div class="card">
69
+ <div class="mem-item">
70
+ <span class="mem-key">${tr('portfolio.builder.publicUrl', 'Public URL')}</span>
71
+ <span class="access-copy-val">
72
+ <a href=${url} target="_blank">${url}</a>
73
+ <${CopyButton} text=${url} className="btn-ghost btn-sm" label="📋"
74
+ onCopied=${() => showToast?.(t('profile.access.copied') || 'Copied')} />
75
+ </span>
76
+ </div>
77
+ ${cfg.publishedAt && html`
78
+ <div class="mem-item">
79
+ <span class="mem-key">${tr('portfolio.builder.lastUpdated', 'Last updated')}</span>
80
+ <span>${new Date(cfg.publishedAt).toLocaleString(getLocale() === 'fi' ? 'fi-FI' : undefined)}</span>
81
+ </div>
82
+ `}
83
+ ${cfg.htmlSizeKb && html`
84
+ <div class="mem-item">
85
+ <span class="mem-key">${tr('portfolio.builder.sizeLabel', 'Size')}</span>
86
+ <span>${cfg.htmlSizeKb} KB</span>
87
+ </div>
88
+ `}
89
+ <div class="card-actions">
90
+ <a class="btn-outline btn-sm" href=${url} target="_blank">${tr('portfolio.builder.visitBtn', 'Visit')}</a>
91
+ <button class="btn-outline btn-sm" onClick=${() => navigate('/v1/portfolio')}>${tr('portfolio.builder.editBtn', 'Edit in builder')}</button>
92
+ <button class="btn-danger btn-sm" onClick=${handleUnpublish}>${tr('portfolio.builder.unpublish', 'Unpublish')}</button>
93
+ </div>
94
+ </div>
95
+ <${ConfirmUI} />
96
+ </div>
97
+ `;
98
+ }
@@ -58,6 +58,7 @@ import SecurityTab from './profile/security-tab.js';
58
58
  import EmailTab from './profile/email-tab.js';
59
59
  import KnowledgeTab from './profile/knowledge-tab.js';
60
60
  import OrganismsTab from './profile/organisms-tab.js';
61
+ import OffersTab from './profile/offers-tab.js';
61
62
  import NotificationsTab from './profile/notifications-tab.js';
62
63
  import GeneratorTab from './profile/generator-tab.js';
63
64
  import FoundryTab from './profile/foundry-tab.js';
@@ -72,6 +73,7 @@ import SchedulerTab from './profile/scheduler-tab.js';
72
73
  const TABS = [
73
74
  { id: 'portfolio', key: 'portfolio.tabLabel', component: PortfolioTab, minTier: 'active' },
74
75
  { id: 'agents', key: 'profile.tabs.agents', component: AgentsTab, minTier: 'active' },
76
+ { id: 'offers', key: 'profile.tabs.offers', component: OffersTab, minTier: 'active' },
75
77
  { id: 'scheduler', key: 'profile.tabs.scheduler', component: SchedulerTab, minTier: 'active' },
76
78
  { id: 'chatsessions', key: 'profile.tabs.chatSessions', component: ChatSessionsTab, minTier: 'active' },
77
79
  { id: 'mcp', key: 'profile.tabs.mcp', component: McpTab, minTier: 'active' },
@@ -0,0 +1,281 @@
1
+ /**
2
+ * @file start-flows.js
3
+ * @description Data + logic for the /v1/start diagnosis page: entrance configs
4
+ * (per LinkedIn-post angle, selected via ?from=), diagnostic questions with
5
+ * scores, verdict tiers, the build questions (path / tool / need / automation)
6
+ * and the playbook builder that turns the answers into a checklist with real,
7
+ * verified attach commands (sourced from connect.html — do not invent new ones).
8
+ * All user-visible copy is i18n key + English fallback pairs `{ k, f }`,
9
+ * resolved by the view; Finnish lives in locales/fi.json.
10
+ * @structure ENTRANCES, BUILD_QUESTIONS, scoreAnswers(), tierFor(),
11
+ * buildPlaybook(), playbookToMarkdown()
12
+ * @usage import { ENTRANCES, ... } from './start-flows.js' (relative — no importmap entry)
13
+ * @version-history
14
+ * v1.0.0 — 2026-06-11 — Initial: jira + default entrances, 4 verdict tiers,
15
+ * hosted/self/help playbooks (Spider rules: diagnosis framing, shareable
16
+ * verdict, ends in a copyable prompt, no email gate).
17
+ */
18
+
19
+ // ── Diagnostic questions (shared by jira + default entrances) ──
20
+
21
+ const Q_IDEA_FATE = {
22
+ id: 'ideaFate',
23
+ title: { k: 'start.q1.title', f: 'When someone drops a good idea, what happens to it?' },
24
+ options: [
25
+ { id: 'prod', score: 3, text: { k: 'start.q1.prod', f: "It's in production before the evening news" } },
26
+ { id: 'backlog', score: 1, text: { k: 'start.q1.backlog', f: 'Into the backlog. Groomed. Maybe Q3.' } },
27
+ { id: 'grave', score: 0, text: { k: 'start.q1.grave', f: "It's taken to JIRA to die. Ceremonies twice a week." } },
28
+ { id: 'chaos', score: 0, text: { k: 'start.q1.chaos', f: 'Nowhere. Someone might remember it next week.' } },
29
+ ],
30
+ };
31
+
32
+ const Q_AI_MEMORY = {
33
+ id: 'aiMemory',
34
+ title: { k: 'start.q2.title', f: 'What does your AI remember about yesterday?' },
35
+ options: [
36
+ { id: 'all', score: 3, text: { k: 'start.q2.all', f: 'Everything: it has a memory, a task queue and a work history' } },
37
+ { id: 'paste', score: 1, text: { k: 'start.q2.paste', f: 'Whatever I bother to copy-paste into the chat' } },
38
+ { id: 'none', score: 0, text: { k: 'start.q2.none', f: 'Nothing. I reintroduce myself to it every morning.' } },
39
+ ],
40
+ };
41
+
42
+ const Q_AGENT_HOME = {
43
+ id: 'agentHome',
44
+ title: { k: 'start.q3.title', f: "Where does your AI's work live?" },
45
+ options: [
46
+ { id: 'home', score: 3, text: { k: 'start.q3.home', f: 'In a shared home where the team and other agents can find it' } },
47
+ { id: 'disk', score: 1, text: { k: 'start.q3.disk', f: 'On my machine, in a folder called final_v3_REAL' } },
48
+ { id: 'scattered', score: 0, text: { k: 'start.q3.scattered', f: 'Scattered across chat histories. Nobody digs those up.' } },
49
+ ],
50
+ };
51
+
52
+ // ── Entrances — one per LinkedIn-post angle (?from=jira|sotku|omistus).
53
+ // sotku/omistus copy is not written yet; they fall back to the neutral
54
+ // entrance so the links can ship before their entrance copy does. ──
55
+
56
+ export const ENTRANCES = {
57
+ jira: {
58
+ id: 'jira',
59
+ title: { k: 'start.jira.title', f: 'How dead is your JIRA?' },
60
+ sub: { k: 'start.jira.sub', f: 'The diagnosis takes 90 seconds. No email gate, no form, no sales call. At the end you get a verdict and a playbook for trying the cure yourself.' },
61
+ questions: [Q_IDEA_FATE, Q_AI_MEMORY, Q_AGENT_HOME],
62
+ },
63
+ default: {
64
+ id: 'default',
65
+ title: { k: 'start.def.title', f: 'What shape are your agents in?' },
66
+ sub: { k: 'start.def.sub', f: 'The diagnosis takes 90 seconds. No email gate, no form, no sales call. At the end you get a verdict and a playbook for trying the cure yourself.' },
67
+ questions: [Q_IDEA_FATE, Q_AI_MEMORY, Q_AGENT_HOME],
68
+ },
69
+ };
70
+
71
+ export function entranceFor(slug) {
72
+ return ENTRANCES[slug] || ENTRANCES.default;
73
+ }
74
+
75
+ // ── Verdict tiers (max score 9) ──
76
+
77
+ export const TIERS = [
78
+ { id: 't0', min: 0, name: { k: 'start.t0.name', f: 'Level 0: The Graveyard' },
79
+ desc: { k: 'start.t0.desc', f: 'Your ideas die ceremonially and your AI has amnesia. You pay for tools that remember nothing and a process that ships nothing. Good news: the only way is up, and the climb starts below.' } },
80
+ { id: 't1', min: 3, name: { k: 'start.t1.name', f: 'Level 1: In Therapy' },
81
+ desc: { k: 'start.t1.desc', f: 'Your AI goes to therapy, not to work: you carry its context in every morning and it forgets everything by night. You hired a brilliant employee and gave it amnesia.' } },
82
+ { id: 't2', min: 6, name: { k: 'start.t2.name', f: 'Level 2: Working Without a Manager' },
83
+ desc: { k: 'start.t2.desc', f: 'You have agents and they do things, but nothing sticks and nobody runs the whole. Missing: a home, a memory and a manager.' } },
84
+ { id: 't3', min: 8, name: { k: 'start.t3.name', f: 'Level 3: Goes to Work' },
85
+ desc: { k: 'start.t3.desc', f: 'Your agents actually go to work. Rare. Congratulations. Your playbook shows how to scale this and own the whole chain yourself.' } },
86
+ ];
87
+
88
+ export function scoreAnswers(questions, answers) {
89
+ let sum = 0;
90
+ for (const q of questions) {
91
+ const opt = q.options.find((o) => o.id === answers[q.id]);
92
+ if (opt) sum += opt.score;
93
+ }
94
+ return sum;
95
+ }
96
+
97
+ export function tierFor(score) {
98
+ let tier = TIERS[0];
99
+ for (const t of TIERS) if (score >= t.min) tier = t;
100
+ return tier;
101
+ }
102
+
103
+ // ── Build questions (asked after the verdict; they become the playbook) ──
104
+
105
+ export const Q_PATH = {
106
+ id: 'path',
107
+ title: { k: 'start.q4.title', f: 'How do you want to try this?' },
108
+ options: [
109
+ { id: 'hosted', text: { k: 'start.q4.hosted', f: 'Try it first: hosted aimeat.io, nothing to install' } },
110
+ { id: 'self', text: { k: 'start.q4.self', f: "I'll build it myself: my own node on my own hardware (MIT, free)" } },
111
+ { id: 'help', text: { k: 'start.q4.help', f: 'I want help: you do it ($$)' } },
112
+ ],
113
+ };
114
+
115
+ export const Q_TOOL = {
116
+ id: 'tool',
117
+ title: { k: 'start.q5.title', f: 'What tool does your AI live in?' },
118
+ options: [
119
+ { id: 'claude', text: { k: 'start.q5.claude', f: 'Claude (web or desktop)' } },
120
+ { id: 'claudeCode', text: { k: 'start.q5.claudeCode', f: 'Claude Code (terminal)' } },
121
+ { id: 'copilot', text: { k: 'start.q5.copilot', f: 'VS Code + Copilot' } },
122
+ { id: 'cursor', text: { k: 'start.q5.cursor', f: 'Cursor' } },
123
+ { id: 'otherChat', text: { k: 'start.q5.otherChat', f: 'Some other chat (ChatGPT, Gemini, …)' } },
124
+ ],
125
+ };
126
+
127
+ export const Q_NEED = {
128
+ id: 'need',
129
+ title: { k: 'start.q5n.title', f: 'What would you want agents to handle first?' },
130
+ options: [
131
+ { id: 'code', text: { k: 'start.q5n.code', f: 'Code and PRs' } },
132
+ { id: 'docs', text: { k: 'start.q5n.docs', f: 'Documentation and a home for knowledge' } },
133
+ { id: 'watch', text: { k: 'start.q5n.watch', f: "Monitoring and reporting: what's happening, every morning" } },
134
+ { id: 'unsure', text: { k: 'start.q5n.unsure', f: "I don't know yet, that's why I'm asking" } },
135
+ ],
136
+ };
137
+
138
+ export const Q_AUTOMATION = {
139
+ id: 'automation',
140
+ title: { k: 'start.q6.title', f: 'Does the work happen by itself, or when you say so?' },
141
+ options: [
142
+ { id: 'manual', text: { k: 'start.q6.manual', f: 'When I say so: the AI helps when I ask' } },
143
+ { id: 'routine', text: { k: 'start.q6.routine', f: 'An agreed routine: the AI checks the queue regularly' } },
144
+ { id: 'continuous', text: { k: 'start.q6.continuous', f: 'By itself: the AI works while I sleep' } },
145
+ ],
146
+ };
147
+
148
+ /** The build-phase question sequence for a chosen path. The help path skips
149
+ * the automation question — its playbook never uses the answer, and we don't
150
+ * ask what we don't use. */
151
+ export function buildQuestionsFor(answers) {
152
+ if (answers.path === 'help') return [Q_PATH, Q_NEED];
153
+ return [Q_PATH, Q_TOOL, Q_AUTOMATION];
154
+ }
155
+
156
+ // ── Playbook builder ──
157
+ // Returns sections: { title: {k,f}, items: [{ text?: {k,f}, code?: string,
158
+ // prompt?: {k,f}, link?: { href, label: {k,f} } }] }.
159
+ // Commands come from connect.html — keep them verbatim, do not invent.
160
+
161
+ export function buildPlaybook(answers, mcpUrl) {
162
+ const { path, tool, need, automation } = answers;
163
+ const sections = [];
164
+
165
+ if (path === 'help') {
166
+ const needText = { k: `start.q5n.${need}`, f: '' };
167
+ sections.push({
168
+ title: { k: 'start.pb.h1.title', f: 'Step 1: Tell us what you need' },
169
+ items: [
170
+ { text: { k: 'start.pb.h1.a', f: "Send a message to jouni.miikki@aimeat.io. Subject 'Agents', body: your pick below. You'll get an honest assessment: what to build yourself, what to buy." } },
171
+ { text: needText, quote: true },
172
+ ],
173
+ });
174
+ sections.push({
175
+ title: { k: 'start.pb.h2.title', f: 'Step 2: See the pricing' },
176
+ items: [
177
+ { text: { k: 'start.pb.h2.a', f: 'Self-serve tiers and done-for-you packages:' },
178
+ link: { href: '/v1/pricing', label: { k: 'start.pb.h2.link', f: 'See pricing →' } } },
179
+ ],
180
+ });
181
+ sections.push({
182
+ title: { k: 'start.pb.h3.title', f: 'Step 3: Try it meanwhile' },
183
+ items: [
184
+ { text: { k: 'start.pb.h3.a', f: "Nothing stops you from seeing it yourself before paying anything: create a free account and connect your own AI." },
185
+ link: { href: '/v1/portal', label: { k: 'start.pb.h3.link', f: 'Create a free account →' } } },
186
+ ],
187
+ });
188
+ return sections;
189
+ }
190
+
191
+ // Step 1 — account / own node
192
+ if (path === 'self') {
193
+ sections.push({
194
+ title: { k: 'start.pb.s1self.title', f: 'Step 1: Install your own node' },
195
+ items: [
196
+ { text: { k: 'start.pb.s1self.a', f: 'Clone and run the init wizard, it asks the rest:' },
197
+ code: 'git clone https://github.com/miikkij/aimeat-protocol\ncd aimeat-protocol\npnpm install && pnpm aimeat init' },
198
+ { text: { k: 'start.pb.s1self.b', f: 'Everything below is then done against your own node: replace the aimeat.io address with yours. Your hardware, your keys, your data.' } },
199
+ ],
200
+ });
201
+ } else {
202
+ sections.push({
203
+ title: { k: 'start.pb.s1.title', f: 'Step 1: An account and a home' },
204
+ items: [
205
+ { text: { k: 'start.pb.s1.a', f: "Create a free account. No card, no payment. The account is your agent's home and your permission for it to exist." },
206
+ link: { href: '/v1/portal', label: { k: 'start.pb.s1.link', f: 'Create account →' } } },
207
+ ],
208
+ });
209
+ }
210
+
211
+ // Step 2 — connect the AI (per tool, commands verbatim from connect.html)
212
+ const connect = { title: { k: 'start.pb.s2.title', f: 'Step 2: Connect your AI' }, items: [] };
213
+ if (tool === 'claude') {
214
+ connect.items.push({ text: { k: 'start.pb.s2.claude', f: 'Open Claude: Settings → Connectors → Add custom connector, and paste this address:' }, code: mcpUrl });
215
+ connect.items.push({ text: { k: 'start.pb.s2.claudeNote', f: 'The free claude.ai tier fits one custom connector, and that is enough for this.' } });
216
+ } else if (tool === 'claudeCode') {
217
+ connect.items.push({ text: { k: 'start.pb.s2.cli', f: 'Run this in a terminal:' }, code: `claude mcp add aimeat --transport http ${mcpUrl}` });
218
+ } else if (tool === 'copilot') {
219
+ connect.items.push({ text: { k: 'start.pb.s2.cli', f: 'Run this in a terminal:' }, code: `code --add-mcp '{"name":"aimeat","url":"${mcpUrl}"}'` });
220
+ } else if (tool === 'cursor') {
221
+ connect.items.push({ text: { k: 'start.pb.s2.cursor', f: 'Use the 1-click install on the connect page:' },
222
+ link: { href: '/v1/connect', label: { k: 'start.pb.s2.cursorLink', f: 'Open the connect page →' } } });
223
+ } else {
224
+ connect.items.push({ text: { k: 'start.pb.s2.other', f: 'If your tool supports Connectors / custom MCP, paste this address into it:' }, code: mcpUrl });
225
+ connect.items.push({ text: { k: 'start.pb.s2.otherNote', f: "No MCP support? AIMEAT also works prompt-driven: the app composes the prompts, you carry them to your chat and bring the answers back. Free, and works with any AI." } });
226
+ }
227
+ connect.items.push({ text: { k: 'start.pb.s2.approve', f: 'When the AI asks for access, approve the agent in your profile and pick its permissions. No agent is ever created without your approval.' } });
228
+ sections.push(connect);
229
+
230
+ // Step 3 — startup prompt (the conversion: the AI creates the first organism)
231
+ sections.push({
232
+ title: { k: 'start.pb.s3.title', f: 'Step 3: Put your AI to work' },
233
+ items: [
234
+ { text: { k: 'start.pb.s3.lead', f: 'Copy this prompt to your AI:' },
235
+ prompt: { k: 'start.pb.s3.prompt', f: "You are connected to the AIMEAT platform via MCP. Create an organism for me called 'My Office' with a workspace. Then read what's there, ask me what I want you to do, and record my answer as the first task in the workspace. Confirm when the task is saved." } },
236
+ { text: { k: 'start.pb.s3.check', f: 'Check your profile: the task appeared. You just watched the whole loop: idea → memory → task queue. No ceremonies.' } },
237
+ ],
238
+ });
239
+
240
+ // Step 4 — first real job
241
+ sections.push({
242
+ title: { k: 'start.pb.s4.title', f: 'Step 4: The first real job' },
243
+ items: [
244
+ { text: { k: 'start.pb.s4.a', f: 'Give the AI one small, real task from your day: file a note, turn a project status into a document, sort a list.' } },
245
+ { text: { k: 'start.pb.s4.b', f: 'Everything the AI writes stays with you. The memory, the documents and the tasks are yours, not an AI vendor’s.' } },
246
+ ],
247
+ });
248
+
249
+ // Step 5 — automation level
250
+ const auto = { title: { k: 'start.pb.s5.title', f: 'Step 5: Automation level' }, items: [] };
251
+ if (automation === 'continuous') {
252
+ auto.items.push({ text: { k: 'start.pb.s5.continuous', f: 'You picked: by itself. A connector serve daemon runs your agent in the background and tasks arrive as pushes, no polling. A fleet of 33 agents runs in production on this model.' },
253
+ link: { href: '/v1/connect', label: { k: 'start.pb.s5.link', f: 'Connector docs →' } } });
254
+ } else if (automation === 'routine') {
255
+ auto.items.push({ text: { k: 'start.pb.s5.routine', f: 'You picked: an agreed routine. Agree a scheduled check with your AI that reads the task queue regularly. AIMEAT’s own schedules work as the wake-up call and reminder.' } });
256
+ } else {
257
+ auto.items.push({ text: { k: 'start.pb.s5.manual', f: "You picked: when I say so. Start every session with the prompt: 'Read the open tasks in the workspace and handle them.' You are the scheduler, and that is enough to start." } });
258
+ }
259
+ if (automation !== 'continuous') {
260
+ auto.items.push({ text: { k: 'start.pb.s5.upgrade', f: 'You can raise the level any time: by hand → routine → daemon. The playbook does not expire.' } });
261
+ }
262
+ sections.push(auto);
263
+
264
+ return sections;
265
+ }
266
+
267
+ /** Plain-markdown rendering of the playbook for the copy button. */
268
+ export function playbookToMarkdown(sections, trr, titleText) {
269
+ const lines = [`# ${titleText}`, ''];
270
+ for (const s of sections) {
271
+ lines.push(`## ${trr(s.title)}`, '');
272
+ for (const it of s.items) {
273
+ if (it.text) lines.push(it.quote ? `> ${trr(it.text)}` : `- [ ] ${trr(it.text)}`);
274
+ if (it.code) lines.push('', '```', it.code, '```', '');
275
+ if (it.prompt) lines.push('', '> ' + trr(it.prompt), '');
276
+ if (it.link) lines.push(` ${trr(it.link.label)} ${it.link.href}`);
277
+ }
278
+ lines.push('');
279
+ }
280
+ return lines.join('\n');
281
+ }