mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 356359d25e | |||
| 83a6069de5 | |||
| e9a1890e54 | |||
| bf928aa06e | |||
| b2dc50590c | |||
| 229e53ac32 | |||
| 51e8a47615 | |||
| e80b58a412 | |||
| 48ced8b079 | |||
| c07e2aea1e | |||
| f3194aa30e | |||
| cb3e4cd951 | |||
| f5d8d029ea | |||
| 7c946c4126 | |||
| ded4ea0d69 | |||
| c180c549fe | |||
| 1f30f1168f | |||
| 9446f15922 | |||
| e13b2c9cd9 | |||
| e9e14e0292 | |||
| added19656 | |||
| 4fa3c4d479 | |||
| 690738de9a | |||
| cb31d27e68 | |||
| e6658df123 | |||
| 0b7154a14c | |||
| 02c1838de5 | |||
| fc455fceb8 | |||
| 8d40cdd234 | |||
| 40145c669a | |||
| 34d2fc233f | |||
| 670ec0381a | |||
| 2128f255fe | |||
| b717bd9a9a | |||
| 8aab9311f5 | |||
| ff3e16ea67 | |||
| 1de039c315 | |||
| d05e1786d7 | |||
| e34b5a7372 | |||
| a1b3d1b508 | |||
| 1ebccdf420 | |||
| e5f674509c | |||
| 197a4ae5c0 | |||
| 64d2dcf39c | |||
| caf54c736b | |||
| 423c2cce28 | |||
| a1af51efcb | |||
| ffc1bf9c58 | |||
| a54bfdb342 | |||
| 03861d2dbd | |||
| 8c080da6bf | |||
| a8c98056b6 | |||
| 78e663f955 | |||
| 70546a5039 | |||
| 30f78b33cb | |||
| 712e8c1f16 | |||
| 933dfdfb53 | |||
| 9ce86b029f | |||
| 13580cc69d | |||
| a7dee0002d | |||
| c84b2df3fa | |||
| d9471a8684 | |||
| ef630c2272 | |||
| e188c71652 | |||
| 910260c2c8 | |||
| 22752abc38 | |||
| 92bc3a5d64 | |||
| 1383752cc1 | |||
| 66af16fb81 | |||
| fc019d7b46 | |||
| ac4f0fcb12 | |||
| a6c2bc663d | |||
| e62ffa02e9 | |||
| a003600839 | |||
| ea73feb06d | |||
| 3bdf69e1b7 | |||
| 590fe78bd1 | |||
| 76187ba0e7 | |||
| 5eba375f4d | |||
| 8fa6a8251f | |||
| 75fa046f30 | |||
| 08a8cd1430 | |||
| 3afbb78a39 | |||
| fca6ccd816 | |||
| 8d351822c1 | |||
| 7d274a31fe | |||
| e36dde0d25 | |||
| 51cc6e5ae5 | |||
| 28d911c617 | |||
| b1e9fe58fb | |||
| 16ba014ade | |||
| e9d5a20c1a | |||
| 6e0036f9c4 | |||
| d7e189aa1c | |||
| ea2b444fb2 | |||
| cd1efaf26e | |||
| e47f0e5d43 | |||
| 5284d37984 | |||
| 1bf6fa0e4d | |||
| fc294c82f1 | |||
| 7b1dc49dda | |||
| d15ddeea24 | |||
| eaac213859 | |||
| 02c1460351 | |||
| 2fff35b7d9 | |||
| c5b9072bde | |||
| 8a570e912a | |||
| 1dcc40afb8 | |||
| c2092f8035 | |||
| 886c4b411e | |||
| 8888fd40cd | |||
| 31cd01bccf | |||
| c59b221004 | |||
| cb3cc3e74c | |||
| 9e90015fcc | |||
| 95e0517056 | |||
| 2b2f47915f | |||
| 9acd178ce1 | |||
| f381f80184 | |||
| c83be61343 | |||
| f6e49d31ec | |||
| cc0429a362 |
@@ -6,10 +6,11 @@
|
||||
"Bash(curl:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git cherry-pick:*)",
|
||||
"Bash(git describe:*)",
|
||||
"Bash(git grep:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git ls-tree:*)",
|
||||
"Bash(git show:*)",
|
||||
"Bash(grep:*)",
|
||||
"Bash(ls:*)",
|
||||
|
||||
@@ -51,7 +51,8 @@ jobs:
|
||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||
# disabling opus for now claude-opus-4-1-20250805
|
||||
# former: claude-sonnet-4-5-20250929
|
||||
claude_args: |
|
||||
--model claude-sonnet-4-5-20250929
|
||||
--model claude-opus-4-5-20251101
|
||||
--max-turns 100
|
||||
--allowedTools "Edit,Read,Write,WebFetch,WebSearch,Bash(cat:*),Bash(cp:*),Bash(find:*),Bash(git branch:*),Bash(grep:*),Bash(ls:*),Bash(mkdir:*),Bash(npm install),Bash(npm install:*),Bash(npm run:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh label:*),Bash(gh pr:*),mcp__chrome-devtools,SlashCommand"
|
||||
--allowedTools "Edit,Read,Write,WebFetch,WebSearch,Bash(cat:*),Bash(cp:*),Bash(find:*),Bash(git branch:*),Bash(grep:*),Bash(ls:*),Bash(mkdir:*),Bash(npm run:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh label:*),Bash(gh pr:*),mcp__chrome-devtools,SlashCommand"
|
||||
|
||||
@@ -72,6 +72,6 @@ jobs:
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
||||
claude_args: |
|
||||
--model claude-sonnet-4-5-20250929
|
||||
--model claude-opus-4-5-20251101
|
||||
--max-turns 75
|
||||
--allowedTools "Edit,Read,Write,WebFetch,WebSearch,Bash(cat:*),Bash(cp:*),Bash(find:*),Bash(git branch:*),Bash(grep:*),Bash(ls:*),Bash(mkdir:*),Bash(npm install),Bash(npm install:*),Bash(npm run:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh label:*),Bash(gh pr:*),mcp__chrome-devtools,SlashCommand"
|
||||
--allowedTools "Edit,Read,Write,WebFetch,WebSearch,Bash(cat:*),Bash(cp:*),Bash(find:*),Bash(git branch:*),Bash(grep:*),Bash(ls:*),Bash(mkdir:*),Bash(npm run:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh label:*),Bash(gh pr:*),mcp__chrome-devtools,SlashCommand"
|
||||
|
||||
@@ -72,6 +72,6 @@ jobs:
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://docs.claude.com/en/docs/claude-code/cli-reference for available options
|
||||
claude_args: |
|
||||
--model claude-sonnet-4-5-20250929
|
||||
--model claude-opus-4-5-20251101
|
||||
--max-turns 100
|
||||
--allowedTools "Edit,Read,Write,WebFetch,WebSearch,Bash(cat:*),Bash(cp:*),Bash(find:*),Bash(git branch:*),Bash(grep:*),Bash(ls:*),Bash(mkdir:*),Bash(npm install),Bash(npm install:*),Bash(npm run:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh label:*),Bash(gh pr:*),mcp__chrome-devtools"
|
||||
--allowedTools "Edit,Read,Write,WebFetch,WebSearch,Bash(cat:*),Bash(cp:*),Bash(find:*),Bash(git branch:*),Bash(grep:*),Bash(ls:*),Bash(mkdir:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh label:*),Bash(gh pr:*),mcp__chrome-devtools"
|
||||
|
||||
@@ -42,7 +42,8 @@ It comes packed with **world-class features** like Beam, and is praised for its
|
||||
[](https://big-agi.com/inspector)
|
||||
|
||||
### What makes Big-AGI different:
|
||||
**Intelligence**: with [Beam & Merge](https://big-agi.com/beam) for multi-model de-hallucination, native search, and bleeding-edge AI models like Nano Banana, Kimi K2 Thinking or GPT 5.1 -
|
||||
|
||||
**Intelligence**: with [Beam & Merge](https://big-agi.com/beam) for multi-model de-hallucination, native search, and bleeding-edge AI models like Opus 4.5, Nano Banana, Kimi K2 or GPT 5.1 -
|
||||
**Control**: with personas, data ownership, requests inspection, unlimited usage with API keys, and *no vendor lock-in* -
|
||||
and **Speed**: with a local-first, over-powered, zero-latency, madly optimized web app.
|
||||
|
||||
@@ -138,9 +139,14 @@ so you **are not vendor locked-in**, and obsessed over a powerful UI that works,
|
||||
NOTE: this is a powerful tool - if you need a toy UI or clone, this ain't it.
|
||||
|
||||
|
||||
## What's New in 2.0 · Oct 31, 2025 · Open
|
||||
---
|
||||
|
||||
👉 **[See the full changelog](https://big-agi.com/changes)**
|
||||
## Release Notes
|
||||
|
||||
👉 **[See the Live Release Notes](https://big-agi.com/changes)**
|
||||
- Open 2.0.2: **Speex** multi-vendor speech synthesis, **Opus 4.5**, **Gemini 3 Pro**, **Nano Banana Pro**, **Grok 4.1**, **GPT-5.1**, **Kimi K2** + 280 fixes
|
||||
|
||||
### What's New in 2.0 · Oct 31, 2025 · Open
|
||||
|
||||
- **Big-AGI Open** is ready and more productive and faster than ever, with:
|
||||
- **Beam 2**: multi-modal, program-based, follow-ups, save presets
|
||||
@@ -153,7 +159,7 @@ NOTE: this is a powerful tool - if you need a toy UI or clone, this ain't it.
|
||||
|
||||
<img width="830" height="385" alt="image" src="https://github.com/user-attachments/assets/ad52761d-7e3f-44d8-b41e-947ce8b4faa1" />
|
||||
|
||||
### Open links: 👉 [changelog](https://big-agi.com/changes) 👉 [installation](docs/installation.md) 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [documentation](docs/README.md)
|
||||
#### **Open** links: 👉 [changelog](https://big-agi.com/changes) 👉 [installation](docs/installation.md) 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [documentation](docs/README.md)
|
||||
|
||||
**For teams and institutions:** Need shared prompts, SSO, or managed deployments? Reach out at enrico@big-agi.com. We're actively collecting requirements from research groups and IT departments.
|
||||
|
||||
@@ -326,7 +332,7 @@ Configure 100s of AI models from 18+ providers:
|
||||
| Multimodal services | [Azure](https://azure.microsoft.com/en-us/products/ai-services/openai-service) · [Anthropic](https://anthropic.com) · [Google Gemini](https://ai.google.dev/) · [OpenAI](https://platform.openai.com/docs/overview) |
|
||||
| LLM services | [Alibaba](https://www.alibabacloud.com/en/product/modelstudio) · [DeepSeek](https://deepseek.com) · [Groq](https://wow.groq.com/) · [Mistral](https://mistral.ai/) · [Moonshot](https://www.moonshot.cn/) · [OpenPipe](https://openpipe.ai/) · [OpenRouter](https://openrouter.ai/) · [Perplexity](https://www.perplexity.ai/) · [Together AI](https://www.together.ai/) · [xAI](https://x.ai/) |
|
||||
| Image services | OpenAI · Google Gemini |
|
||||
| Speech services | [ElevenLabs](https://elevenlabs.io) (Voice synthesis / cloning) |
|
||||
| Speech services | [ElevenLabs](https://elevenlabs.io) · [OpenAI TTS](https://platform.openai.com/docs/guides/text-to-speech) · LocalAI · Browser (Web Speech API) |
|
||||
|
||||
### Additional Integrations
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const handlerNodeRoutes = (req: Request) => fetchRequestHandler({
|
||||
|
||||
// NOTE: the following statement breaks the build on non-pro deployments, and conditionals don't work either
|
||||
// so we resorted to raising the timeout from 10s to 60s in the vercel.json file instead
|
||||
export const maxDuration = 60;
|
||||
// export const maxDuration = 60;
|
||||
export const runtime = 'nodejs';
|
||||
export const dynamic = 'force-dynamic';
|
||||
export { handlerNodeRoutes as GET, handlerNodeRoutes as POST };
|
||||
@@ -14,5 +14,7 @@ const handlerEdgeRoutes = (req: Request) => fetchRequestHandler({
|
||||
: undefined,
|
||||
});
|
||||
|
||||
// NOTE: we don't set maxDuration explicitly here - however we set it in the Vercel project settings, raising to the limit of 300s
|
||||
// export const maxDuration = 60;
|
||||
export const runtime = 'edge';
|
||||
export { handlerEdgeRoutes as GET, handlerEdgeRoutes as POST };
|
||||
+1
-1
@@ -43,7 +43,7 @@ How to set up AI models and features in big-AGI.
|
||||
- **[Web Browsing](config-feature-browse.md)**: Enable web page download through third-party services or your own cloud
|
||||
- **Web Search**: Google Search API (see '[Environment Variables](environment-variables.md)')
|
||||
- **Image Generation**: GPT Image (gpt-image-1), DALL·E 3 and 2
|
||||
- **Voice Synthesis**: ElevenLabs API for voice generation
|
||||
- **Voice Synthesis**: ElevenLabs, OpenAI TTS, LocalAI, or browser Web Speech API
|
||||
|
||||
## Deployment & Customization
|
||||
|
||||
|
||||
@@ -132,10 +132,11 @@ Enable the app to Talk, Draw, and Google things up.
|
||||
|
||||
| Variable | Description |
|
||||
|:---------------------------|:------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Text-To-Speech** | [ElevenLabs](https://elevenlabs.io/) is a high quality speech synthesis service |
|
||||
| **Text-To-Speech** | ElevenLabs, OpenAI TTS, LocalAI, and browser Web Speech API are supported |
|
||||
| `ELEVENLABS_API_KEY` | ElevenLabs API Key - used for calls, etc. |
|
||||
| `ELEVENLABS_API_HOST` | Custom host for ElevenLabs |
|
||||
| `ELEVENLABS_VOICE_ID` | Default voice ID for ElevenLabs |
|
||||
| | *Note: OpenAI TTS and LocalAI TTS reuse credentials from your configured LLM services (no separate env vars needed)* |
|
||||
| **Google Custom Search** | [Google Programmable Search Engine](https://programmablesearchengine.google.com/about/) produces links to pages |
|
||||
| `GOOGLE_CLOUD_API_KEY` | Google Cloud API Key, used with the '/react' command - [Link to GCP](https://console.cloud.google.com/apis/credentials) |
|
||||
| `GOOGLE_CSE_ID` | Google Custom/Programmable Search Engine ID - [Link to PSE](https://programmablesearchengine.google.com/) |
|
||||
|
||||
@@ -14,6 +14,9 @@ Internal documentation for Big-AGI architecture and systems, for use by AI agent
|
||||
- **[AIX.md](modules/AIX.md)** - AIX streaming architecture documentation
|
||||
- **[AIX-callers-analysis.md](modules/AIX-callers-analysis.md)** - Analysis of AIX entry points, call chains, common and different rendering, error handling, etc.
|
||||
|
||||
#### CSF - Client-Side Fetch
|
||||
- **[CSF.md](systems/client-side-fetch.md)** - Direct browser-to-API communication for LLM requests
|
||||
|
||||
### Systems Documentation
|
||||
|
||||
#### Core Platform Systems
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# CSF - Client-Side Fetch
|
||||
|
||||
Client-Side Fetch (CSF) enables direct browser-to-API communication, bypassing the server for LLM requests. When enabled, the browser makes requests directly to vendor APIs (e.g., `api.openai.com`, `api.groq.com`) instead of routing through the Next.js server. This reduces latency, decreases server load, and is particularly useful for local models where the browser can communicate directly with Ollama or LM Studio.
|
||||
|
||||
## Implementation
|
||||
|
||||
CSF is implemented as an opt-in setting stored as `csf: boolean` in each vendor's service settings. The vendor interface exposes `csfAvailable?: (setup) => boolean` to determine if CSF can be enabled (typically checking if an API key or host is configured). The actual execution happens in `aix.client.direct-chatGenerate.ts` which dynamically imports when CSF is active, making direct fetch calls using the same wire protocols as the server.
|
||||
|
||||
All 16 supported vendors (OpenAI, Anthropic, Gemini, Ollama, LocalAI, Deepseek, Groq, Mistral, xAI, OpenRouter, Perplexity, Together AI, Alibaba, Moonshot, OpenPipe, LM Studio) support CSF. Cloud vendors require CORS support from the API provider (all tested vendors return `access-control-allow-origin: *`). Local vendors (Ollama, LocalAI, LM Studio) require CORS to be enabled on the local server.
|
||||
|
||||
## UI
|
||||
|
||||
The CSF toggle appears in each vendor's setup panel under "Advanced" settings, labeled "Direct Connection". It becomes visible when the prerequisites are met (API key present for cloud vendors, host configured for local vendors). The setting is managed through `useModelServiceClientSideFetch` hook which provides `csfAvailable`, `csfActive`, `csfToggle`, and `csfReset` for UI consumption.
|
||||
+1
-1
@@ -141,7 +141,7 @@ if (process.env.POSTHOG_API_KEY && process.env.POSTHOG_ENV_ID) {
|
||||
personalApiKey: process.env.POSTHOG_API_KEY,
|
||||
envId: process.env.POSTHOG_ENV_ID,
|
||||
host: 'https://us.i.posthog.com', // backtrace upload host
|
||||
logLevel: 'info',
|
||||
logLevel: 'error', // lowered, too noisy
|
||||
sourcemaps: {
|
||||
enabled: process.env.NODE_ENV === 'production',
|
||||
project: 'big-agi',
|
||||
|
||||
Generated
+175
-175
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "big-agi",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
@@ -19,7 +19,7 @@
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^5.18.0",
|
||||
"@mui/joy": "^5.0.0-beta.52",
|
||||
"@next/bundle-analyzer": "~15.1.8",
|
||||
"@next/bundle-analyzer": "~15.1.9",
|
||||
"@prisma/client": "~5.22.0",
|
||||
"@tanstack/react-query": "5.90.10",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
@@ -39,11 +39,11 @@
|
||||
"idb-keyval": "^6.2.2",
|
||||
"mammoth": "^1.11.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "~15.1.8",
|
||||
"next": "~15.1.9",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "5.4.54",
|
||||
"posthog-js": "^1.298.0",
|
||||
"posthog-node": "^5.14.0",
|
||||
"posthog-js": "^1.302.2",
|
||||
"posthog-node": "^5.17.2",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer-core": "^24.31.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -58,7 +58,7 @@
|
||||
"remark-mark-highlight": "^0.1.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.34.5",
|
||||
"superjson": "^2.2.5",
|
||||
"superjson": "^2.2.6",
|
||||
"tesseract.js": "^6.0.1",
|
||||
"tiktoken": "^1.0.22",
|
||||
"turndown": "^7.2.2",
|
||||
@@ -66,7 +66,7 @@
|
||||
"zustand": "5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@posthog/nextjs-config": "^1.6.0",
|
||||
"@posthog/nextjs-config": "^1.6.4",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
@@ -76,7 +76,7 @@
|
||||
"@types/turndown": "^5.0.6",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "~15.1.8",
|
||||
"eslint-config-next": "~15.1.9",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "~5.22.0",
|
||||
"typescript": "^5.9.3"
|
||||
@@ -1556,25 +1556,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mux/mux-player": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.9.0.tgz",
|
||||
"integrity": "sha512-OjRXdJFPstCoTipqJCXyC3e3PVoLp8jOheCaWxe2a8qvHkSs/sg+UoYegr++hAoLXXIyy2M7F6vi+tWq0W5bYA==",
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.9.1.tgz",
|
||||
"integrity": "sha512-buvulWJD9qevaCp02oTMezn6QKmV7OwU1WDt7In8IgOJ8GKn1ZWLvfSbWEd1s3Ea1tC33NastKVG8sYlJ7HNkA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mux/mux-video": "0.28.0",
|
||||
"@mux/playback-core": "0.31.3",
|
||||
"@mux/mux-video": "0.28.1",
|
||||
"@mux/playback-core": "0.31.4",
|
||||
"media-chrome": "~4.16.0",
|
||||
"player.style": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mux/mux-player-react": {
|
||||
"version": "3.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.9.0.tgz",
|
||||
"integrity": "sha512-AYBX89T02qOJ6rF4X2sB8WmPoHBQIrASvp6rxCf9wWYdp5lYtAjjTwaAM2aTlVEXSzdDPaOwgC7VmR7LhBJn3g==",
|
||||
"version": "3.9.1",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.9.1.tgz",
|
||||
"integrity": "sha512-4cumt+5ObUKIZioSajLLxIghCzOn4LBcFz2mNj1OGAZGckEk8jbFB/W4o2HRlOS+4cTCEKPnOQ1l7AgjGuzZ2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mux/mux-player": "3.9.0",
|
||||
"@mux/playback-core": "0.31.3",
|
||||
"@mux/mux-player": "3.9.1",
|
||||
"@mux/playback-core": "0.31.4",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -1592,32 +1592,32 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mux/mux-video": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.28.0.tgz",
|
||||
"integrity": "sha512-pqpoaoxHXsGX/l7jOZGZ7jOVBVdct8jq+Be1cQ9+n5N/XrkXIGNvO/liprQET3wRuWs2Xri5lWZFRe3ZkOkYHw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.28.1.tgz",
|
||||
"integrity": "sha512-lmxPmAPtUITZ+EButcZBoVs2FZbt7UgxYV/sCVOo0kGnOCrnmqjw1G3CW9tEcFST+NzdW/nIdzpRu9kg9FxBeg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mux/mux-data-google-ima": "0.2.8",
|
||||
"@mux/playback-core": "0.31.3",
|
||||
"@mux/playback-core": "0.31.4",
|
||||
"castable-video": "~1.1.11",
|
||||
"custom-media-element": "~1.4.5",
|
||||
"media-tracks": "~0.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@mux/playback-core": {
|
||||
"version": "0.31.3",
|
||||
"resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.31.3.tgz",
|
||||
"integrity": "sha512-IiNnF6LeE8xB5lXzwCUmHUi40q/88ook/YYKTwDsIMG/0zY8b9Ypyzw9ghsELZH0mzuFmv+rvm3ufOIoaIi9eA==",
|
||||
"version": "0.31.4",
|
||||
"resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.31.4.tgz",
|
||||
"integrity": "sha512-qQrNAAdJ7vjr1XEObE1hOUmuYngk/fjwmtYhpzkX4jJZwUC8I0rHjeFv7LXuCQD1D/mYJlWEpuyA0gPd6Y2eQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hls.js": "~1.6.13",
|
||||
"hls.js": "~1.6.15",
|
||||
"mux-embed": "^5.8.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.82.tgz",
|
||||
"integrity": "sha512-FGjyUBoF0sl1EenSiE4UV2WYu76q6F9GSYedq5EiOCOyGYoQ/Owulcv6rd7v/tWOpljDDtefXXIaOCJrVKem4w==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.83.tgz",
|
||||
"integrity": "sha512-f9GVB9VNc9vn/nroc9epXRNkVpvNPZh69+qzLJIm9DfruxFqX0/jsXG46OGWAJgkO4mN0HvFHjRROMXKVmPszg==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
@@ -1627,22 +1627,22 @@
|
||||
"node": ">= 10"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.82",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.82",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.82",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.82",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.82",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.82",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.82",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.82",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.82",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.82"
|
||||
"@napi-rs/canvas-android-arm64": "0.1.83",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.83",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.83",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.83",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.83",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.83",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.83",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.83",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.83",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.83"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.82.tgz",
|
||||
"integrity": "sha512-bvZhN0iI54ouaQOrgJV96H2q7J3ZoufnHf4E1fUaERwW29Rz4rgicohnAg4venwBJZYjGl5Yl3CGmlAl1LZowQ==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.83.tgz",
|
||||
"integrity": "sha512-TbKM2fh9zXjqFIU8bgMfzG7rkrIYdLKMafgPhFoPwKrpWk1glGbWP7LEu8Y/WrMDqTGFdRqUmuX89yQEzZbkiw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1656,9 +1656,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.82.tgz",
|
||||
"integrity": "sha512-InuBHKCyuFqhNwNr4gpqazo5Xp6ltKflqOLiROn4hqAS8u21xAHyYCJRgHwd+a5NKmutFTaRWeUIT/vxWbU/iw==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.83.tgz",
|
||||
"integrity": "sha512-gp8IDVUloPUmkepHly4xRUOfUJSFNvA4jR7ZRF5nk3YcGzegSFGeICiT4PnYyPgSKEhYAFe1Y2XNy0Mp6Tu8mQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1672,9 +1672,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.82.tgz",
|
||||
"integrity": "sha512-aQGV5Ynn96onSXcuvYb2y7TRXD/t4CL2EGmnGqvLyeJX1JLSNisKQlWN/1bPDDXymZYSdUqbXehj5qzBlOx+RQ==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.83.tgz",
|
||||
"integrity": "sha512-r4ZJxiP9OgUbdGZhPDEXD3hQ0aIPcVaywtcTXvamYxTU/SWKAbKVhFNTtpRe1J30oQ25gWyxTkUKSBgUkNzdnw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1688,9 +1688,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.82.tgz",
|
||||
"integrity": "sha512-YIUpmHWeHGGRhWitT1KJkgj/JPXPfc9ox8oUoyaGPxolLGPp5AxJkq8wIg8CdFGtutget968dtwmx71m8o3h5g==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.83.tgz",
|
||||
"integrity": "sha512-Uc6aSB05qH1r+9GUDxIE6F5ZF7L0nTFyyzq8ublWUZhw8fEGK8iy931ff1ByGFT04+xHJad1kBcL4R1ZEV8z7Q==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -1704,9 +1704,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.82.tgz",
|
||||
"integrity": "sha512-AwLzwLBgmvk7kWeUgItOUor/QyG31xqtD26w1tLpf4yE0hiXTGp23yc669aawjB6FzgIkjh1NKaNS52B7/qEBQ==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.83.tgz",
|
||||
"integrity": "sha512-eEeaJA7V5KOFq7W0GtoRVbd3ak8UZpK+XLkCgUiFGtlunNw+ZZW9Cr/92MXflGe7o3SqqMUg+f975LPxO/vsOQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1720,9 +1720,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.82.tgz",
|
||||
"integrity": "sha512-moZWuqepAwWBffdF4JDadt8TgBD02iMhG6I1FHZf8xO20AsIp9rB+p0B8Zma2h2vAF/YMjeFCDmW5un6+zZz9g==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.83.tgz",
|
||||
"integrity": "sha512-cAvonp5XpbatVGegF9lMQNchs3z5RH6EtamRVnQvtoRtwbzOMcdzwuLBqDBQxQF79MFbuZNkWj3YRJjZCjHVzw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1736,9 +1736,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.82.tgz",
|
||||
"integrity": "sha512-w9++2df2kG9eC9LWYIHIlMLuhIrKGQYfUxs97CwgxYjITeFakIRazI9LYWgVzEc98QZ9x9GQvlicFsrROV59MQ==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.83.tgz",
|
||||
"integrity": "sha512-WFUPQ9qZy31vmLxIJ3MfmHw+R2g/mLCgk8zmh7maJW8snV3vLPA7pZfIS65Dc61EVDp1vaBskwQ2RqPPzwkaew==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -1752,9 +1752,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.82.tgz",
|
||||
"integrity": "sha512-lZulOPwrRi6hEg/17CaqdwWEUfOlIJuhXxincx1aVzsVOCmyHf+xFq4i6liJl1P+x2v6Iz2Z/H5zHvXJCC7Bwg==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.83.tgz",
|
||||
"integrity": "sha512-X9YwIjsuy50WwOyYeNhEHjKHO8rrfH9M4U8vNqLuGmqsZdKua/GrUhdQGdjq7lTgdY3g4+Ta5jF8MzAa7UAs/g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1768,9 +1768,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.82.tgz",
|
||||
"integrity": "sha512-Be9Wf5RTv1w6GXlTph55K3PH3vsAh1Ax4T1FQY1UYM0QfD0yrwGdnJ8/fhqw7dEgMjd59zIbjJQC8C3msbGn5g==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.83.tgz",
|
||||
"integrity": "sha512-Vv2pLWQS8EnlSM1bstJ7vVhKA+mL4+my4sKUIn/bgIxB5O90dqiDhQjUDLP+5xn9ZMestRWDt3tdQEkGAmzq/A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1784,9 +1784,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.82",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.82.tgz",
|
||||
"integrity": "sha512-LN/i8VrvxTDmEEK1c10z2cdOTkWT76LlTGtyZe5Kr1sqoSomKeExAjbilnu1+oee5lZUgS5yfZ2LNlVhCeARuw==",
|
||||
"version": "0.1.83",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.83.tgz",
|
||||
"integrity": "sha512-K1TtjbScfRNYhq8dengLLufXGbtEtWdUXPV505uLFPovyGHzDUGXLFP/zUJzj6xWXwgUjHNLgEPIt7mye0zr6Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1813,24 +1813,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/bundle-analyzer": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.1.8.tgz",
|
||||
"integrity": "sha512-HNTcO3QhZ3RY3ZpHyqpl30WtAKZGF5WJgi/zVBMcgFuEk0k4cA/kiUCwApxAgkHcfvdBi3JytzQUXCAnSGljjQ==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-15.1.9.tgz",
|
||||
"integrity": "sha512-Jlq1oX7zetqXgPkXF/zjuHSRqlPI5q33W5MRLlQDPXi0YQ8e+5qwkq7m4V7y0cdTBiq7WhQiJvat89Jy0WjOKg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"webpack-bundle-analyzer": "4.10.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.8.tgz",
|
||||
"integrity": "sha512-Kd9zsi2ariJvtAvA5KapkzM/Qp9eXIcVqsuUMQHu9yYmhlGa9kyklf+6TQgVGSCbzsrApKCq9olyk51SmPnyLA==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.9.tgz",
|
||||
"integrity": "sha512-Te1wbiJ//I40T7UePOUG8QBwh+VVMCc0OTuqesOcD3849TVOVOyX4Hdrkx7wcpLpy/LOABIcGyLX5P/SzzXhFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/eslint-plugin-next": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.8.tgz",
|
||||
"integrity": "sha512-1VC0ctUmwQjI9gZJRQgw2c8GKkr4+DTXVu2yLje3PpFfnxnuuZ8Mrv1Whn9tF+CYkMHiEgCaqyGvT44qYNLVcw==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-15.1.9.tgz",
|
||||
"integrity": "sha512-H7CuatO2RXQQmm40cX3C6kFPNh/v6Dx2oEy1iKZKfubL0mhuuDMBLSUdwu5JgCP1mtuPBufK1h7WSIVjBADZtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1838,9 +1838,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.8.tgz",
|
||||
"integrity": "sha512-Mc++CDJgInIjIc1uA5+K6Lde8wObQztaXnuz6rOsN7tVgYBWvwKSa9wtXQDEETl46WNI8ksgpth2SR1DDo52xQ==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.9.tgz",
|
||||
"integrity": "sha512-sQF6MfW4nk0PwMYYq8xNgqyxZJGIJV16QqNDgaZ5ze9YoVzm4/YNx17X0exZudayjL9PF0/5RGffDtzXapch0Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1854,9 +1854,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.8.tgz",
|
||||
"integrity": "sha512-xmek+PBDN9K7rjDXCXgLsEzgmeJcevm3531pJOriqK+zh7k+yZEEE44G6lOnOqjVdc7ErLoDX6GxuHicDTatkw==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.9.tgz",
|
||||
"integrity": "sha512-fp0c1rB6jZvdSDhprOur36xzQvqelAkNRXM/An92sKjjtaJxjlqJR8jiQLQImPsClIu8amQn+ZzFwl1lsEf62w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1870,9 +1870,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.8.tgz",
|
||||
"integrity": "sha512-jrmutnfNjpLUB8bk+n2yJ8tzNdS+A8Q9UxzWUTCcxU08Q96eRtMY2/o/x1y2e5Yu79CgYPYuEe6E0SBOU+HU0Q==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.9.tgz",
|
||||
"integrity": "sha512-77rYykF6UtaXvxh9YyRIKoaYPI6/YX6cy8j1DL5/1XkjbfOwFDfTEhH7YGPqG/ePl+emBcbDYC2elgEqY2e+ag==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1886,9 +1886,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.8.tgz",
|
||||
"integrity": "sha512-lq1YacM3+Cyc8iwXD0h16AKp1e786KPFUpcIgFnsmjjOrMU5xBosBN2S395yD791P8i6q0qbbMnAoNOFLiaKhw==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.9.tgz",
|
||||
"integrity": "sha512-uZ1HazKcyWC7RA6j+S/8aYgvxmDqwnG+gE5S9MhY7BTMj7ahXKunpKuX8/BA2M7OvINLv7LTzoobQbw928p3WA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1902,9 +1902,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.8.tgz",
|
||||
"integrity": "sha512-fmllobaA+xGh8Rlb4CcF84sniDKADIXuAvLJ5nKtDCR0BbfQtHmK4xR2z1E+c9B6dbASW3MCXRj35KBmtAhhnw==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.9.tgz",
|
||||
"integrity": "sha512-gQIX1d3ct2RBlgbbWOrp+SHExmtmFm/HSW1Do5sSGMDyzbkYhS2sdq5LRDJWWsQu+/MqpgJHqJT6ORolKp/U1g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1918,9 +1918,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.8.tgz",
|
||||
"integrity": "sha512-PX0010o4k+w4M4Z38UfcxDGup1O36n10GUrENQANQMOjcE1cA6Gbb+/R6pBKeIqSOaxsPBIanDlbaQ7f6ylB8g==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.9.tgz",
|
||||
"integrity": "sha512-fJOwxAbCeq6Vo7pXZGDP6iA4+yIBGshp7ie2Evvge7S7lywyg7b/SGqcvWq/jYcmd0EbXdb7hBfdqSQwTtGTPg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1934,9 +1934,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.8.tgz",
|
||||
"integrity": "sha512-5zPbJAzaJvEo/UPR8ch4isVOjUP17/6qLU9TyF7Bl1EYN3c5zguAki5WN6QXMEjWAirerR2EFgE1B6VUHzt2Qg==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.9.tgz",
|
||||
"integrity": "sha512-crfbUkAd9PVg9nGfyjSzQbz82dPvc4pb1TeP0ZaAdGzTH6OfTU9kxidpFIogw0DYIEadI7hRSvuihy2NezkaNQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1950,9 +1950,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.8.tgz",
|
||||
"integrity": "sha512-tWR35z+E8rThPnwIMtOHwF/7lh7x1eB5p1wW0e5sWtyDIc+HRikxxuDc0U8B5G4YqGPX+O9NOgX35pCeKL28EA==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.9.tgz",
|
||||
"integrity": "sha512-SBB0oA4E2a0axUrUwLqXlLkSn+bRx9OWU6LheqmRrO53QEAJP7JquKh3kF0jRzmlYOWFZtQwyIWJMEJMtvvDcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2994,24 +2994,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.6.0.tgz",
|
||||
"integrity": "sha512-Tbh8UACwbb7jFdDC7wwXHtfNzO+4wKh3VbyMHmp2UBe6w1jliJixexTJNfkqdGZm+ht3M10mcKvGGPnoZ2zLBg==",
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.7.1.tgz",
|
||||
"integrity": "sha512-kjK0eFMIpKo9GXIbts8VtAknsoZ18oZorANdtuTj1CbgS28t4ZVq//HAWhnxEuXRTrtkd+SUJ6Ux3j2Af8NCuA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/nextjs-config": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/nextjs-config/-/nextjs-config-1.6.0.tgz",
|
||||
"integrity": "sha512-+IVUfPL0qF66+X2vWmx+OzUavKdxl9joYL8hbs93K7btEE49vVkJ6PC6Exeyr1VV/hJf9Y2lhrKseIMePT1BBw==",
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/nextjs-config/-/nextjs-config-1.6.4.tgz",
|
||||
"integrity": "sha512-N1MmP4Hh1Jp2PRSUrhQfj6t9CvTkZR4xeu+vZcyv/ZPU7yef8ZSF8cfzChyWt/IeoCX7bR4DpXnWtngfvX/nvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/cli": "~0.5.13",
|
||||
"@posthog/core": "1.6.0",
|
||||
"@posthog/webpack-plugin": "1.1.0",
|
||||
"@posthog/cli": "~0.5.16",
|
||||
"@posthog/core": "1.7.1",
|
||||
"@posthog/webpack-plugin": "1.1.4",
|
||||
"semver": "^7.7.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -3022,13 +3022,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/webpack-plugin": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/webpack-plugin/-/webpack-plugin-1.1.0.tgz",
|
||||
"integrity": "sha512-k6Ys20MtgZiESL+XE5C6DerBOcUTcYn0kAyxit7GK44akBMd8bAMDxMdX585/tfP99WMproRAmsElusNaI8cOg==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/webpack-plugin/-/webpack-plugin-1.1.4.tgz",
|
||||
"integrity": "sha512-bT2XDydzOccrSpe2x9dh+UtSoikXXdDcYKFaj62WcN5o+j6a6bpxwRRezfVT5EtRazQoPNtl7vEsCTVyAgkrbw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@posthog/cli": "~0.5.13",
|
||||
"@posthog/core": "1.6.0"
|
||||
"@posthog/cli": "~0.5.16",
|
||||
"@posthog/core": "1.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
@@ -4958,9 +4958,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/cloudflare-video-element": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/cloudflare-video-element/-/cloudflare-video-element-1.3.4.tgz",
|
||||
"integrity": "sha512-F9g+tXzGEXI6v6L48qXxr8vnR8+L6yy7IhpJxK++lpzuVekMHTixxH7/dzLuq6OacVGziU4RB5pzZYJ7/LYtJg==",
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/cloudflare-video-element/-/cloudflare-video-element-1.3.5.tgz",
|
||||
"integrity": "sha512-zj9gjJa6xW8MNrfc4oKuwgGS0njRLpOlQjdifbuNxvy8k4Y3pKCyKCMG2XIsjd2iQGhgjS57b1P5VWdJlxcXBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
@@ -5187,9 +5187,9 @@
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/dash-video-element": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/dash-video-element/-/dash-video-element-0.3.0.tgz",
|
||||
"integrity": "sha512-Pe+BxG153n+CH++3gmWMApVXEUs767YGxsRebdNZRSZdXjbv7OGbsitYbjNMC4QAjCWBvBjIclAYV4hoc7OWSQ==",
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/dash-video-element/-/dash-video-element-0.3.1.tgz",
|
||||
"integrity": "sha512-KSdCd6lqjum4LizHLtB2EGvaGr7YJU7SZekTTDHixRondaRNcm0t9W2V3I7/itNBzQwdDbC1cKkXryc8I8IViA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"custom-media-element": "^1.4.5",
|
||||
@@ -5928,13 +5928,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-next": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.8.tgz",
|
||||
"integrity": "sha512-xB9IwQLS5qlx5bWJKmYtyakq990uGrvCE6birTVsPpb8xusZHtA0eRJtnfjZQu9UFlAPAo4yoh7r6Qa1h+isBg==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-15.1.9.tgz",
|
||||
"integrity": "sha512-Yx0rjzk+o1SdKkzV0R/f/R3gvdpFA8FTtEKdFvESmiQ40uPnbRD52sOlZcTCkP5QLImnjVIit9qo8DYhrTSsIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "15.1.8",
|
||||
"@next/eslint-plugin-next": "15.1.9",
|
||||
"@rushstack/eslint-patch": "^1.10.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
@@ -7034,9 +7034,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hls-video-element": {
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/hls-video-element/-/hls-video-element-1.5.9.tgz",
|
||||
"integrity": "sha512-hDXhSI3IpSSODJF8ecNzDHKP5cqsouOuKDMjoTexyFePKr9KpXVCPAnVrXFTTH8VbOim4xkLtPkVJFt7J1Rs6w==",
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/hls-video-element/-/hls-video-element-1.5.10.tgz",
|
||||
"integrity": "sha512-FruzD03CaQlPlNKfXO1njPbo3jCSImAtFwX1OqgFbMllTQzdYqAHODiWan0q3mr1cYCONOWiAz2/nX+2qHHC+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"custom-media-element": "^1.4.5",
|
||||
@@ -8446,12 +8446,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/media-chrome": {
|
||||
"version": "4.16.0",
|
||||
"resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.16.0.tgz",
|
||||
"integrity": "sha512-c5xpTYcYo9nYsC/G/C1PyOcPXEL6iIaSR9MH3GncVuj4S90aHqvGbsyUWFDPPBKx5sCwWLxDnbszE/24eMT54g==",
|
||||
"version": "4.16.1",
|
||||
"resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.16.1.tgz",
|
||||
"integrity": "sha512-qtFlsy0lNDVCyVo//ZCAfRPKwgehfOYp6rThZzDUuZ5ypv41yqUfAxK+P9TOs+XSVWXATPTT2WRV0fbW0BH4vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ce-la-react": "^0.3.0"
|
||||
"ce-la-react": "^0.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/media-tracks": {
|
||||
@@ -9182,12 +9182,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.1.8",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.1.8.tgz",
|
||||
"integrity": "sha512-lToSu4zUZEQw1nHUsmmPpkrWM8Zk/J7RXL7E7x/Kbk9SZ6rz3VK8knTaJ+Vtdj6RV4XFZS1qp93hgm8z8j6UGw==",
|
||||
"version": "15.1.9",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.1.9.tgz",
|
||||
"integrity": "sha512-OoQpDPV2i3o5Hnn46nz2x6fzdFxFO+JsU4ZES12z65/feMjPHKKHLDVQ2NuEvTaXTRisix/G5+6hyTkwK329kA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.1.8",
|
||||
"@next/env": "15.1.9",
|
||||
"@swc/counter": "0.1.3",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"busboy": "1.6.0",
|
||||
@@ -9202,14 +9202,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.1.8",
|
||||
"@next/swc-darwin-x64": "15.1.8",
|
||||
"@next/swc-linux-arm64-gnu": "15.1.8",
|
||||
"@next/swc-linux-arm64-musl": "15.1.8",
|
||||
"@next/swc-linux-x64-gnu": "15.1.8",
|
||||
"@next/swc-linux-x64-musl": "15.1.8",
|
||||
"@next/swc-win32-arm64-msvc": "15.1.8",
|
||||
"@next/swc-win32-x64-msvc": "15.1.8",
|
||||
"@next/swc-darwin-arm64": "15.1.9",
|
||||
"@next/swc-darwin-x64": "15.1.9",
|
||||
"@next/swc-linux-arm64-gnu": "15.1.9",
|
||||
"@next/swc-linux-arm64-musl": "15.1.9",
|
||||
"@next/swc-linux-x64-gnu": "15.1.9",
|
||||
"@next/swc-linux-x64-musl": "15.1.9",
|
||||
"@next/swc-win32-arm64-msvc": "15.1.9",
|
||||
"@next/swc-win32-x64-msvc": "15.1.9",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -10207,12 +10207,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.298.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.298.0.tgz",
|
||||
"integrity": "sha512-Zwzsf7TO8qJ6DFLuUlQSsT/5OIOcxSBZlKOSk3satkEnwKdmnBXUuxgVXRHrvq1kj7OB2PVAPgZiQ8iHHj9DRA==",
|
||||
"version": "1.302.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.302.2.tgz",
|
||||
"integrity": "sha512-4voih22zQe7yHA7DynlQ3B7kgzJOaKIjzV7K3jJ2Qf+UDXd1ZgO7xYmLWYVtuKEvD1OXHbKk/fPhUTZeHEWpBw==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.6.0",
|
||||
"@posthog/core": "1.7.1",
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
@@ -10220,12 +10220,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "5.14.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.14.0.tgz",
|
||||
"integrity": "sha512-cKY2Wdtjx5wlIWL9/Im5/FT+zeKaYjtG94rfrIFKTVoCdwV7S9PaU8frLD/8TpGx1SwOFgw7QTjhpa/vnxTlww==",
|
||||
"version": "5.17.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.17.2.tgz",
|
||||
"integrity": "sha512-lz3YJOr0Nmiz0yHASaINEDHqoV+0bC3eD8aZAG+Ky292dAnVYul+ga/dMX8KCBXg8hHfKdxw0SztYD5j6dgUqQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.6.0"
|
||||
"@posthog/core": "1.7.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
@@ -11209,9 +11209,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/spotify-audio-element": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/spotify-audio-element/-/spotify-audio-element-1.0.3.tgz",
|
||||
"integrity": "sha512-I1/qD8cg/UnTlCIMiKSdZUJTyYfYhaqFK7LIVElc48eOqUUbVCaw1bqL8I6mJzdMJTh3eoNyF/ewvB7NoS/g9A==",
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/spotify-audio-element/-/spotify-audio-element-1.0.4.tgz",
|
||||
"integrity": "sha512-QdKrJPkYCzaNwwz2vN2eDGyoW0KmQFmnwVprB41mpMzj4qujbqr6pegEchQeTn0b5PceKiLoVu0pp2QDpTcWnw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
@@ -11502,9 +11502,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/superjson": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
|
||||
"integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==",
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz",
|
||||
"integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-anything": "^4"
|
||||
@@ -11641,9 +11641,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tiktok-video-element": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tiktok-video-element/-/tiktok-video-element-0.1.1.tgz",
|
||||
"integrity": "sha512-BaiVzvNz2UXDKTdSrXzrNf4q6Ecc+/utYUh7zdEu2jzYcJVDoqYbVfUl0bCfMoOeeAqg28vD/yN63Y3E9jOrlA==",
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/tiktok-video-element/-/tiktok-video-element-0.1.2.tgz",
|
||||
"integrity": "sha512-w6TboLm236XJKKiIXIhCbYCnUxbixBbaAoty0etaEAZ/2kHkVIdfZdv2oouMU/HGMsWCHI/VjQ3wU3MJ+s192Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiktoken": {
|
||||
@@ -11790,9 +11790,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/twitch-video-element": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/twitch-video-element/-/twitch-video-element-0.1.5.tgz",
|
||||
"integrity": "sha512-3UdWMa5ytWFdpgJAM6XEqqRK/1FvWdJVcKDOw4IHBPt4p52E+4fXT42fBdRZFfoxBPXQNZUDDNHFW8wIopD7Og==",
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/twitch-video-element/-/twitch-video-element-0.1.6.tgz",
|
||||
"integrity": "sha512-X7l8gy+DEFKJ/EztUwaVnAYwQN9fUJxPkOVJj2sE62sGvGU4DNLyvmOsmVulM+8Plc5dMg6hYIMNRAPaH+39Uw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
@@ -12181,9 +12181,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vimeo-video-element": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/vimeo-video-element/-/vimeo-video-element-1.6.1.tgz",
|
||||
"integrity": "sha512-UwDLzhgg98pct1xb6799I1vRDXIzaAX6rs1TG/QOf6y+VrXpTFrI7mYz2gnj9QCtBcGK68f4z64A+MRYRsLJaQ==",
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/vimeo-video-element/-/vimeo-video-element-1.6.2.tgz",
|
||||
"integrity": "sha512-/l+/ugpFdCMLUDfUanzP8aSxiaNNsWvecnoIi1ziWd1shS0q7UAG3dqV9mFwO6l2GMOKtpl9uPHNHpV6RloSQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vimeo/player": "2.29.0"
|
||||
@@ -12432,9 +12432,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wistia-video-element": {
|
||||
"version": "1.3.5",
|
||||
"resolved": "https://registry.npmjs.org/wistia-video-element/-/wistia-video-element-1.3.5.tgz",
|
||||
"integrity": "sha512-aIG0xEtclPb9xfklAkOwHFv/BMiH3Ql0yWWKQ1XyUCoSDaF3sOD+JNLmakOChvn2LLUX7FqH/mYb8bXT4ACnMw==",
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/wistia-video-element/-/wistia-video-element-1.3.6.tgz",
|
||||
"integrity": "sha512-wPizIpXDaCs6fvDzhU3MBtEpxIqhgXlu00kSrKgmjPb5DRqZt927xZZjE1qm81Df40d445u4a/mRKX5I66zaYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"super-media-element": "~1.4.2"
|
||||
@@ -12589,9 +12589,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/youtube-video-element": {
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/youtube-video-element/-/youtube-video-element-1.8.0.tgz",
|
||||
"integrity": "sha512-u3M0MgO+KUtVwIyKJXZXXJ0As0k6d5NflOrh1GjyG8NNOp+liW2nFU29hpXeUcxUWbVKhudIYd39hMVeEgCilQ==",
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/youtube-video-element/-/youtube-video-element-1.8.1.tgz",
|
||||
"integrity": "sha512-+5UuAGaj+5AnBf39huLVpy/4dLtR0rmJP1TxOHVZ81bac4ZHFpTtQ4Dz2FAn2GPnfXISezvUEaQoAdFW4hH9Xg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zlibjs": {
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "2.0.1",
|
||||
"version": "2.0.2",
|
||||
"private": true,
|
||||
"author": "Enrico Ros <enrico.ros@gmail.com>",
|
||||
"repository": "https://github.com/enricoros/big-agi",
|
||||
@@ -31,7 +31,7 @@
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@mui/icons-material": "^5.18.0",
|
||||
"@mui/joy": "^5.0.0-beta.52",
|
||||
"@next/bundle-analyzer": "~15.1.8",
|
||||
"@next/bundle-analyzer": "~15.1.9",
|
||||
"@prisma/client": "~5.22.0",
|
||||
"@tanstack/react-query": "5.90.10",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
@@ -51,11 +51,11 @@
|
||||
"idb-keyval": "^6.2.2",
|
||||
"mammoth": "^1.11.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next": "~15.1.8",
|
||||
"next": "~15.1.9",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "5.4.54",
|
||||
"posthog-js": "^1.298.0",
|
||||
"posthog-node": "^5.14.0",
|
||||
"posthog-js": "^1.302.2",
|
||||
"posthog-node": "^5.17.2",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer-core": "^24.31.0",
|
||||
"react": "^18.3.1",
|
||||
@@ -70,7 +70,7 @@
|
||||
"remark-mark-highlight": "^0.1.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.34.5",
|
||||
"superjson": "^2.2.5",
|
||||
"superjson": "^2.2.6",
|
||||
"tesseract.js": "^6.0.1",
|
||||
"tiktoken": "^1.0.22",
|
||||
"turndown": "^7.2.2",
|
||||
@@ -78,7 +78,7 @@
|
||||
"zustand": "5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@posthog/nextjs-config": "^1.6.0",
|
||||
"@posthog/nextjs-config": "^1.6.4",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
@@ -88,7 +88,7 @@
|
||||
"@types/turndown": "^5.0.6",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "~15.1.8",
|
||||
"eslint-config-next": "~15.1.9",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "~5.22.0",
|
||||
"typescript": "^5.9.3"
|
||||
|
||||
@@ -18,7 +18,7 @@ import { ROUTE_APP_CHAT, ROUTE_INDEX } from '~/common/app.routes';
|
||||
import { Release } from '~/common/app.release';
|
||||
|
||||
// capabilities access
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs, useCapabilityTextToImage } from '~/common/components/useCapabilities';
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityTextToImage } from '~/common/components/useCapabilities';
|
||||
|
||||
// stores access
|
||||
import { getLLMsDebugInfo } from '~/common/stores/llms/store-llms';
|
||||
@@ -95,7 +95,6 @@ function AppDebug() {
|
||||
const cProduct = {
|
||||
capabilities: {
|
||||
mic: useCapabilityBrowserSpeechRecognition(),
|
||||
elevenLabs: useCapabilityElevenLabs(),
|
||||
textToImage: useCapabilityTextToImage(),
|
||||
},
|
||||
models: getLLMsDebugInfo(),
|
||||
|
||||
@@ -6,13 +6,15 @@ import ChatIcon from '@mui/icons-material/Chat';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import MicIcon from '@mui/icons-material/Mic';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
|
||||
import { useSpeexGlobalEngine } from '~/modules/speex/store-module-speex';
|
||||
|
||||
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
|
||||
import { animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { navigateBack } from '~/common/app.routes';
|
||||
import { optimaOpenPreferences } from '~/common/layout/optima/useOptima';
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useCapabilityBrowserSpeechRecognition } from '~/common/components/useCapabilities';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useUICounter } from '~/common/stores/store-ui';
|
||||
|
||||
@@ -45,7 +47,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
|
||||
// external state
|
||||
const recognition = useCapabilityBrowserSpeechRecognition();
|
||||
const synthesis = useCapabilityElevenLabs();
|
||||
const speexGlobalEngine = useSpeexGlobalEngine();
|
||||
const chatIsEmpty = useChatStore(state => {
|
||||
if (!props.conversationId)
|
||||
return false;
|
||||
@@ -58,15 +60,16 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
const outOfTheBlue = !props.conversationId;
|
||||
const overriddenEmptyChat = chatEmptyOverride || !chatIsEmpty;
|
||||
const overriddenRecognition = recognitionOverride || recognition.mayWork;
|
||||
const allGood = overriddenEmptyChat && overriddenRecognition && synthesis.mayWork;
|
||||
const fatalGood = overriddenRecognition && synthesis.mayWork;
|
||||
const synthesisShallWork = !!speexGlobalEngine;
|
||||
const allGood = overriddenEmptyChat && overriddenRecognition && synthesisShallWork;
|
||||
const fatalGood = overriddenRecognition && synthesisShallWork;
|
||||
|
||||
|
||||
const handleOverrideChatEmpty = React.useCallback(() => setChatEmptyOverride(true), []);
|
||||
|
||||
const handleOverrideRecognition = React.useCallback(() => setRecognitionOverride(true), []);
|
||||
|
||||
const handleConfigureElevenLabs = React.useCallback(() => optimaOpenPreferences('voice'), []);
|
||||
const handleConfigureVoice = React.useCallback(() => optimaOpenPreferences('voice'), []);
|
||||
|
||||
const handleFinishButton = React.useCallback(() => {
|
||||
if (!allGood)
|
||||
@@ -128,17 +131,17 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
|
||||
{/* Text to Speech status */}
|
||||
<StatusCard
|
||||
icon={<RecordVoiceOverTwoToneIcon />}
|
||||
icon={<PhVoice />}
|
||||
text={
|
||||
(synthesis.mayWork ? 'Voice synthesis should be ready.' : 'There might be an issue with ElevenLabs voice synthesis.')
|
||||
+ (synthesis.isConfiguredServerSide ? '' : (synthesis.isConfiguredClientSide ? '' : ' Please add your API key in the settings.'))
|
||||
(synthesisShallWork ? 'Voice synthesis should be ready.' : 'There might be an issue with voice synthesis.')
|
||||
// + (synthesis.isConfiguredServerSide ? '' : (synthesis.isConfiguredClientSide ? '' : ' Please add your API key in the settings.'))
|
||||
}
|
||||
button={synthesis.mayWork ? undefined : (
|
||||
<Button variant='outlined' onClick={handleConfigureElevenLabs} sx={{ mx: 1 }}>
|
||||
button={synthesisShallWork ? undefined : (
|
||||
<Button variant='outlined' onClick={handleConfigureVoice} sx={{ mx: 1 }}>
|
||||
Configure
|
||||
</Button>
|
||||
)}
|
||||
hasIssue={!synthesis.mayWork}
|
||||
hasIssue={!synthesisShallWork}
|
||||
/>
|
||||
|
||||
{/*<Typography>*/}
|
||||
|
||||
@@ -317,7 +317,7 @@ export function Contacts(props: { setCallIntent: (intent: AppCallIntent) => void
|
||||
issue={354}
|
||||
text='Call App: Support thread and compatibility matrix'
|
||||
note={<>
|
||||
Voice input uses the HTML Web Speech API, and speech output requires an ElevenLabs API Key.
|
||||
Voice input uses the HTML Web Speech API.
|
||||
</>}
|
||||
// note2='Please report any issues you encounter'
|
||||
sx={{
|
||||
|
||||
+17
-30
@@ -7,22 +7,22 @@ import CallEndIcon from '@mui/icons-material/CallEnd';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
import MicIcon from '@mui/icons-material/Mic';
|
||||
import MicNoneIcon from '@mui/icons-material/MicNone';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { useChatLLMDropdown } from '../chat/components/layout-bar/useLLMDropdown';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../data';
|
||||
import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { AixChatGenerateContent_DMessageGuts, aixChatGenerateContent_DMessage_FromConversation } from '~/modules/aix/client/aix.client';
|
||||
import { useElevenLabsVoiceDropdown } from '~/modules/elevenlabs/useElevenLabsVoiceDropdown';
|
||||
|
||||
import { aixChatGenerateContent_DMessage_FromConversation, AixChatGenerateContent_DMessageGuts } from '~/modules/aix/client/aix.client';
|
||||
import { speakText } from '~/modules/speex/speex.client';
|
||||
|
||||
import type { OptimaBarControlMethods } from '~/common/layout/optima/bar/OptimaBarDropdown';
|
||||
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { OptimaPanelGroupedList } from '~/common/layout/optima/panel/OptimaPanelGroupedList';
|
||||
import { OptimaPanelIn, OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
|
||||
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/speechrecognition/useSpeechRecognition';
|
||||
import { conversationTitle, remapMessagesSysToUsr } from '~/common/stores/chat/chat.conversation';
|
||||
import { createDMessageFromFragments, createDMessageTextContent, DMessage, messageFragmentsReduceText, messageWasInterruptedAtStart } from '~/common/stores/chat/chat.message';
|
||||
@@ -43,18 +43,13 @@ import { useAppCallStore } from './state/store-app-call';
|
||||
function CallMenu(props: {
|
||||
pushToTalk: boolean,
|
||||
setPushToTalk: (pushToTalk: boolean) => void,
|
||||
override: boolean,
|
||||
setOverride: (overridePersonaVoice: boolean) => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { grayUI, toggleGrayUI } = useAppCallStore();
|
||||
const { voicesDropdown } = useElevenLabsVoiceDropdown(false, !props.override);
|
||||
|
||||
const handlePushToTalkToggle = () => props.setPushToTalk(!props.pushToTalk);
|
||||
|
||||
const handleChangeVoiceToggle = () => props.setOverride(!props.override);
|
||||
|
||||
return <OptimaPanelGroupedList title='Call'>
|
||||
|
||||
<MenuItem onClick={handlePushToTalkToggle}>
|
||||
@@ -63,17 +58,6 @@ function CallMenu(props: {
|
||||
<Switch checked={props.pushToTalk} onChange={handlePushToTalkToggle} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleChangeVoiceToggle}>
|
||||
<ListItemDecorator><RecordVoiceOverTwoToneIcon /></ListItemDecorator>
|
||||
Change Voice
|
||||
<Switch checked={props.override} onChange={handleChangeVoiceToggle} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem>
|
||||
<ListItemDecorator>{' '}</ListItemDecorator>
|
||||
{voicesDropdown}
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider />
|
||||
|
||||
<MenuItem onClick={toggleGrayUI}>
|
||||
@@ -98,7 +82,6 @@ export function Telephone(props: {
|
||||
const [avatarClickCount, setAvatarClickCount] = React.useState<number>(0);// const [micMuted, setMicMuted] = React.useState(false);
|
||||
const [callElapsedTime, setCallElapsedTime] = React.useState<string>('00:00');
|
||||
const [callMessages, setCallMessages] = React.useState<DMessage[]>([]);
|
||||
const [overridePersonaVoice, setOverridePersonaVoice] = React.useState<boolean>(false);
|
||||
const [personaTextInterim, setPersonaTextInterim] = React.useState<string | null>(null);
|
||||
const [pushToTalk, setPushToTalk] = React.useState(true);
|
||||
const [stage, setStage] = React.useState<'ring' | 'declined' | 'connected' | 'ended'>('ring');
|
||||
@@ -118,7 +101,7 @@ export function Telephone(props: {
|
||||
}));
|
||||
const persona = SystemPurposes[props.callIntent.personaId as SystemPurposeId] ?? undefined;
|
||||
const personaCallStarters = persona?.call?.starters ?? undefined;
|
||||
const personaVoiceId = overridePersonaVoice ? undefined : (persona?.voices?.elevenLabs?.voiceId ?? undefined);
|
||||
// const personaVoiceSelector = React.useMemo(() => personaGetVoiceSelector(persona), [persona]);
|
||||
const personaSystemMessage = persona?.systemMessage ?? undefined;
|
||||
|
||||
// hooks and speech
|
||||
@@ -165,7 +148,6 @@ export function Telephone(props: {
|
||||
};
|
||||
|
||||
// [E] pickup -> seed message and call timer
|
||||
// FIXME: Overriding the voice will reset the call - not a desired behavior
|
||||
React.useEffect(() => {
|
||||
if (!isConnected) return;
|
||||
|
||||
@@ -185,11 +167,14 @@ export function Telephone(props: {
|
||||
|
||||
setCallMessages([createDMessageTextContent('assistant', firstMessage)]); // [state] set assistant:hello message
|
||||
|
||||
// fire/forget
|
||||
void elevenLabsSpeakText(firstMessage, personaVoiceId, true, true);
|
||||
// fire/forget - use 'fast' priority for real-time conversation
|
||||
void speakText(firstMessage,
|
||||
undefined,
|
||||
{ label: 'Call', priority: 'fast' },
|
||||
);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [isConnected, personaCallStarters, personaVoiceId]);
|
||||
}, [isConnected, personaCallStarters]);
|
||||
|
||||
// [E] persona streaming response - upon new user message
|
||||
React.useEffect(() => {
|
||||
@@ -270,9 +255,12 @@ export function Telephone(props: {
|
||||
fullMessage.generator = status.lastDMessage.generator;
|
||||
setCallMessages(messages => [...messages, fullMessage]); // [state] append assistant:call_response
|
||||
|
||||
// fire/forget
|
||||
// fire/forget - use 'fast' priority for real-time conversation
|
||||
if (status.outcome === 'success' && finalText?.length >= 1)
|
||||
void elevenLabsSpeakText(finalText, personaVoiceId, true, true);
|
||||
void speakText(finalText,
|
||||
undefined,
|
||||
{ label: 'Call', priority: 'fast' },
|
||||
);
|
||||
|
||||
}).catch((err: DOMException) => {
|
||||
if (err?.name !== 'AbortError') {
|
||||
@@ -288,7 +276,7 @@ export function Telephone(props: {
|
||||
responseAbortController.current?.abort();
|
||||
responseAbortController.current = null;
|
||||
};
|
||||
}, [isConnected, callMessages, modelId, personaVoiceId, personaSystemMessage, reMessages]);
|
||||
}, [callMessages, isConnected, modelId, personaSystemMessage, reMessages]);
|
||||
|
||||
// [E] Message interrupter
|
||||
const abortTrigger = isConnected && recognitionState.hasSpeech;
|
||||
@@ -325,7 +313,6 @@ export function Telephone(props: {
|
||||
<OptimaPanelIn>
|
||||
<CallMenu
|
||||
pushToTalk={pushToTalk} setPushToTalk={setPushToTalk}
|
||||
override={overridePersonaVoice} setOverride={setOverridePersonaVoice}
|
||||
/>
|
||||
</OptimaPanelIn>
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import type { TradeConfig } from '~/modules/trade/TradeModal';
|
||||
import { downloadSingleChat, importConversationsFromFilesAtRest, openConversationsAtRestPicker } from '~/modules/trade/trade.client';
|
||||
import { imaginePromptFromTextOrThrow } from '~/modules/aifn/imagine/imaginePromptFromText';
|
||||
import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { useAreBeamsOpen } from '~/modules/beam/store-beam.hooks';
|
||||
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
|
||||
@@ -346,11 +345,6 @@ export function AppChat() {
|
||||
});
|
||||
}, [handleExecuteAndOutcome]);
|
||||
|
||||
const handleTextSpeak = React.useCallback(async (text: string): Promise<void> => {
|
||||
await elevenLabsSpeakText(text, undefined, true, true);
|
||||
}, []);
|
||||
|
||||
|
||||
// Chat actions
|
||||
|
||||
const handleConversationNewInFocusedPane = React.useCallback((forceNoRecycle: boolean, isIncognito: boolean) => {
|
||||
@@ -725,7 +719,6 @@ export function AppChat() {
|
||||
onConversationNew={handleConversationNewInFocusedPane}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleImagineFromText}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
sx={chatMessageListSx}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Box, List } from '@mui/joy';
|
||||
import type { SystemPurposeExample } from '../../../data';
|
||||
|
||||
import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { speakText } from '~/modules/speex/speex.client';
|
||||
|
||||
import type { ConversationHandler } from '~/common/chat-overlay/ConversationHandler';
|
||||
import type { DLLMContextTokens } from '~/common/stores/llms/llms.types';
|
||||
@@ -17,7 +18,7 @@ import { createDMessageFromFragments, createDMessageTextContent, DMessage, DMess
|
||||
import { createTextContentFragment, DMessageFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
|
||||
import { openFileForAttaching } from '~/common/components/ButtonAttachFiles';
|
||||
import { optimaOpenPreferences } from '~/common/layout/optima/useOptima';
|
||||
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { stripHtmlColors } from '~/common/util/clipboardUtils';
|
||||
import { useChatOverlayStore } from '~/common/chat-overlay/store-perchat_vanilla';
|
||||
import { useChatStore } from '~/common/stores/chat/store-chats';
|
||||
import { useScrollToBottom } from '~/common/scroll-to-bottom/useScrollToBottom';
|
||||
@@ -50,7 +51,6 @@ export function ChatMessageList(props: {
|
||||
onConversationNew: (forceNoRecycle: boolean, isIncognito: boolean) => void,
|
||||
onTextDiagram: (diagramConfig: DiagramConfig | null) => void,
|
||||
onTextImagine: (conversationId: DConversationId, selectedText: string) => Promise<void>,
|
||||
onTextSpeak: (selectedText: string) => Promise<void>,
|
||||
setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
|
||||
sx?: SxProps,
|
||||
}) {
|
||||
@@ -75,10 +75,9 @@ export function ChatMessageList(props: {
|
||||
_composerInReferenceToCount: state.inReferenceTo?.length ?? 0,
|
||||
ephemerals: state.ephemerals?.length ? state.ephemerals : null,
|
||||
})));
|
||||
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
|
||||
|
||||
// derived state
|
||||
const { conversationHandler, conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
|
||||
const { conversationHandler, conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine } = props;
|
||||
const composerCanAddInReferenceTo = _composerInReferenceToCount < 5;
|
||||
const composerHasInReferenceto = _composerInReferenceToCount > 0;
|
||||
|
||||
@@ -212,12 +211,15 @@ export function ChatMessageList(props: {
|
||||
}, [capabilityHasT2I, conversationId, onTextImagine]);
|
||||
|
||||
const handleTextSpeak = React.useCallback(async (text: string) => {
|
||||
if (!isSpeakable)
|
||||
return optimaOpenPreferences('voice');
|
||||
// sandwich the speaking with the indicator
|
||||
setIsSpeaking(true);
|
||||
await onTextSpeak(text);
|
||||
const result = await speakText(text, undefined, { label: 'Chat speak' });
|
||||
setIsSpeaking(false);
|
||||
}, [isSpeakable, onTextSpeak]);
|
||||
|
||||
// open voice preferences
|
||||
if (!result.success && (result.errorType === 'tts-no-engine' || result.errorType === 'tts-unconfigured'))
|
||||
optimaOpenPreferences('voice');
|
||||
}, []);
|
||||
|
||||
|
||||
// operate on the local selection set
|
||||
@@ -286,6 +288,22 @@ export function ChatMessageList(props: {
|
||||
}, [conversationId, notifyBooting]);
|
||||
|
||||
|
||||
// "ctrl + c" copy handler - strip theme-dependent colors from copied content (keep formatting like font sizes)
|
||||
// similar to ChatMessage.handleOpsCopy
|
||||
const handleCopyHTMLWithoutColors = React.useCallback((event: React.ClipboardEvent) => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed) return;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.appendChild(selection.getRangeAt(0).cloneContents());
|
||||
stripHtmlColors(div);
|
||||
|
||||
event.clipboardData?.setData('text/html', div.innerHTML);
|
||||
event.clipboardData?.setData('text/plain', selection.toString());
|
||||
event.preventDefault();
|
||||
}, []);
|
||||
|
||||
|
||||
// style memo
|
||||
const listSx: SxProps = React.useMemo(() => ({
|
||||
p: 0,
|
||||
@@ -322,7 +340,7 @@ export function ChatMessageList(props: {
|
||||
);
|
||||
|
||||
return (
|
||||
<List role='chat-messages-list' sx={listSx}>
|
||||
<List role='chat-messages-list' sx={listSx} onCopy={handleCopyHTMLWithoutColors}>
|
||||
|
||||
{props.isMessageSelectionMode && (
|
||||
<MessagesSelectionHeader
|
||||
@@ -377,7 +395,7 @@ export function ChatMessageList(props: {
|
||||
onMessageTruncate={handleMessageTruncate}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={capabilityHasT2I ? handleTextImagine : undefined}
|
||||
onTextSpeak={isSpeakable ? handleTextSpeak : undefined}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
/>
|
||||
|
||||
);
|
||||
|
||||
@@ -905,7 +905,7 @@ export function Composer(props: {
|
||||
)}
|
||||
|
||||
{!showChatInReferenceTo && !isDraw && tokenLimit > 0 && (
|
||||
<TokenBadgeMemo hideBelowDollars={0.005} chatPricing={tokenChatPricing} direct={tokensComposer} history={tokensHistory} responseMax={tokensResponseMax} limit={tokenLimit} showCost={labsShowCost} enableHover={!isMobile} showExcess absoluteBottomRight />
|
||||
<TokenBadgeMemo hideBelowDollars={0.01} chatPricing={tokenChatPricing} direct={tokensComposer} history={tokensHistory} responseMax={tokensResponseMax} limit={tokenLimit} showCost={labsShowCost} enableHover={!isMobile} showExcess absoluteBottomRight />
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -47,9 +47,9 @@ function TokenBadge(props: {
|
||||
const showAltCosts = !!props.showCost && !!costMax && costMin !== undefined;
|
||||
if (showAltCosts) {
|
||||
// Note: switched to 'min cost (>= ...)' on mobile as well, to restore the former behavior, just uncomment the !props.enableHover (a proxy for isMobile)
|
||||
badgeValue = (/*!props.enableHover ||*/ isHovering)
|
||||
? '< ' + formatModelsCost(costMax)
|
||||
: '> ' + formatModelsCost(costMin);
|
||||
badgeValue =
|
||||
// (/*!props.enableHover ||*/ isHovering) ? '< ' + formatModelsCost(costMax) :
|
||||
'> ' + formatModelsCost(costMin);
|
||||
} else {
|
||||
|
||||
// show the direct tokens, unless we exceed the limit and 'showExcess' is enabled
|
||||
@@ -77,7 +77,7 @@ function TokenBadge(props: {
|
||||
slotProps={{
|
||||
root: {
|
||||
sx: {
|
||||
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: 8 }),
|
||||
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: '1rem' }),
|
||||
cursor: 'help',
|
||||
...(shallInvisible && {
|
||||
opacity: 0,
|
||||
@@ -92,6 +92,13 @@ function TokenBadge(props: {
|
||||
fontFamily: 'code',
|
||||
fontSize: 'xs',
|
||||
...((props.absoluteBottomRight || props.inline) && { position: 'static', transform: 'none' }),
|
||||
// make it transparent over text
|
||||
// backgroundColor: `rgb(var(--joy-palette-${color}-lightChannel) / 15%)`, // similar to success.50
|
||||
background: 'transparent',
|
||||
boxShadow: 'none', // outline
|
||||
'&:hover': {
|
||||
backgroundColor: `${color}.softHoverBg`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -21,7 +21,6 @@ import InsertLinkIcon from '@mui/icons-material/InsertLink';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
||||
import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined';
|
||||
import RecordVoiceOverOutlinedIcon from '@mui/icons-material/RecordVoiceOverOutlined';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import ReplyAllRoundedIcon from '@mui/icons-material/ReplyAllRounded';
|
||||
import ReplyRoundedIcon from '@mui/icons-material/ReplyRounded';
|
||||
@@ -40,11 +39,12 @@ import { CloseablePopup } from '~/common/components/CloseablePopup';
|
||||
import { DMessage, DMessageId, DMessageUserFlag, DMetaReferenceItem, MESSAGE_FLAG_AIX_SKIP, MESSAGE_FLAG_NOTIFY_COMPLETE, MESSAGE_FLAG_STARRED, MESSAGE_FLAG_VND_ANT_CACHE_AUTO, MESSAGE_FLAG_VND_ANT_CACHE_USER, messageFragmentsReduceText, messageHasUserFlag } from '~/common/stores/chat/chat.message';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { MarkHighlightIcon } from '~/common/components/icons/MarkHighlightIcon';
|
||||
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
|
||||
import { Release } from '~/common/app.release';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { adjustContentScaling, themeScalingMap, themeZIndexChatBubble } from '~/common/app.theme';
|
||||
import { avatarIconSx, makeMessageAvatarIcon, messageBackground, useMessageAvatarLabel } from '~/common/util/dMessageUtils';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { copyToClipboard, copyToClipboardHtmlMinusColors } from '~/common/util/clipboardUtils';
|
||||
import { createTextContentFragment, DMessageFragment, DMessageFragmentId, updateFragmentWithEditedText } from '~/common/stores/chat/chat.fragments';
|
||||
import { useFragmentBuckets } from '~/common/stores/chat/hooks/useFragmentBuckets';
|
||||
import { useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
@@ -315,7 +315,12 @@ export function ChatMessage(props: {
|
||||
const handleCloseOpsMenu = React.useCallback(() => setOpsMenuAnchor(null), []);
|
||||
|
||||
const handleOpsCopy = (e: React.MouseEvent) => {
|
||||
copyToClipboard(textSubject, 'Text');
|
||||
const html = blocksRendererRef.current?.innerHTML;
|
||||
if (html) {
|
||||
// same as ChatMessageList.handleCopyHTMLWithoutColors
|
||||
copyToClipboardHtmlMinusColors(html, textSubject, 'Message');
|
||||
} else
|
||||
copyToClipboard(textSubject, 'Text');
|
||||
e.preventDefault();
|
||||
handleCloseOpsMenu();
|
||||
closeContextMenu();
|
||||
@@ -1027,7 +1032,7 @@ export function ChatMessage(props: {
|
||||
)}
|
||||
{!!props.onTextSpeak && (
|
||||
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverOutlinedIcon />}</ListItemDecorator>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <PhVoice />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>
|
||||
)}
|
||||
@@ -1155,7 +1160,7 @@ export function ChatMessage(props: {
|
||||
</Tooltip>}
|
||||
{!!props.onTextSpeak && <Tooltip disableInteractive arrow placement='top' title='Speak'>
|
||||
<IconButton color='success' onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
{!props.isSpeaking ? <RecordVoiceOverOutlinedIcon /> : <CircularProgress sx={{ '--CircularProgress-size': '16px' }} />}
|
||||
{!props.isSpeaking ? <PhVoice /> : <CircularProgress sx={{ '--CircularProgress-size': '16px' }} />}
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{(!!props.onTextDiagram || !!props.onTextImagine || !!props.onTextSpeak) && <Divider />}
|
||||
@@ -1195,7 +1200,7 @@ export function ChatMessage(props: {
|
||||
Auto-Draw
|
||||
</MenuItem>}
|
||||
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverOutlinedIcon />}</ListItemDecorator>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <PhVoice />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>}
|
||||
</CloseablePopup>
|
||||
|
||||
+2
-2
@@ -7,13 +7,13 @@ import CodeIcon from '@mui/icons-material/Code';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import RecordVoiceOverOutlinedIcon from '@mui/icons-material/RecordVoiceOverOutlined';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
import TextureIcon from '@mui/icons-material/Texture';
|
||||
|
||||
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { DMessageAttachmentFragment, DMessageFragmentId, DVMimeType, isDocPart } from '~/common/stores/chat/chat.fragments';
|
||||
import { LiveFileIcon } from '~/common/livefile/liveFile.icons';
|
||||
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { ellipsizeMiddle } from '~/common/util/textUtils';
|
||||
import { useLiveFileMetadata } from '~/common/livefile/useLiveFileMetadata';
|
||||
@@ -41,7 +41,7 @@ export function buttonIconForFragment(part: DMessageAttachmentFragment['part']):
|
||||
case 'image':
|
||||
return ImageOutlinedIcon;
|
||||
case 'audio':
|
||||
return RecordVoiceOverOutlinedIcon;
|
||||
return PhVoice;
|
||||
default:
|
||||
const _exhaustiveCheck: never = assetType;
|
||||
return TextureIcon; // missing zync asset type
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ContentScaling } from '~/common/app.theme';
|
||||
import type { DMessageErrorPart } from '~/common/stores/chat/chat.fragments';
|
||||
import type { DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { BlockPartError_NetDisconnected } from './BlockPartError_NetDisconnected';
|
||||
import { BlockPartError_RequestExceeded } from './BlockPartError_RequestExceeded';
|
||||
|
||||
|
||||
@@ -19,6 +20,19 @@ export function BlockPartError(props: {
|
||||
|
||||
// special error presentation, based on hints
|
||||
switch (props.errorHint) {
|
||||
case 'aix-net-disconnected':
|
||||
// determine the 2 'kinds' of disconnection errors in aix.client.ts
|
||||
const kind =
|
||||
props.errorText.includes('**network error**') ? 'net-client-closed'
|
||||
: props.errorText.includes('**connection terminated**') ? 'net-server-closed'
|
||||
: 'net-unknown-closed';
|
||||
|
||||
// For client-side error, we don't show the _NetDisconnected component
|
||||
if (kind === 'net-client-closed')
|
||||
break;
|
||||
|
||||
return <BlockPartError_NetDisconnected disconnectionKind={kind} messageGeneratorLlmId={props.messageGeneratorLlmId} contentScaling={props.contentScaling} />;
|
||||
|
||||
case 'aix-request-exceeded':
|
||||
return <BlockPartError_RequestExceeded messageGeneratorLlmId={props.messageGeneratorLlmId} contentScaling={props.contentScaling} />;
|
||||
|
||||
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Alert, Box, FormHelperText, Switch } from '@mui/joy';
|
||||
import WifiOffRoundedIcon from '@mui/icons-material/WifiOffRounded';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import { useLLM } from '~/common/stores/llms/llms.hooks';
|
||||
import { useModelServiceClientSideFetch } from '~/common/stores/llms/hooks/useModelServiceClientSideFetch';
|
||||
|
||||
|
||||
/**
|
||||
* Error recovery component for "Connection terminated" errors.
|
||||
*/
|
||||
export function BlockPartError_NetDisconnected(props: {
|
||||
disconnectionKind: 'net-client-closed' | 'net-server-closed' | 'net-unknown-closed';
|
||||
messageGeneratorLlmId?: string | null;
|
||||
contentScaling: ContentScaling;
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const model = useLLM(props.messageGeneratorLlmId) ?? null;
|
||||
const isServerSideClosed = props.disconnectionKind === 'net-server-closed'; // do not show CSF option for non-server-side
|
||||
const { csfAvailable, csfActive, csfToggle, vendorName } = useModelServiceClientSideFetch(isServerSideClosed, model);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
size={props.contentScaling === 'xs' ? 'sm' : 'md'}
|
||||
color='danger'
|
||||
variant='plain'
|
||||
sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}
|
||||
>
|
||||
|
||||
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 0.5, alignItems: 'flex-start' }}>
|
||||
|
||||
{/* Header */}
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<WifiOffRoundedIcon sx={{ flexShrink: 0, mt: 0.5 }} />
|
||||
<div>
|
||||
<Box fontSize='larger'>
|
||||
Connection Terminated
|
||||
</Box>
|
||||
<div>
|
||||
The connection was unexpectedly closed before the response completed.
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Recovery options */}
|
||||
{csfAvailable ? <>
|
||||
|
||||
{/* Explanation */}
|
||||
<Box color='text.tertiary' fontSize='sm' my={2}>
|
||||
<strong>Experimental:</strong> enable direct connection to {vendorName} to bypass server timeouts - then try again.
|
||||
</Box>
|
||||
|
||||
{/* Toggle */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
borderRadius: 'sm',
|
||||
bgcolor: 'background.popup',
|
||||
boxShadow: 'md',
|
||||
// border: '1px solid',
|
||||
// borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box color={!csfActive ? undefined : 'primary.solidBg'} fontWeight='lg' mb={0.5}>
|
||||
Direct Connection {csfActive && '- Now Try Again'}
|
||||
</Box>
|
||||
<FormHelperText>
|
||||
Connect directly from this client -> {vendorName || 'AI service'}
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
|
||||
<Switch
|
||||
checked={csfActive}
|
||||
onChange={(e) => csfToggle(e.target.checked)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
</> : (
|
||||
<div>
|
||||
<Box sx={{ color: 'text.secondary', my: 1 }}>
|
||||
Suggestions:
|
||||
</Box>
|
||||
<Box component='ul' sx={{ color: 'text.secondary' }}>
|
||||
<li>Check your internet connection and try again</li>
|
||||
<li>The AI service may be experiencing issues - wait a moment and retry</li>
|
||||
<li>If the issue persists, please let us know promptly on Discord or GitHib</li>
|
||||
</Box>
|
||||
</div>
|
||||
)}
|
||||
</Box>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
+58
-59
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Alert, Box, FormHelperText, Switch, Typography } from '@mui/joy';
|
||||
import { Alert, Box, FormHelperText, Switch } from '@mui/joy';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
@@ -19,13 +19,13 @@ export function BlockPartError_RequestExceeded(props: {
|
||||
|
||||
// external state
|
||||
const model = useLLM(props.messageGeneratorLlmId) ?? null;
|
||||
const { csfAvailable, csfActive, csfToggle } = useModelServiceClientSideFetch(true, model);
|
||||
const { csfAvailable, csfActive, csfToggle, vendorName } = useModelServiceClientSideFetch(true, model);
|
||||
|
||||
return (
|
||||
<Alert
|
||||
size={props.contentScaling === 'xs' ? 'sm' : 'md'}
|
||||
color='danger'
|
||||
sx={{ display: 'flex', alignItems: 'flex-start', gap: 1 }}
|
||||
color='warning'
|
||||
sx={{ display: 'flex', alignItems: 'flex-start', gap: 1, border: '1px solid', borderColor: 'warning.outlinedBorder' }}
|
||||
>
|
||||
|
||||
<WarningRoundedIcon sx={{ flexShrink: 0, mt: 0.25 }} />
|
||||
@@ -36,70 +36,69 @@ export function BlockPartError_RequestExceeded(props: {
|
||||
Request Too Large
|
||||
</Box>
|
||||
<div>
|
||||
Your message or attachments exceed the limit of the Vercel edge network.
|
||||
Your message or attachments exceed the limit of the Vercel edge network
|
||||
</div>
|
||||
|
||||
{/* Recovery options */}
|
||||
{csfAvailable ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{csfAvailable ? <>
|
||||
|
||||
{/* Explanation */}
|
||||
<Box color='text.secondary' fontSize='sm'>
|
||||
<strong>Experimental:</strong> enable direct connection to the AI services, and try again.
|
||||
</Box>
|
||||
|
||||
{/* Toggle */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
borderRadius: 'sm',
|
||||
bgcolor: 'background.popup',
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box color={!csfActive ? undefined : 'primary.solidBg'} fontWeight='lg' mb={0.5}>
|
||||
Direct Connection {csfActive && '- Now Try Again'}
|
||||
</Box>
|
||||
<FormHelperText>
|
||||
Bypassing servers and connect directly from this client -> AI provider
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
|
||||
<Switch
|
||||
checked={csfActive}
|
||||
onChange={(e) => csfToggle(e.target.checked)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Regenerate button */}
|
||||
{/*{props.onRegenerate && (*/}
|
||||
{/* <Button*/}
|
||||
{/* size='sm'*/}
|
||||
{/* variant={csfActive ? 'solid' : 'outlined'}*/}
|
||||
{/* color={csfActive ? 'success' : 'neutral'}*/}
|
||||
{/* startDecorator={<RefreshIcon />}*/}
|
||||
{/* onClick={props.onRegenerate}*/}
|
||||
{/* sx={{ alignSelf: 'flex-start' }}*/}
|
||||
{/* >*/}
|
||||
{/* {csfActive ? 'Regenerate with Direct Connection' : 'Regenerate'}*/}
|
||||
{/* </Button>*/}
|
||||
{/*)}*/}
|
||||
{/* Explanation */}
|
||||
<Box color='text.secondary' fontSize='sm' my={2}>
|
||||
<strong>Experimental:</strong> enable Direct Connection to {vendorName} to work around size limitations.
|
||||
</Box>
|
||||
) : (
|
||||
|
||||
{/* Toggle */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
p: 2,
|
||||
borderRadius: 'sm',
|
||||
bgcolor: 'background.popup',
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box color={!csfActive ? undefined : 'primary.solidBg'} fontWeight='lg' mb={0.5}>
|
||||
Direct Connection {csfActive && '- Now Try Again'}
|
||||
</Box>
|
||||
<FormHelperText>
|
||||
Connect directly from this client -> {vendorName || 'AI service'}
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
|
||||
<Switch
|
||||
checked={csfActive}
|
||||
onChange={(e) => csfToggle(e.target.checked)}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Regenerate button */}
|
||||
{/*{props.onRegenerate && (*/}
|
||||
{/* <Button*/}
|
||||
{/* size='sm'*/}
|
||||
{/* variant={csfActive ? 'solid' : 'outlined'}*/}
|
||||
{/* color={csfActive ? 'success' : 'neutral'}*/}
|
||||
{/* startDecorator={<RefreshIcon />}*/}
|
||||
{/* onClick={props.onRegenerate}*/}
|
||||
{/* sx={{ alignSelf: 'flex-start' }}*/}
|
||||
{/* >*/}
|
||||
{/* {csfActive ? 'Regenerate with Direct Connection' : 'Regenerate'}*/}
|
||||
{/* </Button>*/}
|
||||
{/*)}*/}
|
||||
|
||||
</> : (
|
||||
<Box>
|
||||
<Typography level='body-sm' sx={{ mb: 1 }}>
|
||||
<strong>Suggestions:</strong>
|
||||
</Typography>
|
||||
<Typography component='ul' level='body-sm' sx={{ pl: 2, m: 0 }}>
|
||||
<Box sx={{ color: 'text.secondary', my: 1 }}>
|
||||
Suggestions:
|
||||
</Box>
|
||||
<Box component='ul' sx={{ color: 'text.secondary' }}>
|
||||
<li>Use the cleanup button in the right pane to hide old messages</li>
|
||||
<li>Remove large attachments from the conversation</li>
|
||||
{/*<li>Reduce conversation length before sending</li>*/}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import type { AixChatGenerateContent_DMessageGuts } from '~/modules/aix/client/aix.client';
|
||||
import { speakText } from '~/modules/speex/speex.client';
|
||||
|
||||
import { isTextContentFragment } from '~/common/stores/chat/chat.fragments';
|
||||
|
||||
import type { AixChatGenerateContent_DMessageGuts } from '~/modules/aix/client/aix.client';
|
||||
|
||||
import type { PersonaProcessorInterface } from '../chat-persona';
|
||||
|
||||
|
||||
@@ -58,7 +57,7 @@ export class PersonaChatMessageSpeak implements PersonaProcessorInterface {
|
||||
#speak(text: string) {
|
||||
console.log('📢 TTS:', text);
|
||||
this.spokenLine = true;
|
||||
// fire/forget: we don't want to stall this loop
|
||||
void elevenLabsSpeakText(text, undefined, false, true);
|
||||
// fire/forget: we don't want to stall streaming
|
||||
void speakText(text, undefined, { label: 'Chat message' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,11 +72,13 @@ export const DevNewsItem: NewsItem = {
|
||||
// news and feature surfaces
|
||||
export const NewsItems: NewsItem[] = [
|
||||
{
|
||||
versionCode: '2.0.1',
|
||||
versionCode: '2.0.2',
|
||||
versionName: 'Heavy Critters',
|
||||
versionDate: new Date('2025-11-24T23:30:00Z'),
|
||||
versionDate: new Date('2025-12-01T06:00:00Z'), // 2.0.2
|
||||
// versionDate: new Date('2025-11-24T23:30:00Z'), // 2.0.1
|
||||
items: [
|
||||
{ text: <>New: <B>Opus 4.5</B>, <B>Gemini 3 Pro</B>, <B>Nano Banana Pro</B>, <B>Grok 4.1</B>, <B>GPT-5.1</B>, <B>Kimi K2</B></> },
|
||||
{ text: <><B>New in 2.0.2</B> Speech synthesis with Web Speech, LocalAI, OpenAI and more</> },
|
||||
{ text: <><B>Opus 4.5</B>, <B>Gemini 3 Pro</B>, <B>Nano Banana Pro</B>, <B>Grok 4.1</B>, <B>GPT-5.1</B>, <B>Kimi K2</B></> },
|
||||
{ text: <><B>Image Generation</B> with Azure and LocalAI providers, in addition to OpenAI</> },
|
||||
{ text: <>Enhanced <B>OpenRouter</B> integration with auto-capabilities and reasoning</> },
|
||||
{ text: <>Call transcripts, generate persona images, search button in beams</> },
|
||||
|
||||
@@ -14,6 +14,37 @@ import { InlineError } from '~/common/components/InlineError';
|
||||
import type { SimplePersonaProvenance } from '../store-app-personas';
|
||||
|
||||
|
||||
// configuration
|
||||
const TEMP_DISABLE_YOUTUBE_TRANSCRIPT = true;
|
||||
|
||||
|
||||
function YouTubeDisabledCard() {
|
||||
return (
|
||||
<Card
|
||||
variant='soft'
|
||||
color='primary'
|
||||
invertedColors
|
||||
sx={{
|
||||
p: 3,
|
||||
textAlign: 'center',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.solidBg',
|
||||
}}
|
||||
>
|
||||
<Typography level='title-sm' sx={{ mb: 1 }}>
|
||||
Temporarily Disabled
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ mb: 2 }}>
|
||||
YouTube transcript extraction is currently unavailable due to API changes.
|
||||
</Typography>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Download transcripts manually and use the "From Text" option instead.
|
||||
</Typography>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function YouTubeVideoTranscriptCard(props: { transcript: YTVideoTranscript, onClose: () => void, sx?: SxProps }) {
|
||||
const { transcript } = props;
|
||||
return (
|
||||
@@ -109,6 +140,13 @@ export function FromYouTube(props: {
|
||||
setVideoID(videoId);
|
||||
};
|
||||
|
||||
if (TEMP_DISABLE_YOUTUBE_TRANSCRIPT)
|
||||
return <>
|
||||
<Typography level='title-md' startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />} sx={{ mb: 3 }}>
|
||||
YouTube -> Persona
|
||||
</Typography>
|
||||
<YouTubeDisabledCard />
|
||||
</>;
|
||||
|
||||
return <>
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import { Accordion, AccordionDetails, accordionDetailsClasses, AccordionGroup, AccordionSummary, accordionSummaryClasses, Avatar, Box, Button, ListItemContent, styled, Tab, TabList, TabPanel, Tabs } from '@mui/joy';
|
||||
|
||||
import { Accordion, AccordionDetails, AccordionGroup, AccordionSummary, accordionSummaryClasses, Avatar, Box, Button, ListItemContent, styled, Tab, TabList, TabPanel, Tabs, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import KeyboardCommandKeyOutlinedIcon from '@mui/icons-material/KeyboardCommandKeyOutlined';
|
||||
import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded';
|
||||
import MicIcon from '@mui/icons-material/Mic';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import ScienceIcon from '@mui/icons-material/Science';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import TerminalOutlinedIcon from '@mui/icons-material/TerminalOutlined';
|
||||
|
||||
import { BrowseSettings } from '~/modules/browse/BrowseSettings';
|
||||
import { DallESettings } from '~/modules/t2i/dalle/DallESettings';
|
||||
import { ElevenlabsSettings } from '~/modules/elevenlabs/ElevenlabsSettings';
|
||||
import { GoogleSearchSettings } from '~/modules/google/GoogleSearchSettings';
|
||||
import { T2ISettings } from '~/modules/t2i/T2ISettings';
|
||||
|
||||
@@ -20,14 +19,15 @@ import type { PreferencesTabId } from '~/common/layout/optima/store-layout-optim
|
||||
import { AppBreadcrumbs } from '~/common/components/AppBreadcrumbs';
|
||||
import { DarkModeToggleButton, darkModeToggleButtonSx } from '~/common/components/DarkModeToggleButton';
|
||||
import { GoodModal } from '~/common/components/modals/GoodModal';
|
||||
import { Is } from '~/common/util/pwaUtils';
|
||||
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
|
||||
import { optimaActions } from '~/common/layout/optima/useOptima';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
|
||||
import { AppChatSettingsAI } from './AppChatSettingsAI';
|
||||
import { AppChatSettingsUI } from './settings-ui/AppChatSettingsUI';
|
||||
import { UxLabsSettings } from './UxLabsSettings';
|
||||
import { VoiceSettings } from './VoiceSettings';
|
||||
import { VoiceInSettings } from './VoiceInSettings';
|
||||
import { VoiceOutSettings } from './VoiceOutSettings';
|
||||
|
||||
|
||||
// configuration
|
||||
@@ -130,6 +130,7 @@ const _styles = {
|
||||
|
||||
// modal: undefined,
|
||||
modal: {
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level1',
|
||||
} as const,
|
||||
|
||||
@@ -209,7 +210,7 @@ export function SettingsModal(props: {
|
||||
<GoodModal
|
||||
// title='Preferences' strongerTitle
|
||||
title={
|
||||
<AppBreadcrumbs size='md' rootTitle='App'>
|
||||
<AppBreadcrumbs size='md' rootTitle={isMobile ? 'App' : 'Application'}>
|
||||
<AppBreadcrumbs.Leaf><b>Preferences</b></AppBreadcrumbs.Leaf>
|
||||
</AppBreadcrumbs>
|
||||
}
|
||||
@@ -271,10 +272,10 @@ export function SettingsModal(props: {
|
||||
<TabPanel value='voice' color='primary' variant='outlined' sx={_styles.tabPanel}>
|
||||
<Topics>
|
||||
<Topic icon={/*'🎙️'*/ <MicIcon />} title='Microphone'>
|
||||
<VoiceSettings />
|
||||
<VoiceInSettings isMobile={isMobile} />
|
||||
</Topic>
|
||||
<Topic icon={/*'📢'*/ <RecordVoiceOverIcon />} title='ElevenLabs API'>
|
||||
<ElevenlabsSettings />
|
||||
<Topic icon={/*'📢'*/ <PhVoice />} title={'Speech'/*<>Voices <GoodBadge badge='New' /></>*/}>
|
||||
<VoiceOutSettings isMobile={isMobile} />
|
||||
</Topic>
|
||||
</Topics>
|
||||
</TabPanel>
|
||||
@@ -291,6 +292,44 @@ export function SettingsModal(props: {
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value='tools' color='primary' variant='outlined' sx={_styles.tabPanel}>
|
||||
|
||||
{/* Search Modifier Info */}
|
||||
<Box sx={{
|
||||
p: 2,
|
||||
borderRadius: 'calc(var(--joy-radius-md) - 1px)',
|
||||
// backgroundColor: 'background.level1',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
}}>
|
||||
<Button
|
||||
variant='soft'
|
||||
color='success'
|
||||
startDecorator={<SearchIcon />}
|
||||
sx={{
|
||||
// this is copied frmo ButtonSearchControl._styles.desktop
|
||||
minWidth: 100,
|
||||
justifyContent: 'flex-start',
|
||||
borderRadius: '18px',
|
||||
pointerEvents: 'none',
|
||||
'[data-joy-color-scheme="light"] &': {
|
||||
bgcolor: '#d5ec31',
|
||||
},
|
||||
boxShadow: 'inset 0 2px 4px -1px rgba(0,0,0,0.15)',
|
||||
textWrap: 'nowrap',
|
||||
}}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 'md', mb: 0.5 }}>
|
||||
Use the Search button
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ color: 'text.secondary' }}>
|
||||
Modern AI models have native search built-in. Click the Search button when chatting to enable real-time web search.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<Topics>
|
||||
<Topic icon={<LanguageRoundedIcon />} title='Load Web Pages (with images)' startCollapsed>
|
||||
<BrowseSettings />
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FormControl } from '@mui/joy';
|
||||
|
||||
import { useChatMicTimeoutMs } from '../chat/store-app-chat';
|
||||
|
||||
import type { FormRadioOption } from '~/common/components/forms/FormRadioControl';
|
||||
import { FormChipControl } from '~/common/components/forms/FormChipControl';
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { LanguageSelect } from '~/common/components/LanguageSelect';
|
||||
|
||||
|
||||
const _minTimeouts: ReadonlyArray<FormRadioOption<string>> = [
|
||||
{ value: '600', label: '0.6s', description: 'Best for quick calls' },
|
||||
{ value: '2000', label: '2s', description: 'Standard' },
|
||||
{ value: '5000', label: '5s', description: 'Breathe' },
|
||||
{ value: '15000', label: '15s', description: 'Best for thinking' },
|
||||
] as const;
|
||||
|
||||
|
||||
export function VoiceInSettings(props: { isMobile: boolean }) {
|
||||
|
||||
// external state
|
||||
const [chatTimeoutMs, setChatTimeoutMs] = useChatMicTimeoutMs();
|
||||
|
||||
// derived - converts from string keys to numbers and vice versa
|
||||
const chatTimeoutValue: string = '' + chatTimeoutMs;
|
||||
const setChatTimeoutValue = React.useCallback((value: string) => {
|
||||
value && setChatTimeoutMs(parseInt(value));
|
||||
}, [setChatTimeoutMs]);
|
||||
|
||||
return <>
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart
|
||||
title='Language'
|
||||
description='Mic and voice'
|
||||
// tooltip='For Microphone input and Voice output. Microphone support varies by browser (iPhone/Safari lacks speech input).'
|
||||
/>
|
||||
<LanguageSelect />
|
||||
</FormControl>
|
||||
|
||||
{!props.isMobile && (
|
||||
<FormChipControl
|
||||
title='Timeout'
|
||||
// color='primary'
|
||||
options={_minTimeouts}
|
||||
value={chatTimeoutValue}
|
||||
onChange={setChatTimeoutValue}
|
||||
/>
|
||||
)}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { SpeexConfigureEngines } from '~/modules/speex/components/SpeexConfigureEngines';
|
||||
import { useSpeexEngines } from '~/modules/speex/store-module-speex';
|
||||
|
||||
import { ChatAutoSpeakType, useChatAutoAI } from '../chat/store-app-chat';
|
||||
|
||||
import { FormRadioOption } from '~/common/components/forms/FormRadioControl';
|
||||
import { FormChipControl } from '~/common/components/forms/FormChipControl';
|
||||
|
||||
|
||||
const _autoSpeakOptions: FormRadioOption<ChatAutoSpeakType>[] = [
|
||||
{ value: 'off', label: 'No', description: 'Off' },
|
||||
{ value: 'firstLine', label: 'Start', description: 'First paragraph' },
|
||||
{ value: 'all', label: 'Full', description: 'Complete response' },
|
||||
] as const;
|
||||
|
||||
|
||||
/**
|
||||
* Voice output settings - Auto-speak mode and TTS engine configuration
|
||||
*/
|
||||
export function VoiceOutSettings(props: { isMobile: boolean }) {
|
||||
|
||||
// external state
|
||||
const { autoSpeak, setAutoSpeak } = useChatAutoAI();
|
||||
|
||||
// external state - module
|
||||
const hasEngines = useSpeexEngines().length > 0;
|
||||
|
||||
return <>
|
||||
|
||||
{/* Auto-speak setting */}
|
||||
<FormChipControl
|
||||
title='Speak Chats'
|
||||
size='md'
|
||||
// color='primary'
|
||||
tooltip={!hasEngines ? 'No voice engines available. Configure a TTS service or use system voice.' : undefined}
|
||||
disabled={!hasEngines}
|
||||
options={_autoSpeakOptions}
|
||||
value={autoSpeak}
|
||||
onChange={setAutoSpeak}
|
||||
/>
|
||||
|
||||
{/* Engine configuration */}
|
||||
<SpeexConfigureEngines isMobile={props.isMobile} />
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FormControl } from '@mui/joy';
|
||||
|
||||
import { useChatMicTimeoutMs } from '../chat/store-app-chat';
|
||||
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormRadioControl } from '~/common/components/forms/FormRadioControl';
|
||||
import { LanguageSelect } from '~/common/components/LanguageSelect';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
|
||||
|
||||
export function VoiceSettings() {
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const [chatTimeoutMs, setChatTimeoutMs] = useChatMicTimeoutMs();
|
||||
|
||||
|
||||
// this converts from string keys to numbers and vice versa
|
||||
const chatTimeoutValue: string = '' + chatTimeoutMs;
|
||||
const setChatTimeoutValue = (value: string) => value && setChatTimeoutMs(parseInt(value));
|
||||
|
||||
return <>
|
||||
|
||||
{/* LanguageSelect: moved from the UI settings (where it logically belongs), just to group things better from an UX perspective */}
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title='Language'
|
||||
description='ASR and TTS'
|
||||
tooltip='Currently for Microphone input and Voice output. Microphone support varies by browser (iPhone/Safari lacks speech input). We will use the ElevenLabs MultiLanguage model if a language other than English is selected.' />
|
||||
<LanguageSelect />
|
||||
</FormControl>
|
||||
|
||||
{!isMobile && <FormRadioControl
|
||||
title='Mic Timeout'
|
||||
description={chatTimeoutMs < 1000 ? 'Best for quick calls' : chatTimeoutMs > 5000 ? 'Best for thinking' : 'Standard'}
|
||||
options={[
|
||||
{ value: '600', label: '.6s' },
|
||||
{ value: '2000', label: '2s' },
|
||||
{ value: '5000', label: '5s' },
|
||||
{ value: '15000', label: '15s' },
|
||||
]}
|
||||
value={chatTimeoutValue} onChange={setChatTimeoutValue}
|
||||
/>}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -23,8 +23,8 @@ export const Release = {
|
||||
|
||||
// this is here to trigger revalidation of data, e.g. models refresh
|
||||
Monotonics: {
|
||||
Aix: 42,
|
||||
NewsVersion: 201,
|
||||
Aix: 45,
|
||||
NewsVersion: 202,
|
||||
},
|
||||
|
||||
// Frontend: pretty features
|
||||
|
||||
@@ -141,6 +141,7 @@ export async function attachmentLoadInputAsync(source: Readonly<AttachmentDraftS
|
||||
} else
|
||||
edit({ inputError: 'No content or file found at this link' });
|
||||
} catch (error: any) {
|
||||
console.log('[DEV] Issue downloading page for attachment:', { error });
|
||||
edit({ inputError: `Issue downloading page: ${error?.message || (typeof error === 'string' ? error : JSON.stringify(error))}` });
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Chip, ChipProps } from '@mui/joy';
|
||||
|
||||
|
||||
/**
|
||||
* Simple badge/label component for inline status indicators like "New", "Beta", etc.
|
||||
*/
|
||||
export function GoodBadge(props: {
|
||||
badge: React.ReactNode;
|
||||
color?: ChipProps['color'];
|
||||
variant?: ChipProps['variant'];
|
||||
sx?: ChipProps['sx'];
|
||||
}) {
|
||||
return (
|
||||
<Chip
|
||||
size='sm'
|
||||
color={props.color ?? 'success'}
|
||||
variant={props.variant ?? 'soft'}
|
||||
sx={{
|
||||
ml: 1.5,
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'md',
|
||||
borderRadius: 'xs',
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
// default "new" color - lime/yellow-green
|
||||
...(props.color === undefined && {
|
||||
bgcolor: '#d5ec31',
|
||||
color: 'primary.softColor',
|
||||
}),
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{props.badge}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Option, Select } from '@mui/joy';
|
||||
import { Option, optionClasses, Select, SelectSlotsAndSlotProps } from '@mui/joy';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
|
||||
import { useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
@@ -10,6 +10,20 @@ import { useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
import languages from './Languages.json';
|
||||
|
||||
|
||||
// copied from useLLMSelect.tsx - inspired by optimaSelectSlotProps.listbox
|
||||
const _selectSlotProps: SelectSlotsAndSlotProps<false>['slotProps'] = {
|
||||
root: { sx: { minWidth: 200 } },
|
||||
listbox: {
|
||||
sx: {
|
||||
boxShadow: 'xl',
|
||||
[`& .${optionClasses.root}`]: {
|
||||
maxWidth: 'min(640px, calc(100dvw - 0.25rem))',
|
||||
},
|
||||
},
|
||||
} as const,
|
||||
} as const;
|
||||
|
||||
|
||||
export function LanguageSelect() {
|
||||
// external state
|
||||
|
||||
@@ -32,19 +46,19 @@ export function LanguageSelect() {
|
||||
</Option>
|
||||
) : (
|
||||
Object.entries(localesOrCode).map(([country, code]) => (
|
||||
<Option key={code} value={code}>
|
||||
<Option key={code} value={code} label={language}>
|
||||
{`${language} (${country})`}
|
||||
</Option>
|
||||
))
|
||||
)), []);
|
||||
|
||||
return (
|
||||
<Select value={preferredLanguage} onChange={handleLanguageChanged}
|
||||
indicator={<KeyboardArrowDownIcon />}
|
||||
slotProps={{
|
||||
root: { sx: { minWidth: 200 } },
|
||||
indicator: { sx: { opacity: 0.5 } },
|
||||
}}>
|
||||
<Select
|
||||
value={preferredLanguage}
|
||||
onChange={handleLanguageChanged}
|
||||
indicator={<KeyboardArrowDownIcon />}
|
||||
slotProps={_selectSlotProps}
|
||||
>
|
||||
{languageOptions}
|
||||
</Select>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,13 @@ const _styles = {
|
||||
gap: 1,
|
||||
} as const,
|
||||
|
||||
chipGroupEnd: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
gap: 1,
|
||||
} as const,
|
||||
|
||||
chip: {
|
||||
'--Chip-minHeight': '1.75rem', // this makes it prob better
|
||||
px: 1.5,
|
||||
@@ -36,6 +43,7 @@ export const FormChipControl = <TValue extends string>(props: {
|
||||
// specific
|
||||
size?: 'sm' | 'md' | 'lg',
|
||||
color?: ColorPaletteProp,
|
||||
alignEnd?: boolean,
|
||||
// =FormRadioControl
|
||||
title: string | React.JSX.Element;
|
||||
description?: string | React.JSX.Element;
|
||||
@@ -48,6 +56,9 @@ export const FormChipControl = <TValue extends string>(props: {
|
||||
|
||||
const { onChange } = props;
|
||||
|
||||
const selectedOption = props.options.find(option => option.value === props.value);
|
||||
const description = selectedOption?.description ?? props.description;
|
||||
|
||||
const handleChipClick = React.useCallback((value: Immutable<TValue>) => {
|
||||
if (!props.disabled)
|
||||
onChange(value);
|
||||
@@ -55,8 +66,8 @@ export const FormChipControl = <TValue extends string>(props: {
|
||||
|
||||
return (
|
||||
<FormControl orientation='horizontal' disabled={props.disabled} sx={_styles.control}>
|
||||
{(!!props.title || !!props.description) && <FormLabelStart title={props.title} description={props.description} tooltip={props.tooltip} />}
|
||||
<Box sx={_styles.chipGroup}>
|
||||
{(!!props.title || !!description) && <FormLabelStart title={props.title} description={description} tooltip={props.tooltip} />}
|
||||
<Box sx={props.alignEnd ? _styles.chipGroupEnd : _styles.chipGroup}>
|
||||
{props.options.map((option) => (
|
||||
<Chip
|
||||
key={'opt-' + option.value}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { FormControl, IconButton, Input } from '@mui/joy';
|
||||
import KeyIcon from '@mui/icons-material/Key';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
|
||||
import { FormLabelStart } from './FormLabelStart';
|
||||
|
||||
|
||||
const _styles = {
|
||||
formControl: {
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
inputDefault: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
} as const satisfies Record<string, SxProps>;
|
||||
|
||||
|
||||
/**
|
||||
* Secret/API key form field with visibility toggle.
|
||||
* Same inline layout as FormTextField but with secret-specific features:
|
||||
* - Password masking with visibility toggle
|
||||
* - Key icon (customizable)
|
||||
* - Password manager integration
|
||||
*/
|
||||
export function FormSecretField(props: {
|
||||
autoCompleteId: string;
|
||||
title: string | React.JSX.Element;
|
||||
description?: string | React.JSX.Element;
|
||||
tooltip?: string | React.JSX.Element;
|
||||
placeholder?: string;
|
||||
value: string;
|
||||
onChange: (text: string) => void;
|
||||
// Behavior
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
isError?: boolean;
|
||||
// Appearance
|
||||
inputSx?: SxProps;
|
||||
/** Custom start decorator, or false to hide. Default: KeyIcon */
|
||||
startDecorator?: React.ReactNode | false;
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [isVisible, setIsVisible] = React.useState(false);
|
||||
|
||||
// derived
|
||||
const acId = 'secret-' + props.autoCompleteId;
|
||||
// password manager username
|
||||
const ghost = props.autoCompleteId.replace(/-key$/, '').replace(/-/g, ' ');
|
||||
|
||||
const endDecorator = React.useMemo(() => !!props.value && (
|
||||
<IconButton size='sm' onClick={() => setIsVisible(on => !on)}>
|
||||
{isVisible ? <VisibilityIcon sx={{ fontSize: 'md' }} /> : <VisibilityOffIcon sx={{ fontSize: 'md' }} />}
|
||||
</IconButton>
|
||||
), [props.value, isVisible]);
|
||||
|
||||
return (
|
||||
<FormControl
|
||||
id={acId}
|
||||
orientation='horizontal'
|
||||
disabled={props.disabled}
|
||||
sx={_styles.formControl}
|
||||
>
|
||||
<FormLabelStart title={props.title} description={props.description} tooltip={props.tooltip} />
|
||||
{/* Hidden username field for password manager association */}
|
||||
<input
|
||||
type='text'
|
||||
autoComplete='username'
|
||||
value={ghost}
|
||||
readOnly
|
||||
tabIndex={-1}
|
||||
style={{ display: 'none' }}
|
||||
/>
|
||||
<Input
|
||||
name={acId}
|
||||
type={isVisible ? 'text' : 'password'}
|
||||
autoComplete='new-password'
|
||||
variant='outlined'
|
||||
placeholder={props.required && !props.placeholder ? 'required' : props.placeholder}
|
||||
error={props.isError}
|
||||
value={props.value}
|
||||
onChange={event => props.onChange(event.target.value)}
|
||||
startDecorator={props.startDecorator ?? <KeyIcon sx={{ fontSize: 'md' }} />}
|
||||
endDecorator={endDecorator}
|
||||
sx={props.inputSx ?? _styles.inputDefault}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ export function FormSliderControl(props: {
|
||||
startAdornment?: React.ReactNode,
|
||||
endAdornment?: React.ReactNode,
|
||||
styleNoTrack?: boolean,
|
||||
sliderSx?: SxProps,
|
||||
}) {
|
||||
|
||||
|
||||
@@ -66,8 +67,7 @@ export function FormSliderControl(props: {
|
||||
onChange={handleChange}
|
||||
onChangeCommitted={handleChangeCommitted}
|
||||
valueLabelDisplay={props.valueLabelDisplay}
|
||||
sx={props.styleNoTrack ? _styleNoTrack : undefined}
|
||||
// sx={{ py: 1, mt: 1.1 }}
|
||||
sx={props.styleNoTrack ? _styleNoTrack : props.sliderSx}
|
||||
/>
|
||||
{props.endAdornment}
|
||||
</FormControl>
|
||||
|
||||
@@ -6,11 +6,16 @@ import { FormControl, Input } from '@mui/joy';
|
||||
import { FormLabelStart } from './FormLabelStart';
|
||||
|
||||
|
||||
const formControlSx: SxProps = {
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
};
|
||||
const _styles = {
|
||||
formControl: {
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
},
|
||||
inputDefault: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
} as const satisfies Record<string, SxProps>;
|
||||
|
||||
|
||||
/**
|
||||
@@ -23,6 +28,7 @@ export function FormTextField(props: {
|
||||
tooltip?: string | React.JSX.Element,
|
||||
placeholder?: string, isError?: boolean, disabled?: boolean,
|
||||
value: string | undefined, onChange: (text: string) => void,
|
||||
inputSx?: SxProps,
|
||||
}) {
|
||||
const acId = 'text-' + props.autoCompleteId;
|
||||
return (
|
||||
@@ -30,7 +36,7 @@ export function FormTextField(props: {
|
||||
id={acId}
|
||||
orientation='horizontal'
|
||||
disabled={props.disabled}
|
||||
sx={formControlSx}
|
||||
sx={_styles.formControl}
|
||||
>
|
||||
<FormLabelStart title={props.title} description={props.description} tooltip={props.tooltip} />
|
||||
<Input
|
||||
@@ -39,7 +45,7 @@ export function FormTextField(props: {
|
||||
autoComplete='off'
|
||||
variant='outlined' placeholder={props.placeholder} error={props.isError}
|
||||
value={props.value} onChange={event => props.onChange(event.target.value)}
|
||||
sx={{ flexGrow: 1 }}
|
||||
sx={props.inputSx ?? _styles.inputDefault}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - user-sound
|
||||
*/
|
||||
export function PhVoice(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M144,165.68a68,68,0,1,0-71.9,0c-20.65,6.76-39.23,19.39-54.17,37.17a8,8,0,0,0,12.25,10.3C50.25,189.19,77.91,176,108,176s57.75,13.19,77.88,37.15a8,8,0,1,0,12.25-10.3C183.18,185.07,164.6,172.44,144,165.68ZM56,108a52,52,0,1,1,52,52A52.06,52.06,0,0,1,56,108ZM207.36,65.6a108.36,108.36,0,0,1,0,84.8,8,8,0,0,1-7.36,4.86,8,8,0,0,1-7.36-11.15,92.26,92.26,0,0,0,0-72.22,8,8,0,0,1,14.72-6.29ZM248,108a139,139,0,0,1-11.29,55.15,8,8,0,0,1-14.7-6.3,124.43,124.43,0,0,0,0-97.7,8,8,0,1,1,14.7-6.3A139,139,0,0,1,248,108Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function ElevenLabsIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 24 24' width='24' height='24' fill='currentColor' {...props}>
|
||||
<path d='M7 4h3v16H7V4zm7 0h3v16h-3V4z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -25,17 +25,6 @@ export interface CapabilityBrowserSpeechRecognition {
|
||||
export { browserSpeechRecognitionCapability as useCapabilityBrowserSpeechRecognition } from './speechrecognition/useSpeechRecognition';
|
||||
|
||||
|
||||
/// Speech Synthesis: ElevenLabs
|
||||
|
||||
export interface CapabilityElevenLabsSpeechSynthesis {
|
||||
mayWork: boolean;
|
||||
isConfiguredServerSide: boolean;
|
||||
isConfiguredClientSide: boolean;
|
||||
}
|
||||
|
||||
export { useCapability as useCapabilityElevenLabs } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
|
||||
|
||||
/// Image Generation
|
||||
|
||||
export interface TextToImageProvider {
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Alert, IconButton } from '@mui/joy';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
|
||||
import { Is, isBrowser, isPwa } from '~/common/util/pwaUtils';
|
||||
import { isBrowser, isPwa } from '~/common/util/pwaUtils';
|
||||
import { useUICounter } from '~/common/stores/store-ui';
|
||||
|
||||
|
||||
@@ -31,15 +31,14 @@ export function usePWADesktopModeWarning() {
|
||||
|
||||
// if PWA
|
||||
const isInPwaMode = isPwa();
|
||||
if (!isInPwaMode) return false;
|
||||
// if Mobile device (detected using touch points), while desktop have 0 touch points
|
||||
const isTouchDevice = navigator?.maxTouchPoints > 0;
|
||||
// if Physical Screen is small - typical mobile screen size - e.g. 412 for SGS24 Ultra
|
||||
const isSmallScreen = window.screen?.width < 600;
|
||||
// if Desktop mode - e.g. "Desktop Site" reports a large viewport width, typically 9xx+
|
||||
const isDesktopWidth = window.matchMedia('(min-width: 900px)').matches;
|
||||
|
||||
// if OS is mobile
|
||||
const isMobileOS = Is.OS.iOS || Is.OS.Android;
|
||||
if (!isMobileOS) return false;
|
||||
|
||||
// Check if viewport width suggests desktop mode (>= 900px)
|
||||
// This matches the mobile breakpoint used in useMatchMedia.ts
|
||||
return window.matchMedia('(min-width: 900px)').matches;
|
||||
return isInPwaMode && isTouchDevice && isSmallScreen && isDesktopWidth;
|
||||
}, []);
|
||||
|
||||
const showWarning = isInDesktopMode && !hideWarning && lessThanFive;
|
||||
|
||||
@@ -7,36 +7,38 @@ import type { DModelsService } from '../llms.service.types';
|
||||
import { llmsStoreActions, useModelsStore } from '../store-llms';
|
||||
|
||||
|
||||
const CSF_KEY = 'csf';
|
||||
|
||||
|
||||
/**
|
||||
* Hook to manage client-side fetch setting for a model's service
|
||||
* The CSF setting is stored as 'csf' in service settings for all vendors
|
||||
*/
|
||||
export function useModelServiceClientSideFetch(enabled: boolean, model: DLLM | null) {
|
||||
|
||||
// memo vendor
|
||||
const { vendor, csfKey } = React.useMemo(() => {
|
||||
if (!enabled) return { vendor: null, csfKey: '' };
|
||||
const vendor = findModelVendor(model?.vId);
|
||||
const csfKey = vendor?.csfKey || '';
|
||||
return { vendor, csfKey };
|
||||
const vendor = React.useMemo(() => {
|
||||
if (!enabled) return null;
|
||||
return findModelVendor(model?.vId);
|
||||
}, [enabled, model?.vId]);
|
||||
|
||||
// external state
|
||||
const service: null | DModelsService = useModelsStore(state => !model?.sId ? null : state.sources.find(s => s.id === model.sId) ?? null);
|
||||
|
||||
// actual state
|
||||
const csfAvailable: boolean | undefined = !!csfKey && vendor?.csfAvailable?.(service?.setup);
|
||||
const csfActive: boolean | undefined = csfAvailable && (service?.setup as any)?.[csfKey];
|
||||
const csfAvailable: boolean | undefined = !!vendor?.csfAvailable && vendor?.csfAvailable?.(service?.setup);
|
||||
const csfActive: boolean | undefined = csfAvailable && (service?.setup as any)?.[CSF_KEY];
|
||||
|
||||
const serviceId = service?.id || '';
|
||||
const csfToggle = React.useCallback((value: boolean) => {
|
||||
if (csfKey && serviceId)
|
||||
llmsStoreActions().updateServiceSettings(serviceId, { [csfKey]: value });
|
||||
}, [csfKey, serviceId]);
|
||||
if (serviceId)
|
||||
llmsStoreActions().updateServiceSettings(serviceId, { [CSF_KEY]: value });
|
||||
}, [serviceId]);
|
||||
|
||||
const csfReset = React.useCallback(() => {
|
||||
if (csfKey && serviceId)
|
||||
llmsStoreActions().updateServiceSettings(serviceId, { [csfKey]: false });
|
||||
}, [csfKey, serviceId]);
|
||||
if (serviceId)
|
||||
llmsStoreActions().updateServiceSettings(serviceId, { [CSF_KEY]: false });
|
||||
}, [serviceId]);
|
||||
|
||||
return { csfAvailable, csfActive, csfToggle, csfReset };
|
||||
return { csfAvailable, csfActive, csfToggle, csfReset, vendorName: vendor?.name || vendor?.id || 'AI Service' };
|
||||
}
|
||||
|
||||
@@ -212,6 +212,14 @@ export const DModelParameterRegistry = {
|
||||
// No initialValue - undefined means 'dynamic', which for Gemini Pro is the same as 'high' (which is the equivalent of 'medium' for OpenAI's effort levels.. somehow)
|
||||
} as const,
|
||||
|
||||
llmVndGeminiInteractionsAgent: {
|
||||
label: 'Agent (Interactions API)',
|
||||
type: 'string' as const,
|
||||
description: 'Uses Gemini Interactions API with the specified agent (e.g., deep-research-pro-preview-12-2025)',
|
||||
hidden: true, // Auto-set by model definition
|
||||
requiredFallback: 'deep-research-pro-preview-12-2025',
|
||||
} as const,
|
||||
|
||||
// NOTE: we don't have this as a parameter, as for now we use it in tandem with llmVndGeminiGoogleSearch
|
||||
// llmVndGeminiUrlContext: {
|
||||
// label: 'URL Context',
|
||||
@@ -249,6 +257,23 @@ export const DModelParameterRegistry = {
|
||||
requiredFallback: 'medium',
|
||||
} as const,
|
||||
|
||||
llmVndOaiReasoningEffort52: {
|
||||
label: 'Reasoning Effort',
|
||||
type: 'enum' as const,
|
||||
description: 'Constrains effort on reasoning for GPT-5.2 models. When unset, defaults to none (fast responses).',
|
||||
values: ['none', 'low', 'medium', 'high', 'xhigh'] as const,
|
||||
// No requiredFallback - unset = none (the default for GPT-5.2)
|
||||
// No initialValue - starts undefined, which the UI should display as "none"
|
||||
} as const,
|
||||
|
||||
llmVndOaiReasoningEffort52Pro: {
|
||||
label: 'Reasoning Effort',
|
||||
type: 'enum' as const,
|
||||
description: 'Constrains effort on reasoning for GPT-5.2 Pro. Defaults to medium.',
|
||||
values: ['medium', 'high', 'xhigh'] as const,
|
||||
// No requiredFallback - unset = medium (the default for GPT-5.2 Pro)
|
||||
} as const,
|
||||
|
||||
llmVndOaiRestoreMarkdown: {
|
||||
label: 'Restore Markdown',
|
||||
type: 'boolean' as const,
|
||||
|
||||
@@ -151,6 +151,7 @@ export type DModelInterfaceV1 =
|
||||
| 'oai-realtime'
|
||||
| 'oai-responses'
|
||||
| 'gem-code-execution'
|
||||
| 'gem-interactions' // [Gemini] Interactions API (Deep Research agent)
|
||||
| 'outputs-audio' // TEMP: ui flag - supports audio output (e.g., text-to-speech)
|
||||
| 'outputs-image' // TEMP: ui flag - supports image output (image generation)
|
||||
| 'outputs-no-text' // disable text outputs (used in conjunction with alt-outputs) - assumed off
|
||||
@@ -181,6 +182,7 @@ export const LLM_IF_OAI_PromptCaching: DModelInterfaceV1 = 'oai-prompt-caching';
|
||||
export const LLM_IF_OAI_Realtime: DModelInterfaceV1 = 'oai-realtime';
|
||||
export const LLM_IF_OAI_Responses: DModelInterfaceV1 = 'oai-responses';
|
||||
export const LLM_IF_GEM_CodeExecution: DModelInterfaceV1 = 'gem-code-execution';
|
||||
export const LLM_IF_GEM_Interactions: DModelInterfaceV1 = 'gem-interactions';
|
||||
export const LLM_IF_HOTFIX_NoStream: DModelInterfaceV1 = 'hotfix-no-stream';
|
||||
export const LLM_IF_HOTFIX_NoTemperature: DModelInterfaceV1 = 'hotfix-no-temperature';
|
||||
export const LLM_IF_HOTFIX_StripImages: DModelInterfaceV1 = 'hotfix-strip-images';
|
||||
@@ -205,6 +207,7 @@ export const LLMS_ALL_INTERFACES = [
|
||||
// Vendor-specific capabilities
|
||||
LLM_IF_ANT_PromptCaching, // [Anthropic] model supports anthropic-specific caching
|
||||
LLM_IF_GEM_CodeExecution, // [Gemini] Tool: code execution
|
||||
LLM_IF_GEM_Interactions, // [Gemini] Interactions API (Deep Research agent)
|
||||
LLM_IF_OAI_PromptCaching, // [OpenAI] model supports OpenAI prompt caching
|
||||
LLM_IF_OAI_Realtime, // [OpenAI] realtime API support - unused
|
||||
LLM_IF_OAI_Responses, // [OpenAI] Responses API (new) support
|
||||
|
||||
@@ -19,6 +19,10 @@ export class AudioLivePlayer {
|
||||
this.audioElement.src = URL.createObjectURL(this.mediaSource);
|
||||
this.audioElement.autoplay = true;
|
||||
|
||||
// Suppress Android media notification by clearing media session metadata
|
||||
if ('mediaSession' in navigator)
|
||||
navigator.mediaSession.metadata = null;
|
||||
|
||||
// Connect the audio element to the audio context
|
||||
const sourceNode = this.audioContext.createMediaElementSource(this.audioElement);
|
||||
sourceNode.connect(this.audioContext.destination);
|
||||
@@ -60,14 +64,12 @@ export class AudioLivePlayer {
|
||||
private onSourceBufferUpdateEnd = () => {
|
||||
this.isSourceBufferUpdating = false;
|
||||
|
||||
// Continue appending if there's more data
|
||||
if (!this.isMediaSourceEnded) {
|
||||
// Always continue appending if there's more data in the queue
|
||||
if (this.chunkQueue.length > 0) {
|
||||
this.appendNextChunk();
|
||||
} else {
|
||||
// End the stream if all data has been appended
|
||||
if (this.sourceBuffer && !this.sourceBuffer.updating && this.chunkQueue.length === 0) {
|
||||
this.mediaSource.endOfStream();
|
||||
}
|
||||
} else if (this.isMediaSourceEnded) {
|
||||
// Only end the stream when queue is fully drained
|
||||
this._safeEndOfStream();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -86,9 +88,17 @@ export class AudioLivePlayer {
|
||||
}
|
||||
}
|
||||
} else if (this.isMediaSourceEnded) {
|
||||
if (this.sourceBuffer && !this.sourceBuffer.updating) {
|
||||
this.mediaSource.endOfStream();
|
||||
}
|
||||
if (this.sourceBuffer && !this.sourceBuffer.updating)
|
||||
this._safeEndOfStream();
|
||||
}
|
||||
}
|
||||
|
||||
private _safeEndOfStream() {
|
||||
if (this.mediaSource.readyState !== 'open') return;
|
||||
try {
|
||||
this.mediaSource.endOfStream();
|
||||
} catch (e) {
|
||||
// Ignore - MediaSource may have already ended
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +116,8 @@ export class AudioLivePlayer {
|
||||
public endPlayback() {
|
||||
this.isMediaSourceEnded = true;
|
||||
// If the sourceBuffer is not updating, we can end the stream
|
||||
if (this.sourceBuffer && !this.sourceBuffer.updating && this.chunkQueue.length === 0) {
|
||||
this.mediaSource.endOfStream();
|
||||
}
|
||||
if (this.sourceBuffer && !this.sourceBuffer.updating && this.chunkQueue.length === 0)
|
||||
this._safeEndOfStream();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -119,14 +128,13 @@ export class AudioLivePlayer {
|
||||
this.chunkQueue = [];
|
||||
this.isMediaSourceEnded = true;
|
||||
|
||||
if (this.sourceBuffer) {
|
||||
// only abort SourceBuffer when MediaSource is 'open'
|
||||
if (this.sourceBuffer && this.mediaSource.readyState === 'open') {
|
||||
try {
|
||||
if (this.mediaSource.readyState === 'open') {
|
||||
this.mediaSource.endOfStream();
|
||||
}
|
||||
this.sourceBuffer.abort();
|
||||
this.mediaSource.endOfStream();
|
||||
} catch (e) {
|
||||
console.warn('Error stopping playback:', e);
|
||||
// Ignore - may race with natural stream end
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,35 @@
|
||||
import { addSnackbar } from '../components/snackbar/useSnackbarsStore';
|
||||
import { Is, isBrowser } from './pwaUtils';
|
||||
|
||||
|
||||
/** Strip theme-dependent colors from an HTML element tree (in-place) */
|
||||
export function stripHtmlColors(element: HTMLElement) {
|
||||
element.querySelectorAll('*').forEach((el) => {
|
||||
if (el instanceof HTMLElement)
|
||||
['color', 'background', 'background-color'].forEach(p => el.style.removeProperty(p));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy HTML to clipboard with theme-dependent colors stripped (keeps formatting like font sizes).
|
||||
* Falls back to plain text if HTML clipboard write fails.
|
||||
*/
|
||||
export function copyToClipboardHtmlMinusColors(html: string, plainText: string, typeLabel: string) {
|
||||
if (!isBrowser) return;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = html;
|
||||
stripHtmlColors(div);
|
||||
|
||||
const blob = new Blob([div.innerHTML], { type: 'text/html' });
|
||||
const textBlob = new Blob([plainText], { type: 'text/plain' });
|
||||
|
||||
navigator.clipboard.write([new ClipboardItem({ 'text/html': blob, 'text/plain': textBlob })])
|
||||
.then(() => addSnackbar({ key: 'copy-to-clipboard', message: `${typeLabel} copied to clipboard`, type: 'success', closeButton: false, overrides: { autoHideDuration: 2000 } }))
|
||||
.catch(() => copyToClipboard(plainText, typeLabel)); // fallback to plain text
|
||||
}
|
||||
|
||||
|
||||
export function copyToClipboard(text: string, typeLabel: string) {
|
||||
if (!isBrowser)
|
||||
return;
|
||||
|
||||
@@ -457,8 +457,14 @@ export function prettyShortChatModelName(model: string | undefined): string {
|
||||
// start past the last /, if any
|
||||
const lastSlashIndex = model.lastIndexOf('/');
|
||||
const modelName = lastSlashIndex === -1 ? model : model.slice(lastSlashIndex + 1);
|
||||
return modelName.replace('deepseek-', ' Deepseek ')
|
||||
.replace('reasoner', 'R1').replace('r1', 'R1')
|
||||
return modelName
|
||||
// map these for each release
|
||||
.replace('-reasoner', ' 3.2 Reasoner')
|
||||
.replace('-chat', ' 3.2 Chat')
|
||||
.replace('-v3', ' 3')
|
||||
// default replacements
|
||||
.replace('deepseek', 'Deepseek')
|
||||
.replace('speciale', 'Speciale').replace('@', ' ')
|
||||
.replaceAll('-', ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ export function agiCustomId(digits: number) {
|
||||
type UuidV4Scope =
|
||||
| 'conversation-2'
|
||||
| 'persona-2'
|
||||
| 'speex.engine.instance'
|
||||
;
|
||||
|
||||
|
||||
|
||||
@@ -28,3 +28,182 @@ export function countKeys(obj: object | null | undefined): number {
|
||||
for (const _ in obj) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip undefined fields from an object.
|
||||
*
|
||||
* Useful to prevent undefined becoming null over the wire (JSON serialization).
|
||||
*/
|
||||
export function stripUndefined<T extends object>(obj: T): T;
|
||||
export function stripUndefined<T extends object>(obj: T | null): T | null;
|
||||
export function stripUndefined<T extends object>(obj: T | null): T | null {
|
||||
if (!obj) return null;
|
||||
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v !== undefined)) as T;
|
||||
}
|
||||
|
||||
|
||||
// === Size Estimation ===
|
||||
|
||||
/**
|
||||
* Estimates JSON serialized size without actually stringifying.
|
||||
*
|
||||
* Avoids memory allocation spike on large objects. Useful for progress tracking,
|
||||
* batching decisions, or debug output sizing.
|
||||
*
|
||||
* Note: This is an ESTIMATE. It doesn't account for:
|
||||
* - UTF-8 multi-byte characters (assumes 1 byte per char)
|
||||
* - JSON escape sequences (\n, \t, unicode escapes)
|
||||
* - Floating point precision differences
|
||||
*
|
||||
* Returns 0 for the cyclic portion if circular references are detected.
|
||||
*/
|
||||
export function objectEstimateJsonSize(value: unknown, debugCaller: string): number {
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
function estimate(val: unknown): number {
|
||||
if (val === null) return 4; // "null"
|
||||
if (val === undefined) return 0; // omitted in JSON
|
||||
|
||||
switch (typeof val) {
|
||||
case 'string':
|
||||
return val.length + 2; // quotes
|
||||
case 'number':
|
||||
return String(val).length;
|
||||
case 'boolean':
|
||||
return val ? 4 : 5; // "true" or "false"
|
||||
case 'object': {
|
||||
// cycle detection
|
||||
if (seen.has(val as object)) {
|
||||
console.warn(`[estimateJsonSize (${debugCaller})] Circular reference detected, returning 0 for this branch`);
|
||||
return 0;
|
||||
}
|
||||
seen.add(val as object);
|
||||
|
||||
if (Array.isArray(val)) {
|
||||
let size = 2; // []
|
||||
for (let i = 0; i < val.length; i++) {
|
||||
size += estimate(val[i]);
|
||||
if (i < val.length - 1) size += 1; // comma
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
// plain object
|
||||
let size = 2; // {}
|
||||
const keys = Object.keys(val);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
size += key.length + 3; // "key":
|
||||
size += estimate((val as Record<string, unknown>)[key]);
|
||||
if (i < keys.length - 1) size += 1; // comma
|
||||
}
|
||||
return size;
|
||||
}
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
return estimate(value);
|
||||
}
|
||||
|
||||
|
||||
// === Object Traversal ===
|
||||
|
||||
/**
|
||||
* Deep clones an object while truncating strings that exceed maxBytes.
|
||||
*
|
||||
* Useful for debug logging of large objects (e.g., requests with base64 images).
|
||||
* Truncates strings in the middle, preserving start and end with a byte count.
|
||||
*
|
||||
* @returns Deep clone with truncated strings, or "[Circular]" for cyclic refs
|
||||
*/
|
||||
export function objectDeepCloneWithStringLimit(value: unknown, debugCaller: string, maxBytes: number = 2048): unknown {
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
function clone(val: unknown): unknown {
|
||||
// handle primitives first
|
||||
if (val === null || val === undefined) return val;
|
||||
|
||||
// handle strings - truncate if too long
|
||||
if (typeof val === 'string') {
|
||||
if (val.length <= maxBytes) return val;
|
||||
const ellipsis = `...[${(val.length - maxBytes).toLocaleString()} bytes]...`;
|
||||
const half = Math.floor((maxBytes - ellipsis.length) / 2);
|
||||
return val.slice(0, half) + ellipsis + val.slice(-half);
|
||||
}
|
||||
|
||||
// handle other primitives
|
||||
if (typeof val !== 'object') return val;
|
||||
|
||||
// cycle detection
|
||||
if (seen.has(val)) return '[Circular]';
|
||||
seen.add(val);
|
||||
|
||||
// handle arrays - recurse
|
||||
if (Array.isArray(val))
|
||||
return val.map(item => clone(item));
|
||||
|
||||
// handle objects - recurse
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const key in val)
|
||||
if (Object.prototype.hasOwnProperty.call(val, key))
|
||||
result[key] = clone((val as Record<string, unknown>)[key]);
|
||||
return result;
|
||||
}
|
||||
|
||||
return clone(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the largest string values in an object tree
|
||||
*
|
||||
* Recursively traverses an object to find the top N largest string values,
|
||||
* returning their paths, lengths, and preview snippets.
|
||||
*
|
||||
* @returns Array of {path, length, preview} sorted by length (descending)
|
||||
*/
|
||||
export function objectFindLargestStringPaths(obj: unknown, debugCaller: string, topN: number = 5, maxDepth: number = 20): Array<{ path: string; length: number; preview: string }> {
|
||||
const results: Array<{ path: string; length: number; preview: string }> = [];
|
||||
const seen = new WeakSet<object>();
|
||||
|
||||
function traverse(current: unknown, path: string, depth: number) {
|
||||
// prevent infinite recursion
|
||||
if (depth > maxDepth) return;
|
||||
|
||||
// handle strings
|
||||
if (typeof current === 'string') {
|
||||
results.push({
|
||||
path,
|
||||
length: current.length,
|
||||
preview: current.substring(0, 100) + (current.length > 100 ? '...' : ''),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// handle non-objects
|
||||
if (current === null || typeof current !== 'object') return;
|
||||
|
||||
// cycle detection
|
||||
if (seen.has(current)) {
|
||||
console.warn(`[findLargestStringPaths (${debugCaller})] Circular reference at path: ${path}`);
|
||||
return;
|
||||
}
|
||||
seen.add(current);
|
||||
|
||||
// handle arrays
|
||||
if (Array.isArray(current))
|
||||
return current.forEach((item, index) => traverse(item, `${path}[${index}]`, depth + 1));
|
||||
|
||||
// handle objects
|
||||
for (const [key, value] of Object.entries(current))
|
||||
traverse(value, path ? `${path}.${key}` : key, depth + 1);
|
||||
}
|
||||
|
||||
traverse(obj, '', 0);
|
||||
|
||||
// sort by length descending and return top N
|
||||
return results
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.slice(0, topN);
|
||||
}
|
||||
|
||||
@@ -604,6 +604,8 @@ export class ContentReassembler {
|
||||
this.accumulator.fragments.unshift(placeholderFragment); // Add to beginning
|
||||
|
||||
// Placeholders don't affect text fragment indexing
|
||||
// NOTE: we could have placeholders breaking text accumulation into new fragments with `this.currentTextFragmentIndex = null;`, however
|
||||
// since placeholders are used a lot with hosted tool calls, this could lead to way too many fragments being created
|
||||
}
|
||||
|
||||
private onSetVendorState(vs: Extract<AixWire_Particles.PartParticleOp, { p: 'svs' }>): void {
|
||||
@@ -619,7 +621,7 @@ export class ContentReassembler {
|
||||
lastFragment.vendorState = {
|
||||
...lastFragment.vendorState,
|
||||
[vendor]: state,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Helper to remove placeholder when real content arrives
|
||||
|
||||
@@ -36,13 +36,14 @@ export function aixClassifyStreamingError(error: any, isUserAbort: boolean, hasF
|
||||
|
||||
// Browser-level network connection drops (TypeError, happens below tRPC error wrapping layer)
|
||||
// Network errors - when the client is disconnected (Vercel 5min timeout, Mobile timeout / disconnect, etc) - they show up as TypeErrors
|
||||
// IMPORTANT: we will differentiate between the 2 'net-disconnected' cases in the UI, checking for the errorMessage '**network error**' vs '**connection terminated**'
|
||||
if (error instanceof TypeError && error.message === 'network error')
|
||||
return { errorType: 'net-disconnected', errorMessage: 'An unexpected issue occurred: **network error**.' };
|
||||
return { errorType: 'net-disconnected', errorMessage: 'An unexpected issue occurred: **network error**.' /* DO NOT CHANGE '**network error**' - usually client-side broken */ };
|
||||
|
||||
// tRPC <= 11.5.1 - Vercel Edge network disconnects are thrown form tRPC as 'Stream closed'
|
||||
// NOTE The behavior changed in 11.6+ for which we have an open upstream ticket: #6989
|
||||
if (error instanceof Error && error.message === 'Stream closed')
|
||||
return { errorType: 'net-disconnected', errorMessage: 'An unexpected issue occurred: **connection terminated**.' };
|
||||
return { errorType: 'net-disconnected', errorMessage: 'An unexpected issue occurred: **connection terminated**.' /* DO NOT CHANGE '**connection terminated**' - usually server (Vercel) side broken */ };
|
||||
|
||||
|
||||
// tRPC-level protocol errors (wrapped by tRPC client)
|
||||
|
||||
@@ -48,9 +48,9 @@ export function aixCreateModelFromLLMOptions(
|
||||
const {
|
||||
llmRef, llmTemperature, llmResponseTokens, llmTopP,
|
||||
llmVndAnt1MContext, llmVndAntSkills, llmVndAntThinkingBudget, llmVndAntWebFetch, llmVndAntWebSearch, llmVndAntEffort,
|
||||
llmVndGeminiAspectRatio, llmVndGeminiImageSize, llmVndGeminiCodeExecution, llmVndGeminiComputerUse, llmVndGeminiGoogleSearch, llmVndGeminiMediaResolution, llmVndGeminiShowThoughts, llmVndGeminiThinkingBudget, llmVndGeminiThinkingLevel,
|
||||
llmVndGeminiAspectRatio, llmVndGeminiImageSize, llmVndGeminiCodeExecution, llmVndGeminiComputerUse, llmVndGeminiGoogleSearch, llmVndGeminiInteractionsAgent, llmVndGeminiMediaResolution, llmVndGeminiShowThoughts, llmVndGeminiThinkingBudget, llmVndGeminiThinkingLevel,
|
||||
// llmVndMoonshotWebSearch,
|
||||
llmVndOaiReasoningEffort, llmVndOaiReasoningEffort4, llmVndOaiRestoreMarkdown, llmVndOaiVerbosity, llmVndOaiWebSearchContext, llmVndOaiWebSearchGeolocation, llmVndOaiImageGeneration,
|
||||
llmVndOaiReasoningEffort, llmVndOaiReasoningEffort4, llmVndOaiReasoningEffort52, llmVndOaiReasoningEffort52Pro, llmVndOaiRestoreMarkdown, llmVndOaiVerbosity, llmVndOaiWebSearchContext, llmVndOaiWebSearchGeolocation, llmVndOaiImageGeneration,
|
||||
llmVndOrtWebSearch,
|
||||
llmVndPerplexityDateFilter, llmVndPerplexitySearchMode,
|
||||
llmVndXaiSearchMode, llmVndXaiSearchSources, llmVndXaiSearchDateFilter,
|
||||
@@ -118,10 +118,11 @@ export function aixCreateModelFromLLMOptions(
|
||||
...(llmVndGeminiShowThoughts ? { vndGeminiShowThoughts: llmVndGeminiShowThoughts } : {}),
|
||||
...(llmVndGeminiThinkingBudget !== undefined ? { vndGeminiThinkingBudget: llmVndGeminiThinkingBudget } : {}),
|
||||
...(llmVndGeminiThinkingLevel ? { vndGeminiThinkingLevel: llmVndGeminiThinkingLevel } : {}),
|
||||
...(llmVndGeminiInteractionsAgent ? { vndGeminiInteractionsAgent: llmVndGeminiInteractionsAgent } : {}),
|
||||
// ...(llmVndGeminiUrlContext === 'auto' ? { vndGeminiUrlContext: llmVndGeminiUrlContext } : {}),
|
||||
// ...(llmVndMoonshotWebSearch === 'auto' ? { vndMoonshotWebSearch: 'auto' } : {}),
|
||||
...(llmVndOaiResponsesAPI ? { vndOaiResponsesAPI: true } : {}),
|
||||
...((llmVndOaiReasoningEffort4 || llmVndOaiReasoningEffort) ? { vndOaiReasoningEffort: llmVndOaiReasoningEffort4 || llmVndOaiReasoningEffort } : {}),
|
||||
...((llmVndOaiReasoningEffort52Pro || llmVndOaiReasoningEffort52 || llmVndOaiReasoningEffort4 || llmVndOaiReasoningEffort) ? { vndOaiReasoningEffort: llmVndOaiReasoningEffort52Pro || llmVndOaiReasoningEffort52 || llmVndOaiReasoningEffort4 || llmVndOaiReasoningEffort } : {}),
|
||||
...(llmVndOaiRestoreMarkdown ? { vndOaiRestoreMarkdown: llmVndOaiRestoreMarkdown } : {}),
|
||||
...(llmVndOaiVerbosity ? { vndOaiVerbosity: llmVndOaiVerbosity } : {}),
|
||||
...(llmVndOaiWebSearchContext ? { vndOaiWebSearchContext: llmVndOaiWebSearchContext } : {}),
|
||||
|
||||
@@ -468,11 +468,17 @@ export namespace AixWire_API {
|
||||
vndGeminiThinkingBudget: z.number().optional(), // old param
|
||||
vndGeminiThinkingLevel: z.enum(['high', 'medium', 'low']).optional(), // new param
|
||||
vndGeminiUrlContext: z.enum(['auto']).optional(),
|
||||
/**
|
||||
* [Gemini, 2025-12-19] Interactions API for Deep Research agent
|
||||
* When set to an agent name, uses the Interactions API instead of generateContent
|
||||
* See: https://ai.google.dev/gemini-api/docs/interactions
|
||||
*/
|
||||
vndGeminiInteractionsAgent: z.string().optional(),
|
||||
// Moonshot
|
||||
vndMoonshotWebSearch: z.enum(['auto']).optional(),
|
||||
// OpenAI
|
||||
vndOaiResponsesAPI: z.boolean().optional(),
|
||||
vndOaiReasoningEffort: z.enum(['minimal', 'low', 'medium', 'high']).optional(),
|
||||
vndOaiReasoningEffort: z.enum(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']).optional(),
|
||||
vndOaiRestoreMarkdown: z.boolean().optional(),
|
||||
vndOaiVerbosity: z.enum(['low', 'medium', 'high']).optional(),
|
||||
vndOaiWebSearchContext: z.enum(['low', 'medium', 'high']).optional(),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { SERVER_DEBUG_WIRE } from '~/server/wire';
|
||||
import { serverSideId } from '~/server/trpc/trpc.nanoid';
|
||||
|
||||
import { objectDeepCloneWithStringLimit, objectEstimateJsonSize } from '~/common/util/objectUtils';
|
||||
|
||||
import type { AixWire_Particles } from '../../api/aix.wiretypes';
|
||||
|
||||
import type { IParticleTransmitter, ParticleServerLogLevel } from './parsers/IParticleTransmitter';
|
||||
@@ -25,70 +27,6 @@ export const IssueSymbols = {
|
||||
};
|
||||
|
||||
|
||||
/** Estimates JSON size without stringifying (avoids memory spike on large objects). */
|
||||
function _fastEstimateJsonSize(value: any): number {
|
||||
if (value === null) return 4; // "null"
|
||||
if (value === undefined) return 0; // omitted in JSON
|
||||
if (typeof value === 'string')
|
||||
return value.length + 2; // quotes
|
||||
if (typeof value === 'number')
|
||||
return String(value).length;
|
||||
if (typeof value === 'boolean')
|
||||
return value ? 4 : 5; // "true" or "false"
|
||||
if (Array.isArray(value)) {
|
||||
let size = 2; // []
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
size += _fastEstimateJsonSize(value[i]);
|
||||
if (i < value.length - 1) size += 1; // comma
|
||||
}
|
||||
return size;
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
let size = 2; // {}
|
||||
const keys = Object.keys(value);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
size += key.length + 3; // "key":
|
||||
size += _fastEstimateJsonSize(value[key]);
|
||||
if (i < keys.length - 1) size += 1; // comma
|
||||
}
|
||||
return size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Deep-clones an object while ellipsizing any string exceeding maxBytes in the middle. */
|
||||
function _fastEllipsizeStringsInObject(value: any, maxBytes: number = DEBUG_REQUEST_MAX_STRING_BYTES): any {
|
||||
// handle primitives first
|
||||
if (value === null || value === undefined)
|
||||
return value;
|
||||
|
||||
// handle strings - ellipsize if too long
|
||||
if (typeof value === 'string') {
|
||||
if (value.length <= maxBytes)
|
||||
return value;
|
||||
const ellipsis = `...[${(value.length - maxBytes).toLocaleString()} bytes]...`;
|
||||
const half = Math.floor((maxBytes - ellipsis.length) / 2);
|
||||
return value.slice(0, half) + ellipsis + value.slice(-half);
|
||||
}
|
||||
|
||||
// handle other primitives (number, boolean)
|
||||
if (typeof value !== 'object')
|
||||
return value;
|
||||
|
||||
// handle arrays - recurse
|
||||
if (Array.isArray(value))
|
||||
return value.map(item => _fastEllipsizeStringsInObject(item, maxBytes));
|
||||
|
||||
// handle objects - recurse
|
||||
const result: any = {};
|
||||
for (const key in value)
|
||||
if (value.hasOwnProperty(key))
|
||||
result[key] = _fastEllipsizeStringsInObject(value[key], maxBytes);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Queues up and emits small messages (particles) to the client, for the purpose of a stateful
|
||||
* full reconstruction of the AixWire_Parts[] objects.
|
||||
@@ -194,7 +132,7 @@ export class ChatGenerateTransmitter implements IParticleTransmitter {
|
||||
|
||||
addDebugRequest(hideSensitiveData: boolean, url: string, headers: HeadersInit, body?: object) {
|
||||
// Ellipsize individual strings in the body object (e.g., base64 images) to reduce debug packet size
|
||||
const ellipsizedBody = body ? _fastEllipsizeStringsInObject(body) : undefined;
|
||||
const ellipsizedBody = body ? objectDeepCloneWithStringLimit(body, 'aix.addDebugRequest', DEBUG_REQUEST_MAX_STRING_BYTES) : undefined;
|
||||
const processedBody = ellipsizedBody ? JSON.stringify(ellipsizedBody, null, 2) : '';
|
||||
|
||||
this.transmissionQueue.push({
|
||||
@@ -204,7 +142,7 @@ export class ChatGenerateTransmitter implements IParticleTransmitter {
|
||||
url: url,
|
||||
headers: hideSensitiveData ? '(hidden sensitive data)' : JSON.stringify(headers, null, 2),
|
||||
body: processedBody,
|
||||
bodySize: body ? _fastEstimateJsonSize(body) : 0,
|
||||
bodySize: body ? objectEstimateJsonSize(body, 'aix.addDebugRequest') : 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -526,14 +464,15 @@ export class ChatGenerateTransmitter implements IParticleTransmitter {
|
||||
}
|
||||
|
||||
/** Communicates the upstream response handle, for remote control/resumability */
|
||||
setUpstreamHandle(handle: string, _type: 'oai-responses' /* the only one for now, used for type safety */) {
|
||||
setUpstreamHandle(handle: string, type: 'oai-responses' | 'gemini-interactions') {
|
||||
if (SERVER_DEBUG_WIRE)
|
||||
console.log('|response-handle|', handle);
|
||||
console.log('|response-handle|', handle, type);
|
||||
// NOTE: if needed, we could store the handle locally for server-side resumability, but we just implement client-side (correction, manual) for now
|
||||
const uht = type === 'gemini-interactions' ? 'vnd.gemini.interactions' : 'vnd.oai.responses';
|
||||
this.transmissionQueue.push({
|
||||
cg: 'set-upstream-handle',
|
||||
handle: {
|
||||
uht: 'vnd.oai.responses',
|
||||
uht: uht as any, // TODO: add 'vnd.gemini.interactions' to the type union in aix.wiretypes.ts
|
||||
responseId: handle,
|
||||
expiresAt: Date.now() + 30 * 24 * 3600 * 1000, // default: 30 days expiry
|
||||
},
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import type { AixAPI_Model, AixAPIChatGenerate_Request, AixMessages_ChatMessage } from '../../../api/aix.wiretypes';
|
||||
import { GeminiWire_API_Interactions } from '../../wiretypes/gemini.wiretypes';
|
||||
|
||||
import { aixSpillSystemToUser, approxDocPart_To_String } from './adapters.common';
|
||||
|
||||
|
||||
type TRequest = GeminiWire_API_Interactions.Request;
|
||||
|
||||
|
||||
/**
|
||||
* Gemini Interactions API adapter
|
||||
*
|
||||
* Converts AIX format to Gemini Interactions API format.
|
||||
* Used specifically for agents like Deep Research.
|
||||
*
|
||||
* Key differences from generateContent:
|
||||
* - Uses 'agent' instead of 'model' for agent-based interactions
|
||||
* - Uses 'input' with turns/content parts instead of 'contents'
|
||||
* - Supports background execution for long-running tasks
|
||||
* - Uses different streaming format (event_type-based)
|
||||
*/
|
||||
export function aixToGeminiInteractions(
|
||||
model: AixAPI_Model,
|
||||
_chatGenerate: AixAPIChatGenerate_Request,
|
||||
streaming: boolean,
|
||||
): TRequest {
|
||||
|
||||
// Pre-process CGR - approximate spill of System to User message
|
||||
const chatGenerate = aixSpillSystemToUser(_chatGenerate);
|
||||
|
||||
// Build system instruction from system message
|
||||
let systemInstruction: string | undefined = undefined;
|
||||
if (chatGenerate.systemMessage?.parts.length) {
|
||||
const systemParts: string[] = [];
|
||||
for (const part of chatGenerate.systemMessage.parts) {
|
||||
switch (part.pt) {
|
||||
case 'text':
|
||||
systemParts.push(part.text);
|
||||
break;
|
||||
case 'doc':
|
||||
systemParts.push(approxDocPart_To_String(part));
|
||||
break;
|
||||
case 'inline_image':
|
||||
case 'meta_cache_control':
|
||||
// Ignore these for system instruction
|
||||
break;
|
||||
default:
|
||||
console.warn(`[Gemini Interactions] Unsupported system part type: ${(part as any).pt}`);
|
||||
}
|
||||
}
|
||||
if (systemParts.length > 0)
|
||||
systemInstruction = systemParts.join('\n\n');
|
||||
}
|
||||
|
||||
// Convert chat sequence to turns
|
||||
const input = _toInteractionsTurns(chatGenerate.chatSequence);
|
||||
|
||||
// Get the agent name from the model's vndGeminiInteractionsAgent property
|
||||
const agentName = model.vndGeminiInteractionsAgent;
|
||||
|
||||
// For Deep Research and other background agents, we use background=true
|
||||
// This allows the agent to run asynchronously
|
||||
const isBackgroundAgent = agentName?.includes('deep-research');
|
||||
|
||||
// Construct the request payload
|
||||
const payload: TRequest = {
|
||||
// Agent-based interactions use 'agent' instead of 'model'
|
||||
agent: agentName,
|
||||
|
||||
// Input as array of turns
|
||||
input,
|
||||
|
||||
// System instruction (if any)
|
||||
system_instruction: systemInstruction,
|
||||
|
||||
// Generation config
|
||||
generation_config: {
|
||||
temperature: model.temperature ?? undefined,
|
||||
max_output_tokens: model.maxTokens ?? undefined,
|
||||
// Map thinking level for agents that support it
|
||||
thinking_level: model.vndGeminiThinkingLevel ?? undefined,
|
||||
},
|
||||
|
||||
// API options
|
||||
stream: streaming,
|
||||
background: isBackgroundAgent, // Enable background for Deep Research
|
||||
store: true, // Enable storage for state management
|
||||
};
|
||||
|
||||
// Clean up undefined values
|
||||
if (!payload.system_instruction)
|
||||
delete payload.system_instruction;
|
||||
if (payload.generation_config) {
|
||||
if (payload.generation_config.temperature === undefined)
|
||||
delete payload.generation_config.temperature;
|
||||
if (payload.generation_config.max_output_tokens === undefined)
|
||||
delete payload.generation_config.max_output_tokens;
|
||||
if (payload.generation_config.thinking_level === undefined)
|
||||
delete payload.generation_config.thinking_level;
|
||||
if (Object.keys(payload.generation_config).length === 0)
|
||||
delete payload.generation_config;
|
||||
}
|
||||
|
||||
// Validate the payload
|
||||
const validated = GeminiWire_API_Interactions.Request_schema.safeParse(payload);
|
||||
if (!validated.success) {
|
||||
console.warn('Gemini Interactions: invalid payload. Error:', validated.error.message);
|
||||
throw new Error(`Invalid sequence for Gemini Interactions API: ${validated.error.issues?.[0]?.message || validated.error.message || validated.error}.`);
|
||||
}
|
||||
|
||||
return validated.data;
|
||||
}
|
||||
|
||||
|
||||
// Content part type for Interactions API input
|
||||
type TContentPart =
|
||||
| { type: 'text'; text: string }
|
||||
| { type: 'image'; data?: string; mime_type?: string }
|
||||
| { type: 'audio'; data?: string; mime_type?: string }
|
||||
| { type: 'function_result'; name: string; call_id: string; result: unknown };
|
||||
|
||||
// Turn type for Interactions API input
|
||||
type TTurn = {
|
||||
role: 'user' | 'model';
|
||||
content: TContentPart[];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Convert AIX chat messages to Interactions API turns format
|
||||
*/
|
||||
function _toInteractionsTurns(chatSequence: AixMessages_ChatMessage[]): TTurn[] {
|
||||
return chatSequence.map(message => {
|
||||
const content: TContentPart[] = [];
|
||||
|
||||
for (const part of message.parts) {
|
||||
switch (part.pt) {
|
||||
|
||||
case 'text':
|
||||
content.push({
|
||||
type: 'text',
|
||||
text: part.text,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'inline_image':
|
||||
content.push({
|
||||
type: 'image',
|
||||
data: part.base64,
|
||||
mime_type: part.mimeType,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'inline_audio':
|
||||
content.push({
|
||||
type: 'audio',
|
||||
data: part.base64,
|
||||
mime_type: part.mimeType,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'doc':
|
||||
// Convert doc to text for now
|
||||
content.push({
|
||||
type: 'text',
|
||||
text: approxDocPart_To_String(part),
|
||||
});
|
||||
break;
|
||||
|
||||
case 'ma':
|
||||
// Model artifact (thinking) - skip for input
|
||||
break;
|
||||
|
||||
case 'meta_cache_control':
|
||||
case 'meta_in_reference_to':
|
||||
// Skip metadata parts
|
||||
break;
|
||||
|
||||
case 'tool_invocation':
|
||||
// For function calls, we'd need to handle these specially
|
||||
// For Deep Research, this is less relevant
|
||||
console.warn('[Gemini Interactions] Tool invocations not yet supported in input');
|
||||
break;
|
||||
|
||||
case 'tool_response':
|
||||
// Function results
|
||||
if (part.response.type === 'function_call') {
|
||||
content.push({
|
||||
type: 'function_result',
|
||||
name: part.response._name || part.id,
|
||||
call_id: part.id,
|
||||
result: part.response.result,
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`[Gemini Interactions] Unsupported part type: ${(part as any).pt}`);
|
||||
}
|
||||
}
|
||||
|
||||
// If no content, add empty text
|
||||
if (content.length === 0)
|
||||
content.push({ type: 'text', text: '' });
|
||||
|
||||
return {
|
||||
role: message.role === 'model' ? 'model' : 'user',
|
||||
content,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -38,7 +38,6 @@ export function aixToOpenAIChatCompletions(openAIDialect: OpenAIDialects, model:
|
||||
const hotFixAlternateUserAssistantRoles = openAIDialect === 'deepseek' || openAIDialect === 'perplexity';
|
||||
const hotFixRemoveEmptyMessages = openAIDialect === 'perplexity';
|
||||
const hotFixRemoveStreamOptions = openAIDialect === 'azure' || openAIDialect === 'mistral';
|
||||
const hotFixSquashMultiPartText = openAIDialect === 'deepseek';
|
||||
const hotFixThrowCannotFC =
|
||||
// [OpenRouter] 2025-10-02: do not throw, rather let it fail if upstream has issues
|
||||
// openAIDialect === 'openrouter' || /* OpenRouter FC support is not good (as of 2024-07-15) */
|
||||
@@ -60,8 +59,6 @@ export function aixToOpenAIChatCompletions(openAIDialect: OpenAIDialects, model:
|
||||
let chatMessages = _toOpenAIMessages(chatGenerate.systemMessage, chatGenerate.chatSequence, hotFixOpenAIOFamily);
|
||||
|
||||
// Apply hotfixes
|
||||
if (hotFixSquashMultiPartText)
|
||||
chatMessages = _fixSquashMultiPartText(chatMessages);
|
||||
|
||||
if (hotFixRemoveEmptyMessages)
|
||||
chatMessages = _fixRemoveEmptyMessages(chatMessages);
|
||||
@@ -248,11 +245,11 @@ export function aixToOpenAIChatCompletions(openAIDialect: OpenAIDialects, model:
|
||||
else if (model.vndGeminiThinkingBudget !== undefined)
|
||||
payload.reasoning = { max_tokens: model.vndGeminiThinkingBudget || 8192 };
|
||||
// OpenAI via OpenRouter
|
||||
else if (model.vndOaiReasoningEffort && model.vndOaiReasoningEffort !== 'minimal')
|
||||
else if (model.vndOaiReasoningEffort && model.vndOaiReasoningEffort !== 'minimal' && model.vndOaiReasoningEffort !== 'none')
|
||||
payload.reasoning = { effort: model.vndOaiReasoningEffort };
|
||||
|
||||
// FIX double-reasoning request - remove reasoning_effort after transferring it to reasoning (unless already set)
|
||||
if (payload.reasoning_effort && payload.reasoning_effort !== 'minimal') {
|
||||
if (payload.reasoning_effort && payload.reasoning_effort !== 'minimal' && payload.reasoning_effort !== 'none') {
|
||||
// we don't know which one takes precedence, so we prioritize .reasoning (OpenRouter) even if .reasoning_effort (OpenAI) is present
|
||||
if (!payload.reasoning)
|
||||
payload.reasoning = { effort: payload.reasoning_effort };
|
||||
@@ -350,17 +347,6 @@ function _fixRemoveStreamOptions(payload: TRequest): TRequest {
|
||||
return rest;
|
||||
}
|
||||
|
||||
function _fixSquashMultiPartText(chatMessages: TRequestMessages): TRequestMessages {
|
||||
// Convert multi-part text messages to single strings for older OpenAI dialects
|
||||
return chatMessages.reduce((acc, message) => {
|
||||
if (message.role === 'user' && Array.isArray(message.content))
|
||||
acc.push({ role: message.role, content: message.content.filter(part => part.type === 'text').map(textPart => textPart.text).filter(text => !!text).join(hotFixSquashTextSeparator) });
|
||||
else
|
||||
acc.push(message);
|
||||
return acc;
|
||||
}, [] as TRequestMessages);
|
||||
}
|
||||
|
||||
function _fixVndOaiRestoreMarkdown_Inline(payload: TRequest) {
|
||||
|
||||
// OpenAI - https://platform.openai.com/docs/guides/reasoning/advice-on-prompting#advice-on-prompting
|
||||
|
||||
@@ -2,20 +2,24 @@ import { anthropicAccess } from '~/modules/llms/server/anthropic/anthropic.acces
|
||||
import { geminiAccess } from '~/modules/llms/server/gemini/gemini.access';
|
||||
import { ollamaAccess } from '~/modules/llms/server/ollama/ollama.access';
|
||||
import { openAIAccess } from '~/modules/llms/server/openai/openai.access';
|
||||
// [DeepSeek, 2025-12-01] V3.2-Speciale temporary endpoint
|
||||
import { DEEPSEEK_SPECIALE_HOST, DEEPSEEK_SPECIALE_SUFFIX } from '~/modules/llms/server/openai/models/deepseek.models';
|
||||
|
||||
import type { AixAPI_Access, AixAPI_Model, AixAPI_ResumeHandle, AixAPIChatGenerate_Request } from '../../api/aix.wiretypes';
|
||||
import type { AixDemuxers } from '../stream.demuxers';
|
||||
|
||||
import { GeminiWire_API_Generate_Content } from '../wiretypes/gemini.wiretypes';
|
||||
import { GeminiWire_API_Generate_Content, GeminiWire_API_Interactions } from '../wiretypes/gemini.wiretypes';
|
||||
|
||||
import { aixToAnthropicMessageCreate } from './adapters/anthropic.messageCreate';
|
||||
import { aixToGeminiGenerateContent } from './adapters/gemini.generateContent';
|
||||
import { aixToGeminiInteractions } from './adapters/gemini.interactions';
|
||||
import { aixToOpenAIChatCompletions } from './adapters/openai.chatCompletions';
|
||||
import { aixToOpenAIResponses } from './adapters/openai.responsesCreate';
|
||||
|
||||
import type { IParticleTransmitter } from './parsers/IParticleTransmitter';
|
||||
import { createAnthropicMessageParser, createAnthropicMessageParserNS } from './parsers/anthropic.parser';
|
||||
import { createGeminiGenerateContentResponseParser } from './parsers/gemini.parser';
|
||||
import { createGeminiInteractionsResponseParser } from './parsers/gemini.interactions.parser';
|
||||
import { createOpenAIChatCompletionsChunkParser, createOpenAIChatCompletionsParserNS } from './parsers/openai.parser';
|
||||
import { createOpenAIResponseParserNS, createOpenAIResponsesEventParser } from './parsers/openai.responses.parser';
|
||||
|
||||
@@ -81,7 +85,27 @@ export function createChatGenerateDispatch(access: AixAPI_Access, model: AixAPI_
|
||||
};
|
||||
}
|
||||
|
||||
case 'gemini':
|
||||
case 'gemini': {
|
||||
/**
|
||||
* [Gemini, 2025-12-19] Interactions API for agents like Deep Research
|
||||
* When vndGeminiInteractionsAgent is set, use the Interactions API instead of generateContent
|
||||
*/
|
||||
const useInteractionsAPI = !!model.vndGeminiInteractionsAgent;
|
||||
|
||||
if (useInteractionsAPI) {
|
||||
// Use Interactions API for agent-based interactions (e.g., Deep Research)
|
||||
const agentName = model.vndGeminiInteractionsAgent!;
|
||||
return {
|
||||
request: {
|
||||
...geminiAccess(access, null, streaming ? GeminiWire_API_Interactions.streamingPostPath : GeminiWire_API_Interactions.postPath, false),
|
||||
method: 'POST',
|
||||
body: aixToGeminiInteractions(model, chatGenerate, streaming),
|
||||
},
|
||||
demuxerFormat: streaming ? 'fast-sse' : null,
|
||||
chatGenerateParse: createGeminiInteractionsResponseParser(agentName, streaming),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* [Gemini, 2025-04-17] For newer thinking parameters, use v1alpha (we only see statistically better results)
|
||||
*/
|
||||
@@ -96,6 +120,7 @@ export function createChatGenerateDispatch(access: AixAPI_Access, model: AixAPI_
|
||||
demuxerFormat: streaming ? 'fast-sse' : null,
|
||||
chatGenerateParse: createGeminiGenerateContentResponseParser(model.id.replace('models/', ''), streaming),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ollama has now an OpenAI compatibility layer for `chatGenerate` API, but still its own protocol for models listing.
|
||||
@@ -136,6 +161,22 @@ export function createChatGenerateDispatch(access: AixAPI_Access, model: AixAPI_
|
||||
case 'togetherai':
|
||||
case 'xai':
|
||||
|
||||
// [DeepSeek, 2025-12-01] V3.2-Speciale: Handle @speciale model ID marker
|
||||
if (dialect === 'deepseek' && model.id.endsWith(DEEPSEEK_SPECIALE_SUFFIX)) {
|
||||
const actualModelId = model.id.slice(0, -DEEPSEEK_SPECIALE_SUFFIX.length);
|
||||
const { headers } = openAIAccess(access, actualModelId, '/v1/chat/completions');
|
||||
return {
|
||||
request: {
|
||||
url: DEEPSEEK_SPECIALE_HOST + '/v1/chat/completions',
|
||||
headers,
|
||||
method: 'POST',
|
||||
body: aixToOpenAIChatCompletions('deepseek', { ...model, id: actualModelId }, chatGenerate, streaming),
|
||||
},
|
||||
demuxerFormat: streaming ? 'fast-sse' : null,
|
||||
chatGenerateParse: streaming ? createOpenAIChatCompletionsChunkParser() : createOpenAIChatCompletionsParserNS(),
|
||||
};
|
||||
}
|
||||
|
||||
// switch to the Responses API if the model supports it
|
||||
const isResponsesAPI = !!model.vndOaiResponsesAPI;
|
||||
if (isResponsesAPI) {
|
||||
|
||||
@@ -81,7 +81,7 @@ export interface IParticleTransmitter {
|
||||
setModelName(modelName: string): void;
|
||||
|
||||
/** Communicates the upstream response handle, for remote control/resumability */
|
||||
setUpstreamHandle(handle: string, type: 'oai-responses'): void;
|
||||
setUpstreamHandle(handle: string, type: 'oai-responses' | 'gemini-interactions'): void;
|
||||
|
||||
/** Communicates the finish reason to the client */
|
||||
setTokenStopReason(reason: AixWire_Particles.GCTokenStopReason): void;
|
||||
|
||||
@@ -61,6 +61,7 @@ export function createAnthropicMessageParser(): ChatGenerateParseFunction {
|
||||
let timeToFirstEvent: number;
|
||||
let messageStartTime: number | undefined = undefined;
|
||||
let chatInTokens: number | undefined = undefined;
|
||||
let needsTextSeparator = false; // insert text separator when text follows server tool
|
||||
|
||||
return function(pt: IParticleTransmitter, eventData: string, eventName?: string, context?: { retriesAvailable: boolean }): void {
|
||||
|
||||
@@ -155,7 +156,9 @@ export function createAnthropicMessageParser(): ChatGenerateParseFunction {
|
||||
|
||||
switch (content_block.type) { // .content_block_start.type
|
||||
case 'text':
|
||||
pt.appendText(content_block.text);
|
||||
// add separator when text follows server tool execution
|
||||
pt.appendText(!needsTextSeparator ? content_block.text : '\n\n' + content_block.text);
|
||||
needsTextSeparator = false;
|
||||
// Note: In streaming mode, citations arrive via citations_delta events, not on content_block_start
|
||||
break;
|
||||
|
||||
@@ -379,6 +382,11 @@ export function createAnthropicMessageParser(): ChatGenerateParseFunction {
|
||||
const _exhaustiveCheck: never = content_block;
|
||||
throw new Error(`Unexpected content block type: ${(content_block as any).type}`);
|
||||
}
|
||||
|
||||
// set separator flag when server tools complete (text after tools needs visual separation)
|
||||
if (content_block.type.includes('tool_use') || content_block.type.includes('tool_result'))
|
||||
needsTextSeparator = true;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -576,6 +584,7 @@ export function createAnthropicMessageParser(): ChatGenerateParseFunction {
|
||||
|
||||
export function createAnthropicMessageParserNS(): ChatGenerateParseFunction {
|
||||
const parserCreationTimestamp = Date.now();
|
||||
let needsTextSeparator = false; // insert text separator when text follows server tool
|
||||
|
||||
return function(pt: IParticleTransmitter, fullData: string /*, eventName?: string, context?: { retriesAvailable: boolean } */): void {
|
||||
|
||||
@@ -597,7 +606,9 @@ export function createAnthropicMessageParserNS(): ChatGenerateParseFunction {
|
||||
const isLastBlock = i === content.length - 1;
|
||||
switch (contentBlock.type) { // .content_block (non-streaming)
|
||||
case 'text':
|
||||
pt.appendText(contentBlock.text);
|
||||
// add separator when text follows server tool execution
|
||||
pt.appendText(!needsTextSeparator ? contentBlock.text : '\n\n' + contentBlock.text);
|
||||
needsTextSeparator = false;
|
||||
// Handle citations if present (non-streaming mode has all citations attached)
|
||||
if (contentBlock.citations && Array.isArray(contentBlock.citations)) {
|
||||
for (const citation of contentBlock.citations) {
|
||||
@@ -824,6 +835,10 @@ export function createAnthropicMessageParserNS(): ChatGenerateParseFunction {
|
||||
const _exhaustiveCheck: never = contentBlock;
|
||||
throw new Error(`Unexpected content block type: ${(contentBlock as any).type}`);
|
||||
}
|
||||
|
||||
// set separator flag when server tools complete (text after tools needs visual separation)
|
||||
if (contentBlock.type.includes('tool_use') || contentBlock.type.includes('tool_result'))
|
||||
needsTextSeparator = true;
|
||||
}
|
||||
|
||||
// -> Token Stop Reason
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import type { AixWire_Particles } from '../../../api/aix.wiretypes';
|
||||
import type { ChatGenerateParseFunction } from '../chatGenerate.dispatch';
|
||||
import type { IParticleTransmitter } from './IParticleTransmitter';
|
||||
import { IssueSymbols } from '../ChatGenerateTransmitter';
|
||||
|
||||
|
||||
/**
|
||||
* Gemini Interactions API Response Parser
|
||||
*
|
||||
* Parses responses from the Gemini Interactions API, which is used for
|
||||
* agents like Deep Research. Supports both streaming and non-streaming modes.
|
||||
*
|
||||
* Streaming events:
|
||||
* - content.delta: Incremental text/thought updates
|
||||
* - interaction.complete: Final interaction with full response
|
||||
*
|
||||
* Non-streaming:
|
||||
* - Single response object with outputs array
|
||||
*
|
||||
* Deep Research specifics:
|
||||
* - Uses background=true for long-running tasks
|
||||
* - Status can be: in_progress, completed, requires_action, failed, cancelled
|
||||
* - May require polling via interactions.get() for background tasks
|
||||
*/
|
||||
export function createGeminiInteractionsResponseParser(
|
||||
agentName: string,
|
||||
isStreaming: boolean,
|
||||
): ChatGenerateParseFunction {
|
||||
const parserCreationTimestamp = Date.now();
|
||||
let sentAgentName = false;
|
||||
let timeToFirstEvent: number | undefined;
|
||||
let interactionId: string | undefined;
|
||||
|
||||
return function(pt: IParticleTransmitter, rawEventData: string): void {
|
||||
|
||||
// Time to first event
|
||||
if (timeToFirstEvent === undefined)
|
||||
timeToFirstEvent = Date.now() - parserCreationTimestamp;
|
||||
|
||||
// Parse the raw event data
|
||||
let eventData: any;
|
||||
try {
|
||||
eventData = JSON.parse(rawEventData);
|
||||
} catch (e) {
|
||||
return pt.setDialectTerminatingIssue(`Failed to parse Interactions API response: ${e}`, null, 'srv-warn');
|
||||
}
|
||||
|
||||
// Set agent name as model name (if not already set)
|
||||
if (!sentAgentName) {
|
||||
pt.setModelName(agentName);
|
||||
sentAgentName = true;
|
||||
}
|
||||
|
||||
// Handle streaming vs non-streaming
|
||||
if (isStreaming) {
|
||||
_parseStreamingEvent(pt, eventData, parserCreationTimestamp, timeToFirstEvent);
|
||||
} else {
|
||||
_parseNonStreamingResponse(pt, eventData, parserCreationTimestamp, timeToFirstEvent);
|
||||
}
|
||||
|
||||
// Store interaction ID for potential polling
|
||||
if (eventData.id)
|
||||
interactionId = eventData.id;
|
||||
if (eventData.interaction?.id)
|
||||
interactionId = eventData.interaction.id;
|
||||
|
||||
// Store interaction ID for resumability (similar to OpenAI Responses)
|
||||
if (interactionId)
|
||||
pt.setUpstreamHandle(interactionId, 'gemini-interactions');
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse streaming events from the Interactions API
|
||||
*/
|
||||
function _parseStreamingEvent(
|
||||
pt: IParticleTransmitter,
|
||||
eventData: any,
|
||||
parserCreationTimestamp: number,
|
||||
timeToFirstEvent: number | undefined,
|
||||
): void {
|
||||
|
||||
const eventType = eventData.event_type;
|
||||
|
||||
switch (eventType) {
|
||||
|
||||
case 'content.delta':
|
||||
// Incremental content update
|
||||
const delta = eventData.delta;
|
||||
if (delta?.type === 'text' && delta.text) {
|
||||
pt.appendText(delta.text);
|
||||
} else if (delta?.type === 'thought' && delta.thought) {
|
||||
pt.appendReasoningText(delta.thought);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'interaction.complete':
|
||||
// Final interaction response
|
||||
const interaction = eventData.interaction;
|
||||
if (interaction) {
|
||||
_handleInteractionComplete(pt, interaction, parserCreationTimestamp, timeToFirstEvent);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
// Unknown event type - log but don't fail
|
||||
if (eventType)
|
||||
console.warn(`[Gemini Interactions] Unknown streaming event type: ${eventType}`);
|
||||
// For non-event-type responses (like status updates), try to parse as interaction
|
||||
else if (eventData.status)
|
||||
_handleInteractionStatus(pt, eventData);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse non-streaming response from the Interactions API
|
||||
*/
|
||||
function _parseNonStreamingResponse(
|
||||
pt: IParticleTransmitter,
|
||||
eventData: any,
|
||||
parserCreationTimestamp: number,
|
||||
timeToFirstEvent: number | undefined,
|
||||
): void {
|
||||
|
||||
// Non-streaming returns the full interaction object
|
||||
if (eventData.status) {
|
||||
_handleInteractionComplete(pt, eventData, parserCreationTimestamp, timeToFirstEvent);
|
||||
} else {
|
||||
pt.setDialectTerminatingIssue('Invalid Interactions API response: missing status', null, 'srv-warn');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle a complete interaction response
|
||||
*/
|
||||
function _handleInteractionComplete(
|
||||
pt: IParticleTransmitter,
|
||||
interaction: any,
|
||||
parserCreationTimestamp: number,
|
||||
timeToFirstEvent: number | undefined,
|
||||
): void {
|
||||
|
||||
// Handle status
|
||||
const status = interaction.status;
|
||||
switch (status) {
|
||||
|
||||
case 'completed':
|
||||
// Process all outputs
|
||||
if (interaction.outputs?.length) {
|
||||
for (const output of interaction.outputs) {
|
||||
_processOutput(pt, output);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'in_progress':
|
||||
// Background task still running - client should poll
|
||||
pt.appendText('[Deep Research is running in the background. Status: in progress...]\n');
|
||||
// Don't end the stream yet for background tasks
|
||||
return;
|
||||
|
||||
case 'requires_action':
|
||||
// Agent needs user input or function execution
|
||||
pt.appendText('[Agent requires action - function call or user input needed]\n');
|
||||
// Process any outputs that have been generated so far
|
||||
if (interaction.outputs?.length) {
|
||||
for (const output of interaction.outputs) {
|
||||
_processOutput(pt, output);
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'failed':
|
||||
pt.setTokenStopReason('cg-issue');
|
||||
return pt.setDialectTerminatingIssue('Deep Research failed', IssueSymbols.Generic, false);
|
||||
|
||||
case 'cancelled':
|
||||
pt.setTokenStopReason('cg-issue');
|
||||
return pt.setDialectTerminatingIssue('Deep Research was cancelled', null, false);
|
||||
|
||||
default:
|
||||
console.warn(`[Gemini Interactions] Unknown status: ${status}`);
|
||||
}
|
||||
|
||||
// Update metrics
|
||||
if (interaction.usage) {
|
||||
const metricsUpdate: AixWire_Particles.CGSelectMetrics = {
|
||||
TIn: interaction.usage.input_tokens,
|
||||
TOut: interaction.usage.output_tokens,
|
||||
};
|
||||
if (timeToFirstEvent !== undefined)
|
||||
metricsUpdate.dtStart = timeToFirstEvent;
|
||||
metricsUpdate.dtAll = Date.now() - parserCreationTimestamp;
|
||||
pt.updateMetrics(metricsUpdate);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle interaction status updates (for polling scenarios)
|
||||
*/
|
||||
function _handleInteractionStatus(
|
||||
pt: IParticleTransmitter,
|
||||
eventData: any,
|
||||
): void {
|
||||
const status = eventData.status;
|
||||
|
||||
switch (status) {
|
||||
case 'in_progress':
|
||||
// Still running - this might be a poll response
|
||||
pt.appendText('[Research in progress...]\n');
|
||||
break;
|
||||
|
||||
case 'completed':
|
||||
case 'requires_action':
|
||||
case 'failed':
|
||||
case 'cancelled':
|
||||
// Handle as complete interaction
|
||||
_handleInteractionComplete(pt, eventData, Date.now(), 0);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`[Gemini Interactions] Unknown status in poll: ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Process a single output from the interaction
|
||||
*/
|
||||
function _processOutput(pt: IParticleTransmitter, output: any): void {
|
||||
const outputType = output.type;
|
||||
|
||||
switch (outputType) {
|
||||
|
||||
case 'text':
|
||||
if (output.text)
|
||||
pt.appendText(output.text);
|
||||
break;
|
||||
|
||||
case 'thought':
|
||||
if (output.thought)
|
||||
pt.appendReasoningText(output.thought);
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
if (output.data && output.mime_type) {
|
||||
pt.appendImageInline(
|
||||
output.mime_type,
|
||||
output.data,
|
||||
'Gemini Generated Image',
|
||||
'Gemini Deep Research',
|
||||
'',
|
||||
);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'function_call':
|
||||
// Handle function calls from the agent
|
||||
pt.startFunctionCallInvocation(
|
||||
output.id || null,
|
||||
output.name,
|
||||
'json_object',
|
||||
output.arguments,
|
||||
);
|
||||
pt.endMessagePart();
|
||||
break;
|
||||
|
||||
case 'google_search_result':
|
||||
case 'url_context_result':
|
||||
// These are metadata/context outputs - could be used for citations
|
||||
// For now, we skip them as they're supplementary to the main text output
|
||||
break;
|
||||
|
||||
default:
|
||||
console.warn(`[Gemini Interactions] Unknown output type: ${outputType}`);
|
||||
}
|
||||
}
|
||||
@@ -586,6 +586,11 @@ export namespace AnthropicWire_Tools {
|
||||
required: z.array(z.string()).optional(), // 2025-02-24: seems to be removed; we may still have this, but it may also be within the 'properties' object
|
||||
}),
|
||||
|
||||
/**
|
||||
* [Anthropic, 2025-11-13] Structured Outputs - guarantees tool inputs to match `input_schema` exactly.
|
||||
*/
|
||||
strict: z.boolean().optional(),
|
||||
|
||||
/**
|
||||
* [Anthropic, 2025-11-24] Tool Search Tool - when true, this tool is not loaded into context initially and can be discovered via the tool search tool when needed.
|
||||
*/
|
||||
|
||||
@@ -894,3 +894,239 @@ export namespace GeminiWire_API_Models_List {
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// Interactions API (Deep Research Agent)
|
||||
// https://ai.google.dev/gemini-api/docs/interactions
|
||||
//
|
||||
export namespace GeminiWire_API_Interactions {
|
||||
|
||||
export const postPath = '/v1beta/interactions';
|
||||
export const streamingPostPath = '/v1beta/interactions?alt=sse';
|
||||
export const getPath = (interactionId: string) => `/v1beta/interactions/${interactionId}`;
|
||||
|
||||
// Input content types for the Interactions API
|
||||
|
||||
const TextInput_schema = z.object({
|
||||
type: z.literal('text'),
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
const ImageInput_schema = z.object({
|
||||
type: z.literal('image'),
|
||||
data: z.string().optional(), // base64-encoded
|
||||
uri: z.string().optional(),
|
||||
mime_type: z.string().optional(),
|
||||
});
|
||||
|
||||
const AudioInput_schema = z.object({
|
||||
type: z.literal('audio'),
|
||||
data: z.string().optional(), // base64-encoded
|
||||
mime_type: z.string().optional(),
|
||||
});
|
||||
|
||||
const VideoInput_schema = z.object({
|
||||
type: z.literal('video'),
|
||||
data: z.string().optional(), // base64-encoded
|
||||
mime_type: z.string().optional(),
|
||||
});
|
||||
|
||||
const DocumentInput_schema = z.object({
|
||||
type: z.literal('document'),
|
||||
data: z.string().optional(), // base64-encoded
|
||||
mime_type: z.string().optional(),
|
||||
});
|
||||
|
||||
const FunctionResultInput_schema = z.object({
|
||||
type: z.literal('function_result'),
|
||||
name: z.string(),
|
||||
call_id: z.string(),
|
||||
result: z.any(),
|
||||
});
|
||||
|
||||
const ContentPart_Input_schema = z.union([
|
||||
TextInput_schema,
|
||||
ImageInput_schema,
|
||||
AudioInput_schema,
|
||||
VideoInput_schema,
|
||||
DocumentInput_schema,
|
||||
FunctionResultInput_schema,
|
||||
]);
|
||||
|
||||
const Turn_schema = z.object({
|
||||
role: z.enum(['user', 'model']),
|
||||
content: z.union([
|
||||
z.array(ContentPart_Input_schema),
|
||||
z.string(),
|
||||
]),
|
||||
});
|
||||
|
||||
// Function tool definition
|
||||
const FunctionTool_schema = z.object({
|
||||
type: z.literal('function'),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
parameters: z.object({
|
||||
type: z.literal('object'),
|
||||
properties: z.record(z.string(), z.any()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
}).optional(),
|
||||
});
|
||||
|
||||
// Built-in tools
|
||||
const GoogleSearchTool_schema = z.object({
|
||||
type: z.literal('google_search'),
|
||||
});
|
||||
|
||||
const CodeExecutionTool_schema = z.object({
|
||||
type: z.literal('code_execution'),
|
||||
});
|
||||
|
||||
const UrlContextTool_schema = z.object({
|
||||
type: z.literal('url_context'),
|
||||
});
|
||||
|
||||
const McpServerTool_schema = z.object({
|
||||
type: z.literal('mcp_server'),
|
||||
name: z.string(),
|
||||
url: z.string(),
|
||||
});
|
||||
|
||||
const Tool_schema = z.union([
|
||||
FunctionTool_schema,
|
||||
GoogleSearchTool_schema,
|
||||
CodeExecutionTool_schema,
|
||||
UrlContextTool_schema,
|
||||
McpServerTool_schema,
|
||||
]);
|
||||
|
||||
// Generation config
|
||||
const GenerationConfig_schema = z.object({
|
||||
temperature: z.number().optional(),
|
||||
max_output_tokens: z.number().optional(),
|
||||
thinking_level: z.enum(['minimal', 'low', 'medium', 'high']).optional(),
|
||||
});
|
||||
|
||||
// Request
|
||||
export type Request = z.infer<typeof Request_schema>;
|
||||
export const Request_schema = z.object({
|
||||
// One of model or agent must be provided
|
||||
model: z.string().optional(),
|
||||
agent: z.string().optional(),
|
||||
|
||||
// Input can be a string, array of content parts, or array of turns
|
||||
input: z.union([
|
||||
z.string(),
|
||||
z.array(ContentPart_Input_schema),
|
||||
z.array(Turn_schema),
|
||||
]),
|
||||
|
||||
// Optional configuration
|
||||
tools: z.array(Tool_schema).optional(),
|
||||
response_format: z.any().optional(), // JSON schema for structured output
|
||||
generation_config: GenerationConfig_schema.optional(),
|
||||
system_instruction: z.string().optional(),
|
||||
|
||||
// Stateful conversation
|
||||
previous_interaction_id: z.string().optional(),
|
||||
|
||||
// API options
|
||||
stream: z.boolean().optional(),
|
||||
background: z.boolean().optional(), // Only for agents
|
||||
store: z.boolean().optional(), // Default: true
|
||||
});
|
||||
|
||||
|
||||
// Output content types
|
||||
|
||||
const TextOutput_schema = z.object({
|
||||
type: z.literal('text'),
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
const ThoughtOutput_schema = z.object({
|
||||
type: z.literal('thought'),
|
||||
thought: z.string(),
|
||||
});
|
||||
|
||||
const ImageOutput_schema = z.object({
|
||||
type: z.literal('image'),
|
||||
data: z.string(), // base64-encoded
|
||||
mime_type: z.string(),
|
||||
});
|
||||
|
||||
const FunctionCallOutput_schema = z.object({
|
||||
type: z.literal('function_call'),
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
arguments: z.any(),
|
||||
});
|
||||
|
||||
const GoogleSearchResultOutput_schema = z.object({
|
||||
type: z.literal('google_search_result'),
|
||||
// Search result data
|
||||
});
|
||||
|
||||
const UrlContextResultOutput_schema = z.object({
|
||||
type: z.literal('url_context_result'),
|
||||
// URL context data
|
||||
});
|
||||
|
||||
const ContentPart_Output_schema = z.union([
|
||||
TextOutput_schema,
|
||||
ThoughtOutput_schema,
|
||||
ImageOutput_schema,
|
||||
FunctionCallOutput_schema,
|
||||
GoogleSearchResultOutput_schema,
|
||||
UrlContextResultOutput_schema,
|
||||
]);
|
||||
|
||||
// Usage metadata
|
||||
const Usage_schema = z.object({
|
||||
input_tokens: z.number().optional(),
|
||||
output_tokens: z.number().optional(),
|
||||
total_tokens: z.number().optional(),
|
||||
});
|
||||
|
||||
// Interaction status
|
||||
const Status_enum = z.enum([
|
||||
'in_progress',
|
||||
'completed',
|
||||
'requires_action',
|
||||
'failed',
|
||||
'cancelled',
|
||||
]);
|
||||
|
||||
// Response (non-streaming)
|
||||
export type Response = z.infer<typeof Response_schema>;
|
||||
export const Response_schema = z.object({
|
||||
id: z.string(),
|
||||
status: Status_enum,
|
||||
outputs: z.array(ContentPart_Output_schema).optional(),
|
||||
usage: Usage_schema.optional(),
|
||||
});
|
||||
|
||||
|
||||
// Streaming event types
|
||||
|
||||
const ContentDeltaEvent_schema = z.object({
|
||||
event_type: z.literal('content.delta'),
|
||||
delta: z.union([
|
||||
z.object({ type: z.literal('text'), text: z.string() }),
|
||||
z.object({ type: z.literal('thought'), thought: z.string() }),
|
||||
]),
|
||||
});
|
||||
|
||||
const InteractionCompleteEvent_schema = z.object({
|
||||
event_type: z.literal('interaction.complete'),
|
||||
interaction: Response_schema,
|
||||
});
|
||||
|
||||
export type StreamEvent = z.infer<typeof StreamEvent_schema>;
|
||||
export const StreamEvent_schema = z.union([
|
||||
ContentDeltaEvent_schema,
|
||||
InteractionCompleteEvent_schema,
|
||||
]);
|
||||
|
||||
}
|
||||
|
||||
@@ -312,11 +312,11 @@ export namespace OpenAIWire_API_Chat_Completions {
|
||||
stream_options: z.object({
|
||||
include_usage: z.boolean().optional(), // If set, an additional chunk will be streamed with a 'usage' field on the entire request.
|
||||
}).optional(),
|
||||
reasoning_effort: z.enum(['minimal', 'low', 'medium', 'high']).optional(), // [OpenAI, 2024-12-17] [Perplexity, 2025-06-23] reasoning effort
|
||||
reasoning_effort: z.enum(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']).optional(), // [OpenAI, 2024-12-17] [Perplexity, 2025-06-23] reasoning effort
|
||||
// [OpenRouter, 2025-11-11] Unified reasoning parameter for all models
|
||||
reasoning: z.object({
|
||||
max_tokens: z.number().int().positive().optional(), // Token-based control (Anthropic, Gemini): 1024-32000
|
||||
effort: z.enum(['low', 'medium', 'high']).optional(), // Effort-based control (OpenAI o1/o3, DeepSeek): allocates % of max_tokens
|
||||
effort: z.enum(['none', 'low', 'medium', 'high', 'xhigh']).optional(), // Effort-based control (OpenAI o1/o3/GPT-5, DeepSeek): allocates % of max_tokens
|
||||
enabled: z.boolean().optional(), // Simple enable with medium effort defaults
|
||||
exclude: z.boolean().optional(), // Use reasoning internally without returning it in response
|
||||
}).optional(),
|
||||
@@ -1065,6 +1065,13 @@ export namespace OpenAIWire_Responses_Items {
|
||||
type: z.literal('web_search_call'),
|
||||
id: z.string(), // unique ID of the output item
|
||||
|
||||
// BREAKING CHANGE from OpenAI - 2025-12-11
|
||||
// redefining the following because we need 'searching' too here (seen during web search streaming)
|
||||
status: z.enum([
|
||||
'searching', // 2025-12-11: seen on OpenAI for `web_search_call` items when used with GPT 5.2 Pro, with web search on
|
||||
'in_progress', 'completed', 'incomplete',
|
||||
]).optional(),
|
||||
|
||||
// action may be present with `include: ['web_search_call.action.sources']`
|
||||
action: z.union([
|
||||
|
||||
@@ -1413,7 +1420,7 @@ export namespace OpenAIWire_API_Responses {
|
||||
|
||||
// configure reasoning
|
||||
reasoning: z.object({
|
||||
effort: z.enum(['minimal', 'low', 'medium', 'high']).nullish(), // defaults to 'medium'
|
||||
effort: z.enum(['none', 'minimal', 'low', 'medium', 'high', 'xhigh']).nullish(), // defaults to 'none' for GPT-5.2, 'medium' for older
|
||||
summary: z.enum(['auto', 'concise', 'detailed']).nullish(),
|
||||
}).nullish(),
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import { ChatMessageMemo } from '../../../apps/chat/components/message/ChatMessage';
|
||||
|
||||
import type { DLLMId } from '~/common/stores/llms/llms.types';
|
||||
import type { DMessageFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
|
||||
import type { DMessageId } from '~/common/stores/chat/chat.message';
|
||||
import { messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
|
||||
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
@@ -104,6 +106,20 @@ export function Fusion(props: {
|
||||
toggleFusionGathering(props.fusionId);
|
||||
}, [props.fusionId, toggleFusionGathering]);
|
||||
|
||||
const handleFragmentDelete = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId) => {
|
||||
const { fusions, fusionDeleteFragment } = props.beamStore.getState();
|
||||
const fusion = fusions.find(f => f.outputDMessage?.id === messageId);
|
||||
if (fusion)
|
||||
fusionDeleteFragment(fusion.fusionId, fragmentId);
|
||||
}, [props.beamStore]);
|
||||
|
||||
const handleFragmentReplace = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => {
|
||||
const { fusions, fusionReplaceFragment } = props.beamStore.getState();
|
||||
const fusion = fusions.find(f => f.outputDMessage?.id === messageId);
|
||||
if (fusion)
|
||||
fusionReplaceFragment(fusion.fusionId, fragmentId, newFragment);
|
||||
}, [props.beamStore]);
|
||||
|
||||
// escape hatch: no factory, no fusion - nothing to do
|
||||
if (!fusion || !factory)
|
||||
return;
|
||||
@@ -168,6 +184,8 @@ export function Fusion(props: {
|
||||
hideAvatar
|
||||
showUnsafeHtmlCode={true}
|
||||
adjustContentScaling={-1}
|
||||
onMessageFragmentDelete={handleFragmentDelete}
|
||||
onMessageFragmentReplace={handleFragmentReplace}
|
||||
sx={!cardScrolling ? beamCardMessageSx : beamCardMessageScrollingSx}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { StateCreator } from 'zustand/vanilla';
|
||||
|
||||
import type { DLLMId } from '~/common/stores/llms/llms.types';
|
||||
import type { DMessage } from '~/common/stores/chat/chat.message';
|
||||
import type { DMessageFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
|
||||
import { agiUuid } from '~/common/util/idUtils';
|
||||
|
||||
import { CUSTOM_FACTORY_ID, FFactoryId, findFusionFactory, FUSION_FACTORIES, FUSION_FACTORY_DEFAULT } from './instructions/beam.gather.factories';
|
||||
@@ -131,6 +132,8 @@ export interface GatherStoreSlice extends GatherStateSlice {
|
||||
fusionRecreateAsCustom: (sourceFusionId: BFusionId) => void;
|
||||
fusionInstructionUpdate: (fusionId: BFusionId, instructionIndex: number, update: Partial<Instruction>) => void;
|
||||
fusionSetLlmId: (fusionId: BFusionId, llmId: DLLMId | null) => void;
|
||||
fusionDeleteFragment: (fusionId: BFusionId, fragmentId: DMessageFragmentId) => void;
|
||||
fusionReplaceFragment: (fusionId: BFusionId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => void;
|
||||
|
||||
createFusion: () => void;
|
||||
removeFusion: (fusionId: BFusionId) => void;
|
||||
@@ -213,6 +216,58 @@ export const createGatherSlice: StateCreator<RootStoreSlice & ScatterStoreSlice
|
||||
llmId,
|
||||
}),
|
||||
|
||||
fusionDeleteFragment: (fusionId: BFusionId, fragmentId: DMessageFragmentId) =>
|
||||
_get()._fusionUpdate(fusionId, (fusion) => {
|
||||
// Ensure there's an output message
|
||||
if (!fusion.outputDMessage) {
|
||||
console.error(`fusionDeleteFragment: No output message for fusion ${fusionId}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Find the fragment to delete
|
||||
const fragmentIndex = fusion.outputDMessage.fragments.findIndex(f => f.fId === fragmentId);
|
||||
if (fragmentIndex < 0) {
|
||||
console.error(`fusionDeleteFragment: Fragment not found for ID ${fragmentId} in fusion ${fusionId}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
outputDMessage: {
|
||||
...fusion.outputDMessage,
|
||||
fragments: fusion.outputDMessage.fragments.filter((_, index) => index !== fragmentIndex),
|
||||
updated: Date.now(),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
fusionReplaceFragment: (fusionId: BFusionId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) =>
|
||||
_get()._fusionUpdate(fusionId, (fusion) => {
|
||||
// Ensure there's an output message
|
||||
if (!fusion.outputDMessage) {
|
||||
console.error(`fusionReplaceFragment: No output message for fusion ${fusionId}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Find the fragment to replace
|
||||
const fragmentIndex = fusion.outputDMessage.fragments.findIndex(f => f.fId === fragmentId);
|
||||
if (fragmentIndex < 0) {
|
||||
console.error(`fusionReplaceFragment: Fragment not found for ID ${fragmentId} in fusion ${fusionId}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
outputDMessage: {
|
||||
...fusion.outputDMessage,
|
||||
fragments: fusion.outputDMessage.fragments.map((fragment, index) =>
|
||||
(index === fragmentIndex)
|
||||
? { ...newFragment }
|
||||
: fragment,
|
||||
),
|
||||
updated: Date.now(),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
|
||||
createFusion: () => {
|
||||
// get factory
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { SvgIcon } from '@mui/material';
|
||||
import type { SvgIcon } from '@mui/joy';
|
||||
import BuildRoundedIcon from '@mui/icons-material/BuildRounded';
|
||||
import CheckBoxOutlinedIcon from '@mui/icons-material/CheckBoxOutlined';
|
||||
import MediationOutlinedIcon from '@mui/icons-material/MediationOutlined';
|
||||
@@ -33,7 +33,7 @@ export const FUSION_FACTORIES: FusionFactorySpec[] = [
|
||||
shortLabel: 'Fuse',
|
||||
addLabel: 'Add Fusion',
|
||||
cardTitle: 'Combined Response',
|
||||
Icon: MediationOutlinedIcon,
|
||||
Icon: MediationOutlinedIcon as typeof SvgIcon,
|
||||
description: 'AI combines conversation details and ideas into one clear, comprehensive answer.',
|
||||
createInstructions: () => [
|
||||
{
|
||||
@@ -59,7 +59,7 @@ Synthesize the perfect cohesive response to my last message that merges the coll
|
||||
shortLabel: 'Guided',
|
||||
addLabel: 'Add Checklist',
|
||||
cardTitle: 'Guided Response',
|
||||
Icon: CheckBoxOutlinedIcon,
|
||||
Icon: CheckBoxOutlinedIcon as typeof SvgIcon,
|
||||
description: 'Choose between options extracted by AI from the replies, and the model will combine your selections into a single answer.',
|
||||
// description: 'This approach employs a two-stage, interactive process where an AI first generates a checklist of insights from a conversation for user selection, then synthesizes those selections into a tailored, comprehensive response, integrating user preferences with AI analysis and creativity.',
|
||||
createInstructions: () => [
|
||||
@@ -121,7 +121,7 @@ The final output should reflect a deep understanding of the user's preferences a
|
||||
shortLabel: 'Compare',
|
||||
addLabel: 'Add Breakdown',
|
||||
cardTitle: 'Evaluation Table',
|
||||
Icon: TableViewRoundedIcon,
|
||||
Icon: TableViewRoundedIcon as typeof SvgIcon,
|
||||
description: 'Analyzes and compares AI responses, offering a structured framework to support your response choice.',
|
||||
createInstructions: () => [
|
||||
{
|
||||
@@ -168,7 +168,7 @@ Only work with the provided {{N}} responses. Begin with listing the criteria.`.t
|
||||
shortLabel: 'Custom',
|
||||
addLabel: 'Add Custom',
|
||||
cardTitle: 'User Defined',
|
||||
Icon: BuildRoundedIcon,
|
||||
Icon: BuildRoundedIcon as typeof SvgIcon,
|
||||
description: 'Define your own fusion prompt.',
|
||||
createInstructions: () => [
|
||||
{
|
||||
|
||||
@@ -13,6 +13,8 @@ import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { ChatMessageMemo } from '../../../apps/chat/components/message/ChatMessage';
|
||||
|
||||
import type { DMessageFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
|
||||
import type { DMessageId } from '~/common/stores/chat/chat.message';
|
||||
import { DLLMId, LLM_IF_OAI_Reasoning } from '~/common/stores/llms/llms.types';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
@@ -216,6 +218,20 @@ export function BeamRay(props: {
|
||||
rayToggleScattering(props.rayId);
|
||||
}, [props.rayId, rayToggleScattering]);
|
||||
|
||||
const handleFragmentDelete = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId) => {
|
||||
const { rays, rayDeleteFragment } = props.beamStore.getState();
|
||||
const ray = rays.find(ray => ray.message.id === messageId);
|
||||
if (ray)
|
||||
rayDeleteFragment(ray.rayId, fragmentId);
|
||||
}, [props.beamStore]);
|
||||
|
||||
const handleFragmentReplace = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => {
|
||||
const { rays, rayReplaceFragment } = props.beamStore.getState();
|
||||
const ray = rays.find(ray => ray.message.id === messageId);
|
||||
if (ray)
|
||||
rayReplaceFragment(ray.rayId, fragmentId, newFragment);
|
||||
}, [props.beamStore]);
|
||||
|
||||
/*const handleRayToggleSelect = React.useCallback(() => {
|
||||
toggleUserSelection(props.rayId);
|
||||
}, [props.rayId, toggleUserSelection]);*/
|
||||
@@ -264,6 +280,8 @@ export function BeamRay(props: {
|
||||
hideAvatar
|
||||
showUnsafeHtmlCode={true}
|
||||
adjustContentScaling={-1}
|
||||
onMessageFragmentDelete={handleFragmentDelete}
|
||||
onMessageFragmentReplace={handleFragmentReplace}
|
||||
sx={!cardScrolling ? beamCardMessageSx : beamCardMessageScrollingSx}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { AixChatGenerateContent_DMessageGuts, aixChatGenerateContent_DMessage_Fr
|
||||
import type { DLLMId } from '~/common/stores/llms/llms.types';
|
||||
import { agiUuid } from '~/common/util/idUtils';
|
||||
import { createDMessageEmpty, DMessage, duplicateDMessage, messageWasInterruptedAtStart } from '~/common/stores/chat/chat.message';
|
||||
import { createPlaceholderVoidFragment } from '~/common/stores/chat/chat.fragments';
|
||||
import { createPlaceholderVoidFragment, DMessageFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
|
||||
import { findLLMOrThrow } from '~/common/stores/llms/store-llms';
|
||||
import { getUXLabsHighPerformance } from '~/common/stores/store-ux-labs';
|
||||
import { splitSystemMessageFromHistory } from '~/common/stores/chat/chat.conversation';
|
||||
@@ -190,6 +190,8 @@ export interface ScatterStoreSlice extends ScatterStateSlice {
|
||||
stopScatteringAll: () => void;
|
||||
rayToggleScattering: (rayId: BRayId) => void;
|
||||
raySetLlmId: (rayId: BRayId, llmId: DLLMId | null) => void;
|
||||
rayDeleteFragment: (rayId: BRayId, fragmentId: DMessageFragmentId) => void;
|
||||
rayReplaceFragment: (rayId: BRayId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => void;
|
||||
_rayUpdate: (rayId: BRayId, update: Partial<BRay> | ((ray: BRay) => Partial<BRay>)) => void;
|
||||
|
||||
_storeLastScatterConfig: () => void;
|
||||
@@ -345,6 +347,46 @@ export const createScatterSlice: StateCreator<RootStoreSlice & ScatterStoreSlice
|
||||
_storeLastScatterConfig();
|
||||
},
|
||||
|
||||
rayDeleteFragment: (rayId: BRayId, fragmentId: DMessageFragmentId) =>
|
||||
_get()._rayUpdate(rayId, (ray) => {
|
||||
// Find the fragment to delete
|
||||
const fragmentIndex = ray.message.fragments.findIndex(f => f.fId === fragmentId);
|
||||
if (fragmentIndex < 0) {
|
||||
console.error(`rayDeleteFragment: Fragment not found for ID ${fragmentId} in ray ${rayId}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
message: {
|
||||
...ray.message,
|
||||
fragments: ray.message.fragments.filter((_, index) => index !== fragmentIndex),
|
||||
updated: Date.now(),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
rayReplaceFragment: (rayId: BRayId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) =>
|
||||
_get()._rayUpdate(rayId, (ray) => {
|
||||
// Find the fragment to replace
|
||||
const fragmentIndex = ray.message.fragments.findIndex(f => f.fId === fragmentId);
|
||||
if (fragmentIndex < 0) {
|
||||
console.error(`rayReplaceFragment: Fragment not found for ID ${fragmentId} in ray ${rayId}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
message: {
|
||||
...ray.message,
|
||||
fragments: ray.message.fragments.map((fragment, index) =>
|
||||
(index === fragmentIndex)
|
||||
? { ...newFragment }
|
||||
: fragment,
|
||||
),
|
||||
updated: Date.now(),
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
_rayUpdate: (rayId: BRayId, update: Partial<BRay> | ((ray: BRay) => Partial<BRay>)) =>
|
||||
_set(state => ({
|
||||
rays: state.rays.map(ray => (ray.rayId === rayId)
|
||||
|
||||
@@ -143,13 +143,31 @@ async function workerPuppeteer(
|
||||
};
|
||||
|
||||
// [puppeteer] start the remote session
|
||||
const browser: Browser = await puppeteer.connect({
|
||||
browserWSEndpoint,
|
||||
// Add default options for better stability
|
||||
// defaultViewport: { width: 1024, height: 768 },
|
||||
// acceptInsecureCerts: true,
|
||||
protocolTimeout: WORKER_TIMEOUT + 2000, // 2s extra for taking the screenshot?
|
||||
});
|
||||
let browser: Browser;
|
||||
try {
|
||||
browser = await puppeteer.connect({
|
||||
browserWSEndpoint,
|
||||
// Add default options for better stability
|
||||
// defaultViewport: { width: 1024, height: 768 },
|
||||
// acceptInsecureCerts: true,
|
||||
protocolTimeout: WORKER_TIMEOUT + 2000, // 2s extra for taking the screenshot?
|
||||
});
|
||||
} catch (connectError: any) {
|
||||
// Transform connection errors into user-friendly messages
|
||||
const errorMessage = connectError?.message || '';
|
||||
if (errorMessage.includes('403'))
|
||||
throw new Error('Browse service authentication failed (403). Please check your browser endpoint credentials.');
|
||||
if (errorMessage.includes('401'))
|
||||
throw new Error('Browse service unauthorized (401). Invalid credentials for the browser endpoint.');
|
||||
if (errorMessage.includes('429'))
|
||||
throw new Error('Browse service rate limited (429). Too many requests, please try again later.');
|
||||
if (errorMessage.includes('502') || errorMessage.includes('503') || errorMessage.includes('504'))
|
||||
throw new Error('Browse service temporarily unavailable. Please try again later.');
|
||||
if (errorMessage.includes('ECONNREFUSED') || errorMessage.includes('ENOTFOUND'))
|
||||
throw new Error('Browse service unreachable. The browser endpoint is not accessible.');
|
||||
// Re-throw with a cleaner message for other connection errors
|
||||
throw new Error(`Browse service connection failed: ${errorMessage || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
// for local testing, open an incognito context, to separate cookies
|
||||
let incognitoContext: BrowserContext | null = null;
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FormControl } from '@mui/joy';
|
||||
|
||||
import { useChatAutoAI } from '../../apps/chat/store-app-chat';
|
||||
|
||||
import { AlreadySet } from '~/common/components/AlreadySet';
|
||||
import { FormInputKey } from '~/common/components/forms/FormInputKey';
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormRadioControl } from '~/common/components/forms/FormRadioControl';
|
||||
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
|
||||
import { isElevenLabsEnabled } from './elevenlabs.client';
|
||||
import { useElevenLabsVoiceDropdown, useElevenLabsVoices } from './useElevenLabsVoiceDropdown';
|
||||
import { useElevenLabsApiKey } from './store-module-elevenlabs';
|
||||
|
||||
|
||||
export function ElevenlabsSettings() {
|
||||
|
||||
// state
|
||||
const [apiKey, setApiKey] = useElevenLabsApiKey();
|
||||
|
||||
// external state
|
||||
const { autoSpeak, setAutoSpeak } = useChatAutoAI();
|
||||
const { hasVoices } = useElevenLabsVoices();
|
||||
const { isConfiguredServerSide } = useCapabilityElevenLabs();
|
||||
const { voicesDropdown } = useElevenLabsVoiceDropdown(true);
|
||||
|
||||
|
||||
// derived state
|
||||
const isValidKey = isElevenLabsEnabled(apiKey);
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
{/*<FormHelperText>*/}
|
||||
{/* 📢 Hear AI responses, even in your own voice*/}
|
||||
{/*</FormHelperText>*/}
|
||||
|
||||
<FormRadioControl
|
||||
title='Speak Responses'
|
||||
description={autoSpeak === 'off' ? 'Off' : 'First paragraph'}
|
||||
tooltip={!hasVoices ? 'No voices available, please configure a voice synthesis service' : undefined}
|
||||
disabled={!hasVoices}
|
||||
options={[
|
||||
{ value: 'off', label: 'Off' },
|
||||
{ value: 'firstLine', label: 'Start' },
|
||||
{ value: 'all', label: 'Full' },
|
||||
]}
|
||||
value={autoSpeak} onChange={setAutoSpeak}
|
||||
/>
|
||||
|
||||
|
||||
{!isConfiguredServerSide && <FormInputKey
|
||||
autoCompleteId='elevenlabs-key' label='ElevenLabs API Key'
|
||||
rightLabel={<AlreadySet required={!isConfiguredServerSide} />}
|
||||
value={apiKey} onChange={setApiKey}
|
||||
required={!isConfiguredServerSide} isError={!isValidKey}
|
||||
/>}
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title='Assistant Voice' />
|
||||
{voicesDropdown}
|
||||
</FormControl>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities';
|
||||
|
||||
import { AudioLivePlayer } from '~/common/util/audio/AudioLivePlayer';
|
||||
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
|
||||
import { CapabilityElevenLabsSpeechSynthesis } from '~/common/components/useCapabilities';
|
||||
import { apiStream } from '~/common/util/trpc.client';
|
||||
import { convert_Base64_To_UInt8Array } from '~/common/util/blobUtils';
|
||||
import { useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
|
||||
import { getElevenLabsData, useElevenLabsData } from './store-module-elevenlabs';
|
||||
|
||||
|
||||
export const isValidElevenLabsApiKey = (apiKey?: string) => !!apiKey && apiKey.trim()?.length >= 32;
|
||||
|
||||
export const isElevenLabsEnabled = (apiKey?: string) =>
|
||||
apiKey ? isValidElevenLabsApiKey(apiKey)
|
||||
: getBackendCapabilities().hasVoiceElevenLabs;
|
||||
|
||||
|
||||
export function useCapability(): CapabilityElevenLabsSpeechSynthesis {
|
||||
const [clientApiKey, voiceId] = useElevenLabsData();
|
||||
const isConfiguredServerSide = getBackendCapabilities().hasVoiceElevenLabs;
|
||||
const isConfiguredClientSide = clientApiKey ? isValidElevenLabsApiKey(clientApiKey) : false;
|
||||
const mayWork = isConfiguredServerSide || isConfiguredClientSide || !!voiceId;
|
||||
return { mayWork, isConfiguredServerSide, isConfiguredClientSide };
|
||||
}
|
||||
|
||||
|
||||
interface ElevenLabsSpeakResult {
|
||||
success: boolean;
|
||||
audioBase64?: string; // Available when not streaming
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Speaks text using ElevenLabs TTS
|
||||
* @returns Object with success status and optionally the audio base64 (when not streaming)
|
||||
*/
|
||||
export async function elevenLabsSpeakText(text: string, voiceId: string | undefined, audioStreaming: boolean, audioTurbo: boolean): Promise<ElevenLabsSpeakResult> {
|
||||
// Early validation
|
||||
if (!(text?.trim())) {
|
||||
// console.log('ElevenLabs: No text to speak');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const { elevenLabsApiKey, elevenLabsVoiceId } = getElevenLabsData();
|
||||
if (!isElevenLabsEnabled(elevenLabsApiKey)) {
|
||||
// console.warn('ElevenLabs: Service not enabled or configured');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const { preferredLanguage } = useUIPreferencesStore.getState();
|
||||
const nonEnglish = !(preferredLanguage?.toLowerCase()?.startsWith('en'));
|
||||
|
||||
// audio live player instance, if needed
|
||||
let liveAudioPlayer: AudioLivePlayer | undefined;
|
||||
let playbackStarted = false;
|
||||
let audioBase64: string | undefined;
|
||||
|
||||
try {
|
||||
|
||||
const stream = await apiStream.elevenlabs.speech.mutate({
|
||||
xiKey: elevenLabsApiKey,
|
||||
voiceId: voiceId || elevenLabsVoiceId,
|
||||
text: text,
|
||||
nonEnglish,
|
||||
audioStreaming,
|
||||
audioTurbo,
|
||||
});
|
||||
|
||||
for await (const piece of stream) {
|
||||
|
||||
// ElevenLabs stream buffer
|
||||
if (piece.audioChunk) {
|
||||
try {
|
||||
// create the live audio player as needed
|
||||
// NOTE: in the future we can have a centralized audio playing system
|
||||
if (!liveAudioPlayer)
|
||||
liveAudioPlayer = new AudioLivePlayer();
|
||||
|
||||
// enqueue a decoded audio chunk - this will throw on malformed base64 data
|
||||
const chunkArray = convert_Base64_To_UInt8Array(piece.audioChunk.base64, 'elevenLabsSpeakText (chunk)');
|
||||
liveAudioPlayer.enqueueChunk(chunkArray.buffer);
|
||||
playbackStarted = true;
|
||||
} catch (audioError) {
|
||||
console.error('ElevenLabs audio chunk error:', audioError);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
// ElevenLabs full audio buffer
|
||||
else if (piece.audio) {
|
||||
try {
|
||||
// return base64 for potential reuse
|
||||
if (!audioStreaming)
|
||||
audioBase64 = piece.audio.base64;
|
||||
|
||||
// also consider merging LiveAudioPlayer into AudioPlayer - note this will throw on malformed base64 data
|
||||
const audioArray = convert_Base64_To_UInt8Array(piece.audio.base64, 'elevenLabsSpeakText');
|
||||
void AudioPlayer.playBuffer(audioArray.buffer); // fire/forget - it's a single piece of audio (could be long tho)
|
||||
playbackStarted = true;
|
||||
} catch (audioError) {
|
||||
console.error('ElevenLabs audio buffer error:', audioError);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
|
||||
// Errors
|
||||
else if (piece.errorMessage) {
|
||||
console.error('ElevenLabs error:', piece.errorMessage);
|
||||
return { success: false };
|
||||
} else if (piece.warningMessage) {
|
||||
console.warn('ElevenLabs warning:', piece.warningMessage);
|
||||
// Continue processing warnings
|
||||
} else if (piece.control === 'start' || piece.control === 'end') {
|
||||
// Control messages - continue processing
|
||||
} else {
|
||||
console.log('ElevenLabs unknown piece:', piece);
|
||||
}
|
||||
}
|
||||
return { success: playbackStarted, audioBase64 };
|
||||
} catch (error) {
|
||||
console.error('ElevenLabs playback error:', error);
|
||||
return { success: false };
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/trpc/trpc.server';
|
||||
import { env } from '~/server/env.server';
|
||||
import { fetchJsonOrTRPCThrow, fetchResponseOrTRPCThrow } from '~/server/trpc/trpc.router.fetchers';
|
||||
|
||||
|
||||
// configuration
|
||||
const SAFETY_TEXT_LENGTH = 1000;
|
||||
const MIN_CHUNK_SIZE = 4096; // Minimum chunk size in bytes
|
||||
|
||||
|
||||
// Schema definitions
|
||||
export type SpeechInputSchema = z.infer<typeof speechInputSchema>;
|
||||
export const speechInputSchema = z.object({
|
||||
xiKey: z.string().optional(),
|
||||
voiceId: z.string().optional(),
|
||||
text: z.string(),
|
||||
nonEnglish: z.boolean(),
|
||||
audioStreaming: z.boolean(),
|
||||
audioTurbo: z.boolean(),
|
||||
});
|
||||
|
||||
export type VoiceSchema = z.infer<typeof voiceSchema>;
|
||||
const voiceSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string().nullable(),
|
||||
previewUrl: z.string().nullable(),
|
||||
category: z.string(),
|
||||
default: z.boolean(),
|
||||
});
|
||||
|
||||
|
||||
export const elevenlabsRouter = createTRPCRouter({
|
||||
|
||||
/**
|
||||
* List Voices available to this API key
|
||||
*/
|
||||
listVoices: publicProcedure
|
||||
.input(z.object({
|
||||
elevenKey: z.string().optional(),
|
||||
}))
|
||||
.output(z.object({
|
||||
voices: z.array(voiceSchema),
|
||||
}))
|
||||
.query(async ({ input }) => {
|
||||
|
||||
const { elevenKey } = input;
|
||||
const { headers, url } = elevenlabsAccess(elevenKey, '/v1/voices');
|
||||
|
||||
const voicesList = await fetchJsonOrTRPCThrow<ElevenlabsWire.VoicesList>({
|
||||
url,
|
||||
headers,
|
||||
name: 'ElevenLabs',
|
||||
});
|
||||
|
||||
// bring category != 'premade' to the top
|
||||
voicesList.voices.sort((a, b) => {
|
||||
if (a.category === 'premade' && b.category !== 'premade') return 1;
|
||||
if (a.category !== 'premade' && b.category === 'premade') return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return {
|
||||
voices: voicesList.voices.map((voice, idx) => ({
|
||||
id: voice.voice_id,
|
||||
name: voice.name,
|
||||
description: voice.description,
|
||||
previewUrl: voice.preview_url,
|
||||
category: voice.category,
|
||||
default: idx === 0,
|
||||
})),
|
||||
};
|
||||
|
||||
}),
|
||||
|
||||
/**
|
||||
* Speech synthesis procedure using tRPC streaming
|
||||
*/
|
||||
speech: publicProcedure
|
||||
.input(speechInputSchema)
|
||||
.mutation(async function* ({ input: { xiKey, text, voiceId, nonEnglish, audioStreaming, audioTurbo }, ctx }) {
|
||||
|
||||
// start streaming back
|
||||
yield { control: 'start' };
|
||||
|
||||
// Safety check: trim text that's too long
|
||||
if (text.length > SAFETY_TEXT_LENGTH) {
|
||||
text = text.slice(0, SAFETY_TEXT_LENGTH);
|
||||
yield { warningMessage: 'text was truncated to maximum length' };
|
||||
}
|
||||
|
||||
let response: Response;
|
||||
try {
|
||||
|
||||
// Prepare the upstream request
|
||||
const path = `/v1/text-to-speech/${elevenlabsVoiceId(voiceId)}${audioStreaming ? '/stream' : ''}`;
|
||||
const { headers, url } = elevenlabsAccess(xiKey, path);
|
||||
const body: ElevenlabsWire.TTSRequest = {
|
||||
text: text,
|
||||
model_id:
|
||||
audioTurbo ? 'eleven_turbo_v2_5'
|
||||
: nonEnglish ? 'eleven_multilingual_v2'
|
||||
: 'eleven_multilingual_v2', // even for english, use the latest multilingual model
|
||||
};
|
||||
|
||||
// Blocking fetch
|
||||
response = await fetchResponseOrTRPCThrow({ url, method: 'POST', headers, body, signal: ctx.reqSignal, name: 'ElevenLabs' });
|
||||
|
||||
} catch (error: any) {
|
||||
yield { errorMessage: `fetch issue: ${error.message || 'Unknown error'}` };
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse headers
|
||||
const responseHeaders = _safeParseTTSResponseHeaders(response.headers);
|
||||
|
||||
// If not streaming, return the entire audio
|
||||
if (!audioStreaming) {
|
||||
const audioArrayBuffer = await response.arrayBuffer();
|
||||
yield {
|
||||
audio: {
|
||||
base64: Buffer.from(audioArrayBuffer).toString('base64'),
|
||||
contentType: responseHeaders.contentType,
|
||||
characterCost: responseHeaders.characterCost,
|
||||
ttsLatencyMs: responseHeaders.ttsLatencyMs,
|
||||
},
|
||||
};
|
||||
yield { control: 'end' };
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) {
|
||||
yield { errorMessage: 'stream issue: No reader' };
|
||||
return;
|
||||
}
|
||||
|
||||
// STREAM the audio chunks back to the client
|
||||
try {
|
||||
|
||||
// Initialize a buffer to accumulate chunks
|
||||
const accumulatedChunks: Uint8Array[] = [];
|
||||
let accumulatedSize = 0;
|
||||
|
||||
// Read loop
|
||||
while (true) {
|
||||
const { value, done: readerDone } = await reader.read();
|
||||
if (readerDone) break;
|
||||
if (!value) continue;
|
||||
|
||||
// Accumulate chunks
|
||||
accumulatedChunks.push(value);
|
||||
accumulatedSize += value.length;
|
||||
|
||||
// When accumulated size reaches or exceeds MIN_CHUNK_SIZE, yield the chunk
|
||||
if (accumulatedSize >= MIN_CHUNK_SIZE) {
|
||||
yield {
|
||||
audioChunk: {
|
||||
base64: Buffer.concat(accumulatedChunks).toString('base64'),
|
||||
},
|
||||
};
|
||||
// Reset the accumulation
|
||||
accumulatedChunks.length = 0;
|
||||
accumulatedSize = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// If there's any remaining data, yield it as well
|
||||
if (accumulatedSize) {
|
||||
yield {
|
||||
audioChunk: {
|
||||
base64: Buffer.concat(accumulatedChunks).toString('base64'),
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
yield { errorMessage: `stream issue: ${error.message || 'Unknown error'}` };
|
||||
return;
|
||||
}
|
||||
|
||||
// end streaming (if a control error wasn't thrown)
|
||||
yield { control: 'end' };
|
||||
}),
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* Helper function to construct ElevenLabs API access details
|
||||
*/
|
||||
export function elevenlabsAccess(elevenKey: string | undefined, apiPath: string): { headers: HeadersInit; url: string } {
|
||||
// API key
|
||||
elevenKey = (elevenKey || env.ELEVENLABS_API_KEY || '').trim();
|
||||
if (!elevenKey)
|
||||
throw new Error('Missing ElevenLabs API key.');
|
||||
|
||||
// API host
|
||||
let host = (env.ELEVENLABS_API_HOST || 'api.elevenlabs.io').trim();
|
||||
if (!host.startsWith('http'))
|
||||
host = `https://${host}`;
|
||||
if (host.endsWith('/') && apiPath.startsWith('/'))
|
||||
host = host.slice(0, -1);
|
||||
|
||||
return {
|
||||
headers: {
|
||||
'Accept': 'audio/mpeg',
|
||||
'Content-Type': 'application/json',
|
||||
'xi-api-key': elevenKey,
|
||||
},
|
||||
url: host + apiPath,
|
||||
};
|
||||
}
|
||||
|
||||
export function elevenlabsVoiceId(voiceId?: string): string {
|
||||
return voiceId?.trim() || env.ELEVENLABS_VOICE_ID || '21m00Tcm4TlvDq8ikWAM';
|
||||
}
|
||||
|
||||
|
||||
function _safeParseTTSResponseHeaders(headers: Headers): ElevenlabsWire.TTSResponseHeaders {
|
||||
return {
|
||||
contentType: headers.get('content-type') || 'audio/mpeg',
|
||||
characterCost: parseInt(headers.get('character-cost') || '0'),
|
||||
currentConcurrentRequests: parseInt(headers.get('current-concurrent-requests') || '0'),
|
||||
maximumConcurrentRequests: parseInt(headers.get('maximum-concurrent-requests') || '0'),
|
||||
ttsLatencyMs: parseInt(headers.get('tts-latency-ms') || '0'),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/// This is the upstream API [rev-eng on 2023-04-12]
|
||||
export namespace ElevenlabsWire {
|
||||
export interface TTSRequest {
|
||||
text: string;
|
||||
model_id?:
|
||||
| 'eleven_monolingual_v1'
|
||||
| 'eleven_multilingual_v1'
|
||||
| 'eleven_multilingual_v2'
|
||||
| 'eleven_turbo_v2'
|
||||
| 'eleven_turbo_v2_5';
|
||||
voice_settings?: {
|
||||
stability: number;
|
||||
similarity_boost: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface TTSResponseHeaders {
|
||||
// Response metadata
|
||||
contentType: string; // Should be 'audio/mpeg'
|
||||
|
||||
// Cost and usage metrics
|
||||
characterCost: number; // Cost in characters for this generation
|
||||
currentConcurrentRequests: number; // Current number of concurrent requests
|
||||
maximumConcurrentRequests: number; // Maximum allowed concurrent requests
|
||||
ttsLatencyMs?: number; // Time taken to generate speech (not in streaming mode)
|
||||
}
|
||||
|
||||
export interface VoicesList {
|
||||
voices: Voice[];
|
||||
}
|
||||
|
||||
interface Voice {
|
||||
voice_id: string;
|
||||
name: string;
|
||||
//samples: Sample[];
|
||||
category: string;
|
||||
// fine_tuning: FineTuning;
|
||||
labels: Record<string, string>;
|
||||
description: string;
|
||||
preview_url: string;
|
||||
// available_for_tiers: string[];
|
||||
settings: {
|
||||
stability: number;
|
||||
similarity_boost: number;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
|
||||
interface ModuleElevenlabsStore {
|
||||
|
||||
// ElevenLabs Text to Speech settings
|
||||
|
||||
elevenLabsApiKey: string;
|
||||
setElevenLabsApiKey: (apiKey: string) => void;
|
||||
|
||||
elevenLabsVoiceId: string;
|
||||
setElevenLabsVoiceId: (voiceId: string) => void;
|
||||
|
||||
}
|
||||
|
||||
const useElevenlabsStore = create<ModuleElevenlabsStore>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
|
||||
// ElevenLabs Text to Speech settings
|
||||
|
||||
elevenLabsApiKey: '',
|
||||
setElevenLabsApiKey: (elevenLabsApiKey: string) => set({ elevenLabsApiKey }),
|
||||
|
||||
elevenLabsVoiceId: '',
|
||||
setElevenLabsVoiceId: (elevenLabsVoiceId: string) => set({ elevenLabsVoiceId }),
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'app-module-elevenlabs',
|
||||
}),
|
||||
);
|
||||
|
||||
export const useElevenLabsApiKey = (): [string, (apiKey: string) => void] => {
|
||||
const apiKey = useElevenlabsStore(state => state.elevenLabsApiKey);
|
||||
return [apiKey, useElevenlabsStore.getState().setElevenLabsApiKey];
|
||||
};
|
||||
|
||||
export const useElevenLabsVoiceId = (): [string, (voiceId: string) => void] => {
|
||||
const voiceId = useElevenlabsStore(state => state.elevenLabsVoiceId);
|
||||
return [voiceId, useElevenlabsStore.getState().setElevenLabsVoiceId];
|
||||
};
|
||||
|
||||
export const useElevenLabsData = (): [string, string] =>
|
||||
useElevenlabsStore(useShallow(state => [state.elevenLabsApiKey, state.elevenLabsVoiceId]));
|
||||
|
||||
export const getElevenLabsData = (): { elevenLabsApiKey: string, elevenLabsVoiceId: string } =>
|
||||
useElevenlabsStore.getState();
|
||||
@@ -1,102 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { CircularProgress, Option, Select } from '@mui/joy';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
|
||||
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
|
||||
import { apiQuery } from '~/common/util/trpc.client';
|
||||
|
||||
import { VoiceSchema } from './elevenlabs.router';
|
||||
import { isElevenLabsEnabled } from './elevenlabs.client';
|
||||
import { useElevenLabsApiKey, useElevenLabsVoiceId } from './store-module-elevenlabs';
|
||||
|
||||
|
||||
function VoicesDropdown(props: {
|
||||
isValidKey: boolean,
|
||||
isFetchingVoices: boolean,
|
||||
isErrorVoices: boolean,
|
||||
disabled?: boolean,
|
||||
voices: VoiceSchema[],
|
||||
voiceId: string | null,
|
||||
setVoiceId: (voiceId: string) => void,
|
||||
}) {
|
||||
|
||||
const handleVoiceChange = (_event: any, value: string | null) => props.setVoiceId(value || '');
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={props.voiceId} onChange={handleVoiceChange}
|
||||
variant='outlined' disabled={props.disabled || !props.voices.length}
|
||||
// color={props.isErrorVoices ? 'danger' : undefined}
|
||||
placeholder={props.isErrorVoices ? 'Issue loading voices' : props.isValidKey ? 'Select a voice' : 'Missing API Key'}
|
||||
startDecorator={<RecordVoiceOverTwoToneIcon />}
|
||||
endDecorator={props.isValidKey && props.isFetchingVoices && <CircularProgress size='sm' />}
|
||||
indicator={<KeyboardArrowDownIcon />}
|
||||
slotProps={{
|
||||
root: { sx: { width: '100%' } },
|
||||
indicator: { sx: { opacity: 0.5 } },
|
||||
}}
|
||||
>
|
||||
{props.voices.map(voice => (
|
||||
<Option key={voice.id} value={voice.id}>
|
||||
{voice.name}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function useElevenLabsVoices() {
|
||||
const [apiKey] = useElevenLabsApiKey();
|
||||
|
||||
const isConfigured = isElevenLabsEnabled(apiKey);
|
||||
|
||||
const { data, isError, isFetching, isPending } = apiQuery.elevenlabs.listVoices.useQuery({ elevenKey: apiKey }, {
|
||||
enabled: isConfigured,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
});
|
||||
|
||||
return {
|
||||
isConfigured,
|
||||
isError,
|
||||
isFetching,
|
||||
hasVoices: !isPending && !!data?.voices.length,
|
||||
voices: data?.voices || [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
export function useElevenLabsVoiceDropdown(autoSpeak: boolean, disabled?: boolean) {
|
||||
|
||||
// external state
|
||||
const { isConfigured, isError, isFetching, hasVoices, voices } = useElevenLabsVoices();
|
||||
const [voiceId, setVoiceId] = useElevenLabsVoiceId();
|
||||
|
||||
// derived state
|
||||
const voice: VoiceSchema | undefined = voices.find(voice => voice.id === voiceId);
|
||||
|
||||
// [E] autoSpeak
|
||||
const previewUrl = (autoSpeak && voice?.previewUrl) || null;
|
||||
React.useEffect(() => {
|
||||
if (previewUrl)
|
||||
void AudioPlayer.playUrl(previewUrl);
|
||||
}, [previewUrl]);
|
||||
|
||||
const voicesDropdown = React.useMemo(() =>
|
||||
<VoicesDropdown
|
||||
isValidKey={isConfigured} isFetchingVoices={isFetching} isErrorVoices={isError} disabled={disabled}
|
||||
voices={voices}
|
||||
voiceId={voiceId} setVoiceId={setVoiceId}
|
||||
/>,
|
||||
[disabled, isConfigured, isError, isFetching, setVoiceId, voiceId, voices],
|
||||
);
|
||||
|
||||
return {
|
||||
hasVoices,
|
||||
voiceId,
|
||||
voiceName: voice?.name,
|
||||
voicesDropdown,
|
||||
};
|
||||
}
|
||||
@@ -32,6 +32,18 @@ const _reasoningEffort4Options = [
|
||||
{ value: 'minimal', label: 'Minimal', description: 'Fastest, cheapest, least reasoning' } as const,
|
||||
{ value: _UNSPECIFIED, label: 'Default', description: 'Default value (unset)' } as const,
|
||||
] as const;
|
||||
const _reasoningEffort52Options = [
|
||||
{ value: 'xhigh', label: 'Max', description: 'Hardest thinking, best quality' } as const,
|
||||
{ value: 'high', label: 'High', description: 'Deep, thorough analysis' } as const,
|
||||
{ value: 'medium', label: 'Medium', description: 'Balanced reasoning depth' } as const,
|
||||
{ value: 'low', label: 'Low', description: 'Quick, concise responses' } as const,
|
||||
{ value: _UNSPECIFIED, label: 'None', description: '-' } as const,
|
||||
] as const;
|
||||
const _reasoningEffort52ProOptions = [
|
||||
{ value: 'xhigh', label: 'Max', description: 'Hardest thinking, best quality' } as const,
|
||||
{ value: 'high', label: 'High', description: 'Deep, thorough analysis' } as const,
|
||||
{ value: _UNSPECIFIED, label: 'Medium', description: '-' } as const,
|
||||
] as const;
|
||||
const _verbosityOptions = [
|
||||
{ value: 'high', label: 'Detailed', description: 'Thorough responses, great for audits' } as const,
|
||||
{ value: 'medium', label: 'Balanced', description: 'Standard detail level (default)' } as const,
|
||||
@@ -209,6 +221,8 @@ export function LLMParametersEditor(props: {
|
||||
// llmVndMoonshotWebSearch,
|
||||
llmVndOaiReasoningEffort,
|
||||
llmVndOaiReasoningEffort4,
|
||||
llmVndOaiReasoningEffort52,
|
||||
llmVndOaiReasoningEffort52Pro,
|
||||
llmVndOaiRestoreMarkdown,
|
||||
llmVndOaiWebSearchContext,
|
||||
llmVndOaiWebSearchGeolocation,
|
||||
@@ -257,8 +271,8 @@ export function LLMParametersEditor(props: {
|
||||
const gemTBSpec = modelParamSpec['llmVndGeminiThinkingBudget'];
|
||||
const gemTBMinMax = gemTBSpec?.rangeOverride || defGemTB.range;
|
||||
|
||||
// Check if web search should be disabled due to minimal reasoning effort
|
||||
const isOaiReasoningEffortMinimal = llmVndOaiReasoningEffort4 === 'minimal';
|
||||
// Check if web search should be disabled due to minimal/none reasoning effort
|
||||
const isOaiReasoningEffortMinimal = llmVndOaiReasoningEffort4 === 'minimal' || llmVndOaiReasoningEffort52 === 'none';
|
||||
|
||||
return <>
|
||||
|
||||
@@ -617,6 +631,34 @@ export function LLMParametersEditor(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{showParam('llmVndOaiReasoningEffort52') && (
|
||||
<FormSelectControl
|
||||
title='Reasoning Effort'
|
||||
tooltip='Controls how much effort the model spends on reasoning (5-level scale for GPT-5.2)'
|
||||
value={(!llmVndOaiReasoningEffort52 || llmVndOaiReasoningEffort52 === 'none') ? _UNSPECIFIED : llmVndOaiReasoningEffort52}
|
||||
onChange={(value) => {
|
||||
if (value === _UNSPECIFIED || !value)
|
||||
onRemoveParameter('llmVndOaiReasoningEffort52');
|
||||
else
|
||||
onChangeParameter({ llmVndOaiReasoningEffort52: value });
|
||||
}}
|
||||
options={_reasoningEffort52Options}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showParam('llmVndOaiReasoningEffort52Pro') && (
|
||||
<FormSelectControl
|
||||
title='Reasoning Effort'
|
||||
tooltip='Controls how much effort the model spends on reasoning (3-level scale for GPT-5.2 Pro)'
|
||||
value={(!llmVndOaiReasoningEffort52Pro || llmVndOaiReasoningEffort52Pro === 'medium') ? _UNSPECIFIED : llmVndOaiReasoningEffort52Pro}
|
||||
onChange={(value) => {
|
||||
if (value === _UNSPECIFIED || !value) onRemoveParameter('llmVndOaiReasoningEffort52Pro');
|
||||
else onChangeParameter({ llmVndOaiReasoningEffort52Pro: value });
|
||||
}}
|
||||
options={_reasoningEffort52ProOptions}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showParam('llmVndOaiVerbosity') && (
|
||||
<FormSelectControl
|
||||
title='Verbosity'
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, Button, Checkbox, Divider } from '@mui/joy';
|
||||
import { Box, Button, Checkbox, Divider, Typography } from '@mui/joy';
|
||||
|
||||
import type { DModelsService } from '~/common/stores/llms/llms.service.types';
|
||||
import { AppBreadcrumbs } from '~/common/components/AppBreadcrumbs';
|
||||
@@ -10,7 +10,8 @@ import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { optimaActions } from '~/common/layout/optima/useOptima';
|
||||
import { useHasLLMs } from '~/common/stores/llms/llms.hooks';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
import { useModelsZeroState } from '~/common/stores/llms/hooks/useModelsZeroState';
|
||||
import { useUICounter, useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
|
||||
import { LLMVendorSetup } from '../components/LLMVendorSetup';
|
||||
import { ModelsList } from './ModelsList';
|
||||
@@ -39,6 +40,7 @@ export function ModelsConfiguratorModal(props: {
|
||||
// state
|
||||
// const [showAllServices, setShowAllServices] = React.useState<boolean>(false);
|
||||
const [tab, setTab] = React.useState<TabValue>(MODELS_WIZARD_ENABLE_INITIALLY && !modelsServices.length ? 'wizard' : 'setup');
|
||||
const [unsavedWizardProviders, setUnsavedWizardProviders] = React.useState<Set<string>>(new Set());
|
||||
const showAllServices = false;
|
||||
|
||||
// external state
|
||||
@@ -82,6 +84,17 @@ export function ModelsConfiguratorModal(props: {
|
||||
const handleShowWizard = React.useCallback(() => setTab('wizard'), []);
|
||||
// const handleToggleDefaults = React.useCallback(() => setTab(tab => tab === 'defaults' ? 'setup' : 'defaults'), []);
|
||||
|
||||
// callback for wizard to report unsaved provider changes
|
||||
const handleWizardProviderUnsavedChange = React.useCallback((providerId: string, hasUnsaved: boolean) => {
|
||||
setUnsavedWizardProviders(prev => {
|
||||
const next = new Set(prev);
|
||||
if (hasUnsaved) next.add(providerId);
|
||||
else next.delete(providerId);
|
||||
// only update if actually changed
|
||||
return next.size !== prev.size || (hasUnsaved && !prev.has(providerId)) ? next : prev;
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
// start button
|
||||
const startButton = React.useMemo(() => {
|
||||
@@ -111,11 +124,118 @@ export function ModelsConfiguratorModal(props: {
|
||||
}, [handleShowAdvanced, handleShowWizard, hasAnyServices, hasLLMs, isMobile, isTabSetup, isTabWizard, setShowModelsHidden, showModelsHidden]);
|
||||
|
||||
|
||||
// custom done button for wizard mode (combines start and close buttons)
|
||||
|
||||
const wizardButtons = React.useMemo(() => {
|
||||
if (!isTabWizard) return undefined;
|
||||
|
||||
const hasUnsavedChanges = unsavedWizardProviders.size > 0;
|
||||
// const tooltipTitle = !hasLLMs ? 'Please save at least one API key to continue'
|
||||
// : hasUnsavedChanges ? 'You have unsaved changes - click Save first'
|
||||
// : '';
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', width: '100%', gap: 1, justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
{startButton}
|
||||
|
||||
{/* unsaved warning */}
|
||||
{hasUnsavedChanges && (
|
||||
<Typography color='warning' level='body-sm' ml='auto'>
|
||||
{isMobile ? 'Unsaved' : `You have ${unsavedWizardProviders.size} unsaved change${ unsavedWizardProviders.size > 1 ? 's' : '' }`}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* "Done" button */}
|
||||
<Button
|
||||
variant='solid'
|
||||
color='neutral'
|
||||
disabled={!hasLLMs || hasUnsavedChanges}
|
||||
onClick={optimaActions().closeModels}
|
||||
sx={{ ml: 'auto', minWidth: 100 }}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</Box>
|
||||
);
|
||||
}, [hasLLMs, unsavedWizardProviders, isMobile, isTabWizard, startButton]);
|
||||
|
||||
|
||||
// Explainer section
|
||||
const isMissingModels = useModelsZeroState();
|
||||
const { novel: isFirstVisit, touch: markVisited } = useUICounter('models-setup-first-visit', 1);
|
||||
const [showExplainer, setShowExplainer] = React.useState(isMissingModels && isFirstVisit); // show the explainer only if we don't have models and it's the first visit
|
||||
|
||||
const handleShowExplainerAgain = React.useCallback(() => {
|
||||
setShowExplainer(true);
|
||||
}, []);
|
||||
|
||||
const handleDismissExplainer = React.useCallback((event: React.BaseSyntheticEvent, reason: 'backdropClick' | 'escapeKeyDown' | 'closeClick') => {
|
||||
// hide for both the 'x' button and close
|
||||
setShowExplainer(false);
|
||||
|
||||
// mark as visited on close only
|
||||
if (reason === 'closeClick')
|
||||
markVisited();
|
||||
}, [markVisited]);
|
||||
|
||||
if (showExplainer) {
|
||||
return (
|
||||
<GoodModal
|
||||
title={
|
||||
<AppBreadcrumbs size='md' rootTitle='Welcome'>
|
||||
<AppBreadcrumbs.Leaf>Notice on linking AI services</AppBreadcrumbs.Leaf>
|
||||
{/*<AppBreadcrumbs.Leaf>Important <b>AI Models</b> Notice</AppBreadcrumbs.Leaf>*/}
|
||||
</AppBreadcrumbs>
|
||||
}
|
||||
open
|
||||
onClose={handleDismissExplainer}
|
||||
disableBackdropClose
|
||||
animateEnter
|
||||
unfilterBackdrop
|
||||
sx={{ maxWidth: '28rem' }}
|
||||
// closeText='Got It'
|
||||
closeText='I understand'
|
||||
>
|
||||
<Box sx={{
|
||||
py: 3,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// justifyContent: 'center',
|
||||
gap: 3,
|
||||
// textAlign: 'center',
|
||||
maxWidth: '500px',
|
||||
minHeight: '14rem',
|
||||
m: 'auto',
|
||||
}}>
|
||||
{/*<Typography level='title-md' mb={1}>*/}
|
||||
{/* Bring your own AI Keys*/}
|
||||
{/*</Typography>*/}
|
||||
<Typography level='body-md' lineHeight='lg'>
|
||||
You'll need to <strong>provide your API credentials</strong> to use AI services.
|
||||
</Typography>
|
||||
<Typography level='body-sm' textColor='text.secondary' lineHeight='lg'>
|
||||
Big-AGI connects directly to the latest AI models using your API keys.{' '}
|
||||
{/*Big-AGI is a local App running on your computer.{' '}*/}
|
||||
{/*We want you to have access to the top models. */}
|
||||
We don't limit or bill your usage, giving you full control,
|
||||
privacy, freedom of choice and unparalleled speed.
|
||||
</Typography>
|
||||
<Typography level='body-sm' textColor='text.secondary' lineHeight='lg'>
|
||||
You get the cleanest AI experience.
|
||||
{/*You want the cleanest AI experience possible.*/}
|
||||
</Typography>
|
||||
</Box>
|
||||
</GoodModal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<GoodModal
|
||||
title={isTabWizard ? (
|
||||
<AppBreadcrumbs size='md' rootTitle='Welcome'>
|
||||
<AppBreadcrumbs.Leaf>Setup <b>AI Models</b></AppBreadcrumbs.Leaf>
|
||||
<AppBreadcrumbs.Leaf><b>Setup AI Models</b></AppBreadcrumbs.Leaf>
|
||||
{/*<AppBreadcrumbs.Leaf>Setup <b>AI Models</b></AppBreadcrumbs.Leaf>*/}
|
||||
</AppBreadcrumbs>
|
||||
) : (
|
||||
// <>Configure <b>AI Models</b></>
|
||||
@@ -135,16 +255,25 @@ export function ModelsConfiguratorModal(props: {
|
||||
)}
|
||||
open onClose={optimaActions().closeModels}
|
||||
darkBottomClose={!isTabWizard}
|
||||
hideBottomClose={isTabWizard}
|
||||
startButton={isTabWizard ? wizardButtons : startButton}
|
||||
closeText={isTabWizard ? 'Done' : undefined}
|
||||
animateEnter={!hasLLMs}
|
||||
unfilterBackdrop
|
||||
startButton={startButton}
|
||||
autoOverflow={true /* forces some shrinkage of the contents (ModelsList) */}
|
||||
fullscreen={isMobile ? 'button' : undefined} // NOTE: was disabled because on mobile there's one screen with a stretch issue - but can't reproduce
|
||||
>
|
||||
|
||||
{isTabWizard && <Divider />}
|
||||
{isTabWizard && <ModelsWizard isMobile={isMobile} onSkip={optimaActions().closeModels} onSwitchToAdvanced={handleShowAdvanced} />}
|
||||
{isTabWizard && (
|
||||
<ModelsWizard
|
||||
isMobile={isMobile}
|
||||
onSkip={optimaActions().closeModels}
|
||||
onSwitchToAdvanced={handleShowAdvanced}
|
||||
onSwitchToWhy={handleShowExplainerAgain}
|
||||
onProviderUnsavedChange={handleWizardProviderUnsavedChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isTabSetup && <ModelsServiceSelector modelsServices={modelsServices} selectedServiceId={activeServiceId} setSelectedServiceId={setConfServiceId} onSwitchToWizard={handleShowWizard} />}
|
||||
{isTabSetup && <Divider sx={activeService ? undefined : { visibility: 'hidden' }} />}
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ModelVendorLMStudio } from '../vendors/lmstudio/lmstudio.vendor';
|
||||
import { ModelVendorLocalAI } from '../vendors/localai/localai.vendor';
|
||||
import { ModelVendorOllama } from '../vendors/ollama/ollama.vendor';
|
||||
import { ModelVendorOpenAI } from '../vendors/openai/openai.vendor';
|
||||
import { ModelVendorOpenRouter } from '../vendors/openrouter/openrouter.vendor';
|
||||
import { llmsUpdateModelsForServiceOrThrow } from '../llm.client';
|
||||
|
||||
|
||||
@@ -24,10 +25,10 @@ const WizardProviders: ReadonlyArray<WizardProvider> = [
|
||||
{ cat: 'popular', vendor: ModelVendorOpenAI, settingsKey: 'oaiKey' } as const,
|
||||
{ cat: 'popular', vendor: ModelVendorAnthropic, settingsKey: 'anthropicKey' } as const,
|
||||
{ cat: 'popular', vendor: ModelVendorGemini, settingsKey: 'geminiKey' } as const,
|
||||
{ cat: 'popular', vendor: ModelVendorOpenRouter, settingsKey: 'oaiKey' } as const,
|
||||
{ cat: 'local', vendor: ModelVendorLocalAI, settingsKey: 'localAIHost' } as const,
|
||||
{ cat: 'local', vendor: ModelVendorOllama, settingsKey: 'ollamaHost' } as const,
|
||||
{ cat: 'local', vendor: ModelVendorLMStudio, settingsKey: 'oaiHost', omit: true } as const,
|
||||
// { vendor: ModelVendorOpenRouter, settingsKey: 'oaiKey' } as const,
|
||||
] as const;
|
||||
|
||||
type VendorCategory = 'popular' | 'local';
|
||||
@@ -89,6 +90,7 @@ function WizardProviderSetup(props: {
|
||||
provider: WizardProvider,
|
||||
isFirst: boolean,
|
||||
isHidden: boolean,
|
||||
onUnsavedChange: (providerId: string, hasUnsaved: boolean) => void,
|
||||
}) {
|
||||
|
||||
const { cat: providerCat, vendor: providerVendor, settingsKey: providerSettingsKey, omit: providerOmit } = props.provider;
|
||||
@@ -133,6 +135,22 @@ function WizardProviderSetup(props: {
|
||||
const autoCompleteId = isLocal ? `${providerVendor.id}-host` : `${providerVendor.id}-key`;
|
||||
|
||||
|
||||
// wrapped setter that notifies parent of unsaved state
|
||||
|
||||
const { onUnsavedChange } = props;
|
||||
|
||||
const handleLocalValueChange = React.useCallback((newValue: string) => {
|
||||
// set locally
|
||||
setLocalValue(newValue);
|
||||
|
||||
// notify parent of unsaved state
|
||||
if (providerOmit || !onUnsavedChange) return;
|
||||
const hasUnsaved = newValue !== (serviceKeyValue || '');
|
||||
const hasValue = !!newValue.trim();
|
||||
onUnsavedChange(providerVendor.id, hasUnsaved && hasValue);
|
||||
}, [onUnsavedChange, providerOmit, providerVendor.id, serviceKeyValue]);
|
||||
|
||||
|
||||
// handlers
|
||||
|
||||
|
||||
@@ -148,6 +166,10 @@ function WizardProviderSetup(props: {
|
||||
const newKey = localValue?.trim() ?? '';
|
||||
updateServiceSettings(vendorServiceId, { [providerSettingsKey]: newKey });
|
||||
|
||||
// notify parent that changes are now saved
|
||||
if (!providerOmit)
|
||||
onUnsavedChange(providerVendor.id, false);
|
||||
|
||||
// if the key is empty, remove the models
|
||||
if (!newKey) {
|
||||
setUpdateError(null);
|
||||
@@ -169,7 +191,7 @@ function WizardProviderSetup(props: {
|
||||
}
|
||||
setIsLoading(false);
|
||||
|
||||
}, [localValue, providerSettingsKey, providerVendor, valueName]);
|
||||
}, [localValue, onUnsavedChange, providerOmit, providerSettingsKey, providerVendor, valueName]);
|
||||
|
||||
|
||||
// memoed components
|
||||
@@ -231,7 +253,7 @@ function WizardProviderSetup(props: {
|
||||
autoCompleteId={autoCompleteId}
|
||||
value={localValue ?? ''}
|
||||
placeholder={`${vendorName} ${valueName}`}
|
||||
onChange={setLocalValue}
|
||||
onChange={handleLocalValueChange}
|
||||
required={false}
|
||||
/>
|
||||
</Box>
|
||||
@@ -260,6 +282,8 @@ export function ModelsWizard(props: {
|
||||
isMobile: boolean,
|
||||
onSkip?: () => void,
|
||||
onSwitchToAdvanced?: () => void,
|
||||
onSwitchToWhy?: () => void,
|
||||
onProviderUnsavedChange: (providerId: string, hasUnsaved: boolean) => void,
|
||||
}) {
|
||||
|
||||
// state
|
||||
@@ -280,7 +304,7 @@ export function ModelsWizard(props: {
|
||||
<Chip variant={isLocal ? 'solid' : 'outlined'} sx={{ mx: 0.25 }} onClick={() => setActiveCategory('local')}>
|
||||
Local
|
||||
</Chip>
|
||||
{' '}AI services below.
|
||||
{' '}<Box component='a' onClick={props.onSwitchToWhy} sx={{ color: 'text.tertiary', cursor: 'pointer' }}>AI services </Box> below.
|
||||
</Typography>
|
||||
{/*<Box sx={{ fontSize: 'sm', color: 'text.primary' }}>*/}
|
||||
{/* Enter API keys to connect your AI services.{' '}*/}
|
||||
@@ -294,6 +318,7 @@ export function ModelsWizard(props: {
|
||||
provider={provider}
|
||||
isFirst={!index}
|
||||
isHidden={provider.cat !== activeCategory}
|
||||
onUnsavedChange={props.onProviderUnsavedChange}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
@@ -35,6 +35,14 @@ export const geminiAccessSchema = z.object({
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Build Gemini API access parameters for generateContent and other model APIs.
|
||||
*
|
||||
* @param access Gemini access configuration
|
||||
* @param modelRefId Model ID to use in the path (e.g., 'models/gemini-pro')
|
||||
* @param apiPath API path template (e.g., '/v1beta/{model=models/*}:generateContent')
|
||||
* @param useV1Alpha Whether to use v1alpha API version (for experimental features)
|
||||
*/
|
||||
export function geminiAccess(access: GeminiAccessSchema, modelRefId: string | null, apiPath: string, useV1Alpha: boolean): { headers: HeadersInit, url: string } {
|
||||
|
||||
const geminiHost = llmsFixupHost(access.geminiHost || DEFAULT_GEMINI_HOST, apiPath);
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { GeminiWire_API_Models_List } from '~/modules/aix/server/dispatch/w
|
||||
|
||||
import type { ModelDescriptionSchema } from '../llm.server.types';
|
||||
|
||||
import { LLM_IF_GEM_CodeExecution, LLM_IF_HOTFIX_NoStream, LLM_IF_HOTFIX_NoTemperature, LLM_IF_HOTFIX_StripImages, LLM_IF_HOTFIX_StripSys0, LLM_IF_HOTFIX_Sys0ToUsr0, LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Json, LLM_IF_OAI_PromptCaching, LLM_IF_OAI_Reasoning, LLM_IF_OAI_Vision, LLM_IF_Outputs_Audio, LLM_IF_Outputs_Image, LLM_IF_Outputs_NoText } from '~/common/stores/llms/llms.types';
|
||||
import { LLM_IF_GEM_CodeExecution, LLM_IF_GEM_Interactions, LLM_IF_HOTFIX_NoStream, LLM_IF_HOTFIX_NoTemperature, LLM_IF_HOTFIX_StripImages, LLM_IF_HOTFIX_StripSys0, LLM_IF_HOTFIX_Sys0ToUsr0, LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Json, LLM_IF_OAI_PromptCaching, LLM_IF_OAI_Reasoning, LLM_IF_OAI_Vision, LLM_IF_Outputs_Audio, LLM_IF_Outputs_Image, LLM_IF_Outputs_NoText } from '~/common/stores/llms/llms.types';
|
||||
import { Release } from '~/common/app.release';
|
||||
|
||||
|
||||
@@ -198,6 +198,23 @@ const _knownGeminiModels: ({
|
||||
benchmark: undefined, // Non-benchmarkable because generates images
|
||||
},
|
||||
|
||||
/// Agents (Interactions API)
|
||||
|
||||
// Deep Research Agent - Available via Interactions API
|
||||
// https://ai.google.dev/gemini-api/docs/deep-research
|
||||
{
|
||||
id: 'agents/deep-research-pro-preview-12-2025',
|
||||
labelOverride: 'Deep Research Pro Preview',
|
||||
isPreview: true,
|
||||
chatPrice: gemini25ProPricing, // Uses similar pricing to 2.5 Pro
|
||||
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, LLM_IF_OAI_Reasoning, LLM_IF_GEM_Interactions],
|
||||
parameterSpecs: [
|
||||
{ paramId: 'llmVndGeminiInteractionsAgent' }, // Enables Interactions API with agent name
|
||||
],
|
||||
benchmark: undefined, // Agent-based, not benchmarkable
|
||||
// Note: This model uses background=true by default for long-running research tasks
|
||||
},
|
||||
|
||||
/// Generation 2.5
|
||||
|
||||
// 2.5 Pro (Stable) - Released June 17, 2025
|
||||
@@ -710,6 +727,10 @@ const _sortOderIdPrefix: string[] = [
|
||||
'models/gemini-3-pro',
|
||||
'models/gemini-3-',
|
||||
|
||||
// Agents (Interactions API)
|
||||
'agents/deep-research-pro-preview',
|
||||
'agents/',
|
||||
|
||||
'models/gemini-exp',
|
||||
|
||||
'models/gemini-2.5-pro',
|
||||
|
||||
@@ -29,7 +29,7 @@ import { openAIAccess } from './openai/openai.access';
|
||||
import { alibabaModelFilter, alibabaModelSort, alibabaModelToModelDescription } from './openai/models/alibaba.models';
|
||||
import { azureDeploymentFilter, azureDeploymentToModelDescription, azureParseFromDeploymentsAPI } from './openai/models/azure.models';
|
||||
import { chutesAIHeuristic, chutesAIModelsToModelDescriptions } from './openai/models/chutesai.models';
|
||||
import { deepseekModelFilter, deepseekModelSort, deepseekModelToModelDescription } from './openai/models/deepseek.models';
|
||||
import { deepseekInjectVariants, deepseekModelFilter, deepseekModelSort, deepseekModelToModelDescription } from './openai/models/deepseek.models';
|
||||
import { fastAPIHeuristic, fastAPIModels } from './openai/models/fastapi.models';
|
||||
import { fireworksAIHeuristic, fireworksAIModelsToModelDescriptions } from './openai/models/fireworksai.models';
|
||||
import { groqModelFilter, groqModelSortFn, groqModelToModelDescription } from './openai/models/groq.models';
|
||||
@@ -345,9 +345,11 @@ function _listModelsCreateDispatch(access: AixAPI_Access, signal?: AbortSignal):
|
||||
.sort(openAISortModels);
|
||||
|
||||
case 'deepseek':
|
||||
// [DeepSeek, 2025-12-01] Inject V3.2-Speciale variant via reduce
|
||||
return maybeModels
|
||||
.filter(({ id }) => deepseekModelFilter(id))
|
||||
.map(({ id }) => deepseekModelToModelDescription(id))
|
||||
.reduce(deepseekInjectVariants, [] as ModelDescriptionSchema[])
|
||||
.sort(deepseekModelSort);
|
||||
|
||||
case 'groq':
|
||||
|
||||
@@ -94,12 +94,15 @@ const ModelParameterSpec_schema = z.object({
|
||||
'llmVndGeminiShowThoughts',
|
||||
'llmVndGeminiThinkingBudget',
|
||||
'llmVndGeminiThinkingLevel',
|
||||
'llmVndGeminiInteractionsAgent',
|
||||
// 'llmVndGeminiUrlContext',
|
||||
// Moonshot
|
||||
'llmVndMoonshotWebSearch',
|
||||
// OpenAI
|
||||
'llmVndOaiReasoningEffort',
|
||||
'llmVndOaiReasoningEffort4',
|
||||
'llmVndOaiReasoningEffort52',
|
||||
'llmVndOaiReasoningEffort52Pro',
|
||||
'llmVndOaiRestoreMarkdown',
|
||||
'llmVndOaiVerbosity',
|
||||
'llmVndOaiWebSearchContext',
|
||||
|
||||
@@ -1,30 +1,32 @@
|
||||
import { LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Json, LLM_IF_OAI_Reasoning } from '~/common/stores/llms/llms.types';
|
||||
import { LLM_IF_HOTFIX_StripImages, LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Json, LLM_IF_OAI_Reasoning } from '~/common/stores/llms/llms.types';
|
||||
|
||||
import type { ModelDescriptionSchema } from '../../llm.server.types';
|
||||
|
||||
import { fromManualMapping, ManualMappings } from '../../models.mappings';
|
||||
|
||||
|
||||
const IF_3 = [LLM_IF_HOTFIX_StripImages, LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Json];
|
||||
|
||||
const _knownDeepseekChatModels: ManualMappings = [
|
||||
// [Models and Pricing](https://api-docs.deepseek.com/quick_start/pricing)
|
||||
// [List Models](https://api-docs.deepseek.com/api/list-models)
|
||||
// [Release Notes - V3.2-Exp](https://api-docs.deepseek.com/news/news250929) - Released 2025-09-29
|
||||
// [Release Notes - V3.2](https://api-docs.deepseek.com/news/news251201) - Released 2025-12-01
|
||||
{
|
||||
idPrefix: 'deepseek-reasoner',
|
||||
label: 'DeepSeek V3.2-Exp (Reasoner)',
|
||||
label: 'DeepSeek V3.2 (Reasoner)',
|
||||
description: 'Reasoning model with Chain-of-Thought capabilities, 128K context length. Supports JSON output and function calling.',
|
||||
contextWindow: 131072, // 128K
|
||||
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Json, LLM_IF_OAI_Reasoning],
|
||||
interfaces: [...IF_3, LLM_IF_OAI_Reasoning],
|
||||
maxCompletionTokens: 32768, // default, max: 65536
|
||||
chatPrice: { input: 0.28, output: 0.42, cache: { cType: 'oai-ac', read: 0.028 } },
|
||||
benchmark: { cbaElo: 1418 }, // deepseek-r1-0528
|
||||
},
|
||||
{
|
||||
idPrefix: 'deepseek-chat',
|
||||
label: 'DeepSeek V3.2-Exp',
|
||||
label: 'DeepSeek V3.2',
|
||||
description: 'General-purpose model with 128K context length. Supports JSON output and function calling.',
|
||||
contextWindow: 131072, // 128K
|
||||
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_Fn, LLM_IF_OAI_Json],
|
||||
interfaces: IF_3,
|
||||
maxCompletionTokens: 8192, // default is 4096, max is 8192
|
||||
chatPrice: { input: 0.28, output: 0.42, cache: { cType: 'oai-ac', read: 0.028 } },
|
||||
benchmark: { cbaElo: 1419 }, // deepseek-v3.1-thinking
|
||||
@@ -59,3 +61,28 @@ export function deepseekModelSort(a: ModelDescriptionSchema, b: ModelDescription
|
||||
return aIndex - bIndex;
|
||||
return a.id.localeCompare(b.id);
|
||||
}
|
||||
|
||||
|
||||
// [DeepSeek, 2025-12-01] V3.2-Speciale: Temporary endpoint until Dec 15, 2025 15:59 UTC
|
||||
// Thinking mode only (deepseek-reasoner), 128K max output, no JSON/tool calling
|
||||
export const DEEPSEEK_SPECIALE_HOST = 'https://api.deepseek.com/v3.2_speciale_expires_on_20251215';
|
||||
export const DEEPSEEK_SPECIALE_SUFFIX = '@speciale';
|
||||
|
||||
const _hardcodedDeepseekVariants: { [modelId: string]: Partial<ModelDescriptionSchema> } = {
|
||||
'deepseek-reasoner': {
|
||||
id: 'deepseek-reasoner' + DEEPSEEK_SPECIALE_SUFFIX, // [DeepSeek, 2025-12-01] marker for dispatch routing (no idVariant - the @speciale suffix serves as both)
|
||||
label: 'DeepSeek V3.2 Speciale',
|
||||
description: 'V3.2-Speciale reasoning model. 128K max output, no JSON/tool calling. Expires Dec 15, 2025.',
|
||||
interfaces: [LLM_IF_HOTFIX_StripImages, LLM_IF_OAI_Chat, LLM_IF_OAI_Reasoning], // NO Fn, NO Json
|
||||
// contextWindow: null,
|
||||
// maxCompletionTokens: undefined, // default 64K, max 128K (higher than regular reasoner's 32K default)
|
||||
},
|
||||
};
|
||||
|
||||
export function deepseekInjectVariants(models: ModelDescriptionSchema[], model: ModelDescriptionSchema): ModelDescriptionSchema[] {
|
||||
// [DeepSeek, 2025-12-01] Inject Speciale variant for deepseek-reasoner
|
||||
if (_hardcodedDeepseekVariants[model.id])
|
||||
models.push({ ...model, ..._hardcodedDeepseekVariants[model.id] });
|
||||
models.push(model);
|
||||
return models;
|
||||
}
|
||||
|
||||
@@ -11,112 +11,116 @@ const MISTRAL_DEV_SHOW_GAPS = Release.IsNodeDevBuild;
|
||||
|
||||
|
||||
// [Mistral]
|
||||
// Updated 2025-10-28
|
||||
// Updated 2025-12-09
|
||||
// - models on: https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
// - pricing on: https://mistral.ai/pricing#api-pricing
|
||||
// - benchmark elo on CBA
|
||||
|
||||
const _knownMistralModelDetails: Record<string, {
|
||||
label?: string; // override the API-provided name
|
||||
chatPrice?: { input: number; output: number };
|
||||
benchmark?: { cbaElo: number };
|
||||
hidden?: boolean;
|
||||
}> = {
|
||||
|
||||
// Premier models
|
||||
'mistral-medium-2508': { chatPrice: { input: 0.4, output: 2 } }, // mistral-medium-3 (Aug 2025)
|
||||
// Premier models - Mistral 3 (Dec 2025)
|
||||
'mistral-large-2512': { chatPrice: { input: 0.5, output: 1.5 } }, // Mistral Large 3 - MoE 41B active / 675B total
|
||||
'mistral-large-2411': { chatPrice: { input: 2, output: 6 }, benchmark: { cbaElo: 1305 }, hidden: true }, // older version
|
||||
'mistral-large-latest': { chatPrice: { input: 0.5, output: 1.5 }, hidden: true }, // → 2512
|
||||
|
||||
'mistral-medium-2508': { chatPrice: { input: 0.4, output: 2 } }, // Mistral Medium 3
|
||||
'mistral-medium-2505': { chatPrice: { input: 0.4, output: 2 }, benchmark: { cbaElo: 1383 }, hidden: true }, // older version
|
||||
'mistral-medium-latest': { chatPrice: { input: 0.4, output: 2 }, hidden: true }, // → 2508
|
||||
'mistral-medium': { chatPrice: { input: 0.4, output: 2 }, benchmark: { cbaElo: 1165 }, hidden: true }, // old symlink
|
||||
'mistral-medium': { chatPrice: { input: 0.4, output: 2 }, hidden: true }, // symlink
|
||||
|
||||
'magistral-medium-2509': { chatPrice: { input: 2, output: 5 } }, // v25.09
|
||||
'magistral-medium-2506': { chatPrice: { input: 2, output: 5 }, hidden: true }, // older version
|
||||
'magistral-medium-2509': { chatPrice: { input: 2, output: 5 } }, // reasoning
|
||||
'magistral-medium-latest': { chatPrice: { input: 2, output: 5 }, hidden: true }, // symlink
|
||||
|
||||
'devstral-medium-2507': { chatPrice: { input: 0.4, output: 2 } }, // v25.07
|
||||
'devstral-2512': { label: 'Devstral 2 (2512)', chatPrice: { input: 0.4, output: 2 } }, // Devstral 2 - 123B coding agents (API returns "Mistral Vibe Cli")
|
||||
'devstral-latest': { label: 'Devstral 2 (latest)', chatPrice: { input: 0.4, output: 2 }, hidden: true }, // symlink
|
||||
'mistral-vibe-cli-latest': { label: 'Devstral 2 (latest)', chatPrice: { input: 0.4, output: 2 }, hidden: true }, // alternate ID for devstral-latest
|
||||
'devstral-medium-2507': { chatPrice: { input: 0.4, output: 2 }, hidden: true }, // older version
|
||||
|
||||
'mistral-large-2411': { chatPrice: { input: 2, output: 6 }, benchmark: { cbaElo: 1305 } }, // mistral-large-2411
|
||||
'mistral-large-2407': { chatPrice: { input: 2, output: 6 }, benchmark: { cbaElo: 1314 }, hidden: true }, // older version
|
||||
'mistral-large-latest': { chatPrice: { input: 2, output: 6 }, benchmark: { cbaElo: 1305 }, hidden: true }, // symlink
|
||||
|
||||
'pixtral-large-2411': { chatPrice: { input: 2, output: 6 } },
|
||||
'mistral-large-pixtral-2411': { chatPrice: { input: 2, output: 6 } }, // Pixtral Large (alternate ID)
|
||||
'pixtral-large-2411': { chatPrice: { input: 2, output: 6 }, hidden: true }, // symlink
|
||||
'pixtral-large-latest': { chatPrice: { input: 2, output: 6 }, hidden: true }, // symlink
|
||||
|
||||
'codestral-2508': { chatPrice: { input: 0.3, output: 0.9 } }, // v25.08
|
||||
'codestral-2501': { chatPrice: { input: 0.3, output: 0.9 }, hidden: true }, // older version
|
||||
'codestral-2508': { chatPrice: { input: 0.3, output: 0.9 } }, // code generation
|
||||
'codestral-latest': { chatPrice: { input: 0.3, output: 0.9 }, hidden: true }, // symlink
|
||||
|
||||
'voxtral-small-2507': { chatPrice: { input: 0.1, output: 0.3 } }, // v25.07 (text tokens)
|
||||
'voxtral-small-2507': { chatPrice: { input: 0.1, output: 0.3 } }, // voice (text tokens)
|
||||
'voxtral-small-latest': { chatPrice: { input: 0.1, output: 0.3 }, hidden: true }, // symlink
|
||||
|
||||
'voxtral-mini-2507': { chatPrice: { input: 0.04, output: 0.04 } }, // v25.07 (text tokens)
|
||||
'voxtral-mini-2507': { chatPrice: { input: 0.04, output: 0.04 } }, // voice (text tokens)
|
||||
'voxtral-mini-latest': { chatPrice: { input: 0.04, output: 0.04 }, hidden: true }, // symlink
|
||||
|
||||
'ministral-8b-2410': { chatPrice: { input: 0.1, output: 0.1 }, benchmark: { cbaElo: 1240 } }, // ministral-8b-2410
|
||||
'ministral-8b-latest': { chatPrice: { input: 0.1, output: 0.1 }, benchmark: { cbaElo: 1240 }, hidden: true }, // symlink
|
||||
// Ministral 3 family (Dec 2025) - multimodal, multilingual, Apache 2.0
|
||||
'ministral-14b-2512': { chatPrice: { input: 0.2, output: 0.2 } }, // Ministral 3 14B
|
||||
'ministral-14b-latest': { chatPrice: { input: 0.2, output: 0.2 }, hidden: true }, // symlink
|
||||
|
||||
// Note: mistral-saba, ministral-3b, embed, and moderation models are filtered out (not chat models or not available via API)
|
||||
'ministral-8b-2512': { chatPrice: { input: 0.15, output: 0.15 } }, // Ministral 3 8B
|
||||
'ministral-8b-2410': { chatPrice: { input: 0.1, output: 0.1 }, benchmark: { cbaElo: 1240 }, hidden: true }, // older version
|
||||
'ministral-8b-latest': { chatPrice: { input: 0.15, output: 0.15 }, hidden: true }, // symlink
|
||||
|
||||
'ministral-3b-2512': { chatPrice: { input: 0.1, output: 0.1 } }, // Ministral 3 3B
|
||||
'ministral-3b-2410': { chatPrice: { input: 0.04, output: 0.04 }, hidden: true }, // older version
|
||||
'ministral-3b-latest': { chatPrice: { input: 0.1, output: 0.1 }, hidden: true }, // symlink
|
||||
|
||||
// Open models
|
||||
'mistral-small-2506': { chatPrice: { input: 0.1, output: 0.3 } },
|
||||
'mistral-small-2503': { chatPrice: { input: 0.1, output: 0.3 }, benchmark: { cbaElo: 1298 }, hidden: true }, // older version
|
||||
'mistral-small-2501': { chatPrice: { input: 0.1, output: 0.3 }, benchmark: { cbaElo: 1235 }, hidden: true }, // older version
|
||||
'mistral-small-2409': { chatPrice: { input: 0.1, output: 0.3 }, hidden: true }, // older version
|
||||
'mistral-small-2506': { chatPrice: { input: 0.1, output: 0.3 } }, // Mistral Small 3.2
|
||||
'mistral-small-latest': { chatPrice: { input: 0.1, output: 0.3 }, hidden: true }, // symlink
|
||||
'mistral-small': { chatPrice: { input: 0.1, output: 0.3 }, hidden: true }, // symlink
|
||||
|
||||
'magistral-small-2509': { chatPrice: { input: 0.5, output: 1.5 } }, // v25.09
|
||||
'magistral-small-2506': { chatPrice: { input: 0.5, output: 1.5 }, hidden: true }, // older version
|
||||
'magistral-small-2509': { chatPrice: { input: 0.5, output: 1.5 } }, // reasoning
|
||||
'magistral-small-latest': { chatPrice: { input: 0.5, output: 1.5 }, hidden: true }, // symlink
|
||||
|
||||
'devstral-small-2507': { chatPrice: { input: 0.1, output: 0.3 } }, // v25.07
|
||||
'devstral-small-2505': { chatPrice: { input: 0.1, output: 0.3 }, hidden: true }, // older version
|
||||
'devstral-small-latest': { chatPrice: { input: 0.1, output: 0.3 }, hidden: true }, // symlink
|
||||
'devstral-small-2512': { label: 'Devstral Small 2 (2512)', chatPrice: { input: 0.1, output: 0.3 } }, // Devstral Small 2 - 24B coding agents
|
||||
'devstral-small-2507': { chatPrice: { input: 0.1, output: 0.3 }, hidden: true }, // older version
|
||||
'devstral-small-latest': { label: 'Devstral Small 2 (latest)', chatPrice: { input: 0.1, output: 0.3 }, hidden: true }, // symlink
|
||||
|
||||
'pixtral-12b-2409': { chatPrice: { input: 0.15, output: 0.15 } },
|
||||
'pixtral-12b-2409': { chatPrice: { input: 0.15, output: 0.15 } }, // vision
|
||||
'pixtral-12b-latest': { chatPrice: { input: 0.15, output: 0.15 }, hidden: true }, // symlink
|
||||
'pixtral-12b': { chatPrice: { input: 0.15, output: 0.15 }, hidden: true }, // symlink
|
||||
|
||||
'open-mistral-nemo-2407': { chatPrice: { input: 0.15, output: 0.15 } },
|
||||
'open-mistral-nemo-2407': { chatPrice: { input: 0.15, output: 0.15 } }, // NeMo
|
||||
'open-mistral-nemo': { chatPrice: { input: 0.15, output: 0.15 }, hidden: true }, // symlink
|
||||
|
||||
// Legacy models
|
||||
'open-mixtral-8x22b-2404': { chatPrice: { input: 2, output: 6 }, benchmark: { cbaElo: 1165 }, hidden: true }, // legacy
|
||||
'open-mixtral-8x22b': { chatPrice: { input: 2, output: 6 }, benchmark: { cbaElo: 1165 }, hidden: true }, // legacy symlink
|
||||
'open-mixtral-8x7b': { chatPrice: { input: 0.7, output: 0.7 }, benchmark: { cbaElo: 1131 }, hidden: true }, // legacy
|
||||
'open-mistral-7b': { chatPrice: { input: 0.25, output: 0.25 }, hidden: true }, // legacy
|
||||
// Legacy (kept for reference, no longer in API)
|
||||
'open-mistral-7b': { chatPrice: { input: 0.25, output: 0.25 }, hidden: true },
|
||||
};
|
||||
|
||||
|
||||
const mistralModelFamilyOrder = [
|
||||
// Mistral 3 (Dec 2025)
|
||||
'mistral-large-2512', // Mistral Large 3 - specific prefix must come before generic 'mistral-large'
|
||||
'ministral-14b',
|
||||
'ministral-8b',
|
||||
'ministral-3b',
|
||||
// Premier
|
||||
'magistral-medium',
|
||||
'mistral-medium',
|
||||
'devstral-2512', // Devstral 2 - must come before generic 'devstral'
|
||||
'mistral-vibe-cli', // alternate ID for Devstral 2
|
||||
'devstral-medium',
|
||||
'mistral-large',
|
||||
'mistral-large-pixtral', // Pixtral Large uses 'mistral-large-pixtral-2411' ID - must come before 'mistral-large'
|
||||
'pixtral-large',
|
||||
'mistral-large', // Generic fallback for other mistral-large variants
|
||||
'codestral',
|
||||
'magistral-small',
|
||||
'mistral-small',
|
||||
'devstral-small-2512', // Devstral Small 2 - must come before generic 'devstral-small'
|
||||
'devstral-small',
|
||||
'voxtral-small',
|
||||
'voxtral-mini',
|
||||
'mistral-embed',
|
||||
'mistral-ocr',
|
||||
'ministral-8b',
|
||||
'ministral-3b',
|
||||
'codestral-embed',
|
||||
'mistral-moderation',
|
||||
// Open
|
||||
'open-codestral-mamba',
|
||||
'pixtral-12b',
|
||||
'open-mistral-nemo',
|
||||
// Legacy
|
||||
'open-mixtral-8x22b',
|
||||
'open-mixtral-8x7b',
|
||||
'mistral-small-2312', // note: this is set here explicitly, because otherwise it would show up earlier in the list due to its real name being the open mixtral 8x7b
|
||||
// Legacy (no longer in API, kept for fallback)
|
||||
'open-mistral-7b',
|
||||
// Open
|
||||
'mistral-saba',
|
||||
// Deprecated
|
||||
'mistral-tiny',
|
||||
// Symlinks at the bottom
|
||||
@@ -160,6 +164,8 @@ function _mistralCapabilitiesToInterfaces(capabilities: WireMistralModel['capabi
|
||||
interfaces.push(LLM_IF_OAI_Fn);
|
||||
if (!capabilities || capabilities.vision)
|
||||
interfaces.push(LLM_IF_OAI_Vision);
|
||||
// if (!capabilities || capabilities.audio)
|
||||
// interfaces.push(...audio input...); // Voxtral
|
||||
// Add reasoning interface for magistral models
|
||||
if (modelId.includes('magistral'))
|
||||
interfaces.push(LLM_IF_OAI_Reasoning);
|
||||
@@ -212,10 +218,11 @@ export function mistralModels(wireModels: unknown): ModelDescriptionSchema[] {
|
||||
const prettyName = _prettyMistralName(name);
|
||||
|
||||
const extraDetails = _knownMistralModelDetails[id] || {};
|
||||
const labelOverride = extraDetails.label;
|
||||
|
||||
return {
|
||||
id: id,
|
||||
label: !isSymlink ? prettyName : `🔗 ${id} → ${prettyName}`,
|
||||
label: labelOverride ?? (!isSymlink ? prettyName : `🔗 ${id} → ${prettyName}`),
|
||||
created: created || 0,
|
||||
updated: /*updated ||*/ created || 0,
|
||||
description: description,
|
||||
@@ -279,11 +286,14 @@ const wireMistralModelSchema = z.object({
|
||||
|
||||
capabilities: z.object({
|
||||
completion_chat: z.boolean(), // used to remove other models
|
||||
completion_fim: z.boolean().nullish(),
|
||||
function_calling: z.boolean().nullish(),
|
||||
completion_fim: z.boolean().nullish(),
|
||||
fine_tuning: z.boolean().nullish(),
|
||||
vision: z.boolean().nullish(),
|
||||
ocr: z.boolean().nullish(),
|
||||
classification: z.boolean().nullish(),
|
||||
moderation: z.boolean().nullish(),
|
||||
audio: z.boolean().nullish(),
|
||||
}).nullish(),
|
||||
|
||||
// UI description fields
|
||||
|
||||
@@ -51,6 +51,75 @@ const PS_DEEP_RESEARCH = [{ paramId: 'llmVndOaiWebSearchContext' as const, initi
|
||||
// - "Structured Outputs" is LLM_IF_OAI_Json
|
||||
export const _knownOpenAIChatModels: ManualMappings = [
|
||||
|
||||
/// GPT-5.2 series - Released December 11, 2025
|
||||
|
||||
// GPT-5.2
|
||||
{
|
||||
idPrefix: 'gpt-5.2-2025-12-11',
|
||||
label: 'GPT-5.2 (2025-12-11)',
|
||||
description: 'Most capable model for professional work and long-running agents. Improvements in general intelligence, long-context, agentic tool-calling, and vision.',
|
||||
contextWindow: 400000,
|
||||
maxCompletionTokens: 128000,
|
||||
trainingDataCutoff: 'Aug 2025',
|
||||
interfaces: [LLM_IF_OAI_Responses, ...IFS_CHAT_CACHE_REASON, LLM_IF_Tools_WebSearch, LLM_IF_HOTFIX_NoTemperature],
|
||||
parameterSpecs: [
|
||||
{ paramId: 'llmVndOaiReasoningEffort52' },
|
||||
{ paramId: 'llmVndOaiWebSearchContext' },
|
||||
{ paramId: 'llmVndOaiRestoreMarkdown' },
|
||||
{ paramId: 'llmVndOaiVerbosity' },
|
||||
{ paramId: 'llmVndOaiImageGeneration' },
|
||||
{ paramId: 'llmForceNoStream' },
|
||||
],
|
||||
chatPrice: { input: 1.75, cache: { cType: 'oai-ac', read: 0.175 }, output: 14 },
|
||||
// benchmark: TBD
|
||||
},
|
||||
{
|
||||
idPrefix: 'gpt-5.2',
|
||||
label: 'GPT-5.2',
|
||||
symLink: 'gpt-5.2-2025-12-11',
|
||||
},
|
||||
|
||||
// GPT-5.2 Chat Latest
|
||||
{
|
||||
idPrefix: 'gpt-5.2-chat-latest',
|
||||
label: 'GPT-5.2 Instant',
|
||||
description: 'GPT-5.2 model powering ChatGPT. Fast, capable for everyday work with clear improvements in info-seeking, how-tos, technical writing.',
|
||||
contextWindow: 400000,
|
||||
maxCompletionTokens: 128000,
|
||||
trainingDataCutoff: 'Aug 2025',
|
||||
interfaces: [LLM_IF_OAI_Responses, ...IFS_CHAT_CACHE, LLM_IF_Tools_WebSearch, LLM_IF_HOTFIX_NoTemperature],
|
||||
parameterSpecs: [
|
||||
{ paramId: 'llmVndOaiWebSearchContext' },
|
||||
{ paramId: 'llmVndOaiImageGeneration' },
|
||||
],
|
||||
chatPrice: { input: 1.75, cache: { cType: 'oai-ac', read: 0.175 }, output: 14 },
|
||||
// benchmark: TBD
|
||||
},
|
||||
|
||||
// GPT-5.2 Pro
|
||||
{
|
||||
idPrefix: 'gpt-5.2-pro-2025-12-11',
|
||||
label: 'GPT-5.2 Pro (2025-12-11)',
|
||||
description: 'Smartest and most trustworthy option for difficult questions. Uses more compute for harder thinking on complex domains like programming.',
|
||||
contextWindow: 400000,
|
||||
maxCompletionTokens: 272000,
|
||||
trainingDataCutoff: 'Aug 2025',
|
||||
interfaces: [LLM_IF_OAI_Responses, ...IFS_CHAT_MIN, LLM_IF_OAI_Reasoning, LLM_IF_HOTFIX_NoTemperature],
|
||||
parameterSpecs: [
|
||||
{ paramId: 'llmVndOaiReasoningEffort52Pro' },
|
||||
{ paramId: 'llmVndOaiWebSearchContext' },
|
||||
{ paramId: 'llmForceNoStream' },
|
||||
],
|
||||
chatPrice: { input: 21, output: 168 },
|
||||
// benchmark: TBD
|
||||
},
|
||||
{
|
||||
idPrefix: 'gpt-5.2-pro',
|
||||
label: 'GPT-5.2 Pro',
|
||||
symLink: 'gpt-5.2-pro-2025-12-11',
|
||||
},
|
||||
|
||||
|
||||
/// GPT-5.1 series - Released November 13, 2025
|
||||
|
||||
// GPT-5.1
|
||||
@@ -99,6 +168,22 @@ export const _knownOpenAIChatModels: ManualMappings = [
|
||||
// benchmark: TBD
|
||||
},
|
||||
|
||||
// GPT-5.1 Codex Max
|
||||
{
|
||||
idPrefix: 'gpt-5.1-codex-max',
|
||||
label: 'GPT-5.1 Codex Max',
|
||||
description: 'Our most intelligent coding model optimized for long-horizon, agentic coding tasks.',
|
||||
contextWindow: 400000,
|
||||
maxCompletionTokens: 128000,
|
||||
trainingDataCutoff: 'Sep 30, 2024',
|
||||
interfaces: [LLM_IF_OAI_Responses, ...IFS_CHAT_CACHE_REASON, LLM_IF_HOTFIX_NoTemperature],
|
||||
parameterSpecs: [
|
||||
{ paramId: 'llmVndOaiReasoningEffort4' },
|
||||
{ paramId: 'llmForceNoStream' },
|
||||
],
|
||||
chatPrice: { input: 1.25, cache: { cType: 'oai-ac', read: 0.125 }, output: 10 },
|
||||
// benchmark: TBD
|
||||
},
|
||||
// GPT-5.1 Codex
|
||||
{
|
||||
idPrefix: 'gpt-5.1-codex',
|
||||
@@ -457,26 +542,6 @@ export const _knownOpenAIChatModels: ManualMappings = [
|
||||
},
|
||||
|
||||
|
||||
// o1-mini (deprecated - shutdown 2025-10-27)
|
||||
{
|
||||
hidden: true, // DEPRECATED - shutdown 2025-10-27
|
||||
idPrefix: 'o1-mini-2024-09-12',
|
||||
label: 'o1 Mini (2024-09-12)', // ⏱️
|
||||
description: 'DEPRECATED: Will be shut down on 2025-10-27. Fast, cost-efficient reasoning model tailored to coding, math, and science use cases.',
|
||||
contextWindow: 128000,
|
||||
maxCompletionTokens: 65536,
|
||||
trainingDataCutoff: 'Oct 2023',
|
||||
interfaces: [LLM_IF_OAI_Chat, LLM_IF_OAI_PromptCaching, LLM_IF_OAI_Reasoning, LLM_IF_HOTFIX_StripImages, LLM_IF_HOTFIX_Sys0ToUsr0],
|
||||
chatPrice: { input: 1.1, cache: { cType: 'oai-ac', read: 0.55 }, output: 4.4 },
|
||||
benchmark: { cbaElo: 1304 },
|
||||
isLegacy: true,
|
||||
},
|
||||
{
|
||||
idPrefix: 'o1-mini',
|
||||
label: 'o1 Mini',
|
||||
symLink: 'o1-mini-2024-09-12',
|
||||
},
|
||||
|
||||
/// GPT-4.1 series
|
||||
|
||||
// GPT-4.1
|
||||
@@ -673,19 +738,6 @@ export const _knownOpenAIChatModels: ManualMappings = [
|
||||
// benchmarks don't apply to audio models
|
||||
isPreview: true,
|
||||
},
|
||||
{
|
||||
hidden: true, // old
|
||||
idPrefix: 'gpt-4o-audio-preview-2024-10-01',
|
||||
label: 'GPT-4o Audio Preview (2024-10-01)',
|
||||
description: 'Snapshot for the Audio API model.',
|
||||
contextWindow: 128000,
|
||||
maxCompletionTokens: 16384,
|
||||
trainingDataCutoff: 'Oct 2023',
|
||||
interfaces: IFS_GPT_AUDIO,
|
||||
chatPrice: { input: 2.5, output: 10 /* AUDIO PRICING UNSUPPORTED 40/80 */ },
|
||||
// benchmarks don't apply to audio models
|
||||
isPreview: true,
|
||||
},
|
||||
{
|
||||
idPrefix: 'gpt-4o-audio-preview',
|
||||
label: 'GPT-4o Audio Preview',
|
||||
@@ -883,6 +935,7 @@ const openAIModelsDenyList: string[] = [
|
||||
'4o-realtime',
|
||||
'4o-mini-realtime',
|
||||
'gpt-realtime',
|
||||
'gpt-realtime-mini',
|
||||
|
||||
// [OpenAI, 2025-03-11] FIXME: NOT YET SUPPORTED - "RESPONSES API"
|
||||
'computer-use-preview', 'computer-use-preview-2025-03-11', // FIXME: support these
|
||||
@@ -933,9 +986,16 @@ export function openAIModelToModelDescription(modelId: string, modelCreated: num
|
||||
|
||||
|
||||
const _manualOrderingIdPrefixes = [
|
||||
// GPT-5.2
|
||||
'gpt-5.2-20',
|
||||
'gpt-5.2-pro-20',
|
||||
'gpt-5.2-pro',
|
||||
'gpt-5.2-chat-latest',
|
||||
'gpt-5.2',
|
||||
// GPT-5.1
|
||||
'gpt-5.1-20',
|
||||
'gpt-5.1-chat-latest',
|
||||
'gpt-5.1-codex-max',
|
||||
'gpt-5.1-codex',
|
||||
'gpt-5.1-codex-mini',
|
||||
'gpt-5.1',
|
||||
@@ -970,7 +1030,6 @@ const _manualOrderingIdPrefixes = [
|
||||
'o1-pro',
|
||||
'o1-20',
|
||||
'o1-preview-',
|
||||
'o1-mini-',
|
||||
'o1-',
|
||||
// GPT-4.5
|
||||
'gpt-4.5-20',
|
||||
|
||||
+1
-2
@@ -20,8 +20,7 @@ export interface IModelVendor<TServiceSettings extends Record<string, any> = {},
|
||||
readonly hasServerConfigKey?: keyof BackendCapabilities;
|
||||
|
||||
/// client-side-fetch ///
|
||||
readonly csfKey?: string; // was keyof TServiceSettings, but caused TS troubles
|
||||
readonly csfAvailable?: (setup?: Partial<TServiceSettings>) => boolean; // undefined: not even, false: conditions not met
|
||||
readonly csfAvailable?: (setup?: Partial<TServiceSettings>) => boolean; // undefined: not supported, false: conditions not met
|
||||
|
||||
/// abstraction interface ///
|
||||
|
||||
|
||||
+11
-2
@@ -9,6 +9,7 @@ import { FormInputKey } from '~/common/components/forms/FormInputKey';
|
||||
import { FormTextField } from '~/common/components/forms/FormTextField';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { SetupFormClientSideToggle } from '~/common/components/forms/SetupFormClientSideToggle';
|
||||
import { SetupFormRefetchButton } from '~/common/components/forms/SetupFormRefetchButton';
|
||||
import { useToggleableBoolean } from '~/common/util/hooks/useToggleableBoolean';
|
||||
|
||||
@@ -36,10 +37,11 @@ export function AlibabaServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
} = useServiceSetup(props.serviceId, ModelVendorAlibaba);
|
||||
|
||||
// derived state
|
||||
const { oaiKey: alibabaOaiKey, oaiHost: alibabaOaiHost } = serviceAccess;
|
||||
const { clientSideFetch, oaiKey: alibabaOaiKey, oaiHost: alibabaOaiHost } = serviceAccess;
|
||||
const needsUserKey = !serviceHasCloudTenantConfig;
|
||||
const shallFetchSucceed = !needsUserKey || (!!alibabaOaiKey && serviceSetupValid);
|
||||
const showKeyError = !!alibabaOaiKey && !serviceSetupValid;
|
||||
const showAdvanced = advanced.on || !!clientSideFetch;
|
||||
|
||||
// fetch models
|
||||
const { isFetching, refetch, isError, error } =
|
||||
@@ -73,7 +75,7 @@ export function AlibabaServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
{/* See the <ExternalLink href={ALIBABA_REG_LINK}>Alibaba Cloud Model Studio</ExternalLink> for more information.*/}
|
||||
{/*</Typography>*/}
|
||||
|
||||
{advanced.on && <FormTextField
|
||||
{showAdvanced && <FormTextField
|
||||
autoCompleteId='alibaba-host'
|
||||
title='API Endpoint'
|
||||
tooltip={`The API endpoint for the Alibaba Cloud OpenAI service, to be used instead of the default endpoint.`}
|
||||
@@ -82,6 +84,13 @@ export function AlibabaServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
onChange={text => updateSettings({ alibabaOaiHost: text })}
|
||||
/>}
|
||||
|
||||
{showAdvanced && <SetupFormClientSideToggle
|
||||
visible={!!alibabaOaiKey}
|
||||
checked={!!clientSideFetch}
|
||||
onChange={on => updateSettings({ csf: on })}
|
||||
helpText='Connect directly to Alibaba Cloud API from your browser instead of through the server.'
|
||||
/>}
|
||||
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={/*!shallFetchSucceed ||*/ isFetching} loading={isFetching} error={isError} advanced={advanced} />
|
||||
|
||||
{isError && <InlineError error={error} />}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ModelVendorOpenAI } from '../openai/openai.vendor';
|
||||
interface DAlibabaServiceSettings {
|
||||
alibabaOaiKey: string;
|
||||
alibabaOaiHost: string;
|
||||
csf?: boolean;
|
||||
}
|
||||
|
||||
export const ModelVendorAlibaba: IModelVendor<DAlibabaServiceSettings, OpenAIAccessSchema> = {
|
||||
@@ -17,6 +18,9 @@ export const ModelVendorAlibaba: IModelVendor<DAlibabaServiceSettings, OpenAIAcc
|
||||
instanceLimit: 1,
|
||||
hasServerConfigKey: 'hasLlmAlibaba',
|
||||
|
||||
/// client-side-fetch ///
|
||||
csfAvailable: _csfAlibabaAvailable,
|
||||
|
||||
// functions
|
||||
initializeSetup: () => ({
|
||||
alibabaOaiKey: '',
|
||||
@@ -27,6 +31,7 @@ export const ModelVendorAlibaba: IModelVendor<DAlibabaServiceSettings, OpenAIAcc
|
||||
},
|
||||
getTransportAccess: (partialSetup) => ({
|
||||
dialect: 'alibaba',
|
||||
clientSideFetch: _csfAlibabaAvailable(partialSetup) && !!partialSetup?.csf,
|
||||
oaiKey: partialSetup?.alibabaOaiKey || '',
|
||||
oaiOrg: '',
|
||||
oaiHost: partialSetup?.alibabaOaiHost || '',
|
||||
@@ -37,3 +42,7 @@ export const ModelVendorAlibaba: IModelVendor<DAlibabaServiceSettings, OpenAIAcc
|
||||
// OpenAI transport ('alibaba' dialect in 'access')
|
||||
rpcUpdateModelsOrThrow: ModelVendorOpenAI.rpcUpdateModelsOrThrow,
|
||||
};
|
||||
|
||||
function _csfAlibabaAvailable(s?: Partial<DAlibabaServiceSettings>) {
|
||||
return !!s?.alibabaOaiKey;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useChatAutoAI } from '../../../../apps/chat/store-app-chat';
|
||||
|
||||
import type { DModelsServiceId } from '~/common/stores/llms/llms.service.types';
|
||||
import { AlreadySet } from '~/common/components/AlreadySet';
|
||||
import { ExternalLink } from '~/common/components/ExternalLink';
|
||||
import { FormInputKey } from '~/common/components/forms/FormInputKey';
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
|
||||
@@ -110,7 +109,7 @@ export function AnthropicServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
{showAdvanced && <SetupFormClientSideToggle
|
||||
visible={!!anthropicKey}
|
||||
checked={!!clientSideFetch}
|
||||
onChange={on => updateSettings({ anthropicCSF: on })}
|
||||
onChange={on => updateSettings({ csf: on })}
|
||||
helpText="Fetch models and make requests directly to Anthropic's API using your browser instead of through the server. Useful for bypassing server limitations or ensuring requests use your API key directly."
|
||||
/>}
|
||||
|
||||
|
||||
+2
-3
@@ -10,7 +10,7 @@ export const isValidAnthropicApiKey = (apiKey?: string) => !!apiKey && (apiKey.s
|
||||
interface DAnthropicServiceSettings {
|
||||
anthropicKey: string;
|
||||
anthropicHost: string;
|
||||
anthropicCSF?: boolean;
|
||||
csf?: boolean;
|
||||
heliconeKey: string;
|
||||
}
|
||||
|
||||
@@ -25,13 +25,12 @@ export const ModelVendorAnthropic: IModelVendor<DAnthropicServiceSettings, Anthr
|
||||
hasServerConfigKey: 'hasLlmAnthropic',
|
||||
|
||||
/// client-side-fetch ///
|
||||
csfKey: 'anthropicCSF',
|
||||
csfAvailable: _csfAnthropicAvailable,
|
||||
|
||||
// functions
|
||||
getTransportAccess: (partialSetup): AnthropicAccessSchema => ({
|
||||
dialect: 'anthropic',
|
||||
clientSideFetch: _csfAnthropicAvailable(partialSetup) && !!partialSetup?.anthropicCSF,
|
||||
clientSideFetch: _csfAnthropicAvailable(partialSetup) && !!partialSetup?.csf,
|
||||
anthropicKey: partialSetup?.anthropicKey || '',
|
||||
anthropicHost: partialSetup?.anthropicHost || null,
|
||||
heliconeKey: partialSetup?.heliconeKey || null,
|
||||
|
||||
+13
-2
@@ -9,8 +9,10 @@ import { FormInputKey } from '~/common/components/forms/FormInputKey';
|
||||
import { FormTextField } from '~/common/components/forms/FormTextField';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { SetupFormClientSideToggle } from '~/common/components/forms/SetupFormClientSideToggle';
|
||||
import { SetupFormRefetchButton } from '~/common/components/forms/SetupFormRefetchButton';
|
||||
import { asValidURL } from '~/common/util/urlUtils';
|
||||
import { useToggleableBoolean } from '~/common/util/hooks/useToggleableBoolean';
|
||||
|
||||
import { ApproximateCosts } from '../ApproximateCosts';
|
||||
import { useLlmUpdateModels } from '../../llm.client.hooks';
|
||||
@@ -22,6 +24,7 @@ import { isValidAzureApiKey, ModelVendorAzure } from './azure.vendor';
|
||||
export function AzureServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
|
||||
// state
|
||||
const advanced = useToggleableBoolean();
|
||||
const [checkboxExpanded, setCheckboxExpanded] = React.useState(false);
|
||||
|
||||
// external state
|
||||
@@ -29,8 +32,9 @@ export function AzureServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
useServiceSetup(props.serviceId, ModelVendorAzure);
|
||||
|
||||
// derived state
|
||||
const { oaiKey: azureKey, oaiHost: azureEndpoint } = serviceAccess;
|
||||
const { clientSideFetch, oaiKey: azureKey, oaiHost: azureEndpoint } = serviceAccess;
|
||||
const needsUserKey = !serviceHasCloudTenantConfig;
|
||||
const showAdvanced = advanced.on || !!clientSideFetch;
|
||||
|
||||
const keyValid = isValidAzureApiKey(azureKey);
|
||||
const keyError = (/*needsUserKey ||*/ !!azureKey) && !keyValid;
|
||||
@@ -81,7 +85,14 @@ export function AzureServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
placeholder='...'
|
||||
/>
|
||||
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} loading={isFetching} error={isError} />
|
||||
{showAdvanced && <SetupFormClientSideToggle
|
||||
visible={!!(azureKey && azureEndpoint)}
|
||||
checked={!!clientSideFetch}
|
||||
onChange={on => updateSettings({ csf: on })}
|
||||
helpText='Connect directly to Azure OpenAI API from your browser instead of through the server.'
|
||||
/>}
|
||||
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} loading={isFetching} error={isError} advanced={advanced} />
|
||||
|
||||
{isError && <InlineError error={error} />}
|
||||
|
||||
|
||||
+10
-1
@@ -10,6 +10,7 @@ export const isValidAzureApiKey = (apiKey?: string) => !!apiKey && apiKey.length
|
||||
interface DAzureServiceSettings {
|
||||
azureEndpoint: string;
|
||||
azureKey: string;
|
||||
csf?: boolean;
|
||||
}
|
||||
|
||||
/** Implementation Notes for the Azure Vendor
|
||||
@@ -37,9 +38,13 @@ export const ModelVendorAzure: IModelVendor<DAzureServiceSettings, OpenAIAccessS
|
||||
instanceLimit: 2,
|
||||
hasServerConfigKey: 'hasLlmAzureOpenAI',
|
||||
|
||||
/// client-side-fetch ///
|
||||
csfAvailable: _csfAzureAvailable,
|
||||
|
||||
// functions
|
||||
getTransportAccess: (partialSetup): OpenAIAccessSchema => ({
|
||||
dialect: 'azure',
|
||||
clientSideFetch: _csfAzureAvailable(partialSetup) && !!partialSetup?.csf,
|
||||
oaiKey: partialSetup?.azureKey || '',
|
||||
oaiOrg: '',
|
||||
oaiHost: partialSetup?.azureEndpoint || '',
|
||||
@@ -50,4 +55,8 @@ export const ModelVendorAzure: IModelVendor<DAzureServiceSettings, OpenAIAccessS
|
||||
// OpenAI transport ('azure' dialect in 'access')
|
||||
rpcUpdateModelsOrThrow: ModelVendorOpenAI.rpcUpdateModelsOrThrow,
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
function _csfAzureAvailable(s?: Partial<DAzureServiceSettings>) {
|
||||
return !!(s?.azureKey && s?.azureEndpoint);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { AlreadySet } from '~/common/components/AlreadySet';
|
||||
import { FormInputKey } from '~/common/components/forms/FormInputKey';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { SetupFormClientSideToggle } from '~/common/components/forms/SetupFormClientSideToggle';
|
||||
import { SetupFormRefetchButton } from '~/common/components/forms/SetupFormRefetchButton';
|
||||
import { useToggleableBoolean } from '~/common/util/hooks/useToggleableBoolean';
|
||||
|
||||
@@ -30,8 +31,9 @@ export function DeepseekAIServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
} = useServiceSetup(props.serviceId, ModelVendorDeepseek);
|
||||
|
||||
// derived state
|
||||
const { oaiKey: deepseekKey } = serviceAccess;
|
||||
const { clientSideFetch, oaiKey: deepseekKey } = serviceAccess;
|
||||
const needsUserKey = !serviceHasCloudTenantConfig;
|
||||
const showAdvanced = advanced.on || !!clientSideFetch;
|
||||
|
||||
// validate if url is a well formed proper url with zod
|
||||
const shallFetchSucceed = !needsUserKey || (!!deepseekKey && serviceSetupValid);
|
||||
@@ -57,6 +59,13 @@ export function DeepseekAIServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
placeholder='...'
|
||||
/>
|
||||
|
||||
{showAdvanced && <SetupFormClientSideToggle
|
||||
visible={!!deepseekKey}
|
||||
checked={!!clientSideFetch}
|
||||
onChange={on => updateSettings({ csf: on })}
|
||||
helpText='Connect directly to Deepseek API from your browser instead of through the server.'
|
||||
/>}
|
||||
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={/*!shallFetchSucceed ||*/ isFetching} loading={isFetching} error={isError} advanced={advanced} />
|
||||
|
||||
{isError && <InlineError error={error} />}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ModelVendorOpenAI } from '../openai/openai.vendor';
|
||||
|
||||
export interface DDeepseekServiceSettings {
|
||||
deepseekKey: string;
|
||||
csf?: boolean;
|
||||
}
|
||||
|
||||
export const ModelVendorDeepseek: IModelVendor<DDeepseekServiceSettings, OpenAIAccessSchema> = {
|
||||
@@ -17,6 +18,9 @@ export const ModelVendorDeepseek: IModelVendor<DDeepseekServiceSettings, OpenAIA
|
||||
instanceLimit: 1,
|
||||
hasServerConfigKey: 'hasLlmDeepseek',
|
||||
|
||||
/// client-side-fetch ///
|
||||
csfAvailable: _csfDeepseekAvailable,
|
||||
|
||||
// functions
|
||||
initializeSetup: () => ({
|
||||
deepseekKey: '',
|
||||
@@ -26,6 +30,7 @@ export const ModelVendorDeepseek: IModelVendor<DDeepseekServiceSettings, OpenAIA
|
||||
},
|
||||
getTransportAccess: (partialSetup) => ({
|
||||
dialect: 'deepseek',
|
||||
clientSideFetch: _csfDeepseekAvailable(partialSetup) && !!partialSetup?.csf,
|
||||
oaiKey: partialSetup?.deepseekKey || '',
|
||||
oaiOrg: '',
|
||||
oaiHost: '',
|
||||
@@ -37,3 +42,7 @@ export const ModelVendorDeepseek: IModelVendor<DDeepseekServiceSettings, OpenAIA
|
||||
rpcUpdateModelsOrThrow: ModelVendorOpenAI.rpcUpdateModelsOrThrow,
|
||||
|
||||
};
|
||||
|
||||
function _csfDeepseekAvailable(s?: Partial<DDeepseekServiceSettings>) {
|
||||
return !!s?.deepseekKey;
|
||||
}
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ export function GeminiServiceSetup(props: { serviceId: DModelsServiceId }) {
|
||||
{showAdvanced && <SetupFormClientSideToggle
|
||||
visible={!!geminiKey}
|
||||
checked={!!clientSideFetch}
|
||||
onChange={on => updateSettings({ geminiCSF: on })}
|
||||
onChange={on => updateSettings({ csf: on })}
|
||||
helpText="Fetch models and make requests directly to Google's Gemini API using your browser instead of through the server."
|
||||
/>}
|
||||
|
||||
|
||||
+2
-3
@@ -9,7 +9,7 @@ import type { IModelVendor } from '../IModelVendor';
|
||||
interface DGeminiServiceSettings {
|
||||
geminiKey: string;
|
||||
geminiHost: string;
|
||||
geminiCSF?: boolean;
|
||||
csf?: boolean;
|
||||
minSafetyLevel: GeminiWire_Safety.HarmBlockThreshold;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ export const ModelVendorGemini: IModelVendor<DGeminiServiceSettings, GeminiAcces
|
||||
hasServerConfigKey: 'hasLlmGemini',
|
||||
|
||||
/// client-side-fetch ///
|
||||
csfKey: 'geminiCSF',
|
||||
csfAvailable: _csfGeminiAvailable,
|
||||
|
||||
// functions
|
||||
@@ -48,7 +47,7 @@ export const ModelVendorGemini: IModelVendor<DGeminiServiceSettings, GeminiAcces
|
||||
},
|
||||
getTransportAccess: (partialSetup): GeminiAccessSchema => ({
|
||||
dialect: 'gemini',
|
||||
clientSideFetch: _csfGeminiAvailable(partialSetup) && !!partialSetup?.geminiCSF,
|
||||
clientSideFetch: _csfGeminiAvailable(partialSetup) && !!partialSetup?.csf,
|
||||
geminiKey: partialSetup?.geminiKey || '',
|
||||
geminiHost: partialSetup?.geminiHost || '',
|
||||
minSafetyLevel: partialSetup?.minSafetyLevel || 'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user