@runcore-sh/runcore 0.1.2
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/LICENSE +21 -0
- package/README.md +353 -0
- package/dist/activity/log.d.ts +37 -0
- package/dist/activity/log.d.ts.map +1 -0
- package/dist/activity/log.js +259 -0
- package/dist/activity/log.js.map +1 -0
- package/dist/adapters/storage/gdrive-backup.d.ts +20 -0
- package/dist/adapters/storage/gdrive-backup.d.ts.map +1 -0
- package/dist/adapters/storage/gdrive-backup.js +244 -0
- package/dist/adapters/storage/gdrive-backup.js.map +1 -0
- package/dist/adapters/storage/local.d.ts +19 -0
- package/dist/adapters/storage/local.d.ts.map +1 -0
- package/dist/adapters/storage/local.js +101 -0
- package/dist/adapters/storage/local.js.map +1 -0
- package/dist/adapters/storage/types.d.ts +44 -0
- package/dist/adapters/storage/types.d.ts.map +1 -0
- package/dist/adapters/storage/types.js +6 -0
- package/dist/adapters/storage/types.js.map +1 -0
- package/dist/agents/autonomous.d.ts +67 -0
- package/dist/agents/autonomous.d.ts.map +1 -0
- package/dist/agents/autonomous.js +710 -0
- package/dist/agents/autonomous.js.map +1 -0
- package/dist/agents/commit.d.ts +22 -0
- package/dist/agents/commit.d.ts.map +1 -0
- package/dist/agents/commit.js +120 -0
- package/dist/agents/commit.js.map +1 -0
- package/dist/agents/continue.d.ts +19 -0
- package/dist/agents/continue.d.ts.map +1 -0
- package/dist/agents/continue.js +158 -0
- package/dist/agents/continue.js.map +1 -0
- package/dist/agents/cooldown.d.ts +127 -0
- package/dist/agents/cooldown.d.ts.map +1 -0
- package/dist/agents/cooldown.js +396 -0
- package/dist/agents/cooldown.js.map +1 -0
- package/dist/agents/dedup-guard.d.ts +15 -0
- package/dist/agents/dedup-guard.d.ts.map +1 -0
- package/dist/agents/dedup-guard.js +128 -0
- package/dist/agents/dedup-guard.js.map +1 -0
- package/dist/agents/index.d.ts +34 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +51 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/instance-manager.d.ts +262 -0
- package/dist/agents/instance-manager.d.ts.map +1 -0
- package/dist/agents/instance-manager.js +850 -0
- package/dist/agents/instance-manager.js.map +1 -0
- package/dist/agents/locks.d.ts +81 -0
- package/dist/agents/locks.d.ts.map +1 -0
- package/dist/agents/locks.js +234 -0
- package/dist/agents/locks.js.map +1 -0
- package/dist/agents/memory.d.ts +37 -0
- package/dist/agents/memory.d.ts.map +1 -0
- package/dist/agents/memory.js +92 -0
- package/dist/agents/memory.js.map +1 -0
- package/dist/agents/monitor.d.ts +16 -0
- package/dist/agents/monitor.d.ts.map +1 -0
- package/dist/agents/monitor.js +235 -0
- package/dist/agents/monitor.js.map +1 -0
- package/dist/agents/orchestration.d.ts +218 -0
- package/dist/agents/orchestration.d.ts.map +1 -0
- package/dist/agents/orchestration.js +715 -0
- package/dist/agents/orchestration.js.map +1 -0
- package/dist/agents/recover.d.ts +30 -0
- package/dist/agents/recover.d.ts.map +1 -0
- package/dist/agents/recover.js +166 -0
- package/dist/agents/recover.js.map +1 -0
- package/dist/agents/reflection.d.ts +36 -0
- package/dist/agents/reflection.d.ts.map +1 -0
- package/dist/agents/reflection.js +198 -0
- package/dist/agents/reflection.js.map +1 -0
- package/dist/agents/runtime/bus.d.ts +46 -0
- package/dist/agents/runtime/bus.d.ts.map +1 -0
- package/dist/agents/runtime/bus.js +174 -0
- package/dist/agents/runtime/bus.js.map +1 -0
- package/dist/agents/runtime/config.d.ts +14 -0
- package/dist/agents/runtime/config.d.ts.map +1 -0
- package/dist/agents/runtime/config.js +100 -0
- package/dist/agents/runtime/config.js.map +1 -0
- package/dist/agents/runtime/driver.d.ts +25 -0
- package/dist/agents/runtime/driver.d.ts.map +1 -0
- package/dist/agents/runtime/driver.js +215 -0
- package/dist/agents/runtime/driver.js.map +1 -0
- package/dist/agents/runtime/errors.d.ts +30 -0
- package/dist/agents/runtime/errors.d.ts.map +1 -0
- package/dist/agents/runtime/errors.js +40 -0
- package/dist/agents/runtime/errors.js.map +1 -0
- package/dist/agents/runtime/index.d.ts +29 -0
- package/dist/agents/runtime/index.d.ts.map +1 -0
- package/dist/agents/runtime/index.js +54 -0
- package/dist/agents/runtime/index.js.map +1 -0
- package/dist/agents/runtime/lifecycle.d.ts +46 -0
- package/dist/agents/runtime/lifecycle.d.ts.map +1 -0
- package/dist/agents/runtime/lifecycle.js +116 -0
- package/dist/agents/runtime/lifecycle.js.map +1 -0
- package/dist/agents/runtime/manager.d.ts +129 -0
- package/dist/agents/runtime/manager.d.ts.map +1 -0
- package/dist/agents/runtime/manager.js +947 -0
- package/dist/agents/runtime/manager.js.map +1 -0
- package/dist/agents/runtime/registry.d.ts +66 -0
- package/dist/agents/runtime/registry.d.ts.map +1 -0
- package/dist/agents/runtime/registry.js +195 -0
- package/dist/agents/runtime/registry.js.map +1 -0
- package/dist/agents/runtime/resources.d.ts +49 -0
- package/dist/agents/runtime/resources.d.ts.map +1 -0
- package/dist/agents/runtime/resources.js +146 -0
- package/dist/agents/runtime/resources.js.map +1 -0
- package/dist/agents/runtime/types.d.ts +168 -0
- package/dist/agents/runtime/types.d.ts.map +1 -0
- package/dist/agents/runtime/types.js +24 -0
- package/dist/agents/runtime/types.js.map +1 -0
- package/dist/agents/runtime.d.ts +240 -0
- package/dist/agents/runtime.d.ts.map +1 -0
- package/dist/agents/runtime.js +577 -0
- package/dist/agents/runtime.js.map +1 -0
- package/dist/agents/spawn.d.ts +49 -0
- package/dist/agents/spawn.d.ts.map +1 -0
- package/dist/agents/spawn.js +975 -0
- package/dist/agents/spawn.js.map +1 -0
- package/dist/agents/store.d.ts +29 -0
- package/dist/agents/store.d.ts.map +1 -0
- package/dist/agents/store.js +174 -0
- package/dist/agents/store.js.map +1 -0
- package/dist/agents/triage.d.ts +23 -0
- package/dist/agents/triage.d.ts.map +1 -0
- package/dist/agents/triage.js +81 -0
- package/dist/agents/triage.js.map +1 -0
- package/dist/agents/types.d.ts +37 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/agents/workflow.d.ts +137 -0
- package/dist/agents/workflow.d.ts.map +1 -0
- package/dist/agents/workflow.js +542 -0
- package/dist/agents/workflow.js.map +1 -0
- package/dist/auth/crypto.d.ts +22 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +42 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/identity.d.ts +89 -0
- package/dist/auth/identity.d.ts.map +1 -0
- package/dist/auth/identity.js +264 -0
- package/dist/auth/identity.js.map +1 -0
- package/dist/avatar/client.d.ts +34 -0
- package/dist/avatar/client.d.ts.map +1 -0
- package/dist/avatar/client.js +172 -0
- package/dist/avatar/client.js.map +1 -0
- package/dist/avatar/sidecar.d.ts +16 -0
- package/dist/avatar/sidecar.d.ts.map +1 -0
- package/dist/avatar/sidecar.js +125 -0
- package/dist/avatar/sidecar.js.map +1 -0
- package/dist/board/provider.d.ts +13 -0
- package/dist/board/provider.d.ts.map +1 -0
- package/dist/board/provider.js +19 -0
- package/dist/board/provider.js.map +1 -0
- package/dist/board/types.d.ts +76 -0
- package/dist/board/types.d.ts.map +1 -0
- package/dist/board/types.js +7 -0
- package/dist/board/types.js.map +1 -0
- package/dist/brain/skills.d.ts +177 -0
- package/dist/brain/skills.d.ts.map +1 -0
- package/dist/brain/skills.js +452 -0
- package/dist/brain/skills.js.map +1 -0
- package/dist/brain.d.ts +42 -0
- package/dist/brain.d.ts.map +1 -0
- package/dist/brain.js +98 -0
- package/dist/brain.js.map +1 -0
- package/dist/browser/sessions.d.ts +23 -0
- package/dist/browser/sessions.d.ts.map +1 -0
- package/dist/browser/sessions.js +121 -0
- package/dist/browser/sessions.js.map +1 -0
- package/dist/cache/file.d.ts +56 -0
- package/dist/cache/file.d.ts.map +1 -0
- package/dist/cache/file.js +176 -0
- package/dist/cache/file.js.map +1 -0
- package/dist/cache/index.d.ts +64 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +108 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/keys.d.ts +29 -0
- package/dist/cache/keys.d.ts.map +1 -0
- package/dist/cache/keys.js +52 -0
- package/dist/cache/keys.js.map +1 -0
- package/dist/cache/llm-cache.d.ts +70 -0
- package/dist/cache/llm-cache.d.ts.map +1 -0
- package/dist/cache/llm-cache.js +165 -0
- package/dist/cache/llm-cache.js.map +1 -0
- package/dist/cache/memory.d.ts +53 -0
- package/dist/cache/memory.d.ts.map +1 -0
- package/dist/cache/memory.js +114 -0
- package/dist/cache/memory.js.map +1 -0
- package/dist/calendar/google-adapter.d.ts +16 -0
- package/dist/calendar/google-adapter.d.ts.map +1 -0
- package/dist/calendar/google-adapter.js +163 -0
- package/dist/calendar/google-adapter.js.map +1 -0
- package/dist/calendar/index.d.ts +8 -0
- package/dist/calendar/index.d.ts.map +1 -0
- package/dist/calendar/index.js +7 -0
- package/dist/calendar/index.js.map +1 -0
- package/dist/calendar/routes.d.ts +7 -0
- package/dist/calendar/routes.d.ts.map +1 -0
- package/dist/calendar/routes.js +199 -0
- package/dist/calendar/routes.js.map +1 -0
- package/dist/calendar/store.d.ts +73 -0
- package/dist/calendar/store.d.ts.map +1 -0
- package/dist/calendar/store.js +373 -0
- package/dist/calendar/store.js.map +1 -0
- package/dist/calendar/types.d.ts +99 -0
- package/dist/calendar/types.d.ts.map +1 -0
- package/dist/calendar/types.js +7 -0
- package/dist/calendar/types.js.map +1 -0
- package/dist/capabilities/definitions/board.d.ts +7 -0
- package/dist/capabilities/definitions/board.d.ts.map +1 -0
- package/dist/capabilities/definitions/board.js +232 -0
- package/dist/capabilities/definitions/board.js.map +1 -0
- package/dist/capabilities/definitions/browser.d.ts +18 -0
- package/dist/capabilities/definitions/browser.d.ts.map +1 -0
- package/dist/capabilities/definitions/browser.js +242 -0
- package/dist/capabilities/definitions/browser.js.map +1 -0
- package/dist/capabilities/definitions/calendar-context.d.ts +8 -0
- package/dist/capabilities/definitions/calendar-context.d.ts.map +1 -0
- package/dist/capabilities/definitions/calendar-context.js +41 -0
- package/dist/capabilities/definitions/calendar-context.js.map +1 -0
- package/dist/capabilities/definitions/calendar.d.ts +7 -0
- package/dist/capabilities/definitions/calendar.d.ts.map +1 -0
- package/dist/capabilities/definitions/calendar.js +173 -0
- package/dist/capabilities/definitions/calendar.js.map +1 -0
- package/dist/capabilities/definitions/docs.d.ts +6 -0
- package/dist/capabilities/definitions/docs.d.ts.map +1 -0
- package/dist/capabilities/definitions/docs.js +62 -0
- package/dist/capabilities/definitions/docs.js.map +1 -0
- package/dist/capabilities/definitions/email-context.d.ts +7 -0
- package/dist/capabilities/definitions/email-context.d.ts.map +1 -0
- package/dist/capabilities/definitions/email-context.js +55 -0
- package/dist/capabilities/definitions/email-context.js.map +1 -0
- package/dist/capabilities/definitions/email.d.ts +7 -0
- package/dist/capabilities/definitions/email.d.ts.map +1 -0
- package/dist/capabilities/definitions/email.js +94 -0
- package/dist/capabilities/definitions/email.js.map +1 -0
- package/dist/capabilities/definitions/task-done.d.ts +10 -0
- package/dist/capabilities/definitions/task-done.d.ts.map +1 -0
- package/dist/capabilities/definitions/task-done.js +83 -0
- package/dist/capabilities/definitions/task-done.js.map +1 -0
- package/dist/capabilities/definitions/vault-context.d.ts +12 -0
- package/dist/capabilities/definitions/vault-context.d.ts.map +1 -0
- package/dist/capabilities/definitions/vault-context.js +62 -0
- package/dist/capabilities/definitions/vault-context.js.map +1 -0
- package/dist/capabilities/definitions/web-search-context.d.ts +22 -0
- package/dist/capabilities/definitions/web-search-context.d.ts.map +1 -0
- package/dist/capabilities/definitions/web-search-context.js +60 -0
- package/dist/capabilities/definitions/web-search-context.js.map +1 -0
- package/dist/capabilities/index.d.ts +18 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +21 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/registry.d.ts +84 -0
- package/dist/capabilities/registry.d.ts.map +1 -0
- package/dist/capabilities/registry.js +248 -0
- package/dist/capabilities/registry.js.map +1 -0
- package/dist/capabilities/types.d.ts +157 -0
- package/dist/capabilities/types.d.ts.map +1 -0
- package/dist/capabilities/types.js +35 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/channels/whatsapp.d.ts +88 -0
- package/dist/channels/whatsapp.d.ts.map +1 -0
- package/dist/channels/whatsapp.js +200 -0
- package/dist/channels/whatsapp.js.map +1 -0
- package/dist/cli/backup.d.ts +13 -0
- package/dist/cli/backup.d.ts.map +1 -0
- package/dist/cli/backup.js +176 -0
- package/dist/cli/backup.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +231 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/defaults.d.ts +45 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +54 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/contacts/index.d.ts +3 -0
- package/dist/contacts/index.d.ts.map +1 -0
- package/dist/contacts/index.js +2 -0
- package/dist/contacts/index.js.map +1 -0
- package/dist/contacts/store.d.ts +58 -0
- package/dist/contacts/store.d.ts.map +1 -0
- package/dist/contacts/store.js +278 -0
- package/dist/contacts/store.js.map +1 -0
- package/dist/contacts/types.d.ts +47 -0
- package/dist/contacts/types.d.ts.map +1 -0
- package/dist/contacts/types.js +5 -0
- package/dist/contacts/types.js.map +1 -0
- package/dist/context/assembler.d.ts +26 -0
- package/dist/context/assembler.d.ts.map +1 -0
- package/dist/context/assembler.js +65 -0
- package/dist/context/assembler.js.map +1 -0
- package/dist/context/compaction.d.ts +34 -0
- package/dist/context/compaction.d.ts.map +1 -0
- package/dist/context/compaction.js +84 -0
- package/dist/context/compaction.js.map +1 -0
- package/dist/context/index.d.ts +3 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +2 -0
- package/dist/context/index.js.map +1 -0
- package/dist/core/registry/index.d.ts +12 -0
- package/dist/core/registry/index.d.ts.map +1 -0
- package/dist/core/registry/index.js +14 -0
- package/dist/core/registry/index.js.map +1 -0
- package/dist/core/registry/publisher.d.ts +22 -0
- package/dist/core/registry/publisher.d.ts.map +1 -0
- package/dist/core/registry/publisher.js +195 -0
- package/dist/core/registry/publisher.js.map +1 -0
- package/dist/core/registry/registry.d.ts +92 -0
- package/dist/core/registry/registry.d.ts.map +1 -0
- package/dist/core/registry/registry.js +254 -0
- package/dist/core/registry/registry.js.map +1 -0
- package/dist/core/registry/search.d.ts +12 -0
- package/dist/core/registry/search.d.ts.map +1 -0
- package/dist/core/registry/search.js +132 -0
- package/dist/core/registry/search.js.map +1 -0
- package/dist/core/registry/store.d.ts +55 -0
- package/dist/core/registry/store.d.ts.map +1 -0
- package/dist/core/registry/store.js +185 -0
- package/dist/core/registry/store.js.map +1 -0
- package/dist/core/registry/types.d.ts +141 -0
- package/dist/core/registry/types.d.ts.map +1 -0
- package/dist/core/registry/types.js +30 -0
- package/dist/core/registry/types.js.map +1 -0
- package/dist/core/registry/versions.d.ts +56 -0
- package/dist/core/registry/versions.d.ts.map +1 -0
- package/dist/core/registry/versions.js +101 -0
- package/dist/core/registry/versions.js.map +1 -0
- package/dist/credentials/store.d.ts +59 -0
- package/dist/credentials/store.d.ts.map +1 -0
- package/dist/credentials/store.js +178 -0
- package/dist/credentials/store.js.map +1 -0
- package/dist/files/agent-api.d.ts +50 -0
- package/dist/files/agent-api.d.ts.map +1 -0
- package/dist/files/agent-api.js +126 -0
- package/dist/files/agent-api.js.map +1 -0
- package/dist/files/compress.d.ts +20 -0
- package/dist/files/compress.d.ts.map +1 -0
- package/dist/files/compress.js +83 -0
- package/dist/files/compress.js.map +1 -0
- package/dist/files/extract.d.ts +11 -0
- package/dist/files/extract.d.ts.map +1 -0
- package/dist/files/extract.js +33 -0
- package/dist/files/extract.js.map +1 -0
- package/dist/files/gdrive.d.ts +56 -0
- package/dist/files/gdrive.d.ts.map +1 -0
- package/dist/files/gdrive.js +246 -0
- package/dist/files/gdrive.js.map +1 -0
- package/dist/files/ingest-folder.d.ts +22 -0
- package/dist/files/ingest-folder.d.ts.map +1 -0
- package/dist/files/ingest-folder.js +71 -0
- package/dist/files/ingest-folder.js.map +1 -0
- package/dist/files/ingest.d.ts +13 -0
- package/dist/files/ingest.d.ts.map +1 -0
- package/dist/files/ingest.js +127 -0
- package/dist/files/ingest.js.map +1 -0
- package/dist/files/manager.d.ts +117 -0
- package/dist/files/manager.d.ts.map +1 -0
- package/dist/files/manager.js +306 -0
- package/dist/files/manager.js.map +1 -0
- package/dist/files/store.d.ts +41 -0
- package/dist/files/store.d.ts.map +1 -0
- package/dist/files/store.js +271 -0
- package/dist/files/store.js.map +1 -0
- package/dist/files/templates.d.ts +45 -0
- package/dist/files/templates.d.ts.map +1 -0
- package/dist/files/templates.js +179 -0
- package/dist/files/templates.js.map +1 -0
- package/dist/files/types.d.ts +115 -0
- package/dist/files/types.d.ts.map +1 -0
- package/dist/files/types.js +20 -0
- package/dist/files/types.js.map +1 -0
- package/dist/files/validate.d.ts +15 -0
- package/dist/files/validate.d.ts.map +1 -0
- package/dist/files/validate.js +213 -0
- package/dist/files/validate.js.map +1 -0
- package/dist/files/version.d.ts +31 -0
- package/dist/files/version.d.ts.map +1 -0
- package/dist/files/version.js +129 -0
- package/dist/files/version.js.map +1 -0
- package/dist/github/client.d.ts +83 -0
- package/dist/github/client.d.ts.map +1 -0
- package/dist/github/client.js +408 -0
- package/dist/github/client.js.map +1 -0
- package/dist/github/commit-analysis.d.ts +30 -0
- package/dist/github/commit-analysis.d.ts.map +1 -0
- package/dist/github/commit-analysis.js +276 -0
- package/dist/github/commit-analysis.js.map +1 -0
- package/dist/github/contributor-stats.d.ts +18 -0
- package/dist/github/contributor-stats.d.ts.map +1 -0
- package/dist/github/contributor-stats.js +119 -0
- package/dist/github/contributor-stats.js.map +1 -0
- package/dist/github/issue-sla.d.ts +25 -0
- package/dist/github/issue-sla.d.ts.map +1 -0
- package/dist/github/issue-sla.js +220 -0
- package/dist/github/issue-sla.js.map +1 -0
- package/dist/github/issue-triage.d.ts +49 -0
- package/dist/github/issue-triage.d.ts.map +1 -0
- package/dist/github/issue-triage.js +286 -0
- package/dist/github/issue-triage.js.map +1 -0
- package/dist/github/pr-readiness.d.ts +18 -0
- package/dist/github/pr-readiness.d.ts.map +1 -0
- package/dist/github/pr-readiness.js +197 -0
- package/dist/github/pr-readiness.js.map +1 -0
- package/dist/github/pr-review.d.ts +17 -0
- package/dist/github/pr-review.d.ts.map +1 -0
- package/dist/github/pr-review.js +410 -0
- package/dist/github/pr-review.js.map +1 -0
- package/dist/github/release-notes.d.ts +32 -0
- package/dist/github/release-notes.d.ts.map +1 -0
- package/dist/github/release-notes.js +227 -0
- package/dist/github/release-notes.js.map +1 -0
- package/dist/github/repo-health.d.ts +17 -0
- package/dist/github/repo-health.d.ts.map +1 -0
- package/dist/github/repo-health.js +303 -0
- package/dist/github/repo-health.js.map +1 -0
- package/dist/github/retry.d.ts +39 -0
- package/dist/github/retry.d.ts.map +1 -0
- package/dist/github/retry.js +117 -0
- package/dist/github/retry.js.map +1 -0
- package/dist/github/types.d.ts +527 -0
- package/dist/github/types.d.ts.map +1 -0
- package/dist/github/types.js +8 -0
- package/dist/github/types.js.map +1 -0
- package/dist/github/webhooks.d.ts +36 -0
- package/dist/github/webhooks.d.ts.map +1 -0
- package/dist/github/webhooks.js +153 -0
- package/dist/github/webhooks.js.map +1 -0
- package/dist/goals/loop.d.ts +27 -0
- package/dist/goals/loop.d.ts.map +1 -0
- package/dist/goals/loop.js +239 -0
- package/dist/goals/loop.js.map +1 -0
- package/dist/goals/notifications.d.ts +20 -0
- package/dist/goals/notifications.d.ts.map +1 -0
- package/dist/goals/notifications.js +101 -0
- package/dist/goals/notifications.js.map +1 -0
- package/dist/goals/timer.d.ts +21 -0
- package/dist/goals/timer.d.ts.map +1 -0
- package/dist/goals/timer.js +60 -0
- package/dist/goals/timer.js.map +1 -0
- package/dist/google/auth.d.ts +84 -0
- package/dist/google/auth.d.ts.map +1 -0
- package/dist/google/auth.js +323 -0
- package/dist/google/auth.js.map +1 -0
- package/dist/google/calendar-timer.d.ts +20 -0
- package/dist/google/calendar-timer.d.ts.map +1 -0
- package/dist/google/calendar-timer.js +91 -0
- package/dist/google/calendar-timer.js.map +1 -0
- package/dist/google/calendar.d.ts +126 -0
- package/dist/google/calendar.d.ts.map +1 -0
- package/dist/google/calendar.js +270 -0
- package/dist/google/calendar.js.map +1 -0
- package/dist/google/docs.d.ts +87 -0
- package/dist/google/docs.d.ts.map +1 -0
- package/dist/google/docs.js +309 -0
- package/dist/google/docs.js.map +1 -0
- package/dist/google/gmail-send.d.ts +58 -0
- package/dist/google/gmail-send.d.ts.map +1 -0
- package/dist/google/gmail-send.js +219 -0
- package/dist/google/gmail-send.js.map +1 -0
- package/dist/google/gmail-timer.d.ts +34 -0
- package/dist/google/gmail-timer.d.ts.map +1 -0
- package/dist/google/gmail-timer.js +223 -0
- package/dist/google/gmail-timer.js.map +1 -0
- package/dist/google/gmail.d.ts +172 -0
- package/dist/google/gmail.d.ts.map +1 -0
- package/dist/google/gmail.js +470 -0
- package/dist/google/gmail.js.map +1 -0
- package/dist/google/tasks-timer.d.ts +20 -0
- package/dist/google/tasks-timer.d.ts.map +1 -0
- package/dist/google/tasks-timer.js +107 -0
- package/dist/google/tasks-timer.js.map +1 -0
- package/dist/google/tasks.d.ts +167 -0
- package/dist/google/tasks.d.ts.map +1 -0
- package/dist/google/tasks.js +331 -0
- package/dist/google/tasks.js.map +1 -0
- package/dist/google/temporal.d.ts +76 -0
- package/dist/google/temporal.d.ts.map +1 -0
- package/dist/google/temporal.js +176 -0
- package/dist/google/temporal.js.map +1 -0
- package/dist/health/alert-defaults.d.ts +12 -0
- package/dist/health/alert-defaults.d.ts.map +1 -0
- package/dist/health/alert-defaults.js +88 -0
- package/dist/health/alert-defaults.js.map +1 -0
- package/dist/health/alert-types.d.ts +97 -0
- package/dist/health/alert-types.d.ts.map +1 -0
- package/dist/health/alert-types.js +8 -0
- package/dist/health/alert-types.js.map +1 -0
- package/dist/health/alerting.d.ts +66 -0
- package/dist/health/alerting.d.ts.map +1 -0
- package/dist/health/alerting.js +373 -0
- package/dist/health/alerting.js.map +1 -0
- package/dist/health/checker.d.ts +32 -0
- package/dist/health/checker.d.ts.map +1 -0
- package/dist/health/checker.js +138 -0
- package/dist/health/checker.js.map +1 -0
- package/dist/health/checks/openrouter.d.ts +29 -0
- package/dist/health/checks/openrouter.d.ts.map +1 -0
- package/dist/health/checks/openrouter.js +75 -0
- package/dist/health/checks/openrouter.js.map +1 -0
- package/dist/health/checks.d.ts +26 -0
- package/dist/health/checks.d.ts.map +1 -0
- package/dist/health/checks.js +122 -0
- package/dist/health/checks.js.map +1 -0
- package/dist/health/components.d.ts +38 -0
- package/dist/health/components.d.ts.map +1 -0
- package/dist/health/components.js +112 -0
- package/dist/health/components.js.map +1 -0
- package/dist/health/index.d.ts +19 -0
- package/dist/health/index.d.ts.map +1 -0
- package/dist/health/index.js +23 -0
- package/dist/health/index.js.map +1 -0
- package/dist/health/recovery.d.ts +42 -0
- package/dist/health/recovery.d.ts.map +1 -0
- package/dist/health/recovery.js +138 -0
- package/dist/health/recovery.js.map +1 -0
- package/dist/health/types.d.ts +68 -0
- package/dist/health/types.d.ts.map +1 -0
- package/dist/health/types.js +5 -0
- package/dist/health/types.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/instance.d.ts +24 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +48 -0
- package/dist/instance.js.map +1 -0
- package/dist/integrations/github.d.ts +83 -0
- package/dist/integrations/github.d.ts.map +1 -0
- package/dist/integrations/github.js +331 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/google-tasks.d.ts +232 -0
- package/dist/integrations/google-tasks.d.ts.map +1 -0
- package/dist/integrations/google-tasks.js +432 -0
- package/dist/integrations/google-tasks.js.map +1 -0
- package/dist/learning/extractor.d.ts +28 -0
- package/dist/learning/extractor.d.ts.map +1 -0
- package/dist/learning/extractor.js +135 -0
- package/dist/learning/extractor.js.map +1 -0
- package/dist/lib/BasePlugin.d.ts +58 -0
- package/dist/lib/BasePlugin.d.ts.map +1 -0
- package/dist/lib/BasePlugin.js +181 -0
- package/dist/lib/BasePlugin.js.map +1 -0
- package/dist/lib/PluginRegistry.d.ts +56 -0
- package/dist/lib/PluginRegistry.d.ts.map +1 -0
- package/dist/lib/PluginRegistry.js +172 -0
- package/dist/lib/PluginRegistry.js.map +1 -0
- package/dist/lib/brain-io.d.ts +60 -0
- package/dist/lib/brain-io.d.ts.map +1 -0
- package/dist/lib/brain-io.js +180 -0
- package/dist/lib/brain-io.js.map +1 -0
- package/dist/lib/encryption-config.d.ts +16 -0
- package/dist/lib/encryption-config.d.ts.map +1 -0
- package/dist/lib/encryption-config.js +40 -0
- package/dist/lib/encryption-config.js.map +1 -0
- package/dist/lib/encryption.d.ts +19 -0
- package/dist/lib/encryption.d.ts.map +1 -0
- package/dist/lib/encryption.js +65 -0
- package/dist/lib/encryption.js.map +1 -0
- package/dist/lib/key-store.d.ts +24 -0
- package/dist/lib/key-store.d.ts.map +1 -0
- package/dist/lib/key-store.js +38 -0
- package/dist/lib/key-store.js.map +1 -0
- package/dist/lib/schema-migration.d.ts +34 -0
- package/dist/lib/schema-migration.d.ts.map +1 -0
- package/dist/lib/schema-migration.js +77 -0
- package/dist/lib/schema-migration.js.map +1 -0
- package/dist/library/brain-shadow.d.ts +10 -0
- package/dist/library/brain-shadow.d.ts.map +1 -0
- package/dist/library/brain-shadow.js +158 -0
- package/dist/library/brain-shadow.js.map +1 -0
- package/dist/library/index.d.ts +7 -0
- package/dist/library/index.d.ts.map +1 -0
- package/dist/library/index.js +6 -0
- package/dist/library/index.js.map +1 -0
- package/dist/library/routes.d.ts +7 -0
- package/dist/library/routes.d.ts.map +1 -0
- package/dist/library/routes.js +473 -0
- package/dist/library/routes.js.map +1 -0
- package/dist/library/store.d.ts +54 -0
- package/dist/library/store.d.ts.map +1 -0
- package/dist/library/store.js +403 -0
- package/dist/library/store.js.map +1 -0
- package/dist/library/types.d.ts +56 -0
- package/dist/library/types.d.ts.map +1 -0
- package/dist/library/types.js +12 -0
- package/dist/library/types.js.map +1 -0
- package/dist/llm/cache.d.ts +42 -0
- package/dist/llm/cache.d.ts.map +1 -0
- package/dist/llm/cache.js +104 -0
- package/dist/llm/cache.js.map +1 -0
- package/dist/llm/complete.d.ts +24 -0
- package/dist/llm/complete.d.ts.map +1 -0
- package/dist/llm/complete.js +56 -0
- package/dist/llm/complete.js.map +1 -0
- package/dist/llm/errors.d.ts +28 -0
- package/dist/llm/errors.d.ts.map +1 -0
- package/dist/llm/errors.js +82 -0
- package/dist/llm/errors.js.map +1 -0
- package/dist/llm/ollama.d.ts +21 -0
- package/dist/llm/ollama.d.ts.map +1 -0
- package/dist/llm/ollama.js +116 -0
- package/dist/llm/ollama.js.map +1 -0
- package/dist/llm/openrouter.d.ts +13 -0
- package/dist/llm/openrouter.d.ts.map +1 -0
- package/dist/llm/openrouter.js +105 -0
- package/dist/llm/openrouter.js.map +1 -0
- package/dist/llm/providers/anthropic.d.ts +8 -0
- package/dist/llm/providers/anthropic.d.ts.map +1 -0
- package/dist/llm/providers/anthropic.js +189 -0
- package/dist/llm/providers/anthropic.js.map +1 -0
- package/dist/llm/providers/index.d.ts +20 -0
- package/dist/llm/providers/index.d.ts.map +1 -0
- package/dist/llm/providers/index.js +47 -0
- package/dist/llm/providers/index.js.map +1 -0
- package/dist/llm/providers/ollama.d.ts +13 -0
- package/dist/llm/providers/ollama.d.ts.map +1 -0
- package/dist/llm/providers/ollama.js +188 -0
- package/dist/llm/providers/ollama.js.map +1 -0
- package/dist/llm/providers/openai.d.ts +8 -0
- package/dist/llm/providers/openai.d.ts.map +1 -0
- package/dist/llm/providers/openai.js +144 -0
- package/dist/llm/providers/openai.js.map +1 -0
- package/dist/llm/providers/openrouter.d.ts +7 -0
- package/dist/llm/providers/openrouter.d.ts.map +1 -0
- package/dist/llm/providers/openrouter.js +158 -0
- package/dist/llm/providers/openrouter.js.map +1 -0
- package/dist/llm/providers/types.d.ts +29 -0
- package/dist/llm/providers/types.d.ts.map +1 -0
- package/dist/llm/providers/types.js +6 -0
- package/dist/llm/providers/types.js.map +1 -0
- package/dist/llm/retry.d.ts +29 -0
- package/dist/llm/retry.d.ts.map +1 -0
- package/dist/llm/retry.js +139 -0
- package/dist/llm/retry.js.map +1 -0
- package/dist/memory/file-backed.d.ts +36 -0
- package/dist/memory/file-backed.d.ts.map +1 -0
- package/dist/memory/file-backed.js +178 -0
- package/dist/memory/file-backed.js.map +1 -0
- package/dist/memory/index.d.ts +7 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +6 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/long-term.d.ts +38 -0
- package/dist/memory/long-term.d.ts.map +1 -0
- package/dist/memory/long-term.js +58 -0
- package/dist/memory/long-term.js.map +1 -0
- package/dist/memory/vector-index.d.ts +55 -0
- package/dist/memory/vector-index.d.ts.map +1 -0
- package/dist/memory/vector-index.js +207 -0
- package/dist/memory/vector-index.js.map +1 -0
- package/dist/memory/visual.d.ts +53 -0
- package/dist/memory/visual.d.ts.map +1 -0
- package/dist/memory/visual.js +218 -0
- package/dist/memory/visual.js.map +1 -0
- package/dist/memory/working.d.ts +12 -0
- package/dist/memory/working.d.ts.map +1 -0
- package/dist/memory/working.js +34 -0
- package/dist/memory/working.js.map +1 -0
- package/dist/metrics/aggregator.d.ts +58 -0
- package/dist/metrics/aggregator.d.ts.map +1 -0
- package/dist/metrics/aggregator.js +253 -0
- package/dist/metrics/aggregator.js.map +1 -0
- package/dist/metrics/collector.d.ts +26 -0
- package/dist/metrics/collector.d.ts.map +1 -0
- package/dist/metrics/collector.js +346 -0
- package/dist/metrics/collector.js.map +1 -0
- package/dist/metrics/firewall-metrics.d.ts +95 -0
- package/dist/metrics/firewall-metrics.d.ts.map +1 -0
- package/dist/metrics/firewall-metrics.js +261 -0
- package/dist/metrics/firewall-metrics.js.map +1 -0
- package/dist/metrics/index.d.ts +20 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +23 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instruments.d.ts +89 -0
- package/dist/metrics/instruments.d.ts.map +1 -0
- package/dist/metrics/instruments.js +172 -0
- package/dist/metrics/instruments.js.map +1 -0
- package/dist/metrics/middleware.d.ts +12 -0
- package/dist/metrics/middleware.d.ts.map +1 -0
- package/dist/metrics/middleware.js +47 -0
- package/dist/metrics/middleware.js.map +1 -0
- package/dist/metrics/prometheus.d.ts +39 -0
- package/dist/metrics/prometheus.d.ts.map +1 -0
- package/dist/metrics/prometheus.js +115 -0
- package/dist/metrics/prometheus.js.map +1 -0
- package/dist/metrics/registry.d.ts +58 -0
- package/dist/metrics/registry.d.ts.map +1 -0
- package/dist/metrics/registry.js +145 -0
- package/dist/metrics/registry.js.map +1 -0
- package/dist/metrics/reporter.d.ts +21 -0
- package/dist/metrics/reporter.d.ts.map +1 -0
- package/dist/metrics/reporter.js +207 -0
- package/dist/metrics/reporter.js.map +1 -0
- package/dist/metrics/store.d.ts +47 -0
- package/dist/metrics/store.d.ts.map +1 -0
- package/dist/metrics/store.js +209 -0
- package/dist/metrics/store.js.map +1 -0
- package/dist/metrics/system.d.ts +20 -0
- package/dist/metrics/system.d.ts.map +1 -0
- package/dist/metrics/system.js +109 -0
- package/dist/metrics/system.js.map +1 -0
- package/dist/metrics/types.d.ts +101 -0
- package/dist/metrics/types.d.ts.map +1 -0
- package/dist/metrics/types.js +6 -0
- package/dist/metrics/types.js.map +1 -0
- package/dist/modules/index.d.ts +6 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +6 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/registry.d.ts +36 -0
- package/dist/modules/registry.d.ts.map +1 -0
- package/dist/modules/registry.js +155 -0
- package/dist/modules/registry.js.map +1 -0
- package/dist/modules/types.d.ts +37 -0
- package/dist/modules/types.d.ts.map +1 -0
- package/dist/modules/types.js +9 -0
- package/dist/modules/types.js.map +1 -0
- package/dist/notifications/channel.d.ts +25 -0
- package/dist/notifications/channel.d.ts.map +1 -0
- package/dist/notifications/channel.js +83 -0
- package/dist/notifications/channel.js.map +1 -0
- package/dist/notifications/email.d.ts +27 -0
- package/dist/notifications/email.d.ts.map +1 -0
- package/dist/notifications/email.js +72 -0
- package/dist/notifications/email.js.map +1 -0
- package/dist/notifications/index.d.ts +16 -0
- package/dist/notifications/index.d.ts.map +1 -0
- package/dist/notifications/index.js +12 -0
- package/dist/notifications/index.js.map +1 -0
- package/dist/notifications/phone.d.ts +16 -0
- package/dist/notifications/phone.d.ts.map +1 -0
- package/dist/notifications/phone.js +48 -0
- package/dist/notifications/phone.js.map +1 -0
- package/dist/notifications/sms.d.ts +26 -0
- package/dist/notifications/sms.d.ts.map +1 -0
- package/dist/notifications/sms.js +65 -0
- package/dist/notifications/sms.js.map +1 -0
- package/dist/notifications/webhook.d.ts +28 -0
- package/dist/notifications/webhook.d.ts.map +1 -0
- package/dist/notifications/webhook.js +65 -0
- package/dist/notifications/webhook.js.map +1 -0
- package/dist/openloop/foldback.d.ts +24 -0
- package/dist/openloop/foldback.d.ts.map +1 -0
- package/dist/openloop/foldback.js +127 -0
- package/dist/openloop/foldback.js.map +1 -0
- package/dist/openloop/index.d.ts +11 -0
- package/dist/openloop/index.d.ts.map +1 -0
- package/dist/openloop/index.js +9 -0
- package/dist/openloop/index.js.map +1 -0
- package/dist/openloop/lifecycle.d.ts +16 -0
- package/dist/openloop/lifecycle.d.ts.map +1 -0
- package/dist/openloop/lifecycle.js +304 -0
- package/dist/openloop/lifecycle.js.map +1 -0
- package/dist/openloop/resolution-scanner.d.ts +17 -0
- package/dist/openloop/resolution-scanner.d.ts.map +1 -0
- package/dist/openloop/resolution-scanner.js +551 -0
- package/dist/openloop/resolution-scanner.js.map +1 -0
- package/dist/openloop/scanner.d.ts +28 -0
- package/dist/openloop/scanner.d.ts.map +1 -0
- package/dist/openloop/scanner.js +587 -0
- package/dist/openloop/scanner.js.map +1 -0
- package/dist/openloop/store.d.ts +41 -0
- package/dist/openloop/store.d.ts.map +1 -0
- package/dist/openloop/store.js +154 -0
- package/dist/openloop/store.js.map +1 -0
- package/dist/openloop/types.d.ts +94 -0
- package/dist/openloop/types.d.ts.map +1 -0
- package/dist/openloop/types.js +6 -0
- package/dist/openloop/types.js.map +1 -0
- package/dist/plugins/google-tasks/index.d.ts +55 -0
- package/dist/plugins/google-tasks/index.d.ts.map +1 -0
- package/dist/plugins/google-tasks/index.js +135 -0
- package/dist/plugins/google-tasks/index.js.map +1 -0
- package/dist/pulse/activation-event.d.ts +66 -0
- package/dist/pulse/activation-event.d.ts.map +1 -0
- package/dist/pulse/activation-event.js +139 -0
- package/dist/pulse/activation-event.js.map +1 -0
- package/dist/pulse/activation-log.d.ts +37 -0
- package/dist/pulse/activation-log.d.ts.map +1 -0
- package/dist/pulse/activation-log.js +101 -0
- package/dist/pulse/activation-log.js.map +1 -0
- package/dist/pulse/index.d.ts +11 -0
- package/dist/pulse/index.d.ts.map +1 -0
- package/dist/pulse/index.js +13 -0
- package/dist/pulse/index.js.map +1 -0
- package/dist/pulse/pressure.d.ts +69 -0
- package/dist/pulse/pressure.d.ts.map +1 -0
- package/dist/pulse/pressure.js +304 -0
- package/dist/pulse/pressure.js.map +1 -0
- package/dist/pulse/types.d.ts +89 -0
- package/dist/pulse/types.d.ts.map +1 -0
- package/dist/pulse/types.js +6 -0
- package/dist/pulse/types.js.map +1 -0
- package/dist/queue/grooming.d.ts +16 -0
- package/dist/queue/grooming.d.ts.map +1 -0
- package/dist/queue/grooming.js +269 -0
- package/dist/queue/grooming.js.map +1 -0
- package/dist/queue/provider.d.ts +50 -0
- package/dist/queue/provider.d.ts.map +1 -0
- package/dist/queue/provider.js +131 -0
- package/dist/queue/provider.js.map +1 -0
- package/dist/queue/store.d.ts +97 -0
- package/dist/queue/store.d.ts.map +1 -0
- package/dist/queue/store.js +448 -0
- package/dist/queue/store.js.map +1 -0
- package/dist/queue/types.d.ts +47 -0
- package/dist/queue/types.d.ts.map +1 -0
- package/dist/queue/types.js +22 -0
- package/dist/queue/types.js.map +1 -0
- package/dist/rate-limit.d.ts +26 -0
- package/dist/rate-limit.d.ts.map +1 -0
- package/dist/rate-limit.js +74 -0
- package/dist/rate-limit.js.map +1 -0
- package/dist/registry/discovery.d.ts +30 -0
- package/dist/registry/discovery.d.ts.map +1 -0
- package/dist/registry/discovery.js +171 -0
- package/dist/registry/discovery.js.map +1 -0
- package/dist/registry/index.d.ts +14 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +18 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/installer.d.ts +45 -0
- package/dist/registry/installer.d.ts.map +1 -0
- package/dist/registry/installer.js +175 -0
- package/dist/registry/installer.js.map +1 -0
- package/dist/registry/registry.d.ts +70 -0
- package/dist/registry/registry.d.ts.map +1 -0
- package/dist/registry/registry.js +153 -0
- package/dist/registry/registry.js.map +1 -0
- package/dist/registry/store.d.ts +69 -0
- package/dist/registry/store.d.ts.map +1 -0
- package/dist/registry/store.js +242 -0
- package/dist/registry/store.js.map +1 -0
- package/dist/registry/types.d.ts +116 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +9 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/registry/validator.d.ts +24 -0
- package/dist/registry/validator.d.ts.map +1 -0
- package/dist/registry/validator.js +203 -0
- package/dist/registry/validator.js.map +1 -0
- package/dist/scheduling/index.d.ts +4 -0
- package/dist/scheduling/index.d.ts.map +1 -0
- package/dist/scheduling/index.js +3 -0
- package/dist/scheduling/index.js.map +1 -0
- package/dist/scheduling/store.d.ts +41 -0
- package/dist/scheduling/store.d.ts.map +1 -0
- package/dist/scheduling/store.js +237 -0
- package/dist/scheduling/store.js.map +1 -0
- package/dist/scheduling/timer.d.ts +25 -0
- package/dist/scheduling/timer.d.ts.map +1 -0
- package/dist/scheduling/timer.js +118 -0
- package/dist/scheduling/timer.js.map +1 -0
- package/dist/scheduling/types.d.ts +43 -0
- package/dist/scheduling/types.d.ts.map +1 -0
- package/dist/scheduling/types.js +5 -0
- package/dist/scheduling/types.js.map +1 -0
- package/dist/search/brain-docs.d.ts +20 -0
- package/dist/search/brain-docs.d.ts.map +1 -0
- package/dist/search/brain-docs.js +103 -0
- package/dist/search/brain-docs.js.map +1 -0
- package/dist/search/browse.d.ts +45 -0
- package/dist/search/browse.d.ts.map +1 -0
- package/dist/search/browse.js +225 -0
- package/dist/search/browse.js.map +1 -0
- package/dist/search/classify.d.ts +23 -0
- package/dist/search/classify.d.ts.map +1 -0
- package/dist/search/classify.js +132 -0
- package/dist/search/classify.js.map +1 -0
- package/dist/search/client.d.ts +32 -0
- package/dist/search/client.d.ts.map +1 -0
- package/dist/search/client.js +72 -0
- package/dist/search/client.js.map +1 -0
- package/dist/search/perplexity.d.ts +13 -0
- package/dist/search/perplexity.d.ts.map +1 -0
- package/dist/search/perplexity.js +41 -0
- package/dist/search/perplexity.js.map +1 -0
- package/dist/search/sidecar.d.ts +20 -0
- package/dist/search/sidecar.d.ts.map +1 -0
- package/dist/search/sidecar.js +103 -0
- package/dist/search/sidecar.js.map +1 -0
- package/dist/server.d.ts +8 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +4851 -0
- package/dist/server.js.map +1 -0
- package/dist/services/backlogReview.d.ts +66 -0
- package/dist/services/backlogReview.d.ts.map +1 -0
- package/dist/services/backlogReview.js +285 -0
- package/dist/services/backlogReview.js.map +1 -0
- package/dist/services/backup.d.ts +43 -0
- package/dist/services/backup.d.ts.map +1 -0
- package/dist/services/backup.js +334 -0
- package/dist/services/backup.js.map +1 -0
- package/dist/services/credit-monitor.d.ts +40 -0
- package/dist/services/credit-monitor.d.ts.map +1 -0
- package/dist/services/credit-monitor.js +147 -0
- package/dist/services/credit-monitor.js.map +1 -0
- package/dist/services/morningBriefing.d.ts +125 -0
- package/dist/services/morningBriefing.d.ts.map +1 -0
- package/dist/services/morningBriefing.js +660 -0
- package/dist/services/morningBriefing.js.map +1 -0
- package/dist/services/routine-patterns.d.ts +21 -0
- package/dist/services/routine-patterns.d.ts.map +1 -0
- package/dist/services/routine-patterns.js +46 -0
- package/dist/services/routine-patterns.js.map +1 -0
- package/dist/services/traceInsights.d.ts +53 -0
- package/dist/services/traceInsights.d.ts.map +1 -0
- package/dist/services/traceInsights.js +762 -0
- package/dist/services/traceInsights.js.map +1 -0
- package/dist/services/training.d.ts +63 -0
- package/dist/services/training.d.ts.map +1 -0
- package/dist/services/training.js +697 -0
- package/dist/services/training.js.map +1 -0
- package/dist/services/whatsapp.d.ts +45 -0
- package/dist/services/whatsapp.d.ts.map +1 -0
- package/dist/services/whatsapp.js +194 -0
- package/dist/services/whatsapp.js.map +1 -0
- package/dist/sessions/store.d.ts +21 -0
- package/dist/sessions/store.d.ts.map +1 -0
- package/dist/sessions/store.js +47 -0
- package/dist/sessions/store.js.map +1 -0
- package/dist/settings.d.ts +111 -0
- package/dist/settings.d.ts.map +1 -0
- package/dist/settings.js +256 -0
- package/dist/settings.js.map +1 -0
- package/dist/skills/index.d.ts +10 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +11 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/loader.d.ts +22 -0
- package/dist/skills/loader.d.ts.map +1 -0
- package/dist/skills/loader.js +65 -0
- package/dist/skills/loader.js.map +1 -0
- package/dist/skills/registry.d.ts +161 -0
- package/dist/skills/registry.d.ts.map +1 -0
- package/dist/skills/registry.js +664 -0
- package/dist/skills/registry.js.map +1 -0
- package/dist/skills/types.d.ts +83 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +31 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/skills/validator.d.ts +36 -0
- package/dist/skills/validator.d.ts.map +1 -0
- package/dist/skills/validator.js +114 -0
- package/dist/skills/validator.js.map +1 -0
- package/dist/slack/channels.d.ts +102 -0
- package/dist/slack/channels.d.ts.map +1 -0
- package/dist/slack/channels.js +277 -0
- package/dist/slack/channels.js.map +1 -0
- package/dist/slack/client.d.ts +151 -0
- package/dist/slack/client.d.ts.map +1 -0
- package/dist/slack/client.js +468 -0
- package/dist/slack/client.js.map +1 -0
- package/dist/slack/retry.d.ts +36 -0
- package/dist/slack/retry.d.ts.map +1 -0
- package/dist/slack/retry.js +100 -0
- package/dist/slack/retry.js.map +1 -0
- package/dist/slack/types.d.ts +271 -0
- package/dist/slack/types.d.ts.map +1 -0
- package/dist/slack/types.js +52 -0
- package/dist/slack/types.js.map +1 -0
- package/dist/slack/webhooks.d.ts +55 -0
- package/dist/slack/webhooks.d.ts.map +1 -0
- package/dist/slack/webhooks.js +285 -0
- package/dist/slack/webhooks.js.map +1 -0
- package/dist/stt/client.d.ts +18 -0
- package/dist/stt/client.d.ts.map +1 -0
- package/dist/stt/client.js +66 -0
- package/dist/stt/client.js.map +1 -0
- package/dist/stt/sidecar.d.ts +16 -0
- package/dist/stt/sidecar.d.ts.map +1 -0
- package/dist/stt/sidecar.js +115 -0
- package/dist/stt/sidecar.js.map +1 -0
- package/dist/tracing/bridge.d.ts +14 -0
- package/dist/tracing/bridge.d.ts.map +1 -0
- package/dist/tracing/bridge.js +70 -0
- package/dist/tracing/bridge.js.map +1 -0
- package/dist/tracing/correlation.d.ts +34 -0
- package/dist/tracing/correlation.d.ts.map +1 -0
- package/dist/tracing/correlation.js +49 -0
- package/dist/tracing/correlation.js.map +1 -0
- package/dist/tracing/index.d.ts +15 -0
- package/dist/tracing/index.d.ts.map +1 -0
- package/dist/tracing/index.js +18 -0
- package/dist/tracing/index.js.map +1 -0
- package/dist/tracing/init.d.ts +42 -0
- package/dist/tracing/init.d.ts.map +1 -0
- package/dist/tracing/init.js +81 -0
- package/dist/tracing/init.js.map +1 -0
- package/dist/tracing/instrument.d.ts +39 -0
- package/dist/tracing/instrument.d.ts.map +1 -0
- package/dist/tracing/instrument.js +145 -0
- package/dist/tracing/instrument.js.map +1 -0
- package/dist/tracing/middleware.d.ts +18 -0
- package/dist/tracing/middleware.d.ts.map +1 -0
- package/dist/tracing/middleware.js +69 -0
- package/dist/tracing/middleware.js.map +1 -0
- package/dist/tracing/tracer.d.ts +105 -0
- package/dist/tracing/tracer.d.ts.map +1 -0
- package/dist/tracing/tracer.js +327 -0
- package/dist/tracing/tracer.js.map +1 -0
- package/dist/tts/client.d.ts +18 -0
- package/dist/tts/client.d.ts.map +1 -0
- package/dist/tts/client.js +48 -0
- package/dist/tts/client.js.map +1 -0
- package/dist/tts/sidecar.d.ts +16 -0
- package/dist/tts/sidecar.d.ts.map +1 -0
- package/dist/tts/sidecar.js +148 -0
- package/dist/tts/sidecar.js.map +1 -0
- package/dist/twilio/call.d.ts +22 -0
- package/dist/twilio/call.d.ts.map +1 -0
- package/dist/twilio/call.js +79 -0
- package/dist/twilio/call.js.map +1 -0
- package/dist/types/plugin.d.ts +342 -0
- package/dist/types/plugin.d.ts.map +1 -0
- package/dist/types/plugin.js +10 -0
- package/dist/types/plugin.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/logger.d.ts +53 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +169 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/vault/matcher.d.ts +39 -0
- package/dist/vault/matcher.d.ts.map +1 -0
- package/dist/vault/matcher.js +197 -0
- package/dist/vault/matcher.js.map +1 -0
- package/dist/vault/personal.d.ts +60 -0
- package/dist/vault/personal.d.ts.map +1 -0
- package/dist/vault/personal.js +162 -0
- package/dist/vault/personal.js.map +1 -0
- package/dist/vault/store.d.ts +39 -0
- package/dist/vault/store.d.ts.map +1 -0
- package/dist/vault/store.js +111 -0
- package/dist/vault/store.js.map +1 -0
- package/dist/webhooks/config.d.ts +64 -0
- package/dist/webhooks/config.d.ts.map +1 -0
- package/dist/webhooks/config.js +214 -0
- package/dist/webhooks/config.js.map +1 -0
- package/dist/webhooks/event-log.d.ts +90 -0
- package/dist/webhooks/event-log.d.ts.map +1 -0
- package/dist/webhooks/event-log.js +132 -0
- package/dist/webhooks/event-log.js.map +1 -0
- package/dist/webhooks/handler.d.ts +92 -0
- package/dist/webhooks/handler.d.ts.map +1 -0
- package/dist/webhooks/handler.js +103 -0
- package/dist/webhooks/handler.js.map +1 -0
- package/dist/webhooks/handlers.d.ts +100 -0
- package/dist/webhooks/handlers.d.ts.map +1 -0
- package/dist/webhooks/handlers.js +178 -0
- package/dist/webhooks/handlers.js.map +1 -0
- package/dist/webhooks/index.d.ts +29 -0
- package/dist/webhooks/index.d.ts.map +1 -0
- package/dist/webhooks/index.js +33 -0
- package/dist/webhooks/index.js.map +1 -0
- package/dist/webhooks/mount.d.ts +77 -0
- package/dist/webhooks/mount.d.ts.map +1 -0
- package/dist/webhooks/mount.js +400 -0
- package/dist/webhooks/mount.js.map +1 -0
- package/dist/webhooks/registry.d.ts +52 -0
- package/dist/webhooks/registry.d.ts.map +1 -0
- package/dist/webhooks/registry.js +143 -0
- package/dist/webhooks/registry.js.map +1 -0
- package/dist/webhooks/relay.d.ts +25 -0
- package/dist/webhooks/relay.d.ts.map +1 -0
- package/dist/webhooks/relay.js +53 -0
- package/dist/webhooks/relay.js.map +1 -0
- package/dist/webhooks/retry.d.ts +92 -0
- package/dist/webhooks/retry.d.ts.map +1 -0
- package/dist/webhooks/retry.js +270 -0
- package/dist/webhooks/retry.js.map +1 -0
- package/dist/webhooks/router.d.ts +94 -0
- package/dist/webhooks/router.d.ts.map +1 -0
- package/dist/webhooks/router.js +290 -0
- package/dist/webhooks/router.js.map +1 -0
- package/dist/webhooks/twilio.d.ts +63 -0
- package/dist/webhooks/twilio.d.ts.map +1 -0
- package/dist/webhooks/twilio.js +129 -0
- package/dist/webhooks/twilio.js.map +1 -0
- package/dist/webhooks/types.d.ts +142 -0
- package/dist/webhooks/types.d.ts.map +1 -0
- package/dist/webhooks/types.js +8 -0
- package/dist/webhooks/types.js.map +1 -0
- package/dist/webhooks/verify.d.ts +51 -0
- package/dist/webhooks/verify.d.ts.map +1 -0
- package/dist/webhooks/verify.js +98 -0
- package/dist/webhooks/verify.js.map +1 -0
- package/package.json +70 -0
- package/public/board.html +1316 -0
- package/public/browser.html +600 -0
- package/public/help.html +655 -0
- package/public/index.html +4689 -0
- package/public/library.html +3642 -0
- package/public/observatory.html +1693 -0
- package/public/ops.html +1129 -0
- package/public/share-modal.js +211 -0
- package/skills/README.md +34 -0
- package/skills/core-architecture.md +33 -0
- package/skills/form-fill.md +98 -0
- package/skills/form-review.md +110 -0
- package/skills/form-scout.md +94 -0
- package/skills/log-decision.md +27 -0
- package/skills/onboard.md +98 -0
- package/skills/voice-guide.md +22 -0
- package/skills/write-blog.md +43 -0
|
@@ -0,0 +1,4689 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Core</title>
|
|
7
|
+
<style>
|
|
8
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
9
|
+
|
|
10
|
+
:root {
|
|
11
|
+
--bg: #0a0a0a;
|
|
12
|
+
--surface: #141414;
|
|
13
|
+
--border: #252525;
|
|
14
|
+
--text: #e0e0e0;
|
|
15
|
+
--text-dim: #707070;
|
|
16
|
+
--accent: #7c6fef;
|
|
17
|
+
--accent-dim: #5a4fc4;
|
|
18
|
+
--error: #ef4444;
|
|
19
|
+
--user-bg: #1a1a2e;
|
|
20
|
+
--assistant-bg: #141414;
|
|
21
|
+
--radius: 8px;
|
|
22
|
+
--max-width: 640px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
body {
|
|
26
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
|
27
|
+
background: var(--bg);
|
|
28
|
+
color: var(--text);
|
|
29
|
+
height: 100dvh;
|
|
30
|
+
display: flex;
|
|
31
|
+
flex-direction: column;
|
|
32
|
+
overflow: hidden;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/* --- Shared --- */
|
|
36
|
+
|
|
37
|
+
.screen { display: none; flex-direction: column; height: 100dvh; }
|
|
38
|
+
.screen.active { display: flex; }
|
|
39
|
+
|
|
40
|
+
.center-wrap {
|
|
41
|
+
flex: 1;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
padding: 24px;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.card {
|
|
49
|
+
width: 100%;
|
|
50
|
+
max-width: 420px;
|
|
51
|
+
background: var(--surface);
|
|
52
|
+
border: 1px solid var(--border);
|
|
53
|
+
border-radius: 12px;
|
|
54
|
+
padding: 32px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.card h1 {
|
|
58
|
+
font-size: 20px;
|
|
59
|
+
font-weight: 600;
|
|
60
|
+
margin-bottom: 4px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.card p {
|
|
64
|
+
color: var(--text-dim);
|
|
65
|
+
font-size: 14px;
|
|
66
|
+
line-height: 1.5;
|
|
67
|
+
margin-bottom: 20px;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.card label {
|
|
71
|
+
display: block;
|
|
72
|
+
font-size: 12px;
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
color: var(--text-dim);
|
|
75
|
+
margin-bottom: 6px;
|
|
76
|
+
text-transform: uppercase;
|
|
77
|
+
letter-spacing: 0.5px;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.card input {
|
|
81
|
+
width: 100%;
|
|
82
|
+
padding: 10px 12px;
|
|
83
|
+
background: var(--bg);
|
|
84
|
+
border: 1px solid var(--border);
|
|
85
|
+
border-radius: var(--radius);
|
|
86
|
+
color: var(--text);
|
|
87
|
+
font-size: 15px;
|
|
88
|
+
font-family: inherit;
|
|
89
|
+
outline: none;
|
|
90
|
+
margin-bottom: 16px;
|
|
91
|
+
transition: border-color 0.15s;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.card input:focus { border-color: var(--accent); }
|
|
95
|
+
|
|
96
|
+
.card input::placeholder { color: var(--text-dim); }
|
|
97
|
+
|
|
98
|
+
.btn {
|
|
99
|
+
display: inline-block;
|
|
100
|
+
padding: 10px 20px;
|
|
101
|
+
background: var(--accent);
|
|
102
|
+
color: #fff;
|
|
103
|
+
border: none;
|
|
104
|
+
border-radius: var(--radius);
|
|
105
|
+
font-size: 14px;
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
font-family: inherit;
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
transition: background 0.15s;
|
|
110
|
+
width: 100%;
|
|
111
|
+
text-align: center;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.btn:hover { background: var(--accent-dim); }
|
|
115
|
+
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
116
|
+
|
|
117
|
+
.link-btn {
|
|
118
|
+
background: none;
|
|
119
|
+
border: none;
|
|
120
|
+
color: var(--accent);
|
|
121
|
+
font-size: 13px;
|
|
122
|
+
cursor: pointer;
|
|
123
|
+
font-family: inherit;
|
|
124
|
+
padding: 0;
|
|
125
|
+
margin-top: 12px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.link-btn:hover { text-decoration: underline; }
|
|
129
|
+
|
|
130
|
+
.error-msg {
|
|
131
|
+
color: var(--error);
|
|
132
|
+
font-size: 13px;
|
|
133
|
+
margin-bottom: 12px;
|
|
134
|
+
display: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.error-msg.visible { display: block; }
|
|
138
|
+
|
|
139
|
+
.step { display: none; }
|
|
140
|
+
.step.active { display: block; }
|
|
141
|
+
|
|
142
|
+
/* --- Chat screen --- */
|
|
143
|
+
|
|
144
|
+
#chat-screen {
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.chat-header {
|
|
149
|
+
padding: 12px 20px;
|
|
150
|
+
border-bottom: 1px solid var(--border);
|
|
151
|
+
background: var(--surface);
|
|
152
|
+
font-size: 14px;
|
|
153
|
+
font-weight: 500;
|
|
154
|
+
display: flex;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 8px;
|
|
157
|
+
flex-shrink: 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.chat-header .dot {
|
|
161
|
+
width: 8px;
|
|
162
|
+
height: 8px;
|
|
163
|
+
border-radius: 50%;
|
|
164
|
+
background: #22c55e;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.chat-header .header-nav {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
margin-left: 12px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.chat-header .header-nav a {
|
|
175
|
+
color: var(--text-dim);
|
|
176
|
+
text-decoration: none;
|
|
177
|
+
font-size: 13px;
|
|
178
|
+
padding: 4px 10px;
|
|
179
|
+
border-radius: 6px;
|
|
180
|
+
border: 1px solid var(--border);
|
|
181
|
+
transition: all 0.15s;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.chat-header .header-nav a:hover { color: var(--text); border-color: var(--accent); background: rgba(109,93,252,0.1); }
|
|
185
|
+
.chat-header .header-nav a.active { color: var(--text); border-color: var(--border); background: var(--surface2); }
|
|
186
|
+
|
|
187
|
+
.header-nav .nav-divider {
|
|
188
|
+
width: 1px;
|
|
189
|
+
height: 18px;
|
|
190
|
+
background: var(--border);
|
|
191
|
+
margin: 0 4px;
|
|
192
|
+
align-self: center;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.messages {
|
|
196
|
+
flex: 1;
|
|
197
|
+
overflow-y: auto;
|
|
198
|
+
padding: 16px 0;
|
|
199
|
+
scroll-behavior: smooth;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.message {
|
|
203
|
+
padding: 12px 20px;
|
|
204
|
+
max-width: var(--max-width);
|
|
205
|
+
margin: 0 auto;
|
|
206
|
+
width: 100%;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.message .role {
|
|
210
|
+
font-size: 11px;
|
|
211
|
+
font-weight: 600;
|
|
212
|
+
text-transform: uppercase;
|
|
213
|
+
letter-spacing: 0.5px;
|
|
214
|
+
margin-bottom: 4px;
|
|
215
|
+
color: var(--text-dim);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.message.user .role { color: var(--accent); }
|
|
219
|
+
|
|
220
|
+
.model-tag {
|
|
221
|
+
font-size: 10px;
|
|
222
|
+
font-weight: 400;
|
|
223
|
+
color: var(--text-dim);
|
|
224
|
+
margin-left: 6px;
|
|
225
|
+
font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.message .content {
|
|
229
|
+
font-size: 15px;
|
|
230
|
+
line-height: 1.6;
|
|
231
|
+
word-wrap: break-word;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.message .content pre {
|
|
235
|
+
background: var(--surface);
|
|
236
|
+
border: 1px solid var(--border);
|
|
237
|
+
border-radius: 6px;
|
|
238
|
+
padding: 8px 12px;
|
|
239
|
+
overflow-x: auto;
|
|
240
|
+
margin: 6px 0;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.message .content code {
|
|
244
|
+
background: var(--surface);
|
|
245
|
+
padding: 1px 4px;
|
|
246
|
+
border-radius: 3px;
|
|
247
|
+
font-size: 13px;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.message .content pre code {
|
|
251
|
+
background: none;
|
|
252
|
+
padding: 0;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.message .content ul {
|
|
256
|
+
margin: 4px 0;
|
|
257
|
+
padding-left: 20px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.message .content strong { font-weight: 600; }
|
|
261
|
+
.message .content em { font-style: italic; }
|
|
262
|
+
|
|
263
|
+
/* Streaming cursor — blinking caret while tokens arrive */
|
|
264
|
+
.streaming-cursor::after {
|
|
265
|
+
content: "";
|
|
266
|
+
display: inline-block;
|
|
267
|
+
width: 2px;
|
|
268
|
+
height: 1em;
|
|
269
|
+
background: var(--accent, #6d5dfc);
|
|
270
|
+
margin-left: 2px;
|
|
271
|
+
vertical-align: text-bottom;
|
|
272
|
+
animation: cursor-blink 0.8s steps(2) infinite;
|
|
273
|
+
}
|
|
274
|
+
@keyframes cursor-blink {
|
|
275
|
+
0%, 100% { opacity: 1; }
|
|
276
|
+
50% { opacity: 0; }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.composer {
|
|
280
|
+
border-top: 1px solid var(--border);
|
|
281
|
+
background: var(--surface);
|
|
282
|
+
padding: 12px 20px;
|
|
283
|
+
flex-shrink: 0;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.composer-inner {
|
|
287
|
+
max-width: var(--max-width);
|
|
288
|
+
margin: 0 auto;
|
|
289
|
+
display: flex;
|
|
290
|
+
gap: 8px;
|
|
291
|
+
align-items: flex-end;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.composer textarea {
|
|
295
|
+
flex: 1;
|
|
296
|
+
padding: 10px 12px;
|
|
297
|
+
background: var(--bg);
|
|
298
|
+
border: 1px solid var(--border);
|
|
299
|
+
border-radius: var(--radius);
|
|
300
|
+
color: var(--text);
|
|
301
|
+
font-size: 15px;
|
|
302
|
+
font-family: inherit;
|
|
303
|
+
outline: none;
|
|
304
|
+
resize: none;
|
|
305
|
+
min-height: 42px;
|
|
306
|
+
max-height: 160px;
|
|
307
|
+
line-height: 1.4;
|
|
308
|
+
transition: border-color 0.15s;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.composer textarea:focus { border-color: var(--accent); }
|
|
312
|
+
.composer textarea::placeholder { color: var(--text-dim); }
|
|
313
|
+
|
|
314
|
+
.message.system {
|
|
315
|
+
text-align: center;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.message.system .content {
|
|
319
|
+
color: var(--text-dim);
|
|
320
|
+
font-size: 13px;
|
|
321
|
+
font-style: italic;
|
|
322
|
+
border-left: 2px solid var(--border);
|
|
323
|
+
padding-left: 12px;
|
|
324
|
+
text-align: left;
|
|
325
|
+
display: inline-block;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ─── Proactive question greeting ─── */
|
|
329
|
+
|
|
330
|
+
.dash-greeting {
|
|
331
|
+
max-width: var(--max-width);
|
|
332
|
+
margin: 12px auto 0;
|
|
333
|
+
padding: 0 20px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.greeting-text {
|
|
337
|
+
font-size: 12px;
|
|
338
|
+
font-weight: 600;
|
|
339
|
+
text-transform: uppercase;
|
|
340
|
+
letter-spacing: 0.5px;
|
|
341
|
+
color: var(--text-dim);
|
|
342
|
+
margin-bottom: 8px;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.question-chips {
|
|
346
|
+
display: flex;
|
|
347
|
+
flex-direction: column;
|
|
348
|
+
gap: 6px;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.question-chip {
|
|
352
|
+
display: flex;
|
|
353
|
+
align-items: center;
|
|
354
|
+
gap: 8px;
|
|
355
|
+
padding: 10px 14px;
|
|
356
|
+
background: var(--surface);
|
|
357
|
+
border: 1px solid var(--border);
|
|
358
|
+
border-radius: var(--radius);
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
transition: border-color 0.15s, background 0.15s;
|
|
361
|
+
font-size: 14px;
|
|
362
|
+
color: var(--text);
|
|
363
|
+
line-height: 1.4;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.question-chip:hover {
|
|
367
|
+
border-color: var(--accent);
|
|
368
|
+
background: rgba(109,93,252,0.06);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
.question-chip .chip-icon {
|
|
372
|
+
flex-shrink: 0;
|
|
373
|
+
width: 22px;
|
|
374
|
+
height: 22px;
|
|
375
|
+
border-radius: 50%;
|
|
376
|
+
background: var(--accent);
|
|
377
|
+
color: #fff;
|
|
378
|
+
display: flex;
|
|
379
|
+
align-items: center;
|
|
380
|
+
justify-content: center;
|
|
381
|
+
font-size: 13px;
|
|
382
|
+
font-weight: 700;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.question-chip .chip-text { flex: 1; }
|
|
386
|
+
|
|
387
|
+
.question-chip .chip-dismiss {
|
|
388
|
+
flex-shrink: 0;
|
|
389
|
+
width: 20px;
|
|
390
|
+
height: 20px;
|
|
391
|
+
border: none;
|
|
392
|
+
background: transparent;
|
|
393
|
+
color: var(--text-dim);
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
font-size: 14px;
|
|
396
|
+
border-radius: 50%;
|
|
397
|
+
display: flex;
|
|
398
|
+
align-items: center;
|
|
399
|
+
justify-content: center;
|
|
400
|
+
opacity: 0;
|
|
401
|
+
transition: opacity 0.15s, background 0.15s;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.question-chip:hover .chip-dismiss { opacity: 1; }
|
|
405
|
+
.question-chip .chip-dismiss:hover { background: var(--surface2); color: var(--text); }
|
|
406
|
+
|
|
407
|
+
.composer button {
|
|
408
|
+
padding: 10px 16px;
|
|
409
|
+
background: var(--accent);
|
|
410
|
+
color: #fff;
|
|
411
|
+
border: none;
|
|
412
|
+
border-radius: var(--radius);
|
|
413
|
+
font-size: 14px;
|
|
414
|
+
font-weight: 500;
|
|
415
|
+
cursor: pointer;
|
|
416
|
+
font-family: inherit;
|
|
417
|
+
transition: background 0.15s;
|
|
418
|
+
white-space: nowrap;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.composer button:hover { background: var(--accent-dim); }
|
|
422
|
+
.composer button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
423
|
+
|
|
424
|
+
.composer-actions {
|
|
425
|
+
display: flex;
|
|
426
|
+
gap: 4px;
|
|
427
|
+
align-items: center;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.composer-actions button {
|
|
431
|
+
padding: 8px;
|
|
432
|
+
background: transparent;
|
|
433
|
+
color: var(--text-dim);
|
|
434
|
+
border: 1px solid var(--border);
|
|
435
|
+
border-radius: var(--radius);
|
|
436
|
+
font-size: 16px;
|
|
437
|
+
cursor: pointer;
|
|
438
|
+
line-height: 1;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
.composer-actions button:hover { background: var(--surface); color: var(--text); }
|
|
442
|
+
#incognito-btn.active { color: #ef4444; background: var(--surface); }
|
|
443
|
+
|
|
444
|
+
/* Incognito mode — red border signals local-only */
|
|
445
|
+
.composer.incognito textarea {
|
|
446
|
+
border-color: #ef4444 !important;
|
|
447
|
+
box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.25);
|
|
448
|
+
}
|
|
449
|
+
.composer.incognito textarea.redacted {
|
|
450
|
+
color: transparent;
|
|
451
|
+
text-shadow: 0 0 8px rgba(224, 224, 224, 0.5);
|
|
452
|
+
}
|
|
453
|
+
.composer.incognito .incognito-label {
|
|
454
|
+
display: block;
|
|
455
|
+
}
|
|
456
|
+
.incognito-label {
|
|
457
|
+
display: none;
|
|
458
|
+
font-size: 11px;
|
|
459
|
+
color: #ef4444;
|
|
460
|
+
text-align: center;
|
|
461
|
+
padding: 2px 0;
|
|
462
|
+
letter-spacing: 0.05em;
|
|
463
|
+
}
|
|
464
|
+
.composer.incognito #attach-btn,
|
|
465
|
+
.composer.incognito #screenshot-btn {
|
|
466
|
+
opacity: 0.2;
|
|
467
|
+
pointer-events: none;
|
|
468
|
+
}
|
|
469
|
+
/* Incognito — selective PII redaction */
|
|
470
|
+
.pii-redact {
|
|
471
|
+
filter: blur(5px);
|
|
472
|
+
transition: filter 0.2s;
|
|
473
|
+
user-select: none;
|
|
474
|
+
cursor: pointer;
|
|
475
|
+
border-radius: 3px;
|
|
476
|
+
padding: 0 2px;
|
|
477
|
+
background: rgba(239, 68, 68, 0.08);
|
|
478
|
+
}
|
|
479
|
+
.pii-redact:hover {
|
|
480
|
+
filter: none;
|
|
481
|
+
user-select: auto;
|
|
482
|
+
background: rgba(239, 68, 68, 0.15);
|
|
483
|
+
}
|
|
484
|
+
/* Block all selection/copy on incognito messages */
|
|
485
|
+
.message.incognito {
|
|
486
|
+
user-select: none;
|
|
487
|
+
-webkit-user-select: none;
|
|
488
|
+
border-left: 3px solid #ef4444;
|
|
489
|
+
padding-left: 12px;
|
|
490
|
+
position: relative;
|
|
491
|
+
}
|
|
492
|
+
.message.incognito .content {
|
|
493
|
+
user-select: none;
|
|
494
|
+
-webkit-user-select: none;
|
|
495
|
+
}
|
|
496
|
+
.message.incognito .role::after {
|
|
497
|
+
content: " \1F512";
|
|
498
|
+
font-size: 11px;
|
|
499
|
+
opacity: 0.5;
|
|
500
|
+
}
|
|
501
|
+
/* Screenshot protection: hide incognito content from print/capture */
|
|
502
|
+
.message.incognito.screen-capture .content { filter: blur(20px) !important; }
|
|
503
|
+
@media print {
|
|
504
|
+
.message.incognito .content { display: none !important; }
|
|
505
|
+
.message.incognito::after {
|
|
506
|
+
content: "[incognito content hidden]";
|
|
507
|
+
color: var(--text-dim);
|
|
508
|
+
font-style: italic;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.attachment-preview {
|
|
513
|
+
display: flex;
|
|
514
|
+
gap: 8px;
|
|
515
|
+
padding: 4px 8px;
|
|
516
|
+
flex-wrap: wrap;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.attachment-preview .thumb {
|
|
520
|
+
position: relative;
|
|
521
|
+
width: 60px;
|
|
522
|
+
height: 60px;
|
|
523
|
+
border-radius: 6px;
|
|
524
|
+
overflow: hidden;
|
|
525
|
+
border: 1px solid var(--border);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.attachment-preview .thumb img {
|
|
529
|
+
width: 100%;
|
|
530
|
+
height: 100%;
|
|
531
|
+
object-fit: cover;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
.attachment-preview .thumb .remove-btn {
|
|
535
|
+
position: absolute;
|
|
536
|
+
top: -4px;
|
|
537
|
+
right: -4px;
|
|
538
|
+
width: 18px;
|
|
539
|
+
height: 18px;
|
|
540
|
+
background: #e44;
|
|
541
|
+
color: #fff;
|
|
542
|
+
border: none;
|
|
543
|
+
border-radius: 50%;
|
|
544
|
+
font-size: 11px;
|
|
545
|
+
line-height: 18px;
|
|
546
|
+
text-align: center;
|
|
547
|
+
cursor: pointer;
|
|
548
|
+
padding: 0;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/* --- Vault slide-over --- */
|
|
552
|
+
|
|
553
|
+
.chat-header .spacer { flex: 1; }
|
|
554
|
+
|
|
555
|
+
.gear-btn {
|
|
556
|
+
background: none;
|
|
557
|
+
border: none;
|
|
558
|
+
color: var(--text-dim);
|
|
559
|
+
font-size: 18px;
|
|
560
|
+
cursor: pointer;
|
|
561
|
+
padding: 4px 6px;
|
|
562
|
+
border-radius: 4px;
|
|
563
|
+
transition: color 0.15s, background 0.15s;
|
|
564
|
+
line-height: 1;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.gear-btn:hover { color: var(--text); background: var(--border); }
|
|
568
|
+
a.gear-btn { text-decoration: none; }
|
|
569
|
+
|
|
570
|
+
.vault-overlay {
|
|
571
|
+
display: none;
|
|
572
|
+
position: fixed;
|
|
573
|
+
inset: 0;
|
|
574
|
+
background: rgba(0,0,0,0.5);
|
|
575
|
+
z-index: 100;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.vault-overlay.open { display: block; }
|
|
579
|
+
|
|
580
|
+
.vault-panel {
|
|
581
|
+
position: fixed;
|
|
582
|
+
top: 0;
|
|
583
|
+
right: 0;
|
|
584
|
+
bottom: 0;
|
|
585
|
+
width: 380px;
|
|
586
|
+
max-width: 90vw;
|
|
587
|
+
background: var(--surface);
|
|
588
|
+
border-left: 1px solid var(--border);
|
|
589
|
+
z-index: 101;
|
|
590
|
+
display: flex;
|
|
591
|
+
flex-direction: column;
|
|
592
|
+
transform: translateX(100%);
|
|
593
|
+
transition: transform 0.2s ease;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
.vault-overlay.open .vault-panel { transform: translateX(0); }
|
|
597
|
+
|
|
598
|
+
.vault-header {
|
|
599
|
+
display: flex;
|
|
600
|
+
align-items: center;
|
|
601
|
+
justify-content: space-between;
|
|
602
|
+
padding: 16px 20px;
|
|
603
|
+
border-bottom: 1px solid var(--border);
|
|
604
|
+
flex-shrink: 0;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.vault-header h2 { font-size: 16px; font-weight: 600; }
|
|
608
|
+
|
|
609
|
+
.settings-nav {
|
|
610
|
+
display: flex;
|
|
611
|
+
align-items: center;
|
|
612
|
+
gap: 4px;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
.settings-nav a {
|
|
616
|
+
display: flex;
|
|
617
|
+
align-items: center;
|
|
618
|
+
justify-content: center;
|
|
619
|
+
width: 30px;
|
|
620
|
+
height: 30px;
|
|
621
|
+
border-radius: 6px;
|
|
622
|
+
color: var(--text-dim);
|
|
623
|
+
text-decoration: none;
|
|
624
|
+
transition: color 0.15s, background 0.15s;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.settings-nav a:hover {
|
|
628
|
+
color: var(--text);
|
|
629
|
+
background: var(--border);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
.vault-close {
|
|
633
|
+
background: none;
|
|
634
|
+
border: none;
|
|
635
|
+
color: var(--text-dim);
|
|
636
|
+
font-size: 20px;
|
|
637
|
+
cursor: pointer;
|
|
638
|
+
padding: 2px 6px;
|
|
639
|
+
border-radius: 4px;
|
|
640
|
+
line-height: 1;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
.vault-close:hover { color: var(--text); background: var(--border); }
|
|
644
|
+
|
|
645
|
+
.vault-body {
|
|
646
|
+
flex: 1;
|
|
647
|
+
overflow-y: auto;
|
|
648
|
+
padding: 16px 20px;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.vault-empty {
|
|
652
|
+
color: var(--text-dim);
|
|
653
|
+
font-size: 13px;
|
|
654
|
+
text-align: center;
|
|
655
|
+
padding: 24px 0;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
.vault-key-item {
|
|
659
|
+
display: flex;
|
|
660
|
+
align-items: center;
|
|
661
|
+
justify-content: space-between;
|
|
662
|
+
padding: 10px 12px;
|
|
663
|
+
background: var(--bg);
|
|
664
|
+
border: 1px solid var(--border);
|
|
665
|
+
border-radius: var(--radius);
|
|
666
|
+
margin-bottom: 8px;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
.vault-key-info {
|
|
670
|
+
flex: 1;
|
|
671
|
+
min-width: 0;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
.vault-key-name {
|
|
675
|
+
font-size: 13px;
|
|
676
|
+
font-weight: 600;
|
|
677
|
+
font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
|
|
678
|
+
word-break: break-all;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
.vault-key-label {
|
|
682
|
+
font-size: 11px;
|
|
683
|
+
color: var(--text-dim);
|
|
684
|
+
margin-top: 2px;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
.vault-key-delete {
|
|
688
|
+
background: none;
|
|
689
|
+
border: none;
|
|
690
|
+
color: var(--text-dim);
|
|
691
|
+
font-size: 14px;
|
|
692
|
+
cursor: pointer;
|
|
693
|
+
padding: 4px 8px;
|
|
694
|
+
border-radius: 4px;
|
|
695
|
+
margin-left: 8px;
|
|
696
|
+
flex-shrink: 0;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
.vault-key-delete:hover { color: var(--error); background: rgba(239,68,68,0.1); }
|
|
700
|
+
|
|
701
|
+
.vault-add {
|
|
702
|
+
border-top: 1px solid var(--border);
|
|
703
|
+
padding: 16px 20px;
|
|
704
|
+
flex-shrink: 0;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.vault-add h3 {
|
|
708
|
+
font-size: 13px;
|
|
709
|
+
font-weight: 600;
|
|
710
|
+
margin-bottom: 12px;
|
|
711
|
+
color: var(--text-dim);
|
|
712
|
+
text-transform: uppercase;
|
|
713
|
+
letter-spacing: 0.5px;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
.vault-add input {
|
|
717
|
+
width: 100%;
|
|
718
|
+
padding: 8px 10px;
|
|
719
|
+
background: var(--bg);
|
|
720
|
+
border: 1px solid var(--border);
|
|
721
|
+
border-radius: var(--radius);
|
|
722
|
+
color: var(--text);
|
|
723
|
+
font-size: 13px;
|
|
724
|
+
font-family: inherit;
|
|
725
|
+
outline: none;
|
|
726
|
+
margin-bottom: 8px;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.vault-add input:focus { border-color: var(--accent); }
|
|
730
|
+
.vault-add input::placeholder { color: var(--text-dim); }
|
|
731
|
+
|
|
732
|
+
.vault-add-btn {
|
|
733
|
+
width: 100%;
|
|
734
|
+
padding: 8px 12px;
|
|
735
|
+
background: var(--accent);
|
|
736
|
+
color: #fff;
|
|
737
|
+
border: none;
|
|
738
|
+
border-radius: var(--radius);
|
|
739
|
+
font-size: 13px;
|
|
740
|
+
font-weight: 500;
|
|
741
|
+
cursor: pointer;
|
|
742
|
+
font-family: inherit;
|
|
743
|
+
transition: background 0.15s;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
.vault-add-btn:hover { background: var(--accent-dim); }
|
|
747
|
+
.vault-add-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
748
|
+
|
|
749
|
+
.vault-error {
|
|
750
|
+
color: var(--error);
|
|
751
|
+
font-size: 12px;
|
|
752
|
+
margin-bottom: 8px;
|
|
753
|
+
display: none;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
.vault-error.visible { display: block; }
|
|
757
|
+
|
|
758
|
+
/* --- Settings sections --- */
|
|
759
|
+
|
|
760
|
+
.settings-section {
|
|
761
|
+
padding-bottom: 16px;
|
|
762
|
+
margin-bottom: 16px;
|
|
763
|
+
border-bottom: 1px solid var(--border);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
.settings-section:last-child {
|
|
767
|
+
border-bottom: none;
|
|
768
|
+
margin-bottom: 0;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.settings-section-title {
|
|
772
|
+
font-size: 13px;
|
|
773
|
+
font-weight: 600;
|
|
774
|
+
color: var(--text-dim);
|
|
775
|
+
text-transform: uppercase;
|
|
776
|
+
letter-spacing: 0.5px;
|
|
777
|
+
margin-bottom: 8px;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.settings-hint {
|
|
781
|
+
font-size: 12px;
|
|
782
|
+
color: var(--text-dim);
|
|
783
|
+
line-height: 1.4;
|
|
784
|
+
margin-bottom: 10px;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
.prompt-textarea {
|
|
788
|
+
width: 100%;
|
|
789
|
+
padding: 10px 12px;
|
|
790
|
+
background: var(--bg);
|
|
791
|
+
border: 1px solid var(--border);
|
|
792
|
+
border-radius: var(--radius);
|
|
793
|
+
color: var(--text);
|
|
794
|
+
font-size: 13px;
|
|
795
|
+
font-family: inherit;
|
|
796
|
+
outline: none;
|
|
797
|
+
resize: vertical;
|
|
798
|
+
min-height: 80px;
|
|
799
|
+
line-height: 1.5;
|
|
800
|
+
transition: border-color 0.15s;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
.prompt-textarea:focus { border-color: var(--accent); }
|
|
804
|
+
.prompt-textarea::placeholder { color: var(--text-dim); }
|
|
805
|
+
|
|
806
|
+
/* Personality trait sliders */
|
|
807
|
+
.personality-traits { display: flex; flex-direction: column; gap: 14px; margin-bottom: 14px; }
|
|
808
|
+
.trait-row {
|
|
809
|
+
display: flex;
|
|
810
|
+
align-items: center;
|
|
811
|
+
gap: 10px;
|
|
812
|
+
font-size: 12px;
|
|
813
|
+
}
|
|
814
|
+
.trait-label { width: 72px; text-align: right; color: var(--text-dim); flex-shrink: 0; }
|
|
815
|
+
.trait-label-right { width: 72px; text-align: left; color: var(--text-dim); flex-shrink: 0; }
|
|
816
|
+
.trait-slider {
|
|
817
|
+
flex: 1;
|
|
818
|
+
position: relative;
|
|
819
|
+
height: 20px;
|
|
820
|
+
display: flex;
|
|
821
|
+
align-items: center;
|
|
822
|
+
justify-content: space-between;
|
|
823
|
+
padding: 0 4px;
|
|
824
|
+
}
|
|
825
|
+
.trait-slider::before {
|
|
826
|
+
content: "";
|
|
827
|
+
position: absolute;
|
|
828
|
+
left: 4px; right: 4px;
|
|
829
|
+
top: 50%;
|
|
830
|
+
height: 2px;
|
|
831
|
+
background: var(--border);
|
|
832
|
+
transform: translateY(-50%);
|
|
833
|
+
border-radius: 1px;
|
|
834
|
+
}
|
|
835
|
+
.trait-dot {
|
|
836
|
+
width: 14px; height: 14px;
|
|
837
|
+
border-radius: 50%;
|
|
838
|
+
border: 2px solid var(--border);
|
|
839
|
+
background: transparent;
|
|
840
|
+
cursor: pointer;
|
|
841
|
+
position: relative;
|
|
842
|
+
z-index: 1;
|
|
843
|
+
transition: background 0.15s, border-color 0.15s;
|
|
844
|
+
padding: 0;
|
|
845
|
+
}
|
|
846
|
+
.trait-dot:hover { border-color: var(--accent); }
|
|
847
|
+
.trait-dot.active {
|
|
848
|
+
background: var(--accent);
|
|
849
|
+
border-color: var(--accent);
|
|
850
|
+
}
|
|
851
|
+
.custom-rules-toggle { margin-top: 4px; }
|
|
852
|
+
.custom-rules-toggle summary {
|
|
853
|
+
font-size: 12px;
|
|
854
|
+
color: var(--text-dim);
|
|
855
|
+
cursor: pointer;
|
|
856
|
+
user-select: none;
|
|
857
|
+
padding: 4px 0;
|
|
858
|
+
}
|
|
859
|
+
.custom-rules-toggle summary:hover { color: var(--text); }
|
|
860
|
+
|
|
861
|
+
.prompt-actions {
|
|
862
|
+
display: flex;
|
|
863
|
+
align-items: center;
|
|
864
|
+
gap: 10px;
|
|
865
|
+
margin-top: 8px;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
.prompt-actions .vault-add-btn { width: auto; }
|
|
869
|
+
|
|
870
|
+
.prompt-status {
|
|
871
|
+
font-size: 12px;
|
|
872
|
+
color: #22c55e;
|
|
873
|
+
opacity: 0;
|
|
874
|
+
transition: opacity 0.3s;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
.prompt-status.visible { opacity: 1; }
|
|
878
|
+
.prompt-status.error { color: var(--error); }
|
|
879
|
+
|
|
880
|
+
/* --- Toggle switch --- */
|
|
881
|
+
|
|
882
|
+
.toggle-row {
|
|
883
|
+
display: flex;
|
|
884
|
+
align-items: center;
|
|
885
|
+
justify-content: space-between;
|
|
886
|
+
padding: 8px 0;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
.toggle-label {
|
|
890
|
+
font-size: 14px;
|
|
891
|
+
font-weight: 500;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
.toggle-sublabel {
|
|
895
|
+
font-size: 12px;
|
|
896
|
+
color: var(--text-dim);
|
|
897
|
+
margin-top: 2px;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.toggle {
|
|
901
|
+
position: relative;
|
|
902
|
+
width: 44px;
|
|
903
|
+
height: 24px;
|
|
904
|
+
flex-shrink: 0;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
.toggle input { opacity: 0; width: 0; height: 0; }
|
|
908
|
+
|
|
909
|
+
.toggle-track {
|
|
910
|
+
position: absolute;
|
|
911
|
+
inset: 0;
|
|
912
|
+
background: var(--border);
|
|
913
|
+
border-radius: 12px;
|
|
914
|
+
cursor: pointer;
|
|
915
|
+
transition: background 0.2s;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
.toggle-track::after {
|
|
919
|
+
content: "";
|
|
920
|
+
position: absolute;
|
|
921
|
+
top: 3px;
|
|
922
|
+
left: 3px;
|
|
923
|
+
width: 18px;
|
|
924
|
+
height: 18px;
|
|
925
|
+
background: var(--text-dim);
|
|
926
|
+
border-radius: 50%;
|
|
927
|
+
transition: transform 0.2s, background 0.2s;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
.toggle input:checked + .toggle-track { background: var(--accent); }
|
|
931
|
+
.toggle input:checked + .toggle-track::after { transform: translateX(20px); background: #fff; }
|
|
932
|
+
|
|
933
|
+
/* --- Model inputs --- */
|
|
934
|
+
|
|
935
|
+
.model-field {
|
|
936
|
+
margin-top: 12px;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
.model-field label {
|
|
940
|
+
display: block;
|
|
941
|
+
font-size: 12px;
|
|
942
|
+
font-weight: 500;
|
|
943
|
+
color: var(--text-dim);
|
|
944
|
+
margin-bottom: 4px;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.model-field input {
|
|
948
|
+
width: 100%;
|
|
949
|
+
padding: 8px 10px;
|
|
950
|
+
background: var(--bg);
|
|
951
|
+
border: 1px solid var(--border);
|
|
952
|
+
border-radius: var(--radius);
|
|
953
|
+
color: var(--text);
|
|
954
|
+
font-size: 13px;
|
|
955
|
+
font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
|
|
956
|
+
outline: none;
|
|
957
|
+
transition: border-color 0.15s;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
.model-field input:focus { border-color: var(--accent); }
|
|
961
|
+
.model-field input::placeholder { color: var(--text-dim); font-family: inherit; }
|
|
962
|
+
|
|
963
|
+
.settings-saved {
|
|
964
|
+
font-size: 12px;
|
|
965
|
+
color: #22c55e;
|
|
966
|
+
opacity: 0;
|
|
967
|
+
transition: opacity 0.3s;
|
|
968
|
+
margin-left: 8px;
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
.settings-saved.visible { opacity: 1; }
|
|
972
|
+
|
|
973
|
+
/* --- Activity tab --- */
|
|
974
|
+
|
|
975
|
+
.tab-group {
|
|
976
|
+
display: flex;
|
|
977
|
+
gap: 2px;
|
|
978
|
+
background: var(--bg);
|
|
979
|
+
border-radius: 6px;
|
|
980
|
+
padding: 2px;
|
|
981
|
+
margin-left: 8px;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
.tab-btn {
|
|
985
|
+
position: relative;
|
|
986
|
+
background: none;
|
|
987
|
+
border: none;
|
|
988
|
+
color: var(--text-dim);
|
|
989
|
+
font-size: 12px;
|
|
990
|
+
font-weight: 500;
|
|
991
|
+
font-family: inherit;
|
|
992
|
+
padding: 4px 12px;
|
|
993
|
+
border-radius: 4px;
|
|
994
|
+
cursor: pointer;
|
|
995
|
+
transition: color 0.15s, background 0.15s;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
.tab-btn:hover { color: var(--text); }
|
|
999
|
+
.tab-btn.active { color: var(--text); background: var(--border); }
|
|
1000
|
+
|
|
1001
|
+
.tab-badge {
|
|
1002
|
+
position: absolute;
|
|
1003
|
+
top: -4px;
|
|
1004
|
+
right: -6px;
|
|
1005
|
+
background: var(--accent);
|
|
1006
|
+
color: #fff;
|
|
1007
|
+
font-size: 10px;
|
|
1008
|
+
font-weight: 600;
|
|
1009
|
+
min-width: 16px;
|
|
1010
|
+
height: 16px;
|
|
1011
|
+
line-height: 16px;
|
|
1012
|
+
text-align: center;
|
|
1013
|
+
border-radius: 8px;
|
|
1014
|
+
padding: 0 4px;
|
|
1015
|
+
display: none;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
.tab-badge.visible { display: block; }
|
|
1019
|
+
|
|
1020
|
+
#activity-view {
|
|
1021
|
+
flex: 1;
|
|
1022
|
+
display: none;
|
|
1023
|
+
flex-direction: column;
|
|
1024
|
+
min-height: 0;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
#activity-view.active { display: flex; }
|
|
1028
|
+
|
|
1029
|
+
#activity-entries {
|
|
1030
|
+
flex: 1;
|
|
1031
|
+
overflow-y: auto;
|
|
1032
|
+
padding: 16px 0;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
.activity-entry {
|
|
1036
|
+
display: flex;
|
|
1037
|
+
align-items: baseline;
|
|
1038
|
+
gap: 10px;
|
|
1039
|
+
padding: 6px 20px;
|
|
1040
|
+
max-width: var(--max-width);
|
|
1041
|
+
margin: 0 auto;
|
|
1042
|
+
width: 100%;
|
|
1043
|
+
font-size: 13px;
|
|
1044
|
+
line-height: 1.5;
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
.activity-time {
|
|
1048
|
+
color: var(--text-dim);
|
|
1049
|
+
font-size: 11px;
|
|
1050
|
+
font-family: "SF Mono", "Cascadia Code", "Consolas", monospace;
|
|
1051
|
+
flex-shrink: 0;
|
|
1052
|
+
min-width: 40px;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
.activity-source {
|
|
1056
|
+
font-size: 10px;
|
|
1057
|
+
font-weight: 600;
|
|
1058
|
+
text-transform: uppercase;
|
|
1059
|
+
letter-spacing: 0.3px;
|
|
1060
|
+
padding: 1px 6px;
|
|
1061
|
+
border-radius: 3px;
|
|
1062
|
+
flex-shrink: 0;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
.activity-source.goal-loop { background: rgba(59,130,246,0.15); color: #60a5fa; }
|
|
1066
|
+
.activity-source.learn { background: rgba(34,197,94,0.15); color: #4ade80; }
|
|
1067
|
+
.activity-source.search { background: rgba(245,158,11,0.15); color: #fbbf24; }
|
|
1068
|
+
.activity-source.system { background: rgba(120,120,120,0.15); color: #9ca3af; }
|
|
1069
|
+
.activity-source.agent { background: rgba(168,85,247,0.15); color: #c084fc; }
|
|
1070
|
+
|
|
1071
|
+
/* Agent waiting indicator (inline in chat) */
|
|
1072
|
+
.agent-status {
|
|
1073
|
+
margin: 8px 0;
|
|
1074
|
+
padding: 10px 14px;
|
|
1075
|
+
background: rgba(168,85,247,0.08);
|
|
1076
|
+
border: 1px solid rgba(168,85,247,0.25);
|
|
1077
|
+
border-radius: var(--radius);
|
|
1078
|
+
font-size: 13px;
|
|
1079
|
+
color: #c084fc;
|
|
1080
|
+
display: flex;
|
|
1081
|
+
align-items: center;
|
|
1082
|
+
gap: 10px;
|
|
1083
|
+
}
|
|
1084
|
+
.agent-status .agent-spinner {
|
|
1085
|
+
width: 14px; height: 14px;
|
|
1086
|
+
border: 2px solid rgba(168,85,247,0.3);
|
|
1087
|
+
border-top-color: #c084fc;
|
|
1088
|
+
border-radius: 50%;
|
|
1089
|
+
animation: spin 0.8s linear infinite;
|
|
1090
|
+
flex-shrink: 0;
|
|
1091
|
+
}
|
|
1092
|
+
.agent-status .agent-label { flex: 1; }
|
|
1093
|
+
.agent-status .agent-cancel {
|
|
1094
|
+
padding: 3px 10px;
|
|
1095
|
+
background: rgba(239,68,68,0.15);
|
|
1096
|
+
color: #f87171;
|
|
1097
|
+
border: 1px solid rgba(239,68,68,0.3);
|
|
1098
|
+
border-radius: 4px;
|
|
1099
|
+
font-size: 12px;
|
|
1100
|
+
cursor: pointer;
|
|
1101
|
+
font-family: inherit;
|
|
1102
|
+
}
|
|
1103
|
+
.agent-status .agent-cancel:hover { background: rgba(239,68,68,0.25); }
|
|
1104
|
+
.agent-status.done { border-color: rgba(34,197,94,0.3); background: rgba(34,197,94,0.08); color: #4ade80; }
|
|
1105
|
+
.agent-status.failed { border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.08); color: #f87171; }
|
|
1106
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1107
|
+
|
|
1108
|
+
/* Board item cards (inline in chat) */
|
|
1109
|
+
.board-cards { display: flex; flex-direction: column; gap: 6px; margin: 8px 0; }
|
|
1110
|
+
.board-card {
|
|
1111
|
+
display: flex;
|
|
1112
|
+
align-items: center;
|
|
1113
|
+
gap: 10px;
|
|
1114
|
+
padding: 10px 14px;
|
|
1115
|
+
background: rgba(59,130,246,0.08);
|
|
1116
|
+
border: 1px solid rgba(59,130,246,0.25);
|
|
1117
|
+
border-radius: var(--radius);
|
|
1118
|
+
font-size: 13px;
|
|
1119
|
+
color: #93c5fd;
|
|
1120
|
+
transition: opacity 0.3s, border-color 0.3s;
|
|
1121
|
+
}
|
|
1122
|
+
.board-card.card-done { opacity: 0.45; border-color: rgba(34,197,94,0.3); }
|
|
1123
|
+
.board-id {
|
|
1124
|
+
font-family: "SF Mono", "Fira Code", "Cascadia Code", monospace;
|
|
1125
|
+
font-size: 12px;
|
|
1126
|
+
padding: 2px 6px;
|
|
1127
|
+
background: rgba(59,130,246,0.15);
|
|
1128
|
+
border-radius: 4px;
|
|
1129
|
+
color: #60a5fa;
|
|
1130
|
+
flex-shrink: 0;
|
|
1131
|
+
font-weight: 600;
|
|
1132
|
+
}
|
|
1133
|
+
.board-title { flex: 1; color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1134
|
+
.board-priority {
|
|
1135
|
+
font-size: 11px;
|
|
1136
|
+
padding: 1px 5px;
|
|
1137
|
+
border-radius: 3px;
|
|
1138
|
+
flex-shrink: 0;
|
|
1139
|
+
}
|
|
1140
|
+
.board-priority.p1 { background: rgba(239,68,68,0.2); color: #f87171; }
|
|
1141
|
+
.board-priority.p2 { background: rgba(245,158,11,0.2); color: #fbbf24; }
|
|
1142
|
+
.board-priority.p3 { background: rgba(59,130,246,0.15); color: #93c5fd; }
|
|
1143
|
+
.board-priority.p4 { background: rgba(120,120,120,0.15); color: #9ca3af; }
|
|
1144
|
+
.board-state {
|
|
1145
|
+
font-size: 11px;
|
|
1146
|
+
padding: 2px 8px;
|
|
1147
|
+
border-radius: 10px;
|
|
1148
|
+
flex-shrink: 0;
|
|
1149
|
+
font-weight: 500;
|
|
1150
|
+
}
|
|
1151
|
+
.board-state.state-started { background: rgba(245,158,11,0.2); color: #fbbf24; }
|
|
1152
|
+
.board-state.state-completed { background: rgba(34,197,94,0.2); color: #4ade80; }
|
|
1153
|
+
.board-state.state-cancelled { background: rgba(239,68,68,0.2); color: #f87171; }
|
|
1154
|
+
.board-state.state-unstarted { background: rgba(59,130,246,0.15); color: #60a5fa; }
|
|
1155
|
+
.board-state.state-backlog { background: rgba(120,120,120,0.15); color: #9ca3af; }
|
|
1156
|
+
.board-state.state-triage { background: rgba(168,85,247,0.2); color: #c084fc; }
|
|
1157
|
+
.board-assignee { font-size: 11px; color: var(--text-dim); flex-shrink: 0; }
|
|
1158
|
+
.board-actions { flex-shrink: 0; }
|
|
1159
|
+
.board-actions button {
|
|
1160
|
+
padding: 3px 10px;
|
|
1161
|
+
background: rgba(34,197,94,0.15);
|
|
1162
|
+
color: #4ade80;
|
|
1163
|
+
border: 1px solid rgba(34,197,94,0.3);
|
|
1164
|
+
border-radius: 4px;
|
|
1165
|
+
font-size: 11px;
|
|
1166
|
+
cursor: pointer;
|
|
1167
|
+
font-family: inherit;
|
|
1168
|
+
}
|
|
1169
|
+
.board-actions button:hover { background: rgba(34,197,94,0.25); }
|
|
1170
|
+
.board-actions button:disabled { opacity: 0.4; cursor: default; }
|
|
1171
|
+
|
|
1172
|
+
.activity-body { flex: 1; min-width: 0; }
|
|
1173
|
+
|
|
1174
|
+
.activity-summary {
|
|
1175
|
+
color: var(--text);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
.activity-detail {
|
|
1179
|
+
color: var(--text-dim);
|
|
1180
|
+
font-size: 12px;
|
|
1181
|
+
margin-top: 2px;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
.activity-empty {
|
|
1185
|
+
color: var(--text-dim);
|
|
1186
|
+
font-size: 13px;
|
|
1187
|
+
text-align: center;
|
|
1188
|
+
padding: 40px 20px;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
.activity-entry {
|
|
1192
|
+
cursor: pointer;
|
|
1193
|
+
border-left: 2px solid transparent;
|
|
1194
|
+
transition: background 0.15s, border-color 0.15s;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
.activity-entry:hover {
|
|
1198
|
+
background: rgba(124, 111, 239, 0.04);
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
.activity-entry.selected {
|
|
1202
|
+
border-left-color: var(--accent);
|
|
1203
|
+
background: rgba(124, 111, 239, 0.08);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
.activity-entry.selected .activity-summary {
|
|
1207
|
+
color: #fff;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
.activity-entry.has-backref {
|
|
1211
|
+
border-left-color: rgba(124, 111, 239, 0.3);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
.activity-lineage {
|
|
1215
|
+
color: var(--accent);
|
|
1216
|
+
font-size: 14px;
|
|
1217
|
+
margin-left: 6px;
|
|
1218
|
+
opacity: 0.6;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
/* Stream controls bar */
|
|
1222
|
+
.stream-controls {
|
|
1223
|
+
display: flex;
|
|
1224
|
+
align-items: center;
|
|
1225
|
+
gap: 10px;
|
|
1226
|
+
padding: 8px 20px;
|
|
1227
|
+
border-bottom: 1px solid var(--border);
|
|
1228
|
+
flex-shrink: 0;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
.stream-freeze-btn {
|
|
1232
|
+
background: none;
|
|
1233
|
+
border: 1px solid var(--border);
|
|
1234
|
+
color: var(--text-dim);
|
|
1235
|
+
font-size: 12px;
|
|
1236
|
+
font-family: inherit;
|
|
1237
|
+
padding: 4px 10px;
|
|
1238
|
+
border-radius: 4px;
|
|
1239
|
+
cursor: pointer;
|
|
1240
|
+
transition: color 0.15s, border-color 0.15s;
|
|
1241
|
+
display: flex;
|
|
1242
|
+
align-items: center;
|
|
1243
|
+
gap: 5px;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
.stream-freeze-btn:hover { color: var(--text); border-color: var(--text-dim); }
|
|
1247
|
+
|
|
1248
|
+
.stream-freeze-btn.frozen {
|
|
1249
|
+
color: var(--accent);
|
|
1250
|
+
border-color: var(--accent);
|
|
1251
|
+
animation: pulse-freeze 2s ease-in-out infinite;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
@keyframes pulse-freeze {
|
|
1255
|
+
0%, 100% { opacity: 1; }
|
|
1256
|
+
50% { opacity: 0.6; }
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
.stream-selection-count {
|
|
1260
|
+
font-size: 12px;
|
|
1261
|
+
color: var(--text-dim);
|
|
1262
|
+
display: none;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
.stream-selection-count.visible { display: inline; }
|
|
1266
|
+
|
|
1267
|
+
.stream-branch-btn {
|
|
1268
|
+
background: var(--accent);
|
|
1269
|
+
color: #fff;
|
|
1270
|
+
border: none;
|
|
1271
|
+
font-size: 12px;
|
|
1272
|
+
font-weight: 500;
|
|
1273
|
+
font-family: inherit;
|
|
1274
|
+
padding: 4px 12px;
|
|
1275
|
+
border-radius: 4px;
|
|
1276
|
+
cursor: pointer;
|
|
1277
|
+
display: none;
|
|
1278
|
+
transition: background 0.15s;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
.stream-branch-btn:hover { background: var(--accent-dim); }
|
|
1282
|
+
.stream-branch-btn.visible { display: inline-block; }
|
|
1283
|
+
|
|
1284
|
+
/* Branch panel */
|
|
1285
|
+
.branch-panel {
|
|
1286
|
+
display: none;
|
|
1287
|
+
flex-direction: column;
|
|
1288
|
+
max-height: 40vh;
|
|
1289
|
+
border-top: 1px solid var(--border);
|
|
1290
|
+
background: var(--surface);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
.branch-panel.open { display: flex; }
|
|
1294
|
+
|
|
1295
|
+
.branch-header {
|
|
1296
|
+
display: flex;
|
|
1297
|
+
align-items: center;
|
|
1298
|
+
gap: 8px;
|
|
1299
|
+
padding: 8px 16px;
|
|
1300
|
+
font-size: 12px;
|
|
1301
|
+
color: var(--text-dim);
|
|
1302
|
+
border-bottom: 1px solid var(--border);
|
|
1303
|
+
flex-shrink: 0;
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
.branch-header-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1307
|
+
|
|
1308
|
+
.branch-close-btn {
|
|
1309
|
+
background: none;
|
|
1310
|
+
border: none;
|
|
1311
|
+
color: var(--text-dim);
|
|
1312
|
+
font-size: 16px;
|
|
1313
|
+
cursor: pointer;
|
|
1314
|
+
padding: 0 4px;
|
|
1315
|
+
line-height: 1;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
.branch-close-btn:hover { color: var(--text); }
|
|
1319
|
+
|
|
1320
|
+
.branch-messages {
|
|
1321
|
+
flex: 1;
|
|
1322
|
+
overflow-y: auto;
|
|
1323
|
+
padding: 12px 16px;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
.branch-message {
|
|
1327
|
+
margin-bottom: 10px;
|
|
1328
|
+
font-size: 13px;
|
|
1329
|
+
line-height: 1.5;
|
|
1330
|
+
max-width: var(--max-width);
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
.branch-message.user { color: var(--text-dim); }
|
|
1334
|
+
.branch-message.assistant { color: var(--text); }
|
|
1335
|
+
|
|
1336
|
+
.branch-message .branch-role {
|
|
1337
|
+
font-size: 11px;
|
|
1338
|
+
font-weight: 600;
|
|
1339
|
+
text-transform: uppercase;
|
|
1340
|
+
letter-spacing: 0.3px;
|
|
1341
|
+
margin-bottom: 2px;
|
|
1342
|
+
color: var(--text-dim);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
.branch-composer {
|
|
1346
|
+
display: flex;
|
|
1347
|
+
gap: 8px;
|
|
1348
|
+
padding: 8px 16px;
|
|
1349
|
+
border-top: 1px solid var(--border);
|
|
1350
|
+
flex-shrink: 0;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
.branch-input {
|
|
1354
|
+
flex: 1;
|
|
1355
|
+
padding: 8px 12px;
|
|
1356
|
+
background: var(--bg);
|
|
1357
|
+
border: 1px solid var(--border);
|
|
1358
|
+
border-radius: var(--radius);
|
|
1359
|
+
color: var(--text);
|
|
1360
|
+
font-size: 13px;
|
|
1361
|
+
font-family: inherit;
|
|
1362
|
+
outline: none;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
.branch-input:focus { border-color: var(--accent); }
|
|
1366
|
+
|
|
1367
|
+
.branch-send-btn {
|
|
1368
|
+
background: var(--accent);
|
|
1369
|
+
color: #fff;
|
|
1370
|
+
border: none;
|
|
1371
|
+
font-size: 13px;
|
|
1372
|
+
font-family: inherit;
|
|
1373
|
+
padding: 8px 16px;
|
|
1374
|
+
border-radius: var(--radius);
|
|
1375
|
+
cursor: pointer;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
.branch-send-btn:hover { background: var(--accent-dim); }
|
|
1379
|
+
|
|
1380
|
+
#send-btn.stop {
|
|
1381
|
+
background: #dc2626;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
#send-btn.stop:hover {
|
|
1385
|
+
background: #b91c1c;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/* --- Avatar + Voice --- */
|
|
1389
|
+
|
|
1390
|
+
.chat-body {
|
|
1391
|
+
display: flex;
|
|
1392
|
+
flex: 1;
|
|
1393
|
+
overflow: hidden;
|
|
1394
|
+
min-height: 0;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
|
|
1398
|
+
/* ─── Thread sidebar ─── */
|
|
1399
|
+
|
|
1400
|
+
.thread-sidebar {
|
|
1401
|
+
width: 260px;
|
|
1402
|
+
flex-shrink: 0;
|
|
1403
|
+
display: flex;
|
|
1404
|
+
flex-direction: column;
|
|
1405
|
+
border-right: 1px solid var(--border);
|
|
1406
|
+
background: var(--surface);
|
|
1407
|
+
overflow: hidden;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
.thread-sidebar.hidden { display: none; }
|
|
1411
|
+
|
|
1412
|
+
.thread-sidebar-header {
|
|
1413
|
+
display: flex;
|
|
1414
|
+
align-items: center;
|
|
1415
|
+
gap: 8px;
|
|
1416
|
+
padding: 12px 14px;
|
|
1417
|
+
border-bottom: 1px solid var(--border);
|
|
1418
|
+
flex-shrink: 0;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
.thread-sidebar-header span {
|
|
1422
|
+
font-size: 12px;
|
|
1423
|
+
font-weight: 600;
|
|
1424
|
+
text-transform: uppercase;
|
|
1425
|
+
letter-spacing: 0.5px;
|
|
1426
|
+
color: var(--text-dim);
|
|
1427
|
+
flex: 1;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
.thread-new-btn {
|
|
1431
|
+
padding: 4px 10px;
|
|
1432
|
+
font-size: 12px;
|
|
1433
|
+
background: var(--accent);
|
|
1434
|
+
color: #fff;
|
|
1435
|
+
border: none;
|
|
1436
|
+
border-radius: 4px;
|
|
1437
|
+
cursor: pointer;
|
|
1438
|
+
font-weight: 500;
|
|
1439
|
+
}
|
|
1440
|
+
.thread-new-btn:hover { background: var(--accent-dim); }
|
|
1441
|
+
|
|
1442
|
+
.thread-list {
|
|
1443
|
+
flex: 1;
|
|
1444
|
+
overflow-y: auto;
|
|
1445
|
+
padding: 6px 0;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
.thread-item {
|
|
1449
|
+
display: flex;
|
|
1450
|
+
flex-direction: column;
|
|
1451
|
+
padding: 10px 14px;
|
|
1452
|
+
cursor: pointer;
|
|
1453
|
+
border-left: 3px solid transparent;
|
|
1454
|
+
transition: background 0.1s, border-color 0.1s;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
.thread-item:hover { background: rgba(255,255,255,0.03); }
|
|
1458
|
+
|
|
1459
|
+
.thread-item.active {
|
|
1460
|
+
background: rgba(109,93,252,0.08);
|
|
1461
|
+
border-left-color: var(--accent);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.thread-item-title {
|
|
1465
|
+
font-size: 13px;
|
|
1466
|
+
font-weight: 500;
|
|
1467
|
+
color: var(--text);
|
|
1468
|
+
white-space: nowrap;
|
|
1469
|
+
overflow: hidden;
|
|
1470
|
+
text-overflow: ellipsis;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
.thread-item-meta {
|
|
1474
|
+
font-size: 11px;
|
|
1475
|
+
color: var(--text-dim);
|
|
1476
|
+
margin-top: 2px;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.thread-toggle-btn {
|
|
1480
|
+
width: 32px;
|
|
1481
|
+
height: 32px;
|
|
1482
|
+
border: 1px solid var(--border);
|
|
1483
|
+
background: var(--surface);
|
|
1484
|
+
color: var(--text-dim);
|
|
1485
|
+
border-radius: 6px;
|
|
1486
|
+
cursor: pointer;
|
|
1487
|
+
font-size: 14px;
|
|
1488
|
+
display: flex;
|
|
1489
|
+
align-items: center;
|
|
1490
|
+
justify-content: center;
|
|
1491
|
+
flex-shrink: 0;
|
|
1492
|
+
}
|
|
1493
|
+
.thread-toggle-btn:hover { color: var(--text); border-color: var(--accent); }
|
|
1494
|
+
|
|
1495
|
+
.thread-current-label {
|
|
1496
|
+
font-size: 12px;
|
|
1497
|
+
color: var(--text-dim);
|
|
1498
|
+
max-width: 160px;
|
|
1499
|
+
white-space: nowrap;
|
|
1500
|
+
overflow: hidden;
|
|
1501
|
+
text-overflow: ellipsis;
|
|
1502
|
+
cursor: pointer;
|
|
1503
|
+
}
|
|
1504
|
+
.thread-current-label:hover { color: var(--text); }
|
|
1505
|
+
|
|
1506
|
+
.mic-btn {
|
|
1507
|
+
width: 42px;
|
|
1508
|
+
height: 42px;
|
|
1509
|
+
border-radius: 50%;
|
|
1510
|
+
border: 1px solid var(--border);
|
|
1511
|
+
background: var(--bg);
|
|
1512
|
+
color: var(--text-dim);
|
|
1513
|
+
font-size: 18px;
|
|
1514
|
+
cursor: pointer;
|
|
1515
|
+
display: flex;
|
|
1516
|
+
align-items: center;
|
|
1517
|
+
justify-content: center;
|
|
1518
|
+
transition: all 0.15s;
|
|
1519
|
+
flex-shrink: 0;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
.mic-btn:hover { border-color: var(--accent); color: var(--text); }
|
|
1523
|
+
|
|
1524
|
+
.mic-btn.listening {
|
|
1525
|
+
border-color: #ef4444;
|
|
1526
|
+
background: rgba(239,68,68,0.1);
|
|
1527
|
+
color: #ef4444;
|
|
1528
|
+
animation: mic-listen 2s ease-in-out infinite;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
@keyframes mic-listen {
|
|
1532
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(239,68,68,0.3); }
|
|
1533
|
+
50% { box-shadow: 0 0 0 4px rgba(239,68,68,0); }
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
.mic-btn.recording {
|
|
1537
|
+
border-color: #f59e0b;
|
|
1538
|
+
background: rgba(245,158,11,0.1);
|
|
1539
|
+
color: #f59e0b;
|
|
1540
|
+
animation: mic-pulse 1.2s ease-in-out infinite;
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
@keyframes mic-pulse {
|
|
1544
|
+
0%, 100% { box-shadow: 0 0 0 0 rgba(245,158,11,0.3); }
|
|
1545
|
+
50% { box-shadow: 0 0 0 6px rgba(245,158,11,0); }
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
.mic-btn.hidden { display: none; }
|
|
1549
|
+
|
|
1550
|
+
.speaker-btn {
|
|
1551
|
+
background: none;
|
|
1552
|
+
border: none;
|
|
1553
|
+
color: var(--text-dim);
|
|
1554
|
+
cursor: pointer;
|
|
1555
|
+
font-size: 14px;
|
|
1556
|
+
padding: 2px 4px;
|
|
1557
|
+
margin-left: 6px;
|
|
1558
|
+
opacity: 0.6;
|
|
1559
|
+
transition: opacity 0.15s;
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.speaker-btn:hover { opacity: 1; }
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
/* UI update toast */
|
|
1566
|
+
.ui-update-toast {
|
|
1567
|
+
position: fixed;
|
|
1568
|
+
top: 16px;
|
|
1569
|
+
left: 50%;
|
|
1570
|
+
transform: translateX(-50%) translateY(-100px);
|
|
1571
|
+
background: var(--surface);
|
|
1572
|
+
border: 1px solid var(--accent);
|
|
1573
|
+
border-radius: var(--radius);
|
|
1574
|
+
padding: 12px 20px;
|
|
1575
|
+
display: flex;
|
|
1576
|
+
align-items: center;
|
|
1577
|
+
gap: 12px;
|
|
1578
|
+
z-index: 10000;
|
|
1579
|
+
opacity: 0;
|
|
1580
|
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
1581
|
+
box-shadow: 0 4px 20px rgba(124, 111, 239, 0.15);
|
|
1582
|
+
font-size: 13px;
|
|
1583
|
+
color: var(--text);
|
|
1584
|
+
}
|
|
1585
|
+
.ui-update-toast.visible {
|
|
1586
|
+
transform: translateX(-50%) translateY(0);
|
|
1587
|
+
opacity: 1;
|
|
1588
|
+
}
|
|
1589
|
+
.ui-update-toast button {
|
|
1590
|
+
background: var(--accent);
|
|
1591
|
+
color: #fff;
|
|
1592
|
+
border: none;
|
|
1593
|
+
border-radius: 4px;
|
|
1594
|
+
padding: 5px 14px;
|
|
1595
|
+
cursor: pointer;
|
|
1596
|
+
font-size: 12px;
|
|
1597
|
+
font-weight: 600;
|
|
1598
|
+
}
|
|
1599
|
+
.ui-update-toast button:hover { opacity: 0.85; }
|
|
1600
|
+
.ui-update-toast .dismiss {
|
|
1601
|
+
background: none;
|
|
1602
|
+
color: var(--text-dim);
|
|
1603
|
+
padding: 5px 8px;
|
|
1604
|
+
font-size: 16px;
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
/* ─── Mobile ─── */
|
|
1608
|
+
@media (max-width: 640px) {
|
|
1609
|
+
/* Header: hide full nav, show hamburger-style overflow */
|
|
1610
|
+
.chat-header {
|
|
1611
|
+
padding: 8px 12px;
|
|
1612
|
+
gap: 6px;
|
|
1613
|
+
flex-wrap: wrap;
|
|
1614
|
+
}
|
|
1615
|
+
.chat-header .header-nav {
|
|
1616
|
+
order: 10;
|
|
1617
|
+
width: 100%;
|
|
1618
|
+
overflow-x: auto;
|
|
1619
|
+
-webkit-overflow-scrolling: touch;
|
|
1620
|
+
scrollbar-width: none;
|
|
1621
|
+
margin-left: 0;
|
|
1622
|
+
padding: 4px 0;
|
|
1623
|
+
gap: 6px;
|
|
1624
|
+
}
|
|
1625
|
+
.chat-header .header-nav::-webkit-scrollbar { display: none; }
|
|
1626
|
+
.chat-header .header-nav a {
|
|
1627
|
+
font-size: 12px;
|
|
1628
|
+
padding: 4px 8px;
|
|
1629
|
+
white-space: nowrap;
|
|
1630
|
+
flex-shrink: 0;
|
|
1631
|
+
}
|
|
1632
|
+
.nav-divider { display: none; }
|
|
1633
|
+
|
|
1634
|
+
/* Tab group compact */
|
|
1635
|
+
.tab-group { margin-left: 4px; }
|
|
1636
|
+
.tab-btn { font-size: 11px; padding: 3px 8px; }
|
|
1637
|
+
|
|
1638
|
+
/* Thread sidebar: full overlay on mobile */
|
|
1639
|
+
.thread-sidebar {
|
|
1640
|
+
position: fixed;
|
|
1641
|
+
top: 0; left: 0; bottom: 0;
|
|
1642
|
+
width: 280px;
|
|
1643
|
+
z-index: 50;
|
|
1644
|
+
box-shadow: 4px 0 20px rgba(0,0,0,0.5);
|
|
1645
|
+
}
|
|
1646
|
+
.thread-sidebar.hidden { display: none; }
|
|
1647
|
+
|
|
1648
|
+
/* Messages: tighter padding */
|
|
1649
|
+
.message { padding: 10px 12px; }
|
|
1650
|
+
.message .content { font-size: 14px; }
|
|
1651
|
+
.message .role { font-size: 10px; }
|
|
1652
|
+
|
|
1653
|
+
/* Composer: stack on small screens */
|
|
1654
|
+
.composer { padding: 8px 10px; }
|
|
1655
|
+
.composer-inner { gap: 6px; }
|
|
1656
|
+
.composer textarea {
|
|
1657
|
+
font-size: 16px; /* prevents iOS zoom on focus */
|
|
1658
|
+
padding: 8px 10px;
|
|
1659
|
+
min-height: 38px;
|
|
1660
|
+
}
|
|
1661
|
+
.composer button { padding: 8px 12px; font-size: 13px; }
|
|
1662
|
+
.composer-actions button { padding: 6px; font-size: 14px; }
|
|
1663
|
+
.mic-btn { width: 34px; height: 34px; font-size: 16px; }
|
|
1664
|
+
|
|
1665
|
+
/* Settings panel: full width on mobile */
|
|
1666
|
+
.vault-panel { width: 100vw; max-width: 100vw; }
|
|
1667
|
+
|
|
1668
|
+
/* Proactive chips: horizontal scroll */
|
|
1669
|
+
.question-chips { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
|
1670
|
+
.question-chip { flex-shrink: 0; }
|
|
1671
|
+
|
|
1672
|
+
/* Gear buttons: bigger tap targets */
|
|
1673
|
+
.gear-btn { padding: 6px 8px; font-size: 20px; }
|
|
1674
|
+
|
|
1675
|
+
/* Toast: full width on mobile */
|
|
1676
|
+
.ui-update-toast {
|
|
1677
|
+
left: 8px;
|
|
1678
|
+
right: 8px;
|
|
1679
|
+
transform: translateX(0) translateY(-100px);
|
|
1680
|
+
width: auto;
|
|
1681
|
+
}
|
|
1682
|
+
.ui-update-toast.visible {
|
|
1683
|
+
transform: translateX(0) translateY(0);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/* Thread toggle: always visible */
|
|
1687
|
+
.thread-toggle-btn { display: inline-flex !important; }
|
|
1688
|
+
|
|
1689
|
+
/* Hide stream controls text on narrow */
|
|
1690
|
+
.stream-controls { font-size: 12px; }
|
|
1691
|
+
|
|
1692
|
+
/* Code blocks: smaller on mobile */
|
|
1693
|
+
.message .content pre { font-size: 12px; padding: 6px 8px; }
|
|
1694
|
+
.message .content code { font-size: 12px; }
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
/* Small phone (< 380px) */
|
|
1698
|
+
@media (max-width: 380px) {
|
|
1699
|
+
.composer-actions button:not(#incognito-btn) { display: none; }
|
|
1700
|
+
.chat-header .header-nav a { font-size: 11px; padding: 3px 6px; }
|
|
1701
|
+
.gear-btn { font-size: 18px; padding: 4px 6px; }
|
|
1702
|
+
}
|
|
1703
|
+
</style>
|
|
1704
|
+
</head>
|
|
1705
|
+
<body>
|
|
1706
|
+
|
|
1707
|
+
<!-- Pairing screen -->
|
|
1708
|
+
<div id="pair-screen" class="screen">
|
|
1709
|
+
<div class="center-wrap">
|
|
1710
|
+
<div class="card">
|
|
1711
|
+
<!-- Step 1: Pairing code (manual flow only) -->
|
|
1712
|
+
<div id="pair-step-1" class="step active">
|
|
1713
|
+
<h1>Hello.</h1>
|
|
1714
|
+
<p>Enter the pairing code from your terminal to get started.</p>
|
|
1715
|
+
<div id="pair-error" class="error-msg"></div>
|
|
1716
|
+
<label for="pair-code">Pairing Code</label>
|
|
1717
|
+
<input type="text" id="pair-code" placeholder="amber-castle-seven-river-oak-noon" autocomplete="off" spellcheck="false">
|
|
1718
|
+
<button class="btn" id="pair-code-btn">Verify</button>
|
|
1719
|
+
</div>
|
|
1720
|
+
|
|
1721
|
+
<!-- Step 2: Name -->
|
|
1722
|
+
<div id="pair-step-2" class="step">
|
|
1723
|
+
<h1>Hello.</h1>
|
|
1724
|
+
<p>What should I call you?</p>
|
|
1725
|
+
<div id="pair-error-2" class="error-msg"></div>
|
|
1726
|
+
<label for="pair-name">Your Name</label>
|
|
1727
|
+
<input type="text" id="pair-name" placeholder="First name is fine" autocomplete="off">
|
|
1728
|
+
<button class="btn" id="pair-name-btn">Continue</button>
|
|
1729
|
+
</div>
|
|
1730
|
+
|
|
1731
|
+
<!-- Step 2b: Safe word -->
|
|
1732
|
+
<div id="pair-step-2b" class="step">
|
|
1733
|
+
<h1 id="pair-greeting">Nice to meet you.</h1>
|
|
1734
|
+
<p>Choose a safe word — something only we'll know. This encrypts your data.</p>
|
|
1735
|
+
<div id="pair-error-2b" class="error-msg"></div>
|
|
1736
|
+
<label for="pair-safeword">Safe Word</label>
|
|
1737
|
+
<input type="password" id="pair-safeword" placeholder="Something memorable">
|
|
1738
|
+
<label for="pair-safeword-confirm">Confirm Safe Word</label>
|
|
1739
|
+
<input type="password" id="pair-safeword-confirm" placeholder="Type it again">
|
|
1740
|
+
<button class="btn" id="pair-sw-btn">Continue</button>
|
|
1741
|
+
</div>
|
|
1742
|
+
|
|
1743
|
+
<!-- Step 3: Recovery -->
|
|
1744
|
+
<div id="pair-step-3" class="step">
|
|
1745
|
+
<h1>Almost there.</h1>
|
|
1746
|
+
<p>Set a recovery question in case you forget the safe word. Make it something personal.</p>
|
|
1747
|
+
<div id="pair-error-3" class="error-msg"></div>
|
|
1748
|
+
<label for="pair-question">Recovery Question</label>
|
|
1749
|
+
<input type="text" id="pair-question" placeholder="What was the name of...?">
|
|
1750
|
+
<label for="pair-answer">Recovery Answer</label>
|
|
1751
|
+
<input type="text" id="pair-answer" placeholder="Your answer">
|
|
1752
|
+
<button class="btn" id="pair-recovery-btn">Continue</button>
|
|
1753
|
+
</div>
|
|
1754
|
+
|
|
1755
|
+
<!-- Step 4: Name the agent -->
|
|
1756
|
+
<div id="pair-step-4" class="step">
|
|
1757
|
+
<h1 id="pair-agent-greeting">One more thing.</h1>
|
|
1758
|
+
<p>What would you like to call me?</p>
|
|
1759
|
+
<div id="pair-error-4" class="error-msg"></div>
|
|
1760
|
+
<label for="pair-agent-name">Agent Name</label>
|
|
1761
|
+
<input type="text" id="pair-agent-name" placeholder="Dash, Atlas, Nova... anything" autocomplete="off">
|
|
1762
|
+
<button class="btn" id="pair-finish-btn">Let's go</button>
|
|
1763
|
+
</div>
|
|
1764
|
+
|
|
1765
|
+
<!-- Step 5: Transition -->
|
|
1766
|
+
<div id="pair-step-5" class="step">
|
|
1767
|
+
<h1 id="pair-ready-msg">Setting things up...</h1>
|
|
1768
|
+
<p id="pair-ready-sub" style="opacity:0.6;"></p>
|
|
1769
|
+
</div>
|
|
1770
|
+
</div>
|
|
1771
|
+
</div>
|
|
1772
|
+
</div>
|
|
1773
|
+
|
|
1774
|
+
<!-- Auth screen -->
|
|
1775
|
+
<div id="auth-screen" class="screen">
|
|
1776
|
+
<div class="center-wrap">
|
|
1777
|
+
<div class="card">
|
|
1778
|
+
<div id="auth-main" class="step active">
|
|
1779
|
+
<h1>Welcome back.</h1>
|
|
1780
|
+
<p>What's the word?</p>
|
|
1781
|
+
<div id="auth-error" class="error-msg"></div>
|
|
1782
|
+
<label for="auth-safeword">Safe Word</label>
|
|
1783
|
+
<input type="password" id="auth-safeword" placeholder="Enter safe word">
|
|
1784
|
+
<button class="btn" id="auth-btn">Enter</button>
|
|
1785
|
+
<button class="link-btn" id="forgot-btn">Forgot the safe word?</button>
|
|
1786
|
+
</div>
|
|
1787
|
+
|
|
1788
|
+
<div id="auth-recover" class="step">
|
|
1789
|
+
<h1>Recovery</h1>
|
|
1790
|
+
<p id="recover-question-text"></p>
|
|
1791
|
+
<div id="recover-error" class="error-msg"></div>
|
|
1792
|
+
<label for="recover-answer">Your Answer</label>
|
|
1793
|
+
<input type="text" id="recover-answer" placeholder="Answer the question">
|
|
1794
|
+
<label for="recover-new-sw">New Safe Word</label>
|
|
1795
|
+
<input type="password" id="recover-new-sw" placeholder="Choose a new safe word">
|
|
1796
|
+
<button class="btn" id="recover-btn">Reset Safe Word</button>
|
|
1797
|
+
<button class="link-btn" id="back-to-auth-btn">Back</button>
|
|
1798
|
+
</div>
|
|
1799
|
+
</div>
|
|
1800
|
+
</div>
|
|
1801
|
+
</div>
|
|
1802
|
+
|
|
1803
|
+
<!-- Chat screen -->
|
|
1804
|
+
<div id="chat-screen" class="screen">
|
|
1805
|
+
<div class="chat-header">
|
|
1806
|
+
<span class="dot"></span>
|
|
1807
|
+
<a href="/" style="color:inherit;text-decoration:none;font-weight:600;" id="agent-name-header">Core</a>
|
|
1808
|
+
<nav class="header-nav" id="header-nav">
|
|
1809
|
+
<a href="/" class="active">Chat</a>
|
|
1810
|
+
<a href="/library">Library</a>
|
|
1811
|
+
<a href="/personal">Personal</a>
|
|
1812
|
+
<a href="/life">Life</a>
|
|
1813
|
+
<a href="/registry">Registry</a>
|
|
1814
|
+
<span class="nav-divider"></span>
|
|
1815
|
+
<a href="/observatory">Observatory</a>
|
|
1816
|
+
<a href="/ops">Operations</a>
|
|
1817
|
+
<a href="/board">Board</a>
|
|
1818
|
+
<a href="/roadmap">Roadmap</a>
|
|
1819
|
+
</nav>
|
|
1820
|
+
<button class="thread-toggle-btn" id="thread-toggle-btn" title="Toggle threads">☰</button>
|
|
1821
|
+
<span class="thread-current-label" id="thread-current-label" title="Click to toggle threads"></span>
|
|
1822
|
+
<div class="tab-group" id="tab-group">
|
|
1823
|
+
<button class="tab-btn active" id="tab-chat" onclick="window.__switchTab('chat')">Chat</button>
|
|
1824
|
+
<button class="tab-btn" id="tab-activity" onclick="window.__switchTab('activity')">Stream<span class="tab-badge" id="activity-badge"></span></button>
|
|
1825
|
+
</div>
|
|
1826
|
+
<span class="spacer"></span>
|
|
1827
|
+
<button class="gear-btn" onclick="openShareModal()" title="Share Core">✉</button>
|
|
1828
|
+
<button class="gear-btn" id="vault-open-btn" title="Settings">⚙</button>
|
|
1829
|
+
</div>
|
|
1830
|
+
<div class="chat-body">
|
|
1831
|
+
<div class="thread-sidebar hidden" id="thread-sidebar">
|
|
1832
|
+
<div class="thread-sidebar-header">
|
|
1833
|
+
<span>Threads</span>
|
|
1834
|
+
<button class="thread-new-btn" id="thread-new-btn">+ New</button>
|
|
1835
|
+
</div>
|
|
1836
|
+
<div class="thread-list" id="thread-list"></div>
|
|
1837
|
+
</div>
|
|
1838
|
+
<div style="display:flex;flex-direction:column;flex:1;min-width:0;">
|
|
1839
|
+
<div id="dash-greeting" class="dash-greeting" style="display:none;">
|
|
1840
|
+
<div class="greeting-text" id="greeting-text">Thinking...</div>
|
|
1841
|
+
<div id="question-chips" class="question-chips"></div>
|
|
1842
|
+
</div>
|
|
1843
|
+
<div class="messages" id="messages"></div>
|
|
1844
|
+
<div id="activity-view">
|
|
1845
|
+
<div class="stream-controls">
|
|
1846
|
+
<button class="stream-freeze-btn" id="stream-freeze-btn" title="Freeze/unfreeze stream">▶ Live</button>
|
|
1847
|
+
<span class="stream-selection-count" id="stream-selection-count"></span>
|
|
1848
|
+
<span style="flex:1;"></span>
|
|
1849
|
+
<button class="stream-branch-btn" id="stream-branch-btn">Branch</button>
|
|
1850
|
+
</div>
|
|
1851
|
+
<div id="activity-entries"><div class="activity-empty" id="activity-empty">No activity yet. Background actions will appear here.</div></div>
|
|
1852
|
+
<div class="branch-panel" id="branch-panel">
|
|
1853
|
+
<div class="branch-header">
|
|
1854
|
+
<span class="branch-header-text" id="branch-header-text"></span>
|
|
1855
|
+
<button class="branch-close-btn" id="branch-close-btn" title="Close branch">×</button>
|
|
1856
|
+
</div>
|
|
1857
|
+
<div class="branch-messages" id="branch-messages"></div>
|
|
1858
|
+
<div class="branch-composer">
|
|
1859
|
+
<input class="branch-input" id="branch-input" placeholder="Ask about these entries..." />
|
|
1860
|
+
<button class="branch-send-btn" id="branch-send-btn">Send</button>
|
|
1861
|
+
</div>
|
|
1862
|
+
</div>
|
|
1863
|
+
</div>
|
|
1864
|
+
<div class="composer" id="composer">
|
|
1865
|
+
<div class="incognito-label">incognito — local only, no memory</div>
|
|
1866
|
+
<div class="attachment-preview" id="attachment-preview"></div>
|
|
1867
|
+
<div class="composer-inner">
|
|
1868
|
+
<button class="mic-btn" id="mic-btn" title="Click to speak">🎤</button>
|
|
1869
|
+
<div class="composer-actions">
|
|
1870
|
+
<button id="attach-btn" title="Attach image">📎</button>
|
|
1871
|
+
<button id="screenshot-btn" title="Screen capture">📷</button>
|
|
1872
|
+
<button id="incognito-btn" title="Incognito — local only, no memory, plain text">🕵</button>
|
|
1873
|
+
</div>
|
|
1874
|
+
<input type="file" id="file-input" accept="image/*,.pdf,.docx,.doc,.txt,.md,.csv,.json,.js,.ts,.py,.html,.css,.xml,.yaml,.yml,.toml,.log,.sh,.bat,.c,.cpp,.java,.go,.rs,.rb,.php,.sql,.r,.swift" multiple style="display:none;" />
|
|
1875
|
+
<textarea id="chat-input" placeholder="Say something..." rows="1"></textarea>
|
|
1876
|
+
<button id="send-btn">Send</button>
|
|
1877
|
+
</div>
|
|
1878
|
+
</div>
|
|
1879
|
+
</div>
|
|
1880
|
+
</div>
|
|
1881
|
+
</div>
|
|
1882
|
+
|
|
1883
|
+
<!-- Settings slide-over -->
|
|
1884
|
+
<div class="vault-overlay" id="vault-overlay">
|
|
1885
|
+
<div class="vault-panel" id="vault-panel">
|
|
1886
|
+
<div class="vault-header">
|
|
1887
|
+
<h2>Settings</h2>
|
|
1888
|
+
<nav class="settings-nav">
|
|
1889
|
+
<a href="/board" title="Board"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></a>
|
|
1890
|
+
<a href="/ops" title="Ops"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V10"/><path d="M18 20V4"/><path d="M6 20v-4"/></svg></a>
|
|
1891
|
+
<a href="/observatory" title="Observatory"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg></a>
|
|
1892
|
+
<a href="/library" title="Library"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg></a>
|
|
1893
|
+
<a href="/registry" title="Registry"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg></a>
|
|
1894
|
+
</nav>
|
|
1895
|
+
<button class="vault-close" id="vault-close-btn">×</button>
|
|
1896
|
+
</div>
|
|
1897
|
+
<div class="vault-body" id="vault-body">
|
|
1898
|
+
<div style="text-align:center;margin-bottom:12px;">
|
|
1899
|
+
<a href="/registry" style="color:var(--accent);font-size:13px;text-decoration:none;">Manage Services & Capabilities →</a>
|
|
1900
|
+
</div>
|
|
1901
|
+
<!-- Personality section -->
|
|
1902
|
+
<div class="settings-section">
|
|
1903
|
+
<h3 class="settings-section-title">Personality</h3>
|
|
1904
|
+
<p class="settings-hint">Customize behavior. Slide traits away from center to shape the personality.</p>
|
|
1905
|
+
<div class="personality-traits" id="personality-traits">
|
|
1906
|
+
<div class="trait-row" data-trait="verbosity">
|
|
1907
|
+
<span class="trait-label">Concise</span>
|
|
1908
|
+
<div class="trait-slider">
|
|
1909
|
+
<button class="trait-dot" data-value="1" title="Concise"></button>
|
|
1910
|
+
<button class="trait-dot active" data-value="2" title="Balanced"></button>
|
|
1911
|
+
<button class="trait-dot" data-value="3" title="Verbose"></button>
|
|
1912
|
+
</div>
|
|
1913
|
+
<span class="trait-label-right">Verbose</span>
|
|
1914
|
+
</div>
|
|
1915
|
+
<div class="trait-row" data-trait="assertiveness">
|
|
1916
|
+
<span class="trait-label">Gentle</span>
|
|
1917
|
+
<div class="trait-slider">
|
|
1918
|
+
<button class="trait-dot" data-value="1" title="Gentle"></button>
|
|
1919
|
+
<button class="trait-dot active" data-value="2" title="Moderate"></button>
|
|
1920
|
+
<button class="trait-dot" data-value="3" title="Pushy"></button>
|
|
1921
|
+
</div>
|
|
1922
|
+
<span class="trait-label-right">Pushy</span>
|
|
1923
|
+
</div>
|
|
1924
|
+
<div class="trait-row" data-trait="tone">
|
|
1925
|
+
<span class="trait-label">Edgy</span>
|
|
1926
|
+
<div class="trait-slider">
|
|
1927
|
+
<button class="trait-dot" data-value="1" title="Edgy"></button>
|
|
1928
|
+
<button class="trait-dot active" data-value="2" title="Neutral"></button>
|
|
1929
|
+
<button class="trait-dot" data-value="3" title="Safe"></button>
|
|
1930
|
+
</div>
|
|
1931
|
+
<span class="trait-label-right">Safe</span>
|
|
1932
|
+
</div>
|
|
1933
|
+
<div class="trait-row" data-trait="formality">
|
|
1934
|
+
<span class="trait-label">Casual</span>
|
|
1935
|
+
<div class="trait-slider">
|
|
1936
|
+
<button class="trait-dot" data-value="1" title="Casual"></button>
|
|
1937
|
+
<button class="trait-dot active" data-value="2" title="Adaptive"></button>
|
|
1938
|
+
<button class="trait-dot" data-value="3" title="Formal"></button>
|
|
1939
|
+
</div>
|
|
1940
|
+
<span class="trait-label-right">Formal</span>
|
|
1941
|
+
</div>
|
|
1942
|
+
<div class="trait-row" data-trait="curiosity">
|
|
1943
|
+
<span class="trait-label">Task-focused</span>
|
|
1944
|
+
<div class="trait-slider">
|
|
1945
|
+
<button class="trait-dot" data-value="1" title="Task-focused"></button>
|
|
1946
|
+
<button class="trait-dot active" data-value="2" title="Balanced"></button>
|
|
1947
|
+
<button class="trait-dot" data-value="3" title="Exploratory"></button>
|
|
1948
|
+
</div>
|
|
1949
|
+
<span class="trait-label-right">Exploratory</span>
|
|
1950
|
+
</div>
|
|
1951
|
+
</div>
|
|
1952
|
+
<details class="custom-rules-toggle">
|
|
1953
|
+
<summary>Custom rules</summary>
|
|
1954
|
+
<textarea class="prompt-textarea" id="custom-rules-textarea" placeholder="One rule per line, e.g. Don't use emojis Always greet me by name" rows="4"></textarea>
|
|
1955
|
+
</details>
|
|
1956
|
+
<div class="prompt-actions">
|
|
1957
|
+
<button class="vault-add-btn" id="prompt-save-btn">Save</button>
|
|
1958
|
+
<span class="prompt-status" id="prompt-status"></span>
|
|
1959
|
+
</div>
|
|
1960
|
+
</div>
|
|
1961
|
+
<!-- Security section -->
|
|
1962
|
+
<div class="settings-section" id="security-settings-section">
|
|
1963
|
+
<h3 class="settings-section-title">Security</h3>
|
|
1964
|
+
<div class="toggle-row">
|
|
1965
|
+
<div>
|
|
1966
|
+
<div class="toggle-label">Safe Word Mode</div>
|
|
1967
|
+
<div class="toggle-sublabel" id="safeword-mode-label">Require safe word on every page load</div>
|
|
1968
|
+
</div>
|
|
1969
|
+
<select id="safeword-mode-select" style="background:var(--surface);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 10px;font-size:13px;outline:none;">
|
|
1970
|
+
<option value="always">Every load</option>
|
|
1971
|
+
<option value="restart">After restart only</option>
|
|
1972
|
+
</select>
|
|
1973
|
+
</div>
|
|
1974
|
+
<div class="prompt-actions" style="margin-top: 10px;">
|
|
1975
|
+
<button class="vault-add-btn" id="security-save-btn">Save</button>
|
|
1976
|
+
<span class="settings-saved" id="security-save-status">Saved</span>
|
|
1977
|
+
</div>
|
|
1978
|
+
</div>
|
|
1979
|
+
<!-- Network / Mesh section -->
|
|
1980
|
+
<div class="settings-section" id="mesh-settings-section">
|
|
1981
|
+
<h3 class="settings-section-title">Network</h3>
|
|
1982
|
+
<div class="toggle-row">
|
|
1983
|
+
<div>
|
|
1984
|
+
<div class="toggle-label">LAN Announcement</div>
|
|
1985
|
+
<div class="toggle-sublabel">Announce this instance on the local network via mDNS. Disable on shared or public networks.</div>
|
|
1986
|
+
</div>
|
|
1987
|
+
<label class="toggle">
|
|
1988
|
+
<input type="checkbox" id="mesh-lan-toggle">
|
|
1989
|
+
<span class="toggle-track"></span>
|
|
1990
|
+
</label>
|
|
1991
|
+
</div>
|
|
1992
|
+
<div class="toggle-row" style="margin-top: 8px;">
|
|
1993
|
+
<div>
|
|
1994
|
+
<div class="toggle-label">Allow Incoming</div>
|
|
1995
|
+
<div class="toggle-sublabel">Accept mesh connections from discovered peers on this network.</div>
|
|
1996
|
+
</div>
|
|
1997
|
+
<label class="toggle">
|
|
1998
|
+
<input type="checkbox" id="mesh-incoming-toggle">
|
|
1999
|
+
<span class="toggle-track"></span>
|
|
2000
|
+
</label>
|
|
2001
|
+
</div>
|
|
2002
|
+
<div class="prompt-actions" style="margin-top: 10px;">
|
|
2003
|
+
<button class="vault-add-btn" id="mesh-save-btn">Save</button>
|
|
2004
|
+
<span class="settings-saved" id="mesh-save-status">Saved</span>
|
|
2005
|
+
</div>
|
|
2006
|
+
</div>
|
|
2007
|
+
<!-- LLM Provider section -->
|
|
2008
|
+
<div class="settings-section" id="llm-settings-section">
|
|
2009
|
+
<h3 class="settings-section-title">LLM Provider</h3>
|
|
2010
|
+
<div class="toggle-row">
|
|
2011
|
+
<div>
|
|
2012
|
+
<div class="toggle-label">Airplane Mode</div>
|
|
2013
|
+
<div class="toggle-sublabel">ON = Ollama (local) · OFF = OpenRouter (cloud)</div>
|
|
2014
|
+
</div>
|
|
2015
|
+
<label class="toggle">
|
|
2016
|
+
<input type="checkbox" id="airplane-toggle">
|
|
2017
|
+
<span class="toggle-track"></span>
|
|
2018
|
+
</label>
|
|
2019
|
+
</div>
|
|
2020
|
+
<div class="model-field">
|
|
2021
|
+
<label>Chat model</label>
|
|
2022
|
+
<input type="text" id="model-chat" placeholder="auto (provider default)">
|
|
2023
|
+
</div>
|
|
2024
|
+
<div class="model-field">
|
|
2025
|
+
<label>Utility model</label>
|
|
2026
|
+
<input type="text" id="model-utility" placeholder="auto (provider default)">
|
|
2027
|
+
</div>
|
|
2028
|
+
<div class="prompt-actions" style="margin-top: 10px;">
|
|
2029
|
+
<button class="vault-add-btn" id="llm-save-btn">Save</button>
|
|
2030
|
+
<span class="settings-saved" id="llm-save-status">Saved</span>
|
|
2031
|
+
</div>
|
|
2032
|
+
</div>
|
|
2033
|
+
<!-- Voice section -->
|
|
2034
|
+
<div class="settings-section" id="voice-settings-section">
|
|
2035
|
+
<h3 class="settings-section-title">Voice</h3>
|
|
2036
|
+
<div class="toggle-row">
|
|
2037
|
+
<div>
|
|
2038
|
+
<div class="toggle-label">Text-to-Speech</div>
|
|
2039
|
+
<div class="toggle-sublabel" id="tts-status-label">Checking...</div>
|
|
2040
|
+
</div>
|
|
2041
|
+
<label class="toggle">
|
|
2042
|
+
<input type="checkbox" id="tts-toggle">
|
|
2043
|
+
<span class="toggle-track"></span>
|
|
2044
|
+
</label>
|
|
2045
|
+
</div>
|
|
2046
|
+
<div class="toggle-row">
|
|
2047
|
+
<div>
|
|
2048
|
+
<div class="toggle-label">Speech-to-Text</div>
|
|
2049
|
+
<div class="toggle-sublabel" id="stt-status-label">Checking...</div>
|
|
2050
|
+
</div>
|
|
2051
|
+
<label class="toggle">
|
|
2052
|
+
<input type="checkbox" id="stt-toggle">
|
|
2053
|
+
<span class="toggle-track"></span>
|
|
2054
|
+
</label>
|
|
2055
|
+
</div>
|
|
2056
|
+
<div class="toggle-row">
|
|
2057
|
+
<div>
|
|
2058
|
+
<div class="toggle-label">Auto-play responses</div>
|
|
2059
|
+
<div class="toggle-sublabel">Automatically speak replies</div>
|
|
2060
|
+
</div>
|
|
2061
|
+
<label class="toggle">
|
|
2062
|
+
<input type="checkbox" id="autoplay-toggle">
|
|
2063
|
+
<span class="toggle-track"></span>
|
|
2064
|
+
</label>
|
|
2065
|
+
</div>
|
|
2066
|
+
<div class="prompt-actions" style="margin-top: 10px;">
|
|
2067
|
+
<button class="vault-add-btn" id="voice-save-btn">Save</button>
|
|
2068
|
+
<span class="settings-saved" id="voice-save-status">Saved</span>
|
|
2069
|
+
</div>
|
|
2070
|
+
</div>
|
|
2071
|
+
<!-- Google Workspace section -->
|
|
2072
|
+
<div class="settings-section" id="google-settings-section">
|
|
2073
|
+
<h3 class="settings-section-title">Google Workspace</h3>
|
|
2074
|
+
<div class="toggle-row">
|
|
2075
|
+
<div>
|
|
2076
|
+
<div class="toggle-label" id="google-status-label">Checking...</div>
|
|
2077
|
+
<div class="toggle-sublabel" id="google-scopes-label"></div>
|
|
2078
|
+
</div>
|
|
2079
|
+
<span id="google-status-dot" style="width:10px;height:10px;border-radius:50%;background:var(--text-dim);flex-shrink:0;"></span>
|
|
2080
|
+
</div>
|
|
2081
|
+
<p class="settings-hint" id="google-hint">Add <code>GOOGLE_CLIENT_ID</code> and <code>GOOGLE_CLIENT_SECRET</code> in the vault below, then click Connect.</p>
|
|
2082
|
+
<button class="vault-add-btn" id="google-connect-btn" style="margin-top:8px;">Connect Google</button>
|
|
2083
|
+
</div>
|
|
2084
|
+
|
|
2085
|
+
<!-- Task Board section -->
|
|
2086
|
+
<div class="settings-section" id="board-settings-section">
|
|
2087
|
+
<h3 class="settings-section-title">Task Board</h3>
|
|
2088
|
+
<div class="toggle-row">
|
|
2089
|
+
<div>
|
|
2090
|
+
<div class="toggle-label" id="board-provider-label">Checking...</div>
|
|
2091
|
+
<div class="toggle-sublabel" id="board-status-label">Checking connection...</div>
|
|
2092
|
+
</div>
|
|
2093
|
+
<span id="board-status-dot" style="width:10px;height:10px;border-radius:50%;background:var(--text-dim);flex-shrink:0;"></span>
|
|
2094
|
+
</div>
|
|
2095
|
+
<p class="settings-hint" id="board-hint">Add <code>LINEAR_API_KEY</code> in the vault below to connect your Linear workspace. Use <code>issues</code> or <code>todo <title></code> in chat.</p>
|
|
2096
|
+
</div>
|
|
2097
|
+
<!-- Key Vault section -->
|
|
2098
|
+
<div class="settings-section">
|
|
2099
|
+
<h3 class="settings-section-title">Key Vault</h3>
|
|
2100
|
+
<div class="vault-empty" id="vault-empty">No keys stored yet.</div>
|
|
2101
|
+
<div id="vault-list"></div>
|
|
2102
|
+
</div>
|
|
2103
|
+
</div>
|
|
2104
|
+
<div class="vault-add">
|
|
2105
|
+
<h3>Add Key</h3>
|
|
2106
|
+
<div class="vault-error" id="vault-error"></div>
|
|
2107
|
+
<input type="text" id="vault-key-name" placeholder="KEY_NAME (e.g. OPENROUTER_API_KEY)">
|
|
2108
|
+
<input type="password" id="vault-key-value" placeholder="Value">
|
|
2109
|
+
<input type="text" id="vault-key-label" placeholder="Label (optional)">
|
|
2110
|
+
<button class="vault-add-btn" id="vault-add-btn">Add Key</button>
|
|
2111
|
+
</div>
|
|
2112
|
+
</div>
|
|
2113
|
+
</div>
|
|
2114
|
+
|
|
2115
|
+
<script>
|
|
2116
|
+
(function() {
|
|
2117
|
+
// --- State ---
|
|
2118
|
+
let sessionId = null;
|
|
2119
|
+
let userName = null;
|
|
2120
|
+
let agentName = "Core";
|
|
2121
|
+
let streaming = false;
|
|
2122
|
+
let abortController = null;
|
|
2123
|
+
|
|
2124
|
+
// Voice state
|
|
2125
|
+
let voiceStatus = { tts: false, stt: false };
|
|
2126
|
+
let ttsAutoPlay = true;
|
|
2127
|
+
let micRecording = false;
|
|
2128
|
+
let mediaRecorder = null;
|
|
2129
|
+
let micStream = null;
|
|
2130
|
+
let currentAudio = null;
|
|
2131
|
+
|
|
2132
|
+
// Convert any audio blob to WAV PCM16 (whisper-server needs WAV, browser records webm)
|
|
2133
|
+
async function blobToWav(blob) {
|
|
2134
|
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 });
|
|
2135
|
+
const arrayBuf = await blob.arrayBuffer();
|
|
2136
|
+
const decoded = await audioCtx.decodeAudioData(arrayBuf);
|
|
2137
|
+
const samples = decoded.getChannelData(0);
|
|
2138
|
+
const numSamples = samples.length;
|
|
2139
|
+
const buffer = new ArrayBuffer(44 + numSamples * 2);
|
|
2140
|
+
const view = new DataView(buffer);
|
|
2141
|
+
const sr = decoded.sampleRate;
|
|
2142
|
+
// RIFF header
|
|
2143
|
+
const writeStr = (off, s) => { for (let i = 0; i < s.length; i++) view.setUint8(off + i, s.charCodeAt(i)); };
|
|
2144
|
+
writeStr(0, "RIFF");
|
|
2145
|
+
view.setUint32(4, 36 + numSamples * 2, true);
|
|
2146
|
+
writeStr(8, "WAVE");
|
|
2147
|
+
writeStr(12, "fmt ");
|
|
2148
|
+
view.setUint32(16, 16, true);
|
|
2149
|
+
view.setUint16(20, 1, true);
|
|
2150
|
+
view.setUint16(22, 1, true);
|
|
2151
|
+
view.setUint32(24, sr, true);
|
|
2152
|
+
view.setUint32(28, sr * 2, true);
|
|
2153
|
+
view.setUint16(32, 2, true);
|
|
2154
|
+
view.setUint16(34, 16, true);
|
|
2155
|
+
writeStr(36, "data");
|
|
2156
|
+
view.setUint32(40, numSamples * 2, true);
|
|
2157
|
+
let off = 44;
|
|
2158
|
+
for (let i = 0; i < numSamples; i++, off += 2) {
|
|
2159
|
+
const s = Math.max(-1, Math.min(1, samples[i]));
|
|
2160
|
+
view.setInt16(off, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
|
|
2161
|
+
}
|
|
2162
|
+
audioCtx.close();
|
|
2163
|
+
return new Blob([buffer], { type: "audio/wav" });
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2166
|
+
// Pairing step state (collected across steps)
|
|
2167
|
+
let pairCode = "";
|
|
2168
|
+
let pairName = "";
|
|
2169
|
+
let pairSafeWord = "";
|
|
2170
|
+
let pairAgentName = "";
|
|
2171
|
+
let isFirstRun = false;
|
|
2172
|
+
|
|
2173
|
+
// --- DOM refs ---
|
|
2174
|
+
const pairScreen = document.getElementById("pair-screen");
|
|
2175
|
+
const authScreen = document.getElementById("auth-screen");
|
|
2176
|
+
const chatScreen = document.getElementById("chat-screen");
|
|
2177
|
+
const messagesEl = document.getElementById("messages");
|
|
2178
|
+
const chatInput = document.getElementById("chat-input");
|
|
2179
|
+
const sendBtn = document.getElementById("send-btn");
|
|
2180
|
+
const fileInput = document.getElementById("file-input");
|
|
2181
|
+
const attachBtn = document.getElementById("attach-btn");
|
|
2182
|
+
const screenshotBtn = document.getElementById("screenshot-btn");
|
|
2183
|
+
const attachmentPreview = document.getElementById("attachment-preview");
|
|
2184
|
+
const composerEl = document.getElementById("composer");
|
|
2185
|
+
const incognitoBtn = document.getElementById("incognito-btn");
|
|
2186
|
+
|
|
2187
|
+
// --- Incognito mode state ---
|
|
2188
|
+
let incognitoMode = false;
|
|
2189
|
+
let incognitoHistory = [];
|
|
2190
|
+
let incognitoModel = "llama3.1:8b"; // updated on toggle via /api/local-model
|
|
2191
|
+
const defaultPlaceholder = "Say something...";
|
|
2192
|
+
const incognitoPlaceholder = "incognito — local only...";
|
|
2193
|
+
|
|
2194
|
+
incognitoBtn.addEventListener("click", async () => {
|
|
2195
|
+
incognitoMode = !incognitoMode;
|
|
2196
|
+
composerEl.classList.toggle("incognito", incognitoMode);
|
|
2197
|
+
incognitoBtn.classList.toggle("active", incognitoMode);
|
|
2198
|
+
chatInput.placeholder = incognitoMode ? incognitoPlaceholder : defaultPlaceholder;
|
|
2199
|
+
if (incognitoMode) {
|
|
2200
|
+
incognitoHistory = [];
|
|
2201
|
+
// Auto-detect best local model
|
|
2202
|
+
try {
|
|
2203
|
+
const r = await fetch("/api/local-model");
|
|
2204
|
+
if (r.ok) { const d = await r.json(); incognitoModel = d.model; }
|
|
2205
|
+
} catch {}
|
|
2206
|
+
addMessage("system", "Incognito mode on — " + incognitoModel + ", local only, no memory.");
|
|
2207
|
+
} else {
|
|
2208
|
+
chatInput.classList.remove("redacted");
|
|
2209
|
+
addMessage("system", "Incognito mode off — back to normal.");
|
|
2210
|
+
}
|
|
2211
|
+
chatInput.focus();
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
// In incognito: block copy from chat messages
|
|
2215
|
+
document.addEventListener("copy", (e) => {
|
|
2216
|
+
if (!incognitoMode) return;
|
|
2217
|
+
const sel = window.getSelection();
|
|
2218
|
+
if (!sel || !sel.rangeCount) return;
|
|
2219
|
+
// Check if selection touches an incognito message
|
|
2220
|
+
const anchor = sel.anchorNode?.parentElement?.closest?.(".message.incognito");
|
|
2221
|
+
const focus = sel.focusNode?.parentElement?.closest?.(".message.incognito");
|
|
2222
|
+
if (anchor || focus) {
|
|
2223
|
+
e.preventDefault();
|
|
2224
|
+
e.clipboardData?.setData("text/plain", "[redacted — incognito]");
|
|
2225
|
+
}
|
|
2226
|
+
});
|
|
2227
|
+
|
|
2228
|
+
// In incognito: strip paste to plain text, block file attach
|
|
2229
|
+
chatInput.addEventListener("paste", (e) => {
|
|
2230
|
+
if (!incognitoMode) return; // normal mode — let default happen
|
|
2231
|
+
e.preventDefault();
|
|
2232
|
+
const text = (e.clipboardData || window.clipboardData).getData("text/plain");
|
|
2233
|
+
document.execCommand("insertText", false, text);
|
|
2234
|
+
});
|
|
2235
|
+
chatInput.addEventListener("drop", (e) => { if (incognitoMode) e.preventDefault(); });
|
|
2236
|
+
chatInput.addEventListener("dragover", (e) => { if (incognitoMode) e.preventDefault(); });
|
|
2237
|
+
|
|
2238
|
+
// Screenshot protection: blur incognito messages on PrintScreen / screen capture
|
|
2239
|
+
function setScreenCapture(on) {
|
|
2240
|
+
document.querySelectorAll(".message.incognito").forEach(el => {
|
|
2241
|
+
el.classList.toggle("screen-capture", on);
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
// PrintScreen key detection (Windows/Linux)
|
|
2245
|
+
document.addEventListener("keyup", (e) => {
|
|
2246
|
+
if (e.key === "PrintScreen") {
|
|
2247
|
+
setScreenCapture(true);
|
|
2248
|
+
setTimeout(() => setScreenCapture(false), 3000);
|
|
2249
|
+
}
|
|
2250
|
+
});
|
|
2251
|
+
// Windows Snipping Tool: Win+Shift+S triggers blur on keydown
|
|
2252
|
+
document.addEventListener("keydown", (e) => {
|
|
2253
|
+
if (e.key === "s" && e.shiftKey && e.metaKey) {
|
|
2254
|
+
setScreenCapture(true);
|
|
2255
|
+
setTimeout(() => setScreenCapture(false), 5000);
|
|
2256
|
+
}
|
|
2257
|
+
});
|
|
2258
|
+
// Visibility change: blur when tab loses focus (screen share, some capture tools)
|
|
2259
|
+
document.addEventListener("visibilitychange", () => {
|
|
2260
|
+
if (document.hidden) {
|
|
2261
|
+
setScreenCapture(true);
|
|
2262
|
+
} else {
|
|
2263
|
+
setTimeout(() => setScreenCapture(false), 500);
|
|
2264
|
+
}
|
|
2265
|
+
});
|
|
2266
|
+
|
|
2267
|
+
// Auto-redact: visible on hover/focus, blurs when mouse leaves and not focused
|
|
2268
|
+
chatInput.addEventListener("mouseenter", () => {
|
|
2269
|
+
if (incognitoMode) chatInput.classList.remove("redacted");
|
|
2270
|
+
});
|
|
2271
|
+
chatInput.addEventListener("mouseleave", () => {
|
|
2272
|
+
if (incognitoMode && document.activeElement !== chatInput && chatInput.value.trim()) chatInput.classList.add("redacted");
|
|
2273
|
+
});
|
|
2274
|
+
chatInput.addEventListener("focus", () => {
|
|
2275
|
+
if (incognitoMode) chatInput.classList.remove("redacted");
|
|
2276
|
+
});
|
|
2277
|
+
chatInput.addEventListener("blur", () => {
|
|
2278
|
+
if (incognitoMode && chatInput.value.trim()) chatInput.classList.add("redacted");
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
// --- Thread state ---
|
|
2282
|
+
const threadSidebar = document.getElementById("thread-sidebar");
|
|
2283
|
+
const threadListEl = document.getElementById("thread-list");
|
|
2284
|
+
const threadToggleBtn = document.getElementById("thread-toggle-btn");
|
|
2285
|
+
const threadNewBtn = document.getElementById("thread-new-btn");
|
|
2286
|
+
const threadCurrentLabel = document.getElementById("thread-current-label");
|
|
2287
|
+
let currentThreadId = null;
|
|
2288
|
+
let threadSidebarOpen = false;
|
|
2289
|
+
let threads = [];
|
|
2290
|
+
|
|
2291
|
+
// Mobile thread backdrop
|
|
2292
|
+
const threadBackdrop = document.createElement("div");
|
|
2293
|
+
threadBackdrop.style.cssText = "display:none;position:fixed;inset:0;background:rgba(0,0,0,0.4);z-index:49;";
|
|
2294
|
+
document.body.appendChild(threadBackdrop);
|
|
2295
|
+
threadBackdrop.addEventListener("click", () => toggleThreadSidebar());
|
|
2296
|
+
|
|
2297
|
+
function toggleThreadSidebar() {
|
|
2298
|
+
threadSidebarOpen = !threadSidebarOpen;
|
|
2299
|
+
threadSidebar.classList.toggle("hidden", !threadSidebarOpen);
|
|
2300
|
+
// Show backdrop on mobile when sidebar is open
|
|
2301
|
+
const isMobile = window.innerWidth <= 640;
|
|
2302
|
+
threadBackdrop.style.display = (threadSidebarOpen && isMobile) ? "block" : "none";
|
|
2303
|
+
if (threadSidebarOpen) loadThreads();
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
threadToggleBtn.addEventListener("click", toggleThreadSidebar);
|
|
2307
|
+
threadCurrentLabel.addEventListener("click", toggleThreadSidebar);
|
|
2308
|
+
|
|
2309
|
+
async function loadThreads() {
|
|
2310
|
+
try {
|
|
2311
|
+
const data = await api("/api/threads?sessionId=" + encodeURIComponent(sessionId));
|
|
2312
|
+
threads = data.threads || [];
|
|
2313
|
+
renderThreadList();
|
|
2314
|
+
} catch (err) {
|
|
2315
|
+
console.log("Failed to load threads:", err.message);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function renderThreadList() {
|
|
2320
|
+
threadListEl.innerHTML = "";
|
|
2321
|
+
// Add "Main chat" as the default option
|
|
2322
|
+
const mainItem = document.createElement("div");
|
|
2323
|
+
mainItem.className = "thread-item" + (currentThreadId === null ? " active" : "");
|
|
2324
|
+
mainItem.innerHTML = '<div class="thread-item-title">Main chat</div><div class="thread-item-meta">Default conversation</div>';
|
|
2325
|
+
mainItem.addEventListener("click", () => switchThread(null));
|
|
2326
|
+
threadListEl.appendChild(mainItem);
|
|
2327
|
+
|
|
2328
|
+
for (const t of threads) {
|
|
2329
|
+
const item = document.createElement("div");
|
|
2330
|
+
item.className = "thread-item" + (currentThreadId === t.id ? " active" : "");
|
|
2331
|
+
item.dataset.id = t.id;
|
|
2332
|
+
const ago = timeAgo(t.updatedAt);
|
|
2333
|
+
item.innerHTML = '<div class="thread-item-title">' + escapeHtml(t.title) + '</div>'
|
|
2334
|
+
+ '<div class="thread-item-meta">' + ago + '</div>';
|
|
2335
|
+
item.addEventListener("click", () => switchThread(t.id));
|
|
2336
|
+
threadListEl.appendChild(item);
|
|
2337
|
+
}
|
|
2338
|
+
}
|
|
2339
|
+
|
|
2340
|
+
function escapeHtml(text) {
|
|
2341
|
+
const div = document.createElement("div");
|
|
2342
|
+
div.textContent = text;
|
|
2343
|
+
return div.innerHTML;
|
|
2344
|
+
}
|
|
2345
|
+
|
|
2346
|
+
function timeAgo(iso) {
|
|
2347
|
+
const diff = Date.now() - new Date(iso).getTime();
|
|
2348
|
+
const mins = Math.floor(diff / 60000);
|
|
2349
|
+
if (mins < 1) return "just now";
|
|
2350
|
+
if (mins < 60) return mins + "m ago";
|
|
2351
|
+
const hours = Math.floor(mins / 60);
|
|
2352
|
+
if (hours < 24) return hours + "h ago";
|
|
2353
|
+
const days = Math.floor(hours / 24);
|
|
2354
|
+
return days + "d ago";
|
|
2355
|
+
}
|
|
2356
|
+
|
|
2357
|
+
async function switchThread(threadId) {
|
|
2358
|
+
if (threadId === currentThreadId) return;
|
|
2359
|
+
currentThreadId = threadId;
|
|
2360
|
+
messagesEl.innerHTML = "";
|
|
2361
|
+
renderThreadList();
|
|
2362
|
+
updateThreadLabel();
|
|
2363
|
+
|
|
2364
|
+
if (threadId) {
|
|
2365
|
+
// Load thread-specific history
|
|
2366
|
+
try {
|
|
2367
|
+
const data = await api("/api/threads/" + encodeURIComponent(threadId) + "/history?sessionId=" + encodeURIComponent(sessionId));
|
|
2368
|
+
if (data.messages) {
|
|
2369
|
+
for (const msg of data.messages) {
|
|
2370
|
+
let content = msg.content;
|
|
2371
|
+
if (typeof content === "string" && content.includes("[AGENT_REQUEST]")) {
|
|
2372
|
+
content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
|
|
2373
|
+
}
|
|
2374
|
+
addMessage(msg.role === "user" ? "user" : "assistant", content);
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
} catch (err) {
|
|
2378
|
+
console.log("Failed to load thread history:", err.message);
|
|
2379
|
+
}
|
|
2380
|
+
} else {
|
|
2381
|
+
// Load main session history
|
|
2382
|
+
try {
|
|
2383
|
+
const data = await api("/api/history?sessionId=" + encodeURIComponent(sessionId));
|
|
2384
|
+
if (data.messages) {
|
|
2385
|
+
for (const msg of data.messages) {
|
|
2386
|
+
let content = msg.content;
|
|
2387
|
+
if (typeof content === "string" && content.includes("[AGENT_REQUEST]")) {
|
|
2388
|
+
content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
|
|
2389
|
+
}
|
|
2390
|
+
addMessage(msg.role === "user" ? "user" : "assistant", content);
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
} catch (err) {
|
|
2394
|
+
console.log("Failed to load history:", err.message);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
|
|
2399
|
+
function updateThreadLabel() {
|
|
2400
|
+
if (currentThreadId) {
|
|
2401
|
+
const t = threads.find(t => t.id === currentThreadId);
|
|
2402
|
+
threadCurrentLabel.textContent = t ? t.title : "Thread";
|
|
2403
|
+
} else {
|
|
2404
|
+
threadCurrentLabel.textContent = "";
|
|
2405
|
+
}
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
threadNewBtn.addEventListener("click", async () => {
|
|
2409
|
+
try {
|
|
2410
|
+
const data = await fetch("/api/threads", {
|
|
2411
|
+
method: "POST",
|
|
2412
|
+
headers: { "Content-Type": "application/json" },
|
|
2413
|
+
body: JSON.stringify({ sessionId }),
|
|
2414
|
+
}).then(r => r.json());
|
|
2415
|
+
|
|
2416
|
+
if (data.thread) {
|
|
2417
|
+
await loadThreads();
|
|
2418
|
+
await switchThread(data.thread.id);
|
|
2419
|
+
}
|
|
2420
|
+
} catch (err) {
|
|
2421
|
+
console.error("Failed to create thread:", err.message);
|
|
2422
|
+
}
|
|
2423
|
+
});
|
|
2424
|
+
|
|
2425
|
+
// --- Attached files ---
|
|
2426
|
+
let pendingImages = []; // { data: base64, mimeType: string }
|
|
2427
|
+
let pendingFiles = []; // { name: string, text: string }
|
|
2428
|
+
|
|
2429
|
+
const TEXT_EXTENSIONS = [".txt",".md",".csv",".json",".js",".ts",".py",".html",".css",".xml",".yaml",".yml",".toml",".log",".sh",".bat",".c",".cpp",".java",".go",".rs",".rb",".php",".sql",".r",".swift"];
|
|
2430
|
+
const EXTRACT_EXTENSIONS = [".pdf",".docx",".doc"];
|
|
2431
|
+
|
|
2432
|
+
function getFileExt(name) { return "." + name.split(".").pop().toLowerCase(); }
|
|
2433
|
+
|
|
2434
|
+
attachBtn.addEventListener("click", () => { if (incognitoMode) return; fileInput.click(); });
|
|
2435
|
+
fileInput.addEventListener("change", async () => {
|
|
2436
|
+
for (const file of fileInput.files) {
|
|
2437
|
+
const ext = getFileExt(file.name);
|
|
2438
|
+
|
|
2439
|
+
if (file.type.startsWith("image/")) {
|
|
2440
|
+
// Image: base64 for vision
|
|
2441
|
+
const reader = new FileReader();
|
|
2442
|
+
reader.onload = () => {
|
|
2443
|
+
const base64 = reader.result.split(",")[1];
|
|
2444
|
+
pendingImages.push({ data: base64, mimeType: file.type });
|
|
2445
|
+
renderAttachmentPreviews();
|
|
2446
|
+
};
|
|
2447
|
+
reader.readAsDataURL(file);
|
|
2448
|
+
} else if (TEXT_EXTENSIONS.includes(ext)) {
|
|
2449
|
+
// Text file: read client-side
|
|
2450
|
+
const text = await file.text();
|
|
2451
|
+
pendingFiles.push({ name: file.name, text: text.slice(0, 50000) });
|
|
2452
|
+
renderAttachmentPreviews();
|
|
2453
|
+
} else if (EXTRACT_EXTENSIONS.includes(ext)) {
|
|
2454
|
+
// PDF/DOCX: extract server-side
|
|
2455
|
+
const formData = new FormData();
|
|
2456
|
+
formData.append("file", file);
|
|
2457
|
+
try {
|
|
2458
|
+
const res = await fetch("/api/extract", { method: "POST", body: formData });
|
|
2459
|
+
const data = await res.json();
|
|
2460
|
+
if (data.text) {
|
|
2461
|
+
pendingFiles.push({ name: file.name, text: data.text });
|
|
2462
|
+
renderAttachmentPreviews();
|
|
2463
|
+
} else {
|
|
2464
|
+
addMessage("system", "Failed to extract: " + (data.error || file.name));
|
|
2465
|
+
}
|
|
2466
|
+
} catch (err) {
|
|
2467
|
+
addMessage("system", "Upload failed: " + err.message);
|
|
2468
|
+
}
|
|
2469
|
+
} else {
|
|
2470
|
+
// Try as text
|
|
2471
|
+
try {
|
|
2472
|
+
const text = await file.text();
|
|
2473
|
+
pendingFiles.push({ name: file.name, text: text.slice(0, 50000) });
|
|
2474
|
+
renderAttachmentPreviews();
|
|
2475
|
+
} catch {
|
|
2476
|
+
addMessage("system", "Unsupported file: " + file.name);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
fileInput.value = "";
|
|
2481
|
+
});
|
|
2482
|
+
|
|
2483
|
+
screenshotBtn.addEventListener("click", async () => {
|
|
2484
|
+
if (incognitoMode) return;
|
|
2485
|
+
try {
|
|
2486
|
+
// Use html2canvas-style approach: capture the visible page
|
|
2487
|
+
const canvas = document.createElement("canvas");
|
|
2488
|
+
const rect = document.body.getBoundingClientRect();
|
|
2489
|
+
canvas.width = Math.min(window.innerWidth, 1280);
|
|
2490
|
+
canvas.height = Math.min(window.innerHeight, 960);
|
|
2491
|
+
// Use the Screen Capture API
|
|
2492
|
+
const stream = await navigator.mediaDevices.getDisplayMedia({ video: { width: 1280, height: 960 } });
|
|
2493
|
+
const video = document.createElement("video");
|
|
2494
|
+
video.srcObject = stream;
|
|
2495
|
+
await video.play();
|
|
2496
|
+
// Wait a frame for the video to render
|
|
2497
|
+
await new Promise(r => setTimeout(r, 100));
|
|
2498
|
+
canvas.width = video.videoWidth;
|
|
2499
|
+
canvas.height = video.videoHeight;
|
|
2500
|
+
canvas.getContext("2d").drawImage(video, 0, 0);
|
|
2501
|
+
stream.getTracks().forEach(t => t.stop());
|
|
2502
|
+
const dataUrl = canvas.toDataURL("image/jpeg", 0.8);
|
|
2503
|
+
const base64 = dataUrl.split(",")[1];
|
|
2504
|
+
pendingImages.push({ data: base64, mimeType: "image/jpeg" });
|
|
2505
|
+
renderAttachmentPreviews();
|
|
2506
|
+
} catch (err) {
|
|
2507
|
+
console.log("Screen capture cancelled or failed:", err.message);
|
|
2508
|
+
}
|
|
2509
|
+
});
|
|
2510
|
+
|
|
2511
|
+
function renderAttachmentPreviews() {
|
|
2512
|
+
attachmentPreview.innerHTML = "";
|
|
2513
|
+
pendingImages.forEach((img, i) => {
|
|
2514
|
+
const thumb = document.createElement("div");
|
|
2515
|
+
thumb.className = "thumb";
|
|
2516
|
+
const imgEl = document.createElement("img");
|
|
2517
|
+
imgEl.src = "data:" + img.mimeType + ";base64," + img.data;
|
|
2518
|
+
thumb.appendChild(imgEl);
|
|
2519
|
+
const removeBtn = document.createElement("button");
|
|
2520
|
+
removeBtn.className = "remove-btn";
|
|
2521
|
+
removeBtn.textContent = "x";
|
|
2522
|
+
removeBtn.addEventListener("click", () => { pendingImages.splice(i, 1); renderAttachmentPreviews(); });
|
|
2523
|
+
thumb.appendChild(removeBtn);
|
|
2524
|
+
attachmentPreview.appendChild(thumb);
|
|
2525
|
+
});
|
|
2526
|
+
pendingFiles.forEach((file, i) => {
|
|
2527
|
+
const chip = document.createElement("div");
|
|
2528
|
+
chip.className = "thumb";
|
|
2529
|
+
chip.style.cssText = "width:auto;height:auto;padding:4px 8px;display:flex;align-items:center;gap:4px;font-size:12px;color:var(--text);";
|
|
2530
|
+
chip.textContent = file.name;
|
|
2531
|
+
const removeBtn = document.createElement("button");
|
|
2532
|
+
removeBtn.className = "remove-btn";
|
|
2533
|
+
removeBtn.textContent = "x";
|
|
2534
|
+
removeBtn.addEventListener("click", () => { pendingFiles.splice(i, 1); renderAttachmentPreviews(); });
|
|
2535
|
+
chip.appendChild(removeBtn);
|
|
2536
|
+
attachmentPreview.appendChild(chip);
|
|
2537
|
+
});
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
// --- Screen switching ---
|
|
2541
|
+
function showScreen(id) {
|
|
2542
|
+
document.querySelectorAll(".screen").forEach(s => s.classList.remove("active"));
|
|
2543
|
+
document.getElementById(id).classList.add("active");
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
function showStep(parentId, stepId) {
|
|
2547
|
+
const parent = document.getElementById(parentId);
|
|
2548
|
+
parent.querySelectorAll(".step").forEach(s => s.classList.remove("active"));
|
|
2549
|
+
document.getElementById(stepId).classList.add("active");
|
|
2550
|
+
}
|
|
2551
|
+
|
|
2552
|
+
function showError(id, msg) {
|
|
2553
|
+
const el = document.getElementById(id);
|
|
2554
|
+
el.textContent = msg;
|
|
2555
|
+
el.classList.add("visible");
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
function clearError(id) {
|
|
2559
|
+
const el = document.getElementById(id);
|
|
2560
|
+
el.textContent = "";
|
|
2561
|
+
el.classList.remove("visible");
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// --- API helpers ---
|
|
2565
|
+
async function api(path, opts = {}) {
|
|
2566
|
+
const res = await fetch(path, {
|
|
2567
|
+
method: opts.method || "GET",
|
|
2568
|
+
headers: opts.body ? { "Content-Type": "application/json" } : {},
|
|
2569
|
+
body: opts.body ? JSON.stringify(opts.body) : undefined,
|
|
2570
|
+
});
|
|
2571
|
+
const data = await res.json();
|
|
2572
|
+
if (!res.ok) throw new Error(data.error || "Request failed");
|
|
2573
|
+
return data;
|
|
2574
|
+
}
|
|
2575
|
+
|
|
2576
|
+
// --- Init ---
|
|
2577
|
+
async function init() {
|
|
2578
|
+
try {
|
|
2579
|
+
// Check for startup token in URL (zero-friction local auth)
|
|
2580
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
2581
|
+
const token = urlParams.get("token");
|
|
2582
|
+
|
|
2583
|
+
if (token) {
|
|
2584
|
+
// Clean the token from the URL (don't leave it in browser history)
|
|
2585
|
+
window.history.replaceState({}, "", window.location.pathname);
|
|
2586
|
+
|
|
2587
|
+
try {
|
|
2588
|
+
const tokenResult = await api("/api/auth/token?t=" + encodeURIComponent(token));
|
|
2589
|
+
if (tokenResult.valid) {
|
|
2590
|
+
if (tokenResult.needsPairing) {
|
|
2591
|
+
// First run: skip code entry, go straight to name + safe word
|
|
2592
|
+
showScreen("pair-screen");
|
|
2593
|
+
showStep("pair-screen", "pair-step-2");
|
|
2594
|
+
pairCode = "__auto_token__";
|
|
2595
|
+
isFirstRun = true;
|
|
2596
|
+
document.getElementById("pair-name").focus();
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
// Already paired — try to resume session
|
|
2600
|
+
const check = await api("/api/auth/active-session");
|
|
2601
|
+
if (check.valid) {
|
|
2602
|
+
sessionId = check.sessionId;
|
|
2603
|
+
userName = check.name;
|
|
2604
|
+
enterChat();
|
|
2605
|
+
return;
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
} catch (e) {
|
|
2609
|
+
console.log("Token auth failed, falling back to normal flow:", e);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
|
|
2613
|
+
const status = await api("/api/status");
|
|
2614
|
+
if (status.agentName) agentName = status.agentName;
|
|
2615
|
+
if (status.paired) {
|
|
2616
|
+
// "restart" mode: try to resume session
|
|
2617
|
+
if (status.safeWordMode === "restart") {
|
|
2618
|
+
// First try sessionStorage (set by enterChat on previous visit)
|
|
2619
|
+
const storedSid = sessionStorage.getItem("dash_sid");
|
|
2620
|
+
if (storedSid) {
|
|
2621
|
+
try {
|
|
2622
|
+
const check = await api("/api/auth/validate?sessionId=" + encodeURIComponent(storedSid));
|
|
2623
|
+
if (check.valid) {
|
|
2624
|
+
sessionId = storedSid;
|
|
2625
|
+
userName = check.name;
|
|
2626
|
+
enterChat();
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
} catch {}
|
|
2630
|
+
sessionStorage.removeItem("dash_sid");
|
|
2631
|
+
}
|
|
2632
|
+
// No stored session — check if server has an active session
|
|
2633
|
+
// (user may have navigated from /library or /board without going through index.html first)
|
|
2634
|
+
try {
|
|
2635
|
+
const check = await api("/api/auth/active-session");
|
|
2636
|
+
if (check.valid) {
|
|
2637
|
+
sessionId = check.sessionId;
|
|
2638
|
+
userName = check.name;
|
|
2639
|
+
enterChat();
|
|
2640
|
+
return;
|
|
2641
|
+
}
|
|
2642
|
+
} catch {}
|
|
2643
|
+
}
|
|
2644
|
+
showScreen("auth-screen");
|
|
2645
|
+
document.getElementById("auth-safeword").focus();
|
|
2646
|
+
} else if (status.needsCode) {
|
|
2647
|
+
showScreen("pair-screen");
|
|
2648
|
+
document.getElementById("pair-code").focus();
|
|
2649
|
+
} else {
|
|
2650
|
+
// No pairing code yet — unusual, show pairing screen anyway
|
|
2651
|
+
showScreen("pair-screen");
|
|
2652
|
+
}
|
|
2653
|
+
} catch (err) {
|
|
2654
|
+
console.error("Init failed:", err);
|
|
2655
|
+
showScreen("pair-screen");
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
// --- Pairing flow ---
|
|
2660
|
+
|
|
2661
|
+
// Step 1: Verify code (client-side only, actual verify happens on submit)
|
|
2662
|
+
document.getElementById("pair-code-btn").addEventListener("click", () => {
|
|
2663
|
+
clearError("pair-error");
|
|
2664
|
+
const code = document.getElementById("pair-code").value.trim();
|
|
2665
|
+
if (!code) {
|
|
2666
|
+
showError("pair-error", "Enter the pairing code");
|
|
2667
|
+
return;
|
|
2668
|
+
}
|
|
2669
|
+
pairCode = code;
|
|
2670
|
+
isFirstRun = true;
|
|
2671
|
+
showStep("pair-screen", "pair-step-2");
|
|
2672
|
+
document.getElementById("pair-name").focus();
|
|
2673
|
+
});
|
|
2674
|
+
|
|
2675
|
+
document.getElementById("pair-code").addEventListener("keydown", (e) => {
|
|
2676
|
+
if (e.key === "Enter") document.getElementById("pair-code-btn").click();
|
|
2677
|
+
});
|
|
2678
|
+
|
|
2679
|
+
// Step 2: Name only
|
|
2680
|
+
document.getElementById("pair-name-btn").addEventListener("click", () => {
|
|
2681
|
+
clearError("pair-error-2");
|
|
2682
|
+
const name = document.getElementById("pair-name").value.trim();
|
|
2683
|
+
if (!name) { showError("pair-error-2", "Enter your name"); return; }
|
|
2684
|
+
pairName = name;
|
|
2685
|
+
document.getElementById("pair-greeting").textContent = `Nice to meet you, ${name}.`;
|
|
2686
|
+
showStep("pair-screen", "pair-step-2b");
|
|
2687
|
+
document.getElementById("pair-safeword").focus();
|
|
2688
|
+
});
|
|
2689
|
+
|
|
2690
|
+
document.getElementById("pair-name").addEventListener("keydown", (e) => {
|
|
2691
|
+
if (e.key === "Enter") document.getElementById("pair-name-btn").click();
|
|
2692
|
+
});
|
|
2693
|
+
|
|
2694
|
+
// Step 2b: Safe word with confirmation
|
|
2695
|
+
document.getElementById("pair-sw-btn").addEventListener("click", () => {
|
|
2696
|
+
clearError("pair-error-2b");
|
|
2697
|
+
const sw = document.getElementById("pair-safeword").value.trim();
|
|
2698
|
+
const confirm = document.getElementById("pair-safeword-confirm").value.trim();
|
|
2699
|
+
if (!sw) { showError("pair-error-2b", "Choose a safe word"); return; }
|
|
2700
|
+
if (sw !== confirm) { showError("pair-error-2b", "Safe words don't match"); return; }
|
|
2701
|
+
pairSafeWord = sw;
|
|
2702
|
+
document.getElementById("pair-agent-greeting").textContent = `Thanks, ${pairName}.`;
|
|
2703
|
+
showStep("pair-screen", "pair-step-4");
|
|
2704
|
+
document.getElementById("pair-agent-name").focus();
|
|
2705
|
+
});
|
|
2706
|
+
|
|
2707
|
+
document.getElementById("pair-safeword-confirm").addEventListener("keydown", (e) => {
|
|
2708
|
+
if (e.key === "Enter") document.getElementById("pair-sw-btn").click();
|
|
2709
|
+
});
|
|
2710
|
+
|
|
2711
|
+
// Step 3: Recovery → go to agent naming
|
|
2712
|
+
document.getElementById("pair-recovery-btn").addEventListener("click", () => {
|
|
2713
|
+
clearError("pair-error-3");
|
|
2714
|
+
const question = document.getElementById("pair-question").value.trim();
|
|
2715
|
+
const answer = document.getElementById("pair-answer").value.trim();
|
|
2716
|
+
if (!question) { showError("pair-error-3", "Enter a recovery question"); return; }
|
|
2717
|
+
if (!answer) { showError("pair-error-3", "Enter a recovery answer"); return; }
|
|
2718
|
+
document.getElementById("pair-agent-greeting").textContent = `Thanks, ${pairName}.`;
|
|
2719
|
+
showStep("pair-screen", "pair-step-4");
|
|
2720
|
+
document.getElementById("pair-agent-name").focus();
|
|
2721
|
+
});
|
|
2722
|
+
|
|
2723
|
+
document.getElementById("pair-answer").addEventListener("keydown", (e) => {
|
|
2724
|
+
if (e.key === "Enter") document.getElementById("pair-recovery-btn").click();
|
|
2725
|
+
});
|
|
2726
|
+
|
|
2727
|
+
// Step 4: Name the agent → submit all
|
|
2728
|
+
document.getElementById("pair-finish-btn").addEventListener("click", async () => {
|
|
2729
|
+
clearError("pair-error-4");
|
|
2730
|
+
const agentName = document.getElementById("pair-agent-name").value.trim();
|
|
2731
|
+
if (!agentName) { showError("pair-error-4", "Give me a name"); return; }
|
|
2732
|
+
pairAgentName = agentName;
|
|
2733
|
+
|
|
2734
|
+
const btn = document.getElementById("pair-finish-btn");
|
|
2735
|
+
btn.disabled = true;
|
|
2736
|
+
btn.textContent = "Setting up...";
|
|
2737
|
+
|
|
2738
|
+
try {
|
|
2739
|
+
const result = await api("/api/pair", {
|
|
2740
|
+
method: "POST",
|
|
2741
|
+
body: {
|
|
2742
|
+
code: pairCode,
|
|
2743
|
+
name: pairName,
|
|
2744
|
+
safeWord: pairSafeWord,
|
|
2745
|
+
agentName: pairAgentName,
|
|
2746
|
+
},
|
|
2747
|
+
});
|
|
2748
|
+
sessionId = result.sessionId;
|
|
2749
|
+
userName = result.name;
|
|
2750
|
+
|
|
2751
|
+
// Transition moment
|
|
2752
|
+
showStep("pair-screen", "pair-step-5");
|
|
2753
|
+
document.getElementById("pair-ready-msg").textContent = `${pairAgentName} is ready.`;
|
|
2754
|
+
document.getElementById("pair-ready-sub").textContent = `Nice to meet you, ${pairName}.`;
|
|
2755
|
+
await new Promise(r => setTimeout(r, 1800));
|
|
2756
|
+
enterChat();
|
|
2757
|
+
} catch (err) {
|
|
2758
|
+
showError("pair-error-4", err.message);
|
|
2759
|
+
if (err.message.toLowerCase().includes("code")) {
|
|
2760
|
+
showStep("pair-screen", "pair-step-1");
|
|
2761
|
+
showError("pair-error", err.message);
|
|
2762
|
+
}
|
|
2763
|
+
} finally {
|
|
2764
|
+
btn.disabled = false;
|
|
2765
|
+
btn.textContent = "Let's go";
|
|
2766
|
+
}
|
|
2767
|
+
});
|
|
2768
|
+
|
|
2769
|
+
document.getElementById("pair-agent-name").addEventListener("keydown", (e) => {
|
|
2770
|
+
if (e.key === "Enter") document.getElementById("pair-finish-btn").click();
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
// --- Auth flow ---
|
|
2774
|
+
|
|
2775
|
+
document.getElementById("auth-btn").addEventListener("click", async () => {
|
|
2776
|
+
clearError("auth-error");
|
|
2777
|
+
const sw = document.getElementById("auth-safeword").value.trim();
|
|
2778
|
+
if (!sw) { showError("auth-error", "Enter your safe word"); return; }
|
|
2779
|
+
|
|
2780
|
+
const btn = document.getElementById("auth-btn");
|
|
2781
|
+
btn.disabled = true;
|
|
2782
|
+
|
|
2783
|
+
try {
|
|
2784
|
+
const result = await api("/api/auth", {
|
|
2785
|
+
method: "POST",
|
|
2786
|
+
body: { safeWord: sw },
|
|
2787
|
+
});
|
|
2788
|
+
sessionId = result.sessionId;
|
|
2789
|
+
userName = result.name;
|
|
2790
|
+
enterChat();
|
|
2791
|
+
} catch (err) {
|
|
2792
|
+
showError("auth-error", err.message);
|
|
2793
|
+
} finally {
|
|
2794
|
+
btn.disabled = false;
|
|
2795
|
+
}
|
|
2796
|
+
});
|
|
2797
|
+
|
|
2798
|
+
document.getElementById("auth-safeword").addEventListener("keydown", (e) => {
|
|
2799
|
+
if (e.key === "Enter") document.getElementById("auth-btn").click();
|
|
2800
|
+
});
|
|
2801
|
+
|
|
2802
|
+
// --- Recovery flow ---
|
|
2803
|
+
|
|
2804
|
+
document.getElementById("forgot-btn").addEventListener("click", async () => {
|
|
2805
|
+
try {
|
|
2806
|
+
const data = await api("/api/recover");
|
|
2807
|
+
document.getElementById("recover-question-text").textContent = data.question;
|
|
2808
|
+
showStep("auth-screen", "auth-recover");
|
|
2809
|
+
document.getElementById("recover-answer").focus();
|
|
2810
|
+
} catch (err) {
|
|
2811
|
+
showError("auth-error", err.message);
|
|
2812
|
+
}
|
|
2813
|
+
});
|
|
2814
|
+
|
|
2815
|
+
document.getElementById("back-to-auth-btn").addEventListener("click", () => {
|
|
2816
|
+
showStep("auth-screen", "auth-main");
|
|
2817
|
+
});
|
|
2818
|
+
|
|
2819
|
+
document.getElementById("recover-btn").addEventListener("click", async () => {
|
|
2820
|
+
clearError("recover-error");
|
|
2821
|
+
const answer = document.getElementById("recover-answer").value.trim();
|
|
2822
|
+
const newSw = document.getElementById("recover-new-sw").value.trim();
|
|
2823
|
+
if (!answer) { showError("recover-error", "Enter your answer"); return; }
|
|
2824
|
+
if (!newSw) { showError("recover-error", "Choose a new safe word"); return; }
|
|
2825
|
+
|
|
2826
|
+
const btn = document.getElementById("recover-btn");
|
|
2827
|
+
btn.disabled = true;
|
|
2828
|
+
|
|
2829
|
+
try {
|
|
2830
|
+
const result = await api("/api/recover", {
|
|
2831
|
+
method: "POST",
|
|
2832
|
+
body: { answer, newSafeWord: newSw },
|
|
2833
|
+
});
|
|
2834
|
+
sessionId = result.sessionId;
|
|
2835
|
+
userName = result.name;
|
|
2836
|
+
enterChat();
|
|
2837
|
+
} catch (err) {
|
|
2838
|
+
showError("recover-error", err.message);
|
|
2839
|
+
} finally {
|
|
2840
|
+
btn.disabled = false;
|
|
2841
|
+
}
|
|
2842
|
+
});
|
|
2843
|
+
|
|
2844
|
+
document.getElementById("recover-new-sw").addEventListener("keydown", (e) => {
|
|
2845
|
+
if (e.key === "Enter") document.getElementById("recover-btn").click();
|
|
2846
|
+
});
|
|
2847
|
+
|
|
2848
|
+
// --- Chat ---
|
|
2849
|
+
|
|
2850
|
+
async function enterChat() {
|
|
2851
|
+
// Persist session for "restart" mode — survives page refresh but not browser close
|
|
2852
|
+
if (sessionId) sessionStorage.setItem("dash_sid", sessionId);
|
|
2853
|
+
// Update header with agent name
|
|
2854
|
+
const hdr = document.getElementById("agent-name-header");
|
|
2855
|
+
if (hdr) hdr.textContent = agentName;
|
|
2856
|
+
document.title = agentName;
|
|
2857
|
+
|
|
2858
|
+
// First-run: simplified UI — just chat, settings, share
|
|
2859
|
+
if (isFirstRun) {
|
|
2860
|
+
const nav = document.getElementById("header-nav");
|
|
2861
|
+
const tabs = document.getElementById("tab-group");
|
|
2862
|
+
const threadBtn = document.getElementById("thread-toggle-btn");
|
|
2863
|
+
const threadLabel = document.getElementById("thread-current-label");
|
|
2864
|
+
if (nav) nav.style.display = "none";
|
|
2865
|
+
if (tabs) tabs.style.display = "none";
|
|
2866
|
+
if (threadBtn) threadBtn.style.display = "none";
|
|
2867
|
+
if (threadLabel) threadLabel.style.display = "none";
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
showScreen("chat-screen");
|
|
2871
|
+
chatInput.focus();
|
|
2872
|
+
|
|
2873
|
+
// Load stored conversation history
|
|
2874
|
+
try {
|
|
2875
|
+
const data = await api("/api/history?sessionId=" + encodeURIComponent(sessionId));
|
|
2876
|
+
if (data.messages && data.messages.length > 0) {
|
|
2877
|
+
messagesEl.innerHTML = "";
|
|
2878
|
+
for (const msg of data.messages) {
|
|
2879
|
+
let content = msg.content;
|
|
2880
|
+
if (content.includes("[AGENT_REQUEST]")) {
|
|
2881
|
+
content = content.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "").trim();
|
|
2882
|
+
}
|
|
2883
|
+
addMessage(msg.role === "user" ? "user" : "assistant", content);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
} catch (err) {
|
|
2887
|
+
console.log("No history to restore:", err.message);
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
// First-run: agent introduces itself
|
|
2891
|
+
if (isFirstRun) {
|
|
2892
|
+
isFirstRun = false;
|
|
2893
|
+
const intro = `Hey ${userName || "there"}! I'm ${agentName}. I'm your personal AI — everything stays local on your machine. No cloud, no tracking, just us.\n\nHere's what I can help with:\n- **Chat** about anything — I'll remember our conversations\n- **Settings** (gear icon) to customize my personality\n- **Share** (envelope icon) to invite someone to run their own instance\n\nWhat would you like to talk about first?`;
|
|
2894
|
+
addMessage("assistant", intro);
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
startActivityPolling();
|
|
2898
|
+
|
|
2899
|
+
// Pre-load threads list
|
|
2900
|
+
await loadThreads();
|
|
2901
|
+
|
|
2902
|
+
// Restore thread + scroll after toast refresh
|
|
2903
|
+
try {
|
|
2904
|
+
const restore = sessionStorage.getItem("_dash_restore");
|
|
2905
|
+
if (restore) {
|
|
2906
|
+
sessionStorage.removeItem("_dash_restore");
|
|
2907
|
+
const { thread, scroll } = JSON.parse(restore);
|
|
2908
|
+
if (thread) await switchThread(thread);
|
|
2909
|
+
if (scroll && messagesEl) requestAnimationFrame(() => { messagesEl.scrollTop = scroll; });
|
|
2910
|
+
}
|
|
2911
|
+
} catch (e) { console.log("Restore failed:", e); }
|
|
2912
|
+
|
|
2913
|
+
// Initialize voice features (non-blocking)
|
|
2914
|
+
initVoice();
|
|
2915
|
+
|
|
2916
|
+
// Load proactive questions ("Dash is wondering...")
|
|
2917
|
+
loadPendingQuestions();
|
|
2918
|
+
|
|
2919
|
+
// Auto-open settings if linked from another page via ?settings=open
|
|
2920
|
+
if (new URLSearchParams(location.search).get('settings') === 'open') {
|
|
2921
|
+
document.getElementById('vault-open-btn')?.click();
|
|
2922
|
+
history.replaceState({}, '', '/');
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function loadPendingQuestions() {
|
|
2927
|
+
const greetingEl = document.getElementById("dash-greeting");
|
|
2928
|
+
const chipsEl = document.getElementById("question-chips");
|
|
2929
|
+
if (!greetingEl || !chipsEl) return;
|
|
2930
|
+
|
|
2931
|
+
fetch("/api/pending-questions")
|
|
2932
|
+
.then(r => r.json())
|
|
2933
|
+
.then(data => {
|
|
2934
|
+
const questions = data.questions;
|
|
2935
|
+
if (!questions || questions.length === 0) {
|
|
2936
|
+
greetingEl.style.display = "none";
|
|
2937
|
+
return;
|
|
2938
|
+
}
|
|
2939
|
+
|
|
2940
|
+
chipsEl.innerHTML = "";
|
|
2941
|
+
for (const q of questions) {
|
|
2942
|
+
const chip = document.createElement("div");
|
|
2943
|
+
chip.className = "question-chip";
|
|
2944
|
+
chip.dataset.questionId = q.id;
|
|
2945
|
+
|
|
2946
|
+
const icon = document.createElement("span");
|
|
2947
|
+
icon.className = "chip-icon";
|
|
2948
|
+
icon.textContent = "?";
|
|
2949
|
+
|
|
2950
|
+
const text = document.createElement("span");
|
|
2951
|
+
text.className = "chip-text";
|
|
2952
|
+
text.textContent = q.question;
|
|
2953
|
+
|
|
2954
|
+
const dismiss = document.createElement("button");
|
|
2955
|
+
dismiss.className = "chip-dismiss";
|
|
2956
|
+
dismiss.textContent = "\u00d7";
|
|
2957
|
+
dismiss.title = "Dismiss";
|
|
2958
|
+
dismiss.addEventListener("click", (e) => {
|
|
2959
|
+
e.stopPropagation();
|
|
2960
|
+
fetch("/api/dismiss-question", {
|
|
2961
|
+
method: "POST",
|
|
2962
|
+
headers: { "Content-Type": "application/json" },
|
|
2963
|
+
body: JSON.stringify({ questionId: q.id }),
|
|
2964
|
+
});
|
|
2965
|
+
chip.remove();
|
|
2966
|
+
if (chipsEl.children.length === 0) greetingEl.style.display = "none";
|
|
2967
|
+
});
|
|
2968
|
+
|
|
2969
|
+
chip.appendChild(icon);
|
|
2970
|
+
chip.appendChild(text);
|
|
2971
|
+
chip.appendChild(dismiss);
|
|
2972
|
+
|
|
2973
|
+
chip.addEventListener("click", () => {
|
|
2974
|
+
chatInput.value = q.question;
|
|
2975
|
+
greetingEl.style.display = "none";
|
|
2976
|
+
sendMessage();
|
|
2977
|
+
});
|
|
2978
|
+
|
|
2979
|
+
chipsEl.appendChild(chip);
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
greetingEl.style.display = "block";
|
|
2983
|
+
})
|
|
2984
|
+
.catch(() => {
|
|
2985
|
+
greetingEl.style.display = "none";
|
|
2986
|
+
});
|
|
2987
|
+
|
|
2988
|
+
// Hide greeting when user starts typing their own message
|
|
2989
|
+
chatInput.addEventListener("input", function hideGreeting() {
|
|
2990
|
+
if (chatInput.value.trim().length > 0) {
|
|
2991
|
+
greetingEl.style.display = "none";
|
|
2992
|
+
chatInput.removeEventListener("input", hideGreeting);
|
|
2993
|
+
}
|
|
2994
|
+
});
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// Lightweight markdown → HTML (bold, italic, code, bullets, line breaks)
|
|
2998
|
+
function renderMarkdown(text) {
|
|
2999
|
+
let html = text
|
|
3000
|
+
.replace(/&/g, "&")
|
|
3001
|
+
.replace(/</g, "<")
|
|
3002
|
+
.replace(/>/g, ">");
|
|
3003
|
+
// Code blocks (``` ... ```)
|
|
3004
|
+
html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
|
|
3005
|
+
// Inline code
|
|
3006
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
3007
|
+
// Bold
|
|
3008
|
+
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
3009
|
+
// Italic
|
|
3010
|
+
html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
|
3011
|
+
// Bullet lists (lines starting with - or *)
|
|
3012
|
+
html = html.replace(/^[\-\*] (.+)$/gm, '<li>$1</li>');
|
|
3013
|
+
html = html.replace(/(<li>.*<\/li>\n?)+/g, '<ul>$&</ul>');
|
|
3014
|
+
// Numbered lists
|
|
3015
|
+
html = html.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
|
|
3016
|
+
// Line breaks (but not inside pre/ul)
|
|
3017
|
+
html = html.replace(/\n/g, '<br>');
|
|
3018
|
+
// Clean up extra <br> inside block elements
|
|
3019
|
+
html = html.replace(/<br><\/(ul|pre|li)>/g, '</$1>');
|
|
3020
|
+
html = html.replace(/<(ul|pre)><br>/g, '<$1>');
|
|
3021
|
+
return html;
|
|
3022
|
+
}
|
|
3023
|
+
|
|
3024
|
+
// Streaming-aware markdown: auto-close incomplete patterns so partial
|
|
3025
|
+
// markdown renders progressively as tokens arrive instead of staying
|
|
3026
|
+
// unformatted until the closing syntax appears.
|
|
3027
|
+
function renderStreamingMarkdown(text) {
|
|
3028
|
+
var patched = text;
|
|
3029
|
+
|
|
3030
|
+
// 1. Auto-close unclosed fenced code blocks
|
|
3031
|
+
var codeBlockTicks = patched.match(/```/g);
|
|
3032
|
+
if (codeBlockTicks && codeBlockTicks.length % 2 !== 0) {
|
|
3033
|
+
patched += "\n```";
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
// Strip code blocks + inline code for further pattern counting
|
|
3037
|
+
var noCodeBlocks = patched.replace(/```[\s\S]*?```/g, "");
|
|
3038
|
+
var noInline = noCodeBlocks.replace(/`[^`]*`/g, "");
|
|
3039
|
+
|
|
3040
|
+
// 2. Auto-close unclosed inline code
|
|
3041
|
+
var backticks = noCodeBlocks.match(/`/g);
|
|
3042
|
+
if (backticks && backticks.length % 2 !== 0) {
|
|
3043
|
+
patched += "`";
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// 3. Auto-close unclosed bold **
|
|
3047
|
+
var boldMarkers = noInline.match(/\*\*/g);
|
|
3048
|
+
if (boldMarkers && boldMarkers.length % 2 !== 0) {
|
|
3049
|
+
patched += "**";
|
|
3050
|
+
}
|
|
3051
|
+
|
|
3052
|
+
// 4. Auto-close unclosed italic * (after stripping bold pairs)
|
|
3053
|
+
var afterBold = noInline.replace(/\*\*/g, "");
|
|
3054
|
+
var italicMarkers = afterBold.match(/\*/g);
|
|
3055
|
+
if (italicMarkers && italicMarkers.length % 2 !== 0) {
|
|
3056
|
+
patched += "*";
|
|
3057
|
+
}
|
|
3058
|
+
|
|
3059
|
+
return renderMarkdown(patched);
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
function friendlyLLMError(raw) {
|
|
3063
|
+
if (/402|credits|afford/i.test(raw)) return "I'm out of API credits — top up at openrouter.ai/settings/credits and try again.";
|
|
3064
|
+
if (/429|rate.limit/i.test(raw)) return "Rate limited by the LLM provider. Wait a moment and try again.";
|
|
3065
|
+
if (/401|unauthorized|invalid.*key/i.test(raw)) return "API key is invalid or expired. Check Settings.";
|
|
3066
|
+
if (/timeout|timed.out/i.test(raw)) return "LLM request timed out. Try again.";
|
|
3067
|
+
if (/5\d{2}|server.error|internal/i.test(raw)) return "LLM provider is having issues. Try again shortly.";
|
|
3068
|
+
return "LLM error: " + raw;
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// PII detection — wraps sensitive patterns in redaction spans
|
|
3072
|
+
const PII_PATTERNS = [
|
|
3073
|
+
// Phone numbers: (213) 248-4250, 213-248-4250, 213.248.4250, +1 213 248 4250, 5555555555
|
|
3074
|
+
/(?:\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
|
|
3075
|
+
// Email addresses
|
|
3076
|
+
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
|
|
3077
|
+
// SSN: 123-45-6789 or 123 45 6789
|
|
3078
|
+
/\b\d{3}[-\s]\d{2}[-\s]\d{4}\b/g,
|
|
3079
|
+
// Credit card-ish: 4 groups of 4 digits
|
|
3080
|
+
/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
|
|
3081
|
+
// IP addresses
|
|
3082
|
+
/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g,
|
|
3083
|
+
// Common profanity
|
|
3084
|
+
/\b(?:fuck|shit|damn|ass|bitch|hell|crap|dick|cock|pussy|bastard|asshole|bullshit|motherfuck\w*)\b/gi,
|
|
3085
|
+
];
|
|
3086
|
+
|
|
3087
|
+
function redactPII(text) {
|
|
3088
|
+
// Collect all match ranges
|
|
3089
|
+
const ranges = [];
|
|
3090
|
+
for (const pat of PII_PATTERNS) {
|
|
3091
|
+
pat.lastIndex = 0;
|
|
3092
|
+
let m;
|
|
3093
|
+
while ((m = pat.exec(text)) !== null) {
|
|
3094
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
3095
|
+
}
|
|
3096
|
+
}
|
|
3097
|
+
if (ranges.length === 0) return null; // nothing to redact
|
|
3098
|
+
|
|
3099
|
+
// Sort and merge overlapping ranges
|
|
3100
|
+
ranges.sort((a, b) => a[0] - b[0]);
|
|
3101
|
+
const merged = [ranges[0]];
|
|
3102
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
3103
|
+
const last = merged[merged.length - 1];
|
|
3104
|
+
if (ranges[i][0] <= last[1]) {
|
|
3105
|
+
last[1] = Math.max(last[1], ranges[i][1]);
|
|
3106
|
+
} else {
|
|
3107
|
+
merged.push(ranges[i]);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
// Build HTML with redaction spans
|
|
3112
|
+
let html = "";
|
|
3113
|
+
let cursor = 0;
|
|
3114
|
+
for (const [start, end] of merged) {
|
|
3115
|
+
html += escapeHtml(text.slice(cursor, start));
|
|
3116
|
+
html += '<span class="pii-redact">' + escapeHtml(text.slice(start, end)) + '</span>';
|
|
3117
|
+
cursor = end;
|
|
3118
|
+
}
|
|
3119
|
+
html += escapeHtml(text.slice(cursor));
|
|
3120
|
+
return html;
|
|
3121
|
+
}
|
|
3122
|
+
|
|
3123
|
+
function escapeHtml(s) {
|
|
3124
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3125
|
+
}
|
|
3126
|
+
|
|
3127
|
+
function addMessage(role, content) {
|
|
3128
|
+
const div = document.createElement("div");
|
|
3129
|
+
div.className = "message " + role;
|
|
3130
|
+
if (role === "system") {
|
|
3131
|
+
div.innerHTML = '<div class="content"></div>';
|
|
3132
|
+
} else {
|
|
3133
|
+
div.innerHTML = '<div class="role">' + (role === "user" ? userName || "You" : agentName) + '</div>'
|
|
3134
|
+
+ '<div class="content"></div>';
|
|
3135
|
+
}
|
|
3136
|
+
// Render markdown for assistant messages, plain text for user
|
|
3137
|
+
if (role === "assistant") {
|
|
3138
|
+
div.querySelector(".content").innerHTML = renderMarkdown(content);
|
|
3139
|
+
} else {
|
|
3140
|
+
div.querySelector(".content").textContent = content;
|
|
3141
|
+
}
|
|
3142
|
+
messagesEl.appendChild(div);
|
|
3143
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3144
|
+
return div;
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
function createStreamingMessage() {
|
|
3148
|
+
const div = document.createElement("div");
|
|
3149
|
+
div.className = "message assistant";
|
|
3150
|
+
div.innerHTML = '<div class="role">' + agentName + '<span class="model-tag" id="streaming-model-tag"></span></div><div class="content"></div>';
|
|
3151
|
+
messagesEl.appendChild(div);
|
|
3152
|
+
return div.querySelector(".content");
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
function setButtonStop() {
|
|
3156
|
+
sendBtn.textContent = "Stop";
|
|
3157
|
+
sendBtn.classList.add("stop");
|
|
3158
|
+
sendBtn.disabled = false;
|
|
3159
|
+
}
|
|
3160
|
+
|
|
3161
|
+
function setButtonSend() {
|
|
3162
|
+
sendBtn.textContent = "Send";
|
|
3163
|
+
sendBtn.classList.remove("stop");
|
|
3164
|
+
sendBtn.disabled = false;
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
async function sendMessage() {
|
|
3168
|
+
const text = chatInput.value.trim();
|
|
3169
|
+
if (!text && pendingImages.length === 0 && pendingFiles.length === 0) return;
|
|
3170
|
+
if (streaming) return;
|
|
3171
|
+
|
|
3172
|
+
// Stop any currently playing TTS audio
|
|
3173
|
+
stopSpeaking();
|
|
3174
|
+
|
|
3175
|
+
// Build message text with file content prepended
|
|
3176
|
+
let msgText = text || (pendingImages.length > 0 ? "What do you see in this image?" : "");
|
|
3177
|
+
const fileTexts = pendingFiles.map(f => "--- " + f.name + " ---\n" + f.text + "\n--- end " + f.name + " ---");
|
|
3178
|
+
if (fileTexts.length > 0) {
|
|
3179
|
+
msgText = fileTexts.join("\n\n") + "\n\n" + msgText;
|
|
3180
|
+
}
|
|
3181
|
+
const imgPayload = pendingImages.length > 0 ? [...pendingImages] : undefined;
|
|
3182
|
+
|
|
3183
|
+
// Build display text
|
|
3184
|
+
const attachCount = pendingImages.length + pendingFiles.length;
|
|
3185
|
+
const attachLabel = attachCount > 0 ? " [+" + attachCount + " file" + (attachCount > 1 ? "s" : "") + "]" : "";
|
|
3186
|
+
|
|
3187
|
+
chatInput.value = "";
|
|
3188
|
+
pendingImages = [];
|
|
3189
|
+
pendingFiles = [];
|
|
3190
|
+
renderAttachmentPreviews();
|
|
3191
|
+
autoResize();
|
|
3192
|
+
const userMsg = addMessage("user", text + attachLabel);
|
|
3193
|
+
if (incognitoMode) {
|
|
3194
|
+
userMsg.classList.add("incognito");
|
|
3195
|
+
const redacted = redactPII(text + attachLabel);
|
|
3196
|
+
if (redacted) userMsg.querySelector(".content").innerHTML = redacted;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
streaming = true;
|
|
3200
|
+
abortController = new AbortController();
|
|
3201
|
+
setButtonStop();
|
|
3202
|
+
|
|
3203
|
+
// --- Incognito: route to local Ollama, skip server entirely ---
|
|
3204
|
+
if (incognitoMode) {
|
|
3205
|
+
incognitoHistory.push({ role: "user", content: msgText });
|
|
3206
|
+
const ollamaMessages = [
|
|
3207
|
+
{ role: "system", content: "You are " + agentName + ", running in incognito mode. Local only. No memory. No network. Be concise." },
|
|
3208
|
+
...incognitoHistory.slice(-10)
|
|
3209
|
+
];
|
|
3210
|
+
// 30s timeout for local model
|
|
3211
|
+
const ollamaTimeout = setTimeout(() => { if (abortController) abortController.abort(); }, 35000);
|
|
3212
|
+
try {
|
|
3213
|
+
const res = await fetch("http://localhost:11434/api/chat", {
|
|
3214
|
+
method: "POST",
|
|
3215
|
+
headers: { "Content-Type": "application/json" },
|
|
3216
|
+
body: JSON.stringify({ model: incognitoModel, messages: ollamaMessages, stream: true }),
|
|
3217
|
+
signal: abortController.signal,
|
|
3218
|
+
});
|
|
3219
|
+
if (!res.ok) {
|
|
3220
|
+
addMessage("system", "Ollama error: " + res.status + " — is Ollama running?");
|
|
3221
|
+
return;
|
|
3222
|
+
}
|
|
3223
|
+
const contentEl = createStreamingMessage();
|
|
3224
|
+
contentEl.closest(".message").classList.add("incognito");
|
|
3225
|
+
contentEl.classList.add("streaming-cursor");
|
|
3226
|
+
const tag = document.getElementById("streaming-model-tag");
|
|
3227
|
+
if (tag) { tag.textContent = "ollama/local"; tag.removeAttribute("id"); }
|
|
3228
|
+
const reader = res.body.getReader();
|
|
3229
|
+
const decoder = new TextDecoder();
|
|
3230
|
+
let buffer = "", fullReply = "";
|
|
3231
|
+
while (true) {
|
|
3232
|
+
const { done, value } = await reader.read();
|
|
3233
|
+
if (done) break;
|
|
3234
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3235
|
+
const lines = buffer.split("\n");
|
|
3236
|
+
buffer = lines.pop() || "";
|
|
3237
|
+
for (const line of lines) {
|
|
3238
|
+
if (!line.trim()) continue;
|
|
3239
|
+
try {
|
|
3240
|
+
const parsed = JSON.parse(line);
|
|
3241
|
+
const token = parsed.message?.content || "";
|
|
3242
|
+
fullReply += token;
|
|
3243
|
+
contentEl.innerHTML = renderStreamingMarkdown(fullReply);
|
|
3244
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3245
|
+
} catch {}
|
|
3246
|
+
}
|
|
3247
|
+
}
|
|
3248
|
+
contentEl.classList.remove("streaming-cursor");
|
|
3249
|
+
const redactedReply = redactPII(fullReply);
|
|
3250
|
+
contentEl.innerHTML = redactedReply || (fullReply ? renderMarkdown(fullReply) : "");
|
|
3251
|
+
incognitoHistory.push({ role: "assistant", content: fullReply });
|
|
3252
|
+
} catch (err) {
|
|
3253
|
+
if (err.name === "AbortError") {
|
|
3254
|
+
addMessage("system", "Local model timed out or was stopped.");
|
|
3255
|
+
} else {
|
|
3256
|
+
addMessage("system", "Ollama connection failed — is it running?");
|
|
3257
|
+
}
|
|
3258
|
+
var cursorEl = messagesEl.querySelector(".streaming-cursor");
|
|
3259
|
+
if (cursorEl) cursorEl.classList.remove("streaming-cursor");
|
|
3260
|
+
} finally {
|
|
3261
|
+
clearTimeout(ollamaTimeout);
|
|
3262
|
+
streaming = false;
|
|
3263
|
+
abortController = null;
|
|
3264
|
+
setButtonSend();
|
|
3265
|
+
chatInput.focus();
|
|
3266
|
+
}
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
try {
|
|
3271
|
+
const payload = { sessionId, message: msgText };
|
|
3272
|
+
if (imgPayload) payload.images = imgPayload;
|
|
3273
|
+
if (currentThreadId) payload.threadId = currentThreadId;
|
|
3274
|
+
|
|
3275
|
+
const res = await fetch("/api/chat", {
|
|
3276
|
+
method: "POST",
|
|
3277
|
+
headers: { "Content-Type": "application/json" },
|
|
3278
|
+
body: JSON.stringify(payload),
|
|
3279
|
+
signal: abortController.signal,
|
|
3280
|
+
});
|
|
3281
|
+
|
|
3282
|
+
if (!res.ok) {
|
|
3283
|
+
const data = await res.json();
|
|
3284
|
+
if (data.error && data.error.includes("session")) {
|
|
3285
|
+
showScreen("auth-screen");
|
|
3286
|
+
return;
|
|
3287
|
+
}
|
|
3288
|
+
addMessage("system", "Error: " + (data.error || "Request failed"));
|
|
3289
|
+
return;
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3292
|
+
// System command response (learn) — not streamed
|
|
3293
|
+
const contentType = res.headers.get("content-type") || "";
|
|
3294
|
+
if (contentType.includes("application/json")) {
|
|
3295
|
+
const data = await res.json();
|
|
3296
|
+
if (data.system) {
|
|
3297
|
+
if (data.boardItems && data.boardItems.issues) {
|
|
3298
|
+
renderBoardCards(data.boardItems.issues);
|
|
3299
|
+
} else {
|
|
3300
|
+
addMessage("system", data.content);
|
|
3301
|
+
}
|
|
3302
|
+
return;
|
|
3303
|
+
}
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3306
|
+
// Streamed chat response
|
|
3307
|
+
const contentEl = createStreamingMessage();
|
|
3308
|
+
contentEl.classList.add("streaming-cursor");
|
|
3309
|
+
const reader = res.body.getReader();
|
|
3310
|
+
const decoder = new TextDecoder();
|
|
3311
|
+
let buffer = "";
|
|
3312
|
+
let streamingText = "";
|
|
3313
|
+
|
|
3314
|
+
while (true) {
|
|
3315
|
+
const { done, value } = await reader.read();
|
|
3316
|
+
if (done) break;
|
|
3317
|
+
|
|
3318
|
+
buffer += decoder.decode(value, { stream: true });
|
|
3319
|
+
const lines = buffer.split("\n");
|
|
3320
|
+
buffer = lines.pop() || "";
|
|
3321
|
+
|
|
3322
|
+
for (const line of lines) {
|
|
3323
|
+
const trimmed = line.trim();
|
|
3324
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
3325
|
+
try {
|
|
3326
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
3327
|
+
if (data.meta) {
|
|
3328
|
+
const tag = document.getElementById("streaming-model-tag");
|
|
3329
|
+
if (tag) {
|
|
3330
|
+
tag.textContent = data.meta.model;
|
|
3331
|
+
tag.removeAttribute("id");
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
if (data.token) {
|
|
3335
|
+
streamingText += data.token;
|
|
3336
|
+
contentEl.innerHTML = renderStreamingMarkdown(streamingText);
|
|
3337
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3338
|
+
}
|
|
3339
|
+
if (data.agentSpawned) {
|
|
3340
|
+
// Server confirmed it parsed and spawned an agent — start polling
|
|
3341
|
+
console.log("[agent] Server spawned:", data.agentSpawned.label);
|
|
3342
|
+
startAgentPoll();
|
|
3343
|
+
}
|
|
3344
|
+
if (data.agentError) {
|
|
3345
|
+
console.error("[agent] Spawn failed:", data.agentError);
|
|
3346
|
+
addMessage("system", "Agent spawn failed: " + data.agentError.label + " — " + data.agentError.error);
|
|
3347
|
+
}
|
|
3348
|
+
if (data.boardItems && data.boardItems.issues) {
|
|
3349
|
+
if (data.boardItems.issues.length > 0) {
|
|
3350
|
+
renderBoardCards(data.boardItems.issues);
|
|
3351
|
+
} else if (data.boardItems.empty) {
|
|
3352
|
+
addMessage("system", "No items found for that view.");
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
if (data.error) {
|
|
3356
|
+
addMessage("system", friendlyLLMError(data.error));
|
|
3357
|
+
}
|
|
3358
|
+
} catch {}
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
// Strip [AGENT_REQUEST]...[/AGENT_REQUEST] and start agent poll
|
|
3362
|
+
let finalText = streamingText;
|
|
3363
|
+
if (finalText.includes("[AGENT_REQUEST]")) {
|
|
3364
|
+
// Count how many blocks were found
|
|
3365
|
+
const blockMatches = finalText.match(/\[AGENT_REQUEST\]/g);
|
|
3366
|
+
console.log("[agent] Detected " + (blockMatches ? blockMatches.length : 0) + " AGENT_REQUEST block(s) in response");
|
|
3367
|
+
finalText = finalText
|
|
3368
|
+
.replace(/\s*\[AGENT_REQUEST\][\s\S]*?\[\/AGENT_REQUEST\]\s*/g, "")
|
|
3369
|
+
.trim();
|
|
3370
|
+
startAgentPoll();
|
|
3371
|
+
} else if (finalText.includes("AGENT_REQUEST")) {
|
|
3372
|
+
// Partial detection — LLM wrote something agent-related but not in proper format
|
|
3373
|
+
console.warn("[agent] Response mentions AGENT_REQUEST but no complete blocks found");
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
// Strip [BOARD_VIEW]...[/BOARD_VIEW] blocks from visible text
|
|
3377
|
+
if (finalText.includes("[BOARD_VIEW]")) {
|
|
3378
|
+
finalText = finalText
|
|
3379
|
+
.replace(/\s*\[BOARD_VIEW\][\s\S]*?\[\/BOARD_VIEW\]\s*/g, "")
|
|
3380
|
+
.replace(/\s*\[BOARD_VIEW\]\s*\{[\s\S]*?\}\s*/g, "")
|
|
3381
|
+
.trim();
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
// Strip [BOARD_ACTION]...[/BOARD_ACTION] blocks from visible text
|
|
3385
|
+
if (finalText.includes("[BOARD_ACTION]")) {
|
|
3386
|
+
finalText = finalText
|
|
3387
|
+
.replace(/\s*\[BOARD_ACTION\][\s\S]*?\[\/BOARD_ACTION\]\s*/g, "")
|
|
3388
|
+
.replace(/\s*\[BOARD_ACTION\]\s*\{[\s\S]*?\}\s*\]?\s*/g, "")
|
|
3389
|
+
.trim();
|
|
3390
|
+
}
|
|
3391
|
+
|
|
3392
|
+
// Remove streaming cursor and re-render final clean markdown
|
|
3393
|
+
if (contentEl) {
|
|
3394
|
+
contentEl.classList.remove("streaming-cursor");
|
|
3395
|
+
contentEl.innerHTML = finalText ? renderMarkdown(finalText) : "";
|
|
3396
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3397
|
+
}
|
|
3398
|
+
|
|
3399
|
+
// Auto-play TTS for assistant response
|
|
3400
|
+
if (contentEl && voiceStatus.tts && ttsAutoPlay) {
|
|
3401
|
+
if (finalText) speakText(finalText);
|
|
3402
|
+
}
|
|
3403
|
+
} catch (err) {
|
|
3404
|
+
if (err.name === "AbortError") {
|
|
3405
|
+
// User clicked Stop — no error message needed
|
|
3406
|
+
} else {
|
|
3407
|
+
addMessage("system", "Connection error: " + err.message);
|
|
3408
|
+
}
|
|
3409
|
+
// Remove streaming cursor on error/abort too
|
|
3410
|
+
var cursorEl = messagesEl.querySelector(".streaming-cursor");
|
|
3411
|
+
if (cursorEl) cursorEl.classList.remove("streaming-cursor");
|
|
3412
|
+
} finally {
|
|
3413
|
+
streaming = false;
|
|
3414
|
+
abortController = null;
|
|
3415
|
+
setButtonSend();
|
|
3416
|
+
chatInput.focus();
|
|
3417
|
+
// Refresh threads to pick up auto-generated titles
|
|
3418
|
+
if (currentThreadId) loadThreads();
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
|
|
3422
|
+
// --- Agent status polling ---
|
|
3423
|
+
// Polls /api/agents/tasks and dynamically creates/updates status bars
|
|
3424
|
+
// for all running/pending tasks. Keyed by task ID, not label.
|
|
3425
|
+
var agentPollTimer = null;
|
|
3426
|
+
var agentElements = {}; // taskId → DOM element
|
|
3427
|
+
var agentPollSince = null; // ISO timestamp: only show tasks created after this
|
|
3428
|
+
|
|
3429
|
+
function startAgentPoll() {
|
|
3430
|
+
if (!agentPollSince) {
|
|
3431
|
+
// Record a timestamp slightly in the past to account for clock skew
|
|
3432
|
+
// between client and server, and to catch tasks being created right now
|
|
3433
|
+
var d = new Date();
|
|
3434
|
+
d.setSeconds(d.getSeconds() - 10);
|
|
3435
|
+
agentPollSince = d.toISOString();
|
|
3436
|
+
console.log("[agent] Poll started, looking for tasks since", agentPollSince);
|
|
3437
|
+
}
|
|
3438
|
+
if (!agentPollTimer) {
|
|
3439
|
+
// Delay first poll to give server time to create all tasks
|
|
3440
|
+
setTimeout(pollAgentTasks, 1500);
|
|
3441
|
+
agentPollTimer = setInterval(pollAgentTasks, 3000);
|
|
3442
|
+
}
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3445
|
+
async function pollAgentTasks() {
|
|
3446
|
+
try {
|
|
3447
|
+
var data = await api("/api/agents/tasks");
|
|
3448
|
+
if (!data.tasks || data.tasks.length === 0) {
|
|
3449
|
+
console.log("[agent] Poll: no tasks returned from API");
|
|
3450
|
+
return;
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
// Count how many tasks pass the time filter
|
|
3454
|
+
var recentCount = 0;
|
|
3455
|
+
var hasRunning = false;
|
|
3456
|
+
|
|
3457
|
+
for (var i = 0; i < data.tasks.length; i++) {
|
|
3458
|
+
var task = data.tasks[i];
|
|
3459
|
+
|
|
3460
|
+
// Only track tasks created after we started polling
|
|
3461
|
+
if (agentPollSince && task.createdAt < agentPollSince) continue;
|
|
3462
|
+
recentCount++;
|
|
3463
|
+
|
|
3464
|
+
var el = agentElements[task.id];
|
|
3465
|
+
|
|
3466
|
+
if (task.status === "running" || task.status === "pending") {
|
|
3467
|
+
hasRunning = true;
|
|
3468
|
+
|
|
3469
|
+
// Create indicator if it doesn't exist
|
|
3470
|
+
if (!el) {
|
|
3471
|
+
el = document.createElement("div");
|
|
3472
|
+
el.className = "agent-status";
|
|
3473
|
+
el.innerHTML = '<div class="agent-spinner"></div>'
|
|
3474
|
+
+ '<span class="agent-label">Running agent: <strong>' + task.label + '</strong></span>'
|
|
3475
|
+
+ '<button class="agent-cancel">Cancel</button>';
|
|
3476
|
+
messagesEl.appendChild(el);
|
|
3477
|
+
agentElements[task.id] = el;
|
|
3478
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
// Wire cancel button
|
|
3482
|
+
(function(taskId, label, elem) {
|
|
3483
|
+
var btn = elem.querySelector(".agent-cancel");
|
|
3484
|
+
if (btn) btn.onclick = async function() {
|
|
3485
|
+
try {
|
|
3486
|
+
await api("/api/agents/tasks/" + taskId + "/cancel", { method: "POST" });
|
|
3487
|
+
elem.className = "agent-status failed";
|
|
3488
|
+
elem.innerHTML = '<span class="agent-label">Agent cancelled: <strong>' + label + '</strong></span>';
|
|
3489
|
+
} catch {}
|
|
3490
|
+
};
|
|
3491
|
+
})(task.id, task.label, el);
|
|
3492
|
+
|
|
3493
|
+
} else if (task.status === "completed" || task.status === "failed" || task.status === "cancelled") {
|
|
3494
|
+
if (el) {
|
|
3495
|
+
// Resolve existing indicator
|
|
3496
|
+
var isDone = task.status === "completed";
|
|
3497
|
+
el.className = "agent-status " + (isDone ? "done" : "failed");
|
|
3498
|
+
var resultText = isDone ? "completed" : task.status;
|
|
3499
|
+
if (task.resultSummary) {
|
|
3500
|
+
resultText += " — " + task.resultSummary.slice(0, 200);
|
|
3501
|
+
}
|
|
3502
|
+
el.innerHTML = '<span class="agent-label">Agent ' + resultText + ': <strong>'
|
|
3503
|
+
+ task.label + '</strong></span>';
|
|
3504
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3505
|
+
} else {
|
|
3506
|
+
// Task completed before we ever saw it running — show result directly
|
|
3507
|
+
console.log("[agent] Task completed before first poll:", task.id, task.label, task.status);
|
|
3508
|
+
el = document.createElement("div");
|
|
3509
|
+
var isDone2 = task.status === "completed";
|
|
3510
|
+
el.className = "agent-status " + (isDone2 ? "done" : "failed");
|
|
3511
|
+
var rt = isDone2 ? "completed" : task.status;
|
|
3512
|
+
if (task.resultSummary) rt += " — " + task.resultSummary.slice(0, 200);
|
|
3513
|
+
el.innerHTML = '<span class="agent-label">Agent ' + rt + ': <strong>' + task.label + '</strong></span>';
|
|
3514
|
+
messagesEl.appendChild(el);
|
|
3515
|
+
agentElements[task.id] = el;
|
|
3516
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
|
|
3521
|
+
if (recentCount === 0) {
|
|
3522
|
+
console.log("[agent] Poll: " + data.tasks.length + " total tasks but none after", agentPollSince);
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
// Stop polling when no tasks are running
|
|
3526
|
+
if (!hasRunning) {
|
|
3527
|
+
clearInterval(agentPollTimer);
|
|
3528
|
+
agentPollTimer = null;
|
|
3529
|
+
agentPollSince = null;
|
|
3530
|
+
agentElements = {};
|
|
3531
|
+
}
|
|
3532
|
+
} catch (e) { console.error("[agent] Poll error:", e); }
|
|
3533
|
+
}
|
|
3534
|
+
|
|
3535
|
+
// --- Board card rendering ---
|
|
3536
|
+
function escapeHtml(str) {
|
|
3537
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
3538
|
+
}
|
|
3539
|
+
|
|
3540
|
+
function renderBoardCards(issues) {
|
|
3541
|
+
var container = document.createElement("div");
|
|
3542
|
+
container.className = "board-cards";
|
|
3543
|
+
var priorityLabels = { 1: "Urgent", 2: "High", 3: "Medium", 4: "Low" };
|
|
3544
|
+
for (var i = 0; i < issues.length; i++) {
|
|
3545
|
+
var issue = issues[i];
|
|
3546
|
+
var card = document.createElement("div");
|
|
3547
|
+
card.className = "board-card";
|
|
3548
|
+
card.dataset.issueId = issue.id;
|
|
3549
|
+
|
|
3550
|
+
var html = '<span class="board-id">' + escapeHtml(issue.identifier) + '</span>'
|
|
3551
|
+
+ '<span class="board-title">' + escapeHtml(issue.title) + '</span>';
|
|
3552
|
+
|
|
3553
|
+
if (issue.priority >= 1 && issue.priority <= 4) {
|
|
3554
|
+
html += '<span class="board-priority p' + issue.priority + '">' + priorityLabels[issue.priority] + '</span>';
|
|
3555
|
+
}
|
|
3556
|
+
|
|
3557
|
+
html += '<span class="board-state state-' + escapeHtml(issue.stateType || "unstarted") + '">' + escapeHtml(issue.state) + '</span>';
|
|
3558
|
+
|
|
3559
|
+
if (issue.assignee) {
|
|
3560
|
+
html += '<span class="board-assignee">' + escapeHtml(issue.assignee) + '</span>';
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
var doneStates = ["Done", "Cancelled", "done", "cancelled"];
|
|
3564
|
+
if (doneStates.indexOf(issue.state) === -1) {
|
|
3565
|
+
html += '<span class="board-actions"><button data-issue-id="' + escapeHtml(issue.id) + '">Done</button></span>';
|
|
3566
|
+
}
|
|
3567
|
+
|
|
3568
|
+
card.innerHTML = html;
|
|
3569
|
+
container.appendChild(card);
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
// Wire up Done buttons
|
|
3573
|
+
var buttons = container.querySelectorAll(".board-actions button");
|
|
3574
|
+
for (var b = 0; b < buttons.length; b++) {
|
|
3575
|
+
(function(btn) {
|
|
3576
|
+
btn.addEventListener("click", async function() {
|
|
3577
|
+
var issueId = btn.dataset.issueId;
|
|
3578
|
+
btn.disabled = true;
|
|
3579
|
+
btn.textContent = "...";
|
|
3580
|
+
try {
|
|
3581
|
+
var resp = await api("/api/board/issues/" + issueId + "?sessionId=" + encodeURIComponent(sessionKey), {
|
|
3582
|
+
method: "PATCH",
|
|
3583
|
+
headers: { "Content-Type": "application/json" },
|
|
3584
|
+
body: JSON.stringify({ stateId: "done" }),
|
|
3585
|
+
});
|
|
3586
|
+
if (resp.ok) {
|
|
3587
|
+
var parentCard = btn.closest(".board-card");
|
|
3588
|
+
if (parentCard) parentCard.classList.add("card-done");
|
|
3589
|
+
var stateEl = parentCard ? parentCard.querySelector(".board-state") : null;
|
|
3590
|
+
if (stateEl) {
|
|
3591
|
+
stateEl.className = "board-state state-completed";
|
|
3592
|
+
stateEl.textContent = "Done";
|
|
3593
|
+
}
|
|
3594
|
+
btn.textContent = "Done";
|
|
3595
|
+
} else {
|
|
3596
|
+
btn.textContent = "Fail";
|
|
3597
|
+
btn.disabled = false;
|
|
3598
|
+
}
|
|
3599
|
+
} catch {
|
|
3600
|
+
btn.textContent = "Fail";
|
|
3601
|
+
btn.disabled = false;
|
|
3602
|
+
}
|
|
3603
|
+
});
|
|
3604
|
+
})(buttons[b]);
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
messagesEl.appendChild(container);
|
|
3608
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
sendBtn.addEventListener("click", () => {
|
|
3612
|
+
if (streaming && abortController) {
|
|
3613
|
+
abortController.abort();
|
|
3614
|
+
} else {
|
|
3615
|
+
sendMessage();
|
|
3616
|
+
}
|
|
3617
|
+
});
|
|
3618
|
+
|
|
3619
|
+
chatInput.addEventListener("keydown", (e) => {
|
|
3620
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
3621
|
+
e.preventDefault();
|
|
3622
|
+
if (streaming && abortController) {
|
|
3623
|
+
abortController.abort();
|
|
3624
|
+
} else {
|
|
3625
|
+
sendMessage();
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
});
|
|
3629
|
+
|
|
3630
|
+
// Auto-resize textarea
|
|
3631
|
+
function autoResize() {
|
|
3632
|
+
chatInput.style.height = "auto";
|
|
3633
|
+
chatInput.style.height = Math.min(chatInput.scrollHeight, 160) + "px";
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
chatInput.addEventListener("input", autoResize);
|
|
3637
|
+
|
|
3638
|
+
// --- Vault ---
|
|
3639
|
+
|
|
3640
|
+
const vaultOverlay = document.getElementById("vault-overlay");
|
|
3641
|
+
const vaultPanel = document.getElementById("vault-panel");
|
|
3642
|
+
const vaultBody = document.getElementById("vault-body");
|
|
3643
|
+
const vaultList = document.getElementById("vault-list");
|
|
3644
|
+
const vaultEmpty = document.getElementById("vault-empty");
|
|
3645
|
+
const vaultError = document.getElementById("vault-error");
|
|
3646
|
+
|
|
3647
|
+
function openSettings() {
|
|
3648
|
+
vaultOverlay.classList.add("open");
|
|
3649
|
+
loadPersonality();
|
|
3650
|
+
loadSecuritySettings();
|
|
3651
|
+
loadMeshSettings();
|
|
3652
|
+
loadLlmSettings();
|
|
3653
|
+
loadVaultKeys();
|
|
3654
|
+
}
|
|
3655
|
+
|
|
3656
|
+
function closeVault() {
|
|
3657
|
+
vaultOverlay.classList.remove("open");
|
|
3658
|
+
clearVaultError();
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
function showVaultError(msg) {
|
|
3662
|
+
vaultError.textContent = msg;
|
|
3663
|
+
vaultError.classList.add("visible");
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
function clearVaultError() {
|
|
3667
|
+
vaultError.textContent = "";
|
|
3668
|
+
vaultError.classList.remove("visible");
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// --- Personality (trait sliders + custom rules) ---
|
|
3672
|
+
|
|
3673
|
+
const personalityTraits = document.getElementById("personality-traits");
|
|
3674
|
+
const customRulesTextarea = document.getElementById("custom-rules-textarea");
|
|
3675
|
+
const promptStatus = document.getElementById("prompt-status");
|
|
3676
|
+
|
|
3677
|
+
// Dot click handler — activate clicked dot, deactivate siblings
|
|
3678
|
+
personalityTraits.addEventListener("click", (e) => {
|
|
3679
|
+
const dot = e.target.closest(".trait-dot");
|
|
3680
|
+
if (!dot) return;
|
|
3681
|
+
const slider = dot.closest(".trait-slider");
|
|
3682
|
+
slider.querySelectorAll(".trait-dot").forEach(d => d.classList.remove("active"));
|
|
3683
|
+
dot.classList.add("active");
|
|
3684
|
+
});
|
|
3685
|
+
|
|
3686
|
+
async function loadPersonality() {
|
|
3687
|
+
try {
|
|
3688
|
+
const data = await api("/api/personality?sessionId=" + encodeURIComponent(sessionId));
|
|
3689
|
+
// Set trait dots
|
|
3690
|
+
if (data.traits) {
|
|
3691
|
+
for (const [trait, value] of Object.entries(data.traits)) {
|
|
3692
|
+
const row = personalityTraits.querySelector(`[data-trait="${trait}"]`);
|
|
3693
|
+
if (!row) continue;
|
|
3694
|
+
row.querySelectorAll(".trait-dot").forEach(d => {
|
|
3695
|
+
d.classList.toggle("active", d.dataset.value === String(value));
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
// Set custom rules
|
|
3700
|
+
customRulesTextarea.value = (data.customRules || []).join("\n");
|
|
3701
|
+
} catch {
|
|
3702
|
+
// defaults already set in HTML
|
|
3703
|
+
customRulesTextarea.value = "";
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
|
|
3707
|
+
async function savePersonality() {
|
|
3708
|
+
const btn = document.getElementById("prompt-save-btn");
|
|
3709
|
+
btn.disabled = true;
|
|
3710
|
+
promptStatus.textContent = "";
|
|
3711
|
+
promptStatus.className = "prompt-status";
|
|
3712
|
+
|
|
3713
|
+
// Collect trait values
|
|
3714
|
+
const traits = {};
|
|
3715
|
+
personalityTraits.querySelectorAll(".trait-row").forEach(row => {
|
|
3716
|
+
const trait = row.dataset.trait;
|
|
3717
|
+
const active = row.querySelector(".trait-dot.active");
|
|
3718
|
+
traits[trait] = active ? parseInt(active.dataset.value) : 2;
|
|
3719
|
+
});
|
|
3720
|
+
|
|
3721
|
+
// Collect custom rules (non-empty lines)
|
|
3722
|
+
const customRules = customRulesTextarea.value
|
|
3723
|
+
.split("\n")
|
|
3724
|
+
.map(l => l.trim())
|
|
3725
|
+
.filter(Boolean);
|
|
3726
|
+
|
|
3727
|
+
try {
|
|
3728
|
+
await api("/api/personality", {
|
|
3729
|
+
method: "PUT",
|
|
3730
|
+
body: { sessionId, traits, customRules },
|
|
3731
|
+
});
|
|
3732
|
+
promptStatus.textContent = "Saved";
|
|
3733
|
+
promptStatus.classList.add("visible");
|
|
3734
|
+
setTimeout(() => promptStatus.classList.remove("visible"), 2000);
|
|
3735
|
+
} catch (err) {
|
|
3736
|
+
promptStatus.textContent = err.message;
|
|
3737
|
+
promptStatus.className = "prompt-status visible error";
|
|
3738
|
+
} finally {
|
|
3739
|
+
btn.disabled = false;
|
|
3740
|
+
}
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
document.getElementById("prompt-save-btn").addEventListener("click", savePersonality);
|
|
3744
|
+
|
|
3745
|
+
// --- LLM Settings ---
|
|
3746
|
+
|
|
3747
|
+
const airplaneToggle = document.getElementById("airplane-toggle");
|
|
3748
|
+
const modelChatInput = document.getElementById("model-chat");
|
|
3749
|
+
const modelUtilityInput = document.getElementById("model-utility");
|
|
3750
|
+
const llmSaveStatus = document.getElementById("llm-save-status");
|
|
3751
|
+
|
|
3752
|
+
async function loadLlmSettings() {
|
|
3753
|
+
try {
|
|
3754
|
+
const data = await api("/api/settings");
|
|
3755
|
+
airplaneToggle.checked = data.airplaneMode;
|
|
3756
|
+
modelChatInput.value = data.models.chat === "auto" ? "" : data.models.chat;
|
|
3757
|
+
modelUtilityInput.value = data.models.utility === "auto" ? "" : data.models.utility;
|
|
3758
|
+
} catch {
|
|
3759
|
+
// defaults are fine
|
|
3760
|
+
}
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
async function saveLlmSettings() {
|
|
3764
|
+
const btn = document.getElementById("llm-save-btn");
|
|
3765
|
+
btn.disabled = true;
|
|
3766
|
+
llmSaveStatus.classList.remove("visible");
|
|
3767
|
+
|
|
3768
|
+
try {
|
|
3769
|
+
await api("/api/settings", {
|
|
3770
|
+
method: "PUT",
|
|
3771
|
+
body: {
|
|
3772
|
+
airplaneMode: airplaneToggle.checked,
|
|
3773
|
+
models: {
|
|
3774
|
+
chat: modelChatInput.value.trim() || "auto",
|
|
3775
|
+
utility: modelUtilityInput.value.trim() || "auto",
|
|
3776
|
+
},
|
|
3777
|
+
},
|
|
3778
|
+
});
|
|
3779
|
+
llmSaveStatus.classList.add("visible");
|
|
3780
|
+
setTimeout(() => llmSaveStatus.classList.remove("visible"), 2000);
|
|
3781
|
+
} catch (err) {
|
|
3782
|
+
llmSaveStatus.textContent = err.message;
|
|
3783
|
+
llmSaveStatus.classList.add("visible");
|
|
3784
|
+
setTimeout(() => {
|
|
3785
|
+
llmSaveStatus.textContent = "Saved";
|
|
3786
|
+
llmSaveStatus.classList.remove("visible");
|
|
3787
|
+
}, 3000);
|
|
3788
|
+
} finally {
|
|
3789
|
+
btn.disabled = false;
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
document.getElementById("llm-save-btn").addEventListener("click", saveLlmSettings);
|
|
3794
|
+
|
|
3795
|
+
// --- Security settings ---
|
|
3796
|
+
const safewordModeSelect = document.getElementById("safeword-mode-select");
|
|
3797
|
+
const safewordModeLabel = document.getElementById("safeword-mode-label");
|
|
3798
|
+
const securitySaveStatus = document.getElementById("security-save-status");
|
|
3799
|
+
|
|
3800
|
+
function updateSafewordLabel() {
|
|
3801
|
+
safewordModeLabel.textContent = safewordModeSelect.value === "always"
|
|
3802
|
+
? "Require safe word on every page load"
|
|
3803
|
+
: "Skip safe word until server or browser restarts";
|
|
3804
|
+
}
|
|
3805
|
+
safewordModeSelect.addEventListener("change", updateSafewordLabel);
|
|
3806
|
+
|
|
3807
|
+
async function loadSecuritySettings() {
|
|
3808
|
+
try {
|
|
3809
|
+
const data = await api("/api/settings");
|
|
3810
|
+
safewordModeSelect.value = data.safeWordMode || "always";
|
|
3811
|
+
updateSafewordLabel();
|
|
3812
|
+
} catch {}
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
async function saveSecuritySettings() {
|
|
3816
|
+
const btn = document.getElementById("security-save-btn");
|
|
3817
|
+
btn.disabled = true;
|
|
3818
|
+
securitySaveStatus.classList.remove("visible");
|
|
3819
|
+
try {
|
|
3820
|
+
await api("/api/settings", {
|
|
3821
|
+
method: "PUT",
|
|
3822
|
+
body: { safeWordMode: safewordModeSelect.value },
|
|
3823
|
+
});
|
|
3824
|
+
// If switching to "always", clear stored session so next load requires safe word
|
|
3825
|
+
if (safewordModeSelect.value === "always") {
|
|
3826
|
+
sessionStorage.removeItem("dash_sid");
|
|
3827
|
+
}
|
|
3828
|
+
securitySaveStatus.classList.add("visible");
|
|
3829
|
+
setTimeout(() => securitySaveStatus.classList.remove("visible"), 2000);
|
|
3830
|
+
} catch (err) {
|
|
3831
|
+
securitySaveStatus.textContent = err.message;
|
|
3832
|
+
securitySaveStatus.classList.add("visible");
|
|
3833
|
+
setTimeout(() => {
|
|
3834
|
+
securitySaveStatus.textContent = "Saved";
|
|
3835
|
+
securitySaveStatus.classList.remove("visible");
|
|
3836
|
+
}, 3000);
|
|
3837
|
+
} finally {
|
|
3838
|
+
btn.disabled = false;
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
document.getElementById("security-save-btn").addEventListener("click", saveSecuritySettings);
|
|
3843
|
+
|
|
3844
|
+
// --- Mesh / Network settings ---
|
|
3845
|
+
const meshLanToggle = document.getElementById("mesh-lan-toggle");
|
|
3846
|
+
const meshIncomingToggle = document.getElementById("mesh-incoming-toggle");
|
|
3847
|
+
const meshSaveStatus = document.getElementById("mesh-save-status");
|
|
3848
|
+
|
|
3849
|
+
async function loadMeshSettings() {
|
|
3850
|
+
try {
|
|
3851
|
+
const data = await api("/api/settings");
|
|
3852
|
+
meshLanToggle.checked = data.mesh?.lanAnnounce ?? false;
|
|
3853
|
+
meshIncomingToggle.checked = data.mesh?.allowIncoming ?? false;
|
|
3854
|
+
} catch {}
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3857
|
+
async function saveMeshSettings() {
|
|
3858
|
+
const btn = document.getElementById("mesh-save-btn");
|
|
3859
|
+
btn.disabled = true;
|
|
3860
|
+
meshSaveStatus.classList.remove("visible");
|
|
3861
|
+
try {
|
|
3862
|
+
await api("/api/settings", {
|
|
3863
|
+
method: "PUT",
|
|
3864
|
+
body: { mesh: { lanAnnounce: meshLanToggle.checked, allowIncoming: meshIncomingToggle.checked } },
|
|
3865
|
+
});
|
|
3866
|
+
meshSaveStatus.classList.add("visible");
|
|
3867
|
+
setTimeout(() => meshSaveStatus.classList.remove("visible"), 3000);
|
|
3868
|
+
} catch (err) {
|
|
3869
|
+
meshSaveStatus.textContent = "Error";
|
|
3870
|
+
meshSaveStatus.classList.add("visible");
|
|
3871
|
+
setTimeout(() => { meshSaveStatus.textContent = "Saved"; meshSaveStatus.classList.remove("visible"); }, 3000);
|
|
3872
|
+
} finally {
|
|
3873
|
+
btn.disabled = false;
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
document.getElementById("mesh-save-btn").addEventListener("click", saveMeshSettings);
|
|
3878
|
+
|
|
3879
|
+
async function loadVaultKeys() {
|
|
3880
|
+
try {
|
|
3881
|
+
const data = await api("/api/vault?sessionId=" + encodeURIComponent(sessionId));
|
|
3882
|
+
renderVaultKeys(data.keys || []);
|
|
3883
|
+
} catch (err) {
|
|
3884
|
+
vaultList.innerHTML = "";
|
|
3885
|
+
vaultEmpty.style.display = "block";
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
function renderVaultKeys(keys) {
|
|
3890
|
+
vaultList.innerHTML = "";
|
|
3891
|
+
if (keys.length === 0) {
|
|
3892
|
+
vaultEmpty.style.display = "block";
|
|
3893
|
+
return;
|
|
3894
|
+
}
|
|
3895
|
+
vaultEmpty.style.display = "none";
|
|
3896
|
+
for (const k of keys) {
|
|
3897
|
+
const item = document.createElement("div");
|
|
3898
|
+
item.className = "vault-key-item";
|
|
3899
|
+
item.innerHTML = '<div class="vault-key-info">'
|
|
3900
|
+
+ '<div class="vault-key-name"></div>'
|
|
3901
|
+
+ (k.label ? '<div class="vault-key-label"></div>' : '')
|
|
3902
|
+
+ '</div>'
|
|
3903
|
+
+ '<button class="vault-key-delete" title="Delete">×</button>';
|
|
3904
|
+
item.querySelector(".vault-key-name").textContent = k.name;
|
|
3905
|
+
if (k.label) item.querySelector(".vault-key-label").textContent = k.label;
|
|
3906
|
+
item.querySelector(".vault-key-delete").addEventListener("click", () => deleteVaultKeyUI(k.name));
|
|
3907
|
+
vaultList.appendChild(item);
|
|
3908
|
+
}
|
|
3909
|
+
}
|
|
3910
|
+
|
|
3911
|
+
async function addVaultKey() {
|
|
3912
|
+
clearVaultError();
|
|
3913
|
+
const name = document.getElementById("vault-key-name").value.trim();
|
|
3914
|
+
const value = document.getElementById("vault-key-value").value;
|
|
3915
|
+
const label = document.getElementById("vault-key-label").value.trim();
|
|
3916
|
+
|
|
3917
|
+
if (!name) { showVaultError("Key name is required"); return; }
|
|
3918
|
+
if (!value) { showVaultError("Key value is required"); return; }
|
|
3919
|
+
|
|
3920
|
+
const btn = document.getElementById("vault-add-btn");
|
|
3921
|
+
btn.disabled = true;
|
|
3922
|
+
|
|
3923
|
+
try {
|
|
3924
|
+
await api("/api/vault/" + encodeURIComponent(name) + "?sessionId=" + encodeURIComponent(sessionId), {
|
|
3925
|
+
method: "PUT",
|
|
3926
|
+
body: { value, label: label || undefined },
|
|
3927
|
+
});
|
|
3928
|
+
document.getElementById("vault-key-name").value = "";
|
|
3929
|
+
document.getElementById("vault-key-value").value = "";
|
|
3930
|
+
document.getElementById("vault-key-label").value = "";
|
|
3931
|
+
await loadVaultKeys();
|
|
3932
|
+
} catch (err) {
|
|
3933
|
+
showVaultError(err.message);
|
|
3934
|
+
} finally {
|
|
3935
|
+
btn.disabled = false;
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3939
|
+
async function deleteVaultKeyUI(name) {
|
|
3940
|
+
if (!confirm("Delete " + name + " from vault?")) return;
|
|
3941
|
+
try {
|
|
3942
|
+
await api("/api/vault/" + encodeURIComponent(name) + "?sessionId=" + encodeURIComponent(sessionId), {
|
|
3943
|
+
method: "DELETE",
|
|
3944
|
+
});
|
|
3945
|
+
await loadVaultKeys();
|
|
3946
|
+
} catch (err) {
|
|
3947
|
+
showVaultError(err.message);
|
|
3948
|
+
}
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
document.getElementById("vault-open-btn").addEventListener("click", openSettings);
|
|
3952
|
+
document.getElementById("vault-close-btn").addEventListener("click", closeVault);
|
|
3953
|
+
|
|
3954
|
+
// Close on overlay click (outside panel)
|
|
3955
|
+
vaultOverlay.addEventListener("click", (e) => {
|
|
3956
|
+
if (e.target === vaultOverlay) closeVault();
|
|
3957
|
+
});
|
|
3958
|
+
|
|
3959
|
+
// Close on Escape
|
|
3960
|
+
document.addEventListener("keydown", (e) => {
|
|
3961
|
+
if (e.key === "Escape" && vaultOverlay.classList.contains("open")) closeVault();
|
|
3962
|
+
});
|
|
3963
|
+
|
|
3964
|
+
document.getElementById("vault-add-btn").addEventListener("click", addVaultKey);
|
|
3965
|
+
|
|
3966
|
+
// Enter to add
|
|
3967
|
+
document.getElementById("vault-key-label").addEventListener("keydown", (e) => {
|
|
3968
|
+
if (e.key === "Enter") addVaultKey();
|
|
3969
|
+
});
|
|
3970
|
+
document.getElementById("vault-key-value").addEventListener("keydown", (e) => {
|
|
3971
|
+
if (e.key === "Enter") addVaultKey();
|
|
3972
|
+
});
|
|
3973
|
+
|
|
3974
|
+
// --- Activity log ---
|
|
3975
|
+
|
|
3976
|
+
let activeTab = "chat";
|
|
3977
|
+
let lastActivityId = 0;
|
|
3978
|
+
let unseenCount = 0;
|
|
3979
|
+
const activityBuffer = [];
|
|
3980
|
+
let activityPollTimer = null;
|
|
3981
|
+
let streamFrozen = false;
|
|
3982
|
+
const selectedEntryIds = new Set();
|
|
3983
|
+
let branchAbortController = null;
|
|
3984
|
+
let activeBranchEntryIds = [];
|
|
3985
|
+
|
|
3986
|
+
const activityView = document.getElementById("activity-view");
|
|
3987
|
+
const activityBadge = document.getElementById("activity-badge");
|
|
3988
|
+
const activityEmpty = document.getElementById("activity-empty");
|
|
3989
|
+
const activityEntries = document.getElementById("activity-entries");
|
|
3990
|
+
const freezeBtn = document.getElementById("stream-freeze-btn");
|
|
3991
|
+
const selectionCount = document.getElementById("stream-selection-count");
|
|
3992
|
+
const branchBtn = document.getElementById("stream-branch-btn");
|
|
3993
|
+
const branchPanel = document.getElementById("branch-panel");
|
|
3994
|
+
const branchHeaderText = document.getElementById("branch-header-text");
|
|
3995
|
+
const branchMessages = document.getElementById("branch-messages");
|
|
3996
|
+
const branchInput = document.getElementById("branch-input");
|
|
3997
|
+
const branchSendBtn = document.getElementById("branch-send-btn");
|
|
3998
|
+
const branchCloseBtn = document.getElementById("branch-close-btn");
|
|
3999
|
+
|
|
4000
|
+
window.__switchTab = function(tab) {
|
|
4001
|
+
if (tab === activeTab) return;
|
|
4002
|
+
activeTab = tab;
|
|
4003
|
+
|
|
4004
|
+
document.getElementById("tab-chat").classList.toggle("active", tab === "chat");
|
|
4005
|
+
document.getElementById("tab-activity").classList.toggle("active", tab === "activity");
|
|
4006
|
+
|
|
4007
|
+
if (tab === "chat") {
|
|
4008
|
+
messagesEl.style.display = "";
|
|
4009
|
+
composerEl.style.display = "";
|
|
4010
|
+
activityView.classList.remove("active");
|
|
4011
|
+
closeBranch();
|
|
4012
|
+
chatInput.focus();
|
|
4013
|
+
} else {
|
|
4014
|
+
messagesEl.style.display = "none";
|
|
4015
|
+
composerEl.style.display = "none";
|
|
4016
|
+
activityView.classList.add("active");
|
|
4017
|
+
// Flush buffer
|
|
4018
|
+
if (activityBuffer.length > 0) {
|
|
4019
|
+
for (const entry of activityBuffer) appendActivityEntry(entry);
|
|
4020
|
+
activityBuffer.length = 0;
|
|
4021
|
+
}
|
|
4022
|
+
unseenCount = 0;
|
|
4023
|
+
updateBadge();
|
|
4024
|
+
}
|
|
4025
|
+
};
|
|
4026
|
+
|
|
4027
|
+
function updateBadge() {
|
|
4028
|
+
if (unseenCount > 0) {
|
|
4029
|
+
activityBadge.textContent = unseenCount > 99 ? "99+" : String(unseenCount);
|
|
4030
|
+
activityBadge.classList.add("visible");
|
|
4031
|
+
} else {
|
|
4032
|
+
activityBadge.classList.remove("visible");
|
|
4033
|
+
}
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
function appendActivityEntry(entry) {
|
|
4037
|
+
activityEmpty.style.display = "none";
|
|
4038
|
+
const row = document.createElement("div");
|
|
4039
|
+
row.className = "activity-entry";
|
|
4040
|
+
if (entry.backref) row.classList.add("has-backref");
|
|
4041
|
+
row.setAttribute("data-id", String(entry.id));
|
|
4042
|
+
row.setAttribute("data-trace", entry.traceId || "");
|
|
4043
|
+
if (entry.backref) row.setAttribute("data-backref", entry.backref);
|
|
4044
|
+
const time = new Date(entry.timestamp);
|
|
4045
|
+
const hh = String(time.getHours()).padStart(2, "0");
|
|
4046
|
+
const mm = String(time.getMinutes()).padStart(2, "0");
|
|
4047
|
+
let html = '<span class="activity-time">' + hh + ':' + mm + '</span>'
|
|
4048
|
+
+ '<span class="activity-source ' + entry.source + '">' + entry.source + '</span>'
|
|
4049
|
+
+ '<span class="activity-body"><span class="activity-summary"></span>'
|
|
4050
|
+
+ (entry.backref ? '<span class="activity-lineage" title="Branched from ' + entry.backref + '">↳</span>' : '')
|
|
4051
|
+
+ '</span>';
|
|
4052
|
+
row.innerHTML = html;
|
|
4053
|
+
row.querySelector(".activity-summary").textContent = entry.summary;
|
|
4054
|
+
if (entry.detail) {
|
|
4055
|
+
const detailEl = document.createElement("div");
|
|
4056
|
+
detailEl.className = "activity-detail";
|
|
4057
|
+
detailEl.textContent = entry.detail;
|
|
4058
|
+
row.querySelector(".activity-body").appendChild(detailEl);
|
|
4059
|
+
}
|
|
4060
|
+
row.addEventListener("click", () => {
|
|
4061
|
+
const id = parseInt(row.getAttribute("data-id"), 10);
|
|
4062
|
+
if (row.classList.contains("selected")) {
|
|
4063
|
+
row.classList.remove("selected");
|
|
4064
|
+
selectedEntryIds.delete(id);
|
|
4065
|
+
} else {
|
|
4066
|
+
row.classList.add("selected");
|
|
4067
|
+
selectedEntryIds.add(id);
|
|
4068
|
+
}
|
|
4069
|
+
updateSelectionUI();
|
|
4070
|
+
});
|
|
4071
|
+
activityEntries.appendChild(row);
|
|
4072
|
+
if (!streamFrozen) {
|
|
4073
|
+
activityEntries.scrollTop = activityEntries.scrollHeight;
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
|
|
4077
|
+
async function pollActivity() {
|
|
4078
|
+
if (!sessionId) return;
|
|
4079
|
+
try {
|
|
4080
|
+
const data = await api("/api/activity?sessionId=" + encodeURIComponent(sessionId) + "&since=" + lastActivityId);
|
|
4081
|
+
const items = data.activities || [];
|
|
4082
|
+
if (items.length === 0) return;
|
|
4083
|
+
lastActivityId = items[items.length - 1].id;
|
|
4084
|
+
if (activeTab === "activity") {
|
|
4085
|
+
for (const entry of items) appendActivityEntry(entry);
|
|
4086
|
+
} else {
|
|
4087
|
+
activityBuffer.push(...items);
|
|
4088
|
+
unseenCount += items.length;
|
|
4089
|
+
updateBadge();
|
|
4090
|
+
}
|
|
4091
|
+
} catch (err) {
|
|
4092
|
+
// Silently ignore poll errors
|
|
4093
|
+
}
|
|
4094
|
+
}
|
|
4095
|
+
|
|
4096
|
+
function startActivityPolling() {
|
|
4097
|
+
if (activityPollTimer) return;
|
|
4098
|
+
activityPollTimer = setInterval(pollActivity, 5000);
|
|
4099
|
+
pollActivity(); // immediate first poll
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
// --- Stream controls ---
|
|
4103
|
+
|
|
4104
|
+
freezeBtn.addEventListener("click", () => {
|
|
4105
|
+
streamFrozen = !streamFrozen;
|
|
4106
|
+
freezeBtn.classList.toggle("frozen", streamFrozen);
|
|
4107
|
+
freezeBtn.innerHTML = streamFrozen ? "◼ Frozen" : "▶ Live";
|
|
4108
|
+
if (!streamFrozen) {
|
|
4109
|
+
activityEntries.scrollTop = activityEntries.scrollHeight;
|
|
4110
|
+
}
|
|
4111
|
+
});
|
|
4112
|
+
|
|
4113
|
+
function updateSelectionUI() {
|
|
4114
|
+
const count = selectedEntryIds.size;
|
|
4115
|
+
if (count > 0) {
|
|
4116
|
+
selectionCount.textContent = count + " selected";
|
|
4117
|
+
selectionCount.classList.add("visible");
|
|
4118
|
+
branchBtn.classList.add("visible");
|
|
4119
|
+
} else {
|
|
4120
|
+
selectionCount.classList.remove("visible");
|
|
4121
|
+
branchBtn.classList.remove("visible");
|
|
4122
|
+
}
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
branchBtn.addEventListener("click", openBranch);
|
|
4126
|
+
branchCloseBtn.addEventListener("click", closeBranch);
|
|
4127
|
+
branchSendBtn.addEventListener("click", sendBranch);
|
|
4128
|
+
branchInput.addEventListener("keydown", (e) => {
|
|
4129
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
4130
|
+
e.preventDefault();
|
|
4131
|
+
sendBranch();
|
|
4132
|
+
}
|
|
4133
|
+
});
|
|
4134
|
+
|
|
4135
|
+
function openBranch() {
|
|
4136
|
+
activeBranchEntryIds = [...selectedEntryIds];
|
|
4137
|
+
// Build header from selected DOM rows
|
|
4138
|
+
const summaries = [];
|
|
4139
|
+
activityEntries.querySelectorAll(".activity-entry.selected").forEach((row) => {
|
|
4140
|
+
const s = row.querySelector(".activity-summary");
|
|
4141
|
+
if (s) summaries.push(s.textContent);
|
|
4142
|
+
});
|
|
4143
|
+
branchHeaderText.textContent = summaries.length <= 3
|
|
4144
|
+
? summaries.join(" / ")
|
|
4145
|
+
: summaries.slice(0, 2).join(" / ") + " + " + (summaries.length - 2) + " more";
|
|
4146
|
+
branchMessages.innerHTML = "";
|
|
4147
|
+
branchPanel.classList.add("open");
|
|
4148
|
+
branchInput.focus();
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
function closeBranch() {
|
|
4152
|
+
branchPanel.classList.remove("open");
|
|
4153
|
+
if (branchAbortController) {
|
|
4154
|
+
branchAbortController.abort();
|
|
4155
|
+
branchAbortController = null;
|
|
4156
|
+
}
|
|
4157
|
+
// Clear selection
|
|
4158
|
+
selectedEntryIds.clear();
|
|
4159
|
+
activeBranchEntryIds = [];
|
|
4160
|
+
activityEntries.querySelectorAll(".activity-entry.selected").forEach((row) => {
|
|
4161
|
+
row.classList.remove("selected");
|
|
4162
|
+
});
|
|
4163
|
+
updateSelectionUI();
|
|
4164
|
+
}
|
|
4165
|
+
|
|
4166
|
+
async function sendBranch() {
|
|
4167
|
+
const question = branchInput.value.trim();
|
|
4168
|
+
if (!question || !activeBranchEntryIds.length) return;
|
|
4169
|
+
branchInput.value = "";
|
|
4170
|
+
|
|
4171
|
+
// Append user message
|
|
4172
|
+
const userDiv = document.createElement("div");
|
|
4173
|
+
userDiv.className = "branch-message user";
|
|
4174
|
+
userDiv.innerHTML = '<div class="branch-role">' + (userName || "You") + '</div><div class="branch-content"></div>';
|
|
4175
|
+
userDiv.querySelector(".branch-content").textContent = question;
|
|
4176
|
+
branchMessages.appendChild(userDiv);
|
|
4177
|
+
branchMessages.scrollTop = branchMessages.scrollHeight;
|
|
4178
|
+
|
|
4179
|
+
// Append assistant placeholder
|
|
4180
|
+
const asstDiv = document.createElement("div");
|
|
4181
|
+
asstDiv.className = "branch-message assistant";
|
|
4182
|
+
asstDiv.innerHTML = '<div class="branch-role">' + agentName + '</div><div class="branch-content"></div>';
|
|
4183
|
+
branchMessages.appendChild(asstDiv);
|
|
4184
|
+
const contentEl = asstDiv.querySelector(".branch-content");
|
|
4185
|
+
|
|
4186
|
+
branchAbortController = new AbortController();
|
|
4187
|
+
|
|
4188
|
+
try {
|
|
4189
|
+
const res = await fetch("/api/branch", {
|
|
4190
|
+
method: "POST",
|
|
4191
|
+
headers: { "Content-Type": "application/json" },
|
|
4192
|
+
body: JSON.stringify({
|
|
4193
|
+
sessionId,
|
|
4194
|
+
entryIds: activeBranchEntryIds,
|
|
4195
|
+
question,
|
|
4196
|
+
}),
|
|
4197
|
+
signal: branchAbortController.signal,
|
|
4198
|
+
});
|
|
4199
|
+
|
|
4200
|
+
if (!res.ok) {
|
|
4201
|
+
const err = await res.json().catch(() => ({}));
|
|
4202
|
+
contentEl.textContent = "Error: " + (err.error || res.statusText);
|
|
4203
|
+
return;
|
|
4204
|
+
}
|
|
4205
|
+
|
|
4206
|
+
const reader = res.body.getReader();
|
|
4207
|
+
const decoder = new TextDecoder();
|
|
4208
|
+
let buffer = "";
|
|
4209
|
+
let fullText = "";
|
|
4210
|
+
|
|
4211
|
+
while (true) {
|
|
4212
|
+
const { done, value } = await reader.read();
|
|
4213
|
+
if (done) break;
|
|
4214
|
+
|
|
4215
|
+
buffer += decoder.decode(value, { stream: true });
|
|
4216
|
+
const lines = buffer.split("\n");
|
|
4217
|
+
buffer = lines.pop() || "";
|
|
4218
|
+
|
|
4219
|
+
for (const line of lines) {
|
|
4220
|
+
const trimmed = line.trim();
|
|
4221
|
+
if (!trimmed.startsWith("data: ")) continue;
|
|
4222
|
+
try {
|
|
4223
|
+
const data = JSON.parse(trimmed.slice(6));
|
|
4224
|
+
if (data.token) {
|
|
4225
|
+
fullText += data.token;
|
|
4226
|
+
contentEl.textContent = fullText;
|
|
4227
|
+
branchMessages.scrollTop = branchMessages.scrollHeight;
|
|
4228
|
+
}
|
|
4229
|
+
if (data.error) {
|
|
4230
|
+
contentEl.textContent += "\n" + friendlyLLMError(data.error);
|
|
4231
|
+
}
|
|
4232
|
+
} catch {}
|
|
4233
|
+
}
|
|
4234
|
+
}
|
|
4235
|
+
|
|
4236
|
+
// Render markdown on completion
|
|
4237
|
+
if (fullText) {
|
|
4238
|
+
contentEl.innerHTML = renderMarkdown(fullText);
|
|
4239
|
+
branchMessages.scrollTop = branchMessages.scrollHeight;
|
|
4240
|
+
}
|
|
4241
|
+
} catch (err) {
|
|
4242
|
+
if (err.name !== "AbortError") {
|
|
4243
|
+
contentEl.textContent = "Connection error: " + err.message;
|
|
4244
|
+
}
|
|
4245
|
+
} finally {
|
|
4246
|
+
branchAbortController = null;
|
|
4247
|
+
}
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
// --- Voice ---
|
|
4251
|
+
|
|
4252
|
+
const micBtn = document.getElementById("mic-btn");
|
|
4253
|
+
|
|
4254
|
+
async function initVoice() {
|
|
4255
|
+
try {
|
|
4256
|
+
const data = await api("/api/voice-status");
|
|
4257
|
+
voiceStatus = { tts: !!data.tts, stt: !!data.stt };
|
|
4258
|
+
} catch {
|
|
4259
|
+
voiceStatus = { tts: false, stt: false };
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
// Load settings for autoPlay and toggle states
|
|
4263
|
+
try {
|
|
4264
|
+
const settings = await api("/api/settings");
|
|
4265
|
+
ttsAutoPlay = settings.tts?.autoPlay !== false;
|
|
4266
|
+
document.getElementById("tts-toggle").checked = settings.tts?.enabled !== false;
|
|
4267
|
+
document.getElementById("stt-toggle").checked = settings.stt?.enabled !== false;
|
|
4268
|
+
document.getElementById("autoplay-toggle").checked = ttsAutoPlay;
|
|
4269
|
+
document.getElementById("tts-status-label").textContent = voiceStatus.tts ? "Piper running" : "Not available";
|
|
4270
|
+
document.getElementById("stt-status-label").textContent = voiceStatus.stt ? "Whisper running" : "Not available";
|
|
4271
|
+
} catch {}
|
|
4272
|
+
|
|
4273
|
+
// Check board provider status
|
|
4274
|
+
try {
|
|
4275
|
+
const boardStatus = await api("/api/board/status");
|
|
4276
|
+
const providerLabel = document.getElementById("board-provider-label");
|
|
4277
|
+
const statusLabel = document.getElementById("board-status-label");
|
|
4278
|
+
const statusDot = document.getElementById("board-status-dot");
|
|
4279
|
+
const hint = document.getElementById("board-hint");
|
|
4280
|
+
if (boardStatus.available) {
|
|
4281
|
+
providerLabel.textContent = boardStatus.provider || "Task Board";
|
|
4282
|
+
statusLabel.textContent = boardStatus.user ? `Connected as ${boardStatus.user.name}` : "Connected";
|
|
4283
|
+
statusDot.style.background = "#22c55e";
|
|
4284
|
+
hint.textContent = 'Use "issues" or "todo <title>" in chat to manage your board.';
|
|
4285
|
+
} else {
|
|
4286
|
+
providerLabel.textContent = "Not connected";
|
|
4287
|
+
statusLabel.textContent = "No provider configured";
|
|
4288
|
+
statusDot.style.background = "var(--text-dim)";
|
|
4289
|
+
}
|
|
4290
|
+
} catch {
|
|
4291
|
+
document.getElementById("board-provider-label").textContent = "Not connected";
|
|
4292
|
+
document.getElementById("board-status-label").textContent = "Could not check status";
|
|
4293
|
+
}
|
|
4294
|
+
|
|
4295
|
+
// Check Google Workspace status
|
|
4296
|
+
try {
|
|
4297
|
+
const googleStatus = await api("/api/google/status");
|
|
4298
|
+
const gLabel = document.getElementById("google-status-label");
|
|
4299
|
+
const gScopes = document.getElementById("google-scopes-label");
|
|
4300
|
+
const gDot = document.getElementById("google-status-dot");
|
|
4301
|
+
const gHint = document.getElementById("google-hint");
|
|
4302
|
+
const gBtn = document.getElementById("google-connect-btn");
|
|
4303
|
+
if (googleStatus.authenticated) {
|
|
4304
|
+
gLabel.textContent = "Connected";
|
|
4305
|
+
gScopes.textContent = "Calendar, Gmail, Docs, Sheets, Tasks";
|
|
4306
|
+
gDot.style.background = "#22c55e";
|
|
4307
|
+
gHint.textContent = 'Ask about your "schedule", "inbox", or "email from [name]" in chat.';
|
|
4308
|
+
gBtn.textContent = "Reconnect Google";
|
|
4309
|
+
} else if (googleStatus.configured) {
|
|
4310
|
+
gLabel.textContent = "Configured";
|
|
4311
|
+
gScopes.textContent = "Not yet authorized";
|
|
4312
|
+
gDot.style.background = "#eab308";
|
|
4313
|
+
gHint.textContent = "Client ID and secret are set. Click Connect to authorize with Google.";
|
|
4314
|
+
} else {
|
|
4315
|
+
gLabel.textContent = "Not configured";
|
|
4316
|
+
gScopes.textContent = "";
|
|
4317
|
+
gDot.style.background = "var(--text-dim)";
|
|
4318
|
+
gBtn.style.display = "none";
|
|
4319
|
+
}
|
|
4320
|
+
} catch {
|
|
4321
|
+
document.getElementById("google-status-label").textContent = "Not connected";
|
|
4322
|
+
}
|
|
4323
|
+
|
|
4324
|
+
// Google connect button
|
|
4325
|
+
document.getElementById("google-connect-btn").addEventListener("click", function() {
|
|
4326
|
+
window.open("/api/google/auth", "_blank");
|
|
4327
|
+
});
|
|
4328
|
+
|
|
4329
|
+
// Listen for Google OAuth callback to refresh status
|
|
4330
|
+
window.addEventListener("message", async function(e) {
|
|
4331
|
+
if (e.data === "google-connected") {
|
|
4332
|
+
try {
|
|
4333
|
+
const gs = await api("/api/google/status");
|
|
4334
|
+
const gLabel = document.getElementById("google-status-label");
|
|
4335
|
+
const gScopes = document.getElementById("google-scopes-label");
|
|
4336
|
+
const gDot = document.getElementById("google-status-dot");
|
|
4337
|
+
const gHint = document.getElementById("google-hint");
|
|
4338
|
+
const gBtn = document.getElementById("google-connect-btn");
|
|
4339
|
+
if (gs.authenticated) {
|
|
4340
|
+
gLabel.textContent = "Connected";
|
|
4341
|
+
gScopes.textContent = "Calendar, Gmail, Docs, Sheets, Tasks";
|
|
4342
|
+
gDot.style.background = "#22c55e";
|
|
4343
|
+
gHint.textContent = 'Ask about your "schedule", "inbox", or "email from [name]" in chat.';
|
|
4344
|
+
gBtn.textContent = "Reconnect Google";
|
|
4345
|
+
}
|
|
4346
|
+
} catch {}
|
|
4347
|
+
}
|
|
4348
|
+
});
|
|
4349
|
+
|
|
4350
|
+
}
|
|
4351
|
+
|
|
4352
|
+
// TTS: speak a text string — fetches WAV from server, plays in browser
|
|
4353
|
+
async function speakText(text) {
|
|
4354
|
+
if (!voiceStatus.tts || !text) return;
|
|
4355
|
+
const ttsText = text.slice(0, 2000);
|
|
4356
|
+
try {
|
|
4357
|
+
const res = await fetch("/api/tts?text=" + encodeURIComponent(ttsText));
|
|
4358
|
+
if (!res.ok) return;
|
|
4359
|
+
const arrayBuf = await res.arrayBuffer();
|
|
4360
|
+
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
|
4361
|
+
const audioBuffer = await audioCtx.decodeAudioData(arrayBuf);
|
|
4362
|
+
const source = audioCtx.createBufferSource();
|
|
4363
|
+
source.buffer = audioBuffer;
|
|
4364
|
+
source.connect(audioCtx.destination);
|
|
4365
|
+
source.onended = () => { currentAudio = null; };
|
|
4366
|
+
source.start(0);
|
|
4367
|
+
currentAudio = { source, ctx: audioCtx };
|
|
4368
|
+
} catch (err) {
|
|
4369
|
+
console.log("TTS playback error:", err);
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
|
|
4373
|
+
// Stop any currently playing TTS audio
|
|
4374
|
+
function stopSpeaking() {
|
|
4375
|
+
if (currentAudio) {
|
|
4376
|
+
try {
|
|
4377
|
+
if (currentAudio.source) currentAudio.source.stop();
|
|
4378
|
+
if (currentAudio.ctx) currentAudio.ctx.close();
|
|
4379
|
+
} catch {}
|
|
4380
|
+
currentAudio = null;
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4384
|
+
// --- Voice: always-listen mode + barge-in ---
|
|
4385
|
+
let alwaysListening = false;
|
|
4386
|
+
let listenStream = null;
|
|
4387
|
+
let listenInterval = null;
|
|
4388
|
+
let bargeInAnalyser = null;
|
|
4389
|
+
|
|
4390
|
+
// Whisper hallucination patterns — filter these out
|
|
4391
|
+
const WHISPER_NOISE = /^\[.*\]$|^[\s.!?,;:…—–-]*$|^\(.*\)$/;
|
|
4392
|
+
let sttBackoff = 0; // ms to wait after errors
|
|
4393
|
+
let speechBuffer = ""; // Accumulates text across chunks until user pauses
|
|
4394
|
+
let silenceCount = 0; // Consecutive silent chunks
|
|
4395
|
+
|
|
4396
|
+
// Is Dash currently talking? (streaming or TTS)
|
|
4397
|
+
function isDashTalking() {
|
|
4398
|
+
return !!currentAudio || streaming;
|
|
4399
|
+
}
|
|
4400
|
+
|
|
4401
|
+
// Barge-in: stop TTS playback + abort streaming
|
|
4402
|
+
function bargeIn() {
|
|
4403
|
+
console.log("[barge-in] Stopping Dash");
|
|
4404
|
+
stopSpeaking();
|
|
4405
|
+
if (streaming && abortController) {
|
|
4406
|
+
abortController.abort();
|
|
4407
|
+
}
|
|
4408
|
+
}
|
|
4409
|
+
|
|
4410
|
+
// Send accumulated speech buffer as a message
|
|
4411
|
+
function flushSpeechBuffer() {
|
|
4412
|
+
const fullMessage = speechBuffer.trim();
|
|
4413
|
+
speechBuffer = "";
|
|
4414
|
+
silenceCount = 0;
|
|
4415
|
+
micBtn.classList.remove("recording");
|
|
4416
|
+
micBtn.classList.add("listening");
|
|
4417
|
+
if (fullMessage && !isDashTalking()) {
|
|
4418
|
+
console.log("[listen] Sending:", JSON.stringify(fullMessage));
|
|
4419
|
+
chatInput.value = fullMessage;
|
|
4420
|
+
autoResize();
|
|
4421
|
+
sendMessage();
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
|
|
4425
|
+
// Record a short audio chunk from the mic and transcribe via local Whisper
|
|
4426
|
+
async function listenChunk() {
|
|
4427
|
+
if (!alwaysListening || !listenStream) return;
|
|
4428
|
+
try {
|
|
4429
|
+
const recorder = new MediaRecorder(listenStream, { mimeType: "audio/webm;codecs=opus" });
|
|
4430
|
+
const chunks = [];
|
|
4431
|
+
recorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
|
|
4432
|
+
|
|
4433
|
+
const done = new Promise((resolve) => { recorder.onstop = resolve; });
|
|
4434
|
+
recorder.start();
|
|
4435
|
+
setTimeout(() => { if (recorder.state === "recording") recorder.stop(); }, 3000);
|
|
4436
|
+
await done;
|
|
4437
|
+
|
|
4438
|
+
if (chunks.length === 0 || !alwaysListening) return;
|
|
4439
|
+
const webmBlob = new Blob(chunks, { type: "audio/webm" });
|
|
4440
|
+
|
|
4441
|
+
// Convert webm → WAV (whisper-server requires WAV format)
|
|
4442
|
+
const wavBlob = await blobToWav(webmBlob);
|
|
4443
|
+
|
|
4444
|
+
// Voice activity check — skip silence to prevent Whisper hallucinations
|
|
4445
|
+
const wavBuf = await wavBlob.arrayBuffer();
|
|
4446
|
+
const pcm = new Int16Array(wavBuf, 44);
|
|
4447
|
+
let energy = 0;
|
|
4448
|
+
for (let i = 0; i < pcm.length; i++) energy += (pcm[i] / 32768) ** 2;
|
|
4449
|
+
const chunkRms = Math.sqrt(energy / pcm.length);
|
|
4450
|
+
|
|
4451
|
+
if (chunkRms < 0.01) {
|
|
4452
|
+
// Silent chunk — if we have accumulated speech, user has paused → send it
|
|
4453
|
+
silenceCount++;
|
|
4454
|
+
if (speechBuffer && silenceCount >= 1) {
|
|
4455
|
+
flushSpeechBuffer();
|
|
4456
|
+
}
|
|
4457
|
+
return;
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
silenceCount = 0;
|
|
4461
|
+
const wavBlobFinal = new Blob([wavBuf], { type: "audio/wav" });
|
|
4462
|
+
|
|
4463
|
+
// Send to local Whisper for transcription
|
|
4464
|
+
const res = await fetch("/api/stt", { method: "POST", body: wavBlobFinal });
|
|
4465
|
+
if (!res.ok) {
|
|
4466
|
+
sttBackoff = Math.min((sttBackoff || 1000) * 2, 10000);
|
|
4467
|
+
console.log("[listen] STT error", res.status, "— backing off", sttBackoff, "ms");
|
|
4468
|
+
await new Promise(r => setTimeout(r, sttBackoff));
|
|
4469
|
+
return;
|
|
4470
|
+
}
|
|
4471
|
+
sttBackoff = 0;
|
|
4472
|
+
const data = await res.json();
|
|
4473
|
+
const text = (data.text || "").trim();
|
|
4474
|
+
if (!text) return;
|
|
4475
|
+
|
|
4476
|
+
console.log("[listen]", JSON.stringify(text));
|
|
4477
|
+
if (WHISPER_NOISE.test(text)) return;
|
|
4478
|
+
|
|
4479
|
+
// If Dash is talking, user speaking = barge-in (interrupt)
|
|
4480
|
+
if (isDashTalking()) {
|
|
4481
|
+
bargeIn();
|
|
4482
|
+
return;
|
|
4483
|
+
}
|
|
4484
|
+
|
|
4485
|
+
// Accumulate speech — show in input box as preview
|
|
4486
|
+
speechBuffer += (speechBuffer ? " " : "") + text;
|
|
4487
|
+
chatInput.value = speechBuffer;
|
|
4488
|
+
autoResize();
|
|
4489
|
+
micBtn.classList.remove("listening");
|
|
4490
|
+
micBtn.classList.add("recording");
|
|
4491
|
+
} catch (err) {
|
|
4492
|
+
console.log("[listen] error:", err.message);
|
|
4493
|
+
}
|
|
4494
|
+
}
|
|
4495
|
+
|
|
4496
|
+
// Start always-listening loop
|
|
4497
|
+
async function startListening() {
|
|
4498
|
+
try {
|
|
4499
|
+
listenStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
4500
|
+
alwaysListening = true;
|
|
4501
|
+
micBtn.classList.add("listening");
|
|
4502
|
+
micBtn.title = "Mic on — speaking (click to stop)";
|
|
4503
|
+
console.log("[mic] Always-listening started");
|
|
4504
|
+
|
|
4505
|
+
// Set up barge-in volume monitor — uses RMS of time-domain waveform
|
|
4506
|
+
const bargeCtx = new AudioContext();
|
|
4507
|
+
// Resume AudioContext if Chrome suspended it
|
|
4508
|
+
if (bargeCtx.state === "suspended") await bargeCtx.resume();
|
|
4509
|
+
const bargeSource = bargeCtx.createMediaStreamSource(listenStream);
|
|
4510
|
+
bargeInAnalyser = bargeCtx.createAnalyser();
|
|
4511
|
+
bargeInAnalyser.fftSize = 2048;
|
|
4512
|
+
bargeSource.connect(bargeInAnalyser);
|
|
4513
|
+
const waveData = new Uint8Array(bargeInAnalyser.fftSize);
|
|
4514
|
+
let bargeDebounce = 0;
|
|
4515
|
+
let bargeLogTimer = 0;
|
|
4516
|
+
|
|
4517
|
+
function checkBarge() {
|
|
4518
|
+
if (!alwaysListening) { bargeCtx.close(); return; }
|
|
4519
|
+
bargeInAnalyser.getByteTimeDomainData(waveData);
|
|
4520
|
+
let sum = 0;
|
|
4521
|
+
for (let i = 0; i < waveData.length; i++) {
|
|
4522
|
+
const v = (waveData[i] - 128) / 128;
|
|
4523
|
+
sum += v * v;
|
|
4524
|
+
}
|
|
4525
|
+
const rms = Math.sqrt(sum / waveData.length);
|
|
4526
|
+
|
|
4527
|
+
const now = Date.now();
|
|
4528
|
+
if (rms > 0.04 && isDashTalking() && now - bargeDebounce > 1500) {
|
|
4529
|
+
console.log("[barge-in] Voice detected (RMS:", rms.toFixed(3), ") — stopping Dash");
|
|
4530
|
+
bargeDebounce = now;
|
|
4531
|
+
bargeIn();
|
|
4532
|
+
}
|
|
4533
|
+
requestAnimationFrame(checkBarge);
|
|
4534
|
+
}
|
|
4535
|
+
checkBarge();
|
|
4536
|
+
|
|
4537
|
+
// Continuous listen loop (record → transcribe → repeat)
|
|
4538
|
+
(async function loop() {
|
|
4539
|
+
while (alwaysListening) {
|
|
4540
|
+
await listenChunk();
|
|
4541
|
+
// Gap between chunks — gives whisper-server breathing room
|
|
4542
|
+
await new Promise(r => setTimeout(r, 500));
|
|
4543
|
+
}
|
|
4544
|
+
})();
|
|
4545
|
+
} catch (err) {
|
|
4546
|
+
console.log("[mic] Mic access denied:", err.message);
|
|
4547
|
+
addMessage("system", "Mic access denied: " + err.message);
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4551
|
+
function stopListening() {
|
|
4552
|
+
alwaysListening = false;
|
|
4553
|
+
if (listenStream) {
|
|
4554
|
+
listenStream.getTracks().forEach(t => t.stop());
|
|
4555
|
+
listenStream = null;
|
|
4556
|
+
}
|
|
4557
|
+
micBtn.classList.remove("listening", "recording");
|
|
4558
|
+
micBtn.title = "Click to talk";
|
|
4559
|
+
console.log("[mic] Stopped listening");
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
// Click mic to toggle
|
|
4563
|
+
micBtn.addEventListener("click", function(e) {
|
|
4564
|
+
e.preventDefault();
|
|
4565
|
+
e.stopPropagation();
|
|
4566
|
+
if (alwaysListening) {
|
|
4567
|
+
stopListening();
|
|
4568
|
+
} else if (voiceStatus.stt) {
|
|
4569
|
+
startListening();
|
|
4570
|
+
} else {
|
|
4571
|
+
addMessage("system", "STT not available — Whisper sidecar not running");
|
|
4572
|
+
}
|
|
4573
|
+
});
|
|
4574
|
+
|
|
4575
|
+
// Single-shot recording fallback (click without always-listen)
|
|
4576
|
+
async function startRecording() {
|
|
4577
|
+
if (micRecording || alwaysListening) return;
|
|
4578
|
+
|
|
4579
|
+
try {
|
|
4580
|
+
micStream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
4581
|
+
mediaRecorder = new MediaRecorder(micStream, { mimeType: "audio/webm;codecs=opus" });
|
|
4582
|
+
const chunks = [];
|
|
4583
|
+
mediaRecorder.ondataavailable = (e) => { if (e.data.size > 0) chunks.push(e.data); };
|
|
4584
|
+
|
|
4585
|
+
mediaRecorder.onstop = async () => {
|
|
4586
|
+
micStream.getTracks().forEach(t => t.stop());
|
|
4587
|
+
micStream = null;
|
|
4588
|
+
if (chunks.length === 0) return;
|
|
4589
|
+
const webmBlob = new Blob(chunks, { type: "audio/webm" });
|
|
4590
|
+
micBtn.classList.remove("recording");
|
|
4591
|
+
micRecording = false;
|
|
4592
|
+
try {
|
|
4593
|
+
const wavBlob = await blobToWav(webmBlob);
|
|
4594
|
+
const res = await fetch("/api/stt", { method: "POST", body: wavBlob });
|
|
4595
|
+
if (res.ok) {
|
|
4596
|
+
const data = await res.json();
|
|
4597
|
+
if (data.text && data.text.trim()) {
|
|
4598
|
+
chatInput.value = data.text.trim();
|
|
4599
|
+
autoResize();
|
|
4600
|
+
sendMessage();
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
} catch (err) { console.log("STT error:", err); }
|
|
4604
|
+
};
|
|
4605
|
+
|
|
4606
|
+
mediaRecorder.start();
|
|
4607
|
+
micRecording = true;
|
|
4608
|
+
micBtn.classList.add("recording");
|
|
4609
|
+
} catch (err) {
|
|
4610
|
+
console.log("Mic access denied:", err);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
|
|
4614
|
+
function stopRecording() {
|
|
4615
|
+
if (!micRecording || !mediaRecorder) return;
|
|
4616
|
+
mediaRecorder.stop();
|
|
4617
|
+
// State cleanup happens in onstop handler
|
|
4618
|
+
}
|
|
4619
|
+
|
|
4620
|
+
|
|
4621
|
+
// Voice settings save
|
|
4622
|
+
document.getElementById("voice-save-btn").addEventListener("click", async function() {
|
|
4623
|
+
const btn = this;
|
|
4624
|
+
btn.disabled = true;
|
|
4625
|
+
const statusEl = document.getElementById("voice-save-status");
|
|
4626
|
+
statusEl.classList.remove("visible");
|
|
4627
|
+
|
|
4628
|
+
try {
|
|
4629
|
+
const result = await api("/api/settings", {
|
|
4630
|
+
method: "PUT",
|
|
4631
|
+
body: {
|
|
4632
|
+
tts: {
|
|
4633
|
+
enabled: document.getElementById("tts-toggle").checked,
|
|
4634
|
+
autoPlay: document.getElementById("autoplay-toggle").checked,
|
|
4635
|
+
},
|
|
4636
|
+
stt: {
|
|
4637
|
+
enabled: document.getElementById("stt-toggle").checked,
|
|
4638
|
+
},
|
|
4639
|
+
},
|
|
4640
|
+
});
|
|
4641
|
+
ttsAutoPlay = result.tts?.autoPlay !== false;
|
|
4642
|
+
statusEl.classList.add("visible");
|
|
4643
|
+
setTimeout(() => statusEl.classList.remove("visible"), 2000);
|
|
4644
|
+
} catch (err) {
|
|
4645
|
+
statusEl.textContent = err.message;
|
|
4646
|
+
statusEl.classList.add("visible");
|
|
4647
|
+
setTimeout(() => {
|
|
4648
|
+
statusEl.textContent = "Saved";
|
|
4649
|
+
statusEl.classList.remove("visible");
|
|
4650
|
+
}, 3000);
|
|
4651
|
+
} finally {
|
|
4652
|
+
btn.disabled = false;
|
|
4653
|
+
}
|
|
4654
|
+
});
|
|
4655
|
+
|
|
4656
|
+
|
|
4657
|
+
// --- Start ---
|
|
4658
|
+
init();
|
|
4659
|
+
})();
|
|
4660
|
+
</script>
|
|
4661
|
+
|
|
4662
|
+
<script src="/public/share-modal.js"></script>
|
|
4663
|
+
|
|
4664
|
+
<!-- UI update toast -->
|
|
4665
|
+
<div class="ui-update-toast" id="ui-update-toast">
|
|
4666
|
+
<span>Hi — there's been a tweak to this UI. Want to try it?</span>
|
|
4667
|
+
<button onclick="try{sessionStorage.setItem('_dash_restore',JSON.stringify({thread:typeof currentThreadId!=='undefined'?currentThreadId:null,scroll:document.getElementById('messages')?.scrollTop||0}))}catch(e){}location.reload()">Refresh</button>
|
|
4668
|
+
<button class="dismiss" onclick="this.parentElement.classList.remove('visible')">×</button>
|
|
4669
|
+
</div>
|
|
4670
|
+
<script>
|
|
4671
|
+
(function() {
|
|
4672
|
+
let knownVersion = null;
|
|
4673
|
+
async function checkUiVersion() {
|
|
4674
|
+
try {
|
|
4675
|
+
const r = await fetch("/api/ui-version");
|
|
4676
|
+
if (!r.ok) return;
|
|
4677
|
+
const { version } = await r.json();
|
|
4678
|
+
if (!knownVersion) { knownVersion = version; return; }
|
|
4679
|
+
if (version !== knownVersion) {
|
|
4680
|
+
document.getElementById("ui-update-toast").classList.add("visible");
|
|
4681
|
+
}
|
|
4682
|
+
} catch {}
|
|
4683
|
+
}
|
|
4684
|
+
checkUiVersion();
|
|
4685
|
+
setInterval(checkUiVersion, 30000);
|
|
4686
|
+
})();
|
|
4687
|
+
</script>
|
|
4688
|
+
</body>
|
|
4689
|
+
</html>
|