@nac3/forge-cli 0.2.0-alpha.1
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 +45 -0
- package/README.md +371 -0
- package/dist/bin/yf.d.ts +5 -0
- package/dist/bin/yf.d.ts.map +1 -0
- package/dist/bin/yf.js +86 -0
- package/dist/bin/yf.js.map +1 -0
- package/dist/chat/claude.d.ts +100 -0
- package/dist/chat/claude.d.ts.map +1 -0
- package/dist/chat/claude.js +228 -0
- package/dist/chat/claude.js.map +1 -0
- package/dist/chat/ingest_session.d.ts +97 -0
- package/dist/chat/ingest_session.d.ts.map +1 -0
- package/dist/chat/ingest_session.js +99 -0
- package/dist/chat/ingest_session.js.map +1 -0
- package/dist/chat/panel.d.ts +15 -0
- package/dist/chat/panel.d.ts.map +1 -0
- package/dist/chat/panel.js +1526 -0
- package/dist/chat/panel.js.map +1 -0
- package/dist/chat/persistence.d.ts +37 -0
- package/dist/chat/persistence.d.ts.map +1 -0
- package/dist/chat/persistence.js +91 -0
- package/dist/chat/persistence.js.map +1 -0
- package/dist/chat/server.d.ts +34 -0
- package/dist/chat/server.d.ts.map +1 -0
- package/dist/chat/server.js +1540 -0
- package/dist/chat/server.js.map +1 -0
- package/dist/chat/spec_extract.d.ts +35 -0
- package/dist/chat/spec_extract.d.ts.map +1 -0
- package/dist/chat/spec_extract.js +152 -0
- package/dist/chat/spec_extract.js.map +1 -0
- package/dist/chat/spec_plan.d.ts +65 -0
- package/dist/chat/spec_plan.d.ts.map +1 -0
- package/dist/chat/spec_plan.js +160 -0
- package/dist/chat/spec_plan.js.map +1 -0
- package/dist/chat/spec_scaffold.d.ts +95 -0
- package/dist/chat/spec_scaffold.d.ts.map +1 -0
- package/dist/chat/spec_scaffold.js +220 -0
- package/dist/chat/spec_scaffold.js.map +1 -0
- package/dist/chat/tools/git.d.ts +59 -0
- package/dist/chat/tools/git.d.ts.map +1 -0
- package/dist/chat/tools/git.js +313 -0
- package/dist/chat/tools/git.js.map +1 -0
- package/dist/chat/tools/github.d.ts +59 -0
- package/dist/chat/tools/github.d.ts.map +1 -0
- package/dist/chat/tools/github.js +310 -0
- package/dist/chat/tools/github.js.map +1 -0
- package/dist/chat/tools/lifecycle.d.ts +82 -0
- package/dist/chat/tools/lifecycle.d.ts.map +1 -0
- package/dist/chat/tools/lifecycle.js +295 -0
- package/dist/chat/tools/lifecycle.js.map +1 -0
- package/dist/chat/tools/manual.d.ts +26 -0
- package/dist/chat/tools/manual.d.ts.map +1 -0
- package/dist/chat/tools/manual.js +164 -0
- package/dist/chat/tools/manual.js.map +1 -0
- package/dist/chat/tools/reader.d.ts +80 -0
- package/dist/chat/tools/reader.d.ts.map +1 -0
- package/dist/chat/tools/reader.js +471 -0
- package/dist/chat/tools/reader.js.map +1 -0
- package/dist/chat/tools.d.ts +106 -0
- package/dist/chat/tools.d.ts.map +1 -0
- package/dist/chat/tools.js +587 -0
- package/dist/chat/tools.js.map +1 -0
- package/dist/codegen/e2e.d.ts +106 -0
- package/dist/codegen/e2e.d.ts.map +1 -0
- package/dist/codegen/e2e.js +931 -0
- package/dist/codegen/e2e.js.map +1 -0
- package/dist/codegen/v3_flow_emit.d.ts +70 -0
- package/dist/codegen/v3_flow_emit.d.ts.map +1 -0
- package/dist/codegen/v3_flow_emit.js +225 -0
- package/dist/codegen/v3_flow_emit.js.map +1 -0
- package/dist/commands/_stub.d.ts +2 -0
- package/dist/commands/_stub.d.ts.map +1 -0
- package/dist/commands/_stub.js +21 -0
- package/dist/commands/_stub.js.map +1 -0
- package/dist/commands/app.d.ts +31 -0
- package/dist/commands/app.d.ts.map +1 -0
- package/dist/commands/app.js +331 -0
- package/dist/commands/app.js.map +1 -0
- package/dist/commands/chat.d.ts +18 -0
- package/dist/commands/chat.d.ts.map +1 -0
- package/dist/commands/chat.js +76 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/deploy.d.ts +21 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +121 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/doctor.d.ts +14 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +280 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/figma.d.ts +32 -0
- package/dist/commands/figma.d.ts.map +1 -0
- package/dist/commands/figma.js +141 -0
- package/dist/commands/figma.js.map +1 -0
- package/dist/commands/gen-flow-tests.d.ts +8 -0
- package/dist/commands/gen-flow-tests.d.ts.map +1 -0
- package/dist/commands/gen-flow-tests.js +78 -0
- package/dist/commands/gen-flow-tests.js.map +1 -0
- package/dist/commands/gen-tests.d.ts +9 -0
- package/dist/commands/gen-tests.d.ts.map +1 -0
- package/dist/commands/gen-tests.js +118 -0
- package/dist/commands/gen-tests.js.map +1 -0
- package/dist/commands/license.d.ts +14 -0
- package/dist/commands/license.d.ts.map +1 -0
- package/dist/commands/license.js +182 -0
- package/dist/commands/license.js.map +1 -0
- package/dist/commands/log.d.ts +19 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +101 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/migrate.d.ts +118 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +1410 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/mobile.d.ts +27 -0
- package/dist/commands/mobile.d.ts.map +1 -0
- package/dist/commands/mobile.js +90 -0
- package/dist/commands/mobile.js.map +1 -0
- package/dist/commands/new.d.ts +32 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +107 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/pilot.d.ts +8 -0
- package/dist/commands/pilot.d.ts.map +1 -0
- package/dist/commands/pilot.js +104 -0
- package/dist/commands/pilot.js.map +1 -0
- package/dist/commands/projects.d.ts +21 -0
- package/dist/commands/projects.d.ts.map +1 -0
- package/dist/commands/projects.js +238 -0
- package/dist/commands/projects.js.map +1 -0
- package/dist/commands/publish.d.ts +35 -0
- package/dist/commands/publish.d.ts.map +1 -0
- package/dist/commands/publish.js +194 -0
- package/dist/commands/publish.js.map +1 -0
- package/dist/commands/repo.d.ts +59 -0
- package/dist/commands/repo.d.ts.map +1 -0
- package/dist/commands/repo.js +178 -0
- package/dist/commands/repo.js.map +1 -0
- package/dist/commands/review-screens.d.ts +28 -0
- package/dist/commands/review-screens.d.ts.map +1 -0
- package/dist/commands/review-screens.js +345 -0
- package/dist/commands/review-screens.js.map +1 -0
- package/dist/commands/scenarios.d.ts +23 -0
- package/dist/commands/scenarios.d.ts.map +1 -0
- package/dist/commands/scenarios.js +304 -0
- package/dist/commands/scenarios.js.map +1 -0
- package/dist/commands/ship.d.ts +18 -0
- package/dist/commands/ship.d.ts.map +1 -0
- package/dist/commands/ship.js +41 -0
- package/dist/commands/ship.js.map +1 -0
- package/dist/commands/test.d.ts +29 -0
- package/dist/commands/test.d.ts.map +1 -0
- package/dist/commands/test.js +62 -0
- package/dist/commands/test.js.map +1 -0
- package/dist/commands/tunnel.d.ts +22 -0
- package/dist/commands/tunnel.d.ts.map +1 -0
- package/dist/commands/tunnel.js +77 -0
- package/dist/commands/tunnel.js.map +1 -0
- package/dist/commands/validate.d.ts +14 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +51 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/commands/vault.d.ts +32 -0
- package/dist/commands/vault.d.ts.map +1 -0
- package/dist/commands/vault.js +489 -0
- package/dist/commands/vault.js.map +1 -0
- package/dist/commands/voice.d.ts +16 -0
- package/dist/commands/voice.d.ts.map +1 -0
- package/dist/commands/voice.js +69 -0
- package/dist/commands/voice.js.map +1 -0
- package/dist/core/cascade_router.d.ts +90 -0
- package/dist/core/cascade_router.d.ts.map +1 -0
- package/dist/core/cascade_router.js +131 -0
- package/dist/core/cascade_router.js.map +1 -0
- package/dist/core/cf_tunnel.d.ts +52 -0
- package/dist/core/cf_tunnel.d.ts.map +1 -0
- package/dist/core/cf_tunnel.js +134 -0
- package/dist/core/cf_tunnel.js.map +1 -0
- package/dist/core/gha_dispatcher.d.ts +48 -0
- package/dist/core/gha_dispatcher.d.ts.map +1 -0
- package/dist/core/gha_dispatcher.js +198 -0
- package/dist/core/gha_dispatcher.js.map +1 -0
- package/dist/core/logger.d.ts +89 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +245 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/mode.d.ts +26 -0
- package/dist/core/mode.d.ts.map +1 -0
- package/dist/core/mode.js +122 -0
- package/dist/core/mode.js.map +1 -0
- package/dist/core/pairing.d.ts +40 -0
- package/dist/core/pairing.d.ts.map +1 -0
- package/dist/core/pairing.js +145 -0
- package/dist/core/pairing.js.map +1 -0
- package/dist/core/pilot_setup.d.ts +29 -0
- package/dist/core/pilot_setup.d.ts.map +1 -0
- package/dist/core/pilot_setup.js +119 -0
- package/dist/core/pilot_setup.js.map +1 -0
- package/dist/core/polar.d.ts +81 -0
- package/dist/core/polar.d.ts.map +1 -0
- package/dist/core/polar.js +175 -0
- package/dist/core/polar.js.map +1 -0
- package/dist/core/project_picker.d.ts +56 -0
- package/dist/core/project_picker.d.ts.map +1 -0
- package/dist/core/project_picker.js +86 -0
- package/dist/core/project_picker.js.map +1 -0
- package/dist/core/projects.d.ts +58 -0
- package/dist/core/projects.d.ts.map +1 -0
- package/dist/core/projects.js +146 -0
- package/dist/core/projects.js.map +1 -0
- package/dist/core/projects_sync.d.ts +80 -0
- package/dist/core/projects_sync.d.ts.map +1 -0
- package/dist/core/projects_sync.js +278 -0
- package/dist/core/projects_sync.js.map +1 -0
- package/dist/core/remote_runner.d.ts +70 -0
- package/dist/core/remote_runner.d.ts.map +1 -0
- package/dist/core/remote_runner.js +133 -0
- package/dist/core/remote_runner.js.map +1 -0
- package/dist/core/repo_state.d.ts +24 -0
- package/dist/core/repo_state.d.ts.map +1 -0
- package/dist/core/repo_state.js +109 -0
- package/dist/core/repo_state.js.map +1 -0
- package/dist/core/target.d.ts +31 -0
- package/dist/core/target.d.ts.map +1 -0
- package/dist/core/target.js +121 -0
- package/dist/core/target.js.map +1 -0
- package/dist/deploy/aws.d.ts +43 -0
- package/dist/deploy/aws.d.ts.map +1 -0
- package/dist/deploy/aws.js +173 -0
- package/dist/deploy/aws.js.map +1 -0
- package/dist/figma/api.d.ts +35 -0
- package/dist/figma/api.d.ts.map +1 -0
- package/dist/figma/api.js +40 -0
- package/dist/figma/api.js.map +1 -0
- package/dist/figma/decorator.d.ts +74 -0
- package/dist/figma/decorator.d.ts.map +1 -0
- package/dist/figma/decorator.js +210 -0
- package/dist/figma/decorator.js.map +1 -0
- package/dist/figma/heuristics.d.ts +29 -0
- package/dist/figma/heuristics.d.ts.map +1 -0
- package/dist/figma/heuristics.js +110 -0
- package/dist/figma/heuristics.js.map +1 -0
- package/dist/figma/normalize.d.ts +33 -0
- package/dist/figma/normalize.d.ts.map +1 -0
- package/dist/figma/normalize.js +101 -0
- package/dist/figma/normalize.js.map +1 -0
- package/dist/figma/tokens.d.ts +23 -0
- package/dist/figma/tokens.d.ts.map +1 -0
- package/dist/figma/tokens.js +111 -0
- package/dist/figma/tokens.js.map +1 -0
- package/dist/figma/types.d.ts +118 -0
- package/dist/figma/types.d.ts.map +1 -0
- package/dist/figma/types.js +12 -0
- package/dist/figma/types.js.map +1 -0
- package/dist/i18n/index.d.ts +48 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +135 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/types.d.ts +52 -0
- package/dist/i18n/types.d.ts.map +1 -0
- package/dist/i18n/types.js +85 -0
- package/dist/i18n/types.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/lan/mdns_packet.d.ts +74 -0
- package/dist/lan/mdns_packet.d.ts.map +1 -0
- package/dist/lan/mdns_packet.js +247 -0
- package/dist/lan/mdns_packet.js.map +1 -0
- package/dist/lan/mdns_service.d.ts +102 -0
- package/dist/lan/mdns_service.d.ts.map +1 -0
- package/dist/lan/mdns_service.js +206 -0
- package/dist/lan/mdns_service.js.map +1 -0
- package/dist/license/activate.d.ts +33 -0
- package/dist/license/activate.d.ts.map +1 -0
- package/dist/license/activate.js +135 -0
- package/dist/license/activate.js.map +1 -0
- package/dist/license/fingerprint.d.ts +2 -0
- package/dist/license/fingerprint.d.ts.map +1 -0
- package/dist/license/fingerprint.js +29 -0
- package/dist/license/fingerprint.js.map +1 -0
- package/dist/license/hito4_client.d.ts +24 -0
- package/dist/license/hito4_client.d.ts.map +1 -0
- package/dist/license/hito4_client.js +103 -0
- package/dist/license/hito4_client.js.map +1 -0
- package/dist/license/index.d.ts +22 -0
- package/dist/license/index.d.ts.map +1 -0
- package/dist/license/index.js +125 -0
- package/dist/license/index.js.map +1 -0
- package/dist/license/types.d.ts +38 -0
- package/dist/license/types.d.ts.map +1 -0
- package/dist/license/types.js +9 -0
- package/dist/license/types.js.map +1 -0
- package/dist/migrate/ai-apply.d.ts +198 -0
- package/dist/migrate/ai-apply.d.ts.map +1 -0
- package/dist/migrate/ai-apply.js +833 -0
- package/dist/migrate/ai-apply.js.map +1 -0
- package/dist/migrate/ai-decorator.d.ts +87 -0
- package/dist/migrate/ai-decorator.d.ts.map +1 -0
- package/dist/migrate/ai-decorator.js +203 -0
- package/dist/migrate/ai-decorator.js.map +1 -0
- package/dist/migrate/apply.d.ts +28 -0
- package/dist/migrate/apply.d.ts.map +1 -0
- package/dist/migrate/apply.js +119 -0
- package/dist/migrate/apply.js.map +1 -0
- package/dist/migrate/audit.d.ts +9 -0
- package/dist/migrate/audit.d.ts.map +1 -0
- package/dist/migrate/audit.js +197 -0
- package/dist/migrate/audit.js.map +1 -0
- package/dist/migrate/diff.d.ts +28 -0
- package/dist/migrate/diff.d.ts.map +1 -0
- package/dist/migrate/diff.js +154 -0
- package/dist/migrate/diff.js.map +1 -0
- package/dist/migrate/html-orchestrator.d.ts +81 -0
- package/dist/migrate/html-orchestrator.d.ts.map +1 -0
- package/dist/migrate/html-orchestrator.js +233 -0
- package/dist/migrate/html-orchestrator.js.map +1 -0
- package/dist/migrate/html-walker.d.ts +93 -0
- package/dist/migrate/html-walker.d.ts.map +1 -0
- package/dist/migrate/html-walker.js +288 -0
- package/dist/migrate/html-walker.js.map +1 -0
- package/dist/migrate/js-template-walker.d.ts +118 -0
- package/dist/migrate/js-template-walker.d.ts.map +1 -0
- package/dist/migrate/js-template-walker.js +644 -0
- package/dist/migrate/js-template-walker.js.map +1 -0
- package/dist/migrate/manifest-validator.d.ts +30 -0
- package/dist/migrate/manifest-validator.d.ts.map +1 -0
- package/dist/migrate/manifest-validator.js +261 -0
- package/dist/migrate/manifest-validator.js.map +1 -0
- package/dist/migrate/overrides.d.ts +58 -0
- package/dist/migrate/overrides.d.ts.map +1 -0
- package/dist/migrate/overrides.js +193 -0
- package/dist/migrate/overrides.js.map +1 -0
- package/dist/migrate/plugin-scope.d.ts +42 -0
- package/dist/migrate/plugin-scope.d.ts.map +1 -0
- package/dist/migrate/plugin-scope.js +94 -0
- package/dist/migrate/plugin-scope.js.map +1 -0
- package/dist/migrate/types.d.ts +45 -0
- package/dist/migrate/types.d.ts.map +1 -0
- package/dist/migrate/types.js +9 -0
- package/dist/migrate/types.js.map +1 -0
- package/dist/migrate/verb-inference.d.ts +37 -0
- package/dist/migrate/verb-inference.d.ts.map +1 -0
- package/dist/migrate/verb-inference.js +274 -0
- package/dist/migrate/verb-inference.js.map +1 -0
- package/dist/nac3/attrs.d.ts +87 -0
- package/dist/nac3/attrs.d.ts.map +1 -0
- package/dist/nac3/attrs.js +134 -0
- package/dist/nac3/attrs.js.map +1 -0
- package/dist/nac3/scenario_dsl.d.ts +71 -0
- package/dist/nac3/scenario_dsl.d.ts.map +1 -0
- package/dist/nac3/scenario_dsl.js +191 -0
- package/dist/nac3/scenario_dsl.js.map +1 -0
- package/dist/nac3/tokens.d.ts +126 -0
- package/dist/nac3/tokens.d.ts.map +1 -0
- package/dist/nac3/tokens.js +138 -0
- package/dist/nac3/tokens.js.map +1 -0
- package/dist/reader/parsers/csv.d.ts +42 -0
- package/dist/reader/parsers/csv.d.ts.map +1 -0
- package/dist/reader/parsers/csv.js +221 -0
- package/dist/reader/parsers/csv.js.map +1 -0
- package/dist/reader/parsers/docx.d.ts +31 -0
- package/dist/reader/parsers/docx.d.ts.map +1 -0
- package/dist/reader/parsers/docx.js +51 -0
- package/dist/reader/parsers/docx.js.map +1 -0
- package/dist/reader/parsers/epub.d.ts +39 -0
- package/dist/reader/parsers/epub.d.ts.map +1 -0
- package/dist/reader/parsers/epub.js +265 -0
- package/dist/reader/parsers/epub.js.map +1 -0
- package/dist/reader/parsers/html.d.ts +40 -0
- package/dist/reader/parsers/html.d.ts.map +1 -0
- package/dist/reader/parsers/html.js +386 -0
- package/dist/reader/parsers/html.js.map +1 -0
- package/dist/reader/parsers/md.d.ts +30 -0
- package/dist/reader/parsers/md.d.ts.map +1 -0
- package/dist/reader/parsers/md.js +199 -0
- package/dist/reader/parsers/md.js.map +1 -0
- package/dist/reader/parsers/pdf.d.ts +39 -0
- package/dist/reader/parsers/pdf.d.ts.map +1 -0
- package/dist/reader/parsers/pdf.js +220 -0
- package/dist/reader/parsers/pdf.js.map +1 -0
- package/dist/reader/parsers/rtf.d.ts +37 -0
- package/dist/reader/parsers/rtf.d.ts.map +1 -0
- package/dist/reader/parsers/rtf.js +347 -0
- package/dist/reader/parsers/rtf.js.map +1 -0
- package/dist/reader/parsers/source.d.ts +32 -0
- package/dist/reader/parsers/source.d.ts.map +1 -0
- package/dist/reader/parsers/source.js +122 -0
- package/dist/reader/parsers/source.js.map +1 -0
- package/dist/reader/parsers/txt.d.ts +25 -0
- package/dist/reader/parsers/txt.d.ts.map +1 -0
- package/dist/reader/parsers/txt.js +56 -0
- package/dist/reader/parsers/txt.js.map +1 -0
- package/dist/reader/parsers/xlsx.d.ts +33 -0
- package/dist/reader/parsers/xlsx.d.ts.map +1 -0
- package/dist/reader/parsers/xlsx.js +143 -0
- package/dist/reader/parsers/xlsx.js.map +1 -0
- package/dist/reader/registry.d.ts +39 -0
- package/dist/reader/registry.d.ts.map +1 -0
- package/dist/reader/registry.js +172 -0
- package/dist/reader/registry.js.map +1 -0
- package/dist/reader/search.d.ts +27 -0
- package/dist/reader/search.d.ts.map +1 -0
- package/dist/reader/search.js +77 -0
- package/dist/reader/search.js.map +1 -0
- package/dist/reader/state.d.ts +56 -0
- package/dist/reader/state.d.ts.map +1 -0
- package/dist/reader/state.js +179 -0
- package/dist/reader/state.js.map +1 -0
- package/dist/reader/types.d.ts +119 -0
- package/dist/reader/types.d.ts.map +1 -0
- package/dist/reader/types.js +23 -0
- package/dist/reader/types.js.map +1 -0
- package/dist/ship/run.d.ts +26 -0
- package/dist/ship/run.d.ts.map +1 -0
- package/dist/ship/run.js +123 -0
- package/dist/ship/run.js.map +1 -0
- package/dist/template/index.d.ts +50 -0
- package/dist/template/index.d.ts.map +1 -0
- package/dist/template/index.js +140 -0
- package/dist/template/index.js.map +1 -0
- package/dist/ui/colors.d.ts +13 -0
- package/dist/ui/colors.d.ts.map +1 -0
- package/dist/ui/colors.js +26 -0
- package/dist/ui/colors.js.map +1 -0
- package/dist/validate/index.d.ts +19 -0
- package/dist/validate/index.d.ts.map +1 -0
- package/dist/validate/index.js +181 -0
- package/dist/validate/index.js.map +1 -0
- package/dist/vault/catalog.d.ts +55 -0
- package/dist/vault/catalog.d.ts.map +1 -0
- package/dist/vault/catalog.js +424 -0
- package/dist/vault/catalog.js.map +1 -0
- package/dist/vault/crypto.d.ts +82 -0
- package/dist/vault/crypto.d.ts.map +1 -0
- package/dist/vault/crypto.js +173 -0
- package/dist/vault/crypto.js.map +1 -0
- package/dist/vault/git_askpass.d.ts +26 -0
- package/dist/vault/git_askpass.d.ts.map +1 -0
- package/dist/vault/git_askpass.js +104 -0
- package/dist/vault/git_askpass.js.map +1 -0
- package/dist/vault/migrator.d.ts +57 -0
- package/dist/vault/migrator.d.ts.map +1 -0
- package/dist/vault/migrator.js +204 -0
- package/dist/vault/migrator.js.map +1 -0
- package/dist/vault/redactor.d.ts +73 -0
- package/dist/vault/redactor.d.ts.map +1 -0
- package/dist/vault/redactor.js +182 -0
- package/dist/vault/redactor.js.map +1 -0
- package/dist/vault/store.d.ts +132 -0
- package/dist/vault/store.d.ts.map +1 -0
- package/dist/vault/store.js +335 -0
- package/dist/vault/store.js.map +1 -0
- package/dist/version.d.ts +8 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +8 -0
- package/dist/version.js.map +1 -0
- package/dist/voice/chunker.d.ts +43 -0
- package/dist/voice/chunker.d.ts.map +1 -0
- package/dist/voice/chunker.js +133 -0
- package/dist/voice/chunker.js.map +1 -0
- package/dist/voice/config.d.ts +14 -0
- package/dist/voice/config.d.ts.map +1 -0
- package/dist/voice/config.js +51 -0
- package/dist/voice/config.js.map +1 -0
- package/dist/voice/intents.d.ts +71 -0
- package/dist/voice/intents.d.ts.map +1 -0
- package/dist/voice/intents.js +0 -0
- package/dist/voice/intents.js.map +1 -0
- package/dist/voice/providers/elevenlabs.d.ts +53 -0
- package/dist/voice/providers/elevenlabs.d.ts.map +1 -0
- package/dist/voice/providers/elevenlabs.js +159 -0
- package/dist/voice/providers/elevenlabs.js.map +1 -0
- package/dist/voice/providers/google.d.ts +56 -0
- package/dist/voice/providers/google.d.ts.map +1 -0
- package/dist/voice/providers/google.js +253 -0
- package/dist/voice/providers/google.js.map +1 -0
- package/dist/voice/providers/whisper.d.ts +44 -0
- package/dist/voice/providers/whisper.d.ts.map +1 -0
- package/dist/voice/providers/whisper.js +179 -0
- package/dist/voice/providers/whisper.js.map +1 -0
- package/dist/voice/registry.d.ts +35 -0
- package/dist/voice/registry.d.ts.map +1 -0
- package/dist/voice/registry.js +48 -0
- package/dist/voice/registry.js.map +1 -0
- package/dist/voice/router.d.ts +62 -0
- package/dist/voice/router.d.ts.map +1 -0
- package/dist/voice/router.js +175 -0
- package/dist/voice/router.js.map +1 -0
- package/dist/voice/types.d.ts +116 -0
- package/dist/voice/types.d.ts.map +1 -0
- package/dist/voice/types.js +22 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/voiceprint/enrollment.d.ts +36 -0
- package/dist/voice/voiceprint/enrollment.d.ts.map +1 -0
- package/dist/voice/voiceprint/enrollment.js +71 -0
- package/dist/voice/voiceprint/enrollment.js.map +1 -0
- package/dist/voice/voiceprint/identify.d.ts +16 -0
- package/dist/voice/voiceprint/identify.d.ts.map +1 -0
- package/dist/voice/voiceprint/identify.js +20 -0
- package/dist/voice/voiceprint/identify.js.map +1 -0
- package/dist/voice/voiceprint/liveness.d.ts +90 -0
- package/dist/voice/voiceprint/liveness.d.ts.map +1 -0
- package/dist/voice/voiceprint/liveness.js +251 -0
- package/dist/voice/voiceprint/liveness.js.map +1 -0
- package/dist/voice/voiceprint/match.d.ts +54 -0
- package/dist/voice/voiceprint/match.d.ts.map +1 -0
- package/dist/voice/voiceprint/match.js +88 -0
- package/dist/voice/voiceprint/match.js.map +1 -0
- package/dist/voice/voiceprint/providers/local-mfcc-stub.d.ts +44 -0
- package/dist/voice/voiceprint/providers/local-mfcc-stub.d.ts.map +1 -0
- package/dist/voice/voiceprint/providers/local-mfcc-stub.js +92 -0
- package/dist/voice/voiceprint/providers/local-mfcc-stub.js.map +1 -0
- package/dist/voice/voiceprint/store.d.ts +60 -0
- package/dist/voice/voiceprint/store.d.ts.map +1 -0
- package/dist/voice/voiceprint/store.js +155 -0
- package/dist/voice/voiceprint/store.js.map +1 -0
- package/dist/voice/voiceprint/trust.d.ts +90 -0
- package/dist/voice/voiceprint/trust.d.ts.map +1 -0
- package/dist/voice/voiceprint/trust.js +150 -0
- package/dist/voice/voiceprint/trust.js.map +1 -0
- package/dist/voice/voiceprint/types.d.ts +100 -0
- package/dist/voice/voiceprint/types.d.ts.map +1 -0
- package/dist/voice/voiceprint/types.js +23 -0
- package/dist/voice/voiceprint/types.js.map +1 -0
- package/dist/voice/wake.d.ts +64 -0
- package/dist/voice/wake.d.ts.map +1 -0
- package/dist/voice/wake.js +143 -0
- package/dist/voice/wake.js.map +1 -0
- package/docs/manuals/manual.ar.html +91 -0
- package/docs/manuals/manual.de.html +100 -0
- package/docs/manuals/manual.en.html +118 -0
- package/docs/manuals/manual.es.html +120 -0
- package/docs/manuals/manual.fr.html +102 -0
- package/docs/manuals/manual.hi.html +93 -0
- package/docs/manuals/manual.it.html +93 -0
- package/docs/manuals/manual.ja.html +97 -0
- package/docs/manuals/manual.pt.html +103 -0
- package/docs/manuals/manual.zh.html +89 -0
- package/package.json +94 -0
- package/src/i18n/catalogs/ar.json +86 -0
- package/src/i18n/catalogs/de.json +86 -0
- package/src/i18n/catalogs/en.json +86 -0
- package/src/i18n/catalogs/es.json +86 -0
- package/src/i18n/catalogs/fr.json +86 -0
- package/src/i18n/catalogs/hi.json +86 -0
- package/src/i18n/catalogs/it.json +86 -0
- package/src/i18n/catalogs/ja.json +86 -0
- package/src/i18n/catalogs/pt.json +86 -0
- package/src/i18n/catalogs/zh.json +86 -0
- package/templates/react-app/README.md +43 -0
- package/templates/react-app/index.html +12 -0
- package/templates/react-app/package.json +35 -0
- package/templates/react-app/src/App.tsx +106 -0
- package/templates/react-app/src/main.tsx +21 -0
- package/templates/react-app/src/nac/manifest.ts +46 -0
- package/templates/react-app/src/styles.css +68 -0
- package/templates/react-app/tsconfig.json +19 -0
- package/templates/react-app/vite.config.ts +12 -0
- package/templates/react-app/yujin.forge.json +15 -0
|
@@ -0,0 +1,1526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Yujin Forge -- chat panel HTML.
|
|
3
|
+
*
|
|
4
|
+
* Generated as a single self-contained HTML string that the
|
|
5
|
+
* chat server serves at GET /. No framework, no build step --
|
|
6
|
+
* the panel runs inside any modern browser.
|
|
7
|
+
*
|
|
8
|
+
* Three modes (memory rule yujin_assistant_3_modes from Yujin
|
|
9
|
+
* Koe), persisted in localStorage:
|
|
10
|
+
*
|
|
11
|
+
* globito small floating bubble bottom-right
|
|
12
|
+
* ventana chica 360x560 panel: chat history + input
|
|
13
|
+
* ventana grande overlay: chat left, project preview right
|
|
14
|
+
*
|
|
15
|
+
* Voice button is rendered but disabled with a tooltip ('voice
|
|
16
|
+
* arrives in v1.0') until the SPEC 5 voice integration lands.
|
|
17
|
+
*/
|
|
18
|
+
import { VERSION } from '../version.js';
|
|
19
|
+
import { currentLanguage, tForLang, } from '../i18n/index.js';
|
|
20
|
+
import { tokensToCssVars } from '../nac3/tokens.js';
|
|
21
|
+
export function renderPanelHtml(cfg) {
|
|
22
|
+
// Inject runtime config via a <script> block. NEVER use string
|
|
23
|
+
// interpolation for user-controlled values in the HTML body --
|
|
24
|
+
// we control all inputs here (projectRoot + projectName come
|
|
25
|
+
// from the local filesystem, port is a number) but the JSON
|
|
26
|
+
// wrapper keeps the boundary clean.
|
|
27
|
+
const lang = cfg.lang ?? currentLanguage();
|
|
28
|
+
// Resolve user-facing strings using THIS render's lang (not
|
|
29
|
+
// the server-wide setLanguage state). Each request renders
|
|
30
|
+
// independently from a per-request cookie / query -- stateless
|
|
31
|
+
// i18n. Avoids race conditions when concurrent clients use
|
|
32
|
+
// different languages.
|
|
33
|
+
const tr = {
|
|
34
|
+
inputPlaceholder: tForLang(lang, 'panel.input.placeholder'),
|
|
35
|
+
sendButton: tForLang(lang, 'panel.button.send'),
|
|
36
|
+
micButton: tForLang(lang, 'panel.button.mic'),
|
|
37
|
+
micButtonRec: tForLang(lang, 'panel.button.mic.recording'),
|
|
38
|
+
ttsButton: tForLang(lang, 'panel.button.tts'),
|
|
39
|
+
keysButton: tForLang(lang, 'panel.button.keys'),
|
|
40
|
+
minimiseButton: tForLang(lang, 'panel.button.minimise'),
|
|
41
|
+
closeButton: tForLang(lang, 'panel.button.close'),
|
|
42
|
+
languageButton: tForLang(lang, 'panel.button.language'),
|
|
43
|
+
vaultClose: tForLang(lang, 'vault.modal.aria.close'),
|
|
44
|
+
brandKanji: tForLang(lang, 'panel.brand.kanji'),
|
|
45
|
+
streamEmptyKanji: tForLang(lang, 'panel.stream.empty.kanji'),
|
|
46
|
+
streamEmptyBody: tForLang(lang, 'panel.stream.empty.body'),
|
|
47
|
+
traceEmptyKanji: tForLang(lang, 'panel.trace.empty.kanji'),
|
|
48
|
+
traceEmptyBody: tForLang(lang, 'panel.trace.empty.body'),
|
|
49
|
+
};
|
|
50
|
+
const runtimeConfig = JSON.stringify({
|
|
51
|
+
projectName: cfg.projectName,
|
|
52
|
+
projectRoot: cfg.projectRoot,
|
|
53
|
+
port: cfg.port,
|
|
54
|
+
version: VERSION,
|
|
55
|
+
lang,
|
|
56
|
+
i18n: tr,
|
|
57
|
+
});
|
|
58
|
+
// The globito carries the kanji as visible text; screen
|
|
59
|
+
// readers receive a translated aria-label. RTL languages
|
|
60
|
+
// (ar, hi) flip the layout direction.
|
|
61
|
+
const dir = lang === 'ar' ? 'rtl' : 'ltr';
|
|
62
|
+
return `<!DOCTYPE html>
|
|
63
|
+
<html lang="${lang}" dir="${dir}">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="utf-8">
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
67
|
+
<meta name="yf-version" content="${escapeHtml(VERSION)}">
|
|
68
|
+
<meta name="yf-project" content="${escapeHtml(cfg.projectName)}">
|
|
69
|
+
<title>Yujin Forge -- chat (${escapeHtml(cfg.projectName)})</title>
|
|
70
|
+
<style>
|
|
71
|
+
${tokensToCssVars()}
|
|
72
|
+
* { box-sizing: border-box; }
|
|
73
|
+
html, body { margin: 0; padding: 0; height: 100%; }
|
|
74
|
+
body {
|
|
75
|
+
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
76
|
+
background-color: var(--bg-1);
|
|
77
|
+
background-image: linear-gradient(135deg, var(--bg-1), var(--bg-2));
|
|
78
|
+
color: var(--ink); line-height: 1.5;
|
|
79
|
+
}
|
|
80
|
+
.kanji { font-family: "Noto Serif JP", serif; color: var(--amber); }
|
|
81
|
+
|
|
82
|
+
/* ---- globito (collapsed) ---- */
|
|
83
|
+
#yf-globito {
|
|
84
|
+
position: fixed; bottom: 16px; right: 16px;
|
|
85
|
+
width: 56px; height: 56px; border-radius: 50%;
|
|
86
|
+
background: var(--indigo); color: white;
|
|
87
|
+
display: flex; align-items: center; justify-content: center;
|
|
88
|
+
font-family: "Noto Serif JP", serif; font-size: 26px;
|
|
89
|
+
box-shadow: 0 12px 36px rgba(0,0,0,0.18);
|
|
90
|
+
cursor: pointer; border: 0; z-index: 100;
|
|
91
|
+
transition: transform .12s ease;
|
|
92
|
+
}
|
|
93
|
+
#yf-globito:hover { transform: scale(1.05); }
|
|
94
|
+
|
|
95
|
+
/* ---- mini (ventana chica) ---- */
|
|
96
|
+
#yf-mini {
|
|
97
|
+
position: fixed; bottom: 16px; right: 16px;
|
|
98
|
+
width: 360px; height: 560px;
|
|
99
|
+
background: var(--bg-1); border: 1px solid var(--border);
|
|
100
|
+
border-radius: 12px; box-shadow: 0 12px 36px rgba(0,0,0,0.18);
|
|
101
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
102
|
+
z-index: 100;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* ---- full (ventana grande) ---- */
|
|
106
|
+
#yf-full {
|
|
107
|
+
position: fixed; inset: 16px;
|
|
108
|
+
background: rgba(26,26,26,0.35); backdrop-filter: blur(6px);
|
|
109
|
+
display: flex; align-items: stretch; justify-content: center;
|
|
110
|
+
z-index: 200;
|
|
111
|
+
}
|
|
112
|
+
#yf-full .panel {
|
|
113
|
+
display: flex; flex-direction: column;
|
|
114
|
+
width: 100%; max-width: 1200px;
|
|
115
|
+
background: var(--bg-1); border: 1px solid var(--border);
|
|
116
|
+
border-radius: 12px; overflow: hidden;
|
|
117
|
+
box-shadow: 0 12px 36px rgba(0,0,0,0.18);
|
|
118
|
+
}
|
|
119
|
+
#yf-full .body {
|
|
120
|
+
display: flex; flex: 1; min-height: 0;
|
|
121
|
+
}
|
|
122
|
+
#yf-full .body .chat-col {
|
|
123
|
+
width: 420px; flex-shrink: 0;
|
|
124
|
+
display: flex; flex-direction: column;
|
|
125
|
+
border-right: 1px solid var(--border);
|
|
126
|
+
}
|
|
127
|
+
#yf-full .body .pizarra {
|
|
128
|
+
flex: 1; min-width: 0; background: white;
|
|
129
|
+
display: flex; flex-direction: column;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
}
|
|
132
|
+
#yf-full .body .pizarra .pizarra-header {
|
|
133
|
+
padding: 10px 16px; border-bottom: 1px solid var(--border-subtle);
|
|
134
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
135
|
+
flex-shrink: 0;
|
|
136
|
+
}
|
|
137
|
+
#yf-full .body .pizarra .pizarra-header h3 {
|
|
138
|
+
margin: 0; font-size: 14px; font-weight: 600; color: var(--ink);
|
|
139
|
+
}
|
|
140
|
+
#yf-full .body .pizarra .pizarra-header .count {
|
|
141
|
+
font-size: 12px; color: var(--ink-muted);
|
|
142
|
+
}
|
|
143
|
+
#yf-full .body .pizarra .pizarra-body {
|
|
144
|
+
flex: 1; overflow-y: auto; padding: 16px;
|
|
145
|
+
}
|
|
146
|
+
#yf-full .body .pizarra .pizarra-empty {
|
|
147
|
+
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
|
148
|
+
height: 100%; padding: 32px 16px; text-align: center;
|
|
149
|
+
}
|
|
150
|
+
#yf-full .body .pizarra .pizarra-empty .kanji-big {
|
|
151
|
+
font-size: 96px; opacity: 0.4;
|
|
152
|
+
}
|
|
153
|
+
#yf-full .body .pizarra .pizarra-empty p {
|
|
154
|
+
color: var(--ink-muted); max-width: 440px;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/* ---- tool trace card ---- */
|
|
158
|
+
.trace-turn { margin-bottom: 18px; }
|
|
159
|
+
.trace-turn .turn-label {
|
|
160
|
+
font-size: 11px; color: var(--ink-muted);
|
|
161
|
+
text-transform: uppercase; letter-spacing: 0.5px;
|
|
162
|
+
margin-bottom: 6px;
|
|
163
|
+
}
|
|
164
|
+
.trace-round {
|
|
165
|
+
border: 1px solid var(--border);
|
|
166
|
+
border-left: 3px solid var(--indigo);
|
|
167
|
+
border-radius: 6px; padding: 10px 12px;
|
|
168
|
+
background: var(--bg-1);
|
|
169
|
+
margin-bottom: 8px;
|
|
170
|
+
}
|
|
171
|
+
.trace-round.err { border-left-color: var(--err); }
|
|
172
|
+
.trace-round .tool-name {
|
|
173
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
174
|
+
font-size: 13px; font-weight: 600; color: var(--ink);
|
|
175
|
+
display: flex; align-items: center; gap: 8px;
|
|
176
|
+
}
|
|
177
|
+
.trace-round .tool-name .badge {
|
|
178
|
+
font-size: 10px; padding: 1px 6px; border-radius: 3px;
|
|
179
|
+
background: var(--indigo); color: white; letter-spacing: 0.5px;
|
|
180
|
+
}
|
|
181
|
+
.trace-round.err .tool-name .badge {
|
|
182
|
+
background: var(--err);
|
|
183
|
+
}
|
|
184
|
+
/* V1.34 -- distinguish voice-dispatched rounds at a glance. */
|
|
185
|
+
.trace-round .tool-name .badge.voice {
|
|
186
|
+
background: var(--amber); color: var(--ink);
|
|
187
|
+
}
|
|
188
|
+
.trace-round .voice-pattern {
|
|
189
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
190
|
+
font-size: 10px; color: var(--ink-subtle);
|
|
191
|
+
margin-top: 4px; opacity: 0.85;
|
|
192
|
+
}
|
|
193
|
+
/* V1.37 -- drag-drop spec ingest overlay. */
|
|
194
|
+
.drop-overlay {
|
|
195
|
+
position: fixed; inset: 0;
|
|
196
|
+
background: rgba(79, 91, 135, 0.85); color: white;
|
|
197
|
+
display: none; align-items: center; justify-content: center;
|
|
198
|
+
z-index: 10000; pointer-events: none;
|
|
199
|
+
font-family: "Noto Serif JP", serif;
|
|
200
|
+
}
|
|
201
|
+
.drop-overlay.active { display: flex; }
|
|
202
|
+
.drop-overlay .inner {
|
|
203
|
+
border: 3px dashed white; border-radius: 12px;
|
|
204
|
+
padding: 32px 48px; text-align: center;
|
|
205
|
+
background: rgba(0, 0, 0, 0.15);
|
|
206
|
+
}
|
|
207
|
+
.drop-overlay .kanji { font-size: 56px; opacity: 0.85; }
|
|
208
|
+
.drop-overlay .title { font-size: 22px; margin: 12px 0 6px; }
|
|
209
|
+
.drop-overlay .hint { font-size: 13px; opacity: 0.8;
|
|
210
|
+
font-family: system-ui, -apple-system, sans-serif; }
|
|
211
|
+
.trace-round .tool-display {
|
|
212
|
+
font-size: 12px; color: var(--ink-muted); margin-top: 4px;
|
|
213
|
+
}
|
|
214
|
+
.trace-round .tool-args {
|
|
215
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
216
|
+
font-size: 11px; color: var(--ink-subtle); margin-top: 6px;
|
|
217
|
+
background: rgba(0,0,0,0.04); padding: 4px 6px; border-radius: 3px;
|
|
218
|
+
overflow-x: auto;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/* ---- shared header ---- */
|
|
222
|
+
.header {
|
|
223
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
224
|
+
background: var(--indigo); color: white;
|
|
225
|
+
padding: 10px 12px; flex-shrink: 0;
|
|
226
|
+
}
|
|
227
|
+
.header .brand { display: flex; gap: 8px; align-items: center; }
|
|
228
|
+
.header .brand .k { font-family: "Noto Serif JP", serif; font-size: 22px; line-height: 1; }
|
|
229
|
+
.header .brand .t { display: flex; flex-direction: column; line-height: 1.1; }
|
|
230
|
+
.header .brand .t .name { font-size: 14px; font-weight: 600; }
|
|
231
|
+
.header .brand .t .proj { font-size: 11px; opacity: 0.8; }
|
|
232
|
+
.header .actions { display: flex; gap: 4px; }
|
|
233
|
+
.header .actions button {
|
|
234
|
+
background: rgba(255,255,255,0.1); color: white;
|
|
235
|
+
border: 0; padding: 6px 8px; border-radius: 4px;
|
|
236
|
+
font-size: 12px; cursor: pointer;
|
|
237
|
+
}
|
|
238
|
+
.header .actions button:hover { background: rgba(255,255,255,0.2); }
|
|
239
|
+
|
|
240
|
+
/* ---- chat stream ---- */
|
|
241
|
+
.stream { flex: 1; overflow-y: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
|
|
242
|
+
.empty { color: var(--ink-muted); text-align: center; padding: 32px 16px; }
|
|
243
|
+
.empty .k { font-size: 56px; }
|
|
244
|
+
.bubble { max-width: 80%; padding: 9px 12px; border-radius: 14px; font-size: 14px; word-wrap: break-word; }
|
|
245
|
+
.bubble.user { align-self: flex-end;
|
|
246
|
+
background: var(--bubble-user); color: white; border-bottom-right-radius: 4px; }
|
|
247
|
+
.bubble.asst { align-self: flex-start;
|
|
248
|
+
background: var(--bubble-asst); color: var(--ink);
|
|
249
|
+
border: 1px solid var(--border); border-bottom-left-radius: 4px;
|
|
250
|
+
}
|
|
251
|
+
.bubble pre { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12px;
|
|
252
|
+
background: rgba(0,0,0,0.05); padding: 8px; border-radius: 6px; overflow-x: auto; margin: 4px 0; }
|
|
253
|
+
.bubble code { font-family: ui-monospace, monospace; background: rgba(0,0,0,0.05); padding: 1px 4px; border-radius: 3px; font-size: 12px; }
|
|
254
|
+
.thinking { display: flex; gap: 4px; }
|
|
255
|
+
.thinking span { width: 6px; height: 6px; border-radius: 50%; background: var(--indigo); opacity: 0.4; animation: pulse 1s infinite; }
|
|
256
|
+
.thinking span:nth-child(2) { animation-delay: 0.15s; }
|
|
257
|
+
.thinking span:nth-child(3) { animation-delay: 0.3s; }
|
|
258
|
+
@keyframes pulse { 0%,100% { opacity: 0.3; } 50% { opacity: 1; } }
|
|
259
|
+
|
|
260
|
+
/* ---- input bar ---- */
|
|
261
|
+
.bar { border-top: 1px solid var(--border-subtle); background: white;
|
|
262
|
+
padding: 8px; display: flex; gap: 6px; align-items: center; flex-shrink: 0; }
|
|
263
|
+
.bar input { flex: 1; padding: 8px 10px;
|
|
264
|
+
border: 1px solid var(--border); border-radius: 6px; font-size: 14px; font-family: inherit; }
|
|
265
|
+
.bar input:focus { outline: none; border-color: var(--indigo); box-shadow: 0 0 0 3px rgba(79,91,135,0.12); }
|
|
266
|
+
.bar .mic, .bar .send {
|
|
267
|
+
border: 0; border-radius: 50%; width: 36px; height: 36px;
|
|
268
|
+
display: flex; align-items: center; justify-content: center;
|
|
269
|
+
cursor: pointer; color: white;
|
|
270
|
+
}
|
|
271
|
+
.bar .mic { background: var(--indigo); cursor: pointer; transition: background .12s ease; }
|
|
272
|
+
.bar .mic:hover { background: #6573a4; }
|
|
273
|
+
.bar .mic.recording { background: var(--err); animation: rec-pulse 1.2s infinite; }
|
|
274
|
+
.bar .mic.processing { background: var(--warn); cursor: progress; }
|
|
275
|
+
.bar .mic:disabled { background: #aaa; cursor: not-allowed; }
|
|
276
|
+
@keyframes rec-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.7; } }
|
|
277
|
+
.bar .tts-toggle {
|
|
278
|
+
border: 0; background: rgba(0,0,0,0.06); color: var(--ink-muted);
|
|
279
|
+
border-radius: 50%; width: 32px; height: 32px;
|
|
280
|
+
display: flex; align-items: center; justify-content: center;
|
|
281
|
+
cursor: pointer; font-size: 14px;
|
|
282
|
+
}
|
|
283
|
+
.bar .tts-toggle.on { background: var(--indigo); color: white; }
|
|
284
|
+
.bar .send { background: var(--indigo); padding: 0 12px; width: auto; border-radius: 6px;
|
|
285
|
+
font-size: 13px; font-weight: 600; }
|
|
286
|
+
.bar .send:disabled { background: #aaa; cursor: not-allowed; }
|
|
287
|
+
|
|
288
|
+
/* ---- status row ---- */
|
|
289
|
+
.status { font-size: 11px; color: var(--ink-muted); padding: 4px 12px; text-align: center; flex-shrink: 0; }
|
|
290
|
+
.status.err { color: var(--err); }
|
|
291
|
+
|
|
292
|
+
/* ---- vault modal (V0.4) ---- */
|
|
293
|
+
#yf-vault-modal {
|
|
294
|
+
position: fixed; inset: 0;
|
|
295
|
+
background: rgba(26,26,26,0.55); backdrop-filter: blur(4px);
|
|
296
|
+
display: flex; align-items: center; justify-content: center;
|
|
297
|
+
z-index: 300;
|
|
298
|
+
}
|
|
299
|
+
#yf-vault-modal .vault-card {
|
|
300
|
+
width: 100%; max-width: 560px; max-height: 80vh;
|
|
301
|
+
background: var(--bg-1); border: 1px solid var(--border);
|
|
302
|
+
border-radius: 12px; box-shadow: 0 12px 36px rgba(0,0,0,0.22);
|
|
303
|
+
display: flex; flex-direction: column; overflow: hidden;
|
|
304
|
+
}
|
|
305
|
+
#yf-vault-modal .vault-card .v-header {
|
|
306
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
307
|
+
background: var(--indigo); color: white; padding: 10px 14px;
|
|
308
|
+
flex-shrink: 0;
|
|
309
|
+
}
|
|
310
|
+
#yf-vault-modal .vault-card .v-header h2 { font-size: 15px; margin: 0; font-weight: 600; }
|
|
311
|
+
#yf-vault-modal .vault-card .v-header button {
|
|
312
|
+
background: rgba(255,255,255,0.1); border: 0; color: white;
|
|
313
|
+
padding: 6px 10px; border-radius: 4px; cursor: pointer; font-size: 12px;
|
|
314
|
+
}
|
|
315
|
+
#yf-vault-modal .vault-card .v-tabs {
|
|
316
|
+
display: flex; border-bottom: 1px solid var(--border-subtle); background: white;
|
|
317
|
+
flex-shrink: 0;
|
|
318
|
+
}
|
|
319
|
+
#yf-vault-modal .vault-card .v-tab {
|
|
320
|
+
padding: 9px 16px; cursor: pointer; border: 0; background: transparent;
|
|
321
|
+
font-size: 13px; color: var(--ink-muted); border-bottom: 2px solid transparent;
|
|
322
|
+
}
|
|
323
|
+
#yf-vault-modal .vault-card .v-tab.active { color: var(--ink); border-bottom-color: var(--indigo); font-weight: 600; }
|
|
324
|
+
#yf-vault-modal .vault-card .v-body { flex: 1; overflow-y: auto; padding: 16px; }
|
|
325
|
+
.v-form label { display: block; font-size: 12px; color: var(--ink-muted); margin-bottom: 4px; }
|
|
326
|
+
.v-form input {
|
|
327
|
+
width: 100%; padding: 8px 10px; border: 1px solid var(--border);
|
|
328
|
+
border-radius: 6px; font-size: 14px; font-family: inherit; margin-bottom: 12px;
|
|
329
|
+
}
|
|
330
|
+
.v-form input:focus { outline: none; border-color: var(--indigo); box-shadow: 0 0 0 3px rgba(79,91,135,0.12); }
|
|
331
|
+
.v-form .hint { font-size: 11px; color: var(--ink-subtle); margin-top: -8px; margin-bottom: 12px; }
|
|
332
|
+
.v-form button.primary {
|
|
333
|
+
background: var(--indigo); color: white; border: 0;
|
|
334
|
+
padding: 9px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 600;
|
|
335
|
+
}
|
|
336
|
+
.v-form button.primary:disabled { background: #aaa; cursor: not-allowed; }
|
|
337
|
+
.v-msg { padding: 8px 12px; border-radius: 6px; font-size: 13px; margin-bottom: 12px; }
|
|
338
|
+
.v-msg.ok { background: rgba(63,168,94,0.12); color: var(--ok); border: 1px solid rgba(63,168,94,0.3); }
|
|
339
|
+
.v-msg.err { background: rgba(139,53,48,0.12); color: var(--err); border: 1px solid rgba(139,53,48,0.3); }
|
|
340
|
+
.v-list { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
341
|
+
.v-list th, .v-list td { padding: 6px 8px; text-align: left; border-bottom: 1px solid var(--border-subtle); }
|
|
342
|
+
.v-list th { background: rgba(0,0,0,0.04); font-weight: 600; color: var(--ink-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.4px; }
|
|
343
|
+
.v-list td.action { width: 140px; white-space: nowrap; }
|
|
344
|
+
.v-list td.action button { background: var(--err); color: white; border: 0; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 11px; margin-right: 4px; }
|
|
345
|
+
.v-list td.action button[data-vault-test] { background: var(--indigo); }
|
|
346
|
+
.v-list td.action button:disabled { background: #aaa; cursor: progress; }
|
|
347
|
+
.v-list .t-ok { color: var(--ok); font-weight: 600; font-size: 11px; }
|
|
348
|
+
.v-list .t-fail { color: var(--err); font-weight: 600; font-size: 11px; }
|
|
349
|
+
.v-list .t-pending { color: var(--ink-muted); font-size: 11px; }
|
|
350
|
+
.v-list .t-exp { color: var(--warn); font-weight: 600; font-size: 11px; margin-left: 4px; }
|
|
351
|
+
.v-empty { color: var(--ink-muted); text-align: center; padding: 24px 16px; font-size: 13px; }
|
|
352
|
+
|
|
353
|
+
/* Topbar keys button (added to existing header .actions). */
|
|
354
|
+
.header .actions .keys-btn { font-family: inherit; }
|
|
355
|
+
.header .actions .lang-btn { font-family: inherit; font-size: 18px; padding: 2px 8px; }
|
|
356
|
+
/* Language dropdown -- compact, anchored under the globe button. */
|
|
357
|
+
.yf-lang-dropdown {
|
|
358
|
+
position: absolute; top: 56px; right: 16px;
|
|
359
|
+
background: var(--bg-1); border: 1px solid var(--border);
|
|
360
|
+
border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
|
361
|
+
z-index: 9999; padding: 8px 0; min-width: 180px;
|
|
362
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
363
|
+
}
|
|
364
|
+
.yf-lang-dropdown.hidden { display: none; }
|
|
365
|
+
.yf-lang-dropdown .lang-item {
|
|
366
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
367
|
+
padding: 8px 16px; cursor: pointer; color: var(--ink);
|
|
368
|
+
font-size: 14px;
|
|
369
|
+
}
|
|
370
|
+
.yf-lang-dropdown .lang-item:hover { background: var(--bg-2); }
|
|
371
|
+
.yf-lang-dropdown .lang-item.active { color: var(--indigo); font-weight: 600; }
|
|
372
|
+
.yf-lang-dropdown .lang-item .code {
|
|
373
|
+
font-family: ui-monospace, "SF Mono", Menlo, monospace;
|
|
374
|
+
font-size: 11px; opacity: 0.7;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ---- hidden helper ---- */
|
|
378
|
+
.hidden { display: none !important; }
|
|
379
|
+
</style>
|
|
380
|
+
</head>
|
|
381
|
+
<body>
|
|
382
|
+
|
|
383
|
+
<button id="yf-globito" data-nac-id="yujin.panel.globito-trigger"
|
|
384
|
+
aria-label="${escapeHtml(tForLang(lang, 'panel.brand') + ' -- ' + tr.inputPlaceholder)}"
|
|
385
|
+
title="${escapeHtml(tr.inputPlaceholder)}">${escapeHtml(tr.brandKanji)}</button>
|
|
386
|
+
|
|
387
|
+
<div id="yf-mini" data-nac-id="yujin.panel.mini" class="hidden">
|
|
388
|
+
<div class="header">
|
|
389
|
+
<div class="brand">
|
|
390
|
+
<span class="k">${escapeHtml(tr.brandKanji)}</span>
|
|
391
|
+
<div class="t"><span class="name">Yujin Forge</span><span class="proj" id="proj-mini" data-nac-id="yujin.panel.project-mini"></span></div>
|
|
392
|
+
</div>
|
|
393
|
+
<div class="actions">
|
|
394
|
+
<button class="keys-btn" data-vault-open aria-label="${escapeHtml(tr.keysButton)}" title="${escapeHtml(tr.keysButton)}">${escapeHtml(tr.keysButton)}</button>
|
|
395
|
+
<button id="mini-full" data-nac-id="yujin.panel.mini-to-full" aria-label="${escapeHtml(tForLang(lang, 'panel.button.maximise'))}" title="${escapeHtml(tForLang(lang, 'panel.button.maximise'))}">⛶</button>
|
|
396
|
+
<button id="mini-close" data-nac-id="yujin.panel.mini-close" aria-label="${escapeHtml(tr.closeButton)}" title="${escapeHtml(tr.closeButton)}">✕</button>
|
|
397
|
+
</div>
|
|
398
|
+
</div>
|
|
399
|
+
<div id="stream-mini" data-nac-id="yujin.chat.stream-mini" class="stream"></div>
|
|
400
|
+
<div class="status" id="status-mini" data-nac-id="yujin.chat.status-mini"></div>
|
|
401
|
+
<form id="bar-mini" data-nac-id="yujin.chat.bar-mini" class="bar" onsubmit="return false">
|
|
402
|
+
<button type="button" class="mic" data-mic data-mic-btn aria-label="${escapeHtml(tr.micButton)}" title="${escapeHtml(tr.micButton)}">🎙</button>
|
|
403
|
+
<button type="button" class="tts-toggle" data-tts-toggle aria-label="${escapeHtml(tr.ttsButton)}" title="${escapeHtml(tr.ttsButton)}">🔊</button>
|
|
404
|
+
<input type="text" placeholder="${escapeHtml(tr.inputPlaceholder)}" aria-label="${escapeHtml(tr.inputPlaceholder)}" autocomplete="off">
|
|
405
|
+
<button type="submit" class="send">${escapeHtml(tr.sendButton)}</button>
|
|
406
|
+
</form>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<div id="yf-vault-modal" data-nac-id="yujin.vault.modal" class="hidden" role="dialog" aria-modal="true" aria-labelledby="vault-title">
|
|
410
|
+
<div class="vault-card">
|
|
411
|
+
<div class="v-header">
|
|
412
|
+
<h2 id="vault-title" data-nac-id="yujin.vault.title">${escapeHtml(tForLang(lang, 'vault.modal.title'))}</h2>
|
|
413
|
+
<button data-vault-close aria-label="${escapeHtml(tr.vaultClose)}">${escapeHtml(tr.closeButton)}</button>
|
|
414
|
+
</div>
|
|
415
|
+
<div class="v-tabs" role="tablist">
|
|
416
|
+
<button class="v-tab active" data-vault-tab="list" role="tab" aria-selected="true">${escapeHtml(tForLang(lang, 'vault.tab.list'))}</button>
|
|
417
|
+
<button class="v-tab" data-vault-tab="add" role="tab" aria-selected="false">${escapeHtml(tForLang(lang, 'vault.tab.add'))}</button>
|
|
418
|
+
</div>
|
|
419
|
+
<div class="v-body">
|
|
420
|
+
<div data-vault-pane="list" role="tabpanel" aria-labelledby="vault-title">
|
|
421
|
+
<div id="vault-msg-list" data-nac-id="yujin.vault.msg-list"></div>
|
|
422
|
+
<div id="vault-list-body" data-nac-id="yujin.vault.list-body">
|
|
423
|
+
<div class="v-empty">Loading...</div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
<div data-vault-pane="add" role="tabpanel" class="hidden" aria-labelledby="vault-title">
|
|
427
|
+
<div id="vault-msg-add" data-nac-id="yujin.vault.msg-add"></div>
|
|
428
|
+
<form id="vault-add-form" data-nac-id="yujin.vault.add-form" class="v-form" onsubmit="return false">
|
|
429
|
+
<label for="vault-slot-input">Slot name</label>
|
|
430
|
+
<input id="vault-slot-input" data-nac-id="yujin.vault.slot-input" type="text" required pattern="[a-z][a-z0-9_-]*"
|
|
431
|
+
autocomplete="off" autocapitalize="none" spellcheck="false"
|
|
432
|
+
placeholder="anthropic, whisper, elevenlabs, github_token, ..."
|
|
433
|
+
aria-describedby="slot-hint">
|
|
434
|
+
<div class="hint" id="slot-hint" data-nac-id="yujin.vault.slot-hint">Lowercase letters, digits, underscore, hyphen. Must start with a letter.</div>
|
|
435
|
+
<label for="vault-value-input">Plaintext value (masked)</label>
|
|
436
|
+
<input id="vault-value-input" data-nac-id="yujin.vault.value-input" type="password" required
|
|
437
|
+
autocomplete="new-password" autocapitalize="none" spellcheck="false"
|
|
438
|
+
placeholder="paste your secret here"
|
|
439
|
+
aria-describedby="value-hint">
|
|
440
|
+
<div class="hint" id="value-hint" data-nac-id="yujin.vault.value-hint">Stored encrypted at rest. Visible only while typing -- there is no peek path afterwards.</div>
|
|
441
|
+
<button type="submit" class="primary" id="vault-submit" data-nac-id="yujin.vault.submit">Save</button>
|
|
442
|
+
</form>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div id="yf-full" data-nac-id="yujin.panel.full" class="hidden">
|
|
449
|
+
<div class="panel">
|
|
450
|
+
<div class="header">
|
|
451
|
+
<div class="brand">
|
|
452
|
+
<span class="k">${escapeHtml(tr.brandKanji)}</span>
|
|
453
|
+
<div class="t"><span class="name">Yujin Forge</span><span class="proj" id="proj-full" data-nac-id="yujin.panel.project-full"></span></div>
|
|
454
|
+
</div>
|
|
455
|
+
<div class="actions">
|
|
456
|
+
<button class="lang-btn" data-lang-open aria-label="${escapeHtml(tr.languageButton)}" title="${escapeHtml(tr.languageButton)}">🌐</button>
|
|
457
|
+
<button class="keys-btn" data-vault-open aria-label="${escapeHtml(tr.keysButton)}" title="${escapeHtml(tr.keysButton)}">${escapeHtml(tr.keysButton)}</button>
|
|
458
|
+
<button id="full-mini" data-nac-id="yujin.panel.full-to-mini" aria-label="${escapeHtml(tr.minimiseButton)}" title="${escapeHtml(tr.minimiseButton)}">▭</button>
|
|
459
|
+
<button id="full-close" data-nac-id="yujin.panel.full-close" aria-label="${escapeHtml(tr.closeButton)}" title="${escapeHtml(tr.closeButton)}">✕</button>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
<div class="body">
|
|
463
|
+
<div class="chat-col">
|
|
464
|
+
<div id="stream-full" data-nac-id="yujin.chat.stream-full" class="stream"></div>
|
|
465
|
+
<div class="status" id="status-full" data-nac-id="yujin.chat.status-full"></div>
|
|
466
|
+
<form id="bar-full" data-nac-id="yujin.chat.bar-full" class="bar" onsubmit="return false">
|
|
467
|
+
<button type="button" class="mic" data-mic data-mic-btn aria-label="${escapeHtml(tr.micButton)}" title="${escapeHtml(tr.micButton)}">🎙</button>
|
|
468
|
+
<button type="button" class="tts-toggle" data-tts-toggle aria-label="${escapeHtml(tr.ttsButton)}" title="${escapeHtml(tr.ttsButton)}">🔊</button>
|
|
469
|
+
<input type="text" placeholder="${escapeHtml(tr.inputPlaceholder)}" aria-label="${escapeHtml(tr.inputPlaceholder)}" autocomplete="off">
|
|
470
|
+
<button type="submit" class="send">${escapeHtml(tr.sendButton)}</button>
|
|
471
|
+
</form>
|
|
472
|
+
</div>
|
|
473
|
+
<div class="pizarra">
|
|
474
|
+
<div class="pizarra-header">
|
|
475
|
+
<h3>Action trace</h3>
|
|
476
|
+
<span class="count" id="trace-count" data-nac-id="yujin.pizarra.trace-count">0 tool calls</span>
|
|
477
|
+
</div>
|
|
478
|
+
<div class="pizarra-body" id="pizarra-body" data-nac-id="yujin.pizarra.body">
|
|
479
|
+
<div class="pizarra-empty">
|
|
480
|
+
<div class="kanji-big">${escapeHtml(tForLang(lang, 'panel.trace.empty.kanji'))}</div>
|
|
481
|
+
<p>${escapeHtml(tForLang(lang, 'panel.trace.empty.body'))}</p>
|
|
482
|
+
</div>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
|
|
489
|
+
<!-- Fase F.3 language dropdown -->
|
|
490
|
+
<div id="yf-lang-dropdown" data-nac-id="yujin.lang.dropdown" class="yf-lang-dropdown hidden" role="menu" aria-label="Language"></div>
|
|
491
|
+
|
|
492
|
+
<!-- V1.37 drop overlay -->
|
|
493
|
+
<div id="yf-drop-overlay" data-nac-id="yujin.drop.overlay" class="drop-overlay">
|
|
494
|
+
<div class="inner">
|
|
495
|
+
<div class="kanji">巻</div>
|
|
496
|
+
<div class="title">${escapeHtml(tForLang(lang, 'panel.drop.title'))}</div>
|
|
497
|
+
<div class="hint">${escapeHtml(tForLang(lang, 'panel.drop.hint'))}</div>
|
|
498
|
+
</div>
|
|
499
|
+
</div>
|
|
500
|
+
|
|
501
|
+
<script>
|
|
502
|
+
const CONFIG = ${runtimeConfig};
|
|
503
|
+
const MODE_KEY = 'yf-chat-mode';
|
|
504
|
+
|
|
505
|
+
const $ = (sel) => document.querySelector(sel);
|
|
506
|
+
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
|
|
507
|
+
|
|
508
|
+
let state = {
|
|
509
|
+
mode: 'globito',
|
|
510
|
+
messages: [],
|
|
511
|
+
busy: false,
|
|
512
|
+
/* Per-turn array of { turnIndex, rounds[] } so the pizarra
|
|
513
|
+
can render the action trace cumulatively across the
|
|
514
|
+
conversation. */
|
|
515
|
+
toolHistory: [],
|
|
516
|
+
/* doc_id of the most-recently-opened reader session. Sent
|
|
517
|
+
on /api/voice/stt as X-Active-Doc-Id so the V1.31 intent
|
|
518
|
+
matcher can resolve commands that need a target ("siguiente",
|
|
519
|
+
"buscar X", etc). Updated on every forge.reader.open. */
|
|
520
|
+
activeReaderDocId: '',
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
function loadMode() {
|
|
524
|
+
const m = localStorage.getItem(MODE_KEY);
|
|
525
|
+
return (m === 'mini' || m === 'full' || m === 'globito') ? m : 'globito';
|
|
526
|
+
}
|
|
527
|
+
function saveMode(m) { try { localStorage.setItem(MODE_KEY, m); } catch (_) {} }
|
|
528
|
+
|
|
529
|
+
function showMode(mode) {
|
|
530
|
+
state.mode = mode;
|
|
531
|
+
saveMode(mode);
|
|
532
|
+
$('#yf-globito').classList.toggle('hidden', mode !== 'globito');
|
|
533
|
+
$('#yf-mini').classList.toggle('hidden', mode !== 'mini');
|
|
534
|
+
$('#yf-full').classList.toggle('hidden', mode !== 'full');
|
|
535
|
+
if (mode !== 'globito') {
|
|
536
|
+
setTimeout(() => $('#bar-' + (mode === 'full' ? 'full' : 'mini') + ' input').focus(), 30);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function setProj() {
|
|
541
|
+
const txt = CONFIG.projectName + ' ' + 'v' + CONFIG.version;
|
|
542
|
+
$('#proj-mini').textContent = txt;
|
|
543
|
+
$('#proj-full').textContent = txt;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function escapeHtml(s) {
|
|
547
|
+
return String(s)
|
|
548
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
549
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/** Tiny markdown subset for assistant bubbles. */
|
|
553
|
+
function renderMd(src) {
|
|
554
|
+
let out = escapeHtml(src);
|
|
555
|
+
out = out.replace(/\`\`\`([\\s\\S]*?)\`\`\`/g,
|
|
556
|
+
(_, code) => '<pre>' + code.trim() + '</pre>');
|
|
557
|
+
out = out.replace(/\`([^\`]+)\`/g, '<code>$1</code>');
|
|
558
|
+
out = out.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
|
|
559
|
+
out = out.replace(/\\n/g, '<br>');
|
|
560
|
+
return out;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function renderStream() {
|
|
564
|
+
for (const target of [$('#stream-mini'), $('#stream-full')]) {
|
|
565
|
+
target.innerHTML = '';
|
|
566
|
+
if (state.messages.length === 0) {
|
|
567
|
+
target.innerHTML = '<div class="empty"><div class="kanji k">'
|
|
568
|
+
+ escapeHtml(CONFIG.i18n.streamEmptyKanji)
|
|
569
|
+
+ '</div><p>'
|
|
570
|
+
+ escapeHtml(CONFIG.i18n.streamEmptyBody)
|
|
571
|
+
+ '</p></div>';
|
|
572
|
+
} else {
|
|
573
|
+
for (const m of state.messages) {
|
|
574
|
+
const div = document.createElement('div');
|
|
575
|
+
div.className = 'bubble ' + m.role;
|
|
576
|
+
if (m.role === 'assistant') div.innerHTML = renderMd(m.content);
|
|
577
|
+
else div.textContent = m.content;
|
|
578
|
+
target.appendChild(div);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
if (state.busy) {
|
|
582
|
+
const d = document.createElement('div');
|
|
583
|
+
d.className = 'bubble asst';
|
|
584
|
+
d.innerHTML = '<div class="thinking"><span></span><span></span><span></span></div>';
|
|
585
|
+
target.appendChild(d);
|
|
586
|
+
}
|
|
587
|
+
target.scrollTop = target.scrollHeight;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function renderTrace() {
|
|
592
|
+
const body = $('#pizarra-body');
|
|
593
|
+
const count = $('#trace-count');
|
|
594
|
+
if (!body || !count) return;
|
|
595
|
+
const totalCalls = state.toolHistory.reduce((s, t) => s + t.rounds.length, 0);
|
|
596
|
+
count.textContent = totalCalls + ' tool call' + (totalCalls === 1 ? '' : 's');
|
|
597
|
+
if (totalCalls === 0) {
|
|
598
|
+
body.innerHTML = '<div class="pizarra-empty">'
|
|
599
|
+
+ '<div class="kanji-big">' + escapeHtml(CONFIG.i18n.traceEmptyKanji) + '</div>'
|
|
600
|
+
+ '<p>' + escapeHtml(CONFIG.i18n.traceEmptyBody) + '</p>'
|
|
601
|
+
+ '</div>';
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const frags = [];
|
|
605
|
+
for (let i = 0; i < state.toolHistory.length; i++) {
|
|
606
|
+
const turn = state.toolHistory[i];
|
|
607
|
+
frags.push('<div class="trace-turn">');
|
|
608
|
+
frags.push('<div class="turn-label">Turn ' + (i + 1) + '</div>');
|
|
609
|
+
for (const r of turn.rounds) {
|
|
610
|
+
const errCls = r.is_error ? ' err' : '';
|
|
611
|
+
/* V1.34 -- voice-dispatched rounds get the VOZ badge.
|
|
612
|
+
Errors override (caller saw a failure regardless of
|
|
613
|
+
how it was dispatched). */
|
|
614
|
+
const isVoice = !!r.via_voice;
|
|
615
|
+
const badgeCls = isVoice && !r.is_error ? ' voice' : '';
|
|
616
|
+
const badge = r.is_error
|
|
617
|
+
? 'ERROR'
|
|
618
|
+
: (isVoice ? 'VOZ' : 'TOOL');
|
|
619
|
+
const args = r.input && Object.keys(r.input).length > 0
|
|
620
|
+
? JSON.stringify(r.input)
|
|
621
|
+
: '';
|
|
622
|
+
frags.push('<div class="trace-round' + errCls + '">');
|
|
623
|
+
frags.push('<div class="tool-name"><span class="badge' + badgeCls + '">' + badge + '</span>'
|
|
624
|
+
+ escapeHtml(String(r.tool || 'unknown')) + '</div>');
|
|
625
|
+
if (r.display) {
|
|
626
|
+
frags.push('<div class="tool-display">' + escapeHtml(String(r.display)) + '</div>');
|
|
627
|
+
}
|
|
628
|
+
if (isVoice && r.voice_utterance) {
|
|
629
|
+
frags.push('<div class="voice-pattern">"'
|
|
630
|
+
+ escapeHtml(String(r.voice_utterance))
|
|
631
|
+
+ '" -> ' + escapeHtml(String(r.voice_pattern || 'unknown'))
|
|
632
|
+
+ '</div>');
|
|
633
|
+
}
|
|
634
|
+
if (args) {
|
|
635
|
+
frags.push('<div class="tool-args">' + escapeHtml(args) + '</div>');
|
|
636
|
+
}
|
|
637
|
+
frags.push('</div>');
|
|
638
|
+
}
|
|
639
|
+
frags.push('</div>');
|
|
640
|
+
}
|
|
641
|
+
body.innerHTML = frags.join('');
|
|
642
|
+
body.scrollTop = body.scrollHeight;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function setStatus(text, isErr) {
|
|
646
|
+
for (const id of ['#status-mini', '#status-full']) {
|
|
647
|
+
const el = $(id);
|
|
648
|
+
el.textContent = text || '';
|
|
649
|
+
el.classList.toggle('err', !!isErr);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function setBusy(b) {
|
|
654
|
+
state.busy = b;
|
|
655
|
+
for (const sel of ['#bar-mini .send', '#bar-full .send', '#bar-mini input', '#bar-full input']) {
|
|
656
|
+
const el = $(sel);
|
|
657
|
+
if (el) el.disabled = b;
|
|
658
|
+
}
|
|
659
|
+
renderStream();
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function send(text) {
|
|
663
|
+
text = text.trim();
|
|
664
|
+
if (!text || state.busy) return;
|
|
665
|
+
state.messages.push({ role: 'user', content: text });
|
|
666
|
+
renderStream();
|
|
667
|
+
setBusy(true);
|
|
668
|
+
setStatus('');
|
|
669
|
+
try {
|
|
670
|
+
const res = await fetch('/api/chat', {
|
|
671
|
+
method: 'POST',
|
|
672
|
+
headers: { 'content-type': 'application/json' },
|
|
673
|
+
body: JSON.stringify({ messages: state.messages }),
|
|
674
|
+
});
|
|
675
|
+
const data = await res.json();
|
|
676
|
+
if (!res.ok || !data.ok) {
|
|
677
|
+
setStatus(data.error || ('HTTP ' + res.status), true);
|
|
678
|
+
// Drop the unanswered user message so the user can retry.
|
|
679
|
+
state.messages.pop();
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
state.messages.push({ role: 'assistant', content: data.message.text });
|
|
683
|
+
if (Array.isArray(data.tool_rounds) && data.tool_rounds.length > 0) {
|
|
684
|
+
state.toolHistory.push({
|
|
685
|
+
turnIndex: state.messages.length - 1,
|
|
686
|
+
rounds: data.tool_rounds,
|
|
687
|
+
});
|
|
688
|
+
renderTrace();
|
|
689
|
+
}
|
|
690
|
+
} catch (err) {
|
|
691
|
+
setStatus(String(err && err.message ? err.message : err), true);
|
|
692
|
+
state.messages.pop();
|
|
693
|
+
} finally {
|
|
694
|
+
setBusy(false);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function attachBar(form) {
|
|
699
|
+
const input = form.querySelector('input');
|
|
700
|
+
form.addEventListener('submit', (e) => {
|
|
701
|
+
e.preventDefault();
|
|
702
|
+
const v = input.value;
|
|
703
|
+
input.value = '';
|
|
704
|
+
/* SQ 0.8 slash command: /modo didactico|tecnico (alias EN
|
|
705
|
+
/mode). Handled client-side -- no LLM round-trip.
|
|
706
|
+
isSlashCommand is SYNCHRONOUS; handleSlashCommand runs
|
|
707
|
+
fire-and-forget (it does I/O but never blocks the next
|
|
708
|
+
turn). */
|
|
709
|
+
if (isSlashCommand(v)) {
|
|
710
|
+
handleSlashCommand(v).catch(() => undefined);
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
send(v);
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isSlashCommand(raw) {
|
|
718
|
+
return typeof raw === 'string' && raw.trim().startsWith('/');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function handleSlashCommand(raw) {
|
|
722
|
+
if (!isSlashCommand(raw)) return false;
|
|
723
|
+
const text = raw.trim();
|
|
724
|
+
const [cmd, ...rest] = text.slice(1).split(/\s+/);
|
|
725
|
+
const lowerCmd = (cmd || '').toLowerCase();
|
|
726
|
+
if (lowerCmd === 'modo' || lowerCmd === 'mode') {
|
|
727
|
+
const arg = (rest[0] || '').toLowerCase();
|
|
728
|
+
const map = {
|
|
729
|
+
didactic: 'didactico', didactico: 'didactico', d: 'didactico',
|
|
730
|
+
technical: 'tecnico', tecnico: 'tecnico', tecnica: 'tecnico', t: 'tecnico',
|
|
731
|
+
};
|
|
732
|
+
const target = map[arg];
|
|
733
|
+
if (!target) {
|
|
734
|
+
setStatus('Modos disponibles: didactico, tecnico. Uso: /modo tecnico', true);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
try {
|
|
738
|
+
const r = await fetch('/api/forge/mode', {
|
|
739
|
+
method: 'POST',
|
|
740
|
+
headers: { 'content-type': 'application/json' },
|
|
741
|
+
body: JSON.stringify({ mode: target }),
|
|
742
|
+
});
|
|
743
|
+
const data = await r.json();
|
|
744
|
+
if (!data.ok) {
|
|
745
|
+
setStatus('No pude cambiar el modo: ' + (data.error || 'unknown'), true);
|
|
746
|
+
return true;
|
|
747
|
+
}
|
|
748
|
+
try { document.cookie = 'yf-mode=' + target + '; path=/; max-age=' + (60*60*24*365) + '; SameSite=Lax'; } catch (_) {}
|
|
749
|
+
setStatus('Modo cambiado a ' + target + '. El proximo turno ya usa este modo.', false);
|
|
750
|
+
} catch (err) {
|
|
751
|
+
setStatus('Error: ' + String(err && err.message ? err.message : err), true);
|
|
752
|
+
}
|
|
753
|
+
return true;
|
|
754
|
+
}
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/* ---- Vault modal (V0.4) ---- */
|
|
759
|
+
function vaultOpen() {
|
|
760
|
+
$('#yf-vault-modal').classList.remove('hidden');
|
|
761
|
+
vaultShowTab('list');
|
|
762
|
+
vaultRefreshList();
|
|
763
|
+
setTimeout(() => $('#vault-slot-input').focus(), 30);
|
|
764
|
+
}
|
|
765
|
+
function vaultClose() {
|
|
766
|
+
$('#yf-vault-modal').classList.add('hidden');
|
|
767
|
+
}
|
|
768
|
+
function vaultShowTab(name) {
|
|
769
|
+
$$('.v-tab').forEach((b) => {
|
|
770
|
+
const active = b.getAttribute('data-vault-tab') === name;
|
|
771
|
+
b.classList.toggle('active', active);
|
|
772
|
+
b.setAttribute('aria-selected', active ? 'true' : 'false');
|
|
773
|
+
});
|
|
774
|
+
$$('[data-vault-pane]').forEach((p) => {
|
|
775
|
+
p.classList.toggle('hidden', p.getAttribute('data-vault-pane') !== name);
|
|
776
|
+
});
|
|
777
|
+
/* Clear stale messages on tab switch. */
|
|
778
|
+
$('#vault-msg-list').innerHTML = '';
|
|
779
|
+
$('#vault-msg-add').innerHTML = '';
|
|
780
|
+
}
|
|
781
|
+
function vaultMsg(target, text, kind) {
|
|
782
|
+
const el = $(target);
|
|
783
|
+
el.innerHTML = '<div class="v-msg ' + (kind || 'ok') + '">' + escapeHtml(text) + '</div>';
|
|
784
|
+
}
|
|
785
|
+
async function vaultRefreshList() {
|
|
786
|
+
const body = $('#vault-list-body');
|
|
787
|
+
body.innerHTML = '<div class="v-empty">Loading...</div>';
|
|
788
|
+
try {
|
|
789
|
+
const r = await fetch('/api/vault/list');
|
|
790
|
+
const data = await r.json();
|
|
791
|
+
if (!data.ok) {
|
|
792
|
+
body.innerHTML = '<div class="v-msg err">' + escapeHtml(data.error || 'failed') + '</div>';
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (!data.slots || data.slots.length === 0) {
|
|
796
|
+
body.innerHTML = '<div class="v-empty">No slots configured. Add one in the next tab.</div>';
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const rows = data.slots.map((s) => {
|
|
800
|
+
let testCell = '<span class="t-pending" title="not tested yet">--</span>';
|
|
801
|
+
if (s.last_test_ok === true) testCell = '<span class="t-ok" title="tested ok at ' + escapeHtml(s.last_test_at || '') + '">ok</span>';
|
|
802
|
+
else if (s.last_test_ok === false) testCell = '<span class="t-fail" title="tested fail at ' + escapeHtml(s.last_test_at || '') + '">fail</span>';
|
|
803
|
+
const expiryCell = s.expiry ? '<span class="t-exp" title="expires ' + escapeHtml(s.expiry) + '">exp</span>' : '';
|
|
804
|
+
return '<tr>'
|
|
805
|
+
+ '<td>' + escapeHtml(s.name) + '</td>'
|
|
806
|
+
+ '<td><code>' + escapeHtml(s.kind || 'custom') + '</code></td>'
|
|
807
|
+
+ '<td><code>' + escapeHtml(s.prefix) + '</code></td>'
|
|
808
|
+
+ '<td>' + s.length + '</td>'
|
|
809
|
+
+ '<td>' + testCell + ' ' + expiryCell + '</td>'
|
|
810
|
+
+ '<td>' + escapeHtml(s.updated_at.replace('T', ' ').slice(0, 19)) + '</td>'
|
|
811
|
+
+ '<td class="action">'
|
|
812
|
+
+ '<button data-vault-test="' + escapeHtml(s.name) + '" aria-label="Test slot ' + escapeHtml(s.name) + '">Test</button> '
|
|
813
|
+
+ '<button data-vault-remove="' + escapeHtml(s.name) + '" aria-label="Remove slot ' + escapeHtml(s.name) + '">Remove</button>'
|
|
814
|
+
+ '</td>'
|
|
815
|
+
+ '</tr>';
|
|
816
|
+
}).join('');
|
|
817
|
+
body.innerHTML =
|
|
818
|
+
'<table class="v-list">'
|
|
819
|
+
+ '<thead><tr><th>Name</th><th>Kind</th><th>Prefix</th><th>Len</th><th>Test</th><th>Updated</th><th></th></tr></thead>'
|
|
820
|
+
+ '<tbody>' + rows + '</tbody>'
|
|
821
|
+
+ '</table>';
|
|
822
|
+
/* Attach handlers to the new buttons. */
|
|
823
|
+
$$('[data-vault-remove]').forEach((btn) => {
|
|
824
|
+
btn.addEventListener('click', () => vaultRemove(btn.getAttribute('data-vault-remove')));
|
|
825
|
+
});
|
|
826
|
+
$$('[data-vault-test]').forEach((btn) => {
|
|
827
|
+
btn.addEventListener('click', () => vaultTest(btn.getAttribute('data-vault-test'), btn));
|
|
828
|
+
});
|
|
829
|
+
} catch (err) {
|
|
830
|
+
body.innerHTML = '<div class="v-msg err">' + escapeHtml(String(err && err.message ? err.message : err)) + '</div>';
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
async function vaultAdd() {
|
|
834
|
+
const slot = $('#vault-slot-input').value.trim();
|
|
835
|
+
const plaintext = $('#vault-value-input').value;
|
|
836
|
+
if (!slot || !plaintext) {
|
|
837
|
+
vaultMsg('#vault-msg-add', 'Slot name and plaintext both required.', 'err');
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
$('#vault-submit').disabled = true;
|
|
841
|
+
try {
|
|
842
|
+
const r = await fetch('/api/vault/set', {
|
|
843
|
+
method: 'POST',
|
|
844
|
+
headers: { 'content-type': 'application/json' },
|
|
845
|
+
body: JSON.stringify({ slot, plaintext }),
|
|
846
|
+
});
|
|
847
|
+
const data = await r.json();
|
|
848
|
+
if (!data.ok) {
|
|
849
|
+
vaultMsg('#vault-msg-add', data.error || 'failed', 'err');
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
/* Clear the value field immediately so it cannot be re-read
|
|
853
|
+
from the DOM. Slot name kept so the user sees what they
|
|
854
|
+
just stored. */
|
|
855
|
+
$('#vault-value-input').value = '';
|
|
856
|
+
vaultMsg(
|
|
857
|
+
'#vault-msg-add',
|
|
858
|
+
'Slot "' + data.slot.name + '" saved. Prefix: ' + data.slot.prefix
|
|
859
|
+
+ ' -- length: ' + data.slot.length + ' chars. The plaintext is now encrypted at rest.',
|
|
860
|
+
'ok',
|
|
861
|
+
);
|
|
862
|
+
/* Pre-refresh the list so the next tab switch is instant. */
|
|
863
|
+
vaultRefreshList();
|
|
864
|
+
} catch (err) {
|
|
865
|
+
vaultMsg('#vault-msg-add', String(err && err.message ? err.message : err), 'err');
|
|
866
|
+
} finally {
|
|
867
|
+
$('#vault-submit').disabled = false;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
async function vaultRemove(slot) {
|
|
871
|
+
if (!confirm('Remove slot "' + slot + '"? This cannot be undone.')) return;
|
|
872
|
+
try {
|
|
873
|
+
const r = await fetch('/api/vault/remove', {
|
|
874
|
+
method: 'POST',
|
|
875
|
+
headers: { 'content-type': 'application/json' },
|
|
876
|
+
body: JSON.stringify({ slot }),
|
|
877
|
+
});
|
|
878
|
+
const data = await r.json();
|
|
879
|
+
if (!data.ok) {
|
|
880
|
+
vaultMsg('#vault-msg-list', data.error || 'failed', 'err');
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
vaultMsg('#vault-msg-list', 'Slot "' + slot + '" removed.', 'ok');
|
|
884
|
+
vaultRefreshList();
|
|
885
|
+
} catch (err) {
|
|
886
|
+
vaultMsg('#vault-msg-list', String(err && err.message ? err.message : err), 'err');
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async function vaultTest(slot, button) {
|
|
890
|
+
if (button) {
|
|
891
|
+
button.disabled = true;
|
|
892
|
+
button.textContent = 'Testing...';
|
|
893
|
+
}
|
|
894
|
+
try {
|
|
895
|
+
const r = await fetch('/api/vault/test/' + encodeURIComponent(slot), {
|
|
896
|
+
method: 'POST',
|
|
897
|
+
headers: { 'content-type': 'application/json' },
|
|
898
|
+
body: JSON.stringify({ timeout_ms: 5000 }),
|
|
899
|
+
});
|
|
900
|
+
const data = await r.json();
|
|
901
|
+
if (!data.ok) {
|
|
902
|
+
vaultMsg('#vault-msg-list', data.error || 'test failed', 'err');
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
const res = data.result;
|
|
906
|
+
const label = res.ok ? 'ok' : 'fail';
|
|
907
|
+
const detail = res.detail ? ' (' + res.detail + ')' : '';
|
|
908
|
+
vaultMsg('#vault-msg-list',
|
|
909
|
+
'Test ' + slot + ': ' + label + ' -- status=' + res.status + ', ' + res.latency_ms + 'ms' + detail,
|
|
910
|
+
res.ok ? 'ok' : 'err');
|
|
911
|
+
vaultRefreshList();
|
|
912
|
+
} catch (err) {
|
|
913
|
+
vaultMsg('#vault-msg-list', String(err && err.message ? err.message : err), 'err');
|
|
914
|
+
} finally {
|
|
915
|
+
if (button) {
|
|
916
|
+
button.disabled = false;
|
|
917
|
+
button.textContent = 'Test';
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/* ---- Voice integration (V1.6) ---- */
|
|
923
|
+
const TTS_TOGGLE_KEY = 'yf-tts-replies-on';
|
|
924
|
+
let mediaRecorder = null;
|
|
925
|
+
let recordChunks = [];
|
|
926
|
+
let recordingPanelButton = null;
|
|
927
|
+
|
|
928
|
+
function ttsRepliesEnabled() {
|
|
929
|
+
try {
|
|
930
|
+
return localStorage.getItem(TTS_TOGGLE_KEY) === '1';
|
|
931
|
+
} catch (_) { return false; }
|
|
932
|
+
}
|
|
933
|
+
function setTtsReplies(on) {
|
|
934
|
+
try { localStorage.setItem(TTS_TOGGLE_KEY, on ? '1' : '0'); } catch (_) {}
|
|
935
|
+
updateTtsToggleButtons();
|
|
936
|
+
}
|
|
937
|
+
function updateTtsToggleButtons() {
|
|
938
|
+
const on = ttsRepliesEnabled();
|
|
939
|
+
$$('[data-tts-toggle]').forEach((b) => {
|
|
940
|
+
b.classList.toggle('on', on);
|
|
941
|
+
b.setAttribute('aria-pressed', on ? 'true' : 'false');
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
function setMicButtonsState(state) {
|
|
946
|
+
/* state: 'idle' | 'recording' | 'processing' */
|
|
947
|
+
$$('[data-mic-btn]').forEach((b) => {
|
|
948
|
+
b.classList.toggle('recording', state === 'recording');
|
|
949
|
+
b.classList.toggle('processing', state === 'processing');
|
|
950
|
+
b.disabled = state === 'processing';
|
|
951
|
+
b.setAttribute('aria-label',
|
|
952
|
+
state === 'recording' ? CONFIG.i18n.micButtonRec
|
|
953
|
+
: state === 'processing' ? CONFIG.i18n.micButtonRec
|
|
954
|
+
: CONFIG.i18n.micButton);
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
async function micToggle(button) {
|
|
959
|
+
if (mediaRecorder && mediaRecorder.state === 'recording') {
|
|
960
|
+
/* Stop -> onstop handler dispatches transcription. */
|
|
961
|
+
mediaRecorder.stop();
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
965
|
+
setStatus('Your browser does not support microphone access.', true);
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
try {
|
|
969
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
970
|
+
/* Pick the best mime the browser supports. WebM/Opus is the
|
|
971
|
+
universal choice in Chromium + Firefox; Safari prefers
|
|
972
|
+
mp4/aac. Forge accepts both. */
|
|
973
|
+
let mimeType = 'audio/webm';
|
|
974
|
+
let serverFormat = 'webm';
|
|
975
|
+
if (typeof MediaRecorder !== 'undefined' && !MediaRecorder.isTypeSupported(mimeType)) {
|
|
976
|
+
if (MediaRecorder.isTypeSupported('audio/mp4')) {
|
|
977
|
+
mimeType = 'audio/mp4'; serverFormat = 'mp4';
|
|
978
|
+
} else if (MediaRecorder.isTypeSupported('audio/ogg')) {
|
|
979
|
+
mimeType = 'audio/ogg'; serverFormat = 'ogg';
|
|
980
|
+
} else {
|
|
981
|
+
mimeType = ''; serverFormat = 'webm'; /* let browser pick */
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
recordChunks = [];
|
|
985
|
+
recordingPanelButton = button;
|
|
986
|
+
mediaRecorder = mimeType
|
|
987
|
+
? new MediaRecorder(stream, { mimeType })
|
|
988
|
+
: new MediaRecorder(stream);
|
|
989
|
+
mediaRecorder.ondataavailable = (e) => {
|
|
990
|
+
if (e.data && e.data.size > 0) recordChunks.push(e.data);
|
|
991
|
+
};
|
|
992
|
+
mediaRecorder.onstop = async () => {
|
|
993
|
+
stream.getTracks().forEach((t) => t.stop());
|
|
994
|
+
const blob = new Blob(recordChunks, { type: mimeType || 'audio/webm' });
|
|
995
|
+
mediaRecorder = null;
|
|
996
|
+
if (blob.size === 0) {
|
|
997
|
+
setMicButtonsState('idle');
|
|
998
|
+
setStatus('No audio captured. Try again.', true);
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
setMicButtonsState('processing');
|
|
1002
|
+
try {
|
|
1003
|
+
const buf = await blob.arrayBuffer();
|
|
1004
|
+
const headers = {
|
|
1005
|
+
'content-type': 'application/octet-stream',
|
|
1006
|
+
'x-audio-format': serverFormat,
|
|
1007
|
+
};
|
|
1008
|
+
/* V1.32 -- carry the active reader doc_id so the
|
|
1009
|
+
matcher can resolve commands like "siguiente" /
|
|
1010
|
+
"buscar X" against the currently-open document. */
|
|
1011
|
+
if (state.activeReaderDocId) {
|
|
1012
|
+
headers['x-active-doc-id'] = state.activeReaderDocId;
|
|
1013
|
+
}
|
|
1014
|
+
const r = await fetch('/api/voice/stt', {
|
|
1015
|
+
method: 'POST',
|
|
1016
|
+
headers,
|
|
1017
|
+
body: buf,
|
|
1018
|
+
});
|
|
1019
|
+
const data = await r.json();
|
|
1020
|
+
if (!r.ok || !data.ok) {
|
|
1021
|
+
setStatus(data.error || ('HTTP ' + r.status), true);
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
const text = (data.transcript && data.transcript.text) || '';
|
|
1025
|
+
if (!text) {
|
|
1026
|
+
setStatus('Did not catch that -- try again.', true);
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
/* V1.32 -- if the matcher recognised a reader command,
|
|
1030
|
+
dispatch it directly (no Claude round-trip). Otherwise
|
|
1031
|
+
the transcript flows into the chat input as before. */
|
|
1032
|
+
if (data.matched_intent && data.matched_intent.tool) {
|
|
1033
|
+
await dispatchReaderIntent(text, data.matched_intent);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
/* Inject the transcript into the active input + submit. */
|
|
1037
|
+
const inputSel = state.mode === 'full' ? '#bar-full input' : '#bar-mini input';
|
|
1038
|
+
const input = $(inputSel);
|
|
1039
|
+
if (input) {
|
|
1040
|
+
input.value = text;
|
|
1041
|
+
send(text);
|
|
1042
|
+
input.value = '';
|
|
1043
|
+
}
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
setStatus(String(err && err.message ? err.message : err), true);
|
|
1046
|
+
} finally {
|
|
1047
|
+
setMicButtonsState('idle');
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
mediaRecorder.start();
|
|
1051
|
+
setMicButtonsState('recording');
|
|
1052
|
+
setStatus('');
|
|
1053
|
+
} catch (err) {
|
|
1054
|
+
setStatus('Microphone permission denied or unavailable: '
|
|
1055
|
+
+ String(err && err.message ? err.message : err), true);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
/**
|
|
1060
|
+
* V1.32 -- dispatch a matched reader intent directly via the
|
|
1061
|
+
* server's whitelisted /api/forge/tool endpoint. Bypasses the
|
|
1062
|
+
* Claude chat loop for low-latency voice navigation.
|
|
1063
|
+
*
|
|
1064
|
+
* The matcher (V1.29) only resolves reader.* tools and the
|
|
1065
|
+
* dispatch endpoint refuses anything outside that allow-list,
|
|
1066
|
+
* so this codepath cannot fire write-class actions.
|
|
1067
|
+
*
|
|
1068
|
+
* Inputs:
|
|
1069
|
+
* utterance the raw STT transcript (already normalised on the
|
|
1070
|
+
* server for the matcher; we render the raw form
|
|
1071
|
+
* into the conversation so the user sees what was
|
|
1072
|
+
* heard).
|
|
1073
|
+
* intent { tool, args, pattern, utterance_normalised }
|
|
1074
|
+
* from /api/voice/stt.
|
|
1075
|
+
*/
|
|
1076
|
+
async function dispatchReaderIntent(utterance, intent) {
|
|
1077
|
+
/* Show the user what was heard + which tool fired, so the
|
|
1078
|
+
voice command stays auditable. */
|
|
1079
|
+
state.messages.push({ role: 'user', content: utterance });
|
|
1080
|
+
renderStream();
|
|
1081
|
+
|
|
1082
|
+
try {
|
|
1083
|
+
const r = await fetch('/api/forge/tool', {
|
|
1084
|
+
method: 'POST',
|
|
1085
|
+
headers: { 'content-type': 'application/json' },
|
|
1086
|
+
body: JSON.stringify({ tool: intent.tool, args: intent.args }),
|
|
1087
|
+
});
|
|
1088
|
+
const data = await r.json();
|
|
1089
|
+
if (!r.ok || !data.ok) {
|
|
1090
|
+
setStatus(data.error || ('HTTP ' + r.status), true);
|
|
1091
|
+
state.messages.pop();
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
/* Update the active reader doc on a successful open so
|
|
1095
|
+
subsequent voice commands ("siguiente", etc) target it. */
|
|
1096
|
+
if (intent.tool === 'forge.reader.open' && !data.is_error) {
|
|
1097
|
+
const docId = data.result && data.result.doc_id;
|
|
1098
|
+
if (docId) state.activeReaderDocId = String(docId);
|
|
1099
|
+
}
|
|
1100
|
+
/* Record this as a tool round so the pizarra trace shows
|
|
1101
|
+
the dispatch alongside any chat-loop rounds. V1.34 --
|
|
1102
|
+
mark via_voice + carry the matched pattern slug so the
|
|
1103
|
+
trace renders the VOZ badge. */
|
|
1104
|
+
state.toolHistory.push({
|
|
1105
|
+
turnIndex: state.messages.length - 1,
|
|
1106
|
+
rounds: [{
|
|
1107
|
+
tool: intent.tool,
|
|
1108
|
+
input: intent.args,
|
|
1109
|
+
display: data.result && data.result.display,
|
|
1110
|
+
is_error: data.is_error,
|
|
1111
|
+
via_voice: true,
|
|
1112
|
+
voice_pattern: intent.pattern,
|
|
1113
|
+
voice_utterance: utterance,
|
|
1114
|
+
}],
|
|
1115
|
+
});
|
|
1116
|
+
/* Pick a spoken-friendly text out of the tool result for
|
|
1117
|
+
TTS playback. Reader tools shape their results with a
|
|
1118
|
+
text field for next_block and similar; we prefer that
|
|
1119
|
+
when present, then first_text, then a generic fallback. */
|
|
1120
|
+
const spoken =
|
|
1121
|
+
(data.result && typeof data.result.text === 'string' && data.result.text)
|
|
1122
|
+
|| (data.result && typeof data.result.first_text === 'string' && data.result.first_text)
|
|
1123
|
+
|| ('Action ' + intent.tool + ' completed.');
|
|
1124
|
+
state.messages.push({ role: 'assistant', content: spoken });
|
|
1125
|
+
renderStream();
|
|
1126
|
+
renderTrace();
|
|
1127
|
+
void playTtsForText(spoken);
|
|
1128
|
+
} catch (err) {
|
|
1129
|
+
setStatus(String(err && err.message ? err.message : err), true);
|
|
1130
|
+
state.messages.pop();
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
/* ---- TTS chunker (V1.24) ----
|
|
1135
|
+
Inlined from src/voice/chunker.ts. The panel runs as inline
|
|
1136
|
+
HTML+JS so it cannot import the canonical module at runtime;
|
|
1137
|
+
the canonical source has 16 unit tests that lock the
|
|
1138
|
+
contract. Keep these two implementations in sync (search for
|
|
1139
|
+
"DEFAULT_MAX_LEN" in both files when editing). */
|
|
1140
|
+
const _CHUNK_DEFAULT_MAX_LEN = 300;
|
|
1141
|
+
const _CHUNK_DEFAULT_SEMI_BREAK = 80;
|
|
1142
|
+
const _CHUNK_MULTI_LETTER_ABBR = new Set([
|
|
1143
|
+
'sr','sra','srta','dr','dra','prof','mr','mrs','ms',
|
|
1144
|
+
'st','ave','av','no','num','pag','pg','vol','ed',
|
|
1145
|
+
'cap','fig','ej','etc','aprox','lic',
|
|
1146
|
+
'js','ts','py','sh','md','html','css','cli',
|
|
1147
|
+
'api','rfc','iso','utc','utf','cpu','gpu','ram',
|
|
1148
|
+
]);
|
|
1149
|
+
function _chunkIsAbbrevBefore(text, dotPos) {
|
|
1150
|
+
let start = dotPos - 1;
|
|
1151
|
+
while (start >= 0 && /[a-zA-Z]/.test(text.charAt(start))) start -= 1;
|
|
1152
|
+
start += 1;
|
|
1153
|
+
if (start >= dotPos) return false;
|
|
1154
|
+
const word = text.slice(start, dotPos);
|
|
1155
|
+
if (word.length === 1) return /[A-Z]/.test(word);
|
|
1156
|
+
return _CHUNK_MULTI_LETTER_ABBR.has(word.toLowerCase());
|
|
1157
|
+
}
|
|
1158
|
+
function splitIntoTtsChunks(text, opts) {
|
|
1159
|
+
opts = opts || {};
|
|
1160
|
+
const maxLen = Math.max(40, opts.maxLen || _CHUNK_DEFAULT_MAX_LEN);
|
|
1161
|
+
const semiBreak = Math.max(40, opts.semicolonBreakAtLen || _CHUNK_DEFAULT_SEMI_BREAK);
|
|
1162
|
+
const trimmed = String(text || '').replace(/\s+/g, ' ').trim();
|
|
1163
|
+
if (!trimmed) return [];
|
|
1164
|
+
const chunks = [];
|
|
1165
|
+
let buf = '';
|
|
1166
|
+
let i = 0;
|
|
1167
|
+
function flush() {
|
|
1168
|
+
const t = buf.trim();
|
|
1169
|
+
if (t) chunks.push(t);
|
|
1170
|
+
buf = '';
|
|
1171
|
+
}
|
|
1172
|
+
function flushAtMaxLen() {
|
|
1173
|
+
while (buf.length > maxLen) {
|
|
1174
|
+
let cut = buf.lastIndexOf(' ', maxLen);
|
|
1175
|
+
if (cut < maxLen / 2) cut = maxLen;
|
|
1176
|
+
const piece = buf.slice(0, cut).trim();
|
|
1177
|
+
if (piece) chunks.push(piece);
|
|
1178
|
+
buf = buf.slice(cut).trimStart();
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
while (i < trimmed.length) {
|
|
1182
|
+
const ch = trimmed.charAt(i);
|
|
1183
|
+
buf += ch;
|
|
1184
|
+
const isPunct = ch === '.' || ch === '!' || ch === '?';
|
|
1185
|
+
const isSemi = ch === ';';
|
|
1186
|
+
if (isPunct) {
|
|
1187
|
+
const next = trimmed.charAt(i + 1);
|
|
1188
|
+
if (next === '' || next === ' ') {
|
|
1189
|
+
if (ch === '.' && _chunkIsAbbrevBefore(trimmed, i)) { i += 1; continue; }
|
|
1190
|
+
if (ch === '.' && /\d/.test(trimmed.charAt(i - 1) || '')
|
|
1191
|
+
&& /\d/.test(trimmed.charAt(i + 1) || '')) { i += 1; continue; }
|
|
1192
|
+
i += 1;
|
|
1193
|
+
if (trimmed.charAt(i) === ' ') i += 1;
|
|
1194
|
+
flush();
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
} else if (isSemi && buf.length >= semiBreak) {
|
|
1198
|
+
i += 1;
|
|
1199
|
+
if (trimmed.charAt(i) === ' ') i += 1;
|
|
1200
|
+
flush();
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
i += 1;
|
|
1204
|
+
if (buf.length > maxLen) flushAtMaxLen();
|
|
1205
|
+
}
|
|
1206
|
+
flush();
|
|
1207
|
+
return chunks;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** Fetch + decode one TTS chunk. Returns a Blob URL ready to
|
|
1211
|
+
* play, or null on failure (and surfaces the error via the
|
|
1212
|
+
* status bar). */
|
|
1213
|
+
async function _fetchTtsChunk(text) {
|
|
1214
|
+
try {
|
|
1215
|
+
const r = await fetch('/api/voice/tts', {
|
|
1216
|
+
method: 'POST',
|
|
1217
|
+
headers: { 'content-type': 'application/json' },
|
|
1218
|
+
body: JSON.stringify({ text }),
|
|
1219
|
+
});
|
|
1220
|
+
if (!r.ok) {
|
|
1221
|
+
const data = await r.json().catch(() => ({}));
|
|
1222
|
+
setStatus('Voice reply failed: ' + (data.error || ('HTTP ' + r.status)), true);
|
|
1223
|
+
return null;
|
|
1224
|
+
}
|
|
1225
|
+
const ab = await r.arrayBuffer();
|
|
1226
|
+
const contentType = r.headers.get('content-type') || 'audio/mpeg';
|
|
1227
|
+
const blob = new Blob([ab], { type: contentType });
|
|
1228
|
+
return URL.createObjectURL(blob);
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
setStatus('Voice reply error: ' + String(err && err.message ? err.message : err), true);
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/** Play a single Blob URL audio element + resolve when it
|
|
1236
|
+
* ends. The URL is revoked after playback. */
|
|
1237
|
+
function _playChunkUrl(url) {
|
|
1238
|
+
return new Promise((resolve) => {
|
|
1239
|
+
const audio = new Audio(url);
|
|
1240
|
+
const done = () => { URL.revokeObjectURL(url); resolve(); };
|
|
1241
|
+
audio.addEventListener('ended', done, { once: true });
|
|
1242
|
+
audio.addEventListener('error', done, { once: true });
|
|
1243
|
+
audio.play().catch((err) => {
|
|
1244
|
+
setStatus('Could not play audio: ' + String(err && err.message ? err.message : err), true);
|
|
1245
|
+
done();
|
|
1246
|
+
});
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* V1.24 -- chunked TTS playback with pipelined prefetch.
|
|
1252
|
+
*
|
|
1253
|
+
* For short text (single chunk), behaves identically to the
|
|
1254
|
+
* pre-V1.24 single-call flow. For long text (multiple chunks),
|
|
1255
|
+
* starts fetching chunk N+1 while chunk N is playing so the
|
|
1256
|
+
* provider per-call overhead is hidden behind playback. First
|
|
1257
|
+
* audio fires after ~one chunk's STT round-trip instead of
|
|
1258
|
+
* the whole-text round-trip.
|
|
1259
|
+
*/
|
|
1260
|
+
/**
|
|
1261
|
+
* V1.37 + V1.38 -- upload a spec file to /api/forge/ingest
|
|
1262
|
+
* and render the parsed summary in the chat stream. Real
|
|
1263
|
+
* plan-building + scaffold come in V1.39+. For now this is
|
|
1264
|
+
* the "yes, Forge can read this spec" confirmation step.
|
|
1265
|
+
*/
|
|
1266
|
+
async function ingestSpecFile(file) {
|
|
1267
|
+
if (!file) return;
|
|
1268
|
+
setStatus('Leyendo ' + file.name + '...');
|
|
1269
|
+
/* Render an in-progress system message so the user sees
|
|
1270
|
+
immediate feedback. */
|
|
1271
|
+
state.messages.push({
|
|
1272
|
+
role: 'user',
|
|
1273
|
+
content: '(spec adjunta: ' + file.name + ', ' + Math.round(file.size / 1024) + ' KB)',
|
|
1274
|
+
});
|
|
1275
|
+
renderStream();
|
|
1276
|
+
try {
|
|
1277
|
+
const buf = await file.arrayBuffer();
|
|
1278
|
+
const r = await fetch('/api/forge/ingest', {
|
|
1279
|
+
method: 'POST',
|
|
1280
|
+
headers: {
|
|
1281
|
+
'content-type': 'application/octet-stream',
|
|
1282
|
+
'x-filename': file.name,
|
|
1283
|
+
},
|
|
1284
|
+
body: buf,
|
|
1285
|
+
});
|
|
1286
|
+
const data = await r.json();
|
|
1287
|
+
if (!r.ok || !data.ok) {
|
|
1288
|
+
const msg = data && data.error ? data.error : ('HTTP ' + r.status);
|
|
1289
|
+
setStatus('Ingest fallo: ' + msg, true);
|
|
1290
|
+
state.messages.push({
|
|
1291
|
+
role: 'assistant',
|
|
1292
|
+
content: 'No pude leer la especificacion: ' + msg,
|
|
1293
|
+
});
|
|
1294
|
+
renderStream();
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const ing = data.ingested || {};
|
|
1298
|
+
const lines = [
|
|
1299
|
+
'Le1: leida.',
|
|
1300
|
+
'Titulo: ' + (ing.title || '(sin titulo)'),
|
|
1301
|
+
'Formato: ' + ing.format + ' / ' + ing.sections + ' seccion(es) / ' + Math.round(ing.bytes / 1024) + ' KB',
|
|
1302
|
+
];
|
|
1303
|
+
if (ing.first_paragraphs && ing.first_paragraphs.length > 0) {
|
|
1304
|
+
lines.push('Primeras lineas:');
|
|
1305
|
+
for (const p of ing.first_paragraphs) {
|
|
1306
|
+
lines.push(' - ' + p.slice(0, 160) + (p.length > 160 ? '...' : ''));
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
/* Record as a synthetic tool round in the trace so the
|
|
1310
|
+
pizarra reflects the ingest action. */
|
|
1311
|
+
state.toolHistory.push({
|
|
1312
|
+
turnIndex: state.messages.length - 1,
|
|
1313
|
+
rounds: [{
|
|
1314
|
+
tool: 'forge.spec.ingest',
|
|
1315
|
+
input: { filename: ing.filename, format: ing.format },
|
|
1316
|
+
display: 'parsed ' + ing.format + ': ' + (ing.title || ing.filename),
|
|
1317
|
+
is_error: false,
|
|
1318
|
+
}],
|
|
1319
|
+
});
|
|
1320
|
+
state.messages.push({ role: 'assistant', content: lines.join('\\n') });
|
|
1321
|
+
renderStream();
|
|
1322
|
+
renderTrace();
|
|
1323
|
+
setStatus('');
|
|
1324
|
+
} catch (err) {
|
|
1325
|
+
setStatus('Ingest fallo: ' + String(err && err.message ? err.message : err), true);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
async function playTtsForText(text) {
|
|
1330
|
+
if (!ttsRepliesEnabled()) return;
|
|
1331
|
+
if (!text || text.trim() === '') return;
|
|
1332
|
+
const chunks = splitIntoTtsChunks(text);
|
|
1333
|
+
if (chunks.length === 0) return;
|
|
1334
|
+
if (chunks.length === 1) {
|
|
1335
|
+
const url = await _fetchTtsChunk(chunks[0]);
|
|
1336
|
+
if (url) await _playChunkUrl(url);
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
/* Pipeline: prefetch chunk N+1 while chunk N plays. */
|
|
1340
|
+
let nextFetch = _fetchTtsChunk(chunks[0]);
|
|
1341
|
+
for (let i = 0; i < chunks.length; i += 1) {
|
|
1342
|
+
const url = await nextFetch;
|
|
1343
|
+
/* Kick off the next fetch BEFORE awaiting playback so the
|
|
1344
|
+
network round-trip overlaps the audio. */
|
|
1345
|
+
nextFetch = (i + 1 < chunks.length) ? _fetchTtsChunk(chunks[i + 1]) : Promise.resolve(null);
|
|
1346
|
+
if (url) await _playChunkUrl(url);
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
/* Patch send() to trigger TTS playback after assistant turn.
|
|
1351
|
+
Done via a wrapper so the original send is preserved. */
|
|
1352
|
+
const _originalSend = send;
|
|
1353
|
+
send = async function (text) {
|
|
1354
|
+
const beforeLen = state.messages.length;
|
|
1355
|
+
await _originalSend(text);
|
|
1356
|
+
const last = state.messages[state.messages.length - 1];
|
|
1357
|
+
if (state.messages.length > beforeLen && last && last.role === 'assistant') {
|
|
1358
|
+
/* Fire-and-forget; do not await so the chat stream stays responsive. */
|
|
1359
|
+
playTtsForText(last.content);
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
|
|
1363
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
1364
|
+
setProj();
|
|
1365
|
+
showMode(loadMode());
|
|
1366
|
+
$('#yf-globito').addEventListener('click', () => showMode('mini'));
|
|
1367
|
+
$('#mini-full').addEventListener('click', () => showMode('full'));
|
|
1368
|
+
$('#mini-close').addEventListener('click', () => showMode('globito'));
|
|
1369
|
+
$('#full-mini').addEventListener('click', () => showMode('mini'));
|
|
1370
|
+
$('#full-close').addEventListener('click', () => showMode('globito'));
|
|
1371
|
+
attachBar($('#bar-mini'));
|
|
1372
|
+
attachBar($('#bar-full'));
|
|
1373
|
+
renderStream();
|
|
1374
|
+
renderTrace();
|
|
1375
|
+
|
|
1376
|
+
/* Voice controls. */
|
|
1377
|
+
$$('[data-mic-btn]').forEach((b) => b.addEventListener('click', () => micToggle(b)));
|
|
1378
|
+
$$('[data-tts-toggle]').forEach((b) => b.addEventListener('click', () => setTtsReplies(!ttsRepliesEnabled())));
|
|
1379
|
+
setMicButtonsState('idle');
|
|
1380
|
+
updateTtsToggleButtons();
|
|
1381
|
+
|
|
1382
|
+
/* V1.37 -- drag-drop spec ingest. The overlay activates on
|
|
1383
|
+
dragenter anywhere on the document and clears on dragleave
|
|
1384
|
+
(with a small grace window) or on drop. The actual upload
|
|
1385
|
+
fires through ingestSpecFile. */
|
|
1386
|
+
let _dragDepth = 0;
|
|
1387
|
+
const dropOverlay = $('#yf-drop-overlay');
|
|
1388
|
+
document.addEventListener('dragenter', (e) => {
|
|
1389
|
+
if (!e.dataTransfer || !e.dataTransfer.types || !e.dataTransfer.types.includes('Files')) return;
|
|
1390
|
+
_dragDepth += 1;
|
|
1391
|
+
if (dropOverlay) dropOverlay.classList.add('active');
|
|
1392
|
+
e.preventDefault();
|
|
1393
|
+
});
|
|
1394
|
+
document.addEventListener('dragleave', (e) => {
|
|
1395
|
+
_dragDepth = Math.max(0, _dragDepth - 1);
|
|
1396
|
+
if (_dragDepth === 0 && dropOverlay) dropOverlay.classList.remove('active');
|
|
1397
|
+
e.preventDefault();
|
|
1398
|
+
});
|
|
1399
|
+
document.addEventListener('dragover', (e) => {
|
|
1400
|
+
if (e.dataTransfer && e.dataTransfer.types && e.dataTransfer.types.includes('Files')) {
|
|
1401
|
+
e.preventDefault();
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
document.addEventListener('drop', async (e) => {
|
|
1405
|
+
e.preventDefault();
|
|
1406
|
+
_dragDepth = 0;
|
|
1407
|
+
if (dropOverlay) dropOverlay.classList.remove('active');
|
|
1408
|
+
if (!e.dataTransfer || !e.dataTransfer.files || e.dataTransfer.files.length === 0) return;
|
|
1409
|
+
/* Process the first file only -- batch ingest is a later
|
|
1410
|
+
slice. Multi-file selection produces sequential calls. */
|
|
1411
|
+
const file = e.dataTransfer.files[0];
|
|
1412
|
+
await ingestSpecFile(file);
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1415
|
+
/* Fase F.3 -- language selector wiring. */
|
|
1416
|
+
let _langData = null; /* { current, languages[] } cached after first load */
|
|
1417
|
+
async function loadLangData() {
|
|
1418
|
+
if (_langData) return _langData;
|
|
1419
|
+
try {
|
|
1420
|
+
const r = await fetch('/api/i18n/languages');
|
|
1421
|
+
const d = await r.json();
|
|
1422
|
+
if (d && d.ok) {
|
|
1423
|
+
_langData = d;
|
|
1424
|
+
return d;
|
|
1425
|
+
}
|
|
1426
|
+
} catch (_) {}
|
|
1427
|
+
return null;
|
|
1428
|
+
}
|
|
1429
|
+
function renderLangDropdown(data) {
|
|
1430
|
+
const drop = $('#yf-lang-dropdown');
|
|
1431
|
+
if (!drop) return;
|
|
1432
|
+
const items = data.languages.map((l) => {
|
|
1433
|
+
const active = l.code === data.current ? ' active' : '';
|
|
1434
|
+
const dim = l.manual_available ? '' : ' (no manual)';
|
|
1435
|
+
return '<div class="lang-item' + active + '" data-lang="' + l.code + '" role="menuitem">'
|
|
1436
|
+
+ '<span>' + l.display + dim + '</span>'
|
|
1437
|
+
+ '<span class="code">' + l.code + '</span>'
|
|
1438
|
+
+ '</div>';
|
|
1439
|
+
}).join('');
|
|
1440
|
+
drop.innerHTML = items;
|
|
1441
|
+
drop.querySelectorAll('.lang-item').forEach((el) => {
|
|
1442
|
+
el.addEventListener('click', async () => {
|
|
1443
|
+
const code = el.getAttribute('data-lang');
|
|
1444
|
+
try {
|
|
1445
|
+
/* Set a cookie so the SERVER renders the next panel
|
|
1446
|
+
load in this language -- stateless per-request,
|
|
1447
|
+
no shared state. The legacy POST is also fired so
|
|
1448
|
+
/api/voice/tts (which still reads current_language
|
|
1449
|
+
for default voice picking) follows along. */
|
|
1450
|
+
document.cookie = 'yf-lang=' + code + '; path=/; max-age=' + (60 * 60 * 24 * 365) + '; SameSite=Lax';
|
|
1451
|
+
try { localStorage.setItem('yf-lang', code); } catch (_) {}
|
|
1452
|
+
fetch('/api/i18n/language', {
|
|
1453
|
+
method: 'POST',
|
|
1454
|
+
headers: { 'content-type': 'application/json' },
|
|
1455
|
+
body: JSON.stringify({ lang: code }),
|
|
1456
|
+
}).catch(() => undefined);
|
|
1457
|
+
if (_langData) _langData.current = code;
|
|
1458
|
+
drop.classList.add('hidden');
|
|
1459
|
+
/* Reload to apply translated strings everywhere. */
|
|
1460
|
+
location.reload();
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
setStatus('Language switch failed: ' + String(err && err.message ? err.message : err), true);
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
async function toggleLangDropdown() {
|
|
1468
|
+
const drop = $('#yf-lang-dropdown');
|
|
1469
|
+
if (!drop) return;
|
|
1470
|
+
if (!drop.classList.contains('hidden')) {
|
|
1471
|
+
drop.classList.add('hidden');
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
const data = await loadLangData();
|
|
1475
|
+
if (!data) {
|
|
1476
|
+
setStatus('Could not load language list', true);
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
renderLangDropdown(data);
|
|
1480
|
+
drop.classList.remove('hidden');
|
|
1481
|
+
}
|
|
1482
|
+
$$('[data-lang-open]').forEach((b) => b.addEventListener('click', toggleLangDropdown));
|
|
1483
|
+
/* Click outside closes the dropdown. */
|
|
1484
|
+
document.addEventListener('click', (e) => {
|
|
1485
|
+
const drop = $('#yf-lang-dropdown');
|
|
1486
|
+
if (!drop || drop.classList.contains('hidden')) return;
|
|
1487
|
+
const target = e.target;
|
|
1488
|
+
if (target && target.closest && target.closest('[data-lang-open]')) return;
|
|
1489
|
+
if (target && target.closest && target.closest('#yf-lang-dropdown')) return;
|
|
1490
|
+
drop.classList.add('hidden');
|
|
1491
|
+
});
|
|
1492
|
+
/* Restore previous selection from localStorage at boot --
|
|
1493
|
+
if the user has a stored lang AND the cookie is missing
|
|
1494
|
+
(first visit after manual cookie clear), mirror it to the
|
|
1495
|
+
cookie + reload so server-side render picks it up. */
|
|
1496
|
+
try {
|
|
1497
|
+
const stored = localStorage.getItem('yf-lang');
|
|
1498
|
+
const cookieHas = document.cookie.split(';').some((p) => p.trim().startsWith('yf-lang='));
|
|
1499
|
+
if (stored && !cookieHas) {
|
|
1500
|
+
document.cookie = 'yf-lang=' + stored + '; path=/; max-age=' + (60 * 60 * 24 * 365) + '; SameSite=Lax';
|
|
1501
|
+
location.reload();
|
|
1502
|
+
}
|
|
1503
|
+
} catch (_) {}
|
|
1504
|
+
|
|
1505
|
+
/* Vault modal wiring. */
|
|
1506
|
+
$$('[data-vault-open]').forEach((b) => b.addEventListener('click', vaultOpen));
|
|
1507
|
+
$$('[data-vault-close]').forEach((b) => b.addEventListener('click', vaultClose));
|
|
1508
|
+
$$('.v-tab').forEach((b) => b.addEventListener('click', () => vaultShowTab(b.getAttribute('data-vault-tab'))));
|
|
1509
|
+
$('#vault-add-form').addEventListener('submit', (e) => { e.preventDefault(); vaultAdd(); });
|
|
1510
|
+
/* Escape closes the modal. */
|
|
1511
|
+
document.addEventListener('keydown', (e) => {
|
|
1512
|
+
if (e.key === 'Escape' && !$('#yf-vault-modal').classList.contains('hidden')) vaultClose();
|
|
1513
|
+
});
|
|
1514
|
+
});
|
|
1515
|
+
</script>
|
|
1516
|
+
</body>
|
|
1517
|
+
</html>`;
|
|
1518
|
+
}
|
|
1519
|
+
function escapeHtml(s) {
|
|
1520
|
+
return s
|
|
1521
|
+
.replace(/&/g, '&')
|
|
1522
|
+
.replace(/</g, '<')
|
|
1523
|
+
.replace(/>/g, '>')
|
|
1524
|
+
.replace(/"/g, '"');
|
|
1525
|
+
}
|
|
1526
|
+
//# sourceMappingURL=panel.js.map
|