aimeat 1.33.2 → 1.35.0
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/dist/locales/en.json +437 -15
- package/dist/locales/fi.json +437 -15
- package/dist/public/cortex-bundled/aimeat-charts.js +3 -3
- package/dist/public/cortex-bundled/aimeat-charts.yaml +3 -3
- package/dist/public/cortex-bundled/aimeat-i18n.js +123 -0
- package/dist/public/cortex-bundled/aimeat-i18n.yaml +64 -0
- package/dist/public/css/views/landing.css +2 -0
- package/dist/public/css/views/secretary.css +464 -2
- package/dist/public/js/i18n.js +9 -0
- package/dist/public/js/services/apps.js +4 -2
- package/dist/public/js/services/memory.js +9 -3
- package/dist/public/js/services/offers.js +1 -1
- package/dist/public/js/services/organisms.js +49 -17
- package/dist/public/js/services/secretary-helpers.js +266 -17
- package/dist/public/js/services/secretary-policy.js +28 -2
- package/dist/public/js/services/secretary-rules.js +70 -0
- package/dist/public/js/theme.js +20 -0
- package/dist/public/lib/VENDORED-STYLING.md +31 -0
- package/dist/public/lib/aimeat-daisyui-bridge.css +48 -0
- package/dist/public/lib/chartjs@4.js +14 -0
- package/dist/public/lib/daisyui@5.css +7 -0
- package/dist/public/lib/tailwindcss@4.js +8 -0
- package/dist/public/llms-template.txt +9 -6
- package/dist/public/showroom.html +104 -0
- package/dist/public/showroom.js +247 -0
- package/dist/public/spa.html +18 -16
- package/dist/public/views/aimeat-os.js +3 -2
- package/dist/public/views/landing.js +50 -6
- package/dist/public/views/profile/agents/agent-card.js +13 -4
- package/dist/public/views/profile/agents/state-detector.js +6 -0
- package/dist/public/views/profile/agents/tab-integration.js +11 -2
- package/dist/public/views/profile/apps-tab.js +46 -0
- package/dist/public/views/profile/extensions-tab.js +4 -0
- package/dist/public/views/profile/inbox-tab.js +1 -1
- package/dist/public/views/profile/landing-page.js +8 -3
- package/dist/public/views/profile/memory-tab.js +37 -6
- package/dist/public/views/profile/organisms/home.js +32 -0
- package/dist/public/views/profile/organisms/workspace-list.js +36 -5
- package/dist/public/views/profile/organisms/workspace.js +29 -4
- package/dist/public/views/profile/organisms-tab.js +61 -8
- package/dist/public/views/secretary/calendar.js +327 -0
- package/dist/public/views/secretary/cards-reach.js +16 -17
- package/dist/public/views/secretary/cards.js +217 -79
- package/dist/public/views/secretary/dashboard.js +617 -0
- package/dist/public/views/secretary/quality.js +156 -0
- package/dist/public/views/secretary/use-autonomy.js +17 -2
- package/dist/public/views/secretary/use-freshness.js +50 -0
- package/dist/public/views/secretary/use-intake.js +23 -4
- package/dist/public/views/secretary/use-layout.js +122 -0
- package/dist/public/views/secretary/use-quick-actions.js +98 -0
- package/dist/public/views/secretary/use-specialists.js +103 -0
- package/dist/public/views/secretary/use-triggers.js +159 -0
- package/dist/public/views/secretary/use-whats-next.js +340 -0
- package/dist/public/views/secretary/workflow-design.js +112 -0
- package/dist/public/views/secretary.js +836 -44
- package/dist/scripts/check-no-max-tokens.d.ts +2 -0
- package/dist/scripts/check-no-max-tokens.d.ts.map +1 -0
- package/dist/scripts/check-no-max-tokens.js +71 -0
- package/dist/scripts/check-no-max-tokens.js.map +1 -0
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.js +9 -1
- package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
- package/dist/src/cli/connect/onboarding-prompt.d.ts +3 -0
- package/dist/src/cli/connect/onboarding-prompt.d.ts.map +1 -1
- package/dist/src/cli/connect/onboarding-prompt.js +10 -3
- package/dist/src/cli/connect/onboarding-prompt.js.map +1 -1
- package/dist/src/data/aimeat-iam-package.d.ts +25 -0
- package/dist/src/data/aimeat-iam-package.d.ts.map +1 -0
- package/dist/src/data/aimeat-iam-package.js +312 -0
- package/dist/src/data/aimeat-iam-package.js.map +1 -0
- package/dist/src/data/aimeat-marketplace-package.d.ts +25 -0
- package/dist/src/data/aimeat-marketplace-package.d.ts.map +1 -0
- package/dist/src/data/aimeat-marketplace-package.js +231 -0
- package/dist/src/data/aimeat-marketplace-package.js.map +1 -0
- package/dist/src/data/app-templates.d.ts +48 -0
- package/dist/src/data/app-templates.d.ts.map +1 -0
- package/dist/src/data/app-templates.js +856 -0
- package/dist/src/data/app-templates.js.map +1 -0
- package/dist/src/data/example-packages.d.ts.map +1 -1
- package/dist/src/data/example-packages.js +3 -1
- package/dist/src/data/example-packages.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +689 -7
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/generated/prisma-postgres/edge.js +10 -3
- package/dist/src/generated/prisma-postgres/index-browser.js +7 -0
- package/dist/src/generated/prisma-postgres/index.d.ts +205 -2
- package/dist/src/generated/prisma-postgres/index.js +10 -3
- package/dist/src/generated/prisma-postgres/package.json +1 -1
- package/dist/src/generated/prisma-postgres/schema.prisma +23 -13
- package/dist/src/generated/prisma-postgres/wasm.js +10 -3
- package/dist/src/mcp/agent-onboarding.d.ts +4 -0
- package/dist/src/mcp/agent-onboarding.d.ts.map +1 -1
- package/dist/src/mcp/agent-onboarding.js +28 -2
- package/dist/src/mcp/agent-onboarding.js.map +1 -1
- package/dist/src/mcp/annotations.d.ts.map +1 -1
- package/dist/src/mcp/annotations.js +1 -0
- package/dist/src/mcp/annotations.js.map +1 -1
- package/dist/src/mcp/catalog/definitions.d.ts.map +1 -1
- package/dist/src/mcp/catalog/definitions.js +16 -1
- package/dist/src/mcp/catalog/definitions.js.map +1 -1
- package/dist/src/mcp/catalog/surfaces.js +3 -3
- package/dist/src/mcp/catalog/surfaces.js.map +1 -1
- package/dist/src/mcp/index.d.ts +3 -0
- package/dist/src/mcp/index.d.ts.map +1 -1
- package/dist/src/mcp/index.js +11 -0
- package/dist/src/mcp/index.js.map +1 -1
- package/dist/src/mcp/organisms.d.ts.map +1 -1
- package/dist/src/mcp/organisms.js +48 -8
- package/dist/src/mcp/organisms.js.map +1 -1
- package/dist/src/mcp/workspaces.d.ts.map +1 -1
- package/dist/src/mcp/workspaces.js +6 -4
- package/dist/src/mcp/workspaces.js.map +1 -1
- package/dist/src/models/agent-onboarding-schemas.d.ts +47 -0
- package/dist/src/models/agent-onboarding-schemas.d.ts.map +1 -1
- package/dist/src/models/agent-onboarding-schemas.js +117 -0
- package/dist/src/models/agent-onboarding-schemas.js.map +1 -1
- package/dist/src/models/offer-schemas.d.ts +8 -8
- package/dist/src/routes/agent-onboarding.d.ts +5 -0
- package/dist/src/routes/agent-onboarding.d.ts.map +1 -1
- package/dist/src/routes/agent-onboarding.js +21 -2
- package/dist/src/routes/agent-onboarding.js.map +1 -1
- package/dist/src/routes/ai.d.ts +8 -4
- package/dist/src/routes/ai.d.ts.map +1 -1
- package/dist/src/routes/ai.js +38 -14
- package/dist/src/routes/ai.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts +2 -0
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +7 -1
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/app-templates.d.ts +16 -0
- package/dist/src/routes/app-templates.d.ts.map +1 -0
- package/dist/src/routes/app-templates.js +40 -0
- package/dist/src/routes/app-templates.js.map +1 -0
- package/dist/src/routes/apps.d.ts.map +1 -1
- package/dist/src/routes/apps.js +55 -5
- package/dist/src/routes/apps.js.map +1 -1
- package/dist/src/routes/auth.d.ts +3 -0
- package/dist/src/routes/auth.d.ts.map +1 -1
- package/dist/src/routes/auth.js +9 -4
- package/dist/src/routes/auth.js.map +1 -1
- package/dist/src/routes/bootstrap.d.ts.map +1 -1
- package/dist/src/routes/bootstrap.js +1 -0
- package/dist/src/routes/bootstrap.js.map +1 -1
- package/dist/src/routes/generator.d.ts.map +1 -1
- package/dist/src/routes/generator.js +3 -2
- package/dist/src/routes/generator.js.map +1 -1
- package/dist/src/routes/lib-ai.d.ts +0 -1
- package/dist/src/routes/lib-ai.d.ts.map +1 -1
- package/dist/src/routes/lib-ai.js +13 -6
- package/dist/src/routes/lib-ai.js.map +1 -1
- package/dist/src/routes/lib-header.d.ts +2 -0
- package/dist/src/routes/lib-header.d.ts.map +1 -1
- package/dist/src/routes/lib-header.js +7 -16
- package/dist/src/routes/lib-header.js.map +1 -1
- package/dist/src/routes/lib-markdown.d.ts +18 -0
- package/dist/src/routes/lib-markdown.d.ts.map +1 -0
- package/dist/src/routes/lib-markdown.js +236 -0
- package/dist/src/routes/lib-markdown.js.map +1 -0
- package/dist/src/routes/libs.d.ts +22 -0
- package/dist/src/routes/libs.d.ts.map +1 -1
- package/dist/src/routes/libs.js +133 -10
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/memory.d.ts.map +1 -1
- package/dist/src/routes/memory.js +21 -2
- package/dist/src/routes/memory.js.map +1 -1
- package/dist/src/routes/openrouter.d.ts +4 -0
- package/dist/src/routes/openrouter.d.ts.map +1 -1
- package/dist/src/routes/openrouter.js +17 -0
- package/dist/src/routes/openrouter.js.map +1 -1
- package/dist/src/routes/organisms.d.ts.map +1 -1
- package/dist/src/routes/organisms.js +107 -7
- package/dist/src/routes/organisms.js.map +1 -1
- package/dist/src/routes/portal.d.ts +9 -0
- package/dist/src/routes/portal.d.ts.map +1 -1
- package/dist/src/routes/portal.js +1 -1
- package/dist/src/routes/portal.js.map +1 -1
- package/dist/src/routes/public-stats.d.ts +3 -0
- package/dist/src/routes/public-stats.d.ts.map +1 -1
- package/dist/src/routes/public-stats.js +9 -2
- package/dist/src/routes/public-stats.js.map +1 -1
- package/dist/src/routes/secretary.d.ts +34 -0
- package/dist/src/routes/secretary.d.ts.map +1 -0
- package/dist/src/routes/secretary.js +311 -0
- package/dist/src/routes/secretary.js.map +1 -0
- package/dist/src/routes/specialists.d.ts.map +1 -1
- package/dist/src/routes/specialists.js +21 -0
- package/dist/src/routes/specialists.js.map +1 -1
- package/dist/src/routes/subdomains.js +1 -1
- package/dist/src/routes/subdomains.js.map +1 -1
- package/dist/src/routes/workflows.d.ts +9 -3
- package/dist/src/routes/workflows.d.ts.map +1 -1
- package/dist/src/routes/workflows.js +9 -3
- package/dist/src/routes/workflows.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +4 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/server-bootstrap/service-init.d.ts.map +1 -1
- package/dist/src/server-bootstrap/service-init.js +7 -0
- package/dist/src/server-bootstrap/service-init.js.map +1 -1
- package/dist/src/server-bootstrap/static-files.d.ts +4 -0
- package/dist/src/server-bootstrap/static-files.d.ts.map +1 -1
- package/dist/src/server-bootstrap/static-files.js +18 -0
- package/dist/src/server-bootstrap/static-files.js.map +1 -1
- package/dist/src/services/archive.d.ts +74 -0
- package/dist/src/services/archive.d.ts.map +1 -0
- package/dist/src/services/archive.js +132 -0
- package/dist/src/services/archive.js.map +1 -0
- package/dist/src/services/cortex-seeder.d.ts +4 -1
- package/dist/src/services/cortex-seeder.d.ts.map +1 -1
- package/dist/src/services/cortex-seeder.js +25 -6
- package/dist/src/services/cortex-seeder.js.map +1 -1
- package/dist/src/services/directory.d.ts.map +1 -1
- package/dist/src/services/directory.js +3 -2
- package/dist/src/services/directory.js.map +1 -1
- package/dist/src/services/generator-prompt-seeds.d.ts.map +1 -1
- package/dist/src/services/generator-prompt-seeds.js +5 -4
- package/dist/src/services/generator-prompt-seeds.js.map +1 -1
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +14 -0
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/onboarding-guide.d.ts +53 -0
- package/dist/src/services/onboarding-guide.d.ts.map +1 -0
- package/dist/src/services/onboarding-guide.js +81 -0
- package/dist/src/services/onboarding-guide.js.map +1 -0
- package/dist/src/services/organism-search.d.ts +6 -2
- package/dist/src/services/organism-search.d.ts.map +1 -1
- package/dist/src/services/organism-search.js +2 -2
- package/dist/src/services/organism-search.js.map +1 -1
- package/dist/src/services/owner-memory.d.ts +1 -0
- package/dist/src/services/owner-memory.d.ts.map +1 -1
- package/dist/src/services/owner-memory.js.map +1 -1
- package/dist/src/services/owner-session.d.ts +1 -0
- package/dist/src/services/owner-session.d.ts.map +1 -1
- package/dist/src/services/owner-session.js +10 -2
- package/dist/src/services/owner-session.js.map +1 -1
- package/dist/src/services/package-seeder.d.ts +22 -0
- package/dist/src/services/package-seeder.d.ts.map +1 -0
- package/dist/src/services/package-seeder.js +30 -0
- package/dist/src/services/package-seeder.js.map +1 -0
- package/dist/src/services/presence.d.ts +3 -0
- package/dist/src/services/presence.d.ts.map +1 -1
- package/dist/src/services/presence.js +10 -0
- package/dist/src/services/presence.js.map +1 -1
- package/dist/src/services/prompt-defaults.d.ts.map +1 -1
- package/dist/src/services/prompt-defaults.js +8 -3
- package/dist/src/services/prompt-defaults.js.map +1 -1
- package/dist/src/services/scheduler.d.ts +50 -2
- package/dist/src/services/scheduler.d.ts.map +1 -1
- package/dist/src/services/scheduler.js +454 -26
- package/dist/src/services/scheduler.js.map +1 -1
- package/dist/src/services/secretary-authoring.d.ts +71 -0
- package/dist/src/services/secretary-authoring.d.ts.map +1 -0
- package/dist/src/services/secretary-authoring.js +336 -0
- package/dist/src/services/secretary-authoring.js.map +1 -0
- package/dist/src/services/secretary-clarify.d.ts +57 -0
- package/dist/src/services/secretary-clarify.d.ts.map +1 -0
- package/dist/src/services/secretary-clarify.js +104 -0
- package/dist/src/services/secretary-clarify.js.map +1 -0
- package/dist/src/services/secretary-feed.d.ts +11 -0
- package/dist/src/services/secretary-feed.d.ts.map +1 -0
- package/dist/src/services/secretary-feed.js +28 -0
- package/dist/src/services/secretary-feed.js.map +1 -0
- package/dist/src/services/secretary-tick.d.ts +243 -3
- package/dist/src/services/secretary-tick.d.ts.map +1 -1
- package/dist/src/services/secretary-tick.js +273 -3
- package/dist/src/services/secretary-tick.js.map +1 -1
- package/dist/src/services/secretary-workbrief.d.ts +144 -0
- package/dist/src/services/secretary-workbrief.d.ts.map +1 -0
- package/dist/src/services/secretary-workbrief.js +315 -0
- package/dist/src/services/secretary-workbrief.js.map +1 -0
- package/dist/src/services/secretary-worktier.d.ts +118 -0
- package/dist/src/services/secretary-worktier.d.ts.map +1 -0
- package/dist/src/services/secretary-worktier.js +157 -0
- package/dist/src/services/secretary-worktier.js.map +1 -0
- package/dist/src/services/skill-bundle/generator.d.ts +3 -0
- package/dist/src/services/skill-bundle/generator.d.ts.map +1 -1
- package/dist/src/services/skill-bundle/generator.js +19 -0
- package/dist/src/services/skill-bundle/generator.js.map +1 -1
- package/dist/src/services/specialist-runner.d.ts +36 -0
- package/dist/src/services/specialist-runner.d.ts.map +1 -0
- package/dist/src/services/specialist-runner.js +91 -0
- package/dist/src/services/specialist-runner.js.map +1 -0
- package/dist/src/services/specialist.d.ts +9 -0
- package/dist/src/services/specialist.d.ts.map +1 -1
- package/dist/src/services/specialist.js +42 -0
- package/dist/src/services/specialist.js.map +1 -1
- package/dist/src/services/structure-overview.d.ts +13 -0
- package/dist/src/services/structure-overview.d.ts.map +1 -1
- package/dist/src/services/structure-overview.js +34 -11
- package/dist/src/services/structure-overview.js.map +1 -1
- package/dist/src/services/system-agent-responder.d.ts +36 -0
- package/dist/src/services/system-agent-responder.d.ts.map +1 -0
- package/dist/src/services/system-agent-responder.js +107 -0
- package/dist/src/services/system-agent-responder.js.map +1 -0
- package/dist/src/services/system-agent.d.ts +34 -1
- package/dist/src/services/system-agent.d.ts.map +1 -1
- package/dist/src/services/system-agent.js +73 -0
- package/dist/src/services/system-agent.js.map +1 -1
- package/dist/src/storage/interface.d.ts +33 -0
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/interface.js.map +1 -1
- package/dist/src/storage/providers/mongodb/index.d.ts +37 -0
- package/dist/src/storage/providers/mongodb/index.d.ts.map +1 -1
- package/dist/src/storage/providers/mongodb/index.js +132 -12
- package/dist/src/storage/providers/mongodb/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/index.d.ts +23 -1
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +60 -8
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts +29 -1
- package/dist/src/storage/providers/sqlite/repos/memory.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/repos/memory.js +109 -10
- package/dist/src/storage/providers/sqlite/repos/memory.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts +3 -0
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +64 -9
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/app.repository.d.ts +15 -0
- package/dist/src/storage/repositories/app.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/memory.repository.d.ts +41 -1
- package/dist/src/storage/repositories/memory.repository.d.ts.map +1 -1
- package/dist/src/storage/repositories/organism.repository.d.ts +2 -1
- package/dist/src/storage/repositories/organism.repository.d.ts.map +1 -1
- package/dist/static/app-catalog.html +469 -122
- package/package.json +2 -1
- package/prisma/schema.postgres.prisma +10 -0
- package/prisma/schema.prisma +12 -0
|
@@ -8,9 +8,71 @@
|
|
|
8
8
|
* Presentational cards live in ./secretary/cards.js; pure helpers in /js/services/secretary-helpers.js.
|
|
9
9
|
* Pure frontend orchestration over generic endpoints (directives + organisms + memory + ai/complete +
|
|
10
10
|
* agent-messages + discover). Full design: docs/plans/2026-06-23-secretary-feature.md.
|
|
11
|
-
* @structure SECRETARY_ICON · SecretaryView (default) — state, effects, handlers, layout (cards in ./secretary/cards.js + ./secretary/cards-reach.js)
|
|
11
|
+
* @structure SECRETARY_ICON · SecretaryView (default) — state, effects, handlers, layout (cards in ./secretary/cards.js + ./secretary/cards-reach.js + dashboard chrome in ./secretary/dashboard.js)
|
|
12
12
|
* @usage routed at /v1/secretary by spa.html (+ portal.ts spaRoutes).
|
|
13
13
|
* @version-history
|
|
14
|
+
* v0.25.0 — 2026-07-01 — Light-vs-heavy seam: setup no longer cheap-authors content (that hallucinated).
|
|
15
|
+
* applyResult now CREATES the workspace structure and then proposes grounded Work Briefs (POST
|
|
16
|
+
* /v1/secretary/brief) the user hands to a Builder (a connected big AI via the appdev MCP tools). Drops
|
|
17
|
+
* the per-workspace author-now fill loop + the author_content band. The dashboard brief tray (cards)
|
|
18
|
+
* renders the briefs with "Open in Claude Desktop" / "Send to a connected Builder".
|
|
19
|
+
* v0.24.0 — 2026-06-30 — Never-empty + honest fill result. normalizeInlineManifest now GUARANTEES every
|
|
20
|
+
* workspace has a document space (appends a "Muistiinpanot" doc space when the design gave only records
|
|
21
|
+
* spaces) so the setup fill always has a thinking-home, and is genuinely document-first for unspecified
|
|
22
|
+
* spaces. The per-workspace fill loop counts a workspace as filled ONLY on a real 🖊️ authored line (not
|
|
23
|
+
* a ⚠️ summary), retries the HTTP call once (flaky owl-alpha), and the toast names workspaces it
|
|
24
|
+
* couldn't fill instead of a bare count.
|
|
25
|
+
* v0.23.0 — 2026-06-30 — Autonomy chosen up front: "Set up my Secretary" first asks HOW autonomous it
|
|
26
|
+
* should be (Prepare/Draft · Run/Act · Only-when-I-ask/Off), each explained in plain words incl. the
|
|
27
|
+
* AI-credit cost, BEFORE anything runs — so nobody stumbles into Act bands later. The choice sets the
|
|
28
|
+
* new context's author_content band and, for Draft/Act, turns the daily check-in on. Browser-verified.
|
|
29
|
+
* v0.22.0 — 2026-06-30 — Live setup progress: the fill runs ONE workspace at a time (author-now?ws=)
|
|
30
|
+
* and a top-level overlay (survives the hire→dashboard switch) shows "Filling workspace N/total ·
|
|
31
|
+
* <name>" + a progress bar through the whole slow fill, instead of a couple of fleeting toasts.
|
|
32
|
+
* Browser-verified: overlay cycled 1/2 → 2/2 with the bar advancing.
|
|
33
|
+
* v0.21.0 — 2026-06-30 — Deterministic workspace structure (NO AI): genAndApplyManifest normalizes any
|
|
34
|
+
* design-provided inline manifest into a valid one (fixes namespace form + backfills schemaRef/backing/
|
|
35
|
+
* writeRole/mode, rekeys schemas, derives a records schema when missing) and falls back to a plain
|
|
36
|
+
* document space — instead of a flaky per-workspace AI generator call that left some workspaces as
|
|
37
|
+
* "set up when opened". Every workspace now gets its structure reliably, every time. Verified on the
|
|
38
|
+
* dev server: a 6-workspace design (with non-conforming komento.* namespaces) all came out structured.
|
|
39
|
+
* v0.20.0 — 2026-06-30 — Content at handshake: a fresh hire now fills its workspaces with REAL content
|
|
40
|
+
* immediately (POST /v1/secretary/author-now, long timeout) and drops any left empty; the workspace
|
|
41
|
+
* generator runs with skipExamples so no fake "example-N" placeholder records are written.
|
|
42
|
+
* v0.19.0 — 2026-06-30 — Strategy: setup now auto-links milestones to goals. seedGoals returns the
|
|
43
|
+
* assigned goal ids in order; applyResult seeds goals before building the strategy and resolves each
|
|
44
|
+
* milestone's index-based goalRefs to real ids (linkMilestoneGoals); reshape strips dangling index
|
|
45
|
+
* refs; AI re-plan passes existing goals and validates returned id refs (keepKnownGoalRefs). Removes
|
|
46
|
+
* the manual "tick the linked goals" step after setup.
|
|
47
|
+
* v0.18.0 — 2026-06-28 — Strategy: optional per-context steering frame (vision/mission/principles/risks
|
|
48
|
+
* + current → target + ordered milestone gates). strategyCard slot + handlers (edit fields, milestone
|
|
49
|
+
* CRUD/reorder/status, link goals, AI re-plan), enable from Manage & setup, carried in applyResult.
|
|
50
|
+
* v0.17.0 — 2026-06-28 — "Reset Secretary": guarded full-wipe card in Manage & setup (doReset →
|
|
51
|
+
* POST /v1/secretary/reset → reload to the clean setup screen).
|
|
52
|
+
* v0.16.0 — 2026-06-28 — Bound triggers: pass specialists + workflows into triggersCard and the
|
|
53
|
+
* calendar event dialog so a trigger's fire can be bound to a workflow/specialist run.
|
|
54
|
+
* v0.15.0 — 2026-06-28 — B5 (secretary-view-redesign): action-items. The autonomous tick now advances
|
|
55
|
+
* active routines band-driven (act-band file steps auto-run; draft/ask/delegate → action-items) and
|
|
56
|
+
* writes contexts[i].actionItems[]; the dashboard renders them (actionItemsCard) with a one-click
|
|
57
|
+
* handle (open the routine / check a delegated result) or dismiss. Handlers in useWhatsNext.
|
|
58
|
+
* v0.14.0 — 2026-06-27 — B3 (secretary-view-redesign): dynamic quick actions. useQuickActions manages
|
|
59
|
+
* per-context quickActions[] — 2–3 brain-seeded shortcuts (active on hire) + secretary-PROPOSED ones
|
|
60
|
+
* the owner pins. Active dynamic actions render in the quick row (prompt → canned chat message, compose
|
|
61
|
+
* → focus an input); a ✎ panel (quickActionsManager) pins/dismisses proposals + renames/reorders/removes.
|
|
62
|
+
* Security: the Secretary may seed/propose only prompt|compose, never a run verb (sanitizeQuickActions).
|
|
63
|
+
* v0.13.0 — 2026-06-27 — B2 (secretary-view-redesign): Routines + "What's next". The guided plan is
|
|
64
|
+
* generalised into useWhatsNext — propose a new Routine from a goal OR advance an active one, approve
|
|
65
|
+
* each step band-gated (act → run · draft|ask → approve + log a decision · off → skip), execute the
|
|
66
|
+
* automatable part (discover/file; delegation deferred to B4), and persist under
|
|
67
|
+
* contexts[i].routines[]. Active routines render on the dashboard (routinesCard).
|
|
68
|
+
* v0.12.0 — 2026-06-27 — B1 (secretary-view-redesign): dashboard-first IA. A core quick-action row
|
|
69
|
+
* ("Where do things stand?" read-only orientation · Plan/Find/Note focus · Review decisions when
|
|
70
|
+
* due), a "Today" status strip (reliability · budget · next run · last-scan + stale + refresh) above
|
|
71
|
+
* the feed/decision-log, chat below, and the set-up-once config cards (brain/operating/crew/knowledge/
|
|
72
|
+
* access/history/permissions) moved into a collapsed "Manage & setup" disclosure. Chrome in dashboard.js.
|
|
73
|
+
* v0.11.0 — 2026-06-25 — load() reads OpenRouter settings first (which backfills the Secretary agent
|
|
74
|
+
* when a key exists) and tracks hasOpenRouterKey, so a pre-configured owner is provisioned on view
|
|
75
|
+
* load and the "not set up" message correctly distinguishes "no key" from "provisioning".
|
|
14
76
|
* v0.10.0 — 2026-06-24 — P3: doc/image intake into the chat (chatCard 📎 → intake.handleAttach:
|
|
15
77
|
* upload → vision summary → file into the active context's self-organism) and auto-logged decision
|
|
16
78
|
* contracts from answered Ask cards + approved guided plans (learning loop now sees real choices).
|
|
@@ -32,18 +94,27 @@ import { h } from 'preact';
|
|
|
32
94
|
import htm from 'htm';
|
|
33
95
|
const html = htm.bind(h);
|
|
34
96
|
import { useState, useEffect, useCallback, useMemo } from 'preact/hooks';
|
|
35
|
-
import { t } from '/js/i18n.js';
|
|
36
|
-
import { api, apiGet, apiPost, apiPut } from '/js/api.js';
|
|
97
|
+
import { t, getLocale } from '/js/i18n.js';
|
|
98
|
+
import { api, apiGet, apiPost, apiPut, apiDelete } from '/js/api.js';
|
|
37
99
|
import { useViewCSS } from '/components/useViewCSS.js';
|
|
38
100
|
import { useToast } from '/components/Toast.js';
|
|
39
|
-
import { createOrganism, createWorkspace } from '/js/services/organisms.js';
|
|
40
|
-
import { defaultPolicy, mergePolicy } from '/js/services/secretary-policy.js';
|
|
41
|
-
import { buildDesignPrompt, extractJson, snapshotOf, genCtxId, migrateConfig, suggestContextId, computeBudgetInfo, computeReliability, SECRETARY_AIMEAT_PRIMER } from '/js/services/secretary-helpers.js';
|
|
42
|
-
import { contextSwitcher, hirePanel, chatCard, findCard, noteCard, decisionsCard, brainCard, operatingCard, historyCard, metaCard,
|
|
101
|
+
import { createOrganism, createWorkspace, validateGenerated, applyGeneratedWorkspace } from '/js/services/organisms.js';
|
|
102
|
+
import { defaultPolicy, mergePolicy, autonomyPreset } from '/js/services/secretary-policy.js';
|
|
103
|
+
import { buildDesignPrompt, extractJson, snapshotOf, genCtxId, migrateConfig, suggestContextId, computeBudgetInfo, computeReliability, sanitizeQuickActions, normalizeStrategy, MILESTONE_STATUSES, buildStrategyReplanPrompt, linkMilestoneGoals, keepKnownGoalRefs, SECRETARY_AIMEAT_PRIMER } from '/js/services/secretary-helpers.js';
|
|
104
|
+
import { contextSwitcher, hirePanel, chatCard, findCard, noteCard, decisionsCard, brainCard, operatingCard, historyCard, metaCard, whatsNextCard, feedCard, automationCard, goalsCard, decisionLogCard } from '/views/secretary/cards.js';
|
|
43
105
|
import { createResourceCard, knowledgeCard, accessCard, crewCard } from '/views/secretary/cards-reach.js';
|
|
106
|
+
import { quickActionRow, dashStatus, standPanel, briefTrayCard, whatsNextPanel, actionItemsCard, routinesCard, triggersCard, quickActionsManager, manageHeader, workflowDesignPanel, secretaryDangerCard, strategyCard } from '/views/secretary/dashboard.js';
|
|
107
|
+
import { fetchWorkflowOffers, designWorkflow, saveDesignedWorkflow, slugifyWorkflowId } from '/views/secretary/workflow-design.js';
|
|
108
|
+
import { useSpecialists, specialistsCard } from '/views/secretary/use-specialists.js';
|
|
44
109
|
import { useIntake } from '/views/secretary/use-intake.js';
|
|
45
|
-
import {
|
|
110
|
+
import { useWhatsNext } from '/views/secretary/use-whats-next.js';
|
|
111
|
+
import { useQuickActions } from '/views/secretary/use-quick-actions.js';
|
|
112
|
+
import { useFreshness } from '/views/secretary/use-freshness.js';
|
|
46
113
|
import { useAutonomy } from '/views/secretary/use-autonomy.js';
|
|
114
|
+
import { useCalendar, calendarCard, CalendarEventDialog } from '/views/secretary/calendar.js';
|
|
115
|
+
import { useTriggers } from '/views/secretary/use-triggers.js';
|
|
116
|
+
import { useLayout, LayoutCard } from '/views/secretary/use-layout.js';
|
|
117
|
+
import { produceDeliverable, getAiCapability, reasonJson } from '/views/secretary/quality.js';
|
|
47
118
|
import { useLearning } from '/views/secretary/use-learning.js';
|
|
48
119
|
import { useCreateResource } from '/views/secretary/use-create-resource.js';
|
|
49
120
|
import { useKnowledge } from '/views/secretary/use-knowledge.js';
|
|
@@ -62,10 +133,17 @@ export default function SecretaryView() {
|
|
|
62
133
|
useViewCSS('/css/views/secretary.css');
|
|
63
134
|
const { showToast, ToastContainer } = useToast();
|
|
64
135
|
const [secretary, setSecretary] = useState(undefined); // undefined=loading, null=not provisioned
|
|
136
|
+
const [hasOpenRouterKey, setHasOpenRouterKey] = useState(false); // owner has an OpenRouter key configured
|
|
65
137
|
const [config, setConfig] = useState(null); // { contexts:[...], activeContextId }
|
|
138
|
+
const [briefs, setBriefs] = useState([]); // open Work Briefs (heavy work to hand a Builder)
|
|
139
|
+
const [builders, setBuilders] = useState([]); // connected big-AI agents (not Secretary / specialists)
|
|
140
|
+
const [dispatching, setDispatching] = useState(''); // briefId currently being dispatched
|
|
66
141
|
const [result, setResult] = useState('');
|
|
67
142
|
const [needs, setNeeds] = useState('');
|
|
68
143
|
const [applying, setApplying] = useState(false);
|
|
144
|
+
const [setupStep, setSetupStep] = useState(null); // live setup progress: { phase, done, total, name }
|
|
145
|
+
const [chooseMode, setChooseMode] = useState(false); // showing the "how should it work?" autonomy step
|
|
146
|
+
const [setupMode, setSetupMode] = useState('draft'); // chosen autonomy: 'off' | 'draft' | 'act'
|
|
69
147
|
const [generating, setGenerating] = useState(false);
|
|
70
148
|
const [showHire, setShowHire] = useState(false);
|
|
71
149
|
const [hireMode, setHireMode] = useState('new'); // 'new' (add context) | 'edit' (re-run active)
|
|
@@ -76,6 +154,18 @@ export default function SecretaryView() {
|
|
|
76
154
|
const [findScope, setFindScope] = useState('public');
|
|
77
155
|
const [findResults, setFindResults] = useState(null); // null=not searched, []=none, [...]
|
|
78
156
|
const [finding, setFinding] = useState(false);
|
|
157
|
+
const [manageOpen, setManageOpen] = useState(false); // B1: collapsed "Manage & setup" disclosure
|
|
158
|
+
const [stand, setStand] = useState(null); // B1: read-only "where things stand" summary { loading } | { text }
|
|
159
|
+
const [nextAns, setNextAns] = useState(null); // "What's next?" — the Secretary's forward answer { loading } | { text }
|
|
160
|
+
const [pasteDrafts, setPasteDrafts] = useState({}); // prompt-driven path: pasted-back result text, keyed by action id
|
|
161
|
+
const [wfDesign, setWfDesign] = useState(null); // "Design a workflow" flow: null | { outcome, designing, draft, trigger, error, saving, saved, errors }
|
|
162
|
+
const [calEvent, setCalEvent] = useState(null); // clicked calendar entry → inline type-specific editor dialog
|
|
163
|
+
const [resetState, setResetState] = useState({ show: false, text: '', busy: false }); // "Reset Secretary" guard
|
|
164
|
+
const [stratEdit, setStratEdit] = useState(null); // Strategy text/list edit draft | null
|
|
165
|
+
const [msInput, setMsInput] = useState(''); // Strategy "add milestone" input
|
|
166
|
+
const [replan, setReplan] = useState(null); // Strategy re-plan flow: { note } | { busy } | { draft } | null
|
|
167
|
+
const [brainDraft, setBrainDraft] = useState(null); // C2: direct brain editor draft { purpose, rules[] } | null
|
|
168
|
+
const [savingBrain, setSavingBrain] = useState(false);
|
|
79
169
|
|
|
80
170
|
const owner = (secretary && secretary.owner)
|
|
81
171
|
|| (window.AIMEAT && window.AIMEAT.auth && window.AIMEAT.auth.getSession && window.AIMEAT.auth.getSession()?.owner)
|
|
@@ -92,22 +182,52 @@ export default function SecretaryView() {
|
|
|
92
182
|
}, []);
|
|
93
183
|
|
|
94
184
|
const load = useCallback(async () => {
|
|
185
|
+
// Read OpenRouter settings FIRST: this GET also provisions the Secretary agent when a key exists
|
|
186
|
+
// (backfill for owners who configured OpenRouter before the feature shipped), so by the time we
|
|
187
|
+
// list agents below the agent is present. Also tells us whether to show "configure OpenRouter".
|
|
188
|
+
const settingsResp = await apiGet('/v1/openrouter/settings').catch(() => null);
|
|
189
|
+
setHasOpenRouterKey(!!(settingsResp && settingsResp.data && settingsResp.data.hasApiKey));
|
|
95
190
|
const agentsResp = await apiGet('/v1/agents').catch(() => null);
|
|
96
191
|
const agents = (agentsResp && agentsResp.data && agentsResp.data.agents) || [];
|
|
97
192
|
const sec = agents.find((a) => (a.tags || []).includes('system:secretary'))
|
|
98
193
|
|| agents.find((a) => a.name === 'secretary') || null;
|
|
99
194
|
setSecretary(sec);
|
|
195
|
+
// Builders = connected big-AI agents the heavy work is dispatched to — anything that is NOT the
|
|
196
|
+
// Secretary (the watcher) and NOT a node-local specialist. These are visually distinct in the brief tray.
|
|
197
|
+
setBuilders(agents.filter((a) => a && a.name !== 'secretary'
|
|
198
|
+
&& !(a.tags || []).includes('system:secretary') && !(a.tags || []).includes('system:specialist')));
|
|
100
199
|
if (!sec) return;
|
|
101
|
-
const [dir, cfg] = await Promise.all([
|
|
200
|
+
const [dir, cfg, briefResp] = await Promise.all([
|
|
102
201
|
apiGet('/v1/agents/secretary/directives').catch(() => null),
|
|
103
202
|
apiGet('/v1/memory/secretary.config').catch(() => null),
|
|
203
|
+
apiGet('/v1/secretary/briefs').catch(() => null),
|
|
104
204
|
]);
|
|
205
|
+
setBriefs((briefResp && briefResp.data && briefResp.data.briefs) || []);
|
|
105
206
|
const rawCfg = (cfg && cfg.data && cfg.data.value) || null;
|
|
106
207
|
const { config: normalized, changed } = migrateConfig(rawCfg, dir && dir.data);
|
|
107
208
|
if (changed) await apiPost('/v1/memory', { key: 'secretary.config', value: normalized, visibility: 'private' }).catch(() => {});
|
|
108
209
|
setConfig(normalized);
|
|
109
210
|
}, []);
|
|
110
211
|
|
|
212
|
+
// Dispatch a Work Brief to a connected Builder (lane b): queues a task the Builder picks up + runs via
|
|
213
|
+
// the appdev MCP tools. Marks the brief 'dispatched' so the tray reflects "waiting on the Builder".
|
|
214
|
+
const dispatchBrief = useCallback(async (briefId, agentGaii) => {
|
|
215
|
+
if (!briefId || !agentGaii) return;
|
|
216
|
+
setDispatching(briefId);
|
|
217
|
+
try {
|
|
218
|
+
const r = await apiPost(`/v1/secretary/brief/${encodeURIComponent(briefId)}/dispatch`, { targetAgent: agentGaii }).catch(() => null);
|
|
219
|
+
if (r && r.data) { showToast(t('secretary.brief.dispatched')); load(); }
|
|
220
|
+
else showToast(t('secretary.brief.dispatchFailed'), true);
|
|
221
|
+
} finally { setDispatching(''); }
|
|
222
|
+
}, [load, showToast]);
|
|
223
|
+
|
|
224
|
+
// Resolve a Work Brief (the owner handled it themselves, or it's no longer relevant). Marks it 'done'.
|
|
225
|
+
const resolveBrief = useCallback(async (briefId) => {
|
|
226
|
+
if (!briefId) return;
|
|
227
|
+
await apiPost(`/v1/secretary/brief/${encodeURIComponent(briefId)}/done`, {}).catch(() => null);
|
|
228
|
+
load();
|
|
229
|
+
}, [load]);
|
|
230
|
+
|
|
111
231
|
useEffect(() => { load(); }, [load]);
|
|
112
232
|
|
|
113
233
|
// Re-fetch config on server-reported changes: the autonomous tick writes pending decisions + the
|
|
@@ -127,11 +247,36 @@ export default function SecretaryView() {
|
|
|
127
247
|
// Cheap, AI-free routing suggestion: does what you're typing belong to another context? (§22)
|
|
128
248
|
const routeSuggestion = useMemo(() => suggestContextId(chatInput, contexts, activeId), [chatInput, contexts, activeId]);
|
|
129
249
|
|
|
250
|
+
// "Reset Secretary" — full wipe (brain + all secretary.* + self-organisms/workspaces + specialists +
|
|
251
|
+
// tick schedule), keeping the OpenRouter key. Guarded by a typed RESET confirm; reloads to the clean
|
|
252
|
+
// setup screen afterward (state across many hooks is fully torn down server-side).
|
|
253
|
+
const doReset = useCallback(async () => {
|
|
254
|
+
setResetState((s) => ({ ...s, busy: true }));
|
|
255
|
+
try {
|
|
256
|
+
await apiPost('/v1/secretary/reset', {});
|
|
257
|
+
showToast(t('secretary.reset.done'));
|
|
258
|
+
setTimeout(() => window.location.reload(), 700);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
showToast(`${t('secretary.reset.error')}: ${e.message}`, true);
|
|
261
|
+
setResetState((s) => ({ ...s, busy: false }));
|
|
262
|
+
}
|
|
263
|
+
}, [showToast]);
|
|
264
|
+
|
|
130
265
|
// Load the active context's conversation (chat is per-context).
|
|
131
266
|
useEffect(() => {
|
|
267
|
+
setStand(null);
|
|
268
|
+
setNextAns(null);
|
|
132
269
|
if (!activeId) { setChat([]); return; }
|
|
133
270
|
let cancelled = false;
|
|
134
271
|
setChat([]);
|
|
272
|
+
// Restore the last "where things stand" so it persists across reloads (with its timestamp).
|
|
273
|
+
apiGet(`/v1/memory/${encodeURIComponent('secretary.stand.' + activeId)}`)
|
|
274
|
+
.then((r) => { const v = r && r.data && r.data.value; if (!cancelled && v && v.text) setStand({ text: v.text, generatedAt: v.generatedAt }); })
|
|
275
|
+
.catch(() => {});
|
|
276
|
+
// Restore the last "What's next" list (with its do/skip state) so leaving and coming back keeps it.
|
|
277
|
+
apiGet(`/v1/memory/${encodeURIComponent('secretary.next.' + activeId)}`)
|
|
278
|
+
.then((r) => { const v = r && r.data && r.data.value; if (!cancelled && v && Array.isArray(v.actions) && v.actions.length) setNextAns(v); })
|
|
279
|
+
.catch(() => {});
|
|
135
280
|
apiGet(`/v1/memory/${encodeURIComponent('secretary.chat.' + activeId)}`)
|
|
136
281
|
.then((r) => {
|
|
137
282
|
if (cancelled) return;
|
|
@@ -142,14 +287,29 @@ export default function SecretaryView() {
|
|
|
142
287
|
return () => { cancelled = true; };
|
|
143
288
|
}, [activeId]);
|
|
144
289
|
|
|
290
|
+
// Persist the "What's next" list (and its do/skip state) so navigating away and back keeps it.
|
|
291
|
+
// Stamped with ctxId so a context switch never writes the old list under the new context's key.
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (!activeId || !nextAns || !Array.isArray(nextAns.actions) || nextAns.ctxId !== activeId) return;
|
|
294
|
+
apiPost('/v1/memory', { key: `secretary.next.${activeId}`, value: nextAns, visibility: 'private' }).catch(() => {});
|
|
295
|
+
}, [nextAns, activeId]);
|
|
296
|
+
|
|
145
297
|
// Intake (note composer + Ask cards + P2-E cross-context auto-routing) lives in its own hook.
|
|
146
|
-
|
|
298
|
+
// Triggers (Slice 3/4) — declared before intake so a filed note can ask the Secretary to auto-propose a trigger.
|
|
299
|
+
const trig = useTriggers({ active, showToast });
|
|
300
|
+
const intake = useIntake({ active, contexts, config, persistConfig, owner, showToast, onNoteFiled: trig.proposeTriggerFromText });
|
|
147
301
|
const { wsList, suggestedWsId } = intake;
|
|
148
302
|
|
|
149
|
-
//
|
|
150
|
-
const
|
|
303
|
+
// B2 — Routines + "What's next" (generalises the guided plan): propose/advance band-gated routines.
|
|
304
|
+
const next = useWhatsNext({ active, config, persistConfig, policy, wsList, suggestedWsId, showToast });
|
|
305
|
+
// B3 — dynamic quick actions (brain-seeded + secretary-proposed, owner-pinned).
|
|
306
|
+
const qa = useQuickActions({ active, config, persistConfig, owner, showToast });
|
|
151
307
|
// Phase 4 — autonomous tick + Home feed + calendar (own hook).
|
|
152
308
|
const auto = useAutonomy({ showToast });
|
|
309
|
+
// Decision B — real calendar (month/week/day) of feed (past) + tick & routine cadence (future).
|
|
310
|
+
const cal = useCalendar();
|
|
311
|
+
const lay = useLayout(); // customizable dashboard layout (pin to column / reorder / hide; persisted)
|
|
312
|
+
const spec = useSpecialists({ showToast }); // create/list/run the owner's specialists (Phase 3 building blocks)
|
|
153
313
|
// Phase 5 — learning loop: goals + decision-log contracts + review trigger (own hook).
|
|
154
314
|
const learn = useLearning({ active, auto, showToast });
|
|
155
315
|
// P2 reach: create-don't-just-find (A), knowledge custodian (B), access gatekeeper (C), crew setup (D).
|
|
@@ -162,7 +322,108 @@ export default function SecretaryView() {
|
|
|
162
322
|
const budgetInfo = useMemo(() => computeBudgetInfo(policy.dailyMorselBudget, config, activeId), [policy.dailyMorselBudget, config, activeId]);
|
|
163
323
|
const reliability = useMemo(() => computeReliability(learn.decisions), [learn.decisions]);
|
|
164
324
|
|
|
165
|
-
|
|
325
|
+
/** C3: seed the hire-proposed goals as standalone goal records (so the learning loop isn't empty). */
|
|
326
|
+
// Seed the proposed goals and RETURN their assigned ids in input order (null for a skipped/failed one),
|
|
327
|
+
// so milestone goalRefs (the AI links them by 1-based goal index) can be resolved to real goal ids.
|
|
328
|
+
const seedGoals = useCallback(async (goals, contextId, contextName) => {
|
|
329
|
+
const arr = Array.isArray(goals) ? goals.slice(0, 4) : [];
|
|
330
|
+
const ids = [];
|
|
331
|
+
for (const g of arr) {
|
|
332
|
+
const title = String((g && g.title) || '').trim();
|
|
333
|
+
if (!title) { ids.push(null); continue; }
|
|
334
|
+
const gid = 'g' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
335
|
+
const ok = await apiPost('/v1/memory', { key: `secretary.goal.${gid}`, value: { id: gid, title: title.slice(0, 200), why: String((g && g.why) || '').trim().slice(0, 300), status: 'open', contextId, contextName, createdAt: new Date().toISOString() }, visibility: 'private', tags: ['secretary', 'goal', 'open', contextId] }).then(() => true).catch(() => false);
|
|
336
|
+
ids.push(ok ? gid : null);
|
|
337
|
+
}
|
|
338
|
+
return ids;
|
|
339
|
+
}, []);
|
|
340
|
+
|
|
341
|
+
// ── Deterministic workspace structure (NO AI) ──
|
|
342
|
+
// The design JSON's inline manifests routinely fail the workspace validator (wrong namespace form, or
|
|
343
|
+
// missing schemaRef/backing/writeRole), which used to push EACH workspace into a flaky per-workspace AI
|
|
344
|
+
// generator call — owl-alpha failing some of those left workspaces as "set up when opened". Instead we
|
|
345
|
+
// NORMALIZE any inline manifest into a valid one ourselves, and fall back to a plain document space.
|
|
346
|
+
// Zero AI calls → every workspace gets its structure reliably, every time.
|
|
347
|
+
const WS_TYPE_MAP = { text: 'string', number: 'number', date: 'string', select: 'string', boolean: 'boolean', reference: 'string' };
|
|
348
|
+
const wsSlug = (s) => String(s || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'tila';
|
|
349
|
+
const backfillEnvelope = (mf, wsId, name) => {
|
|
350
|
+
if (!mf) return;
|
|
351
|
+
mf.manifestVersion = mf.manifestVersion || '1.0';
|
|
352
|
+
mf.id = mf.id || wsId;
|
|
353
|
+
mf.name = mf.name || name;
|
|
354
|
+
mf.kind = mf.kind || 'project';
|
|
355
|
+
mf.status = ['active', 'paused', 'done', 'archived'].includes(mf.status) ? mf.status : 'active';
|
|
356
|
+
};
|
|
357
|
+
// Make ANY inline manifest valid: backfill the envelope + each objectType's required fields, normalize
|
|
358
|
+
// the namespace to the required shared.* form (rekeying its schema), and derive a schema for a records
|
|
359
|
+
// type that lacks one. Mutates `manifest`; may add keys to `schemas`.
|
|
360
|
+
const normalizeInlineManifest = (manifest, schemas, wsId, name) => {
|
|
361
|
+
backfillEnvelope(manifest, wsId, name);
|
|
362
|
+
const ots = Array.isArray(manifest.objectTypes) ? manifest.objectTypes : [];
|
|
363
|
+
manifest.objectTypes = ots.filter(Boolean).map((ot, i) => {
|
|
364
|
+
const o = { ...ot };
|
|
365
|
+
o.name = String(o.name || ('tila' + (i + 1))).trim() || ('tila' + (i + 1));
|
|
366
|
+
let ns = String(o.namespace || ('shared.' + wsSlug(o.name)));
|
|
367
|
+
if (!/^(meta|shared|member)\./.test(ns)) {
|
|
368
|
+
const tail = ns.includes('.') ? ns.slice(ns.indexOf('.') + 1) : ns;
|
|
369
|
+
const newNs = 'shared.' + (wsSlug(tail) || wsSlug(o.name));
|
|
370
|
+
if (schemas[o.namespace] && !schemas[newNs]) { schemas[newNs] = schemas[o.namespace]; delete schemas[o.namespace]; }
|
|
371
|
+
ns = newNs;
|
|
372
|
+
}
|
|
373
|
+
o.namespace = ns;
|
|
374
|
+
o.backing = o.backing === 'tasks' ? 'tasks' : 'memory';
|
|
375
|
+
o.writeRole = ['owner', 'admin', 'member'].includes(o.writeRole) ? o.writeRole : 'member';
|
|
376
|
+
o.cardinality = o.cardinality === 'one' ? 'one' : 'many';
|
|
377
|
+
o.versioned = o.versioned !== false;
|
|
378
|
+
o.schemaRef = o.schemaRef || ('schema:' + wsSlug(o.name) + '@1');
|
|
379
|
+
// Document-first: keep an explicit mode as the design gave it; for an UNSPECIFIED space default to a
|
|
380
|
+
// DOCUMENT (flexible information — the common case) unless the design clearly describes a record list
|
|
381
|
+
// (real fields, or a schema already supplied for it). A records space is for same-shaped, list-like
|
|
382
|
+
// data (actions/tasks/bugs/a pipeline/a tracker) only.
|
|
383
|
+
if (o.mode !== 'document' && o.mode !== 'records') {
|
|
384
|
+
o.mode = ((Array.isArray(o.fields) && o.fields.length) || schemas[ns]) ? 'records' : 'document';
|
|
385
|
+
}
|
|
386
|
+
if (o.mode !== 'document' && o.backing === 'memory' && !schemas[ns]) {
|
|
387
|
+
const props = {}; (o.fields || []).forEach((f) => { if (f && f.name) props[f.name] = { type: WS_TYPE_MAP[f.type] || 'string' }; });
|
|
388
|
+
if (!props.id) props.id = { type: 'string' };
|
|
389
|
+
schemas[ns] = { type: 'object', properties: props, required: ['id'] };
|
|
390
|
+
}
|
|
391
|
+
return o;
|
|
392
|
+
});
|
|
393
|
+
// Never-empty guarantee: every workspace needs at least one DOCUMENT space — a free-markdown "thinking
|
|
394
|
+
// home" the setup fill can always author grounded content into, even when the design gave only records
|
|
395
|
+
// spaces (records are real-data-only and legitimately start empty). Append a default document space when
|
|
396
|
+
// the design provided none. Additive + idempotent: only when no document space exists.
|
|
397
|
+
if (!manifest.objectTypes.some((o) => o.mode === 'document')) {
|
|
398
|
+
let dns = 'shared.muistiinpanot';
|
|
399
|
+
if (manifest.objectTypes.some((o) => o.namespace === dns)) dns = 'shared.muistiinpanot-' + wsSlug(name);
|
|
400
|
+
manifest.objectTypes.push({ name: 'Muistiinpanot', namespace: dns, backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true, schemaRef: 'schema:doc@1', mode: 'document' });
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
// A plain document space (free markdown) — the deterministic fallback so a workspace with no usable
|
|
404
|
+
// manifest still gets a valid structure at setup, without any AI call.
|
|
405
|
+
const docSpaceManifest = (wsId, name) => ({
|
|
406
|
+
manifest: { manifestVersion: '1.0', id: wsId, name, kind: 'notes', status: 'active',
|
|
407
|
+
objectTypes: [{ name: 'Dokumentit', namespace: 'shared.dokumentit', backing: 'memory', writeRole: 'member', cardinality: 'many', versioned: true, schemaRef: 'schema:doc@1', mode: 'document' }] },
|
|
408
|
+
schemas: {},
|
|
409
|
+
});
|
|
410
|
+
const genAndApplyManifest = useCallback(async (orgId, wsId, ws) => {
|
|
411
|
+
const name = String(ws.name || '').trim() || 'Työtila';
|
|
412
|
+
try {
|
|
413
|
+
// skipExamples: the Secretary fills workspaces with REAL content at setup (author-now), never fakes.
|
|
414
|
+
if (ws.manifest && typeof ws.manifest === 'object') {
|
|
415
|
+
const schemas = { ...(ws.schemas || {}) };
|
|
416
|
+
const manifest = JSON.parse(JSON.stringify(ws.manifest));
|
|
417
|
+
normalizeInlineManifest(manifest, schemas, wsId, name);
|
|
418
|
+
const provided = { manifest, schemas };
|
|
419
|
+
if (!validateGenerated(provided).length) { await applyGeneratedWorkspace(orgId, wsId, provided, { skipExamples: true }); return; }
|
|
420
|
+
}
|
|
421
|
+
// No usable inline manifest → a plain document space, applied deterministically (no AI).
|
|
422
|
+
await applyGeneratedWorkspace(orgId, wsId, docSpaceManifest(wsId, name), { skipExamples: true });
|
|
423
|
+
} catch { /* leave it manifest-less; the owner can set it up manually */ }
|
|
424
|
+
}, []);
|
|
425
|
+
|
|
426
|
+
const applyResult = useCallback(async (mode = 'off') => {
|
|
166
427
|
setApplying(true);
|
|
167
428
|
try {
|
|
168
429
|
const json = extractJson(result);
|
|
@@ -177,6 +438,8 @@ export default function SecretaryView() {
|
|
|
177
438
|
}
|
|
178
439
|
const newBrain = { purpose: b.purpose, rules };
|
|
179
440
|
const isNew = hireMode === 'new' || !active;
|
|
441
|
+
setSetupStep({ phase: 'structure' }); // live progress: building the space + workspaces
|
|
442
|
+
let fillTargets = []; // [{ id, name }] of new workspaces to fill one at a time
|
|
180
443
|
|
|
181
444
|
let next;
|
|
182
445
|
if (isNew) {
|
|
@@ -185,6 +448,7 @@ export default function SecretaryView() {
|
|
|
185
448
|
const orgId = created && created.data && created.data.organism && created.data.organism.id;
|
|
186
449
|
const wsSummary = [];
|
|
187
450
|
if (orgId) {
|
|
451
|
+
const created = [];
|
|
188
452
|
for (const ws of org.workspaces.slice(0, 12)) {
|
|
189
453
|
const name = String(ws.name || '').trim();
|
|
190
454
|
if (!name) continue;
|
|
@@ -192,35 +456,133 @@ export default function SecretaryView() {
|
|
|
192
456
|
if (entry && entry.id && ws.purpose) {
|
|
193
457
|
await apiPost('/v1/memory', { key: `organism.${orgId}.w.${entry.id}.meta.readme`, value: `# ${name}\n\n${ws.purpose}`, visibility: 'private' });
|
|
194
458
|
}
|
|
459
|
+
if (entry && entry.id) created.push({ wsId: entry.id, ws });
|
|
195
460
|
wsSummary.push({ name, purpose: ws.purpose || '' });
|
|
196
461
|
}
|
|
462
|
+
// Design + apply each workspace's manifest in parallel so they come out usable, not empty shells.
|
|
463
|
+
await Promise.all(created.map((c) => genAndApplyManifest(orgId, c.wsId, c.ws)));
|
|
464
|
+
fillTargets = created.map((c) => ({ id: c.wsId, name: String((c.ws && c.ws.name) || '').trim() || c.wsId }));
|
|
197
465
|
}
|
|
198
|
-
|
|
466
|
+
// B3: seed the brain-proposed quick actions — active on hire (run verbs are dropped by the sanitizer).
|
|
467
|
+
const quickActions = sanitizeQuickActions(json.quickActions, 'brain', 'active');
|
|
468
|
+
const ctxId = genCtxId();
|
|
469
|
+
// C3: seed the proposed goals FIRST (so the learning loop starts populated, not "no goals yet") and
|
|
470
|
+
// capture their ids, so each milestone's goalRefs (the AI links them by 1-based goal index) resolve
|
|
471
|
+
// to real goal ids — this is what lets a milestone auto-advance as its goals get done, instead of
|
|
472
|
+
// the owner ticking the linked goals by hand after setup.
|
|
473
|
+
const goalIds = await seedGoals(json.goals, ctxId, org.name);
|
|
474
|
+
const strategy = linkMilestoneGoals(normalizeStrategy(json.strategy), goalIds);
|
|
475
|
+
// Autonomy chosen in the setup step controls only how active the DAILY tick is (it watches, briefs,
|
|
476
|
+
// and proposes Work Briefs — it never authors content itself, so there is no "fill" band any more).
|
|
477
|
+
const policy = defaultPolicy();
|
|
478
|
+
const ctx = { id: ctxId, name: org.name, brain: newBrain, organismId: orgId || null, organismName: org.name, workspaces: wsSummary, policy, brainHistory: [], quickActions, strategy };
|
|
199
479
|
next = { contexts: [...contexts, ctx], activeContextId: ctx.id };
|
|
200
480
|
} else {
|
|
201
|
-
// Re-run on the active context: update its brain, snapshot the old one
|
|
481
|
+
// Re-run on the active context (reshape): update its brain, snapshot the old one, KEEP its
|
|
482
|
+
// organism. C1: ADD any newly-proposed workspaces (additive — never rename/remove existing;
|
|
483
|
+
// removal stays a manual decision in the Organisms view, so no data is ever auto-deleted).
|
|
202
484
|
const prev = snapshotOf(active.brain);
|
|
203
485
|
const history = [prev, ...(active.brainHistory || [])].filter(Boolean).slice(0, 10);
|
|
204
|
-
const
|
|
486
|
+
const existingNames = new Set((active.workspaces || []).map((w) => String(w.name).toLowerCase()));
|
|
487
|
+
const addedWs = [];
|
|
488
|
+
if (active.organismId && Array.isArray(org.workspaces)) {
|
|
489
|
+
const created = [];
|
|
490
|
+
for (const ws of org.workspaces.slice(0, 12)) {
|
|
491
|
+
const name = String(ws.name || '').trim();
|
|
492
|
+
if (!name || existingNames.has(name.toLowerCase())) continue;
|
|
493
|
+
const entry = await createWorkspace(active.organismId, name);
|
|
494
|
+
if (entry && entry.id && ws.purpose) {
|
|
495
|
+
await apiPost('/v1/memory', { key: `organism.${active.organismId}.w.${entry.id}.meta.readme`, value: `# ${name}\n\n${ws.purpose}`, visibility: 'private' });
|
|
496
|
+
}
|
|
497
|
+
if (entry && entry.id) created.push({ wsId: entry.id, ws });
|
|
498
|
+
addedWs.push({ name, purpose: ws.purpose || '' });
|
|
499
|
+
}
|
|
500
|
+
await Promise.all(created.map((c) => genAndApplyManifest(active.organismId, c.wsId, c.ws)));
|
|
501
|
+
}
|
|
502
|
+
// Strategy: if the reshape JSON proposes one, take it; otherwise keep what the context already had.
|
|
503
|
+
// Reshape doesn't re-seed goals, so a milestone's index-based goalRefs can't be resolved here —
|
|
504
|
+
// strip them (linkMilestoneGoals with no id list) rather than persist dangling references.
|
|
505
|
+
const strategy = json.strategy !== undefined ? linkMilestoneGoals(normalizeStrategy(json.strategy), []) : (active.strategy || normalizeStrategy(null));
|
|
506
|
+
const updated = { ...active, brain: newBrain, brainHistory: history, workspaces: [...(active.workspaces || []), ...addedWs], strategy };
|
|
205
507
|
next = { ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) };
|
|
206
508
|
}
|
|
207
509
|
await persistConfig(next);
|
|
208
510
|
await syncDirectives(newBrain);
|
|
209
|
-
|
|
511
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update')); // refresh goals/learning + cards
|
|
512
|
+
// The Secretary NO LONGER authors content one-shot at handshake (the cheap model hallucinated and went
|
|
513
|
+
// "onto the Builders' turf"). Instead it now proposes grounded Work Briefs for the new (empty)
|
|
514
|
+
// workspaces — heavy work a Builder (a connected big AI via the appdev MCP tools) executes. Fast +
|
|
515
|
+
// deterministic (no slow model call); the dashboard's brief tray then offers "Open in Claude Desktop"
|
|
516
|
+
// / "Send to a connected Builder". Only on a fresh hire, so it never touches existing content.
|
|
517
|
+
if (isNew && fillTargets.length) {
|
|
518
|
+
setSetupStep({ phase: 'fill', done: 0, total: fillTargets.length, name: fillTargets[0].name });
|
|
519
|
+
const r = await api('/v1/secretary/brief', { method: 'POST', body: '{}', timeoutMs: 120000, retries: 1 }).catch(() => null);
|
|
520
|
+
const proposed = (r && r.data && typeof r.data.proposed === 'number') ? r.data.proposed : 0;
|
|
521
|
+
setSetupStep({ phase: 'done', done: fillTargets.length, total: fillTargets.length });
|
|
522
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update')); // briefs show up in the tray live
|
|
523
|
+
showToast(proposed ? `${t('secretary.briefsReady')} (${proposed})` : t('secretary.hireDone'));
|
|
524
|
+
} else {
|
|
525
|
+
showToast(t('secretary.hireDone'));
|
|
526
|
+
}
|
|
527
|
+
// If the owner chose an autonomous mode, turn the daily check-in ON for them — so it actually keeps
|
|
528
|
+
// working on its own (the band alone does nothing without a schedule). 'off' stays fully manual.
|
|
529
|
+
if (isNew && mode !== 'off') {
|
|
530
|
+
await apiPost('/v1/schedules', { kind: 'secretary', cron: '0 8 * * *', display_name: t('secretary.auto.scheduleName') }).catch(() => {});
|
|
531
|
+
}
|
|
210
532
|
setResult(''); setNeeds(''); setShowHire(false);
|
|
211
533
|
} catch (e) {
|
|
212
534
|
showToast(`${t('secretary.hireError')}: ${e.message}`, true);
|
|
213
535
|
} finally {
|
|
214
|
-
setApplying(false);
|
|
536
|
+
setApplying(false); setSetupStep(null);
|
|
537
|
+
}
|
|
538
|
+
}, [result, hireMode, active, contexts, config, persistConfig, syncDirectives, seedGoals, genAndApplyManifest, showToast]);
|
|
539
|
+
|
|
540
|
+
// "Set up my Secretary" no longer runs immediately — it first asks (one step, up front) HOW autonomous
|
|
541
|
+
// the owner wants it, explained in plain words, so nobody stumbles into Act bands later. The chosen mode
|
|
542
|
+
// is passed to applyResult. Only for a brand-new hire; re-running an existing context keeps its policy.
|
|
543
|
+
const startSetup = useCallback(() => {
|
|
544
|
+
if (!result.trim()) return;
|
|
545
|
+
try { extractJson(result); } catch { showToast(t('secretary.hireBadShape'), true); return; }
|
|
546
|
+
if (hireMode === 'new' || !active) { setSetupMode('draft'); setChooseMode(true); } else { applyResult('off'); }
|
|
547
|
+
}, [result, hireMode, active, applyResult, showToast]);
|
|
548
|
+
const confirmSetup = useCallback(() => { setChooseMode(false); applyResult(setupMode); }, [applyResult, setupMode]);
|
|
549
|
+
|
|
550
|
+
// C2 — direct brain editor: edit the active context's purpose + rules in place (no AI re-run).
|
|
551
|
+
const startBrainEdit = useCallback(() => {
|
|
552
|
+
if (!active) return;
|
|
553
|
+
setBrainDraft({ purpose: (active.brain && active.brain.purpose) || '', rules: ((active.brain && active.brain.rules) || []).map((r) => ({ id: r.id, description: r.description })) });
|
|
554
|
+
}, [active]);
|
|
555
|
+
const cancelBrainEdit = useCallback(() => setBrainDraft(null), []);
|
|
556
|
+
const saveBrain = useCallback(async () => {
|
|
557
|
+
if (!active || !brainDraft) return;
|
|
558
|
+
const purpose = String(brainDraft.purpose || '').trim();
|
|
559
|
+
if (!purpose) { showToast(t('secretary.brainEmptyPurpose'), true); return; }
|
|
560
|
+
const rules = (brainDraft.rules || [])
|
|
561
|
+
.map((r) => ({ id: r.id || ('r' + Math.random().toString(36).slice(2, 6)), description: String(r.description || '').trim() }))
|
|
562
|
+
.filter((r) => r.description);
|
|
563
|
+
setSavingBrain(true);
|
|
564
|
+
try {
|
|
565
|
+
const newBrain = { purpose, rules };
|
|
566
|
+
const prev = snapshotOf(active.brain);
|
|
567
|
+
const history = [prev, ...(active.brainHistory || [])].filter(Boolean).slice(0, 10);
|
|
568
|
+
const updated = { ...active, brain: newBrain, brainHistory: history };
|
|
569
|
+
await persistConfig({ ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) });
|
|
570
|
+
await syncDirectives(newBrain);
|
|
571
|
+
setBrainDraft(null);
|
|
572
|
+
showToast(t('secretary.brainSaved'));
|
|
573
|
+
} catch (e) {
|
|
574
|
+
showToast(`${t('secretary.hireError')}: ${e.message}`, true);
|
|
575
|
+
} finally {
|
|
576
|
+
setSavingBrain(false);
|
|
215
577
|
}
|
|
216
|
-
}, [
|
|
578
|
+
}, [active, brainDraft, config, contexts, persistConfig, syncDirectives, showToast]);
|
|
217
579
|
|
|
218
580
|
const generateInApp = useCallback(async () => {
|
|
219
581
|
if (!needs.trim()) return;
|
|
220
582
|
setGenerating(true);
|
|
221
583
|
try {
|
|
222
584
|
// Long AI completion: low-level api() with a large timeout + no retries (apiPost's 30s would abort a slow model).
|
|
223
|
-
const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: buildDesignPrompt(owner, needs.trim()), app_id: 'secretary-setup' }), timeoutMs: 1_800_000, retries: 0 });
|
|
585
|
+
const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: buildDesignPrompt(owner, needs.trim(), hireMode === 'edit' ? active : null), app_id: 'secretary-setup' }), timeoutMs: 1_800_000, retries: 0 });
|
|
224
586
|
const content = r && r.data && r.data.content;
|
|
225
587
|
if (!content) throw new Error(t('secretary.inappEmpty'));
|
|
226
588
|
setResult(content);
|
|
@@ -230,17 +592,18 @@ export default function SecretaryView() {
|
|
|
230
592
|
} finally {
|
|
231
593
|
setGenerating(false);
|
|
232
594
|
}
|
|
233
|
-
}, [needs, owner, showToast]);
|
|
595
|
+
}, [needs, owner, hireMode, active, showToast]);
|
|
234
596
|
|
|
235
597
|
/** One chat turn: assemble the active context's brain as the system prompt + the transcript,
|
|
236
598
|
* complete on the owner's key, append + persist. Per-context conversation (secretary.chat.{id}). */
|
|
237
|
-
const sendChat = useCallback(async () => {
|
|
238
|
-
|
|
599
|
+
const sendChat = useCallback(async (override) => {
|
|
600
|
+
// `override` lets a dynamic quick action (kind:'prompt') send a canned message without touching the input.
|
|
601
|
+
const text = (typeof override === 'string' ? override : chatInput).trim();
|
|
239
602
|
if (!text || !active) return;
|
|
240
603
|
setChatSending(true);
|
|
241
604
|
const history = [...chat, { role: 'user', content: text }];
|
|
242
605
|
setChat(history);
|
|
243
|
-
setChatInput('');
|
|
606
|
+
if (typeof override !== 'string') setChatInput('');
|
|
244
607
|
try {
|
|
245
608
|
const rules = (active.brain.rules || []).map((r) => '- ' + r.description).join('\n');
|
|
246
609
|
const wsNames = (active.workspaces || []).map((w) => w.name).join(', ');
|
|
@@ -286,6 +649,286 @@ ${SECRETARY_AIMEAT_PRIMER}`;
|
|
|
286
649
|
}
|
|
287
650
|
}, [findQ, findScope, showToast]);
|
|
288
651
|
|
|
652
|
+
// ── B1/G3 dashboard freshness (lastScan + stale + the real discover Reconcile) — in its own hook ──
|
|
653
|
+
const { lastScan, stale, scanning, reconcile } = useFreshness({ active, config, contexts, persistConfig, auto, showToast });
|
|
654
|
+
|
|
655
|
+
// Quick-action "focus" verbs: smooth-scroll a working card into view and focus its input.
|
|
656
|
+
const focusInto = useCallback((sel, inputSel) => {
|
|
657
|
+
const el = document.querySelector(sel);
|
|
658
|
+
if (!el) return;
|
|
659
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
660
|
+
if (inputSel) { const i = el.querySelector(inputSel); if (i) setTimeout(() => i.focus(), 250); }
|
|
661
|
+
}, []);
|
|
662
|
+
|
|
663
|
+
// "Review decisions" is only offered when an open decision is due for its revisit.
|
|
664
|
+
const decisionsDue = useMemo(() => {
|
|
665
|
+
const now = Date.now();
|
|
666
|
+
return (learn.decisions || []).some((d) => d && d.status === 'open' && d.revisitWhen && new Date(d.revisitWhen).getTime() <= now);
|
|
667
|
+
}, [learn.decisions]);
|
|
668
|
+
|
|
669
|
+
// B3: active dynamic quick actions → row items. prompt → send a canned chat message; compose → focus an input.
|
|
670
|
+
const dynamicQuickItems = useMemo(() => qa.activeActions.map((a) => ({
|
|
671
|
+
key: a.id,
|
|
672
|
+
label: a.label,
|
|
673
|
+
title: a.kind === 'prompt' ? a.prompt : `${t('secretary.qa.composeTarget')}: ${a.target}`,
|
|
674
|
+
onClick: a.kind === 'prompt'
|
|
675
|
+
? () => { focusInto('.sec-chat', null); sendChat(a.prompt); }
|
|
676
|
+
: () => focusInto(a.target === 'find' ? '.sec-find' : a.target === 'note' ? '.sec-note' : '.sec-next', a.target === 'find' ? '.sec-find-in' : 'textarea'),
|
|
677
|
+
})), [qa.activeActions, focusInto, sendChat]);
|
|
678
|
+
|
|
679
|
+
// Read what's ACTUALLY in the active context's workspaces (recent items per workspace) so the
|
|
680
|
+
// Secretary's answers reflect real content — not just goals/decisions/feed. One memory-list call.
|
|
681
|
+
const loadSpaceSnapshot = useCallback(async () => {
|
|
682
|
+
if (!active || !active.organismId) return '';
|
|
683
|
+
const r = await apiGet(`/v1/memory?prefix=${encodeURIComponent('organism.' + active.organismId + '.w.')}&_=${Date.now()}`).catch(() => null);
|
|
684
|
+
const items = (r && r.data && r.data.items) || [];
|
|
685
|
+
const byWs = {};
|
|
686
|
+
for (const it of items) {
|
|
687
|
+
const m = String(it.key || '').match(/\.w\.([^.]+)\.([^.]+)\./);
|
|
688
|
+
if (!m || m[2] === 'meta') continue; // skip per-workspace readme/meta records
|
|
689
|
+
const v = it.value || {};
|
|
690
|
+
const title = String(v.title || v.name || (v.body ? String(v.body).slice(0, 60) : '') || '').replace(/\s+/g, ' ').trim();
|
|
691
|
+
if (!title) continue;
|
|
692
|
+
(byWs[m[1]] = byWs[m[1]] || []).push(title);
|
|
693
|
+
}
|
|
694
|
+
return (wsList || []).map((w) => `- ${w.name}: ${(byWs[w.id] || []).slice(0, 6).join('; ') || '(empty)'}`).join('\n');
|
|
695
|
+
}, [active, wsList]);
|
|
696
|
+
|
|
697
|
+
// "Where do things stand?" — read-only orientation grounded in the brain + open goals/decisions +
|
|
698
|
+
// recent activity AND the real workspace content. No actions taken.
|
|
699
|
+
const runStand = useCallback(async () => {
|
|
700
|
+
if (!active) return;
|
|
701
|
+
setStand({ loading: true });
|
|
702
|
+
try {
|
|
703
|
+
// Quality policy: orientation needs a ≥200k model too (it reads the whole space). No big model →
|
|
704
|
+
// tell the owner instead of running a degraded read.
|
|
705
|
+
const cap = await getAiCapability();
|
|
706
|
+
if (!cap.bigEnough) { setStand({ needModel: true }); return; }
|
|
707
|
+
const openGoals = (learn.goals || []).filter((g) => g.status !== 'done').map((g) => '- ' + g.title).join('\n');
|
|
708
|
+
const openDecs = (learn.decisions || []).filter((d) => d.status !== 'reviewed').map((d) => '- ' + d.decision).join('\n');
|
|
709
|
+
const recent = (auto.feed || []).slice(0, 6).map((f) => '- ' + String(f.text || '').replace(/\s+/g, ' ').slice(0, 160)).join('\n');
|
|
710
|
+
const space = await loadSpaceSnapshot();
|
|
711
|
+
const wsUrl = (wsId) => (active.organismId ? `/v1/profile?tab=organisms&org=${encodeURIComponent(active.organismId)}${wsId ? `&ws=${encodeURIComponent(wsId)}` : ''}` : '');
|
|
712
|
+
const wsLinks = active.organismId ? (wsList || []).map((w) => `- "${w.name}" → ${wsUrl(w.id)}`).join('\n') : '';
|
|
713
|
+
const sys = `You are ${owner || 'the user'}'s personal Secretary in the "${active.name}" context. Give a SHORT, ORGANIZED orientation of where things stand right now. Use Markdown with 2–4 short sections or bullet groups (e.g. In progress / Open items / Needs attention / Recent) — NOT one long paragraph. Ground it in what's ACTUALLY in the workspaces below; do NOT say the space is empty if a workspace has content. When you mention a workspace, write it as a Markdown link using the "Workspace links" below so the owner can jump to it. Do NOT propose actions or claim to have done anything. Reply in ${getLocale() === 'fi' ? 'Finnish' : 'English'}.`;
|
|
714
|
+
const snapshot = `Context purpose: ${active.brain.purpose}\nWorkspace contents:\n${space || '(no workspaces)'}\nWorkspace links:\n${wsLinks || '(none)'}\nOpen goals:\n${openGoals || '(none)'}\nOpen decisions:\n${openDecs || '(none)'}\nRecent autonomous activity:\n${recent || '(none)'}`;
|
|
715
|
+
// Fact-based read → execution model, low temperature.
|
|
716
|
+
const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: snapshot, systemPrompt: sys, modelRole: 'execution', temperature: 0.2, app_id: 'secretary-orient' }), timeoutMs: 1_800_000, retries: 0 });
|
|
717
|
+
const text = ((r && r.data && r.data.content) || '…').trim();
|
|
718
|
+
// Verify the orientation against the facts (reasoning, temp 0) — flag any invented progress/items.
|
|
719
|
+
const check = await reasonJson('You fact-check a status orientation against the facts it was given. Return ONLY JSON {"ok":boolean,"issues":[string]}. ok=false if it states any progress, item, number, or outcome NOT present in the facts. Bracketed [placeholders] are fine.', `Facts:\n${snapshot}\n\nOrientation:\n${text}`, 'secretary-orient-verify');
|
|
720
|
+
const issues = (check && check.ok === false && Array.isArray(check.issues)) ? check.issues.slice(0, 5) : [];
|
|
721
|
+
const generatedAt = new Date().toISOString();
|
|
722
|
+
setStand({ text, generatedAt, issues });
|
|
723
|
+
// Persist so it stays visible across reloads (with its timestamp) — no need to regenerate each time.
|
|
724
|
+
if (active.id) await apiPost('/v1/memory', { key: `secretary.stand.${active.id}`, value: { text, generatedAt, issues }, visibility: 'private' }).catch(() => {});
|
|
725
|
+
} catch (e) {
|
|
726
|
+
setStand(null);
|
|
727
|
+
showToast(`${t('secretary.dash.standError')}: ${e.message}`, true);
|
|
728
|
+
}
|
|
729
|
+
}, [active, owner, learn.goals, learn.decisions, auto.feed, wsList, loadSpaceSnapshot, showToast]);
|
|
730
|
+
|
|
731
|
+
// "What's next?" — the Secretary proposes CONCRETE next actions it can DO (each with Do it / Skip),
|
|
732
|
+
// grounded in the real workspace content + goals/decisions/routines/follow-ups. Not prose to read —
|
|
733
|
+
// an actionable list. Doing one runs it (gather/file/surface); skipping drops it.
|
|
734
|
+
const NEXT_CAPS = ['discover', 'file_intake', 'curate_knowledge', 'briefing', 'reminders', 'create_resource', 'delegate'];
|
|
735
|
+
const runWhatsNext = useCallback(async () => {
|
|
736
|
+
if (!active) return;
|
|
737
|
+
setNextAns({ loading: true });
|
|
738
|
+
try {
|
|
739
|
+
// Deciding the next actions is a reasoning task → requires a ≥200k model. No big model → notice.
|
|
740
|
+
const cap = await getAiCapability();
|
|
741
|
+
if (!cap.bigEnough) { setNextAns({ needModel: true }); return; }
|
|
742
|
+
const openGoals = (learn.goals || []).filter((g) => g.status !== 'done').map((g) => '- ' + g.title).join('\n');
|
|
743
|
+
const dueDecs = (learn.decisions || []).filter((d) => d.status !== 'reviewed').map((d) => '- ' + d.decision).join('\n');
|
|
744
|
+
const routines = (next.activeRoutines || []).map((r) => { const s = next.nextPendingStep(r); return `- ${r.title}${s ? ` (next: ${s.summary})` : ''}`; }).join('\n');
|
|
745
|
+
const followups = (next.actionItems || []).map((a) => '- ' + (a.summary || a.text || '')).join('\n');
|
|
746
|
+
const space = await loadSpaceSnapshot();
|
|
747
|
+
const sys = `You are ${owner || 'the user'}'s personal Secretary in the "${active.name}" context. Propose the concrete NEXT ACTIONS to take now, grounded in the real workspace content + goals/decisions/routines/follow-ups below. Each action is something the owner can approve with one click. Return ONLY a JSON object EXACTLY like {"actions":[{"summary":"a short imperative action (in the user's language)","capability":"discover","why":"one short line why, in the user's language"}]}. Propose 2–5 actions. "capability" MUST be one of: ${NEXT_CAPS.join(', ')} — prefer "discover" to gather info on something, "file_intake"/"curate_knowledge" to record a plan/note, "reminders"/"briefing" to flag something. Do NOT invent capabilities. CRUCIAL: do NOT propose anything already present in the workspace content above — if a deliverable already exists there, propose the genuine NEXT step instead (use, refine, or move it forward), never redo it. Write every "summary" and "why" in ${getLocale() === 'fi' ? 'Finnish' : 'English'}. Output ONLY the JSON object.`;
|
|
748
|
+
const snapshot = `Context purpose: ${active.brain.purpose}\nWorkspace contents:\n${space || '(no workspaces)'}\nOpen goals:\n${openGoals || '(none)'}\nOpen decisions:\n${dueDecs || '(none)'}\nActive routines:\n${routines || '(none)'}\nOpen follow-ups:\n${followups || '(none)'}`;
|
|
749
|
+
// Reasoning/decision task → reasoning model, temperature 0 for sound, repeatable choices.
|
|
750
|
+
const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt: snapshot, systemPrompt: sys, modelRole: 'reasoning', temperature: 0, app_id: 'secretary-next' }), timeoutMs: 1_800_000, retries: 0 });
|
|
751
|
+
let parsed = null;
|
|
752
|
+
try { parsed = extractJson((r && r.data && r.data.content) || ''); } catch { /* fall through */ }
|
|
753
|
+
const raw = (parsed && Array.isArray(parsed.actions)) ? parsed.actions : [];
|
|
754
|
+
const actions = raw.slice(0, 6).map((a) => ({
|
|
755
|
+
id: 'na-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5),
|
|
756
|
+
summary: String((a && a.summary) || '').trim(),
|
|
757
|
+
capability: NEXT_CAPS.includes(a && a.capability) ? a.capability : 'file_intake',
|
|
758
|
+
why: String((a && a.why) || '').trim(),
|
|
759
|
+
status: 'open',
|
|
760
|
+
})).filter((a) => a.summary);
|
|
761
|
+
setNextAns({ actions, ctxId: active.id });
|
|
762
|
+
} catch (e) {
|
|
763
|
+
setNextAns(null);
|
|
764
|
+
showToast(`${t('secretary.next.error')}: ${e.message}`, true);
|
|
765
|
+
}
|
|
766
|
+
}, [active, owner, learn.goals, learn.decisions, next, loadSpaceSnapshot, showToast]);
|
|
767
|
+
|
|
768
|
+
// Carry out one proposed "What's next" action (band-spirit: gather/record/surface). Marks it done.
|
|
769
|
+
// ── Phase 3: "Design a workflow" — the Secretary composes a chain of agent+offer steps toward an
|
|
770
|
+
// outcome, proposes how to arm it, and saves it. ──
|
|
771
|
+
const openWfDesign = useCallback(() => setWfDesign((s) => s || { outcome: '' }), []);
|
|
772
|
+
const designWfFromStep = useCallback((step) => { setWfDesign({ outcome: (step && (step.summary || step.description)) || '' }); try { document.querySelector('.sec-wf')?.scrollIntoView({ behavior: 'smooth', block: 'center' }); } catch { /* noop */ } }, []);
|
|
773
|
+
const discardWfDesign = useCallback(() => setWfDesign(null), []);
|
|
774
|
+
const redoWfDesign = useCallback(() => setWfDesign((s) => ({ outcome: (s && s.outcome) || '' })), []);
|
|
775
|
+
const setWfOutcome = useCallback((v) => setWfDesign((s) => ({ ...(s || {}), outcome: v })), []);
|
|
776
|
+
const setWfTrigKind = useCallback((kind) => setWfDesign((s) => ({ ...s, trigger: kind === 'schedule' ? { kind: 'schedule', cron: (s.trigger && s.trigger.cron) || '0 9 * * *' } : kind === 'event' ? { kind: 'event', on: 'memory.write', match: {} } : { kind: 'manual' } })), []);
|
|
777
|
+
const setWfCron = useCallback((cron) => setWfDesign((s) => ({ ...s, trigger: { kind: 'schedule', cron } })), []);
|
|
778
|
+
const runWfDesign = useCallback(async () => {
|
|
779
|
+
const outcome = ((wfDesign && wfDesign.outcome) || '').trim();
|
|
780
|
+
if (!outcome) return;
|
|
781
|
+
setWfDesign((s) => ({ ...s, designing: true, error: null, draft: null }));
|
|
782
|
+
try {
|
|
783
|
+
const offers = await fetchWorkflowOffers();
|
|
784
|
+
const r = await designWorkflow({ outcome, offers, locale: getLocale() });
|
|
785
|
+
if (!r.ok) { setWfDesign((s) => ({ ...s, designing: false, error: r.error, title: r.title })); return; }
|
|
786
|
+
setWfDesign((s) => ({ ...s, designing: false, draft: r.def, trigger: r.def.trigger, errors: null }));
|
|
787
|
+
} catch (e) { setWfDesign((s) => ({ ...s, designing: false, error: 'fail' })); showToast(e.message, true); }
|
|
788
|
+
}, [wfDesign, showToast]);
|
|
789
|
+
const saveWfDesign = useCallback(async () => {
|
|
790
|
+
const d = wfDesign && wfDesign.draft;
|
|
791
|
+
if (!d) return;
|
|
792
|
+
setWfDesign((s) => ({ ...s, saving: true, errors: null }));
|
|
793
|
+
const id = slugifyWorkflowId(d.title || (wfDesign && wfDesign.outcome) || 'workflow');
|
|
794
|
+
const r = await saveDesignedWorkflow(id, { ...d, trigger: (wfDesign && wfDesign.trigger) || d.trigger });
|
|
795
|
+
if (!r.ok) { setWfDesign((s) => ({ ...s, saving: false, errors: r.errors })); return; }
|
|
796
|
+
setWfDesign((s) => ({ ...s, saving: false, saved: id }));
|
|
797
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update'));
|
|
798
|
+
}, [wfDesign]);
|
|
799
|
+
|
|
800
|
+
const doProposedAction = useCallback(async (action) => {
|
|
801
|
+
if (!active) return;
|
|
802
|
+
const patch = (st, extra) => setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, status: st, ...extra } : a)) } : s);
|
|
803
|
+
patch('doing');
|
|
804
|
+
// Append a visible line to the Home feed ("What I've done") so every Do-it leaves a trail — with a
|
|
805
|
+
// link to where the result landed when there is one.
|
|
806
|
+
const feedLog = async (text, href) => {
|
|
807
|
+
const fr = await apiGet('/v1/memory/secretary.feed').catch(() => null);
|
|
808
|
+
const items = (fr && fr.data && fr.data.value && Array.isArray(fr.data.value.items)) ? fr.data.value.items : [];
|
|
809
|
+
const entry = { id: 'f-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 4), ts: new Date().toISOString(), kind: 'act', contextId: active.id, contextName: active.name || '', text, ...(href ? { href } : {}) };
|
|
810
|
+
await apiPost('/v1/memory', { key: 'secretary.feed', value: { items: [entry, ...items].slice(0, 50) }, visibility: 'private' });
|
|
811
|
+
};
|
|
812
|
+
// Best-matching workspace for the action (by word overlap with its summary).
|
|
813
|
+
const pickWs = () => {
|
|
814
|
+
const wsl = intake.wsList || [];
|
|
815
|
+
const words = new Set(String(action.summary).toLowerCase().split(/[^a-z0-9äöå]+/i).filter((w) => w.length >= 4));
|
|
816
|
+
let ws = wsl[0]; let best = -1;
|
|
817
|
+
for (const w of wsl) { let sc = 0; const hay = String(w.name).toLowerCase(); for (const wd of words) if (hay.includes(wd)) sc++; if (sc > best) { best = sc; ws = w; } }
|
|
818
|
+
return ws;
|
|
819
|
+
};
|
|
820
|
+
// Deep link that opens the organism + the exact workspace in Profile › Organisms (new tab).
|
|
821
|
+
const spaceUrl = (wsId) => (active.organismId ? `/v1/profile?tab=organisms&org=${encodeURIComponent(active.organismId)}${wsId ? `&ws=${encodeURIComponent(wsId)}` : ''}` : null);
|
|
822
|
+
const fileNote = async (ws, title, body) => {
|
|
823
|
+
const id = 'note-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
824
|
+
const key = `organism.${active.organismId}.w.${ws.id}.notes.${id}`;
|
|
825
|
+
await apiPost('/v1/memory', { key, value: { id, title: String(title).slice(0, 80), body, createdAt: new Date().toISOString(), via: 'secretary-next' }, visibility: 'private' });
|
|
826
|
+
return key;
|
|
827
|
+
};
|
|
828
|
+
try {
|
|
829
|
+
const cap = action.capability;
|
|
830
|
+
let resultMsg = ''; let href = null; let preview = ''; let noteKey = null;
|
|
831
|
+
if (cap === 'discover') {
|
|
832
|
+
// Scout AND KEEP the results: file the top hits as a list the owner can open.
|
|
833
|
+
const d = await apiGet('/v1/discover?scope=public&per_page=10&q=' + encodeURIComponent(action.summary)).catch(() => null);
|
|
834
|
+
const entries = (d && d.data && Array.isArray(d.data.entries)) ? d.data.entries : [];
|
|
835
|
+
const ws = pickWs();
|
|
836
|
+
if (active.organismId && ws && entries.length) {
|
|
837
|
+
const list = entries.slice(0, 10).map((e) => `- ${e.title || e.id}${e.type ? ` (${e.type})` : ''}${e.url ? ` — ${e.url}` : ''}`).join('\n');
|
|
838
|
+
preview = `Found ${entries.length}:\n\n${list}`;
|
|
839
|
+
noteKey = await fileNote(ws, `Scouted: ${action.summary}`, preview);
|
|
840
|
+
resultMsg = t('secretary.next.didScoutedSaved', { n: entries.length, ws: ws.name }); href = spaceUrl(ws.id);
|
|
841
|
+
} else {
|
|
842
|
+
resultMsg = t('secretary.next.didDiscover', { n: entries.length });
|
|
843
|
+
}
|
|
844
|
+
await feedLog(`🔎 ${action.summary} — ${t('secretary.next.didDiscover', { n: entries.length })}`, href);
|
|
845
|
+
} else if (cap === 'briefing' || cap === 'reminders') {
|
|
846
|
+
await feedLog(`⚑ ${action.summary}`);
|
|
847
|
+
resultMsg = t('secretary.next.didSurface');
|
|
848
|
+
} else {
|
|
849
|
+
// file_intake / curate_knowledge / create_resource → run the multi-step QUALITY pipeline
|
|
850
|
+
// (triage → gather → produce → verify) on a ≥200k model, grounded + no fabrication. If no big
|
|
851
|
+
// model is configured, hand the owner a copy-paste prompt for the prompt-driven path instead.
|
|
852
|
+
const ws = pickWs();
|
|
853
|
+
const space = await loadSpaceSnapshot();
|
|
854
|
+
const runDiscover = async (q) => {
|
|
855
|
+
const d = await apiGet('/v1/discover?scope=public&per_page=10&q=' + encodeURIComponent(q)).catch(() => null);
|
|
856
|
+
const es = (d && d.data && Array.isArray(d.data.entries)) ? d.data.entries : [];
|
|
857
|
+
return es.slice(0, 10).map((e) => `- ${e.title || e.id}${e.type ? ` (${e.type})` : ''}${e.url ? ` — ${e.url}` : ''}`).join('\n');
|
|
858
|
+
};
|
|
859
|
+
const out = await produceDeliverable({ action, owner, contextName: active.name, locale: getLocale(), space, runDiscover });
|
|
860
|
+
if (out.mode === 'clarify') {
|
|
861
|
+
// Missing facts only the owner has → ask in the inbox (batch questions); the tick produces the
|
|
862
|
+
// deliverable once answered. Never guess.
|
|
863
|
+
await apiPost('/v1/secretary/clarify', { contextId: active.id, contextName: active.name || '', action: { summary: action.summary, why: action.why || '' }, questions: out.questions, facts: out.facts || space, organismId: active.organismId || '', wsId: (ws && ws.id) || '' });
|
|
864
|
+
patch('asked', {});
|
|
865
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update'));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
if (out.mode === 'prompt-driven') {
|
|
869
|
+
// No ≥200k model — never run a degraded completion; show the composed multi-step prompt to
|
|
870
|
+
// copy into a big AI chat, with a paste-back box to save the result.
|
|
871
|
+
patch('prompt', { promptText: out.prompt, expanded: true });
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
preview = out.content;
|
|
875
|
+
const issues = (out.verify && out.verify.ok === false && Array.isArray(out.verify.issues)) ? out.verify.issues.slice(0, 6) : [];
|
|
876
|
+
if (active.organismId && ws) {
|
|
877
|
+
noteKey = await fileNote(ws, action.summary, out.content);
|
|
878
|
+
resultMsg = t('secretary.next.didDrafted', { ws: ws.name }); href = spaceUrl(ws.id);
|
|
879
|
+
await feedLog(`✍️ ${action.summary} — ${t('secretary.next.didDrafted', { ws: ws.name })}`, href);
|
|
880
|
+
patch('done', { result: resultMsg, href, preview, expanded: true, noteKey, issues });
|
|
881
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update'));
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
resultMsg = t('secretary.next.didNoted');
|
|
885
|
+
}
|
|
886
|
+
patch('done', { result: resultMsg, href, preview, expanded: true, noteKey });
|
|
887
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update'));
|
|
888
|
+
} catch (e) {
|
|
889
|
+
patch('open');
|
|
890
|
+
showToast(`${t('secretary.next.error')}: ${e.message}`, true);
|
|
891
|
+
}
|
|
892
|
+
}, [active, owner, intake.wsList, loadSpaceSnapshot, showToast]);
|
|
893
|
+
|
|
894
|
+
const skipProposedAction = useCallback((action) => {
|
|
895
|
+
setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, status: 'skipped' } : a)) } : s);
|
|
896
|
+
}, []);
|
|
897
|
+
|
|
898
|
+
// Show/hide the produced deliverable inline under a done action (no navigation needed).
|
|
899
|
+
const togglePreview = useCallback((action) => {
|
|
900
|
+
setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, expanded: !a.expanded } : a)) } : s);
|
|
901
|
+
}, []);
|
|
902
|
+
|
|
903
|
+
const setPasteDraft = useCallback((id, v) => setPasteDrafts((m) => ({ ...m, [id]: v })), []);
|
|
904
|
+
|
|
905
|
+
// Prompt-driven path: file the result the owner pasted back from their big AI chat, as a note.
|
|
906
|
+
const savePromptResult = useCallback(async (action) => {
|
|
907
|
+
const text = (pasteDrafts[action.id] || '').trim();
|
|
908
|
+
if (!text || !active) return;
|
|
909
|
+
const wsl = intake.wsList || [];
|
|
910
|
+
const words = new Set(String(action.summary).toLowerCase().split(/[^a-z0-9äöå]+/i).filter((w) => w.length >= 4));
|
|
911
|
+
let ws = wsl[0]; let best = -1;
|
|
912
|
+
for (const w of wsl) { let sc = 0; const hay = String(w.name).toLowerCase(); for (const wd of words) if (hay.includes(wd)) sc++; if (sc > best) { best = sc; ws = w; } }
|
|
913
|
+
let noteKey = null;
|
|
914
|
+
if (active.organismId && ws) {
|
|
915
|
+
const id = 'note-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
916
|
+
noteKey = `organism.${active.organismId}.w.${ws.id}.notes.${id}`;
|
|
917
|
+
await apiPost('/v1/memory', { key: noteKey, value: { id, title: action.summary.slice(0, 80), body: text, createdAt: new Date().toISOString(), via: 'secretary-next' }, visibility: 'private' }).catch(() => {});
|
|
918
|
+
}
|
|
919
|
+
const href = active.organismId && ws ? `/v1/profile?tab=organisms&org=${encodeURIComponent(active.organismId)}&ws=${encodeURIComponent(ws.id)}` : null;
|
|
920
|
+
setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, status: 'done', promptText: '', preview: text, expanded: true, noteKey, result: ws ? t('secretary.next.didDrafted', { ws: ws.name }) : t('secretary.next.didNoted'), href } : a)) } : s);
|
|
921
|
+
setPasteDraft(action.id, '');
|
|
922
|
+
window.dispatchEvent(new CustomEvent('aimeat-live-update'));
|
|
923
|
+
}, [pasteDrafts, active, intake.wsList, setPasteDraft]);
|
|
924
|
+
|
|
925
|
+
// Discard a produced deliverable that's no good (e.g. an off-base draft): delete the filed note and
|
|
926
|
+
// clear the result from the list.
|
|
927
|
+
const discardProposedAction = useCallback(async (action) => {
|
|
928
|
+
if (action.noteKey) await apiDelete(`/v1/memory/${encodeURIComponent(action.noteKey)}`).catch(() => {});
|
|
929
|
+
setNextAns((s) => (s && s.actions) ? { ...s, actions: s.actions.map((a) => (a.id === action.id ? { ...a, status: 'discarded', preview: '', result: '', href: null, noteKey: null, expanded: false } : a)) } : s);
|
|
930
|
+
}, []);
|
|
931
|
+
|
|
289
932
|
const switchContext = useCallback(async (id) => {
|
|
290
933
|
const ctx = contexts.find((c) => c.id === id);
|
|
291
934
|
if (!ctx) return;
|
|
@@ -299,9 +942,56 @@ ${SECRETARY_AIMEAT_PRIMER}`;
|
|
|
299
942
|
persistConfig({ ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) });
|
|
300
943
|
};
|
|
301
944
|
const setBand = (capId, band) => updateActivePolicy({ bands: { ...policy.bands, [capId]: band } });
|
|
945
|
+
// One autonomy toggle: 'suggest' (surface only) | 'assist' (handle the small stuff). Sets the band preset
|
|
946
|
+
// underneath, so the band machinery + parity tests are unchanged — the toggle is just the simple surface.
|
|
947
|
+
const setAutonomy = (level) => updateActivePolicy({ bands: autonomyPreset(level) });
|
|
302
948
|
const toggleStop = () => updateActivePolicy({ stopSpending: !policy.stopSpending });
|
|
303
949
|
const setBudget = (v) => updateActivePolicy({ dailyMorselBudget: v === '' ? null : Math.max(0, Math.floor(Number(v) || 0)) });
|
|
304
950
|
|
|
951
|
+
// Strategy (optional steering frame). Stored on the active context as `strategy = { enabled, vision,
|
|
952
|
+
// mission, principles[], risks[], current, target, milestones:[{id,title,enables,goalRefs[],criterion,status}] }`.
|
|
953
|
+
// Milestones are ordered gates (a checkpoint, not a task — tasks live in goals). Shows only filled parts.
|
|
954
|
+
const strategy = (active && active.strategy && active.strategy.enabled) ? normalizeStrategy(active.strategy) : null;
|
|
955
|
+
const goalsById = useMemo(() => { const m = {}; for (const g of (learn.goals || [])) if (g && g.id) m[g.id] = g; return m; }, [learn.goals]);
|
|
956
|
+
const commitStrategy = (next) => {
|
|
957
|
+
if (!active) return;
|
|
958
|
+
const updated = { ...active, strategy: normalizeStrategy(next) };
|
|
959
|
+
persistConfig({ ...config, contexts: contexts.map((c) => (c.id === active.id ? updated : c)) });
|
|
960
|
+
};
|
|
961
|
+
const curStrat = () => normalizeStrategy(active && active.strategy);
|
|
962
|
+
const startStratEdit = () => { const s = curStrat(); setStratEdit({ vision: s.vision, mission: s.mission, current: s.current, target: s.target, principles: s.principles.join('\n'), risks: s.risks.join('\n') }); };
|
|
963
|
+
const saveStratEdit = () => {
|
|
964
|
+
if (!stratEdit) return;
|
|
965
|
+
const s = curStrat();
|
|
966
|
+
const changedTarget = stratEdit.target.trim() !== (s.target || '').trim();
|
|
967
|
+
commitStrategy({ ...s, enabled: true, vision: stratEdit.vision, mission: stratEdit.mission, current: stratEdit.current, target: stratEdit.target, principles: stratEdit.principles.split('\n'), risks: stratEdit.risks.split('\n') });
|
|
968
|
+
setStratEdit(null);
|
|
969
|
+
// Changing the target state often means the rest of the plan should be re-proposed — offer it.
|
|
970
|
+
if (changedTarget && stratEdit.target.trim()) setReplan({ note: t('secretary.strat.targetChanged') });
|
|
971
|
+
};
|
|
972
|
+
const cycleMilestone = (i) => { const s = curStrat(); const ms = [...s.milestones]; if (!ms[i]) return; const ni = (MILESTONE_STATUSES.indexOf(ms[i].status) + 1) % MILESTONE_STATUSES.length; ms[i] = { ...ms[i], status: MILESTONE_STATUSES[ni] }; commitStrategy({ ...s, milestones: ms }); };
|
|
973
|
+
const addMilestone = () => { const title = msInput.trim(); if (!title) return; const s = curStrat(); commitStrategy({ ...s, milestones: [...s.milestones, { title, enables: '', goalRefs: [], criterion: '', status: 'not-started' }] }); setMsInput(''); };
|
|
974
|
+
const removeMilestone = (i) => { const s = curStrat(); commitStrategy({ ...s, milestones: s.milestones.filter((_, j) => j !== i) }); };
|
|
975
|
+
const moveMilestone = (i, delta) => { const s = curStrat(); const ms = [...s.milestones]; const j = i + delta; if (j < 0 || j >= ms.length) return; [ms[i], ms[j]] = [ms[j], ms[i]]; commitStrategy({ ...s, milestones: ms }); };
|
|
976
|
+
const toggleMilestoneGoal = (i, goalId) => { const s = curStrat(); const ms = [...s.milestones]; if (!ms[i]) return; const refs = new Set(ms[i].goalRefs || []); if (refs.has(goalId)) refs.delete(goalId); else refs.add(goalId); ms[i] = { ...ms[i], goalRefs: [...refs] }; commitStrategy({ ...s, milestones: ms }); };
|
|
977
|
+
const enableStrategy = () => commitStrategy({ ...curStrat(), enabled: true });
|
|
978
|
+
const disableStrategy = () => { commitStrategy({ ...curStrat(), enabled: false }); setStratEdit(null); };
|
|
979
|
+
// Re-plan: change something (e.g. the target), let the AI re-propose a coherent strategy you can apply/edit.
|
|
980
|
+
const startReplan = () => setReplan({ note: '' });
|
|
981
|
+
const runReplan = async () => {
|
|
982
|
+
const note = (replan && replan.note || '').trim(); if (!note) return;
|
|
983
|
+
setReplan((r) => ({ ...r, busy: true }));
|
|
984
|
+
try {
|
|
985
|
+
const prompt = buildStrategyReplanPrompt(owner, active && active.strategy, note, learn.goals);
|
|
986
|
+
const r = await api('/v1/ai/complete', { method: 'POST', body: JSON.stringify({ prompt, app_id: 'secretary-strategy-replan' }), timeoutMs: 1_800_000, retries: 0 });
|
|
987
|
+
const j = extractJson((r && r.data && r.data.content) || '');
|
|
988
|
+
// Re-plan links milestones to EXISTING goal ids — keep only refs that point at a real goal.
|
|
989
|
+
const draft = keepKnownGoalRefs(normalizeStrategy(j.strategy || j), (learn.goals || []).map((g) => g && g.id));
|
|
990
|
+
setReplan({ draft });
|
|
991
|
+
} catch (e) { showToast(`${t('secretary.strat.replanErr')}: ${e.message}`, true); setReplan(null); }
|
|
992
|
+
};
|
|
993
|
+
const applyReplan = () => { if (replan && replan.draft) commitStrategy(replan.draft); setReplan(null); };
|
|
994
|
+
|
|
305
995
|
const restore = useCallback(async (snap) => {
|
|
306
996
|
if (!active) return;
|
|
307
997
|
setApplying(true);
|
|
@@ -339,30 +1029,132 @@ ${SECRETARY_AIMEAT_PRIMER}`;
|
|
|
339
1029
|
${secretary === undefined
|
|
340
1030
|
? html`<div class="sec-empty">…</div>`
|
|
341
1031
|
: secretary === null
|
|
342
|
-
? html`<div class="sec-empty">${t('secretary.notReady')}</div>`
|
|
1032
|
+
? html`<div class="sec-empty">${hasOpenRouterKey ? t('secretary.provisioning') : t('secretary.notReady')}</div>`
|
|
343
1033
|
: html`
|
|
344
1034
|
${hired ? contextSwitcher({ contexts, activeId, switchContext, openAdd }) : null}
|
|
345
|
-
${showHirePanel ? hirePanel({ firstEver, hireMode, owner, needs, setNeeds, result, setResult, applying, generating, generateInApp, applyResult, onCancel: cancelHire }) : null}
|
|
1035
|
+
${showHirePanel ? hirePanel({ firstEver, hireMode, owner, current: hireMode === 'edit' ? active : null, needs, setNeeds, result, setResult, applying, setupStep, generating, generateInApp, applyResult: startSetup, onCancel: cancelHire }) : null}
|
|
346
1036
|
${hired && !showHire && active ? html`
|
|
347
|
-
${
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
${
|
|
359
|
-
${
|
|
360
|
-
|
|
361
|
-
${
|
|
362
|
-
${
|
|
363
|
-
|
|
364
|
-
${
|
|
1037
|
+
${/* Quick-action row (core verbs; dynamic actions arrive in B3) */ ''}
|
|
1038
|
+
${quickActionRow({ items: [
|
|
1039
|
+
{ key: 'next', label: t('secretary.next.title'), primary: true, disabled: !!(nextAns && nextAns.loading), title: t('secretary.next.hint'), onClick: runWhatsNext },
|
|
1040
|
+
{ key: 'stand', label: t('secretary.dash.quickStand'), disabled: !!(stand && stand.loading), onClick: runStand },
|
|
1041
|
+
{ key: 'find', label: t('secretary.dash.quickFind'), title: t('secretary.findTitle'), onClick: () => focusInto('.sec-find', '.sec-find-in') },
|
|
1042
|
+
{ key: 'note', label: t('secretary.dash.quickNote'), title: t('secretary.noteTitle'), hidden: wsList.length === 0, onClick: () => focusInto('.sec-note', 'textarea') },
|
|
1043
|
+
{ key: 'review', label: t('secretary.dash.quickReview'), title: t('secretary.learn.reviewNow'), hidden: !decisionsDue, onClick: () => focusInto('.sec-decisions-log', null) },
|
|
1044
|
+
{ key: 'design-wf', label: t('secretary.wf.quick'), title: t('secretary.wf.title'), onClick: openWfDesign },
|
|
1045
|
+
...dynamicQuickItems,
|
|
1046
|
+
{ key: 'manage-qa', label: '✎', title: t('secretary.qa.title'), onClick: qa.toggleManage },
|
|
1047
|
+
] })}
|
|
1048
|
+
${quickActionsManager(qa)}
|
|
1049
|
+
${workflowDesignPanel({ state: wfDesign, onOutcome: setWfOutcome, onDesign: runWfDesign, onTrigKind: setWfTrigKind, onCron: setWfCron, onSave: saveWfDesign, onRedo: redoWfDesign, onDiscard: discardWfDesign })}
|
|
1050
|
+
|
|
1051
|
+
${/* Today — full-width status band, always on top */ ''}
|
|
1052
|
+
${dashStatus({ reliability, budgetInfo, schedule: auto.schedule, lastScan, stale, onReconcile: reconcile, scanning })}
|
|
1053
|
+
|
|
1054
|
+
${/* Heavy work to hand off — the Work Brief tray (the light-vs-heavy seam made physical). */ ''}
|
|
1055
|
+
${briefTrayCard({ briefs, builders, dispatching, onDispatch: dispatchBrief, onResolve: resolveBrief })}
|
|
1056
|
+
|
|
1057
|
+
${/* Customizable two-column dashboard (pin to column / reorder / hide per card; persisted). */ ''}
|
|
1058
|
+
${(() => {
|
|
1059
|
+
const nodes = {
|
|
1060
|
+
strategy: strategy ? strategyCard({
|
|
1061
|
+
strategy, goals: learn.goals, goalsById,
|
|
1062
|
+
edit: stratEdit, onStartEdit: startStratEdit, onEditField: (k, v) => setStratEdit((d) => ({ ...(d || {}), [k]: v })), onSaveEdit: saveStratEdit, onCancelEdit: () => setStratEdit(null),
|
|
1063
|
+
onCycle: cycleMilestone, msInput, onMsInput: setMsInput, onAdd: addMilestone, onRemove: removeMilestone, onMove: moveMilestone, onToggleGoal: toggleMilestoneGoal,
|
|
1064
|
+
replan, onStartReplan: startReplan, onReplanNote: (v) => setReplan((r) => ({ ...(r || {}), note: v })), onRunReplan: runReplan, onApplyReplan: applyReplan, onCancelReplan: () => setReplan(null),
|
|
1065
|
+
onDisable: disableStrategy,
|
|
1066
|
+
}) : null,
|
|
1067
|
+
whatsNext: whatsNextPanel({ answer: nextAns, onDo: doProposedAction, onSkip: skipProposedAction, onTogglePreview: togglePreview, onDiscard: discardProposedAction, pasteDrafts, onPasteInput: setPasteDraft, onSavePrompt: savePromptResult, onDismiss: () => { setNextAns(null); if (activeId) apiDelete(`/v1/memory/${encodeURIComponent('secretary.next.' + activeId)}`).catch(() => {}); } }),
|
|
1068
|
+
stand: standPanel({ stand, onRefresh: runStand, onDismiss: () => setStand(null) }),
|
|
1069
|
+
actionItems: actionItemsCard(next),
|
|
1070
|
+
routines: routinesCard({ ...next, onDesignWorkflow: designWfFromStep }),
|
|
1071
|
+
decisions: intake.pendingIds.length > 0 ? decisionsCard(intake) : null,
|
|
1072
|
+
automation: automationCard({ ...auto, budgetInfo }),
|
|
1073
|
+
triggers: triggersCard({ ...trig, goals: learn.goals, routines: next.routines, specialists: spec.list }),
|
|
1074
|
+
specialists: specialistsCard({ ...spec }),
|
|
1075
|
+
calendar: calendarCard({ ...cal, feed: auto.feed, schedule: auto.schedule, routines: next.routines, triggers: trig.armed, onEventClick: setCalEvent }),
|
|
1076
|
+
feed: feedCard(auto),
|
|
1077
|
+
goals: goalsCard(learn),
|
|
1078
|
+
decisionLog: decisionLogCard(learn),
|
|
1079
|
+
chat: chatCard({ activeName: active.name, chat, chatSending, chatInput, setChatInput, sendChat, routeSuggestion, switchContext,
|
|
1080
|
+
onAttach: intake.handleAttach, attaching: intake.attaching, attachResult: intake.attachResult, canAttach: intake.wsList.length > 0 }),
|
|
1081
|
+
whatsNextCard: whatsNextCard({ ...next, agents: crew.agents }),
|
|
1082
|
+
find: findCard({ findQ, setFindQ, findScope, setFindScope, finding, doFind, findResults }),
|
|
1083
|
+
createResource: ((findResults && findResults.length === 0) || create.draft || create.created) ? createResourceCard({ ...create, query: findQ }) : null,
|
|
1084
|
+
note: wsList.length > 0 ? noteCard(intake) : null,
|
|
1085
|
+
};
|
|
1086
|
+
const present = lay.layout.filter((e) => nodes[e.key] != null && !e.hidden);
|
|
1087
|
+
const mainCol = present.filter((e) => e.col === 'main');
|
|
1088
|
+
const rightCol = present.filter((e) => e.col === 'right');
|
|
1089
|
+
const renderCol = (list) => list.map((e, i) => LayoutCard({ entry: e, node: nodes[e.key], prevKey: i > 0 ? list[i - 1].key : null, nextKey: i < list.length - 1 ? list[i + 1].key : null, onSwap: lay.swap, onMoveCol: lay.moveCol, onHide: lay.hide }));
|
|
1090
|
+
return html`
|
|
1091
|
+
<div class="sec-grid">
|
|
1092
|
+
<div class="sec-col sec-col-main">${renderCol(mainCol)}</div>
|
|
1093
|
+
<aside class="sec-col sec-col-right">${renderCol(rightCol)}</aside>
|
|
1094
|
+
</div>
|
|
1095
|
+
<div class="sec-layout-bar">
|
|
1096
|
+
${lay.hidden.map((e) => html`<button class="sec-chip" key=${e.key} onClick=${() => lay.unhide(e.key)}>+ ${t('secretary.layout.section.' + e.key)}</button>`)}
|
|
1097
|
+
<button class="sec-linkbtn" onClick=${lay.reset}>${t('secretary.layout.reset')}</button>
|
|
1098
|
+
</div>`;
|
|
1099
|
+
})()}
|
|
1100
|
+
|
|
1101
|
+
${/* Manage & setup — collapsed disclosure (set up once) */ ''}
|
|
1102
|
+
${manageHeader({ open: manageOpen, onToggle: () => setManageOpen((v) => !v),
|
|
1103
|
+
crewSummary: crew.agents && crew.agents.length ? `${crew.agents.length} ${t('secretary.dash.crewAgents')}` : '' })}
|
|
1104
|
+
${manageOpen ? html`
|
|
1105
|
+
${brainCard({ brain, active, openEdit, brainDraft, setBrainDraft, startBrainEdit, saveBrain, cancelBrainEdit, savingBrain })}
|
|
1106
|
+
${operatingCard({ policy, toggleStop, setBudget, setBand, setAutonomy })}
|
|
1107
|
+
${!strategy ? html`<section class="sec-card">
|
|
1108
|
+
<h2 class="sec-h2">${t('secretary.strat.title')}</h2>
|
|
1109
|
+
<p class="sec-hint">${t('secretary.strat.enableHint')}</p>
|
|
1110
|
+
<button class="btn-outline btn-sm" onClick=${enableStrategy}>${t('secretary.strat.enable')}</button>
|
|
1111
|
+
</section>` : null}
|
|
1112
|
+
${crewCard(crew)}
|
|
1113
|
+
${knowledgeCard(knowledge)}
|
|
1114
|
+
${accessCard(access)}
|
|
1115
|
+
${(Array.isArray(active.brainHistory) && active.brainHistory.length > 0) ? historyCard({ brainHistory: active.brainHistory, applying, restore }) : null}
|
|
1116
|
+
${metaCard({ secretary, reliability })}
|
|
1117
|
+
${secretaryDangerCard({
|
|
1118
|
+
show: resetState.show, text: resetState.text, busy: resetState.busy,
|
|
1119
|
+
onOpen: () => setResetState({ show: true, text: '', busy: false }),
|
|
1120
|
+
onCancel: () => setResetState({ show: false, text: '', busy: false }),
|
|
1121
|
+
onText: (v) => setResetState((s) => ({ ...s, text: v })),
|
|
1122
|
+
onConfirm: doReset,
|
|
1123
|
+
})}
|
|
1124
|
+
` : null}
|
|
365
1125
|
` : null}`}
|
|
1126
|
+
${calEvent ? html`<${CalendarEventDialog} event=${calEvent} onClose=${() => setCalEvent(null)} triggers=${{ update: trig.updateTrigger, togglePause: trig.togglePause, remove: trig.removeTrigger }} workflows=${trig.workflows} specialists=${spec.list} routines=${{ setCadence: next.setRoutineCadence, setStatus: next.setRoutineStatus }} tick=${{ toggle: auto.toggleTick, run: auto.runTick }} feed=${{ remove: auto.deleteFeedItem }} />` : null}
|
|
1127
|
+
${chooseMode ? html`
|
|
1128
|
+
<div class="sec-setup-overlay" onClick=${(e) => { if (e.target === e.currentTarget) setChooseMode(false); }}>
|
|
1129
|
+
<div class="sec-setup-box sec-mode-box">
|
|
1130
|
+
<h3 class="sec-h2">${t('secretary.mode.title')}</h3>
|
|
1131
|
+
<p class="sec-desc">${t('secretary.mode.intro')}</p>
|
|
1132
|
+
${[['draft', t('secretary.mode.draftTitle'), t('secretary.mode.draftDesc')], ['act', t('secretary.mode.actTitle'), t('secretary.mode.actDesc')], ['off', t('secretary.mode.offTitle'), t('secretary.mode.offDesc')]].map(([val, title, desc]) => html`
|
|
1133
|
+
<label class=${'sec-mode-opt' + (setupMode === val ? ' sec-mode-opt--on' : '')}>
|
|
1134
|
+
<input type="radio" name="setupmode" checked=${setupMode === val} onChange=${() => setSetupMode(val)} />
|
|
1135
|
+
<span><span class="sec-mode-opt-t">${title}</span><span class="sec-desc">${desc}</span></span>
|
|
1136
|
+
</label>`)}
|
|
1137
|
+
<p class="sec-hint">${t('secretary.mode.changeLater')}</p>
|
|
1138
|
+
<div class="sec-actions">
|
|
1139
|
+
<button class="btn-ghost btn-sm" onClick=${() => setChooseMode(false)}>${t('secretary.cancel')}</button>
|
|
1140
|
+
<button class="btn-primary" onClick=${confirmSetup}>${t('secretary.apply')}</button>
|
|
1141
|
+
</div>
|
|
1142
|
+
</div>
|
|
1143
|
+
</div>` : null}
|
|
1144
|
+
${applying && setupStep ? (() => {
|
|
1145
|
+
// Top-level setup-progress overlay — stays visible across the hire→dashboard switch (persistConfig
|
|
1146
|
+
// makes the Secretary "exist" before the slow fill, so a panel-local progress view would unmount).
|
|
1147
|
+
const s = setupStep, pct = s.total ? Math.round(((s.done || 0) / s.total) * 100) : null;
|
|
1148
|
+
const msg = s.phase === 'fill'
|
|
1149
|
+
? `${t('secretary.stepFilling')} ${(s.done || 0) + 1}/${s.total}${s.name ? ' · ' + s.name : ''}`
|
|
1150
|
+
: (s.phase === 'done' ? t('secretary.stepDone') : t('secretary.stepStructure'));
|
|
1151
|
+
return html`<div class="sec-setup-overlay"><div class="sec-setup-box">
|
|
1152
|
+
<h3 class="sec-h2">${t('secretary.settingUpTitle')}</h3>
|
|
1153
|
+
<div class="sec-setup-row"><span class="sec-spin"></span><span>${msg}</span></div>
|
|
1154
|
+
${pct != null ? html`<div class="sec-progress-bar"><div class="sec-progress-fill" style=${`width:${pct}%`}></div></div>` : null}
|
|
1155
|
+
<p class="sec-desc">${t('secretary.settingUpHint')}</p>
|
|
1156
|
+
</div></div>`;
|
|
1157
|
+
})() : null}
|
|
366
1158
|
<${ToastContainer} />
|
|
367
1159
|
</div>`;
|
|
368
1160
|
}
|