aimeat 2.4.0 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.env.example +33 -0
- package/dist/build-stamp.json +13 -0
- package/dist/locales/en.json +186 -20
- package/dist/locales/fi.json +186 -20
- package/dist/public/components/VoiceRecorder.js +116 -0
- package/dist/public/connection-done.html +53 -0
- package/dist/public/css/components/voice-recorder.css +66 -0
- package/dist/public/css/views/inbox.css +41 -0
- package/dist/public/css/views/profile.css +171 -95
- package/dist/public/css/views/transparency.css +5 -0
- package/dist/public/js/services/audio-recorder.js +171 -0
- package/dist/public/js/services/messages.js +21 -2
- package/dist/public/spa.html +3 -0
- package/dist/public/views/landing.js +4 -0
- package/dist/public/views/profile/access-tab/connections.js +264 -0
- package/dist/public/views/profile/access-tab.js +2 -0
- package/dist/public/views/profile/ai-transparency-card.js +22 -17
- package/dist/public/views/profile/inbox-tab/components.js +60 -10
- package/dist/public/views/profile/inbox-tab/helpers.js +33 -0
- package/dist/public/views/profile/inbox-tab/panels.js +6 -1
- package/dist/public/views/profile/inbox-tab/use-voice.js +50 -0
- package/dist/public/views/profile/inbox-tab/voice-parts.js +111 -0
- package/dist/public/views/profile/inbox-tab.js +18 -24
- package/dist/public/views/profile/openrouter/budget-panel.js +224 -0
- package/dist/public/views/profile/openrouter/model-picker.js +150 -0
- package/dist/public/views/profile/openrouter/pricing.js +139 -0
- package/dist/public/views/profile/openrouter-settings.js +425 -488
- package/dist/public/views/transparency.js +50 -0
- package/dist/scripts/build-sdk-libs.d.ts.map +1 -1
- package/dist/scripts/build-sdk-libs.js +1 -0
- package/dist/scripts/build-sdk-libs.js.map +1 -1
- package/dist/scripts/check-ai-disclosure.d.ts.map +1 -1
- package/dist/scripts/check-ai-disclosure.js +182 -0
- package/dist/scripts/check-ai-disclosure.js.map +1 -1
- package/dist/src/cli/connect/ai-provenance-carry.d.ts +247 -0
- package/dist/src/cli/connect/ai-provenance-carry.d.ts.map +1 -0
- package/dist/src/cli/connect/ai-provenance-carry.js +334 -0
- package/dist/src/cli/connect/ai-provenance-carry.js.map +1 -0
- package/dist/src/cli/connect/api-client.d.ts +11 -0
- package/dist/src/cli/connect/api-client.d.ts.map +1 -1
- package/dist/src/cli/connect/api-client.js +2 -0
- package/dist/src/cli/connect/api-client.js.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/local-server.js +11 -0
- package/dist/src/cli/connect/mcp/local-server.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-messages.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/agent-messages.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-messages.js +5 -2
- package/dist/src/cli/connect/mcp/tools/agent-messages.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-tasks.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/agent-tasks.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/agent-tasks.js +5 -2
- package/dist/src/cli/connect/mcp/tools/agent-tasks.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/apps.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/apps.js +13 -5
- package/dist/src/cli/connect/mcp/tools/apps.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/boards.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/boards.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/boards.js +5 -2
- package/dist/src/cli/connect/mcp/tools/boards.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.d.ts +5 -0
- package/dist/src/cli/connect/mcp/tools/core.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/core.js +25 -5
- package/dist/src/cli/connect/mcp/tools/core.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/dm-messages.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/dm-messages.js +11 -6
- package/dist/src/cli/connect/mcp/tools/dm-messages.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/exchange.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/exchange.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/exchange.js +8 -2
- package/dist/src/cli/connect/mcp/tools/exchange.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/knowledge.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/knowledge.js +7 -3
- package/dist/src/cli/connect/mcp/tools/knowledge.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts +2 -0
- package/dist/src/cli/connect/mcp/tools/organisms.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/organisms.js +7 -2
- package/dist/src/cli/connect/mcp/tools/organisms.js.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workspaces.d.ts +3 -0
- package/dist/src/cli/connect/mcp/tools/workspaces.d.ts.map +1 -1
- package/dist/src/cli/connect/mcp/tools/workspaces.js +8 -2
- package/dist/src/cli/connect/mcp/tools/workspaces.js.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-core.d.ts +3 -0
- package/dist/src/cli/connect/tool-call-defs-core.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call-defs-core.js +7 -0
- package/dist/src/cli/connect/tool-call-defs-core.js.map +1 -1
- package/dist/src/cli/connect/tool-call.d.ts +3 -0
- package/dist/src/cli/connect/tool-call.d.ts.map +1 -1
- package/dist/src/cli/connect/tool-call.js +11 -1
- package/dist/src/cli/connect/tool-call.js.map +1 -1
- package/dist/src/config-types.d.ts +14 -0
- package/dist/src/config-types.d.ts.map +1 -1
- package/dist/src/config.d.ts.map +1 -1
- package/dist/src/config.js +10 -0
- package/dist/src/config.js.map +1 -1
- package/dist/src/data/public-pages.d.ts +12 -0
- package/dist/src/data/public-pages.d.ts.map +1 -1
- package/dist/src/data/public-pages.js +12 -0
- package/dist/src/data/public-pages.js.map +1 -1
- package/dist/src/generated/api-types.d.ts +969 -85
- package/dist/src/generated/api-types.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.d.ts +45 -2
- package/dist/src/mcp/ai-provenance-input.d.ts.map +1 -1
- package/dist/src/mcp/ai-provenance-input.js +48 -2
- package/dist/src/mcp/ai-provenance-input.js.map +1 -1
- package/dist/src/mcp/apps.d.ts +7 -0
- package/dist/src/mcp/apps.d.ts.map +1 -1
- package/dist/src/mcp/apps.js +20 -2
- package/dist/src/mcp/apps.js.map +1 -1
- package/dist/src/models/ai-provenance-schemas.d.ts +7 -1
- package/dist/src/models/ai-provenance-schemas.d.ts.map +1 -1
- package/dist/src/models/ai-provenance-schemas.js +11 -1
- package/dist/src/models/ai-provenance-schemas.js.map +1 -1
- package/dist/src/models/connection-schemas.d.ts +203 -0
- package/dist/src/models/connection-schemas.d.ts.map +1 -0
- package/dist/src/models/connection-schemas.js +33 -0
- package/dist/src/models/connection-schemas.js.map +1 -0
- package/dist/src/models/message-schemas.d.ts +40 -0
- package/dist/src/models/message-schemas.d.ts.map +1 -1
- package/dist/src/models/message-schemas.js +23 -0
- package/dist/src/models/message-schemas.js.map +1 -1
- package/dist/src/routes/ai-provenance.d.ts +6 -0
- package/dist/src/routes/ai-provenance.d.ts.map +1 -1
- package/dist/src/routes/ai-provenance.js +34 -1
- package/dist/src/routes/ai-provenance.js.map +1 -1
- package/dist/src/routes/ai-transparency.d.ts +6 -1
- package/dist/src/routes/ai-transparency.d.ts.map +1 -1
- package/dist/src/routes/ai-transparency.js +17 -3
- package/dist/src/routes/ai-transparency.js.map +1 -1
- package/dist/src/routes/ai.d.ts +5 -0
- package/dist/src/routes/ai.d.ts.map +1 -1
- package/dist/src/routes/ai.js +83 -0
- package/dist/src/routes/ai.js.map +1 -1
- package/dist/src/routes/app-grants.d.ts.map +1 -1
- package/dist/src/routes/app-grants.js +4 -0
- package/dist/src/routes/app-grants.js.map +1 -1
- package/dist/src/routes/apps/drafts.d.ts +3 -0
- package/dist/src/routes/apps/drafts.d.ts.map +1 -1
- package/dist/src/routes/apps/drafts.js +13 -0
- package/dist/src/routes/apps/drafts.js.map +1 -1
- package/dist/src/routes/apps/publish.d.ts +5 -0
- package/dist/src/routes/apps/publish.d.ts.map +1 -1
- package/dist/src/routes/apps/publish.js +21 -5
- package/dist/src/routes/apps/publish.js.map +1 -1
- package/dist/src/routes/connections.d.ts +35 -0
- package/dist/src/routes/connections.d.ts.map +1 -0
- package/dist/src/routes/connections.js +406 -0
- package/dist/src/routes/connections.js.map +1 -0
- package/dist/src/routes/libs.js +1 -1
- package/dist/src/routes/libs.js.map +1 -1
- package/dist/src/routes/messages.d.ts +5 -0
- package/dist/src/routes/messages.d.ts.map +1 -1
- package/dist/src/routes/messages.js +92 -0
- package/dist/src/routes/messages.js.map +1 -1
- package/dist/src/routes/openrouter.d.ts +6 -1
- package/dist/src/routes/openrouter.d.ts.map +1 -1
- package/dist/src/routes/openrouter.js +37 -7
- package/dist/src/routes/openrouter.js.map +1 -1
- package/dist/src/routes/upload.d.ts +5 -0
- package/dist/src/routes/upload.d.ts.map +1 -1
- package/dist/src/routes/upload.js +32 -0
- package/dist/src/routes/upload.js.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.d.ts.map +1 -1
- package/dist/src/server-bootstrap/routes-loader.js +2 -0
- package/dist/src/server-bootstrap/routes-loader.js.map +1 -1
- package/dist/src/services/ai-completion.d.ts +48 -0
- package/dist/src/services/ai-completion.d.ts.map +1 -1
- package/dist/src/services/ai-completion.js +112 -72
- package/dist/src/services/ai-completion.js.map +1 -1
- package/dist/src/services/ai-disclosure.d.ts +6 -0
- package/dist/src/services/ai-disclosure.d.ts.map +1 -1
- package/dist/src/services/ai-disclosure.js +20 -2
- package/dist/src/services/ai-disclosure.js.map +1 -1
- package/dist/src/services/ai-provenance-page.d.ts +59 -0
- package/dist/src/services/ai-provenance-page.d.ts.map +1 -0
- package/dist/src/services/ai-provenance-page.js +185 -0
- package/dist/src/services/ai-provenance-page.js.map +1 -0
- package/dist/src/services/ai-provenance.d.ts +6 -0
- package/dist/src/services/ai-provenance.d.ts.map +1 -1
- package/dist/src/services/ai-provenance.js +5 -0
- package/dist/src/services/ai-provenance.js.map +1 -1
- package/dist/src/services/ai-transcription.d.ts +66 -0
- package/dist/src/services/ai-transcription.d.ts.map +1 -0
- package/dist/src/services/ai-transcription.js +90 -0
- package/dist/src/services/ai-transcription.js.map +1 -0
- package/dist/src/services/app-publish.d.ts.map +1 -1
- package/dist/src/services/app-publish.js +16 -1
- package/dist/src/services/app-publish.js.map +1 -1
- package/dist/src/services/build-app-prompt.d.ts.map +1 -1
- package/dist/src/services/build-app-prompt.js +2 -1
- package/dist/src/services/build-app-prompt.js.map +1 -1
- package/dist/src/services/connections/attach.d.ts +62 -0
- package/dist/src/services/connections/attach.d.ts.map +1 -0
- package/dist/src/services/connections/attach.js +168 -0
- package/dist/src/services/connections/attach.js.map +1 -0
- package/dist/src/services/connections/credential.d.ts +39 -0
- package/dist/src/services/connections/credential.d.ts.map +1 -0
- package/dist/src/services/connections/credential.js +66 -0
- package/dist/src/services/connections/credential.js.map +1 -0
- package/dist/src/services/connections/instance.d.ts +65 -0
- package/dist/src/services/connections/instance.d.ts.map +1 -0
- package/dist/src/services/connections/instance.js +144 -0
- package/dist/src/services/connections/instance.js.map +1 -0
- package/dist/src/services/connections/oauth.d.ts +77 -0
- package/dist/src/services/connections/oauth.d.ts.map +1 -0
- package/dist/src/services/connections/oauth.js +290 -0
- package/dist/src/services/connections/oauth.js.map +1 -0
- package/dist/src/services/connections/providers.d.ts +118 -0
- package/dist/src/services/connections/providers.d.ts.map +1 -0
- package/dist/src/services/connections/providers.js +260 -0
- package/dist/src/services/connections/providers.js.map +1 -0
- package/dist/src/services/connections/publish-gate.d.ts +103 -0
- package/dist/src/services/connections/publish-gate.d.ts.map +1 -0
- package/dist/src/services/connections/publish-gate.js +0 -0
- package/dist/src/services/connections/publish-gate.js.map +1 -0
- package/dist/src/services/connections/publish.d.ts +60 -0
- package/dist/src/services/connections/publish.d.ts.map +1 -0
- package/dist/src/services/connections/publish.js +340 -0
- package/dist/src/services/connections/publish.js.map +1 -0
- package/dist/src/services/connections/refresh.d.ts +55 -0
- package/dist/src/services/connections/refresh.d.ts.map +1 -0
- package/dist/src/services/connections/refresh.js +307 -0
- package/dist/src/services/connections/refresh.js.map +1 -0
- package/dist/src/services/markdown-negotiation.d.ts +17 -0
- package/dist/src/services/markdown-negotiation.d.ts.map +1 -1
- package/dist/src/services/markdown-negotiation.js +15 -0
- package/dist/src/services/markdown-negotiation.js.map +1 -1
- package/dist/src/services/message-send.d.ts.map +1 -1
- package/dist/src/services/message-send.js +16 -2
- package/dist/src/services/message-send.js.map +1 -1
- package/dist/src/services/openrouter.d.ts +67 -1
- package/dist/src/services/openrouter.d.ts.map +1 -1
- package/dist/src/services/openrouter.js +102 -11
- package/dist/src/services/openrouter.js.map +1 -1
- package/dist/src/services/upload-token.d.ts +5 -1
- package/dist/src/services/upload-token.d.ts.map +1 -1
- package/dist/src/services/upload-token.js +13 -1
- package/dist/src/services/upload-token.js.map +1 -1
- package/dist/src/services/workflow/engine-util.d.ts +18 -0
- package/dist/src/services/workflow/engine-util.d.ts.map +1 -1
- package/dist/src/services/workflow/engine-util.js +27 -0
- package/dist/src/services/workflow/engine-util.js.map +1 -1
- package/dist/src/services/workflow/engine.d.ts.map +1 -1
- package/dist/src/services/workflow/engine.js +4 -2
- package/dist/src/services/workflow/engine.js.map +1 -1
- package/dist/src/storage/interface.d.ts +2 -1
- package/dist/src/storage/interface.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/db-types.d.ts +65 -0
- package/dist/src/storage/providers/postgres-kysely/db-types.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/index.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/index.js +2 -1
- package/dist/src/storage/providers/postgres-kysely/index.js.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/methods/connections.d.ts +44 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.d.ts.map +1 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.js +308 -0
- package/dist/src/storage/providers/postgres-kysely/methods/connections.js.map +1 -0
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.d.ts.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.js +4 -2
- package/dist/src/storage/providers/postgres-kysely/methods/node-infra.js.map +1 -1
- package/dist/src/storage/providers/postgres-kysely/migrations/0021_connections.sql +116 -0
- package/dist/src/storage/providers/postgres-kysely/migrations/0022_provider_clients.sql +36 -0
- package/dist/src/storage/providers/postgres-kysely/migrations/0023_nonce_payload.sql +13 -0
- package/dist/src/storage/providers/sqlite/index.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/index.js +2 -1
- package/dist/src/storage/providers/sqlite/index.js.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/community.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/community.js +2 -1
- package/dist/src/storage/providers/sqlite/methods/community.js.map +1 -1
- package/dist/src/storage/providers/sqlite/methods/connections.d.ts +60 -0
- package/dist/src/storage/providers/sqlite/methods/connections.d.ts.map +1 -0
- package/dist/src/storage/providers/sqlite/methods/connections.js +287 -0
- package/dist/src/storage/providers/sqlite/methods/connections.js.map +1 -0
- package/dist/src/storage/providers/sqlite/schema-tables-1.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-1.js +4 -0
- package/dist/src/storage/providers/sqlite/schema-tables-1.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-3.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema-tables-3.js +106 -0
- package/dist/src/storage/providers/sqlite/schema-tables-3.js.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.d.ts.map +1 -1
- package/dist/src/storage/providers/sqlite/schema.js +2 -0
- package/dist/src/storage/providers/sqlite/schema.js.map +1 -1
- package/dist/src/storage/repositories/connection.repository.d.ts +124 -0
- package/dist/src/storage/repositories/connection.repository.d.ts.map +1 -0
- package/dist/src/storage/repositories/connection.repository.js +2 -0
- package/dist/src/storage/repositories/connection.repository.js.map +1 -0
- package/dist/src/storage/types/agents-messaging.d.ts +19 -0
- package/dist/src/storage/types/agents-messaging.d.ts.map +1 -1
- package/dist/src/storage/types/agents-messaging.js.map +1 -1
- package/dist/src/storage/types/auth.d.ts +8 -1
- package/dist/src/storage/types/auth.d.ts.map +1 -1
- package/dist/src/utils/build-stamp.d.ts +56 -0
- package/dist/src/utils/build-stamp.d.ts.map +1 -0
- package/dist/src/utils/build-stamp.js +142 -0
- package/dist/src/utils/build-stamp.js.map +1 -0
- package/dist/src/utils/messaging.d.ts +14 -0
- package/dist/src/utils/messaging.d.ts.map +1 -1
- package/dist/src/utils/messaging.js +0 -0
- package/dist/src/utils/messaging.js.map +1 -1
- package/dist/static/sdk-libs/connect/index.js +199 -0
- package/dist/static/sdk-libs/connect/notes.js +47 -0
- package/dist/static/sdk-libs/connect/panel.js +238 -0
- package/dist/static/sdk-libs/data/index.js +63 -11
- package/dist/static/sdk-libs/dist/aimeat-connect.js +357 -0
- package/dist/static/sdk-libs/dist/aimeat-data.js +40 -9
- package/package.json +2 -2
|
@@ -1,35 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file openrouter-settings.js
|
|
3
|
-
* @description AI provider configuration
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
3
|
+
* @description AI provider configuration — OpenRouter / LM Studio / custom OpenAI-compatible
|
|
4
|
+
* endpoint, the API key, the five model roles (default, reasoning, execution, vision,
|
|
5
|
+
* speech-to-text), sampling parameters, and the AI spend panel.
|
|
6
|
+
*
|
|
7
|
+
* Four sections, each saving its OWN fields where the user is looking. One Save at the bottom of a
|
|
8
|
+
* single long form was the standing complaint: the key was typed at the top and the button that
|
|
9
|
+
* stored it sat ten fields and an entire budget panel below, so "did that save?" had no answer
|
|
10
|
+
* where it was asked. PUT /v1/openrouter/settings is a partial update, so per-section saving needs
|
|
11
|
+
* no server change.
|
|
12
|
+
* @structure OpenRouterSettings (container + state) · SttTestPanel · ParamsSection ·
|
|
13
|
+
* openrouter/model-picker.js · openrouter/pricing.js · openrouter/budget-panel.js
|
|
14
|
+
* @usage import { OpenRouterSettings } from './openrouter-settings.js';
|
|
11
15
|
* @version-history
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* v3.0.0 — 2026-08-01 — Sectioned rework. Save-next-to-the-key; searchable model pickers showing
|
|
17
|
+
* price, context and a link to the model's page (a flat <select> of 336 unlabelled options was
|
|
18
|
+
* unusable); reasoning + execution pickers surfaced at last (the server has stored those two
|
|
19
|
+
* since v1.x with no UI to set them); speech-to-text model + language + a MEASURED test
|
|
20
|
+
* transcription, because an audio model's listed price carries no unit; the spend panel
|
|
21
|
+
* translated and moved to openrouter/budget-panel.js (it was hardcoded English). The
|
|
22
|
+
* onSettingsChange contract and the startOpen prop are unchanged for all three callers.
|
|
23
|
+
* v2.1.0 — 2026-08-01 — TARGET-058 Phase 9 step 0. "Test" distinguishes a spent daily budget from
|
|
24
|
+
* a failed connection; headlining a 402 as "Connection failed" sent people off to doubt a key
|
|
25
|
+
* that was working. (Behaviour carried into v3.)
|
|
16
26
|
* v2.0.0 — 2026-07-18 — Split out of generator-settings.js (SettingsCollectionView removed with
|
|
17
|
-
* the deleted Generator feature); dropped the generator.js project-settings import.
|
|
18
|
-
* v1.5.0 — 2026-07-05 — Model dropdown
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* listModels() name→id fallback fix in services/openrouter.ts.
|
|
23
|
-
* v1.4.1 — 2026-07-05 — Per-app limits table also lists apps active in the last 30 days (from the
|
|
24
|
-
* history chart), not only apps that spent TODAY — so a cap can be set even when today's spend is $0.
|
|
25
|
-
* v1.4.0 — 2026-07-05 — AI apps budget panel: add a per-app stacked "usage over time" chart (30 days,
|
|
26
|
-
* cost⇄tokens toggle) fed by GET /v1/ai/usage/history, below the today bar + per-app table.
|
|
27
|
-
* v1.3.0 — 2026-07-05 — AI apps budget panel: per-app daily caps are now VISIBLE + EDITABLE inline
|
|
28
|
-
* (each app defaults to the whole daily budget; set a cap to throttle one app). Was API-only.
|
|
29
|
-
* v1.2.0 — 2026-06-24 — Add a Vision model selector (a vision-capable model, e.g. qwen-2.5-VL) used
|
|
30
|
-
* for image inputs — the Secretary's doc/image intake sends attached images to this model.
|
|
31
|
-
* v1.1.0 — 2026-04-01 — Add temperature, top_p, max_tokens model parameter fields
|
|
32
|
-
* v1.0.0 — 2026-03-22 — Extracted from generator-tab.js (was inline in v5.1.0+)
|
|
27
|
+
* the deleted Generator feature); dropped the generator.js project-settings import.
|
|
28
|
+
* v1.5.0 — 2026-07-05 — Model dropdown populates for custom OpenAI-compatible providers.
|
|
29
|
+
* v1.4.0 — 2026-07-05 — AI apps budget panel: per-app stacked usage chart.
|
|
30
|
+
* v1.2.0 — 2026-06-24 — Vision model selector.
|
|
31
|
+
* v1.0.0 — 2026-03-22 — Extracted from generator-tab.js
|
|
33
32
|
*/
|
|
34
33
|
import { h } from 'preact';
|
|
35
34
|
import { useState, useEffect } from 'preact/hooks';
|
|
@@ -37,43 +36,70 @@ import htm from 'htm';
|
|
|
37
36
|
const html = htm.bind(h);
|
|
38
37
|
import { t } from '/js/i18n.js';
|
|
39
38
|
import { apiGet, apiPut, apiPost, apiDelete } from '/js/api.js';
|
|
40
|
-
import { UsageChart, colorForIndex } from '/components/UsageChart.js';
|
|
41
39
|
import { useConfirm } from '/components/Modal.js';
|
|
40
|
+
import { VoiceRecorder } from '/components/VoiceRecorder.js';
|
|
42
41
|
import { swallowed } from '/js/swallowed.js';
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
42
|
+
import { ModelPicker } from './openrouter/model-picker.js';
|
|
43
|
+
import { AiAppsBudgetPanel } from './openrouter/budget-panel.js';
|
|
44
|
+
|
|
45
|
+
/** Language hints offered for transcription. Auto-detect is first and is the right answer for
|
|
46
|
+
* mixed-language speech; a hint measurably helps when the language is known. */
|
|
47
|
+
const STT_LANGS = ['', 'fi', 'en', 'sv', 'de', 'fr', 'es', 'et'];
|
|
48
|
+
|
|
49
|
+
/** Read a File as bare base64 (no data: prefix) — the shape /v1/ai/transcribe takes inline. */
|
|
50
|
+
function fileToBase64(file) {
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const fr = new FileReader();
|
|
53
|
+
fr.onerror = () => reject(new Error('read failed'));
|
|
54
|
+
fr.onload = () => {
|
|
55
|
+
const s = String(fr.result || '');
|
|
56
|
+
const comma = s.indexOf(',');
|
|
57
|
+
resolve(comma >= 0 ? s.slice(comma + 1) : s);
|
|
58
|
+
};
|
|
59
|
+
fr.readAsDataURL(file);
|
|
60
|
+
});
|
|
50
61
|
}
|
|
51
62
|
|
|
52
|
-
/* ── OpenRouter / LM Studio / Custom Settings ────────── */
|
|
53
|
-
|
|
54
63
|
export function OpenRouterSettings({ onSettingsChange, startOpen = false }) {
|
|
55
64
|
const { confirm, ConfirmUI } = useConfirm();
|
|
56
65
|
const [collapsed, setCollapsed] = useState(!startOpen);
|
|
66
|
+
const [loaded, setLoaded] = useState(false);
|
|
67
|
+
|
|
68
|
+
// connection
|
|
57
69
|
const [hasApiKey, setHasApiKey] = useState(false);
|
|
58
70
|
const [apiKey, setApiKey] = useState('');
|
|
71
|
+
const [provider, setProvider] = useState('openrouter');
|
|
72
|
+
const [baseUrl, setBaseUrl] = useState('');
|
|
73
|
+
const [savingKey, setSavingKey] = useState(false);
|
|
74
|
+
const [testing, setTesting] = useState(false);
|
|
75
|
+
const [connMsg, setConnMsg] = useState(null);
|
|
76
|
+
|
|
77
|
+
// models
|
|
59
78
|
const [model, setModel] = useState('');
|
|
79
|
+
const [reasoningModel, setReasoningModel] = useState('');
|
|
80
|
+
const [executionModel, setExecutionModel] = useState('');
|
|
60
81
|
const [visionModel, setVisionModel] = useState('');
|
|
82
|
+
const [sttModel, setSttModel] = useState('');
|
|
83
|
+
const [sttLanguage, setSttLanguage] = useState('');
|
|
61
84
|
const [models, setModels] = useState([]);
|
|
85
|
+
const [sttModels, setSttModels] = useState([]);
|
|
62
86
|
const [modelsLoading, setModelsLoading] = useState(false);
|
|
63
87
|
const [modelsError, setModelsError] = useState(null);
|
|
88
|
+
const [savingModels, setSavingModels] = useState(false);
|
|
89
|
+
const [modelsMsg, setModelsMsg] = useState(null);
|
|
90
|
+
|
|
91
|
+
// params
|
|
64
92
|
const [autoRetry, setAutoRetry] = useState(false);
|
|
65
93
|
const [maxRetries, setMaxRetries] = useState(3);
|
|
66
|
-
const [provider, setProvider] = useState('openrouter');
|
|
67
|
-
const [baseUrl, setBaseUrl] = useState('');
|
|
68
94
|
const [temperature, setTemperature] = useState('');
|
|
69
95
|
const [topP, setTopP] = useState('');
|
|
70
96
|
const [maxTokens, setMaxTokens] = useState('');
|
|
71
|
-
const [
|
|
72
|
-
const [
|
|
73
|
-
const [message, setMessage] = useState(null); // { text, error }
|
|
74
|
-
const [loaded, setLoaded] = useState(false);
|
|
97
|
+
const [savingParams, setSavingParams] = useState(false);
|
|
98
|
+
const [paramsMsg, setParamsMsg] = useState(null);
|
|
75
99
|
|
|
76
|
-
|
|
100
|
+
const isOpenRouter = provider === 'openrouter';
|
|
101
|
+
|
|
102
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only load; the loaders use stable setters
|
|
77
103
|
useEffect(() => { loadSettings(); }, []);
|
|
78
104
|
|
|
79
105
|
async function loadSettings() {
|
|
@@ -82,7 +108,11 @@ export function OpenRouterSettings({ onSettingsChange, startOpen = false }) {
|
|
|
82
108
|
if (resp.ok !== false && resp.data) {
|
|
83
109
|
setHasApiKey(!!resp.data.hasApiKey);
|
|
84
110
|
setModel(resp.data.model || '');
|
|
111
|
+
setReasoningModel(resp.data.reasoningModel || '');
|
|
112
|
+
setExecutionModel(resp.data.executionModel || '');
|
|
85
113
|
setVisionModel(resp.data.visionModel || '');
|
|
114
|
+
setSttModel(resp.data.sttModel || '');
|
|
115
|
+
setSttLanguage(resp.data.sttLanguage || '');
|
|
86
116
|
setAutoRetry(!!resp.data.autoRetry);
|
|
87
117
|
setMaxRetries(resp.data.maxRetries || 3);
|
|
88
118
|
setProvider(resp.data.provider || 'openrouter');
|
|
@@ -99,528 +129,435 @@ export function OpenRouterSettings({ onSettingsChange, startOpen = false }) {
|
|
|
99
129
|
// Notify parent whenever key settings change
|
|
100
130
|
useEffect(() => {
|
|
101
131
|
if (loaded && onSettingsChange) {
|
|
102
|
-
onSettingsChange({ hasApiKey
|
|
132
|
+
onSettingsChange({ hasApiKey, autoRetry, maxRetries, provider, baseUrl });
|
|
103
133
|
}
|
|
104
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps --
|
|
134
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- onSettingsChange is a caller callback, intentionally excluded so it does not re-fire on every parent render
|
|
105
135
|
}, [loaded, hasApiKey, autoRetry, maxRetries, provider, baseUrl]);
|
|
106
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Two catalogues, not one filtered twice: OpenRouter's default /models contains no transcription
|
|
139
|
+
* models at all, so the STT picker is empty unless its list is fetched with its own modality.
|
|
140
|
+
*/
|
|
107
141
|
async function loadModels() {
|
|
108
142
|
setModelsLoading(true);
|
|
109
143
|
setModelsError(null);
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
144
|
+
const [chat, stt] = await Promise.all([
|
|
145
|
+
apiGet('/v1/openrouter/models').catch(e => ({ ok: false, error: { message: e?.message } })),
|
|
146
|
+
apiGet('/v1/openrouter/models?modality=transcription').catch(e => {
|
|
147
|
+
// A provider with no transcription catalogue and a provider that failed to answer look the
|
|
148
|
+
// same from here, so the failure is logged rather than silently read as "none available".
|
|
149
|
+
swallowed('openrouter-settings: stt model list', e);
|
|
150
|
+
return { ok: false, data: null };
|
|
151
|
+
}),
|
|
152
|
+
]);
|
|
153
|
+
if (chat && chat.ok !== false && Array.isArray(chat.data?.models)) {
|
|
154
|
+
setModels(chat.data.models);
|
|
155
|
+
if (chat.data.models.length === 0) setModelsError(t('profile.openrouter.modelsEmpty'));
|
|
156
|
+
} else {
|
|
120
157
|
setModels([]);
|
|
121
|
-
setModelsError(
|
|
158
|
+
setModelsError(chat?.error?.message || t('profile.openrouter.modelsError'));
|
|
122
159
|
}
|
|
160
|
+
// A provider with no transcription catalogue is not an error — it simply has none to offer, and
|
|
161
|
+
// the STT section says so instead of showing an empty picker with no explanation.
|
|
162
|
+
setSttModels((stt && stt.ok !== false && Array.isArray(stt.data?.models)) ? stt.data.models : []);
|
|
123
163
|
setModelsLoading(false);
|
|
124
164
|
}
|
|
125
165
|
|
|
126
|
-
function
|
|
127
|
-
|
|
128
|
-
setTimeout(() =>
|
|
166
|
+
function flash(setter, text, error = false) {
|
|
167
|
+
setter({ text, error });
|
|
168
|
+
setTimeout(() => setter(null), 5000);
|
|
129
169
|
}
|
|
130
170
|
|
|
131
|
-
|
|
132
|
-
|
|
171
|
+
/** Save ONLY the connection fields, so the answer to "did my key save?" appears beside the key. */
|
|
172
|
+
async function saveKey() {
|
|
173
|
+
setSavingKey(true);
|
|
133
174
|
try {
|
|
134
|
-
const body = {
|
|
135
|
-
body.temperature = temperature !== '' ? parseFloat(temperature) : null;
|
|
136
|
-
body.top_p = topP !== '' ? parseFloat(topP) : null;
|
|
137
|
-
body.max_tokens = maxTokens !== '' ? parseInt(maxTokens) : null;
|
|
175
|
+
const body = { provider, baseUrl };
|
|
138
176
|
if (apiKey) body.apiKey = apiKey;
|
|
139
177
|
const resp = await apiPut('/v1/openrouter/settings', body);
|
|
140
178
|
if (resp.ok === false) {
|
|
141
|
-
|
|
179
|
+
flash(setConnMsg, resp.error?.message || t('profile.openrouter.saveFailed'), true);
|
|
142
180
|
} else {
|
|
143
|
-
|
|
144
|
-
setHasApiKey(true);
|
|
181
|
+
flash(setConnMsg, apiKey ? t('profile.openrouter.keySaved') : t('profile.openrouter.providerSaved'));
|
|
182
|
+
if (apiKey) setHasApiKey(true);
|
|
145
183
|
setApiKey('');
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// to NVIDIA's integrate.api.nvidia.com/v1) refreshes it even with no new key.
|
|
184
|
+
// Refetch after every save: the list comes from the just-saved base URL + key, so switching
|
|
185
|
+
// provider refreshes it even when no new key was typed.
|
|
149
186
|
loadModels();
|
|
150
187
|
}
|
|
151
188
|
} catch (e) {
|
|
152
|
-
|
|
189
|
+
flash(setConnMsg, e.message, true);
|
|
190
|
+
}
|
|
191
|
+
setSavingKey(false);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function saveModels() {
|
|
195
|
+
setSavingModels(true);
|
|
196
|
+
try {
|
|
197
|
+
const resp = await apiPut('/v1/openrouter/settings', {
|
|
198
|
+
model,
|
|
199
|
+
reasoningModel,
|
|
200
|
+
executionModel,
|
|
201
|
+
visionModel: visionModel || null,
|
|
202
|
+
sttModel: sttModel || null,
|
|
203
|
+
sttLanguage: sttLanguage || null,
|
|
204
|
+
provider,
|
|
205
|
+
baseUrl,
|
|
206
|
+
});
|
|
207
|
+
if (resp.ok === false) flash(setModelsMsg, resp.error?.message || t('profile.openrouter.saveFailed'), true);
|
|
208
|
+
else flash(setModelsMsg, t('profile.openrouter.modelsSaved'));
|
|
209
|
+
} catch (e) {
|
|
210
|
+
flash(setModelsMsg, e.message, true);
|
|
153
211
|
}
|
|
154
|
-
|
|
212
|
+
setSavingModels(false);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function saveParams() {
|
|
216
|
+
setSavingParams(true);
|
|
217
|
+
try {
|
|
218
|
+
const resp = await apiPut('/v1/openrouter/settings', {
|
|
219
|
+
autoRetry,
|
|
220
|
+
maxRetries: parseInt(maxRetries) || 3,
|
|
221
|
+
temperature: temperature !== '' ? parseFloat(temperature) : null,
|
|
222
|
+
top_p: topP !== '' ? parseFloat(topP) : null,
|
|
223
|
+
max_tokens: maxTokens !== '' ? parseInt(maxTokens) : null,
|
|
224
|
+
provider,
|
|
225
|
+
baseUrl,
|
|
226
|
+
});
|
|
227
|
+
if (resp.ok === false) flash(setParamsMsg, resp.error?.message || t('profile.openrouter.saveFailed'), true);
|
|
228
|
+
else flash(setParamsMsg, t('profile.openrouter.paramsSaved'));
|
|
229
|
+
} catch (e) {
|
|
230
|
+
flash(setParamsMsg, e.message, true);
|
|
231
|
+
}
|
|
232
|
+
setSavingParams(false);
|
|
155
233
|
}
|
|
156
234
|
|
|
157
235
|
async function handleTest() {
|
|
158
236
|
setTesting(true);
|
|
237
|
+
const started = Date.now();
|
|
159
238
|
try {
|
|
160
239
|
const resp = await apiPost('/v1/openrouter/test');
|
|
161
240
|
if (resp.ok === false) {
|
|
162
|
-
// A
|
|
163
|
-
//
|
|
164
|
-
//
|
|
165
|
-
// conclusion to hand a person who is trying to verify their setup.
|
|
241
|
+
// A refused test is not necessarily a broken connection: the completion chokepoint also
|
|
242
|
+
// refuses when the daily budget is spent. Headlining that "Connection failed" sends someone
|
|
243
|
+
// off to check a key that was never the problem.
|
|
166
244
|
const budgetSpent = resp.error?.code === 'QUOTA_EXHAUSTED' || resp.error?.code === 'APP_QUOTA_EXHAUSTED';
|
|
167
|
-
const head = budgetSpent
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
showMsg(head + (resp.error?.message ? ': ' + resp.error.message : ''), true);
|
|
245
|
+
const head = budgetSpent ? t('profile.openrouter.testBudgetSpent') : t('profile.openrouter.testFail');
|
|
246
|
+
// The provider's own sentence stays: it is the part that says what is actually wrong.
|
|
247
|
+
flash(setConnMsg, head + (resp.error?.message ? ': ' + resp.error.message : ''), true);
|
|
171
248
|
} else {
|
|
172
|
-
|
|
249
|
+
flash(setConnMsg, t('profile.openrouter.testOk', {
|
|
250
|
+
model: resp.data?.model || '', ms: Date.now() - started,
|
|
251
|
+
}));
|
|
173
252
|
}
|
|
174
253
|
} catch (e) {
|
|
175
|
-
|
|
254
|
+
flash(setConnMsg, `${t('profile.openrouter.testFail')}: ${e.message}`, true);
|
|
176
255
|
}
|
|
177
256
|
setTesting(false);
|
|
178
257
|
}
|
|
179
258
|
|
|
180
259
|
function handleDelete() {
|
|
181
|
-
confirm(t('profile.
|
|
260
|
+
confirm(t('profile.openrouter.deleteConfirm'), async () => {
|
|
182
261
|
try {
|
|
183
262
|
await apiDelete('/v1/openrouter/settings');
|
|
184
|
-
setHasApiKey(false);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
setMaxRetries(3);
|
|
192
|
-
setTemperature('');
|
|
193
|
-
setTopP('');
|
|
194
|
-
setMaxTokens('');
|
|
195
|
-
showMsg(t('profile.generator.openrouter.delete'));
|
|
263
|
+
setHasApiKey(false); setApiKey('');
|
|
264
|
+
setModel(''); setReasoningModel(''); setExecutionModel('');
|
|
265
|
+
setVisionModel(''); setSttModel(''); setSttLanguage('');
|
|
266
|
+
setModels([]); setSttModels([]); setModelsError(null);
|
|
267
|
+
setAutoRetry(false); setMaxRetries(3);
|
|
268
|
+
setTemperature(''); setTopP(''); setMaxTokens('');
|
|
269
|
+
flash(setConnMsg, t('profile.openrouter.deleted'));
|
|
196
270
|
} catch (e) {
|
|
197
|
-
|
|
271
|
+
flash(setConnMsg, e.message, true);
|
|
198
272
|
}
|
|
199
273
|
}, { danger: true });
|
|
200
274
|
}
|
|
201
275
|
|
|
202
276
|
if (!loaded) return null;
|
|
203
277
|
|
|
278
|
+
const keyed = hasApiKey || !!apiKey;
|
|
279
|
+
const pickerProps = { models, isOpenRouter, disabled: !keyed };
|
|
280
|
+
|
|
204
281
|
return html`
|
|
205
|
-
<div class="pf-
|
|
206
|
-
<button class="pf-
|
|
207
|
-
<span class="pf-
|
|
208
|
-
<span>${t('profile.
|
|
209
|
-
|
|
282
|
+
<div class="pf-or-wrapper">
|
|
283
|
+
<button class="pf-or-toggle" onClick=${() => setCollapsed(!collapsed)}>
|
|
284
|
+
<span class="pf-or-toggle-icon">${collapsed ? '▶' : '▼'}</span>
|
|
285
|
+
<span>${t('profile.openrouter.title')}</span>
|
|
286
|
+
<span class=${hasApiKey ? 'pf-or-status pf-or-status--on' : 'pf-or-status'}>
|
|
287
|
+
${hasApiKey ? t('profile.openrouter.statusReady') : t('profile.openrouter.statusNoKey')}
|
|
288
|
+
</span>
|
|
210
289
|
</button>
|
|
290
|
+
|
|
211
291
|
${!collapsed && html`
|
|
212
|
-
<div class="pf-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
<
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
292
|
+
<div class="pf-or-panel">
|
|
293
|
+
|
|
294
|
+
<!-- ── 1. Connection ───────────────────────────────── -->
|
|
295
|
+
<section class="pf-or-section">
|
|
296
|
+
<h4 class="pf-or-section-title">${t('profile.openrouter.section.connection')}</h4>
|
|
297
|
+
|
|
298
|
+
<div class="pf-or-field">
|
|
299
|
+
<label class="pf-or-label">${t('profile.openrouter.provider')}</label>
|
|
300
|
+
<div class="pf-or-radio-group">
|
|
301
|
+
${['openrouter', 'lmstudio', 'custom'].map(p => html`
|
|
302
|
+
<label class="pf-or-radio-label" key=${p}>
|
|
303
|
+
<input type="radio" name="ai-provider" value=${p} checked=${provider === p}
|
|
304
|
+
onChange=${() => {
|
|
305
|
+
setProvider(p);
|
|
306
|
+
if (p === 'lmstudio') setBaseUrl('http://localhost:1234/v1');
|
|
307
|
+
else if (p === 'openrouter') setBaseUrl('');
|
|
308
|
+
}} />
|
|
309
|
+
${t('profile.openrouter.provider_' + p)}
|
|
310
|
+
</label>`)}
|
|
311
|
+
</div>
|
|
229
312
|
</div>
|
|
230
|
-
</div>
|
|
231
313
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
314
|
+
${!isOpenRouter && html`
|
|
315
|
+
<div class="pf-or-field">
|
|
316
|
+
<label class="pf-or-label">${t('profile.openrouter.baseUrl')}</label>
|
|
317
|
+
<input type="url" class="pf-or-input" value=${baseUrl}
|
|
318
|
+
placeholder=${t('profile.openrouter.baseUrl_hint')}
|
|
319
|
+
onInput=${e => setBaseUrl(e.target.value)} />
|
|
320
|
+
</div>`}
|
|
321
|
+
|
|
322
|
+
<div class="pf-or-field">
|
|
323
|
+
<label class="pf-or-label">${t('profile.openrouter.apiKey')}</label>
|
|
324
|
+
<div class="pf-or-key-row">
|
|
325
|
+
<input type="password" autocomplete="off" data-1p-ignore data-lpignore="true"
|
|
326
|
+
class="pf-or-input pf-or-key-input"
|
|
327
|
+
placeholder=${hasApiKey ? t('profile.openrouter.apiKeyMasked') : t('profile.openrouter.apiKeyPlaceholder')}
|
|
328
|
+
value=${apiKey} onInput=${e => setApiKey(e.target.value)} />
|
|
329
|
+
<button class="btn-primary btn-sm" onClick=${saveKey} disabled=${savingKey}>
|
|
330
|
+
${savingKey ? '…' : t('profile.openrouter.saveKey')}
|
|
331
|
+
</button>
|
|
332
|
+
${hasApiKey && html`
|
|
333
|
+
<button class="btn-outline btn-sm" onClick=${handleTest} disabled=${testing}>
|
|
334
|
+
${testing ? html`<span class="spinner"></span>` : ''}${t('profile.openrouter.testConnection')}
|
|
335
|
+
</button>
|
|
336
|
+
<button class="btn-ghost btn-sm pf-or-delete" onClick=${handleDelete}>
|
|
337
|
+
${t('profile.openrouter.delete')}
|
|
338
|
+
</button>`}
|
|
339
|
+
</div>
|
|
340
|
+
${connMsg && html`<div class=${`pf-or-message ${connMsg.error ? 'pf-or-message-error' : 'pf-or-message-success'}`}>${connMsg.text}</div>`}
|
|
341
|
+
${isOpenRouter && html`
|
|
342
|
+
<div class="pf-or-links">
|
|
343
|
+
<a href="https://openrouter.ai/keys" target="_blank" rel="noopener">${t('profile.openrouter.getKeyLink')} ↗</a>
|
|
344
|
+
<a href="https://openrouter.ai/credits" target="_blank" rel="noopener">${t('profile.openrouter.creditsLink')} ↗</a>
|
|
345
|
+
</div>`}
|
|
243
346
|
</div>
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
<!--
|
|
247
|
-
<
|
|
248
|
-
<
|
|
249
|
-
<
|
|
250
|
-
type="
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
placeholder=${hasApiKey ? t('profile.generator.openrouter.apiKeyMasked') : t('profile.generator.openrouter.apiKeyPlaceholder')}
|
|
254
|
-
value=${apiKey}
|
|
255
|
-
onInput=${e => setApiKey(e.target.value)}
|
|
256
|
-
/>
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
<!-- Model -->
|
|
260
|
-
<div class="pf-gen-or-field">
|
|
261
|
-
<label class="pf-gen-or-label">${t('profile.generator.openrouter.model')}</label>
|
|
262
|
-
${modelsLoading
|
|
263
|
-
? html`<span class="pf-gen-or-loading">${t('profile.loading')}</span>`
|
|
264
|
-
: html`
|
|
265
|
-
<select
|
|
266
|
-
class="pf-gen-or-select"
|
|
267
|
-
value=${model}
|
|
268
|
-
onChange=${e => setModel(e.target.value)}
|
|
269
|
-
disabled=${!hasApiKey && !apiKey}
|
|
270
|
-
>
|
|
271
|
-
<option value="">${t('profile.generator.openrouter.modelSelect')}</option>
|
|
272
|
-
${models.map(m => html`
|
|
273
|
-
<option value=${m.id}>${m.name || m.id}</option>
|
|
274
|
-
`)}
|
|
275
|
-
</select>
|
|
276
|
-
`
|
|
277
|
-
}
|
|
278
|
-
<div class="pf-gen-or-param-row pf-gen-or-model-row">
|
|
279
|
-
<button type="button" class="btn-outline btn-sm"
|
|
280
|
-
onClick=${loadModels} disabled=${modelsLoading || !hasApiKey}>
|
|
281
|
-
${t('profile.generator.openrouter.modelsRefresh')}${models.length ? ` (${models.length})` : ''}
|
|
347
|
+
</section>
|
|
348
|
+
|
|
349
|
+
<!-- ── 2. Models ───────────────────────────────────── -->
|
|
350
|
+
<section class="pf-or-section">
|
|
351
|
+
<h4 class="pf-or-section-title">${t('profile.openrouter.section.models')}</h4>
|
|
352
|
+
<div class="pf-or-models-head">
|
|
353
|
+
<button type="button" class="btn-outline btn-sm" onClick=${loadModels}
|
|
354
|
+
disabled=${modelsLoading || !keyed}>
|
|
355
|
+
${t('profile.openrouter.modelsRefresh')}${models.length ? ` (${models.length})` : ''}
|
|
282
356
|
</button>
|
|
283
|
-
<span class=${modelsError ? 'pf-
|
|
284
|
-
${modelsError || t('profile.
|
|
357
|
+
<span class=${modelsError ? 'pf-or-hint pf-or-hint--error' : 'pf-or-hint'}>
|
|
358
|
+
${modelsError || t('profile.openrouter.modelsHint')}
|
|
285
359
|
</span>
|
|
286
360
|
</div>
|
|
287
|
-
</div>
|
|
288
|
-
|
|
289
|
-
<!-- Vision model (image inputs — Secretary doc/image intake) -->
|
|
290
|
-
<div class="pf-gen-or-field">
|
|
291
|
-
<label class="pf-gen-or-label">${t('profile.generator.openrouter.visionModel')}</label>
|
|
292
|
-
${modelsLoading
|
|
293
|
-
? html`<span class="pf-gen-or-loading">${t('profile.loading')}</span>`
|
|
294
|
-
: html`
|
|
295
|
-
<select
|
|
296
|
-
class="pf-gen-or-select"
|
|
297
|
-
value=${visionModel}
|
|
298
|
-
onChange=${e => setVisionModel(e.target.value)}
|
|
299
|
-
disabled=${!hasApiKey && !apiKey}
|
|
300
|
-
>
|
|
301
|
-
<option value="">${t('profile.generator.openrouter.visionModelNone')}</option>
|
|
302
|
-
${models.map(m => html`
|
|
303
|
-
<option value=${m.id}>${m.name || m.id}</option>
|
|
304
|
-
`)}
|
|
305
|
-
</select>
|
|
306
|
-
`
|
|
307
|
-
}
|
|
308
|
-
<span class="pf-gen-or-param-hint">${t('profile.generator.openrouter.visionModel_hint')}</span>
|
|
309
|
-
</div>
|
|
310
|
-
|
|
311
|
-
<!-- Auto-retry -->
|
|
312
|
-
<div class="pf-gen-or-field">
|
|
313
|
-
<label class="pf-gen-or-checkbox">
|
|
314
|
-
<input
|
|
315
|
-
type="checkbox"
|
|
316
|
-
checked=${autoRetry}
|
|
317
|
-
onChange=${e => setAutoRetry(e.target.checked)}
|
|
318
|
-
/>
|
|
319
|
-
${t('profile.generator.openrouter.autoRetry')}
|
|
320
|
-
</label>
|
|
321
|
-
</div>
|
|
322
|
-
|
|
323
|
-
<!-- Max retries (conditional) -->
|
|
324
|
-
${autoRetry && html`
|
|
325
|
-
<div class="pf-gen-or-field">
|
|
326
|
-
<label class="pf-gen-or-label">${t('profile.generator.openrouter.maxRetries')}</label>
|
|
327
|
-
<input
|
|
328
|
-
type="number"
|
|
329
|
-
class="pf-gen-or-input pf-gen-or-input-sm"
|
|
330
|
-
min="1"
|
|
331
|
-
max="10"
|
|
332
|
-
value=${maxRetries}
|
|
333
|
-
onInput=${e => setMaxRetries(Math.min(10, Math.max(1, parseInt(e.target.value) || 1)))}
|
|
334
|
-
/>
|
|
335
|
-
</div>
|
|
336
|
-
`}
|
|
337
|
-
|
|
338
|
-
<!-- Model Parameters -->
|
|
339
|
-
<div class="pf-gen-or-field">
|
|
340
|
-
<label class="pf-gen-or-label">${t('profile.generator.openrouter.temperature')}</label>
|
|
341
|
-
<div class="pf-gen-or-param-row">
|
|
342
|
-
<input
|
|
343
|
-
type="number"
|
|
344
|
-
class="pf-gen-or-input pf-gen-or-input-sm"
|
|
345
|
-
min="0"
|
|
346
|
-
max="2"
|
|
347
|
-
step="0.1"
|
|
348
|
-
placeholder="default"
|
|
349
|
-
value=${temperature}
|
|
350
|
-
onInput=${e => setTemperature(e.target.value)}
|
|
351
|
-
/>
|
|
352
|
-
<span class="pf-gen-or-param-hint">${t('profile.generator.openrouter.temperature_hint')}</span>
|
|
353
|
-
</div>
|
|
354
|
-
</div>
|
|
355
|
-
|
|
356
|
-
<div class="pf-gen-or-field">
|
|
357
|
-
<label class="pf-gen-or-label">${t('profile.generator.openrouter.topP')}</label>
|
|
358
|
-
<div class="pf-gen-or-param-row">
|
|
359
|
-
<input
|
|
360
|
-
type="number"
|
|
361
|
-
class="pf-gen-or-input pf-gen-or-input-sm"
|
|
362
|
-
min="0"
|
|
363
|
-
max="1"
|
|
364
|
-
step="0.05"
|
|
365
|
-
placeholder="default"
|
|
366
|
-
value=${topP}
|
|
367
|
-
onInput=${e => setTopP(e.target.value)}
|
|
368
|
-
/>
|
|
369
|
-
<span class="pf-gen-or-param-hint">${t('profile.generator.openrouter.topP_hint')}</span>
|
|
370
|
-
</div>
|
|
371
|
-
</div>
|
|
372
361
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
362
|
+
${modelsLoading ? html`<div class="pf-or-loading">${t('profile.loading')}</div>` : html`
|
|
363
|
+
<div class="pf-or-field">
|
|
364
|
+
<label class="pf-or-label">${t('profile.openrouter.model.default')}</label>
|
|
365
|
+
<${ModelPicker} ...${pickerProps} value=${model} onChange=${setModel} modality="chat"
|
|
366
|
+
allowCustom=${!isOpenRouter} />
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
<details class="pf-or-roles">
|
|
370
|
+
<summary class="pf-or-roles-summary">${t('profile.openrouter.model.rolesSummary')}</summary>
|
|
371
|
+
<div class="pf-or-field">
|
|
372
|
+
<label class="pf-or-label">${t('profile.openrouter.model.reasoning')}</label>
|
|
373
|
+
<${ModelPicker} ...${pickerProps} value=${reasoningModel} onChange=${setReasoningModel}
|
|
374
|
+
modality="chat" allowNone=${true} allowCustom=${!isOpenRouter} />
|
|
375
|
+
<span class="pf-or-hint">${t('profile.openrouter.model.reasoning_hint')}</span>
|
|
376
|
+
</div>
|
|
377
|
+
<div class="pf-or-field">
|
|
378
|
+
<label class="pf-or-label">${t('profile.openrouter.model.execution')}</label>
|
|
379
|
+
<${ModelPicker} ...${pickerProps} value=${executionModel} onChange=${setExecutionModel}
|
|
380
|
+
modality="chat" allowNone=${true} allowCustom=${!isOpenRouter} />
|
|
381
|
+
<span class="pf-or-hint">${t('profile.openrouter.model.execution_hint')}</span>
|
|
382
|
+
</div>
|
|
383
|
+
</details>
|
|
384
|
+
|
|
385
|
+
<div class="pf-or-field">
|
|
386
|
+
<label class="pf-or-label">${t('profile.openrouter.model.vision')}</label>
|
|
387
|
+
<${ModelPicker} ...${pickerProps} value=${visionModel} onChange=${setVisionModel}
|
|
388
|
+
modality="vision" allowNone=${true} allowCustom=${!isOpenRouter}
|
|
389
|
+
noneLabel=${t('profile.openrouter.model.visionNone')} />
|
|
390
|
+
<span class="pf-or-hint">${t('profile.openrouter.model.vision_hint')}</span>
|
|
391
|
+
</div>
|
|
392
|
+
|
|
393
|
+
<div class="pf-or-field pf-or-stt">
|
|
394
|
+
<label class="pf-or-label">${t('profile.openrouter.model.stt')}</label>
|
|
395
|
+
${sttModels.length === 0 && keyed
|
|
396
|
+
? html`<div class="pf-or-hint">${t('profile.openrouter.stt.listEmpty')}</div>` : null}
|
|
397
|
+
<${ModelPicker} models=${sttModels} isOpenRouter=${isOpenRouter} disabled=${!keyed}
|
|
398
|
+
value=${sttModel} onChange=${setSttModel} modality="transcription" allowNone=${true}
|
|
399
|
+
allowCustom=${!isOpenRouter} noneLabel=${t('profile.openrouter.model.sttNone')} />
|
|
400
|
+
<span class="pf-or-hint">${t('profile.openrouter.model.stt_hint')}</span>
|
|
401
|
+
|
|
402
|
+
<div class="pf-or-inline-field">
|
|
403
|
+
<label class="pf-or-label pf-or-label--inline">${t('profile.openrouter.stt.language')}</label>
|
|
404
|
+
<select class="pf-or-select pf-or-select--sm" value=${sttLanguage}
|
|
405
|
+
onChange=${e => setSttLanguage(e.target.value)}>
|
|
406
|
+
${STT_LANGS.map(code => html`
|
|
407
|
+
<option key=${code || 'auto'} value=${code}>
|
|
408
|
+
${code ? t('profile.openrouter.stt.lang_' + code) : t('profile.openrouter.stt.languageAuto')}
|
|
409
|
+
</option>`)}
|
|
410
|
+
</select>
|
|
411
|
+
<span class="pf-or-hint">${t('profile.openrouter.stt.language_hint')}</span>
|
|
412
|
+
</div>
|
|
413
|
+
|
|
414
|
+
<${SttTestPanel} sttModel=${sttModel} sttLanguage=${sttLanguage} />
|
|
415
|
+
</div>
|
|
416
|
+
`}
|
|
389
417
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
${message.text}
|
|
394
|
-
</div>
|
|
395
|
-
`}
|
|
396
|
-
|
|
397
|
-
<!-- Apps AI budget — apps reach this key via /v1/ai/complete -->
|
|
398
|
-
${hasApiKey && html`<${AiAppsBudgetPanel} />`}
|
|
399
|
-
|
|
400
|
-
<!-- Actions -->
|
|
401
|
-
<div class="pf-gen-or-actions">
|
|
402
|
-
<button class="btn-primary" onClick=${handleSave} disabled=${saving}>
|
|
403
|
-
${saving ? '...' : t('profile.generator.openrouter.save')}
|
|
404
|
-
</button>
|
|
405
|
-
${hasApiKey && html`
|
|
406
|
-
<button class="btn-outline" onClick=${handleTest} disabled=${testing}>
|
|
407
|
-
${testing ? html`<span class="spinner"></span>` : ''}
|
|
408
|
-
${t('profile.generator.openrouter.testConnection')}
|
|
418
|
+
<div class="pf-or-actions">
|
|
419
|
+
<button class="btn-primary" onClick=${saveModels} disabled=${savingModels}>
|
|
420
|
+
${savingModels ? '…' : t('profile.openrouter.saveModels')}
|
|
409
421
|
</button>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
422
|
+
${modelsMsg && html`<span class=${`pf-or-message ${modelsMsg.error ? 'pf-or-message-error' : 'pf-or-message-success'}`}>${modelsMsg.text}</span>`}
|
|
423
|
+
</div>
|
|
424
|
+
</section>
|
|
425
|
+
|
|
426
|
+
<!-- ── 3. Parameters ───────────────────────────────── -->
|
|
427
|
+
<${ParamsSection}
|
|
428
|
+
autoRetry=${autoRetry} setAutoRetry=${setAutoRetry}
|
|
429
|
+
maxRetries=${maxRetries} setMaxRetries=${setMaxRetries}
|
|
430
|
+
temperature=${temperature} setTemperature=${setTemperature}
|
|
431
|
+
topP=${topP} setTopP=${setTopP}
|
|
432
|
+
maxTokens=${maxTokens} setMaxTokens=${setMaxTokens}
|
|
433
|
+
saving=${savingParams} onSave=${saveParams} message=${paramsMsg} />
|
|
434
|
+
|
|
435
|
+
<!-- ── 4. Budget ───────────────────────────────────── -->
|
|
436
|
+
${hasApiKey && html`
|
|
437
|
+
<section class="pf-or-section">
|
|
438
|
+
<h4 class="pf-or-section-title">${t('profile.openrouter.section.budget')}</h4>
|
|
439
|
+
<${AiAppsBudgetPanel} />
|
|
440
|
+
</section>`}
|
|
441
|
+
</div>`}
|
|
417
442
|
<${ConfirmUI} />
|
|
418
|
-
</div
|
|
419
|
-
`;
|
|
443
|
+
</div>`;
|
|
420
444
|
}
|
|
421
445
|
|
|
422
446
|
/**
|
|
423
|
-
*
|
|
424
|
-
* total spend and per-app breakdown, lets the user set the daily USD cap.
|
|
447
|
+
* Record a few seconds and transcribe them for real.
|
|
425
448
|
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
449
|
+
* This answers a question the catalogue cannot: what a transcription actually costs. An audio model's
|
|
450
|
+
* listed price carries no unit — the same Whisper Large V3 reports 0.0015 on one provider and 0.111
|
|
451
|
+
* on another, per minute against per hour — so one measured call beats any figure derived from the
|
|
452
|
+
* model list. It also proves the whole chain end to end: microphone, upload, key, model.
|
|
430
453
|
*/
|
|
431
|
-
function
|
|
432
|
-
const [
|
|
433
|
-
const [
|
|
434
|
-
const [
|
|
435
|
-
const [budgetInput, setBudgetInput] = useState('1');
|
|
436
|
-
const [saving, setSaving] = useState(false);
|
|
437
|
-
const [message, setMessage] = useState(null);
|
|
438
|
-
const [caps, setCaps] = useState({}); // app → cap input string ('' = use the daily budget)
|
|
439
|
-
const [savingCaps, setSavingCaps] = useState(false);
|
|
440
|
-
const [capsMsg, setCapsMsg] = useState(null);
|
|
441
|
-
const [history, setHistory] = useState(null); // GET /v1/ai/usage/history — per-day series + rollups
|
|
442
|
-
const [metric, setMetric] = useState('cost'); // chart metric: 'cost' | 'tokens'
|
|
443
|
-
|
|
444
|
-
useEffect(() => { reload(); }, []);
|
|
445
|
-
|
|
446
|
-
async function reload() {
|
|
447
|
-
const [u, s, hist] = await Promise.all([
|
|
448
|
-
apiGet('/v1/ai/usage').catch(err => { swallowed('openrouter-settings: reload', err); return null; }),
|
|
449
|
-
apiGet('/v1/ai/settings').catch(err => { swallowed('openrouter-settings: reload', err); return null; }),
|
|
450
|
-
apiGet('/v1/ai/usage/history?days=30').catch(err => { swallowed('openrouter-settings: reload', err); return null; }),
|
|
451
|
-
]);
|
|
452
|
-
if (hist && hist.ok !== false && hist.data) setHistory(hist.data);
|
|
453
|
-
if (u && u.ok !== false && u.data) setUsage(u.data);
|
|
454
|
-
if (s && s.ok !== false && s.data) {
|
|
455
|
-
setSettings(s.data);
|
|
456
|
-
setBudgetInput(String(s.data.daily_budget_usd ?? 1));
|
|
457
|
-
const q = s.data.app_quotas || {};
|
|
458
|
-
setCaps(Object.fromEntries(Object.entries(q).map(([app, v]) =>
|
|
459
|
-
[app, (v && v.daily_usd != null) ? String(v.daily_usd) : ''])));
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
async function saveCaps() {
|
|
464
|
-
setSavingCaps(true); setCapsMsg(null);
|
|
465
|
-
const app_quotas = {};
|
|
466
|
-
for (const [app, val] of Object.entries(caps)) {
|
|
467
|
-
const s = String(val).trim();
|
|
468
|
-
if (s === '') continue; // blank = no override → app uses the daily budget
|
|
469
|
-
const n = Number(s);
|
|
470
|
-
if (!Number.isFinite(n) || n < 0 || n > 1000) {
|
|
471
|
-
setCapsMsg({ text: `Cap for "${app}" must be a number 0–1000.`, error: true });
|
|
472
|
-
setSavingCaps(false); return;
|
|
473
|
-
}
|
|
474
|
-
app_quotas[app] = { daily_usd: n };
|
|
475
|
-
}
|
|
476
|
-
try {
|
|
477
|
-
const r = await apiPost('/v1/ai/settings', { app_quotas });
|
|
478
|
-
if (r.ok === false) throw new Error(r.error?.message || 'Save failed');
|
|
479
|
-
setCapsMsg({ text: 'Per-app limits saved.' });
|
|
480
|
-
await reload();
|
|
481
|
-
} catch (e) {
|
|
482
|
-
setCapsMsg({ text: e.message || 'Save failed', error: true });
|
|
483
|
-
}
|
|
484
|
-
setSavingCaps(false);
|
|
485
|
-
}
|
|
454
|
+
function SttTestPanel({ sttModel, sttLanguage }) {
|
|
455
|
+
const [busy, setBusy] = useState(false);
|
|
456
|
+
const [result, setResult] = useState(null);
|
|
457
|
+
const [error, setError] = useState(null);
|
|
486
458
|
|
|
487
|
-
async function
|
|
488
|
-
|
|
489
|
-
const n = Number(budgetInput);
|
|
490
|
-
if (!Number.isFinite(n) || n < 0 || n > 1000) {
|
|
491
|
-
setMessage({ text: 'Budget must be a number between 0 and 1000.', error: true });
|
|
492
|
-
setSaving(false); return;
|
|
493
|
-
}
|
|
459
|
+
async function run(file) {
|
|
460
|
+
setBusy(true); setError(null); setResult(null);
|
|
494
461
|
try {
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
462
|
+
const audio_base64 = await fileToBase64(file);
|
|
463
|
+
const resp = await apiPost('/v1/ai/transcribe', {
|
|
464
|
+
audio_base64, mime: file.type, filename: file.name,
|
|
465
|
+
model: sttModel || undefined, language: sttLanguage || undefined, app_id: 'settings-test',
|
|
466
|
+
});
|
|
467
|
+
if (resp.ok === false) throw new Error(resp.error?.message || t('profile.openrouter.stt.testFailed'));
|
|
468
|
+
setResult(resp.data);
|
|
500
469
|
} catch (e) {
|
|
501
|
-
|
|
470
|
+
setError(e.message || t('profile.openrouter.stt.testFailed'));
|
|
502
471
|
}
|
|
503
|
-
|
|
472
|
+
setBusy(false);
|
|
504
473
|
}
|
|
505
474
|
|
|
506
|
-
if (!usage || !settings) return null;
|
|
507
|
-
const budget = usage.daily_budget_usd;
|
|
508
|
-
const spent = usage.spent_today_usd;
|
|
509
|
-
const pct = budget > 0 ? Math.min(100, Math.round((spent / budget) * 100)) : 0;
|
|
510
|
-
const perAppEntries = Object.entries(usage.per_app || {});
|
|
511
|
-
// apps to offer a per-app cap for: spent today, OR active in the last 30 days (from the history
|
|
512
|
-
// chart), OR already have a configured cap. Without the history apps the table is hidden whenever
|
|
513
|
-
// today's spend is $0 — leaving nowhere to set a cap for an app that only ran on earlier days.
|
|
514
|
-
const historyApps = (history && Array.isArray(history.apps)) ? history.apps : [];
|
|
515
|
-
const appNames = Array.from(new Set([...perAppEntries.map(([a]) => a), ...historyApps, ...Object.keys(caps)]));
|
|
516
|
-
|
|
517
475
|
return html`
|
|
518
|
-
<div class="pf-
|
|
519
|
-
<
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
476
|
+
<div class="pf-or-stt-test">
|
|
477
|
+
<div class="pf-or-stt-test-head">
|
|
478
|
+
<${VoiceRecorder} maxSeconds=${30} disabled=${busy || !sttModel}
|
|
479
|
+
label=${t('profile.openrouter.stt.test')} className="btn-outline btn-sm"
|
|
480
|
+
onRecorded=${(file) => run(file)} />
|
|
481
|
+
${busy ? html`<span class="pf-or-hint">${t('profile.openrouter.stt.testing')}</span>` : null}
|
|
482
|
+
${!sttModel ? html`<span class="pf-or-hint">${t('profile.openrouter.stt.testNeedsModel')}</span>` : null}
|
|
524
483
|
</div>
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
<div class="pf-
|
|
528
|
-
<div class="pf-
|
|
529
|
-
<div class="pf-
|
|
530
|
-
|
|
484
|
+
${error ? html`<div class="pf-or-message pf-or-message-error">${error}</div>` : null}
|
|
485
|
+
${result ? html`
|
|
486
|
+
<div class="pf-or-stt-result">
|
|
487
|
+
<div class="pf-or-stt-text">${result.text || t('profile.openrouter.stt.testSilent')}</div>
|
|
488
|
+
<div class="pf-or-stt-meta">
|
|
489
|
+
${t('profile.openrouter.stt.measured', {
|
|
490
|
+
seconds: (Number(result.seconds) || 0).toFixed(1),
|
|
491
|
+
cost: (Number(result.usage?.cost_usd) || 0).toFixed(6),
|
|
492
|
+
})}
|
|
493
|
+
${result.usage?.cost_exact === false
|
|
494
|
+
? html` <span class="pf-or-hint">${t('profile.openrouter.stt.costNotReported')}</span>` : null}
|
|
531
495
|
</div>
|
|
496
|
+
</div>` : null}
|
|
497
|
+
</div>`;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/** Sampling parameters. Collapsed by default: most owners never touch them, and four rarely-used
|
|
501
|
+
* numeric fields sitting open is part of what pushed the save button off the screen before. */
|
|
502
|
+
function ParamsSection({
|
|
503
|
+
autoRetry, setAutoRetry, maxRetries, setMaxRetries, temperature, setTemperature,
|
|
504
|
+
topP, setTopP, maxTokens, setMaxTokens, saving, onSave, message,
|
|
505
|
+
}) {
|
|
506
|
+
return html`
|
|
507
|
+
<section class="pf-or-section">
|
|
508
|
+
<details class="pf-or-params">
|
|
509
|
+
<summary class="pf-or-section-title pf-or-params-summary">${t('profile.openrouter.section.params')}</summary>
|
|
510
|
+
|
|
511
|
+
<div class="pf-or-field">
|
|
512
|
+
<label class="pf-or-checkbox">
|
|
513
|
+
<input type="checkbox" checked=${autoRetry} onChange=${e => setAutoRetry(e.target.checked)} />
|
|
514
|
+
${t('profile.openrouter.autoRetry')}
|
|
515
|
+
</label>
|
|
532
516
|
</div>
|
|
533
|
-
${editing ? html`
|
|
534
|
-
<input type="number" min="0" max="1000" step="0.10" value=${budgetInput}
|
|
535
|
-
onInput=${e => setBudgetInput(e.target.value)}
|
|
536
|
-
class="pf-gen-spend-budget-input" />
|
|
537
|
-
<button class="btn-primary btn-sm" onClick=${saveBudget} disabled=${saving}>
|
|
538
|
-
${saving ? '...' : 'Save'}
|
|
539
|
-
</button>
|
|
540
|
-
<button class="btn-outline btn-sm" onClick=${() => setEditing(false)}>Cancel</button>
|
|
541
|
-
` : html`
|
|
542
|
-
<button class="btn-outline btn-sm" onClick=${() => setEditing(true)}>Change</button>
|
|
543
|
-
`}
|
|
544
|
-
</div>
|
|
545
517
|
|
|
546
|
-
|
|
518
|
+
${autoRetry && html`
|
|
519
|
+
<div class="pf-or-field">
|
|
520
|
+
<label class="pf-or-label">${t('profile.openrouter.maxRetries')}</label>
|
|
521
|
+
<input type="number" class="pf-or-input pf-or-input-sm" min="1" max="10" value=${maxRetries}
|
|
522
|
+
onInput=${e => setMaxRetries(Math.min(10, Math.max(1, parseInt(e.target.value) || 1)))} />
|
|
523
|
+
</div>`}
|
|
524
|
+
|
|
525
|
+
<div class="pf-or-field">
|
|
526
|
+
<label class="pf-or-label">${t('profile.openrouter.temperature')}</label>
|
|
527
|
+
<div class="pf-or-param-row">
|
|
528
|
+
<input type="number" class="pf-or-input pf-or-input-sm" min="0" max="2" step="0.1"
|
|
529
|
+
placeholder=${t('profile.openrouter.paramDefault')} value=${temperature}
|
|
530
|
+
onInput=${e => setTemperature(e.target.value)} />
|
|
531
|
+
<span class="pf-or-hint">${t('profile.openrouter.temperature_hint')}</span>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
547
534
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
<
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
leave blank to use the full budget.
|
|
535
|
+
<div class="pf-or-field">
|
|
536
|
+
<label class="pf-or-label">${t('profile.openrouter.topP')}</label>
|
|
537
|
+
<div class="pf-or-param-row">
|
|
538
|
+
<input type="number" class="pf-or-input pf-or-input-sm" min="0" max="1" step="0.05"
|
|
539
|
+
placeholder=${t('profile.openrouter.paramDefault')} value=${topP}
|
|
540
|
+
onInput=${e => setTopP(e.target.value)} />
|
|
541
|
+
<span class="pf-or-hint">${t('profile.openrouter.topP_hint')}</span>
|
|
556
542
|
</div>
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
</
|
|
566
|
-
<tbody>
|
|
567
|
-
${appNames.map((app) => {
|
|
568
|
-
const s = usage.per_app[app] || { cost_usd: 0, calls: 0 };
|
|
569
|
-
return html`
|
|
570
|
-
<tr key=${app}>
|
|
571
|
-
<td>${app}</td>
|
|
572
|
-
<td class="num">$${(s.cost_usd || 0).toFixed(4)}</td>
|
|
573
|
-
<td class="num">
|
|
574
|
-
<input type="number" min="0" max="1000" step="0.10"
|
|
575
|
-
value=${caps[app] ?? ''} placeholder=${budget.toFixed(2)}
|
|
576
|
-
onInput=${e => setCaps(c => ({ ...c, [app]: e.target.value }))}
|
|
577
|
-
class="pf-gen-spend-cap-input" />
|
|
578
|
-
</td>
|
|
579
|
-
<td class="num">${s.calls || 0}</td>
|
|
580
|
-
</tr>`;
|
|
581
|
-
})}
|
|
582
|
-
</tbody>
|
|
583
|
-
</table>
|
|
584
|
-
<div class="pf-gen-spend-actions">
|
|
585
|
-
<button class="btn-primary btn-sm" onClick=${saveCaps} disabled=${savingCaps}>
|
|
586
|
-
${savingCaps ? '...' : 'Save per-app limits'}
|
|
587
|
-
</button>
|
|
588
|
-
${capsMsg && html`<span class="pf-gen-or-message ${capsMsg.error ? 'pf-gen-or-message-error' : 'pf-gen-or-message-success'}">${capsMsg.text}</span>`}
|
|
543
|
+
</div>
|
|
544
|
+
|
|
545
|
+
<div class="pf-or-field">
|
|
546
|
+
<label class="pf-or-label">${t('profile.openrouter.maxTokens')}</label>
|
|
547
|
+
<div class="pf-or-param-row">
|
|
548
|
+
<input type="number" class="pf-or-input pf-or-input-sm" min="256" max="128000" step="256"
|
|
549
|
+
placeholder=${t('profile.openrouter.paramDefault')} value=${maxTokens}
|
|
550
|
+
onInput=${e => setMaxTokens(e.target.value)} />
|
|
551
|
+
<span class="pf-or-hint">${t('profile.openrouter.maxTokens_hint')}</span>
|
|
589
552
|
</div>
|
|
590
|
-
</
|
|
591
|
-
`}
|
|
592
|
-
|
|
593
|
-
${history && Array.isArray(history.days) && history.days.length > 0 && (() => {
|
|
594
|
-
const labels = history.days.map((d) => d.date.slice(5));
|
|
595
|
-
const chartApps = history.apps || [];
|
|
596
|
-
const isTokens = metric === 'tokens';
|
|
597
|
-
const datasets = chartApps.map((app, i) => ({
|
|
598
|
-
label: app,
|
|
599
|
-
data: history.days.map((d) => {
|
|
600
|
-
const m = (d.per_app && d.per_app[app]) || {};
|
|
601
|
-
return (isTokens ? m.tokens : m.cost_usd) || 0;
|
|
602
|
-
}),
|
|
603
|
-
backgroundColor: colorForIndex(i),
|
|
604
|
-
}));
|
|
605
|
-
const yFormat = isTokens ? (v) => fmtCompact(v) : (v) => '$' + (Number(v) < 1 ? Number(v).toFixed(3) : Number(v).toFixed(2));
|
|
606
|
-
return html`
|
|
607
|
-
<div class="pf-gen-spend-chart-wrap">
|
|
608
|
-
<div class="pf-gen-spend-chart-head">
|
|
609
|
-
<span class="pf-gen-spend-chart-title">${t('profile.generator.aiUsageOverTime') || 'Usage over time (30 days)'}</span>
|
|
610
|
-
<span class="pf-gen-spend-metric-toggle">
|
|
611
|
-
<button class=${!isTokens ? 'btn-primary btn-sm' : 'btn-outline btn-sm'} onClick=${() => setMetric('cost')}>${t('profile.generator.aiMetricCost') || 'Cost'}</button>
|
|
612
|
-
<button class=${isTokens ? 'btn-primary btn-sm' : 'btn-outline btn-sm'} onClick=${() => setMetric('tokens')}>${t('profile.generator.aiMetricTokens') || 'Tokens'}</button>
|
|
613
|
-
</span>
|
|
614
|
-
</div>
|
|
615
|
-
<${UsageChart} stacked labels=${labels} datasets=${datasets} height=${220} yFormat=${yFormat} />
|
|
616
|
-
</div>`;
|
|
617
|
-
})()}
|
|
553
|
+
</div>
|
|
618
554
|
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
555
|
+
<div class="pf-or-actions">
|
|
556
|
+
<button class="btn-primary btn-sm" onClick=${onSave} disabled=${saving}>
|
|
557
|
+
${saving ? '…' : t('profile.openrouter.saveParams')}
|
|
558
|
+
</button>
|
|
559
|
+
${message && html`<span class=${`pf-or-message ${message.error ? 'pf-or-message-error' : 'pf-or-message-success'}`}>${message.text}</span>`}
|
|
560
|
+
</div>
|
|
561
|
+
</details>
|
|
562
|
+
</section>`;
|
|
625
563
|
}
|
|
626
|
-
|