mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
256 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a4794ae034 | |||
| 99e2d5597a | |||
| 74321a44ca | |||
| 7b664affb7 | |||
| c411835f3b | |||
| 7b62c946a5 | |||
| 252e2fcd29 | |||
| aa2731bccc | |||
| 282c439963 | |||
| e99459aba0 | |||
| 4c35cbbe34 | |||
| cab3537ae2 | |||
| c3f211389b | |||
| a4de84a842 | |||
| 2bf1eaaa0f | |||
| 7f5ddd1629 | |||
| ed798fec65 | |||
| 90386f5794 | |||
| 8ada8811bf | |||
| b24badabef | |||
| 4e20cb12cd | |||
| 245da9e6cc | |||
| a800b34aa7 | |||
| 50c3941f42 | |||
| 6e5d5ee36c | |||
| 2c8b713ff3 | |||
| 8162a6706d | |||
| 952f6883fa | |||
| 373f3e3698 | |||
| 17791f631f | |||
| 6987c67cc7 | |||
| 65a59e5d2d | |||
| 05b9a6d412 | |||
| 6608f4f164 | |||
| 93378ad6b0 | |||
| bd4a60203e | |||
| c9e6a62641 | |||
| 68d797fa99 | |||
| 08011d8cf2 | |||
| 2f91bf7f52 | |||
| d5182c05c1 | |||
| 8e0947a833 | |||
| 1d88fc37b0 | |||
| 46bd8e6f4d | |||
| b95b427331 | |||
| 9b574c60eb | |||
| a8b39cc0a4 | |||
| cdbc7dd9b8 | |||
| 08dfec4fcf | |||
| 7f4553225b | |||
| f37e65a91e | |||
| c022f8a68c | |||
| daa7a506a5 | |||
| f3dcf39c15 | |||
| 06cbef16d4 | |||
| ab31bcd3e3 | |||
| 563a99864f | |||
| 39b8abc2c6 | |||
| f3dd837076 | |||
| d6b3a5259d | |||
| 9fea1d5c64 | |||
| 0adb5355c7 | |||
| 01d807b61e | |||
| 285bb812d0 | |||
| d897155d6e | |||
| 7154426279 | |||
| 4526084e4d | |||
| 0c5c786ae3 | |||
| 8a2c4aa356 | |||
| 4cba819edd | |||
| 4db42a2b29 | |||
| fc0ee5b698 | |||
| 2c0c3f1c70 | |||
| 3f3976b73c | |||
| 82d5dcced5 | |||
| f4eaed694a | |||
| 05d9869326 | |||
| 2675934ff8 | |||
| fb6e19d3ea | |||
| f1151d54e1 | |||
| 6a0fa4f9fa | |||
| 20d96fffc8 | |||
| ad6c06308a | |||
| 84ee4171a4 | |||
| 6bc4f8a1e4 | |||
| 8876aa0866 | |||
| 691d2e7228 | |||
| 7a12755de9 | |||
| 8573f56d03 | |||
| 8f3e683321 | |||
| 64867b0b67 | |||
| e42d060e57 | |||
| 2ca9ab8a0c | |||
| fdc0c6b371 | |||
| 8f8779c3cd | |||
| 851877ad8b | |||
| 8df74529ad | |||
| 353f51ebf0 | |||
| 6c5cb08118 | |||
| 54fee92b15 | |||
| 776431c801 | |||
| 9f893ce999 | |||
| 820447670c | |||
| b43c49cd64 | |||
| f9c3558975 | |||
| 1b75250824 | |||
| 3fa3bb5d03 | |||
| ef0ff55f1f | |||
| 66aa8ed177 | |||
| 519286bc69 | |||
| 9882f45fd2 | |||
| 634f6216a0 | |||
| 69574a7d1c | |||
| eddd4b9be8 | |||
| 9a9c31ff53 | |||
| 41ee7a1c85 | |||
| 2f9bbf373c | |||
| d662e10ebb | |||
| cd31092333 | |||
| 1eae7ab6f3 | |||
| ba378f852f | |||
| 5cfd1e557d | |||
| df31d79eaf | |||
| 12d7304325 | |||
| 41424cbdfd | |||
| 05dda519a2 | |||
| 120d39282e | |||
| 8e7d0fd13b | |||
| 3d979fdfbb | |||
| 6ab47ae3cb | |||
| a4977b4924 | |||
| bac9c692b8 | |||
| 6ab15356e1 | |||
| 73cc7121c3 | |||
| 1aeef06f49 | |||
| 3b16bcf01d | |||
| f6351fda41 | |||
| 007e91480d | |||
| 163ef9296e | |||
| fa042f7d68 | |||
| 8a11040dde | |||
| a88971d557 | |||
| 5867e5fcc5 | |||
| 20e587d6d3 | |||
| 6bfa8471cd | |||
| 5c10bce2f4 | |||
| f1663f6668 | |||
| 90c27e0e74 | |||
| b5eac0d907 | |||
| 4eabe2cb3a | |||
| a1c0d30a06 | |||
| 63c9f65040 | |||
| f58a066bff | |||
| 952ea6357a | |||
| 6695973035 | |||
| 3dc28635f4 | |||
| 0bde01a85f | |||
| b9840c2074 | |||
| 8228a76875 | |||
| 46b370a2e3 | |||
| 820e9513ba | |||
| bd71d64db3 | |||
| 9d4baf827c | |||
| d6843d7fcf | |||
| babb1dd962 | |||
| aa32e396a7 | |||
| 1068efcb49 | |||
| 576c7f1458 | |||
| 37c857b055 | |||
| 794dfb44d1 | |||
| 929bb6dc66 | |||
| 28337e31eb | |||
| 09a38c0e4b | |||
| 645b8fb9cd | |||
| 541588948c | |||
| bdd6fcfbbc | |||
| 9e50286c66 | |||
| 418e4649dc | |||
| 4a70f20f4a | |||
| d6eabfcb6d | |||
| d88889d760 | |||
| 85146d8af0 | |||
| 9612572f07 | |||
| 4bb1dddf4d | |||
| b066a86962 | |||
| 6086455782 | |||
| 9020b3cbad | |||
| 5822dea270 | |||
| c445f59664 | |||
| 737e4cb4f9 | |||
| dba7368d01 | |||
| 314c4cd8cc | |||
| 3e46f99e14 | |||
| e0cc552b8d | |||
| 6b5be403af | |||
| 269d5989bc | |||
| edfe3d9b65 | |||
| ffb2c42a26 | |||
| b7de19b020 | |||
| 77cd659b39 | |||
| fbba9d8357 | |||
| f464a9efdf | |||
| 7ec4290582 | |||
| 3f887a1d3a | |||
| ffd76dc587 | |||
| d7f3594a73 | |||
| 32fa5f206b | |||
| 70d2c09e81 | |||
| 17f03806d0 | |||
| b6aba0efa4 | |||
| 65a5e06935 | |||
| f459cb9805 | |||
| f5470aca5d | |||
| c26af97fe7 | |||
| 766ec458a2 | |||
| 48ff78580c | |||
| 396f7524d7 | |||
| da19ef42f5 | |||
| 91abe5aa43 | |||
| 682435321b | |||
| 76f0d60224 | |||
| 628b88ef9f | |||
| 6a792814ce | |||
| 05ce15d677 | |||
| 4a9d0d4f8e | |||
| 16f0552682 | |||
| 9e3819b9c7 | |||
| 233a0d4b35 | |||
| bd95b808ae | |||
| 96132c4585 | |||
| 3edacef572 | |||
| 36889c1695 | |||
| cd2c6c1d8f | |||
| d8c78b1a00 | |||
| 74a22c26cf | |||
| f742eba4c1 | |||
| 36c2812157 | |||
| d353fc4c63 | |||
| 98bd3d6da0 | |||
| cd5ec8d295 | |||
| f91c6456bd | |||
| 67af87968e | |||
| 58ea3e1b35 | |||
| a9435c10e8 | |||
| a86860fe76 | |||
| a3d707f78a | |||
| c502426249 | |||
| 2fb5ffcecf | |||
| 6d995c1253 | |||
| a860c1c490 | |||
| 481d9cc745 | |||
| 7e53a7bc2b | |||
| 4df10e3782 | |||
| 396da65178 | |||
| 87e8faf383 | |||
| 9eb3e6d398 |
@@ -0,0 +1,49 @@
|
||||
---
|
||||
description: Sync OpenRouter API implementation with latest upstream documentation
|
||||
argument-hint: specific feature to check
|
||||
---
|
||||
|
||||
Review the OpenRouter implementation:
|
||||
- Models list: `src/modules/llms/server/openai/openrouter.wiretypes.ts` (list API response schema)
|
||||
- Chat wire types: `src/modules/aix/server/dispatch/wiretypes/openai.wiretypes.ts` (OpenAI-compatible)
|
||||
- Request adapter: `src/modules/aix/server/dispatch/chatGenerate/adapters/openai.chatCompletions.ts` ('openrouter' dialect)
|
||||
- Response parser: `src/modules/aix/server/dispatch/chatGenerate/parsers/openai.parser.ts` (shared OpenAI parser)
|
||||
- Vendor config: `src/modules/llms/vendors/openrouter/openrouter.vendor.ts`
|
||||
|
||||
GOAL: Ensure complete support for OpenRouter's API including advanced features like reasoning/thinking tokens, tool use, search integration, and multi-modal capabilities. OpenRouter is OpenAI-compatible but has important extensions and differences.
|
||||
|
||||
Use Task tool with subagent_type=Explore and thoroughness="very thorough" to discover:
|
||||
1. Map API structure - all endpoints, parameters, capabilities from https://openrouter.ai/docs
|
||||
2. **Advanced features** - How to use: reasoning/thinking tokens (o1, DeepSeek R1), tool use/function calling, search integration, multi-modal (vision/audio)
|
||||
3. Changelog location - How does OpenRouter communicate API updates and breaking changes?
|
||||
4. Model metadata - What capabilities are exposed in the models list API? How to detect feature support?
|
||||
5. OpenAI deviations - Extensions, special headers (HTTP-Referer, X-Title), response fields, streaming differences
|
||||
|
||||
Then check the latest API information. Try these sources (be creative if blocked):
|
||||
|
||||
**Primary Sources:**
|
||||
- API Reference: https://openrouter.ai/docs/api-reference
|
||||
- Chat Completions: https://openrouter.ai/docs/api-reference#chat-completions
|
||||
- Models List: https://openrouter.ai/docs/api-reference#models-list
|
||||
- Parameters Guide: https://openrouter.ai/docs/parameters
|
||||
- Announcements: https://openrouter.ai/announcements (feature launches, API updates, new models)
|
||||
- Models Directory: https://openrouter.ai/models (check metadata for capabilities)
|
||||
|
||||
**Alternative Sources:**
|
||||
- GitHub: https://github.com/OpenRouterTeam (SDKs, examples, issues for recent changes)
|
||||
- Web Search: "openrouter api changelog" or "openrouter reasoning tokens" or "openrouter tool use"
|
||||
|
||||
**If blocked:** Ask user to provide documentation.
|
||||
|
||||
$ARGUMENTS
|
||||
Focus on discrepancies and gaps:
|
||||
- **Request/Response structure**: New fields, changed requirements, streaming event types
|
||||
- **Feature support**: Thinking tokens format, tool calling protocol, search parameters
|
||||
- **Model capabilities**: How to detect and enable advanced features per model
|
||||
- **OpenRouter extensions**: Headers, routing, fallbacks, rate limiting (free vs paid)
|
||||
- **Breaking changes**: Protocol updates, deprecated fields, new required parameters
|
||||
|
||||
Report differences in wire types, adapter logic, parser handling, or dialect-specific quirks.
|
||||
Prioritize new capabilities that improve user experience (reasoning visibility, better tool use, etc.).
|
||||
|
||||
When making changes, add comments with date: `// [OpenRouter, 2025-MM-DD]: explanation`
|
||||
@@ -4,7 +4,7 @@ description: Update Alibaba model definitions with latest pricing and capabiliti
|
||||
|
||||
Update `src/modules/llms/server/openai/models/alibaba.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Models & Pricing: https://www.alibabacloud.com/help/en/model-studio/models
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update Anthropic model definitions with latest pricing and capabili
|
||||
|
||||
Update `src/modules/llms/server/anthropic/anthropic.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Models: https://docs.claude.com/en/docs/about-claude/models/overview
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update DeepSeek model definitions with latest pricing and capabilit
|
||||
|
||||
Update `src/modules/llms/server/openai/models/deepseek.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Pricing: https://api-docs.deepseek.com/quick_start/pricing
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update Gemini model definitions with latest pricing and capabilitie
|
||||
|
||||
Update `src/modules/llms/server/gemini/gemini.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.types.ts`, `src/modules/llms/server/llm.server.types.ts`, and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.types.ts`, `src/modules/llms/server/llm.server.types.ts`, and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Models: https://ai.google.dev/gemini-api/docs/models
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update Groq model definitions with latest pricing and capabilities
|
||||
|
||||
Update `src/modules/llms/server/openai/models/groq.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Models: https://console.groq.com/docs/models
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
description: Update Kimi model definitions with latest pricing and capabilities
|
||||
---
|
||||
|
||||
Update `src/modules/llms/server/openai/models/moonshot.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Pricing: https://platform.moonshot.ai/docs/pricing/chat
|
||||
- API Reference: https://platform.moonshot.ai/docs/api/chat
|
||||
|
||||
**Fallbacks if blocked:** Search "moonshot kimi models latest pricing", "kimi k2 models", "moonshot api models", or search GitHub for latest model prices and context windows
|
||||
|
||||
**Important:**
|
||||
- Review the full model list for additions, removals, and price changes
|
||||
- Minimize whitespace/comment changes, focus on content
|
||||
- Preserve comments to make diffs easy to review
|
||||
- Flag broken links or unexpected content
|
||||
@@ -4,7 +4,7 @@ description: Update Mistral model definitions with latest pricing and capabiliti
|
||||
|
||||
Update `src/modules/llms/server/openai/models/mistral.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Models: https://docs.mistral.ai/getting-started/models/models_overview/
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update Ollama model definitions with latest featured models
|
||||
|
||||
Update `src/modules/llms/server/ollama/ollama.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Automated Workflow:**
|
||||
```bash
|
||||
@@ -29,6 +29,7 @@ The parser outputs: `modelName|pulls|capabilities|sizes`
|
||||
|
||||
**Important:**
|
||||
- Skip models below 50,000 pulls (parser does this automatically)
|
||||
- Skip embedding models (parser does not do this automatically)
|
||||
- Sort them in the EXACT same order as the source (featured models)
|
||||
- Extract tags: 'tools' → hasTools, 'vision' → hasVision, 'embedding' → isEmbeddings (note the 's'), 'thinking' → tags only
|
||||
- Extract 'b' tags (1.5b, 7b, 32b) to tags field
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update OpenAI model definitions with latest pricing and capabilitie
|
||||
|
||||
Update `src/modules/llms/server/openai/models/openai.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Manual hint:** For pricing page, expand all tables before copying content.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update OpenPipe model definitions with latest pricing and capabilit
|
||||
|
||||
Update `src/modules/llms/server/openai/models/openpipe.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Base Models: https://docs.openpipe.ai/base-models
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update Perplexity model definitions with latest pricing and capabil
|
||||
|
||||
Update `src/modules/llms/server/openai/models/perplexity.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Models: https://docs.perplexity.ai/getting-started/models
|
||||
|
||||
@@ -4,7 +4,7 @@ description: Update xAI model definitions with latest pricing and capabilities
|
||||
|
||||
Update `src/modules/llms/server/openai/models/xai.models.ts` with latest model definitions.
|
||||
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.data.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/server/models.mappings.ts` for context only. Focus on the model file, do not descend into other code.
|
||||
|
||||
**Primary Sources:**
|
||||
- Models & Pricing: https://docs.x.ai/docs/models?cluster=us-east-1#detailed-pricing-for-all-grok-models
|
||||
|
||||
@@ -3,9 +3,11 @@
|
||||
"allow": [
|
||||
"Bash(cat:*)",
|
||||
"Bash(cp:*)",
|
||||
"Bash(curl:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(git branch:*)",
|
||||
"Bash(git describe:*)",
|
||||
"Bash(git grep:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git log:*)",
|
||||
"Bash(git show:*)",
|
||||
@@ -16,10 +18,13 @@
|
||||
"Bash(npm install)",
|
||||
"Bash(npm install:*)",
|
||||
"Bash(npm run:*)",
|
||||
"Bash(npx eslint:*)",
|
||||
"Bash(npx tsc:*)",
|
||||
"Bash(rg:*)",
|
||||
"Bash(rm:*)",
|
||||
"Bash(sed:*)",
|
||||
"Bash(tree:*)",
|
||||
"Read(//tmp/**)",
|
||||
"WebFetch",
|
||||
"WebFetch(domain:big-agi.com)",
|
||||
"WebSearch",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
name: 🔥 Make AI Fix This
|
||||
description: Bug, question, or feedback - AI analyzes and changes Big-AGI appropriately
|
||||
labels: [ 'claude-triage' ]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for opening an issue! Our AI will analyze it and change Big-AGI appropriately.
|
||||
|
||||
**What happens next:**
|
||||
- AI searches the codebase and documentation
|
||||
- You get a response, typically within 30 minutes
|
||||
- Ticket gets follow-up and community votes
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: What's happening?
|
||||
description: Describe the bug, feature request, or question. Be as detailed as you can.
|
||||
placeholder: |
|
||||
Bug example: "In Beam, Anthropic models seem to have search off..."
|
||||
Model request: "Add Claude Opus 4.5 out today, see https://..."
|
||||
Feature example: "Add the option to to save frequent prompt templates for reuse..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Where does this happen?
|
||||
description: If this is a bug or issue, where are you experiencing it?
|
||||
options:
|
||||
- Big-AGI Pro (big-agi.com)
|
||||
- Self-deployed from GitHub
|
||||
- Docker deployment
|
||||
- Local development
|
||||
- Not applicable (question/feedback)
|
||||
- Other
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: Impact on your workflow
|
||||
description: How does this affect your use of Big-AGI?
|
||||
options:
|
||||
- Blocking - Can't use Big-AGI
|
||||
- High - Major feature broken
|
||||
- Medium - Workaround exists
|
||||
- Low - Minor inconvenience
|
||||
- None - Just a question/suggestion
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment (if applicable)
|
||||
description: Device, OS, browser - only if reporting a bug
|
||||
placeholder: |
|
||||
Device: Macbook Pro M3
|
||||
OS: macOS 15.2
|
||||
Browser: Chrome 131
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Screenshots, error messages, or anything else that helps
|
||||
placeholder: Paste screenshots or error messages here
|
||||
validations:
|
||||
required: false
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -12,13 +12,14 @@ jobs:
|
||||
!contains(github.event.issue.body, '@claude')
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 30
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
actions: read
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
@@ -35,6 +36,10 @@ jobs:
|
||||
allowed_non_write_users: '*'
|
||||
# track_progress: true # Enables tracking comments
|
||||
|
||||
# This is an optional setting that allows Claude to read CI results on PRs
|
||||
additional_permissions: |
|
||||
actions: read
|
||||
|
||||
prompt: |
|
||||
REPO: ${{ github.repository }}
|
||||
ISSUE NUMBER: #${{ github.event.issue.number }}
|
||||
@@ -61,11 +66,12 @@ jobs:
|
||||
- Link duplicates if found
|
||||
|
||||
If you're uncertain, say so and suggest next steps.
|
||||
If you write any code make sure that it compiles and that you push it.
|
||||
Be welcoming, helpful, professional, solution-focused and no-BS.
|
||||
|
||||
# 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
|
||||
--max-turns 60
|
||||
--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"
|
||||
|
||||
@@ -23,6 +23,7 @@ env:
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60 # Max 1 hour (expected: ~25min)
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
overrides=@mui/material@^5.0.0:
|
||||
dependencies:
|
||||
@mui/material: replaced-by=@mui/joy
|
||||
@@ -117,6 +117,7 @@ Located in `/src/common/layout/optima/`
|
||||
- `store-chats`: Conversations and messages
|
||||
- `store-llms`: Model configurations
|
||||
- `store-ux-labs`: UI preferences and labs features
|
||||
- **Zustand pattern**: Always wrap multi-property selectors with `useShallow` from `zustand/react/shallow` to prevent re-renders on reference changes
|
||||
|
||||
2. **Per-Instance Stores** (Vanilla Zustand)
|
||||
- `store-beam_vanilla`: Beam scatter/gather state
|
||||
|
||||
@@ -1,3 +1,32 @@
|
||||
<div align="center">
|
||||
|
||||
<img width="256" height="256" alt="Big-AGI Logo" src="https://big-agi.com/assets/logo-bright-github.svg" />
|
||||
|
||||
<h1><a href="https://big-agi.com">Big-AGI</a></h1>
|
||||
|
||||
[](https://big-agi.com)
|
||||
[](https://github.com/enricoros/big-AGI/pkgs/container/big-agi)
|
||||
[](https://vercel.com/new/clone?repository-url=https://github.com/enricoros/big-agi)
|
||||
[](https://discord.gg/MkH4qj2Jp9)
|
||||
<br/>
|
||||
[](https://github.com/enricoros/big-agi/commits)
|
||||
[](https://github.com/enricoros/big-AGI/pkgs/container/big-agi)
|
||||
[](https://github.com/enricoros/big-AGI/graphs/contributors)
|
||||
[](https://opensource.org/licenses/MIT)
|
||||
<br/>
|
||||
|
||||
[](https://github.com/enricoros/big-agi/issues/new?template=ai-triage.yml)
|
||||
|
||||
[//]: # ([](https://stats.uptimerobot.com/59MXcnmjrM))
|
||||
[//]: # ([](https://github.com/enricoros/big-AGI/releases/latest))
|
||||
[//]: # ()
|
||||
[//]: # ([](#))
|
||||
[//]: # ([](https://x.com/enricoros))
|
||||
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
# Big-AGI Open 🧠
|
||||
|
||||
This is the open-source foundation of **Big-AGI**, ___the multi-model AI workspace for experts___.
|
||||
@@ -8,18 +37,70 @@ You need to think broader, decide faster, and build with confidence, then you ne
|
||||
It comes packed with **world-class features** like Beam, and is praised for its **best-in-class AI chat UX**.
|
||||
**As an independent, non-VC-funded project, Pro subscriptions at $10.99/mo fund development for everyone, including the free and open-source tiers.**
|
||||
|
||||
**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, or GPT-5 Pro -
|
||||
**Control**: with personas, data ownership, requests inspection, unlimited usage with API keys, and *no vendor lock-in* -
|
||||

|
||||
[](https://big-agi.com/beam)
|
||||
[](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 -
|
||||
**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.
|
||||
|
||||
**Who uses Big-AGI:**
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="25%">
|
||||
<b>🧠 Intelligence</b><br/>
|
||||
<img src="https://img.shields.io/badge/Multi--Model-Trust-4285F4?style=for-the-badge" alt="Multi-Model"/>
|
||||
</td>
|
||||
<td align="center" width="25%">
|
||||
<b>✨ Experience</b><br/>
|
||||
<img src="https://img.shields.io/badge/Clean-UX-34A853?style=for-the-badge" alt="Clean UX"/>
|
||||
</td>
|
||||
<td align="center" width="25%">
|
||||
<b>⚡ Performance</b><br/>
|
||||
<img src="https://img.shields.io/badge/Zero-Latency-EA4335?style=for-the-badge" alt="Zero Latency"/>
|
||||
</td>
|
||||
<td align="center" width="25%">
|
||||
<b>🔒 Control</b><br/>
|
||||
<img src="https://img.shields.io/badge/No-Lock--in-FBBC04?style=for-the-badge" alt="No Lock-in"/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top">
|
||||
Beam & Merge<br/>
|
||||
No context junk<br/>
|
||||
Purest AI outputs
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
Flow-state interface<br/>
|
||||
Higly customizable<br/>
|
||||
Best-in-class UX
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
Local-first<br/>
|
||||
Highly parallel<br/>
|
||||
Madly optimized
|
||||
</td>
|
||||
<td align="center" valign="top">
|
||||
No vendor lock-in<br/>
|
||||
Your API keys<br/>
|
||||
AI Inspector
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
### Who uses Big-AGI:
|
||||
Loved by engineers, founders, researchers, self-hosters, and IT departments for its power, reliability, and transparency.
|
||||
|
||||
<img width="830" height="370" alt="image" src="https://github.com/user-attachments/assets/513c4f77-0970-4a56-b23b-1416c8246174" />
|
||||
|
||||
Choose Big-AGI because you don't need another clone or slop - you need an AI tool that scales with you.
|
||||
|
||||
### Show me a screenshot:
|
||||
Sure - here is real-world screeengrab as I'm writing this, while running a Beam to extract SVG from an image with Sonnet 4.5, Opus 4.1, GPT 5.1, Gemini 2.5 Pro, Nano Banana, etc.
|
||||
<img alt="Real-world screen capture as of Nov 15 2025, 2am" src="https://github.com/user-attachments/assets/853f4160-27cb-4ac9-826b-402f1e63d4af" />
|
||||
|
||||
|
||||
## Get Started
|
||||
|
||||
| Tier | Best For | What You Get | Setup |
|
||||
@@ -31,15 +112,12 @@ Choose Big-AGI because you don't need another clone or slop - you need an AI too
|
||||
\*: **Configuration requires your API keys**. *Big-AGI does not charge for model usage or limit your access*.
|
||||
**Why Pro?** As an independent project, Pro subscriptions fund all development. Early subscribers shape the roadmap directly.
|
||||
|
||||
<a href="https://big-agi.com">
|
||||
<img width="210" height="68" alt="image" src="https://github.com/user-attachments/assets/b2f8a7b8-415f-4c92-b228-4f5a54fe2bdd" />
|
||||
</a>
|
||||
[](https://big-agi.com)
|
||||
|
||||
**Self-host and developers** (full control)
|
||||
- Develop locally or self-host with Docker on your own infrastructure – [guide](docs/installation.md)
|
||||
- Or fork & run on Vercel:
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
|
||||
- Or fork & run on Vercel:
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
|
||||
|
||||
[//]: # (**For the latest Big-AGI:**)
|
||||
|
||||
@@ -225,96 +303,83 @@ https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cf
|
||||
|
||||
For full details and former releases, check out the [archived versions changelog](docs/changelog.md).
|
||||
|
||||
## 👉 Key Features
|
||||
## 👉 Supported Models & Integrations
|
||||
|
||||
|  |  |  |  |  |
|
||||
Delightful UX with latest models exclusive features like Beam for **multi-model AI validation**.
|
||||
> 
|
||||
> [](https://big-agi.com/beam)
|
||||
|
||||
|  |  |  |  |  |
|
||||
|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| **Chat**<br/>**Call**<br/>**Beam**<br/>**Draw**, ... | Local & Cloud<br/>Open & Closed<br/>Cheap & Heavy<br/>Google, Mistral, ... | Attachments<br/>Diagrams<br/>Multi-Chat<br/>Mobile-first UI | Stored Locally<br/>Easy self-Host<br/>Local actions<br/>Data = Gold | AI Personas<br/>Voice Modes<br/>Screen Capture<br/>Camera + OCR |
|
||||
|
||||

|
||||
|
||||
You can easily configure 100s of AI models in big-AGI:
|
||||
### AI Models & Vendors
|
||||
|
||||
| **AI models** | _supported vendors_ |
|
||||
|:--------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Opensource Servers | [LocalAI](https://localai.io/) (multimodal) · [Ollama](https://ollama.com/) |
|
||||
| Local Servers | [LM Studio](https://lmstudio.ai/) |
|
||||
| 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) |
|
||||
| Language services | [Alibaba](https://www.alibabacloud.com/en/product/modelstudio) · [DeepSeek](https://deepseek.com) · [Groq](https://wow.groq.com/) · [Mistral](https://mistral.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) |
|
||||
Configure 100s of AI models from 18+ providers:
|
||||
|
||||
Add extra functionality with these integrations:
|
||||
| **AI models** | _supported vendors_ |
|
||||
|:--------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Opensource Servers | [LocalAI](https://localai.io/) · [Ollama](https://ollama.com/) |
|
||||
| Local Servers | [LM Studio](https://lmstudio.ai/) (non-open) |
|
||||
| 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) |
|
||||
|
||||
| **More** | _integrations_ |
|
||||
|:-------------|:---------------------------------------------------------------------------------------------------------------|
|
||||
| Web Browse | [Browserless](https://www.browserless.io/) · [Puppeteer](https://pptr.dev/)-based |
|
||||
| Web Search | [Google CSE](https://programmablesearchengine.google.com/) |
|
||||
| Code Editors | [CodePen](https://codepen.io/pen/) · [StackBlitz](https://stackblitz.com/) · [JSFiddle](https://jsfiddle.net/) |
|
||||
| Tracking | [Helicone](https://www.helicone.ai) (LLM Observability) |
|
||||
### Additional Integrations
|
||||
|
||||
[//]: # (- [x] **Flow-state UX** for uncompromised productivity)
|
||||
|
||||
[//]: # (- [x] **AI Personas**: Tailor your AI interactions with customizable personas)
|
||||
|
||||
[//]: # (- [x] **Sleek UI/UX**: A smooth, intuitive, and mobile-responsive interface)
|
||||
|
||||
[//]: # (- [x] **Efficient Interaction**: Voice commands, OCR, and drag-and-drop file uploads)
|
||||
|
||||
[//]: # (- [x] **Privacy First**: Self-host and use your own API keys for full control)
|
||||
|
||||
[//]: # (- [x] **Advanced Tools**: Execute code, import PDFs, and summarize documents)
|
||||
|
||||
[//]: # (- [x] **Seamless Integrations**: Enhance functionality with various third-party services)
|
||||
|
||||
[//]: # (- [x] **Open Roadmap**: Contribute to the progress of big-AGI)
|
||||
|
||||
<br/>
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
To get started with big-AGI, follow our comprehensive [Installation Guide](docs/installation.md).
|
||||
The guide covers various installation options, whether you're spinning it up on
|
||||
your local computer, deploying on Vercel, on Cloudflare, or rolling it out
|
||||
through Docker.
|
||||
|
||||
Whether you're a developer, system integrator, or enterprise user, you'll find step-by-step instructions
|
||||
to set up big-AGI quickly and easily.
|
||||
|
||||
[](docs/installation.md)
|
||||
|
||||
Or bring your API keys and jump straight into our free instance on [big-AGI.com](https://big-agi.com).
|
||||
|
||||
<br/>
|
||||
|
||||
# 🌟 Get Involved!
|
||||
|
||||
[//]: # ([](https://discord.gg/MkH4qj2Jp9))
|
||||
[](https://discord.gg/MkH4qj2Jp9)
|
||||
|
||||
- [ ] 📢️ [**Chat with us** on Discord](https://discord.gg/MkH4qj2Jp9)
|
||||
- [ ] ⭐ **Give us a star** on GitHub 👆
|
||||
- [ ] 🚀 **Do you like code**? You'll love this gem of a project! [_Pick up a task!_](https://github.com/users/enricoros/projects/4/views/4) - _easy_ to _pro_
|
||||
- [ ] 💡 Got a feature suggestion? [_Add your roadmap ideas_](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
|
||||
- [ ] ✨ [Deploy](docs/installation.md) your [fork](docs/customizations.md) for your friends and family, or [customize it for work](docs/customizations.md)
|
||||
|
||||
<br/>
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/stargazers))
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/network))
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/pulls))
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/LICENSE))
|
||||
|
||||
## 📜 Licensing
|
||||
|
||||
Big-AGI incorporates third-party software components that are subject
|
||||
to separate license terms. For detailed information about these
|
||||
components and their respective licenses, please refer to
|
||||
the [Third-Party Notices](src/modules/3rdparty/THIRD_PARTY_NOTICES.md).
|
||||
| **More** | _integrations_ |
|
||||
|:--------------|:---------------------------------------------------------------------------------------------------------------|
|
||||
| Web Browse | [Browserless](https://www.browserless.io/) · [Puppeteer](https://pptr.dev/)-based |
|
||||
| Web Search | [Google CSE](https://programmablesearchengine.google.com/) |
|
||||
| Code Editors | [CodePen](https://codepen.io/pen/) · [StackBlitz](https://stackblitz.com/) · [JSFiddle](https://jsfiddle.net/) |
|
||||
| Observability | [Helicone](https://www.helicone.ai) |
|
||||
|
||||
---
|
||||
|
||||
2023-2025 · Enrico Ros x [Big-AGI](https://big-agi.com) · Like this project? Leave a star! 💫⭐
|
||||
## 🚀 Installation
|
||||
|
||||
Self-host with Docker, deploy on Vercel, or develop locally. Full setup guide:
|
||||
|
||||
[](docs/installation.md)
|
||||
|
||||
Or use the hosted version at [big-agi.com](https://big-agi.com) with your API keys.
|
||||
|
||||
---
|
||||
|
||||
## 👋 Community & Contributing
|
||||
|
||||
### Connect
|
||||
|
||||
[](https://discord.gg/MkH4qj2Jp9)
|
||||
|
||||
⭐ [Star the repo](https://github.com/enricoros/big-agi) if Big-AGI is useful to you
|
||||
|
||||
### Contribute
|
||||
|
||||
**🤖 AI-Powered Issue Assistance**
|
||||
|
||||
When you open an issue, our custom AI triage system (powered by [Claude Code](https://github.com/anthropics/claude-code-action) with Big-AGI architecture documentation) analyzes it, searches the codebase, and provides solutions - typically within 30 minutes. We've trained the system on our modules and subsystems so it handles most issues effectively. Your feedback drives development!
|
||||
|
||||
[](https://github.com/enricoros/big-agi/issues/new?template=ai-triage.yml)
|
||||
[](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
|
||||
|
||||
[](https://github.com/users/enricoros/projects/4/views/4)
|
||||
[](docs/customizations.md)
|
||||
[](https://github.com/users/enricoros/projects/4/views/2)
|
||||
|
||||
#### Contributors
|
||||
|
||||
<a href="https://github.com/enricoros/big-agi/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=enricoros/big-agi&max=48&columns=12" />
|
||||
</a>
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License · [Third-Party Notices](src/modules/3rdparty/THIRD_PARTY_NOTICES.md)
|
||||
|
||||
**2023-2025** · Enrico Ros × [Big-AGI](https://big-agi.com)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
|
||||
|
||||
import { appRouterCloud } from '~/server/trpc/trpc.router-cloud';
|
||||
import { createTRPCFetchContext } from '~/server/trpc/trpc.server';
|
||||
import { posthogCaptureServerException } from '~/server/posthog/posthog.server';
|
||||
import { posthogServerSendException } from '~/server/posthog/posthog.server';
|
||||
|
||||
const handlerNodeRoutes = (req: Request) => fetchRequestHandler({
|
||||
endpoint: '/api/cloud',
|
||||
@@ -16,15 +16,15 @@ const handlerNodeRoutes = (req: Request) => fetchRequestHandler({
|
||||
console.error(`❌ tRPC-cloud failed on ${path ?? 'unk-path'}: ${error.message}`);
|
||||
|
||||
// -> Capture node errors
|
||||
await posthogCaptureServerException(error, {
|
||||
await posthogServerSendException(error, undefined, {
|
||||
domain: 'trpc-onerror',
|
||||
runtime: 'nodejs',
|
||||
endpoint: path ?? 'unknown',
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
additionalProperties: {
|
||||
errorCode: error.code,
|
||||
errorType: type,
|
||||
error_code: error.code,
|
||||
error_type: type,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -10,7 +10,7 @@ const handlerEdgeRoutes = (req: Request) => fetchRequestHandler({
|
||||
createContext: createTRPCFetchContext,
|
||||
onError:
|
||||
process.env.NODE_ENV === 'development'
|
||||
? ({ path, error }) => console.error(`❌ tRPC-edge failed on ${path ?? 'unk-path'}: ${error.message}`)
|
||||
? ({ path, error }) => console.error(`\n❌ tRPC-edge failed on ${path ?? 'unk-path'}: ${error.message}`)
|
||||
: undefined,
|
||||
});
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ If the running LocalAI instance is configured with a [Model Gallery](https://loc
|
||||
|
||||
At the time of writing, LocalAI does not publish the model `context window size`.
|
||||
Every model is assumed to be capable of chatting, and with a context window of 4096 tokens.
|
||||
Please update the [src/modules/llms/transports/server/openai/models/models.data.ts](../src/modules/llms/server/openai/models/models.data.ts)
|
||||
Please update the [src/modules/llms/server/models.mappings.ts](../src/modules/llms/server/models.mappings.ts)
|
||||
file with the mapping information between LocalAI model IDs and names/descriptions/tokens, etc.
|
||||
|
||||
# 🤝 Support
|
||||
|
||||
@@ -35,6 +35,7 @@ GROQ_API_KEY=
|
||||
LOCALAI_API_HOST=
|
||||
LOCALAI_API_KEY=
|
||||
MISTRAL_API_KEY=
|
||||
MOONSHOT_API_KEY=
|
||||
OLLAMA_API_HOST=
|
||||
OPENPIPE_API_KEY=
|
||||
OPENROUTER_API_KEY=
|
||||
@@ -105,6 +106,7 @@ requiring the user to enter an API key
|
||||
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
|
||||
| `LOCALAI_API_KEY` | The (Optional) API key for LocalAI | Optional |
|
||||
| `MISTRAL_API_KEY` | The API key for Mistral | Optional |
|
||||
| `MOONSHOT_API_KEY` | The API key for Moonshot AI | Optional |
|
||||
| `OLLAMA_API_HOST` | Changes the backend host for the Ollama vendor. See [config-local-ollama.md](config-local-ollama.md) | |
|
||||
| `OPENPIPE_API_KEY` | The API key for OpenPipe | Optional |
|
||||
| `OPENROUTER_API_KEY` | The API key for OpenRouter | Optional |
|
||||
|
||||
@@ -28,6 +28,7 @@ stringData:
|
||||
LOCALAI_API_HOST: ""
|
||||
LOCALAI_API_KEY: ""
|
||||
MISTRAL_API_KEY: ""
|
||||
MOONSHOT_API_KEY: ""
|
||||
OLLAMA_API_HOST: ""
|
||||
OPENPIPE_API_KEY: ""
|
||||
OPENROUTER_API_KEY: ""
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { defineConfig } from "eslint/config";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import js from "@eslint/js";
|
||||
import { FlatCompat } from "@eslint/eslintrc";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: __dirname,
|
||||
recommendedConfig: js.configs.recommended,
|
||||
allConfig: js.configs.all
|
||||
});
|
||||
|
||||
export default defineConfig([{
|
||||
extends: compat.extends("next/core-web-vitals"),
|
||||
}]);
|
||||
@@ -60,7 +60,7 @@ Shows only parameters that are:
|
||||
|
||||
The AIX client transforms DLLM parameters to wire protocol format. This layer handles parameter precedence rules and name transformations:
|
||||
|
||||
```typescript
|
||||
```
|
||||
// Parameter precedence: newer 4-value version takes priority over 3-value
|
||||
...((llmVndOaiReasoningEffort4 || llmVndOaiReasoningEffort) ?
|
||||
{ vndOaiReasoningEffort: llmVndOaiReasoningEffort4 || llmVndOaiReasoningEffort } : {})
|
||||
|
||||
+26
-5
@@ -1,4 +1,5 @@
|
||||
import type { NextConfig } from 'next';
|
||||
import type { WebpackConfigContext } from 'next/dist/server/config-shared';
|
||||
import { execSync } from 'node:child_process';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
@@ -47,7 +48,7 @@ let nextConfig: NextConfig = {
|
||||
// NOTE: we may not be needing this anymore, as we use '@cloudflare/puppeteer'
|
||||
serverExternalPackages: ['puppeteer-core'],
|
||||
|
||||
webpack: (config: any, { isServer }: { isServer: boolean }) => {
|
||||
webpack: (config: any, { isServer, webpack /*, dev, nextRuntime*/ }: WebpackConfigContext) => {
|
||||
// @mui/joy: anything material gets redirected to Joy
|
||||
config.resolve.alias['@mui/material'] = '@mui/joy';
|
||||
|
||||
@@ -57,8 +58,28 @@ let nextConfig: NextConfig = {
|
||||
layers: true,
|
||||
};
|
||||
|
||||
// fix warnings for async functions in the browser (https://github.com/vercel/next.js/issues/64792)
|
||||
// client-side bundling
|
||||
if (!isServer) {
|
||||
/**
|
||||
* AIX client-side
|
||||
* We replace certain server-only modules with client-side mocks, to reuse the exact same imports
|
||||
* while avoiding importing server-only code which would break the build or break at runtime.
|
||||
*/
|
||||
const serverToClientMocks: ReadonlyArray<[RegExp, string]> = [
|
||||
[/\/posthog\.server/, '/posthog.client-mock'],
|
||||
[/\/env\.server/, '/env.client-mock'],
|
||||
];
|
||||
config.plugins = [
|
||||
...config.plugins,
|
||||
...serverToClientMocks.map(([pattern, replacement]) =>
|
||||
new webpack.NormalModuleReplacementPlugin(pattern, (resource: any) => {
|
||||
// console.log(' 🧠 [WEBPACK REPLACEMENT]:', resource.request, '->', resource.request.replace(pattern, replacement));
|
||||
resource.request = resource.request.replace(pattern, replacement);
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
// cosmetic: fix warnings for (absent!) top-level awaits in the browser (https://github.com/vercel/next.js/issues/64792)
|
||||
config.output.environment = { ...config.output.environment, asyncFunction: true };
|
||||
}
|
||||
|
||||
@@ -108,9 +129,9 @@ let nextConfig: NextConfig = {
|
||||
// },
|
||||
};
|
||||
|
||||
// Validate environment variables, if set at build time. Will be actually read and used at runtime.
|
||||
import { verifyBuildTimeVars } from '~/server/env';
|
||||
verifyBuildTimeVars();
|
||||
// Validate environment variables at build time, if required. Server env vars will be actually read and used at runtime (cloud/edge).
|
||||
import { env as validateEnv } from '~/server/env.server';
|
||||
void validateEnv; // Triggers env validation - throws if required vars are missing
|
||||
|
||||
// PostHog error reporting with source maps for production builds
|
||||
import { withPostHogConfig } from '@posthog/nextjs-config';
|
||||
|
||||
Generated
+231
-216
@@ -22,6 +22,7 @@
|
||||
"@next/bundle-analyzer": "~15.1.8",
|
||||
"@prisma/client": "~5.22.0",
|
||||
"@tanstack/react-query": "5.90.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@trpc/client": "11.5.1",
|
||||
"@trpc/next": "11.5.1",
|
||||
"@trpc/react-query": "11.5.1",
|
||||
@@ -41,15 +42,15 @@
|
||||
"next": "~15.1.8",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "5.4.54",
|
||||
"posthog-js": "^1.275.3",
|
||||
"posthog-node": "^5.10.0",
|
||||
"posthog-js": "^1.297.0",
|
||||
"posthog-node": "^5.13.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer-core": "^24.25.0",
|
||||
"puppeteer-core": "^24.30.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-hook-form": "^7.66.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-player": "^3.3.3",
|
||||
"react-player": "^3.4.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-timeago": "^8.3.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
@@ -57,24 +58,24 @@
|
||||
"remark-mark-highlight": "^0.1.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.33.5",
|
||||
"superjson": "^2.2.2",
|
||||
"superjson": "^2.2.5",
|
||||
"tesseract.js": "^6.0.1",
|
||||
"tiktoken": "^1.0.22",
|
||||
"turndown": "^7.2.1",
|
||||
"turndown": "^7.2.2",
|
||||
"zod": "^4.1.12",
|
||||
"zustand": "5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@posthog/nextjs-config": "^1.3.2",
|
||||
"@types/node": "^24.7.2",
|
||||
"@posthog/nextjs-config": "1.3.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react": "^19.2.6",
|
||||
"@types/react-csv": "^1.1.10",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "~15.1.8",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "~5.22.0",
|
||||
@@ -548,13 +549,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
|
||||
"integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.6",
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
},
|
||||
@@ -563,22 +564,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-helpers": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz",
|
||||
"integrity": "sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==",
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
|
||||
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0"
|
||||
"@eslint/core": "^0.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "0.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
|
||||
"integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
|
||||
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -613,9 +614,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.37.0.tgz",
|
||||
"integrity": "sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==",
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
|
||||
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -626,9 +627,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "2.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
|
||||
"integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
|
||||
"version": "2.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
|
||||
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
@@ -636,13 +637,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/plugin-kit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
|
||||
"integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
|
||||
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"levn": "^0.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1465,25 +1466,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mux/mux-player": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.6.1.tgz",
|
||||
"integrity": "sha512-QidL9CSkRBwa49ItphuDXWtarAiskP8AG/+vj5u0LsCa+VqObQxPfxE9t5S9YO/SDYHXqDMviMpmSzotSROGUQ==",
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-player/-/mux-player-3.8.0.tgz",
|
||||
"integrity": "sha512-2KcJdW4BBX8JDcXpclFKaNBsqpebtaEfTzwm5lPP1Lf6y5OMILvf2tqVCOczurREVFyaEoVD71vL0I5Vvqb1dA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mux/mux-video": "0.27.0",
|
||||
"@mux/playback-core": "0.31.0",
|
||||
"media-chrome": "~4.14.0",
|
||||
"player.style": "^0.2.0"
|
||||
"@mux/mux-video": "0.27.2",
|
||||
"@mux/playback-core": "0.31.2",
|
||||
"media-chrome": "~4.15.1",
|
||||
"player.style": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@mux/mux-player-react": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.6.1.tgz",
|
||||
"integrity": "sha512-YKIieu9GmFI73+1EcAvd63ftZ0Z9ilGbWo2dGXqQeyCEcagIN0oEcXWUPuIuxhvYB0XXsxB8RBAD8SigHkCYAQ==",
|
||||
"version": "3.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-player-react/-/mux-player-react-3.8.0.tgz",
|
||||
"integrity": "sha512-c9TKtK9nsSpXOuC1LVLmmHA+Zlpcx4mzgGaA7ZlukrGMfoXWvA90ROSVAAjXRA+UKSHdLIbvNofgG3P6rEE/4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mux/mux-player": "3.6.1",
|
||||
"@mux/playback-core": "0.31.0",
|
||||
"@mux/mux-player": "3.8.0",
|
||||
"@mux/playback-core": "0.31.2",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -1501,25 +1502,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mux/mux-video": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.27.0.tgz",
|
||||
"integrity": "sha512-Oi142YAcPKrmHTG+eaWHWaE7ucMHeJwx1FXABbLM2hMGj9MQ7kYjsD5J3meFlvuyz5UeVDsPLHeUJgeBXUZovg==",
|
||||
"version": "0.27.2",
|
||||
"resolved": "https://registry.npmjs.org/@mux/mux-video/-/mux-video-0.27.2.tgz",
|
||||
"integrity": "sha512-VAqSw/3kS/qBzjyFSX3wClIX5Kdk6eXXlhxIJRWlClYvUKGm9ruhd7HzkwZVOJguvUh5QbGoiGWBEW2xkNIXzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mux/mux-data-google-ima": "0.2.8",
|
||||
"@mux/playback-core": "0.31.0",
|
||||
"castable-video": "~1.1.10",
|
||||
"@mux/playback-core": "0.31.2",
|
||||
"castable-video": "~1.1.11",
|
||||
"custom-media-element": "~1.4.5",
|
||||
"media-tracks": "~0.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@mux/playback-core": {
|
||||
"version": "0.31.0",
|
||||
"resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.31.0.tgz",
|
||||
"integrity": "sha512-VADcrtS4O6fQBH8qmgavS6h7v7amzy2oCguu1NnLaVZ3Z8WccNXcF0s7jPRoRDyXWGShgtVhypW2uXjLpkPxyw==",
|
||||
"version": "0.31.2",
|
||||
"resolved": "https://registry.npmjs.org/@mux/playback-core/-/playback-core-0.31.2.tgz",
|
||||
"integrity": "sha512-bhOVTGAuKCQuDzNOc3XvDq7vsgqy2DAacLP0WdJciUKjfZhs3oA11NbKG7qAN6akPnZVfgn0Jn/sJN8TRjE30A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hls.js": "~1.6.6",
|
||||
"hls.js": "~1.6.13",
|
||||
"mux-embed": "^5.8.3"
|
||||
}
|
||||
},
|
||||
@@ -2802,9 +2803,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@puppeteer/browsers": {
|
||||
"version": "2.10.12",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.12.tgz",
|
||||
"integrity": "sha512-mP9iLFZwH+FapKJLeA7/fLqOlSUwYpMwjR1P5J23qd4e7qGJwecJccJqHYrjw33jmIZYV4dtiTHPD/J+1e7cEw==",
|
||||
"version": "2.10.13",
|
||||
"resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.10.13.tgz",
|
||||
"integrity": "sha512-a9Ruw3j3qlnB5a/zHRTkruppynxqaeE4H9WNj5eYGRWqw0ZauZ23f4W2ARf3hghF5doozyD+CRtt7XSYuYRI/Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
@@ -2883,6 +2884,33 @@
|
||||
"react": "^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/react-virtual": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
|
||||
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@tanstack/virtual-core": "3.13.12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/virtual-core": {
|
||||
"version": "3.13.12",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
|
||||
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/quickjs-emscripten": {
|
||||
"version": "0.23.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz",
|
||||
@@ -3038,13 +3066,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.7.2.tgz",
|
||||
"integrity": "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==",
|
||||
"version": "24.10.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
|
||||
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.14.0"
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nprogress": {
|
||||
@@ -3074,12 +3102,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
|
||||
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
|
||||
"version": "19.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.6.tgz",
|
||||
"integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-csv": {
|
||||
@@ -3093,9 +3121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"version": "19.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
@@ -3113,9 +3141,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/turndown": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz",
|
||||
"integrity": "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==",
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz",
|
||||
"integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -4149,9 +4177,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.0.tgz",
|
||||
"integrity": "sha512-AOhh6Bg5QmFIXdViHbMc2tLDsBIRxdkIaIddPslJF9Z5De3APBScuqGP2uThXnIpqFrgoxMNC6km7uXNIMLHXA==",
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
|
||||
"integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peerDependencies": {
|
||||
"bare-abort-controller": "*"
|
||||
@@ -4163,9 +4191,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.4.10",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.4.10.tgz",
|
||||
"integrity": "sha512-arqVF+xX/rJHwrONZaSPhlzleT2gXwVs9rsAe1p1mIVwWZI2A76/raio+KwwxfWMO8oV9Wo90EaUkS2QwVmy4w==",
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.0.tgz",
|
||||
"integrity": "sha512-GljgCjeupKZJNetTqxKaQArLK10vpmK28or0+RwWjEl5Rk+/xG3wkpmkv+WrcBm3q1BwHKlnhXzR8O37kcvkXQ==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -4230,9 +4258,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/bare-url": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.0.tgz",
|
||||
"integrity": "sha512-c+RCqMSZbkz97Mw1LWR0gcOqwK82oyYKfLoHJ8k13ybi1+I80ffdDzUy0TdAburdrR/kI0/VuN8YgEnJqX+Nyw==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz",
|
||||
"integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -4588,9 +4616,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/chromium-bidi": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-9.1.0.tgz",
|
||||
"integrity": "sha512-rlUzQ4WzIAWdIbY/viPShhZU2n21CxDUgazXVbw4Hu1MwaeUSEksSeM6DqPgpRjCLXRk702AVRxJxoOz0dw4OA==",
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-11.0.0.tgz",
|
||||
"integrity": "sha512-cM3DI+OOb89T3wO8cpPSro80Q9eKYJ7hGVXoGS3GkDPxnYSqiv+6xwpIf6XERyJ9Tdsl09hmNmY94BkgZdVekw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"mitt": "^3.0.1",
|
||||
@@ -4724,15 +4752,15 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
|
||||
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
"integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-what": "^4.1.8"
|
||||
"is-what": "^5.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
@@ -4793,7 +4821,6 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -4833,9 +4860,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csv-stringify": {
|
||||
@@ -4858,14 +4885,14 @@
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/dash-video-element": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dash-video-element/-/dash-video-element-0.2.0.tgz",
|
||||
"integrity": "sha512-dgmhBOte6JgvSvowvrh0Q/vhSrB52Q/AUl/KqminAUkPuUT3CCUNhto1X8ANigWkmNwhktFc/PCe0lF/4tBFwQ==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"custom-media-element": "^1.4.5",
|
||||
"dashjs": "^5.0.3",
|
||||
"media-tracks": "^0.3.3"
|
||||
"media-tracks": "^0.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/dashjs": {
|
||||
@@ -5074,9 +5101,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/devtools-protocol": {
|
||||
"version": "0.0.1508733",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1508733.tgz",
|
||||
"integrity": "sha512-QJ1R5gtck6nDcdM+nlsaJXcelPEI7ZxSMw1ujHpO1c4+9l+Nue5qlebi9xO1Z2MGr92bFOQTW7/rrheh5hHxDg==",
|
||||
"version": "0.0.1521046",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1521046.tgz",
|
||||
"integrity": "sha512-vhE6eymDQSKWUXwwA37NtTTVEzjtGVfDr3pRbsWEQ5onH/Snp2c+2xZHWJJawG/0hCCJLRGt4xVtEVUVILol4w==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dexie": {
|
||||
@@ -5539,25 +5566,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.37.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"version": "9.39.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
|
||||
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.0",
|
||||
"@eslint/config-helpers": "^0.4.0",
|
||||
"@eslint/core": "^0.16.0",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.37.0",
|
||||
"@eslint/plugin-kit": "^0.4.0",
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"ajv": "^6.12.4",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
@@ -6696,20 +6722,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hls-video-element": {
|
||||
"version": "1.5.8",
|
||||
"resolved": "https://registry.npmjs.org/hls-video-element/-/hls-video-element-1.5.8.tgz",
|
||||
"integrity": "sha512-DdeX5NzhM2Bj+ls5aaRrzSSnriK+r6lCrDa0YyfviNO4zb10JyAnJHZM214lXBWQghCm+fKmlWW1qpzdNoSAvQ==",
|
||||
"version": "1.5.9",
|
||||
"resolved": "https://registry.npmjs.org/hls-video-element/-/hls-video-element-1.5.9.tgz",
|
||||
"integrity": "sha512-hDXhSI3IpSSODJF8ecNzDHKP5cqsouOuKDMjoTexyFePKr9KpXVCPAnVrXFTTH8VbOim4xkLtPkVJFt7J1Rs6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"custom-media-element": "^1.4.5",
|
||||
"hls.js": "^1.6.5",
|
||||
"media-tracks": "^0.3.3"
|
||||
"media-tracks": "^0.3.4"
|
||||
}
|
||||
},
|
||||
"node_modules/hls.js": {
|
||||
"version": "1.6.13",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.13.tgz",
|
||||
"integrity": "sha512-hNEzjZNHf5bFrUNvdS4/1RjIanuJ6szpWNfTaX5I6WfGynWXGT7K/YQLYtemSvFExzeMdgdE4SsyVLJbd5PcZA==",
|
||||
"version": "1.6.14",
|
||||
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.14.tgz",
|
||||
"integrity": "sha512-CSpT2aXsv71HST8C5ETeVo+6YybqCpHBiYrCRQSn3U5QUZuLTSsvtq/bj+zuvjLVADeKxoebzo16OkH8m1+65Q==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/hoist-non-react-statics": {
|
||||
@@ -6929,9 +6955,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
@@ -7424,12 +7450,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/is-what": {
|
||||
"version": "4.1.16",
|
||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
|
||||
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz",
|
||||
"integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.13"
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mesqueeb"
|
||||
@@ -7445,7 +7471,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
@@ -8114,18 +8139,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/media-chrome": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.14.0.tgz",
|
||||
"integrity": "sha512-IEdFb4blyF15vLvQzLIn6USJBv7Kf2ne+TfLQKBYI5Z0f9VEBVZz5MKy4Uhi0iA9lStl2S9ENIujJRuJIa5OiA==",
|
||||
"version": "4.15.1",
|
||||
"resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.15.1.tgz",
|
||||
"integrity": "sha512-Hxqr0qQ67ewmRaLJBqe5ayu53txFX+DODb9xBSHgTbw7j+gITGZ4llbPPEmqMlDnatw7IsF+AUh9rJYbpnn4ZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ce-la-react": "^0.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/media-tracks": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/media-tracks/-/media-tracks-0.3.3.tgz",
|
||||
"integrity": "sha512-9P2FuUHnZZ3iji+2RQk7Zkh5AmZTnOG5fODACnjhCVveX1McY3jmCRHofIEI+yTBqplz7LXy48c7fQ3Uigp88w==",
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/media-tracks/-/media-tracks-0.3.4.tgz",
|
||||
"integrity": "sha512-5SUElzGMYXA7bcyZBL1YzLTxH9Iyw1AeYNJxzByqbestrrtB0F3wfiWUr7aROpwodO4fwnxOt78Xjb3o3ONNQg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
@@ -9335,7 +9360,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -9394,9 +9418,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/player.style": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/player.style/-/player.style-0.2.0.tgz",
|
||||
"integrity": "sha512-Ngoaz49TClptMr8HDA2IFmjT3Iq6R27QEUH/C+On33L59RSF3dCLefBYB1Au2RDZQJ6oVFpc1sXaPVpp7fEzzA==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/player.style/-/player.style-0.3.0.tgz",
|
||||
"integrity": "sha512-ny1TbqA2ZsUd6jzN+F034+UMXVK7n5SrwepsrZ2gIqVz00Hn0ohCUbbUdst/2IOFCy0oiTbaOXkSFxRw1RmSlg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
".",
|
||||
@@ -9406,13 +9430,13 @@
|
||||
"themes/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"media-chrome": "~4.13.0"
|
||||
"media-chrome": "~4.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/player.style/node_modules/media-chrome": {
|
||||
"version": "4.13.1",
|
||||
"resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.13.1.tgz",
|
||||
"integrity": "sha512-jPPwYrFkM4ky27/xNYEeyRPOBC7qvru4Oydy7vQHMHplXLQJmjtcauhlLPvG0O5kkYFEaOBXv5zGYes/UxOoVw==",
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/media-chrome/-/media-chrome-4.14.0.tgz",
|
||||
"integrity": "sha512-IEdFb4blyF15vLvQzLIn6USJBv7Kf2ne+TfLQKBYI5Z0f9VEBVZz5MKy4Uhi0iA9lStl2S9ENIujJRuJIa5OiA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ce-la-react": "^0.3.0"
|
||||
@@ -9475,53 +9499,47 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.275.3",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.275.3.tgz",
|
||||
"integrity": "sha512-LitwVprl0Q8p0fN4O4ThvlOuO6r+TBzLfkGbSyI5tR/YhlWzX3yFf4KKHPXJgxge99sxKa0fuVKNzcspYsDzcg==",
|
||||
"version": "1.297.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.297.0.tgz",
|
||||
"integrity": "sha512-+kHHe3oTRLPBokks5E2pojDfx0yAzkXLeN8BCfVY9kZ7eaaHuezpFb4DQ7i4hzI5nMFDe5qWotsUO73/GR6lmw==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.3.0",
|
||||
"@posthog/core": "1.5.3",
|
||||
"core-js": "^3.38.1",
|
||||
"fflate": "^0.4.8",
|
||||
"preact": "^10.19.3",
|
||||
"web-vitals": "^4.2.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@rrweb/types": "2.0.0-alpha.17",
|
||||
"rrweb-snapshot": "2.0.0-alpha.17"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@rrweb/types": {
|
||||
"optional": true
|
||||
},
|
||||
"rrweb-snapshot": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js/node_modules/@posthog/core": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.3.0.tgz",
|
||||
"integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "5.10.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.10.0.tgz",
|
||||
"integrity": "sha512-uNN+YUuOdbDSbDMGk/Wq57o2YBEH0Unu1kEq2PuYmqFmnu+oYsKyJBrb58VNwEuYsaXVJmk4FtbD+Tl8BT69+w==",
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.5.3.tgz",
|
||||
"integrity": "sha512-1cHCMR2uS/rAdBIFlBPJ4rPYaw1O42VkFy/LwQLtoy2hMQb2DdhCoSHfgA66R9TvcOybZsSANlbuihmGEZUKVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.3.0"
|
||||
"cross-spawn": "^7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "5.13.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.13.0.tgz",
|
||||
"integrity": "sha512-4zVTLnKAxhu6WQB4y/BFBAqtxPjt/oYlxGCfb31ebkrYwV6hnWtrj3mEd0/jmdOmSaCTzils1ApU09QgkhqxPQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node/node_modules/@posthog/core": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.3.0.tgz",
|
||||
"integrity": "sha512-hxLL8kZNHH098geedcxCz8y6xojkNYbmJEW+1vFXsmPcExyCXIUUJ/34X6xa9GcprKxd0Wsx3vfJQLQX4iVPhw==",
|
||||
"license": "MIT"
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.5.3.tgz",
|
||||
"integrity": "sha512-1cHCMR2uS/rAdBIFlBPJ4rPYaw1O42VkFy/LwQLtoy2hMQb2DdhCoSHfgA66R9TvcOybZsSANlbuihmGEZUKVQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.26.9",
|
||||
@@ -9676,17 +9694,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.25.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.25.0.tgz",
|
||||
"integrity": "sha512-8Xs6q3Ut+C8y7sAaqjIhzv1QykGWG4gc2mEZ2mYE7siZFuRp4xQVehOf8uQKSQAkeL7jXUs3mknEeiqnRqUKvQ==",
|
||||
"version": "24.30.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.30.0.tgz",
|
||||
"integrity": "sha512-2S3Smy0t0W4wJnNvDe7W0bE7wDmZjfZ3ljfMgJd6hn2Hq/f0jgN+x9PULZo2U3fu5UUIJ+JP8cNUGllu8P91Pg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.10.12",
|
||||
"chromium-bidi": "9.1.0",
|
||||
"@puppeteer/browsers": "2.10.13",
|
||||
"chromium-bidi": "11.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"devtools-protocol": "0.0.1508733",
|
||||
"devtools-protocol": "0.0.1521046",
|
||||
"typed-query-selector": "^2.12.0",
|
||||
"webdriver-bidi-protocol": "0.3.7",
|
||||
"webdriver-bidi-protocol": "0.3.8",
|
||||
"ws": "^8.18.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -9740,9 +9758,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.65.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz",
|
||||
"integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==",
|
||||
"version": "7.66.1",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.66.1.tgz",
|
||||
"integrity": "sha512-2KnjpgG2Rhbi+CIiIBQQ9Df6sMGH5ExNyFl4Hw9qO7pIqMBR8Bvu9RQyjl3JM4vehzCh9soiNUM/xYMswb2EiA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -9789,21 +9807,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-player": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-player/-/react-player-3.3.3.tgz",
|
||||
"integrity": "sha512-6U2ziVohA3WLdKI/WEQ7v27CIive0TCNIro55lJZka06fjB2kC4lJqBrvddG0yBvTDcn1owiUf2hRNaIzHAjIg==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-player/-/react-player-3.4.0.tgz",
|
||||
"integrity": "sha512-QpQSHXtnMBKjQVNeaCYMtTVcynWQ0DDDhz/FJu1OR9PHLC1Aih94UqNstywzSHbJ6Oc7lI8/7kDDqcIvyTI6zQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mux/mux-player-react": "^3.6.0",
|
||||
"@mux/mux-player-react": "^3.8.0",
|
||||
"cloudflare-video-element": "^1.3.4",
|
||||
"dash-video-element": "^0.2.0",
|
||||
"hls-video-element": "^1.5.8",
|
||||
"dash-video-element": "^0.3.0",
|
||||
"hls-video-element": "^1.5.9",
|
||||
"spotify-audio-element": "^1.0.3",
|
||||
"tiktok-video-element": "^0.1.1",
|
||||
"twitch-video-element": "^0.1.4",
|
||||
"vimeo-video-element": "^1.5.5",
|
||||
"wistia-video-element": "^1.3.4",
|
||||
"youtube-video-element": "^1.6.2"
|
||||
"twitch-video-element": "^0.1.5",
|
||||
"vimeo-video-element": "^1.6.1",
|
||||
"wistia-video-element": "^1.3.5",
|
||||
"youtube-video-element": "^1.8.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18 || ^19",
|
||||
@@ -10305,7 +10323,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -10318,7 +10335,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -10783,12 +10799,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/superjson": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.2.tgz",
|
||||
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.5.tgz",
|
||||
"integrity": "sha512-zWPTX96LVsA/eVYnqOM2+ofcdPqdS1dAF1LN4TS2/MWuUpfitd9ctTa87wt4xrYnZnkLtS69xpBdSxVBP5Rm6w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"copy-anything": "^3.0.2"
|
||||
"copy-anything": "^4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
@@ -11059,18 +11075,18 @@
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/turndown": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.1.tgz",
|
||||
"integrity": "sha512-7YiPJw6rLClQL3oUKN3KgMaXeJJ2lAyZItclgKDurqnH61so4k4IH/qwmMva0zpuJc/FhRExBBnk7EbeFANlgQ==",
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
|
||||
"integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mixmark-io/domino": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/twitch-video-element": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/twitch-video-element/-/twitch-video-element-0.1.4.tgz",
|
||||
"integrity": "sha512-SDpZ4f7sZmwHF6XG5PF0KWuP18pH/kNG04MhTcpqJby7Lk/D3TS/lCYd+RSg0rIAAVi1LDgSIo1yJs9kmHlhgw==",
|
||||
"version": "0.1.5",
|
||||
"resolved": "https://registry.npmjs.org/twitch-video-element/-/twitch-video-element-0.1.5.tgz",
|
||||
"integrity": "sha512-3UdWMa5ytWFdpgJAM6XEqqRK/1FvWdJVcKDOw4IHBPt4p52E+4fXT42fBdRZFfoxBPXQNZUDDNHFW8wIopD7Og==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
@@ -11244,9 +11260,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.14.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
|
||||
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"devOptional": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -11459,9 +11475,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vimeo-video-element": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/vimeo-video-element/-/vimeo-video-element-1.6.0.tgz",
|
||||
"integrity": "sha512-Vs+WWvd6ph6FtY+DqrVO5OHUUS02An87QydUcCUtAsIiXnYhZl0yiDC0XxWiFluo+S6keG38i4xCaQfS1LgeSg==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vimeo/player": "2.29.0"
|
||||
@@ -11499,9 +11515,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webdriver-bidi-protocol": {
|
||||
"version": "0.3.7",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.7.tgz",
|
||||
"integrity": "sha512-wIx5Gu/LLTeexxilpk8WxU2cpGAKlfbWRO5h+my6EMD1k5PYqM1qQO1MHUFf4f3KRnhBvpbZU7VkizAgeSEf7g==",
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.3.8.tgz",
|
||||
"integrity": "sha512-21Yi2GhGntMc671vNBCjiAeEVknXjVRoyu+k+9xOMShu+ZQfpGQwnBqbNz/Sv4GXZ6JmutlPAi2nIJcrymAWuQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
@@ -11602,7 +11618,6 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
@@ -11711,9 +11726,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/wistia-video-element": {
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/wistia-video-element/-/wistia-video-element-1.3.4.tgz",
|
||||
"integrity": "sha512-2l22oaQe4jUfi3yvsh2m2oCEgvbqTzaSYx6aJnZAvV5hlMUJlyZheFUnaj0JU2wGlHdVGV7xNY+5KpKu+ruLYA==",
|
||||
"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==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"super-media-element": "~1.4.2"
|
||||
@@ -11868,9 +11883,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/youtube-video-element": {
|
||||
"version": "1.6.2",
|
||||
"resolved": "https://registry.npmjs.org/youtube-video-element/-/youtube-video-element-1.6.2.tgz",
|
||||
"integrity": "sha512-YHDIOAqgRpfl1Ois9HcB8UFtWOxK8KJrV5TXpImj4BKYP1rWT04f/fMM9tQ9SYZlBKukT7NR+9wcI3UpB5BMDQ==",
|
||||
"version": "1.8.0",
|
||||
"resolved": "https://registry.npmjs.org/youtube-video-element/-/youtube-video-element-1.8.0.tgz",
|
||||
"integrity": "sha512-u3M0MgO+KUtVwIyKJXZXXJ0As0k6d5NflOrh1GjyG8NNOp+liW2nFU29hpXeUcxUWbVKhudIYd39hMVeEgCilQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zlibjs": {
|
||||
|
||||
+14
-13
@@ -33,6 +33,7 @@
|
||||
"@next/bundle-analyzer": "~15.1.8",
|
||||
"@prisma/client": "~5.22.0",
|
||||
"@tanstack/react-query": "5.90.3",
|
||||
"@tanstack/react-virtual": "^3.13.12",
|
||||
"@trpc/client": "11.5.1",
|
||||
"@trpc/next": "11.5.1",
|
||||
"@trpc/react-query": "11.5.1",
|
||||
@@ -52,15 +53,15 @@
|
||||
"next": "~15.1.8",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "5.4.54",
|
||||
"posthog-js": "^1.275.3",
|
||||
"posthog-node": "^5.10.0",
|
||||
"posthog-js": "^1.297.0",
|
||||
"posthog-node": "^5.13.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer-core": "^24.25.0",
|
||||
"puppeteer-core": "^24.30.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.65.0",
|
||||
"react-hook-form": "^7.66.1",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-player": "^3.3.3",
|
||||
"react-player": "^3.4.0",
|
||||
"react-resizable-panels": "^3.0.6",
|
||||
"react-timeago": "^8.3.0",
|
||||
"rehype-katex": "^7.0.1",
|
||||
@@ -68,24 +69,24 @@
|
||||
"remark-mark-highlight": "^0.1.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.33.5",
|
||||
"superjson": "^2.2.2",
|
||||
"superjson": "^2.2.5",
|
||||
"tesseract.js": "^6.0.1",
|
||||
"tiktoken": "^1.0.22",
|
||||
"turndown": "^7.2.1",
|
||||
"turndown": "^7.2.2",
|
||||
"zod": "^4.1.12",
|
||||
"zustand": "5.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@posthog/nextjs-config": "^1.3.2",
|
||||
"@types/node": "^24.7.2",
|
||||
"@posthog/nextjs-config": "1.3.2",
|
||||
"@types/node": "^24.10.1",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/prismjs": "^1.26.5",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react": "^19.2.6",
|
||||
"@types/react-csv": "^1.1.10",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/turndown": "^5.0.5",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.37.0",
|
||||
"eslint": "^9.39.1",
|
||||
"eslint-config-next": "~15.1.8",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "~5.22.0",
|
||||
|
||||
+9
-4
@@ -1,12 +1,17 @@
|
||||
import * as React from 'react';
|
||||
import Head from 'next/head';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { MyAppProps } from 'next/app';
|
||||
import { Analytics as VercelAnalytics } from '@vercel/analytics/next';
|
||||
import { SpeedInsights as VercelSpeedInsights } from '@vercel/speed-insights/next';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { apiQuery } from '~/common/util/trpc.client';
|
||||
|
||||
|
||||
// [server-client-safe] dynamic imports to avoid webpack bundling issues with next/navigation
|
||||
const VercelAnalytics = dynamic(() => import('@vercel/analytics/next').then(mod => mod.Analytics), { ssr: false });
|
||||
const VercelSpeedInsights = dynamic(() => import('@vercel/speed-insights/next').then(mod => mod.SpeedInsights), { ssr: false });
|
||||
|
||||
|
||||
import 'katex/dist/katex.min.css';
|
||||
import '~/common/styles/CodePrism.css';
|
||||
import '~/common/styles/GithubMarkdown.css';
|
||||
@@ -55,10 +60,10 @@ const Big_AGI_App = ({ Component, emotionCache, pageProps }: MyAppProps) => {
|
||||
</ProviderSingleTab>
|
||||
</ProviderTheming>
|
||||
|
||||
{Is.Deployment.VercelFromFrontend && <VercelAnalytics debug={false} />}
|
||||
{Is.Deployment.VercelFromFrontend && <VercelSpeedInsights debug={false} sampleRate={1 / 2} />}
|
||||
{hasGoogleAnalytics && <OptionalGoogleAnalytics />}
|
||||
{hasPostHogAnalytics && <OptionalPostHogAnalytics />}
|
||||
{Is.Deployment.VercelFromFrontend && <VercelAnalytics debug={false} />}
|
||||
{Is.Deployment.VercelFromFrontend && <VercelSpeedInsights debug={false} sampleRate={1 / 2} />}
|
||||
|
||||
</>;
|
||||
};
|
||||
|
||||
@@ -111,7 +111,6 @@ MyDocument.getInitialProps = async (ctx: DocumentContext) => {
|
||||
<style
|
||||
data-emotion={`${style.key} ${style.ids.join(' ')}`}
|
||||
key={style.key}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: style.css }}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -20,7 +20,7 @@ function initTestConversation(): DConversation {
|
||||
return conversation;
|
||||
}
|
||||
|
||||
function initTestBeamStore(messages: DMessage[], beamStore: BeamStoreApi = createBeamVanillaStore()): BeamStoreApi {
|
||||
function initTestBeamStore(messages: DMessage[], beamStore: BeamStoreApi): BeamStoreApi {
|
||||
beamStore.getState().open(messages, null, false, (content) => alert(content));
|
||||
return beamStore;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useChatLLMDropdown } from '../chat/components/layout-bar/useLLMDropdown
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../data';
|
||||
import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { AixChatGenerateContent_DMessage, aixChatGenerateContent_DMessage_FromConversation } from '~/modules/aix/client/aix.client';
|
||||
import { AixChatGenerateContent_DMessageGuts, aixChatGenerateContent_DMessage_FromConversation } from '~/modules/aix/client/aix.client';
|
||||
import { useElevenLabsVoiceDropdown } from '~/modules/elevenlabs/useElevenLabsVoiceDropdown';
|
||||
|
||||
import type { OptimaBarControlMethods } from '~/common/layout/optima/bar/OptimaBarDropdown';
|
||||
@@ -254,7 +254,7 @@ export function Telephone(props: {
|
||||
'call',
|
||||
callMessages[0].id,
|
||||
{ abortSignal: responseAbortController.current.signal },
|
||||
(update: AixChatGenerateContent_DMessage, _isDone: boolean) => {
|
||||
(update: AixChatGenerateContent_DMessageGuts, _isDone: boolean) => {
|
||||
const updatedText = messageFragmentsReduceText(update.fragments).trim();
|
||||
if (updatedText)
|
||||
setPersonaTextInterim(finalText = updatedText);
|
||||
|
||||
@@ -21,7 +21,7 @@ import { ConversationsManager } from '~/common/chat-overlay/ConversationsManager
|
||||
import { ErrorBoundary } from '~/common/components/ErrorBoundary';
|
||||
import { getLLMContextTokens, LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
|
||||
import { OptimaDrawerIn, OptimaPanelIn, OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
|
||||
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { PanelResizeInset } from '~/common/components/PanelResizeInset';
|
||||
import { Release } from '~/common/app.release';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
|
||||
@@ -186,6 +186,7 @@ export function AppChat() {
|
||||
const beamOpenStoreInFocusedPane = focusedPaneIndex === null ? null
|
||||
: !beamsOpens?.[focusedPaneIndex] ? null
|
||||
: paneBeamStores?.[focusedPaneIndex] ?? null;
|
||||
const focusedChatBeamOpen = focusedPaneIndex !== null && !!beamsOpens?.[focusedPaneIndex];
|
||||
|
||||
const {
|
||||
// focused
|
||||
@@ -479,7 +480,7 @@ export function AppChat() {
|
||||
);
|
||||
|
||||
|
||||
// Disabled by default, as it lags the opening of the drawer and immediatly vanishes during the closing animation
|
||||
// Disabled by default, as it lags the opening of the drawer and immediately vanishes during the closing animation
|
||||
const isDrawerOpen = true; // useOptimaDrawerOpen();
|
||||
|
||||
const drawerContent = React.useMemo(() => !isDrawerOpen ? null :
|
||||
@@ -489,6 +490,7 @@ export function AppChat() {
|
||||
activeFolderId={activeFolderId}
|
||||
chatPanesConversationIds={paneUniqueConversationIds}
|
||||
disableNewButton={disableNewButton}
|
||||
focusedChatBeamOpen={focusedChatBeamOpen}
|
||||
onConversationActivate={handleOpenConversationInFocusedPane}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationNew={handleConversationNewInFocusedPane}
|
||||
@@ -497,7 +499,7 @@ export function AppChat() {
|
||||
onConversationsImportDialog={handleConversationImportDialog}
|
||||
setActiveFolderId={setActiveFolderId}
|
||||
/>,
|
||||
[activeFolderId, disableNewButton, focusedPaneConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNewInFocusedPane, handleDeleteConversations, handleOpenConversationInFocusedPane, isDrawerOpen, paneUniqueConversationIds],
|
||||
[activeFolderId, disableNewButton, focusedChatBeamOpen, focusedPaneConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNewInFocusedPane, handleDeleteConversations, handleOpenConversationInFocusedPane, isDrawerOpen, paneUniqueConversationIds],
|
||||
);
|
||||
|
||||
const focusedChatPanelContent = React.useMemo(() => !focusedPaneConversationId ? null :
|
||||
@@ -523,7 +525,7 @@ export function AppChat() {
|
||||
React.useEffect(() => {
|
||||
// Debug: open a null chat
|
||||
if (Release.IsNodeDevBuild && intent.initialConversationId === 'null')
|
||||
openConversationInFocusedPane(null! /* for debugging purporse */);
|
||||
openConversationInFocusedPane(null! /* for debugging purpose */);
|
||||
// Open the initial conversation if set
|
||||
else if (intent.initialConversationId)
|
||||
openConversationInFocusedPane(intent.initialConversationId);
|
||||
@@ -651,7 +653,7 @@ export function AppChat() {
|
||||
setFocusedPaneIndex(idx);
|
||||
}}
|
||||
onCollapse={() => {
|
||||
// NOTE: despite the delay to try to let the draggin settle, there seems to be an issue with the Pane locking the screen
|
||||
// NOTE: despite the delay to try to let the dragging settle, there seems to be an issue with the Pane locking the screen
|
||||
// setTimeout(() => removePane(idx), 50);
|
||||
// more than 2 will result in an assertion from the framework
|
||||
if (chatPanes.length === 2) removePane(idx);
|
||||
@@ -678,7 +680,7 @@ export function AppChat() {
|
||||
// NOTE: this is a workaround for the 'stuck-after-collapse-close' issue. We will collapse the 'other' pane, which
|
||||
// will get it removed (onCollapse), and somehow this pane will be stuck with a pointerEvents: 'none' style, which de-facto
|
||||
// disables further interaction with the chat. This is a workaround to re-enable the pointer events.
|
||||
// The root cause seems to be a Dragstate not being reset properly, however the pointerEvents has been set since 0.0.56 while
|
||||
// The root cause seems to be a Drag state not being reset properly, however the pointerEvents has been set since 0.0.56 while
|
||||
// it was optional before: https://github.com/bvaughn/react-resizable-panels/issues/241
|
||||
pointerEvents: 'auto',
|
||||
}),
|
||||
|
||||
@@ -17,7 +17,7 @@ import { useChatAutoSuggestAttachmentPrompts, useChatMicTimeoutMsValue } from '.
|
||||
import { useAgiAttachmentPrompts } from '~/modules/aifn/agiattachmentprompts/useAgiAttachmentPrompts';
|
||||
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { DLLM, getLLMContextTokens, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
|
||||
import { DLLM, getLLMContextTokens, getLLMPricing, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
|
||||
import { AudioGenerator } from '~/common/util/audio/AudioGenerator';
|
||||
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
|
||||
import { ButtonAttachFilesMemo, openFileForAttaching } from '~/common/components/ButtonAttachFiles';
|
||||
@@ -233,7 +233,7 @@ export function Composer(props: {
|
||||
const tokensHistory = _historyTokenCount;
|
||||
const tokensResponseMax = getModelParameterValueOrThrow('llmResponseTokens', props.chatLLM?.initialParameters, props.chatLLM?.userParameters, 0) ?? 0;
|
||||
const tokenLimit = getLLMContextTokens(props.chatLLM) ?? 0;
|
||||
const tokenChatPricing = props.chatLLM?.pricing?.chat;
|
||||
const tokenChatPricing = getLLMPricing(props.chatLLM)?.chat;
|
||||
|
||||
|
||||
// Effect: load initial text if queued up (e.g. by /link/share_targetF)
|
||||
@@ -859,7 +859,7 @@ export function Composer(props: {
|
||||
<Textarea
|
||||
variant='outlined'
|
||||
color={isDraw ? 'warning' : isReAct ? 'success' : undefined}
|
||||
autoFocus
|
||||
autoFocus={isDesktop}
|
||||
minRows={isMobile ? 3.5 : isDraw ? 4 : agiAttachmentPrompts.hasData ? 3 : showChatInReferenceTo ? 4 : 5}
|
||||
maxRows={isMobile ? 8 : 10}
|
||||
placeholder={textPlaceholder}
|
||||
@@ -905,7 +905,7 @@ export function Composer(props: {
|
||||
)}
|
||||
|
||||
{!showChatInReferenceTo && !isDraw && tokenLimit > 0 && (
|
||||
<TokenBadgeMemo hideBelowDollars={0.0001} chatPricing={tokenChatPricing} direct={tokensComposer} history={tokensHistory} responseMax={tokensResponseMax} limit={tokenLimit} showCost={labsShowCost} enableHover={!isMobile} showExcess absoluteBottomRight />
|
||||
<TokenBadgeMemo hideBelowDollars={0.005} chatPricing={tokenChatPricing} direct={tokensComposer} history={tokensHistory} responseMax={tokensResponseMax} limit={tokenLimit} showCost={labsShowCost} enableHover={!isMobile} showExcess absoluteBottomRight />
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -98,6 +98,7 @@ const converterTypeToIconMap: { [key in AttachmentDraftConverterType]: React.Com
|
||||
'image-resized-high': PhotoSizeSelectLargeOutlinedIcon,
|
||||
'image-resized-low': PhotoSizeSelectSmallOutlinedIcon,
|
||||
'image-to-default': ImageOutlinedIcon,
|
||||
'image-caption': AbcIcon,
|
||||
'image-ocr': AbcIcon,
|
||||
'pdf-text': PictureAsPdfIcon,
|
||||
'pdf-images': PermMediaOutlinedIcon,
|
||||
|
||||
@@ -66,6 +66,7 @@ function ChatDrawer(props: {
|
||||
activeFolderId: string | null,
|
||||
chatPanesConversationIds: DConversationId[],
|
||||
disableNewButton: boolean,
|
||||
focusedChatBeamOpen: boolean,
|
||||
onConversationActivate: (conversationId: DConversationId) => void,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string | null, addSplitPane: boolean) => void,
|
||||
onConversationNew: (forceNoRecycle: boolean, isIncognito: boolean) => void,
|
||||
@@ -456,7 +457,7 @@ function ChatDrawer(props: {
|
||||
{/*<OpenAIIcon sx={{ ml: 'auto' }} />*/}
|
||||
</ListItemButton>
|
||||
|
||||
<ListItemButton disabled={filteredChatsAreEmpty} onClick={handleConversationsExport} sx={{ flex: 1 }}>
|
||||
<ListItemButton disabled={filteredChatsAreEmpty || props.focusedChatBeamOpen} onClick={handleConversationsExport} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator>
|
||||
<FileUploadOutlinedIcon />
|
||||
</ListItemDecorator>
|
||||
|
||||
@@ -4,7 +4,8 @@ import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, ColorPaletteProp } from '@mui/joy';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import { DMessageContentFragment, DMessageTextPart, isTextContentFragment } from '~/common/stores/chat/chat.fragments';
|
||||
import type { InterleavedFragment } from '~/common/stores/chat/hooks/useFragmentBuckets';
|
||||
import { DMessageTextPart, isTextContentFragment } from '~/common/stores/chat/chat.fragments';
|
||||
|
||||
|
||||
// configuration
|
||||
@@ -54,7 +55,7 @@ const optionSx: SxProps = {
|
||||
};
|
||||
|
||||
|
||||
export function optionsExtractFromFragments_dangerModifyFragment(enabled: boolean, fragments: DMessageContentFragment[]): { fragments: DMessageContentFragment[], options: string[], } {
|
||||
export function optionsExtractFromFragments_dangerModifyFragment(enabled: boolean, fragments: InterleavedFragment[]): { fragments: InterleavedFragment[], options: string[] } {
|
||||
if (enabled && fragments.length) {
|
||||
const fragment = fragments[fragments.length - 1];
|
||||
if (isTextContentFragment(fragment)) {
|
||||
|
||||
@@ -217,15 +217,15 @@ export function ChatMessage(props: {
|
||||
const isVndAndCacheUser = !!props.showAntPromptCaching && messageHasUserFlag(props.message, MESSAGE_FLAG_VND_ANT_CACHE_USER);
|
||||
|
||||
const {
|
||||
annotationFragments, // Web Citations, References (rendered at top)
|
||||
interleavedFragments, // Reasoning, Placeholders, Text, Code, Tools (interleaved in temporal order)
|
||||
imageAttachments, // Stamp-sized Images
|
||||
voidFragments, // Model-Aux, Placeholders
|
||||
contentFragments, // Text (Markdown + Code + ... blocks), Errors, (large) Images
|
||||
nonImageAttachments, // Document Attachments, likely the User dropped them in
|
||||
lastFragmentIsError,
|
||||
} = useFragmentBuckets(messageFragments);
|
||||
|
||||
const fragmentFlattenedText = React.useMemo(() => messageFragmentsReduceText(messageFragments), [messageFragments]);
|
||||
const handleHighlightSelText = useSelHighlighterMemo(messageId, selText, contentFragments, fromAssistant, props.onMessageFragmentReplace);
|
||||
const handleHighlightSelText = useSelHighlighterMemo(messageId, selText, interleavedFragments.filter(f => f.ft === 'content'), fromAssistant, props.onMessageFragmentReplace);
|
||||
|
||||
const textSubject = selText ? selText : fragmentFlattenedText;
|
||||
const isSpecialT2I = textSubject.startsWith('/draw ') || textSubject.startsWith('/imagine ') || textSubject.startsWith('/img ');
|
||||
@@ -579,9 +579,9 @@ export function ChatMessage(props: {
|
||||
|
||||
const lookForOptions = props.onMessageContinue !== undefined && props.isBottom === true && messageGenerator?.tokenStopReason !== 'out-of-tokens' && fromAssistant && !messagePendingIncomplete && !isEditingText && uiComplexityMode !== 'minimal' && false;
|
||||
|
||||
const { fragments: renderContentFragments, options: continuationOptions } = React.useMemo(() => {
|
||||
return optionsExtractFromFragments_dangerModifyFragment(lookForOptions, contentFragments);
|
||||
}, [contentFragments, lookForOptions]);
|
||||
const { fragments: renderInterleavedFragments, options: continuationOptions } = React.useMemo(() => {
|
||||
return optionsExtractFromFragments_dangerModifyFragment(lookForOptions, interleavedFragments);
|
||||
}, [interleavedFragments, lookForOptions]);
|
||||
|
||||
|
||||
// style
|
||||
@@ -589,7 +589,7 @@ export function ChatMessage(props: {
|
||||
|
||||
const listItemSx: SxProps = React.useMemo(() => ({
|
||||
// vars
|
||||
'--AGI-overlay-start-opacity': uiComplexityMode === 'extra' ? 0.1 : 0,
|
||||
// '--AGI-overlay-start-opacity': uiComplexityMode === 'extra' ? 0.1 : 0, // disabled - looks worse
|
||||
|
||||
// style
|
||||
backgroundColor: backgroundColor,
|
||||
@@ -773,20 +773,23 @@ export function ChatMessage(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Void Fragments */}
|
||||
{voidFragments.length >= 1 && (
|
||||
{/* Annotation Fragments (absolute top: citations, references) */}
|
||||
{annotationFragments.length >= 1 && (
|
||||
<VoidFragments
|
||||
voidFragments={voidFragments}
|
||||
nonVoidFragmentsCount={renderContentFragments.length}
|
||||
voidFragments={annotationFragments}
|
||||
nonVoidFragmentsCount={interleavedFragments.filter(f => f.ft === 'content').length}
|
||||
contentScaling={adjContentScaling}
|
||||
uiComplexityMode={uiComplexityMode}
|
||||
messageRole={messageRole}
|
||||
messagePendingIncomplete={messagePendingIncomplete}
|
||||
onFragmentDelete={!props.onMessageFragmentDelete ? undefined : handleFragmentDelete}
|
||||
onFragmentReplace={!props.onMessageFragmentReplace ? undefined : handleFragmentReplace}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Content Fragments */}
|
||||
{/* Interleaved Fragments (reasoning + content in temporal order) */}
|
||||
<ContentFragments
|
||||
contentFragments={renderContentFragments}
|
||||
contentFragments={renderInterleavedFragments}
|
||||
showEmptyNotice={!messageFragments.length && !messagePendingIncomplete}
|
||||
|
||||
contentScaling={adjContentScaling}
|
||||
@@ -794,6 +797,7 @@ export function ChatMessage(props: {
|
||||
fitScreen={props.fitScreen}
|
||||
isMobile={props.isMobile}
|
||||
messageRole={messageRole}
|
||||
messagePendingIncomplete={messagePendingIncomplete}
|
||||
optiAllowSubBlocksMemo={!!messagePendingIncomplete}
|
||||
disableMarkdownText={disableMarkdown || fromUser /* User messages are edited as text. Try to have them in plain text. NOTE: This may bite. */}
|
||||
showUnsafeHtmlCode={props.showUnsafeHtmlCode}
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ function _inferInitialViewAsCode(attachmentFragment: DMessageAttachmentFragment)
|
||||
}
|
||||
|
||||
|
||||
export function DocAttachmentFragment(props: {
|
||||
export const DocAttachmentFragmentPane = React.memo(function DocAttachmentFragment(props: {
|
||||
fragment: DMessageAttachmentFragment,
|
||||
controlledEditor: boolean,
|
||||
editedText?: string,
|
||||
@@ -400,4 +400,4 @@ export function DocAttachmentFragment(props: {
|
||||
|
||||
</RenderCodePanelFrame>
|
||||
);
|
||||
}
|
||||
});
|
||||
+37
-6
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { Box } from '@mui/joy';
|
||||
import { Box, Button } from '@mui/joy';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import type { DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
@@ -7,7 +7,7 @@ import { DMessageAttachmentFragment, DMessageFragmentId, isDocPart, updateFragme
|
||||
|
||||
import type { ChatMessageTextPartEditState } from '../ChatMessage';
|
||||
import { DocAttachmentFragmentButton } from './DocAttachmentFragmentButton';
|
||||
import { DocAttachmentFragment } from './DocAttachmentFragment';
|
||||
import { DocAttachmentFragmentPane } from './DocAttachmentFragmentPane';
|
||||
|
||||
|
||||
/**
|
||||
@@ -15,7 +15,7 @@ import { DocAttachmentFragment } from './DocAttachmentFragment';
|
||||
* When one is active, there is a content part just right under (with the collapse mechanism in case it's a user role).
|
||||
* If one is clicked the content part (use ContentPartText) is displayed.
|
||||
*/
|
||||
export function DocumentAttachmentFragments(props: {
|
||||
export const DocumentAttachmentFragments = React.memo(function DocumentAttachmentFragments(props: {
|
||||
attachmentFragments: DMessageAttachmentFragment[],
|
||||
messageRole: DMessageRole,
|
||||
contentScaling: ContentScaling,
|
||||
@@ -30,6 +30,7 @@ export function DocumentAttachmentFragments(props: {
|
||||
// state
|
||||
const [_activeFragmentId, setActiveFragmentId] = React.useState<DMessageFragmentId | null>(null);
|
||||
const [editState, setEditState] = React.useState<ChatMessageTextPartEditState | null>(null);
|
||||
const [showAllAttachments, setShowAllAttachments] = React.useState<boolean>(false);
|
||||
|
||||
|
||||
// derived state
|
||||
@@ -92,6 +93,20 @@ export function DocumentAttachmentFragments(props: {
|
||||
}, []);
|
||||
|
||||
|
||||
// pagination logic
|
||||
const SHOW_LIMIT = 49;
|
||||
const totalAttachments = props.attachmentFragments.length;
|
||||
const hasMoreThanLimit = totalAttachments > SHOW_LIMIT + 1; // +1 to account for "show more" button
|
||||
const visibleAttachments = hasMoreThanLimit && !showAllAttachments
|
||||
? props.attachmentFragments.slice(0, SHOW_LIMIT)
|
||||
: props.attachmentFragments;
|
||||
const remainingCount = totalAttachments - SHOW_LIMIT;
|
||||
|
||||
const handleToggleShowAll = React.useCallback(() => {
|
||||
setShowAllAttachments(prev => !prev);
|
||||
}, []);
|
||||
|
||||
|
||||
// memos
|
||||
const buttonsSx = React.useMemo(() => ({
|
||||
// layout
|
||||
@@ -112,7 +127,7 @@ export function DocumentAttachmentFragments(props: {
|
||||
|
||||
{/* Document buttons */}
|
||||
<Box sx={buttonsSx}>
|
||||
{props.attachmentFragments.map((attachmentFragment) =>
|
||||
{visibleAttachments.map((attachmentFragment) =>
|
||||
<DocAttachmentFragmentButton
|
||||
key={attachmentFragment.fId}
|
||||
fragment={attachmentFragment}
|
||||
@@ -122,11 +137,27 @@ export function DocumentAttachmentFragments(props: {
|
||||
toggleSelected={handleToggleSelectedId}
|
||||
/>,
|
||||
)}
|
||||
|
||||
{/* Show more/less button */}
|
||||
{hasMoreThanLimit && (
|
||||
<Button
|
||||
size={props.contentScaling === 'md' ? 'md' : 'sm'}
|
||||
variant='soft'
|
||||
onClick={handleToggleShowAll}
|
||||
sx={{
|
||||
minHeight: props.contentScaling === 'md' ? 40 : props.contentScaling === 'sm' ? 38 : 36,
|
||||
minWidth: '64px',
|
||||
fontWeight: 'md',
|
||||
}}
|
||||
>
|
||||
{showAllAttachments ? `Show fewer docs...` : `Show ${remainingCount} more...`}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Document Viewer & Editor */}
|
||||
{!!selectedFragment && isDocPart(selectedFragment.part) && (
|
||||
<DocAttachmentFragment
|
||||
<DocAttachmentFragmentPane
|
||||
key={selectedFragment.fId /* this is here for the useLiveFile hook which otherwise would migrate state across fragments */}
|
||||
fragment={selectedFragment}
|
||||
controlledEditor={controlledEditor}
|
||||
@@ -144,4 +175,4 @@ export function DocumentAttachmentFragments(props: {
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
|
||||
import { Sheet } from '@mui/joy';
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, IconButton, Sheet, Typography } from '@mui/joy';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
|
||||
import { BlocksContainer } from '~/modules/blocks/BlocksContainers';
|
||||
import { useScaledTypographySx } from '~/modules/blocks/blocks.styles';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import type { DMessageToolInvocationPart } from '~/common/stores/chat/chat.fragments';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
|
||||
import { humanReadableFunctionName } from './BlockPartToolInvocation.utils';
|
||||
|
||||
|
||||
const keyValueGridSx = {
|
||||
border: '1px solid',
|
||||
borderRadius: 'sm',
|
||||
boxShadow: 'inset 2px 0 4px -2px rgba(0, 0, 0, 0.2)',
|
||||
p: 1.5,
|
||||
// border: '1px solid',
|
||||
// borderRadius: 'sm',
|
||||
// boxShadow: 'inset 2px 0 4px -2px rgba(0, 0, 0, 0.2)',
|
||||
// p: 1.5,
|
||||
|
||||
// Grid layout with 2 columns
|
||||
display: 'grid',
|
||||
@@ -30,36 +35,49 @@ const keyValueGridSx = {
|
||||
// },
|
||||
} as const;
|
||||
|
||||
const _styleKeyValueGrid: SxProps = {
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
p: 0,
|
||||
fontSize: '0.875em',
|
||||
opacity: 0.9,
|
||||
} as const;
|
||||
|
||||
|
||||
export type KeyValueData = { label: string, value: React.ReactNode, asCode?: boolean }[];
|
||||
|
||||
export function KeyValueGrid(props: {
|
||||
data: KeyValueData,
|
||||
contentScaling: ContentScaling,
|
||||
color?: ColorPaletteProp,
|
||||
variant?: VariantProp,
|
||||
stableSx?: SxProps,
|
||||
// contentScaling: ContentScaling,
|
||||
// color?: ColorPaletteProp,
|
||||
// variant?: VariantProp,
|
||||
// stableSx?: SxProps,
|
||||
}) {
|
||||
|
||||
const { fontSize, lineHeight } = useScaledTypographySx(props.contentScaling, false, false);
|
||||
// const { fontSize, lineHeight } = useScaledTypographySx(props.contentScaling, false, false);
|
||||
|
||||
const gridSx = React.useMemo(() => ({
|
||||
...keyValueGridSx,
|
||||
// fontWeight,
|
||||
fontSize,
|
||||
lineHeight,
|
||||
...props.stableSx,
|
||||
}), [fontSize, lineHeight, props.stableSx]);
|
||||
// fontSize,
|
||||
// lineHeight,
|
||||
// ...props.stableSx,
|
||||
_styleKeyValueGrid,
|
||||
}), [/*props.stableSx*/]);
|
||||
|
||||
return (
|
||||
<Sheet color={props.color} variant={props.variant || 'soft'} sx={gridSx}>
|
||||
<Box
|
||||
// color={props.color}
|
||||
// variant={props.variant || 'soft'}
|
||||
sx={gridSx}
|
||||
>
|
||||
{props.data.map(({ label, value }, index) => (
|
||||
<React.Fragment key={index}>
|
||||
<div>{label}</div>
|
||||
<div>{value}</div>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Sheet>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,35 +88,124 @@ export function BlockPartToolInvocation(props: {
|
||||
onDoubleClick?: (event: React.MouseEvent) => void;
|
||||
}) {
|
||||
|
||||
const part = props.toolInvocationPart;
|
||||
// state
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
|
||||
const kvData: KeyValueData = React.useMemo(() => {
|
||||
switch (part.invocation.type) {
|
||||
// external state
|
||||
const { fontSize, lineHeight } = useScaledTypographySx(props.contentScaling, false, false);
|
||||
|
||||
|
||||
// memo name
|
||||
|
||||
const { id: iId, invocation } = props.toolInvocationPart;
|
||||
|
||||
const { humanName, originalName } = React.useMemo(() => {
|
||||
const invocationType = invocation.type;
|
||||
const originalName = invocationType === 'function_call' ? invocation.name : 'code_execution';
|
||||
const humanName = humanReadableFunctionName(originalName, invocationType, 'invocation');
|
||||
return { humanName, originalName };
|
||||
}, [invocation]);
|
||||
|
||||
|
||||
// memo details
|
||||
|
||||
const detailsData: KeyValueData = React.useMemo(() => {
|
||||
switch (invocation.type) {
|
||||
case 'function_call':
|
||||
return [
|
||||
{ label: 'Name', value: <strong>{part.invocation.name}</strong> },
|
||||
{ label: 'Args', value: part.invocation.args || 'None', asCode: true },
|
||||
{ label: 'Id', value: part.id },
|
||||
{ label: 'Name', value: invocation.name },
|
||||
{ label: 'Args', value: invocation.args || 'None', asCode: true },
|
||||
{ label: 'ID', value: iId },
|
||||
];
|
||||
case 'code_execution':
|
||||
return [
|
||||
{ label: 'Language', value: part.invocation.language },
|
||||
{ label: 'Author', value: part.invocation.author },
|
||||
{ label: 'Language', value: invocation.language },
|
||||
{ label: 'Author', value: invocation.author },
|
||||
{
|
||||
label: 'Code',
|
||||
value: <div style={{ whiteSpace: 'pre-wrap' }}>{part.invocation.code.trim()}</div>,
|
||||
value: <div style={{ whiteSpace: 'pre-wrap' }}>{invocation.code.trim()}</div>,
|
||||
},
|
||||
{ label: 'Id', value: part.id },
|
||||
{ label: 'ID', value: iId },
|
||||
];
|
||||
}
|
||||
}, [part]);
|
||||
}, [invocation, iId]);
|
||||
|
||||
|
||||
const toggleExpanded = React.useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setExpanded(prev => !prev);
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<BlocksContainer onDoubleClick={props.onDoubleClick}>
|
||||
<KeyValueGrid
|
||||
data={kvData}
|
||||
contentScaling={props.contentScaling}
|
||||
/>
|
||||
</BlocksContainer>
|
||||
<BlocksContainer onDoubleClick={props.onDoubleClick}><Box /*sx={{ px: 1.5 }}*/>
|
||||
|
||||
<Sheet
|
||||
variant='soft'
|
||||
sx={{
|
||||
borderLeft: '3px solid',
|
||||
borderLeftColor: 'primary.softBg',
|
||||
borderRadius: 'sm',
|
||||
pl: 1,
|
||||
pr: 2,
|
||||
py: 0.75,
|
||||
fontSize,
|
||||
lineHeight,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
...(expanded ? {
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.outlinedBorder',
|
||||
boxShadow: 'inset 2px 0 4px -2px rgba(0, 0, 0, 0.2)',
|
||||
} : {}),
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Compact header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { '& .expand-icon': { opacity: 1 } },
|
||||
}}
|
||||
onClick={toggleExpanded}
|
||||
>
|
||||
<IconButton
|
||||
size='sm'
|
||||
className='expand-icon'
|
||||
sx={{
|
||||
minWidth: 'auto',
|
||||
minHeight: 'auto',
|
||||
padding: 0,
|
||||
opacity: expanded ? 1 : 0.5,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
>
|
||||
{expanded ? <KeyboardArrowDownIcon fontSize='small' /> : <KeyboardArrowRightIcon fontSize='small' />}
|
||||
</IconButton>
|
||||
|
||||
{/*<Tooltip title={humanName !== originalName ? `Original: ${originalName}` : undefined} placement='top'>*/}
|
||||
<Typography level='body-sm' sx={{ fontWeight: 'md' }}>
|
||||
{humanName}
|
||||
</Typography>
|
||||
{/*</Tooltip>*/}
|
||||
</Box>
|
||||
|
||||
{/* Expanded details */}
|
||||
<ExpanderControlledBox expanded={expanded}>
|
||||
{expanded && <Box sx={{ mt: 1, ml: 2.625, pl: 1 }}>
|
||||
<KeyValueGrid
|
||||
data={detailsData}
|
||||
// contentScaling={props.contentScaling}
|
||||
// stableSx={_styleKeyValueGrid}
|
||||
/>
|
||||
</Box>}
|
||||
</ExpanderControlledBox>
|
||||
|
||||
</Sheet>
|
||||
|
||||
</Box></BlocksContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
//
|
||||
// Utilities for rendering tool invocations
|
||||
//
|
||||
|
||||
/**
|
||||
* [EDITORIAL] Known hosted tool name translations
|
||||
*
|
||||
* This mapping provides human-readable names for actual hosted tools
|
||||
* from AI model providers. Only add entries for confirmed provider-hosted tools.
|
||||
*
|
||||
* Note: Tool calls != Function calls
|
||||
* - Tool calls: Provider-hosted tools (e.g., Anthropic's computer use, Gemini's code execution)
|
||||
* - Function calls: User/app-defined functions that the model can invoke
|
||||
*/
|
||||
const KNOWN_TOOL_TRANSLATIONS: Record<string, string> = {
|
||||
// Anthropic Computer Use Tools (hosted)
|
||||
'computer': 'Computer Use',
|
||||
'computer_20241022': 'Computer Use',
|
||||
'bash': 'Bash',
|
||||
'bash_20241022': 'Bash',
|
||||
'text_editor': 'Text Editor',
|
||||
'text_editor_20241022': 'Text Editor',
|
||||
|
||||
// Gemini Tools (hosted)
|
||||
'code_execution': 'Code Execution',
|
||||
'google_search_retrieval': 'Google Search',
|
||||
|
||||
// Add other confirmed provider-hosted tools here as discovered
|
||||
} as const;
|
||||
|
||||
|
||||
/**
|
||||
* Translate a function/tool name to a human-readable format
|
||||
*
|
||||
* First checks for known hosted tools, then applies heuristics for function names
|
||||
*/
|
||||
export function humanReadableFunctionName(name: string, invocationType: 'function_call' | 'code_execution', phase: 'invocation' | 'response'): string {
|
||||
if (invocationType === 'code_execution')
|
||||
return phase === 'invocation' ? 'Generated code' : 'Executed code';
|
||||
|
||||
// check for known hosted tools
|
||||
if (KNOWN_TOOL_TRANSLATIONS[name])
|
||||
return KNOWN_TOOL_TRANSLATIONS[name];
|
||||
|
||||
// apply heuristics for user-defined function names
|
||||
if (name.startsWith('get_'))
|
||||
return _toTitleCase(name.substring(4));
|
||||
if (name.startsWith('fetch_'))
|
||||
return _toTitleCase(name.substring(6));
|
||||
if (name.startsWith('search_'))
|
||||
return _toTitleCase(name.substring(7)) + ' Search';
|
||||
|
||||
return _toTitleCase(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get function display name and color
|
||||
*/
|
||||
export function functionNameAppearance(environment: 'upstream' | 'server' | 'client'): {
|
||||
label: string;
|
||||
color: 'primary' | 'neutral' | 'success';
|
||||
} {
|
||||
switch (environment) {
|
||||
case 'upstream':
|
||||
return { label: 'Hosted', color: 'primary' };
|
||||
case 'server':
|
||||
return { label: 'Server', color: 'neutral' };
|
||||
case 'client':
|
||||
return { label: 'Client', color: 'success' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function _toTitleCase(fName: string): string {
|
||||
// snake_case -> Title Case
|
||||
if (fName.includes('_'))
|
||||
return fName
|
||||
.split('_')
|
||||
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
|
||||
// camelCase -> Title Case
|
||||
const withSpaces = fName.replace(/([A-Z])/g, ' $1').trim();
|
||||
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1);
|
||||
}
|
||||
@@ -1,10 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Chip, IconButton, Sheet, Typography } from '@mui/joy';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
|
||||
import { BlocksContainer } from '~/modules/blocks/BlocksContainers';
|
||||
import { useScaledTypographySx } from '~/modules/blocks/blocks.styles';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import type { DMessageToolResponsePart } from '~/common/stores/chat/chat.fragments';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
|
||||
import { functionNameAppearance, humanReadableFunctionName } from './BlockPartToolInvocation.utils';
|
||||
import { KeyValueData, KeyValueGrid } from './BlockPartToolInvocation';
|
||||
|
||||
|
||||
@@ -14,36 +21,148 @@ export function BlockPartToolResponse(props: {
|
||||
onDoubleClick?: (event: React.MouseEvent) => void;
|
||||
}) {
|
||||
|
||||
const part = props.toolResponsePart;
|
||||
// state
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
|
||||
const kvData: KeyValueData = React.useMemo(() => {
|
||||
switch (part.response.type) {
|
||||
// external state
|
||||
const { fontSize, lineHeight } = useScaledTypographySx(props.contentScaling, false, false);
|
||||
|
||||
|
||||
// memo name
|
||||
|
||||
const { id: rId, response, environment, error: rError } = props.toolResponsePart;
|
||||
|
||||
const { humanName, originalName, envInfo } = React.useMemo(() => {
|
||||
const invocationType = response.type;
|
||||
const originalName = invocationType === 'function_call' ? response.name : 'code_execution';
|
||||
const humanName = humanReadableFunctionName(originalName, invocationType, 'response');
|
||||
const envInfo = functionNameAppearance(environment);
|
||||
return { humanName, originalName, envInfo };
|
||||
}, [response, environment]);
|
||||
|
||||
// memo details data
|
||||
|
||||
const detailsData: KeyValueData = React.useMemo(() => {
|
||||
switch (response.type) {
|
||||
case 'function_call':
|
||||
return [
|
||||
{ label: 'Id', value: part.id },
|
||||
{ label: 'Name', value: <strong>{part.response.name}</strong> },
|
||||
{ label: 'Response', value: part.response.result, asCode: true },
|
||||
...(!part.error ? [] : [{ label: 'Error', value: part.error }]),
|
||||
{ label: 'Environment', value: part.environment },
|
||||
{ label: 'Function', value: response.name },
|
||||
{ label: 'Result', value: response.result, asCode: true },
|
||||
...(!rError ? [] : [{ label: 'Error', value: String(rError) }]),
|
||||
{ label: 'Environment', value: envInfo.label },
|
||||
{ label: 'ID', value: rId },
|
||||
];
|
||||
case 'code_execution':
|
||||
return [
|
||||
{ label: 'Id', value: part.id },
|
||||
{ label: 'Response', value: part.response.result, asCode: true },
|
||||
...(!part.error ? [] : [{ label: 'Error', value: part.error }]),
|
||||
{ label: 'Executor', value: part.response.executor },
|
||||
{ label: 'Environment', value: part.environment },
|
||||
{ label: 'Result', value: response.result, asCode: true },
|
||||
...(!rError ? [] : [{ label: 'Error', value: String(rError) }]),
|
||||
{ label: 'Executor', value: response.executor },
|
||||
{ label: 'Environment', value: envInfo.label },
|
||||
{ label: 'ID', value: rId },
|
||||
];
|
||||
}
|
||||
}, [part]);
|
||||
}, [envInfo.label, rError, rId, response]);
|
||||
|
||||
// memo border color
|
||||
|
||||
const borderColor = React.useMemo(() => {
|
||||
if (rError) return 'danger.softBg';
|
||||
switch (environment) {
|
||||
case 'upstream':
|
||||
return 'primary.softBg'; // Hosted - blue
|
||||
case 'server':
|
||||
return 'neutral.softBg'; // Server - gray
|
||||
case 'client':
|
||||
return 'success.softBg'; // Client - green
|
||||
}
|
||||
}, [rError, environment]);
|
||||
|
||||
|
||||
const toggleExpanded = React.useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setExpanded(prev => !prev);
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<BlocksContainer onDoubleClick={props.onDoubleClick}>
|
||||
<KeyValueGrid
|
||||
data={kvData}
|
||||
contentScaling={props.contentScaling}
|
||||
color={part.error ? 'danger' : 'primary'}
|
||||
/>
|
||||
</BlocksContainer>
|
||||
<BlocksContainer onDoubleClick={props.onDoubleClick}><Box /*sx={{ px: 1.5 }}*/>
|
||||
<Sheet
|
||||
variant='soft'
|
||||
color={rError ? 'danger' : undefined}
|
||||
sx={{
|
||||
borderLeft: '3px solid',
|
||||
borderLeftColor: borderColor,
|
||||
borderRadius: 'sm',
|
||||
pl: 1,
|
||||
pr: 2,
|
||||
py: 0.75,
|
||||
fontSize,
|
||||
lineHeight,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
...(expanded ? {
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.outlinedBorder',
|
||||
boxShadow: 'inset 2px 0 4px -2px rgba(0, 0, 0, 0.2)',
|
||||
} : {}),
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Compact header */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
cursor: 'pointer',
|
||||
'&:hover': { '& .expand-icon': { opacity: 1 } },
|
||||
}}
|
||||
onClick={toggleExpanded}
|
||||
>
|
||||
<IconButton
|
||||
size='sm'
|
||||
className='expand-icon'
|
||||
sx={{
|
||||
minWidth: 'auto',
|
||||
minHeight: 'auto',
|
||||
padding: 0,
|
||||
opacity: expanded ? 1 : 0.5,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
>
|
||||
{expanded ? <KeyboardArrowDownIcon fontSize='small' /> : <KeyboardArrowRightIcon fontSize='small' />}
|
||||
</IconButton>
|
||||
|
||||
{/*<Tooltip title={humanName !== originalName ? `Original: ${originalName}` : undefined} placement='top'>*/}
|
||||
<Typography level='body-sm' sx={{ fontWeight: 'md' }}>
|
||||
{humanName}
|
||||
</Typography>
|
||||
{/*</Tooltip>*/}
|
||||
|
||||
{rError && (
|
||||
<Chip size='sm' color='danger' variant='soft'>
|
||||
Error
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
<Chip size='sm' color={envInfo.color} variant='soft' sx={{ ml: 'auto' }}>
|
||||
{envInfo.label}
|
||||
</Chip>
|
||||
</Box>
|
||||
|
||||
{/* Expanded details */}
|
||||
<ExpanderControlledBox expanded={expanded}>
|
||||
{expanded && <Box sx={{ mt: 1, ml: 2.625, pl: 1 }}>
|
||||
<KeyValueGrid
|
||||
data={detailsData}
|
||||
// contentScaling={props.contentScaling}
|
||||
// stableSx={_styleKeyValueGrid}
|
||||
/>
|
||||
</Box>}
|
||||
</ExpanderControlledBox>
|
||||
|
||||
</Sheet>
|
||||
|
||||
</Box></BlocksContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,16 +6,21 @@ import { ScaledTextBlockRenderer } from '~/modules/blocks/ScaledTextBlockRendere
|
||||
|
||||
import type { ContentScaling, UIComplexityMode } from '~/common/app.theme';
|
||||
import type { DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
import { DMessageContentFragment, DMessageFragmentId, isTextPart } from '~/common/stores/chat/chat.fragments';
|
||||
import type { InterleavedFragment } from '~/common/stores/chat/hooks/useFragmentBuckets';
|
||||
import { DMessageContentFragment, DMessageFragmentId, isTextContentFragment, isTextPart, isVoidPlaceholderFragment } from '~/common/stores/chat/chat.fragments';
|
||||
import { Release } from '~/common/app.release';
|
||||
|
||||
import type { ChatMessageTextPartEditState } from '../ChatMessage';
|
||||
import { BlockEdit_TextFragment } from './BlockEdit_TextFragment';
|
||||
import { BlockOpEmpty } from './BlockOpEmpty';
|
||||
import { BlockPartError } from './BlockPartError';
|
||||
import { BlockPartImageRef } from './BlockPartImageRef';
|
||||
import { BlockPartModelAux } from '../fragments-void/BlockPartModelAux';
|
||||
import { BlockPartPlaceholder } from '../fragments-void/BlockPartPlaceholder';
|
||||
import { BlockPartText_AutoBlocks } from './BlockPartText_AutoBlocks';
|
||||
import { BlockPartToolInvocation } from './BlockPartToolInvocation';
|
||||
import { BlockPartToolResponse } from './BlockPartToolResponse';
|
||||
import { humanReadableFunctionName } from './BlockPartToolInvocation.utils';
|
||||
|
||||
|
||||
const _editLayoutSx: SxProps = {
|
||||
@@ -42,7 +47,7 @@ const _endLayoutSx: SxProps = {
|
||||
|
||||
export function ContentFragments(props: {
|
||||
|
||||
contentFragments: DMessageContentFragment[]
|
||||
contentFragments: InterleavedFragment[]
|
||||
showEmptyNotice: boolean,
|
||||
|
||||
contentScaling: ContentScaling,
|
||||
@@ -50,6 +55,7 @@ export function ContentFragments(props: {
|
||||
fitScreen: boolean,
|
||||
isMobile: boolean,
|
||||
messageRole: DMessageRole,
|
||||
messagePendingIncomplete?: boolean,
|
||||
optiAllowSubBlocksMemo?: boolean,
|
||||
disableMarkdownText: boolean,
|
||||
enhanceCodeBlocks: boolean,
|
||||
@@ -76,8 +82,18 @@ export function ContentFragments(props: {
|
||||
const isEditingText = !!props.textEditsState;
|
||||
const enableRestartFromEdit = !fromAssistant && props.messageRole !== 'system';
|
||||
|
||||
|
||||
// solo placeholder - dataStreamViz trigger
|
||||
const showDataStreamViz =
|
||||
!Release.Features.LIGHTER_ANIMATIONS
|
||||
&& props.uiComplexityMode !== 'minimal'
|
||||
&& props.contentFragments.length === 1
|
||||
// && props.noVoidFragments // not needed, we have all the interleaved fragments here
|
||||
&& isVoidPlaceholderFragment(props.contentFragments[0]);
|
||||
|
||||
|
||||
// Content Fragments Edit Zero-State: button to create a new TextContentFragment
|
||||
if (isEditingText && isEmpty)
|
||||
if (isEditingText && !props.contentFragments.some(isTextContentFragment))
|
||||
return !props.onFragmentAddBlank ? null : (
|
||||
<Button aria-label='message body empty' variant='plain' color='neutral' onClick={props.onFragmentAddBlank} sx={{ justifyContent: 'flex-start' }}>
|
||||
add text ...
|
||||
@@ -92,7 +108,7 @@ export function ContentFragments(props: {
|
||||
if (!props.showEmptyNotice && isEmpty)
|
||||
return null;
|
||||
|
||||
return <Box aria-label='message body' sx={isEditingText ? _editLayoutSx : fromAssistant ? _startLayoutSx : _endLayoutSx}>
|
||||
return <Box aria-label='message body' sx={(showDataStreamViz || isEditingText) ? _editLayoutSx : fromAssistant ? _startLayoutSx : _endLayoutSx}>
|
||||
|
||||
{/* Empty Message Block - if empty */}
|
||||
{props.showEmptyNotice && (
|
||||
@@ -103,35 +119,107 @@ export function ContentFragments(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.contentFragments.map((fragment) => {
|
||||
{props.contentFragments.map((fragment, fragmentIndex) => {
|
||||
|
||||
// simplify
|
||||
const { fId, part } = fragment;
|
||||
const { fId, ft } = fragment;
|
||||
|
||||
// Determine the text to edit based on the part type
|
||||
let editText = '';
|
||||
let editLabel;
|
||||
if (isTextPart(part))
|
||||
editText = part.text;
|
||||
else if (part.pt === 'error')
|
||||
editText = part.error;
|
||||
else if (part.pt === 'tool_invocation') {
|
||||
if (part.invocation.type === 'function_call') {
|
||||
editText = part.invocation.args /* string | null */ || '';
|
||||
editLabel = `[Invocation] Function Call: \`${part.invocation.name}\``;
|
||||
} else {
|
||||
editText = part.invocation.code;
|
||||
editLabel = `[Invocation] Code Execution: \`${part.invocation.language}\``;
|
||||
}
|
||||
} else if (part.pt === 'tool_response') {
|
||||
if (!part.error) {
|
||||
editText = part.response.result;
|
||||
editLabel = `[Response]: ${part.response.type === 'function_call' ? 'Function Call' : 'Code Execution'}: \`${part.id}\``;
|
||||
// VOID FRAGMENTS (reasoning, placeholders - interleaved with content)
|
||||
if (ft === 'void') {
|
||||
const { part } = fragment;
|
||||
switch (part.pt) {
|
||||
|
||||
// Handled by VoidFragments
|
||||
// case 'annotations':
|
||||
// console.warn('[DEV] ContentFragments: annotations fragment found in interleaved list');
|
||||
// return null;
|
||||
|
||||
case 'ma':
|
||||
return (
|
||||
<BlockPartModelAux
|
||||
key={fId}
|
||||
fragmentId={fId}
|
||||
auxType={part.aType}
|
||||
auxText={part.aText}
|
||||
auxHasSignature={part.textSignature !== undefined}
|
||||
auxRedactedDataCount={part.redactedData?.length ?? 0}
|
||||
messagePendingIncomplete={!!props.messagePendingIncomplete}
|
||||
zenMode={props.uiComplexityMode === 'minimal'}
|
||||
contentScaling={props.contentScaling}
|
||||
isLastFragment={fragmentIndex === props.contentFragments.length - 1}
|
||||
onFragmentDelete={props.onFragmentDelete}
|
||||
onFragmentReplace={props.onFragmentReplace}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'ph':
|
||||
return (
|
||||
<BlockPartPlaceholder
|
||||
key={fId}
|
||||
placeholderText={part.pText}
|
||||
placeholderType={part.pType}
|
||||
placeholderModelOp={part.modelOp}
|
||||
placeholderAixControl={part.aixControl}
|
||||
messageRole={props.messageRole}
|
||||
contentScaling={props.contentScaling}
|
||||
showAsItalic
|
||||
showAsDataStreamViz={showDataStreamViz}
|
||||
/>
|
||||
);
|
||||
|
||||
case '_pt_sentinel':
|
||||
return null;
|
||||
|
||||
default:
|
||||
const _exhaustiveVoidCheck: never = part;
|
||||
// fallthrough - we don't handle these here anymore
|
||||
case 'annotations':
|
||||
return (
|
||||
<ScaledTextBlockRenderer
|
||||
key={fId}
|
||||
text={`Unknown Void Fragment: ${(part as any)?.pt}`}
|
||||
contentScaling={props.contentScaling}
|
||||
textRenderVariant='text'
|
||||
showAsDanger
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// CONTENT FRAGMENTS (text, code, tool calls, images, errors)
|
||||
const { part } = fragment;
|
||||
|
||||
// editing for text parts, tool invocations, or tool responses
|
||||
if (props.textEditsState && !!props.setEditedText && (isTextPart(part) || part.pt === 'error' || part.pt === 'tool_invocation' || part.pt === 'tool_response')) {
|
||||
if (props.textEditsState && !!props.setEditedText && (
|
||||
isTextPart(part) || part.pt === 'error' || part.pt === 'tool_invocation' || part.pt === 'tool_response'
|
||||
)) {
|
||||
|
||||
// Determine the text to edit based on the part type
|
||||
let editText = '';
|
||||
let editLabel;
|
||||
if (isTextPart(part)) {
|
||||
editText = part.text;
|
||||
} else if (part.pt === 'error') {
|
||||
editText = part.error;
|
||||
} else if (part.pt === 'tool_invocation') {
|
||||
if (part.invocation.type === 'function_call') {
|
||||
editText = part.invocation.args /* string | null */ || '';
|
||||
const humanName = humanReadableFunctionName(part.invocation.name, 'function_call', 'invocation');
|
||||
editLabel = `[Invocation] ${humanName} · \`${part.invocation.name}\``;
|
||||
} else {
|
||||
editText = part.invocation.code;
|
||||
const humanName = humanReadableFunctionName('code_execution', 'code_execution', 'invocation');
|
||||
editLabel = `[Invocation] ${humanName} · \`${part.invocation.language}\``;
|
||||
}
|
||||
} else if (part.pt === 'tool_response') {
|
||||
if (!part.error) {
|
||||
editText = part.response.result;
|
||||
const responseName = part.response.type === 'function_call' ? part.response.name : 'code_execution';
|
||||
const humanName = humanReadableFunctionName(responseName, part.response.type, 'response');
|
||||
editLabel = `[Response] ${humanName} · \`${part.id}\``;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<BlockEdit_TextFragment
|
||||
key={'edit-' + fId}
|
||||
@@ -165,7 +253,7 @@ export function ContentFragments(props: {
|
||||
const rt = part.rt;
|
||||
switch (rt) {
|
||||
case 'zync':
|
||||
const zt = part.zType
|
||||
const zt = part.zType;
|
||||
switch (zt) {
|
||||
case 'asset':
|
||||
// TODO: [ASSET] future: implement rendering for the real Reference to Zync Asset
|
||||
|
||||
@@ -170,7 +170,9 @@ export function BlockPartModelAnnotations(props: {
|
||||
return null;
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{ mx: 1.5 }}
|
||||
>
|
||||
|
||||
{/* Row of favicons */}
|
||||
<Button
|
||||
|
||||
@@ -3,6 +3,7 @@ import * as React from 'react';
|
||||
import type { ColorPaletteProp } from '@mui/joy/styles/types';
|
||||
import { Box, Chip, Typography } from '@mui/joy';
|
||||
import AllInclusiveIcon from '@mui/icons-material/AllInclusive';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
|
||||
import { RenderMarkdown } from '~/modules/blocks/markdown/RenderMarkdown';
|
||||
@@ -11,6 +12,7 @@ import { useScaledTypographySx } from '~/modules/blocks/blocks.styles';
|
||||
import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
import { adjustContentScaling, ContentScaling } from '~/common/app.theme';
|
||||
import { animationSpinHalfPause } from '~/common/util/animUtils';
|
||||
import { createTextContentFragment, DMessageContentFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
|
||||
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
|
||||
|
||||
@@ -32,17 +34,29 @@ const _styles = {
|
||||
chip: {
|
||||
px: 1.5,
|
||||
py: 0.375,
|
||||
my: '1px', // to not crop the outline on mobile
|
||||
my: '1px', // to not crop the outline on mobile, or on beam
|
||||
outline: '1px solid',
|
||||
outlineColor: `${REASONING_COLOR}.solidBg`, // .outlinedBorder
|
||||
boxShadow: `1px 2px 4px -3px var(--joy-palette-${REASONING_COLOR}-solidBg)`,
|
||||
} as const,
|
||||
|
||||
chipDisabled: {
|
||||
px: 1.5,
|
||||
py: 0.375,
|
||||
my: '1px', // to not crop the outline on mobile, or on beam
|
||||
} as const,
|
||||
|
||||
chipIcon: {
|
||||
fontSize: '1rem',
|
||||
mr: 0.5,
|
||||
} as const,
|
||||
|
||||
chipIconPending: {
|
||||
fontSize: '1rem',
|
||||
mr: 0.5,
|
||||
animation: `${animationSpinHalfPause} 2s ease-in-out infinite`,
|
||||
} as const,
|
||||
|
||||
chipExpanded: {
|
||||
mt: '1px', // need to copy the `chip` mt
|
||||
px: 1.5,
|
||||
@@ -93,8 +107,11 @@ export function BlockPartModelAux(props: {
|
||||
auxText: string,
|
||||
auxHasSignature: boolean,
|
||||
auxRedactedDataCount: number,
|
||||
messagePendingIncomplete: boolean,
|
||||
zenMode: boolean,
|
||||
contentScaling: ContentScaling,
|
||||
isLastFragment: boolean,
|
||||
onFragmentDelete?: (fragmentId: DMessageFragmentId) => void,
|
||||
onFragmentReplace?: (fragmentId: DMessageFragmentId, newFragment: DMessageContentFragment) => void,
|
||||
}) {
|
||||
|
||||
@@ -115,7 +132,8 @@ export function BlockPartModelAux(props: {
|
||||
|
||||
// handlers
|
||||
|
||||
const { onFragmentReplace } = props;
|
||||
const { onFragmentDelete, onFragmentReplace } = props;
|
||||
const showDelete = !!onFragmentDelete;
|
||||
const showInline = !!onFragmentReplace;
|
||||
|
||||
const handleToggleExpanded = React.useCallback(() => {
|
||||
@@ -123,6 +141,23 @@ export function BlockPartModelAux(props: {
|
||||
setExpanded(on => !on);
|
||||
}, []);
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
if (!onFragmentDelete) return;
|
||||
showPromisedOverlay('chat-message-delete-aux', {}, ({ onResolve, onUserReject }) =>
|
||||
<ConfirmationModal
|
||||
open onClose={onUserReject} onPositive={() => onResolve(true)}
|
||||
confirmationText={<>
|
||||
Delete this {typeText.toLowerCase()} completely?
|
||||
<br />
|
||||
This action cannot be undone.
|
||||
</>}
|
||||
positiveActionText='Delete'
|
||||
/>,
|
||||
).then(() => {
|
||||
onFragmentDelete(props.fragmentId);
|
||||
}).catch(() => null /* ignore closure */);
|
||||
}, [onFragmentDelete, props.fragmentId, showPromisedOverlay, typeText]);
|
||||
|
||||
const handleInline = React.useCallback(() => {
|
||||
if (!onFragmentReplace) return;
|
||||
showPromisedOverlay('chat-message-inline-aux', {}, ({ onResolve, onUserReject }) =>
|
||||
@@ -149,29 +184,52 @@ export function BlockPartModelAux(props: {
|
||||
{/* Chip to expand/collapse */}
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Chip
|
||||
color={REASONING_COLOR}
|
||||
color={props.isLastFragment ? REASONING_COLOR : 'neutral'}
|
||||
variant={expanded ? 'solid' : 'soft'}
|
||||
size='sm'
|
||||
onClick={handleToggleExpanded}
|
||||
sx={expanded ? _styles.chipExpanded : _styles.chip}
|
||||
startDecorator={<AllInclusiveIcon sx={_styles.chipIcon} /* sx={{ color: expanded ? undefined : REASONING_COLOR }} */ />}
|
||||
sx={expanded ? _styles.chipExpanded : props.isLastFragment ? _styles.chip : _styles.chipDisabled}
|
||||
startDecorator={
|
||||
<AllInclusiveIcon
|
||||
sx={(props.messagePendingIncomplete && !expanded && props.isLastFragment) ? _styles.chipIconPending : _styles.chipIcon}
|
||||
/* sx={{ color: expanded ? undefined : REASONING_COLOR }} */
|
||||
/>
|
||||
}
|
||||
// startDecorator='🧠'
|
||||
>
|
||||
Show {typeText}
|
||||
</Chip>
|
||||
|
||||
{expanded && showInline && !!props.auxText && (
|
||||
<Chip
|
||||
color={REASONING_COLOR}
|
||||
variant='soft'
|
||||
size='sm'
|
||||
disabled={!onFragmentReplace}
|
||||
onClick={!onFragmentReplace ? undefined : handleInline}
|
||||
endDecorator={<TextFieldsIcon />}
|
||||
sx={_styles.chip}
|
||||
>
|
||||
Make Regular Text
|
||||
</Chip>
|
||||
{expanded && (showInline || showDelete) && !!props.auxText && (
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
|
||||
{/* Make inline */}
|
||||
{showInline && <Chip
|
||||
color={REASONING_COLOR}
|
||||
variant='soft'
|
||||
size='sm'
|
||||
disabled={!onFragmentReplace || props.messagePendingIncomplete}
|
||||
onClick={!onFragmentReplace ? undefined : handleInline}
|
||||
endDecorator={<TextFieldsIcon />}
|
||||
sx={(!onFragmentReplace || props.messagePendingIncomplete) ? _styles.chipDisabled : _styles.chip}
|
||||
>
|
||||
Make Regular Text
|
||||
</Chip>}
|
||||
|
||||
{/* Delete */}
|
||||
{showDelete && <Chip
|
||||
color={REASONING_COLOR}
|
||||
variant='soft'
|
||||
size='sm'
|
||||
disabled={!onFragmentDelete || props.messagePendingIncomplete}
|
||||
onClick={!onFragmentDelete ? undefined : handleDelete}
|
||||
endDecorator={<DeleteOutlineIcon />}
|
||||
sx={(!onFragmentDelete || props.messagePendingIncomplete) ? _styles.chipDisabled : _styles.chip}
|
||||
>
|
||||
Delete
|
||||
</Chip>}
|
||||
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@ import { Box, Chip } from '@mui/joy';
|
||||
import BrushRoundedIcon from '@mui/icons-material/BrushRounded';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
||||
import RepeatIcon from '@mui/icons-material/Repeat';
|
||||
import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
|
||||
|
||||
import { BlocksContainer } from '~/modules/blocks/BlocksContainers';
|
||||
import { ScaledTextBlockRenderer } from '~/modules/blocks/ScaledTextBlockRenderer';
|
||||
|
||||
import type { DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
import type { DVoidPlaceholderModelOp } from '~/common/stores/chat/chat.fragments';
|
||||
import type { DVoidPlaceholderModelOp, DVoidPlaceholderPart } from '~/common/stores/chat/chat.fragments';
|
||||
import { adjustContentScaling, ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { DataStreamViz } from '~/common/components/DataStreamViz';
|
||||
import { animationSpinHalfPause } from '~/common/util/animUtils';
|
||||
@@ -31,6 +32,10 @@ const _styles = {
|
||||
outline: '1px solid',
|
||||
outlineColor: 'primary.solidBg', // .outlinedBorder
|
||||
boxShadow: `1px 2px 4px -3px var(--joy-palette-primary-solidBg)`,
|
||||
|
||||
// wrap text if needed - introduced for retry error messages
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
} as const,
|
||||
|
||||
followUpChipIcon: {
|
||||
@@ -113,8 +118,9 @@ function ModelOperationChip(props: {
|
||||
|
||||
export function BlockPartPlaceholder(props: {
|
||||
placeholderText: string,
|
||||
placeholderType?: 'chat-gen-follow-up',
|
||||
placeholderType?: DVoidPlaceholderPart['pType'],
|
||||
placeholderModelOp?: DVoidPlaceholderModelOp,
|
||||
placeholderAixControl?: DVoidPlaceholderPart['aixControl'],
|
||||
messageRole: DMessageRole,
|
||||
contentScaling: ContentScaling,
|
||||
showAsItalic?: boolean,
|
||||
@@ -146,7 +152,8 @@ export function BlockPartPlaceholder(props: {
|
||||
|
||||
|
||||
// Type-based visualization
|
||||
if (props.placeholderType === 'chat-gen-follow-up') return (
|
||||
const isFollowUp = props.placeholderType === 'chat-gen-follow-up';
|
||||
if (isFollowUp) return (
|
||||
<Chip
|
||||
color='primary'
|
||||
variant='soft'
|
||||
@@ -158,6 +165,34 @@ export function BlockPartPlaceholder(props: {
|
||||
</Chip>
|
||||
);
|
||||
|
||||
// AIX Control renderer (e.g., error correction retry)
|
||||
if (props.placeholderAixControl?.ctl === 'ec-retry') {
|
||||
const { rScope, rCauseHttp, rCauseConn } = props.placeholderAixControl;
|
||||
const color = rScope === 'srv-dispatch' ? 'primary' : rScope === 'srv-op' ? 'warning' : 'danger';
|
||||
return (
|
||||
<Chip
|
||||
// size='sm'
|
||||
color={color}
|
||||
variant='soft'
|
||||
startDecorator={<div style={{ opacity: 0.75 }}>{rCauseHttp || rCauseConn || rScope}</div>}
|
||||
endDecorator={<RepeatIcon style={{ opacity: 0.5 }} />}
|
||||
onClick={() => console.log({ props })}
|
||||
sx={{
|
||||
gap: 1.5,
|
||||
px: 1.5,
|
||||
py: 0.375,
|
||||
my: '1px', // to not crop the outline on mobile, or on beam
|
||||
boxShadow: `1px 2px 4px -3px var(--joy-palette-${color}-solidBg)`,
|
||||
// wrap text if needed - introduced for retry error messages
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{props.placeholderText}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
||||
// Model operation renderer
|
||||
if (props.placeholderModelOp)
|
||||
return (
|
||||
|
||||
@@ -6,29 +6,16 @@ import { Box } from '@mui/joy';
|
||||
import { ScaledTextBlockRenderer } from '~/modules/blocks/ScaledTextBlockRenderer';
|
||||
|
||||
import type { ContentScaling, UIComplexityMode } from '~/common/app.theme';
|
||||
import type { DMessageContentFragment, DMessageFragmentId, DMessageVoidFragment } from '~/common/stores/chat/chat.fragments';
|
||||
import type { DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
import { DMessageContentFragment, DMessageFragmentId, DMessageVoidFragment, isPlaceholderPart } from '~/common/stores/chat/chat.fragments';
|
||||
import { Release } from '~/common/app.release';
|
||||
|
||||
import { BlockPartModelAux } from './BlockPartModelAux';
|
||||
import { BlockPartPlaceholder } from './BlockPartPlaceholder';
|
||||
import { BlockPartModelAnnotations } from './BlockPartModelAnnotations';
|
||||
|
||||
|
||||
const editLayoutSx: SxProps = {
|
||||
const startLayoutSx: SxProps = {
|
||||
display: 'grid',
|
||||
gap: 1.5, // see why we give more space on ChatMessage
|
||||
|
||||
// horizontal separator between messages (second part+ and before)
|
||||
// '& > *:not(:first-of-type)': {
|
||||
// borderTop: '1px solid',
|
||||
// borderTopColor: 'background.level3',
|
||||
// },
|
||||
};
|
||||
|
||||
const startLayoutSx: SxProps = {
|
||||
...editLayoutSx,
|
||||
|
||||
// NOTE: we used to have 'flex-start' here, but it was causing the Annotation fragment to not be able to
|
||||
// stretch to the full with of this 'void fragments' container.
|
||||
// So now we don't have 'flex-start' anymore, and we may expect issues with other Fragment kinds?
|
||||
@@ -36,7 +23,7 @@ const startLayoutSx: SxProps = {
|
||||
};
|
||||
|
||||
const endLayoutSx: SxProps = {
|
||||
...editLayoutSx,
|
||||
...startLayoutSx,
|
||||
justifyContent: 'flex-end',
|
||||
};
|
||||
|
||||
@@ -47,6 +34,7 @@ const endLayoutSx: SxProps = {
|
||||
*
|
||||
* In the future we can revisit this decision in case Content fragments and *Void Fragments** are
|
||||
* interleaved - but for now, Void fragments will be grouped together at the top.
|
||||
* ^ 2025-11-20: NOTE: Lol, yes we did
|
||||
*/
|
||||
export function VoidFragments(props: {
|
||||
|
||||
@@ -56,21 +44,16 @@ export function VoidFragments(props: {
|
||||
contentScaling: ContentScaling,
|
||||
uiComplexityMode: UIComplexityMode,
|
||||
messageRole: DMessageRole,
|
||||
messagePendingIncomplete?: boolean,
|
||||
|
||||
onFragmentDelete?: (fragmentId: DMessageFragmentId) => void,
|
||||
onFragmentReplace?: (fragmentId: DMessageFragmentId, newFragment: DMessageContentFragment) => void,
|
||||
|
||||
}) {
|
||||
|
||||
const showDataStreamViz =
|
||||
!Release.Features.LIGHTER_ANIMATIONS
|
||||
&& props.uiComplexityMode !== 'minimal'
|
||||
&& props.voidFragments.length === 1 && props.nonVoidFragmentsCount === 0
|
||||
&& isPlaceholderPart(props.voidFragments[0].part);
|
||||
|
||||
const fromAssistant = props.messageRole === 'assistant';
|
||||
|
||||
|
||||
return <Box aria-label='message void' sx={showDataStreamViz ? editLayoutSx : fromAssistant ? startLayoutSx : endLayoutSx}>
|
||||
return <Box aria-label='message void' sx={fromAssistant ? startLayoutSx : endLayoutSx}>
|
||||
|
||||
{props.voidFragments.map(({ fId, part }) => {
|
||||
switch (part.pt) {
|
||||
@@ -84,41 +67,15 @@ export function VoidFragments(props: {
|
||||
/>
|
||||
);
|
||||
|
||||
case 'ma':
|
||||
return (
|
||||
<BlockPartModelAux
|
||||
key={fId}
|
||||
fragmentId={fId}
|
||||
auxType={part.aType}
|
||||
auxText={part.aText}
|
||||
auxHasSignature={part.textSignature !== undefined}
|
||||
auxRedactedDataCount={part.redactedData?.length ?? 0}
|
||||
zenMode={props.uiComplexityMode === 'minimal'}
|
||||
contentScaling={props.contentScaling}
|
||||
onFragmentReplace={props.onFragmentReplace}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'ph':
|
||||
return (
|
||||
<BlockPartPlaceholder
|
||||
key={fId}
|
||||
placeholderText={part.pText}
|
||||
placeholderType={part.pType}
|
||||
placeholderModelOp={part.modelOp}
|
||||
messageRole={props.messageRole}
|
||||
contentScaling={props.contentScaling}
|
||||
showAsItalic
|
||||
showAsDataStreamViz={showDataStreamViz}
|
||||
/>
|
||||
);
|
||||
|
||||
case '_pt_sentinel':
|
||||
return null;
|
||||
|
||||
default:
|
||||
// noinspection JSUnusedLocalSymbols
|
||||
const _exhaustiveVoidFragmentCheck: never = part;
|
||||
// fallthrough - we don't handle these here anymore
|
||||
case 'ma':
|
||||
case 'ph':
|
||||
return (
|
||||
<ScaledTextBlockRenderer
|
||||
key={fId}
|
||||
|
||||
@@ -7,6 +7,14 @@ import { wrapWithMarkdownSyntax } from '~/modules/blocks/markdown/markdown.wrapp
|
||||
import { BUBBLE_MIN_TEXT_LENGTH } from './ChatMessage';
|
||||
|
||||
|
||||
/**
|
||||
* Text matching strategy for selection highlighting:
|
||||
* - 'exact': Direct substring match in source (former behavior)
|
||||
* - 'md-approx': Markdown-approximate match - finds rendered text in decorated source (new behavior)
|
||||
*/
|
||||
const MATCH_METHOD: 'exact' | 'md-approx' = 'md-approx';
|
||||
|
||||
|
||||
/* Note: future evolution of Marking:
|
||||
* 'data-purpose'?: 'review' | 'important' | 'note'; // Purpose of the highlight
|
||||
* 'data-user-id'?: string; // Unique user identifier
|
||||
@@ -27,6 +35,112 @@ const APPLY_CUT = (_text: string) => ''; // Cut removes the text entirely
|
||||
|
||||
type HighlightTool = 'highlight' | 'strike' | 'strong' | 'cut';
|
||||
|
||||
|
||||
// -- Matcher algorithms --
|
||||
|
||||
/**
|
||||
* Result from text matching: the source substring and the inner text to apply tools to
|
||||
*/
|
||||
interface MatchResult {
|
||||
sourceText: string; // Text in source (may include decorators)
|
||||
selText: string; // Text to apply tool to (decorators stripped)
|
||||
leadingDecorators: string;
|
||||
trailingDecorators: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds text using exact substring matching.
|
||||
*/
|
||||
function findExactMatch(needle: string, haystack: string): MatchResult | null {
|
||||
const firstIndex = haystack.indexOf(needle);
|
||||
if (firstIndex === -1) return null;
|
||||
|
||||
// Ensure uniqueness - only one occurrence
|
||||
if (haystack.indexOf(needle, firstIndex + 1) !== -1) return null;
|
||||
|
||||
return {
|
||||
sourceText: needle,
|
||||
selText: needle,
|
||||
leadingDecorators: '',
|
||||
trailingDecorators: '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds text in source markdown by stripping decorators and tracking positions.
|
||||
* Returns the source substring (including decorators) that renders to the needle text.
|
||||
*/
|
||||
function findInMarkdownSource(needle: string, haystack: string): MatchResult | null {
|
||||
|
||||
// 1. strip markdown decorators while tracking positions
|
||||
let stripped = '';
|
||||
const posMap: number[] = []; // stripped char index -> haystack char index
|
||||
|
||||
let i = 0;
|
||||
while (i < haystack.length) {
|
||||
const char = haystack[i];
|
||||
|
||||
// skip common markdown decorator characters
|
||||
if (char === '*' || char === '_' || char === '~' || char === '`') {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// regular character - track position
|
||||
stripped += char;
|
||||
posMap.push(i);
|
||||
i++;
|
||||
}
|
||||
|
||||
// if the needle is empty after stripping -- nothing we can do here
|
||||
const idx = stripped.indexOf(needle);
|
||||
if (idx === -1) {
|
||||
// not found - need a different approach
|
||||
return null;
|
||||
}
|
||||
|
||||
// ensure uniqueness - only one occurrence
|
||||
if (stripped.indexOf(needle, idx + 1) !== -1) {
|
||||
// multiple occurrences - need a different approach
|
||||
return null;
|
||||
}
|
||||
|
||||
// map back to source positions
|
||||
const startPos = posMap[idx];
|
||||
const endIdx = idx + needle.length - 1;
|
||||
const endPos = endIdx < posMap.length ? posMap[endIdx] + 1 : haystack.length;
|
||||
|
||||
// expand to include surrounding markdown decorators
|
||||
let actualStart = startPos;
|
||||
let actualEnd = endPos;
|
||||
|
||||
// walk backwards to include opening decorators
|
||||
while (actualStart > 0) {
|
||||
const prevChar = haystack[actualStart - 1];
|
||||
if (prevChar === '*' || prevChar === '_' || prevChar === '~' || prevChar === '`')
|
||||
actualStart--;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
// walk forwards to include closing decorators
|
||||
while (actualEnd < haystack.length) {
|
||||
const nextChar = haystack[actualEnd];
|
||||
if (nextChar === '*' || nextChar === '_' || nextChar === '~' || nextChar === '`')
|
||||
actualEnd++;
|
||||
else
|
||||
break;
|
||||
}
|
||||
|
||||
const sourceText = haystack.substring(actualStart, actualEnd);
|
||||
const leadingDecorators = sourceText.match(/^[*_~`]+/)?.[0] || '';
|
||||
const trailingDecorators = sourceText.match(/[*_~`]+$/)?.[0] || '';
|
||||
const selText = sourceText.slice(leadingDecorators.length, sourceText.length - trailingDecorators.length);
|
||||
|
||||
return { sourceText, selText, leadingDecorators, trailingDecorators };
|
||||
}
|
||||
|
||||
|
||||
export function useSelHighlighterMemo(
|
||||
messageId: DMessageId,
|
||||
selText: string | null,
|
||||
@@ -44,31 +158,35 @@ export function useSelHighlighterMemo(
|
||||
const highlightFunction = fragments.reduce((acc: false /* not found */ | ((tool: HighlightTool) => void) | true /* more than one */, fragment) => {
|
||||
if (!acc && isTextContentFragment(fragment)) {
|
||||
const fragmentText = fragment.part.text;
|
||||
let index = fragmentText.indexOf(selText);
|
||||
const match = MATCH_METHOD === 'md-approx'
|
||||
? findInMarkdownSource(selText, fragmentText)
|
||||
: findExactMatch(selText, fragmentText);
|
||||
|
||||
while (index !== -1) {
|
||||
|
||||
// If we've found more than one occurrence, we can stop
|
||||
if (match) {
|
||||
// If we already found one, this is a duplicate
|
||||
if (acc) return true;
|
||||
|
||||
index = fragmentText.indexOf(selText, index + 1);
|
||||
const { sourceText, selText, leadingDecorators, trailingDecorators } = match;
|
||||
|
||||
// Tool application function
|
||||
acc = (tool: HighlightTool) => {
|
||||
|
||||
// Apply the tool
|
||||
const highlighted =
|
||||
// Apply the tool to the inner text
|
||||
const selProcessed =
|
||||
tool === 'highlight' ? APPLY_HTML_HIGHLIGHT(selText)
|
||||
: tool === 'strike' ? APPLY_HTML_STRIKE(selText)
|
||||
: tool === 'strong' ? APPLY_MD_STRONG(selText)
|
||||
: tool === 'cut' ? APPLY_CUT(selText)
|
||||
: selText;
|
||||
|
||||
// Reconstruct with original decorators
|
||||
const reconstructed = leadingDecorators + selProcessed + trailingDecorators;
|
||||
|
||||
// Toggle, if the tooled text is already present (except for cut which always removes)
|
||||
const newFragmentText =
|
||||
tool === 'cut' ? fragmentText.replace(selText, highlighted) // Cut always removes text
|
||||
: fragmentText.includes(highlighted) ? fragmentText.replace(highlighted, selText) // toggles selection
|
||||
: fragmentText.replace(selText, highlighted);
|
||||
tool === 'cut' ? fragmentText.replace(sourceText, reconstructed) // Cut always removes text
|
||||
: fragmentText.includes(reconstructed) ? fragmentText.replace(reconstructed, sourceText) // toggles selection
|
||||
: fragmentText.replace(sourceText, reconstructed);
|
||||
|
||||
// Replace the whole fragment within the message
|
||||
onMessageFragmentReplace(messageId, fragment.fId, createTextContentFragment(newFragmentText));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AixChatGenerateContent_DMessage, aixChatGenerateContent_DMessage_FromConversation } from '~/modules/aix/client/aix.client';
|
||||
import { AixChatGenerateContent_DMessageGuts, aixChatGenerateContent_DMessage_FromConversation } from '~/modules/aix/client/aix.client';
|
||||
import { autoChatFollowUps } from '~/modules/aifn/auto-chat-follow-ups/autoChatFollowUps';
|
||||
import { autoConversationTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
|
||||
@@ -19,7 +19,7 @@ export const CHATGENERATE_RESPONSE_PLACEHOLDER = '...'; // 💫 ..., 🖊️ ...
|
||||
|
||||
|
||||
export interface PersonaProcessorInterface {
|
||||
handleMessage(accumulatedMessage: AixChatGenerateContent_DMessage, messageComplete: boolean): void;
|
||||
handleMessage(accumulatedMessage: AixChatGenerateContent_DMessageGuts, messageComplete: boolean): void;
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export async function runPersonaOnConversationHead(
|
||||
'conversation',
|
||||
conversationId,
|
||||
{ abortSignal: abortController.signal, throttleParallelThreads: parallelViewCount },
|
||||
(messageOverwrite: AixChatGenerateContent_DMessage, messageComplete: boolean) => {
|
||||
(messageOverwrite: AixChatGenerateContent_DMessageGuts, messageComplete: boolean) => {
|
||||
|
||||
// Note: there was an abort check here, but it removed the last packet, which contained the cause and final text.
|
||||
// if (abortController.signal.aborted)
|
||||
|
||||
@@ -2,7 +2,7 @@ import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
|
||||
import { isTextContentFragment } from '~/common/stores/chat/chat.fragments';
|
||||
|
||||
import type { AixChatGenerateContent_DMessage } from '~/modules/aix/client/aix.client';
|
||||
import type { AixChatGenerateContent_DMessageGuts } from '~/modules/aix/client/aix.client';
|
||||
|
||||
import type { PersonaProcessorInterface } from '../chat-persona';
|
||||
|
||||
@@ -16,7 +16,7 @@ export class PersonaChatMessageSpeak implements PersonaProcessorInterface {
|
||||
constructor(private autoSpeakType: AutoSpeakType) {
|
||||
}
|
||||
|
||||
handleMessage(accumulatedMessage: Partial<AixChatGenerateContent_DMessage>, messageComplete: boolean) {
|
||||
handleMessage(accumulatedMessage: Partial<AixChatGenerateContent_DMessageGuts>, messageComplete: boolean) {
|
||||
if (this.autoSpeakType === 'off' || this.spokenLine) return;
|
||||
|
||||
// Require a Content.Text first fragment
|
||||
|
||||
@@ -129,6 +129,13 @@ export function AppChatSettingsAI() {
|
||||
</>}
|
||||
/>
|
||||
|
||||
<FormControlDomainModel
|
||||
domainId='imageCaption'
|
||||
title='Vision model'
|
||||
description='Image captioning'
|
||||
tooltip='Vision model used to generate text descriptions of images when the Caption (Text) attachment option is selected.'
|
||||
/>
|
||||
|
||||
{labsDevMode && (
|
||||
<FormControlDomainModel
|
||||
domainId='primaryChat'
|
||||
|
||||
@@ -38,6 +38,7 @@ const shortcutsMd = platformAwareKeystrokes(`
|
||||
| Ctrl + , | ⚙️ Preferences |
|
||||
| Ctrl + Shift + M | 🧠 Models |
|
||||
| Ctrl + Shift + O | 💬 Options (current Chat Model) |
|
||||
| Ctrl + Shift + A | Toggle AI Request Inspector |
|
||||
| Ctrl + Shift + + | Increase Text Size |
|
||||
| Ctrl + Shift + - | Decrease Text Size |
|
||||
| Ctrl + Shift + / | Shortcuts |
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Release = {
|
||||
|
||||
// this is here to trigger revalidation of data, e.g. models refresh
|
||||
Monotonics: {
|
||||
Aix: 37,
|
||||
Aix: 40,
|
||||
NewsVersion: 200,
|
||||
},
|
||||
|
||||
@@ -59,6 +59,14 @@ export const Release = {
|
||||
|
||||
|
||||
export const BaseProduct = {
|
||||
ProductName: 'Big-AGI',
|
||||
ProductURL: 'https://big-agi.com',
|
||||
PrivacyPolicy: 'https://big-agi.com/privacy',
|
||||
TermsOfService: 'https://big-agi.com/terms',
|
||||
// ecosystem
|
||||
DocsBaseSite: 'https://big-agi.com/docs',
|
||||
OpenSupportDiscord: 'https://discord.gg/MkH4qj2Jp9',
|
||||
OpenSourceRepo: 'https://github.com/enricoros/big-agi',
|
||||
ReleaseNotes: '',
|
||||
SupportForm: (_userId?: string) => 'https://github.com/enricoros/big-AGI/issues/new',
|
||||
SupportForm: (_userId?: string) => 'https://github.com/enricoros/big-AGI/issues/new?template=ai-triage.yml',
|
||||
} as const;
|
||||
|
||||
@@ -2,12 +2,13 @@ import type { FileWithHandle } from 'browser-fs-access';
|
||||
|
||||
import { callBrowseFetchPageOrThrow } from '~/modules/browse/browse.client';
|
||||
import { extractYoutubeVideoIDFromURL } from '~/modules/youtube/youtube.utils';
|
||||
import { imageCaptionFromImageOrThrow } from '~/modules/aifn/image-caption/imageCaptionFromImage';
|
||||
import { youTubeGetVideoData } from '~/modules/youtube/useYouTubeTranscript';
|
||||
|
||||
import type { CommonImageMimeTypes } from '~/common/util/imageUtils';
|
||||
import { Is } from '~/common/util/pwaUtils';
|
||||
import { PLATFORM_IMAGE_MIMETYPE } from '~/common/util/imageUtils';
|
||||
import { agiCustomId, agiUuid } from '~/common/util/idUtils';
|
||||
import { convert_Base64DataURL_To_Base64WithMimeType, convert_Base64WithMimeType_To_Blob } from '~/common/util/blobUtils';
|
||||
import { getDomainModelConfiguration } from '~/common/stores/llms/hooks/useModelDomain';
|
||||
import { htmlTableToMarkdown } from '~/common/util/htmlTableToMarkdown';
|
||||
import { humanReadableHyphenated } from '~/common/util/textUtils';
|
||||
import { pdfToImageDataURLs, pdfToText } from '~/common/util/pdfUtils';
|
||||
@@ -21,9 +22,6 @@ import { guessInputContentTypeFromMime, heuristicMimeTypeFixup, mimeTypeIsDocX,
|
||||
import { imageDataToImageAttachmentFragmentViaDBlob } from './attachment.dblobs';
|
||||
|
||||
|
||||
// configuration
|
||||
export const DEFAULT_ADRAFT_IMAGE_MIMETYPE: CommonImageMimeTypes = !Is.Browser.Safari ? 'image/webp' : 'image/jpeg';
|
||||
export const DEFAULT_ADRAFT_IMAGE_QUALITY = 0.96;
|
||||
const PDF_IMAGE_PAGE_SCALE = 1.5;
|
||||
const PDF_IMAGE_QUALITY = 0.5;
|
||||
const ENABLE_TEXT_AND_IMAGES = false; // [PROD] ?
|
||||
@@ -279,11 +277,13 @@ export function attachmentDefineConverters(source: AttachmentDraftSource, input:
|
||||
// Images (Known/Unknown)
|
||||
case input.mimeType.startsWith('image/'):
|
||||
const inputImageMimeSupported = mimeTypeIsSupportedImage(input.mimeType);
|
||||
const visionModelMissing = !getDomainModelConfiguration('imageCaption', true, true);
|
||||
converters.push({ id: 'image-resized-high', name: 'Image (high detail)', disabled: !inputImageMimeSupported });
|
||||
converters.push({ id: 'image-resized-low', name: 'Image (low detail)', disabled: !inputImageMimeSupported });
|
||||
converters.push({ id: 'image-original', name: 'Image (original quality)', disabled: !inputImageMimeSupported });
|
||||
if (!inputImageMimeSupported)
|
||||
converters.push({ id: 'image-to-default', name: `As Image (${DEFAULT_ADRAFT_IMAGE_MIMETYPE})` });
|
||||
converters.push({ id: 'image-to-default', name: `As Image (${PLATFORM_IMAGE_MIMETYPE})` });
|
||||
converters.push({ id: 'image-caption', name: 'Caption (Text)', disabled: visionModelMissing });
|
||||
converters.push({ id: 'unhandled', name: 'No Image' });
|
||||
converters.push({ id: 'image-ocr', name: 'Add Text (OCR)', isCheckbox: true });
|
||||
break;
|
||||
@@ -561,7 +561,7 @@ export async function attachmentPerformConversion(
|
||||
// image converted (potentially unsupported mime)
|
||||
case 'image-to-default':
|
||||
if (!_expectBlob(input.data, 'image-to-default')) return;
|
||||
const imageCastF = await imageDataToImageAttachmentFragmentViaDBlob(input.mimeType, input.data, source, title, caption, DEFAULT_ADRAFT_IMAGE_MIMETYPE, false);
|
||||
const imageCastF = await imageDataToImageAttachmentFragmentViaDBlob(input.mimeType, input.data, source, title, caption, PLATFORM_IMAGE_MIMETYPE, false);
|
||||
if (imageCastF)
|
||||
newFragments.push(imageCastF);
|
||||
break;
|
||||
@@ -590,6 +590,35 @@ export async function attachmentPerformConversion(
|
||||
}
|
||||
break;
|
||||
|
||||
// image to caption
|
||||
case 'image-caption':
|
||||
if (!_expectBlob(input.data, 'Image captioning converter')) break;
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
const captionText = await imageCaptionFromImageOrThrow(
|
||||
input.data,
|
||||
input.mimeType,
|
||||
attachment.id,
|
||||
abortController.signal,
|
||||
progress => edit(attachment.id, { outputsConversionProgress: progress / 100 }),
|
||||
);
|
||||
// if we're here we shall have valid text
|
||||
newFragments.push(createDocAttachmentFragment(
|
||||
title,
|
||||
caption + ' (Caption)',
|
||||
DVMimeType.TextPlain,
|
||||
createDMessageDataInlineText(captionText || 'This image could not be described', 'text/plain'),
|
||||
refString,
|
||||
DOCPART_DEFAULT_VERSION,
|
||||
{ ...docMeta, srcOcrFrom: 'image-caption' },
|
||||
));
|
||||
} catch (error: any) {
|
||||
console.log('[DEV] Failed to caption image:', error);
|
||||
const errorText = `[Captioning failed: ${error?.message || String(error)}]`;
|
||||
newFragments.push(createDocAttachmentFragment(title, caption + ' (Error)', DVMimeType.TextPlain, createDMessageDataInlineText(errorText, 'text/plain'), refString, DOCPART_DEFAULT_VERSION, { ...docMeta, srcOcrFrom: 'image-caption' }));
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
// pdf to text
|
||||
case 'pdf-text':
|
||||
@@ -610,7 +639,7 @@ export async function attachmentPerformConversion(
|
||||
if (!_expectBlob(input.data, 'PDF images converter')) break;
|
||||
// Convert Blob to ArrayBuffer for PDF.js
|
||||
try {
|
||||
const imageDataURLs = await pdfToImageDataURLs(await input.data.arrayBuffer(), DEFAULT_ADRAFT_IMAGE_MIMETYPE, PDF_IMAGE_QUALITY, PDF_IMAGE_PAGE_SCALE, (progress) => {
|
||||
const imageDataURLs = await pdfToImageDataURLs(await input.data.arrayBuffer(), PLATFORM_IMAGE_MIMETYPE, PDF_IMAGE_QUALITY, PDF_IMAGE_PAGE_SCALE, (progress) => {
|
||||
edit(attachment.id, { outputsConversionProgress: progress });
|
||||
});
|
||||
for (const pdfPageImage of imageDataURLs) {
|
||||
@@ -634,7 +663,7 @@ export async function attachmentPerformConversion(
|
||||
|
||||
// duplicated from 'pdf-images' (different progress update)
|
||||
const imageFragments: DMessageAttachmentFragment[] = [];
|
||||
const imageDataURLs = await pdfToImageDataURLs(pdfArrayBufferForImages, DEFAULT_ADRAFT_IMAGE_MIMETYPE, PDF_IMAGE_QUALITY, PDF_IMAGE_PAGE_SCALE, (progress) => {
|
||||
const imageDataURLs = await pdfToImageDataURLs(pdfArrayBufferForImages, PLATFORM_IMAGE_MIMETYPE, PDF_IMAGE_QUALITY, PDF_IMAGE_PAGE_SCALE, (progress) => {
|
||||
edit(attachment.id, { outputsConversionProgress: progress / 2 }); // Update progress (0% to 50%)
|
||||
});
|
||||
for (const pdfPageImage of imageDataURLs) {
|
||||
|
||||
@@ -136,7 +136,7 @@ export type AttachmentDraftConverter = {
|
||||
|
||||
export type AttachmentDraftConverterType =
|
||||
| 'text' | 'rich-text' | 'rich-text-cleaner' | 'rich-text-table'
|
||||
| 'image-original' | 'image-resized-high' | 'image-resized-low' | 'image-ocr' | 'image-to-default'
|
||||
| 'image-original' | 'image-resized-high' | 'image-resized-low' | 'image-ocr' | 'image-caption' | 'image-to-default'
|
||||
| 'pdf-text' | 'pdf-images' | 'pdf-text-and-images'
|
||||
| 'docx-to-html'
|
||||
| 'url-page-text' | 'url-page-markdown' | 'url-page-html' | 'url-page-null' | 'url-page-image'
|
||||
|
||||
+16
-7
@@ -9,10 +9,10 @@ export const hasPostHogAnalytics = !!process.env.NEXT_PUBLIC_POSTHOG_KEY;
|
||||
|
||||
|
||||
// global to survive route changes
|
||||
let _posthog: undefined | PostHog | null = undefined; // underined: not loaded, null: loading or opt-out, PostHog: loaded
|
||||
let _posthog: undefined | PostHog | null = undefined; // undefined: not loaded, null: loading or opt-out, PostHog: loaded
|
||||
|
||||
|
||||
// unused yet
|
||||
// noinspection JSUnusedGlobalSymbols - unused yet
|
||||
export function posthogAnalyticsOptOut() {
|
||||
if (isBrowser) {
|
||||
localStorage.setItem('app-analytics-posthog-optout', 'true');
|
||||
@@ -20,10 +20,10 @@ export function posthogAnalyticsOptOut() {
|
||||
}
|
||||
}
|
||||
|
||||
// unused yet
|
||||
export function posthogCaptureEvent(eventName: string, properties?: Properties) {
|
||||
export function posthogCaptureEvent(eventName: string, properties?: Properties, options?: { sendInstantly?: boolean }) {
|
||||
if (isBrowser && hasPostHogAnalytics) {
|
||||
_posthog?.capture(eventName, properties);
|
||||
// For events before navigation (e.g., login button clicks), send immediately
|
||||
_posthog?.capture(eventName, properties, options?.sendInstantly ? { send_instantly: true } : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,9 @@ export function posthogCaptureException(error: Error | unknown, additionalProper
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// unused yet
|
||||
/**
|
||||
* Posthog Identify - Login
|
||||
*/
|
||||
export function posthogUser(userId: string, userProperties?: Record<string, any>) {
|
||||
if (isBrowser && hasPostHogAnalytics) {
|
||||
_posthog?.identify(userId, {
|
||||
@@ -45,6 +46,14 @@ export function posthogUser(userId: string, userProperties?: Record<string, any>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Posthog Reset - Logout
|
||||
*/
|
||||
export function posthogReset() {
|
||||
if (isBrowser && hasPostHogAnalytics)
|
||||
_posthog?.reset();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* PostHog Analytics implementation - with dynamic loading
|
||||
|
||||
@@ -55,6 +55,7 @@ export function DataStreamViz(props: { height: number, speed?: number }) {
|
||||
const tokensRef = React.useRef<Token[]>([]);
|
||||
const lastTimeRef = React.useRef<number>(0);
|
||||
const lastTokenTimeRef = React.useRef<number>(0);
|
||||
const isVisibleRef = React.useRef<boolean>(true);
|
||||
|
||||
// derived
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
@@ -162,6 +163,9 @@ export function DataStreamViz(props: { height: number, speed?: number }) {
|
||||
|
||||
// Animation function
|
||||
const animate = React.useCallback((currentTime: number) => {
|
||||
// early exit if not visible or no animation ID (component unmounting)
|
||||
if (!isVisibleRef.current || !animationRef.current) return;
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
@@ -199,10 +203,46 @@ export function DataStreamViz(props: { height: number, speed?: number }) {
|
||||
}
|
||||
|
||||
lastTimeRef.current = currentTime;
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
|
||||
// only schedule next frame if still visible
|
||||
if (isVisibleRef.current)
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
}, [createToken, drawGrid, drawToken]);
|
||||
|
||||
|
||||
// [effect] Detect visibility
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
|
||||
const observer = new IntersectionObserver(([entry]) => {
|
||||
const visible = entry.isIntersecting;
|
||||
isVisibleRef.current = visible;
|
||||
|
||||
if (visible) {
|
||||
// restart animation when becoming visible (cancel any existing first)
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
} else {
|
||||
// stop animation and clear memory when going off-screen
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
animationRef.current = 0;
|
||||
tokensRef.current = [];
|
||||
lastTimeRef.current = 0;
|
||||
lastTokenTimeRef.current = 0;
|
||||
}
|
||||
}, {
|
||||
threshold: 0.1, // Trigger when at least 10% visible
|
||||
rootMargin: '50px', // Start animating slightly before entering viewport
|
||||
});
|
||||
|
||||
observer.observe(container);
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, [animate]);
|
||||
|
||||
// Canvas setup and animation effect
|
||||
React.useEffect(() => {
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
@@ -216,11 +256,14 @@ export function DataStreamViz(props: { height: number, speed?: number }) {
|
||||
handleResize();
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// start initial animation (cancel any existing first to prevent duplicate loops)
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
animationRef.current = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
cancelAnimationFrame(animationRef.current);
|
||||
animationRef.current = 0; // Prevent RAF callbacks after unmount
|
||||
};
|
||||
}, [animate, props.height, setupCanvas]);
|
||||
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import { InlineTextarea } from './InlineTextarea';
|
||||
|
||||
|
||||
/**
|
||||
* Displays text and switches to edit mode on click
|
||||
*/
|
||||
export function InlineTextareaEditable(props: {
|
||||
value: string;
|
||||
onSave: (newValue: string) => void;
|
||||
renderDisplay: (onClickEdit: (event: React.MouseEvent) => void) => React.ReactNode;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
textareaSx?: SxProps;
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
const valueRef = React.useRef(props.value);
|
||||
valueRef.current = props.value;
|
||||
|
||||
|
||||
// handlers
|
||||
|
||||
const { onSave } = props;
|
||||
|
||||
const handleBeginEditing = React.useCallback((event: React.MouseEvent) => {
|
||||
if (props.disabled) return;
|
||||
if (event.shiftKey) return; // Reserved for debug/inspect
|
||||
setIsEditing(true);
|
||||
}, [props.disabled]);
|
||||
|
||||
const handleSave = React.useCallback((newValue: string) => {
|
||||
setIsEditing(false);
|
||||
const trimmed = newValue.trim();
|
||||
if (!trimmed || trimmed === valueRef.current) return;
|
||||
onSave(trimmed);
|
||||
}, [onSave]);
|
||||
|
||||
const handleCancel = React.useCallback(() => {
|
||||
setIsEditing(false);
|
||||
}, []);
|
||||
|
||||
|
||||
// render
|
||||
|
||||
return !isEditing ? props.renderDisplay(handleBeginEditing) : (
|
||||
<InlineTextarea
|
||||
initialText={props.value}
|
||||
placeholder={props.placeholder}
|
||||
onEdit={handleSave}
|
||||
onCancel={handleCancel}
|
||||
sx={props.textareaSx}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -32,12 +32,17 @@ export const StarredNoXL2 = React.memo(function StarredNoXL2() {
|
||||
return <StarIcon sx={starIconStyles.starredNoXl2} />;
|
||||
});
|
||||
|
||||
export const UnStarred = React.memo(function UnStarred() {
|
||||
const UnStarredNoXL2 = React.memo(function UnStarred() {
|
||||
return <StarBorderIcon />;
|
||||
})
|
||||
|
||||
export const StarredState = React.memo(function StarredState({ isStarred }: { isStarred: boolean }) {
|
||||
return isStarred ? <Starred /> : <UnStarred />;
|
||||
return isStarred ? <Starred /> : <UnStarredNoXL2 />;
|
||||
});
|
||||
|
||||
// have an unstyled that just returns StarIcon or StarBorderIcon and we can use with our own styles and props {...}
|
||||
export const StarIconUnstyled = React.memo(function StarIconUnstyled({ isStarred }: { isStarred: boolean }) {
|
||||
return isStarred ? <StarIcon /> : <StarBorderIcon />;
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ const slotPropsInputSx: InputSlotsAndSlotProps['slotProps'] = {
|
||||
input: {
|
||||
sx: {
|
||||
width: '100%',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ export function FormInputKey(props: {
|
||||
|
||||
const endDecorator = React.useMemo(() => !!props.value && !props.noKey && (
|
||||
<IconButton onClick={() => setIsVisible(!isVisible)}>
|
||||
{isVisible ? <VisibilityIcon sx={{ fontSize: 'lg'}} /> : <VisibilityOffIcon sx={{ fontSize: 'md' }} />}
|
||||
{isVisible ? <VisibilityIcon sx={{ fontSize: 'lg' }} /> : <VisibilityOffIcon sx={{ fontSize: 'md' }} />}
|
||||
</IconButton>
|
||||
), [props.value, props.noKey, isVisible]);
|
||||
|
||||
@@ -78,7 +78,7 @@ export function FormInputKey(props: {
|
||||
placeholder={props.required ? props.placeholder ? 'required: ' + props.placeholder : 'required' : props.placeholder || '...'}
|
||||
type={(isVisible || !!props.noKey) ? 'text' : 'password'}
|
||||
error={props.isError}
|
||||
startDecorator={!props.noKey && <KeyIcon sx={{ fontSize: 'lg' }} />}
|
||||
startDecorator={!props.noKey && <KeyIcon sx={{ fontSize: 'md' }} />}
|
||||
endDecorator={endDecorator}
|
||||
slotProps={slotPropsInputSx}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FormSwitchControl } from './FormSwitchControl';
|
||||
|
||||
|
||||
/**
|
||||
* Reusable toggle for enabling client-side API fetch.
|
||||
* Appears with animation when client key is present.
|
||||
*/
|
||||
export function SetupFormClientSideToggle(props: {
|
||||
visible: boolean;
|
||||
checked: boolean;
|
||||
onChange: (on: boolean) => void;
|
||||
helpText: string;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateRows: props.visible ? '1fr' : '0fr',
|
||||
transition: 'grid-template-rows 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
}}
|
||||
>
|
||||
<div style={{ overflow: 'hidden' }}>
|
||||
<FormSwitchControl
|
||||
title='Direct Connection'
|
||||
description={props.checked ? 'Connect from browser' : 'Via server (default)'}
|
||||
tooltip={props.helpText}
|
||||
checked={props.checked}
|
||||
onChange={props.onChange}
|
||||
disabled={props.disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Chip, ColorPaletteProp, FormControl, IconButton, ListDivider, ListItemDecorator, Option, optionClasses, Select, SelectSlotsAndSlotProps, SvgIconProps, VariantProp } from '@mui/joy';
|
||||
import { Box, Chip, ColorPaletteProp, FormControl, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Option, Select, SelectSlotsAndSlotProps, SvgIconProps, VariantProp, optionClasses } from '@mui/joy';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import BuildCircleIcon from '@mui/icons-material/BuildCircle';
|
||||
@@ -11,12 +11,13 @@ import { findModelVendor } from '~/modules/llms/vendors/vendors.registry';
|
||||
import { llmsGetVendorIcon, LLMVendorIcon } from '~/modules/llms/components/LLMVendorIcon';
|
||||
|
||||
import type { DModelDomainId } from '~/common/stores/llms/model.domains.types';
|
||||
import { DLLM, DLLMId, LLM_IF_OAI_Reasoning, LLM_IF_Outputs_Audio, LLM_IF_Outputs_Image, LLM_IF_Tools_WebSearch } from '~/common/stores/llms/llms.types';
|
||||
import { DLLM, DLLMId, getLLMPricing, LLM_IF_OAI_Reasoning, LLM_IF_Outputs_Audio, LLM_IF_Outputs_Image, LLM_IF_Tools_WebSearch } from '~/common/stores/llms/llms.types';
|
||||
import { PhGearSixIcon } from '~/common/components/icons/phosphor/PhGearSixIcon';
|
||||
import { StarredNoXL2 } from '~/common/components/StarIcons';
|
||||
import { StarIconUnstyled, StarredNoXL2 } from '~/common/components/StarIcons';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { getChatLLMId, llmsStoreActions } from '~/common/stores/llms/store-llms';
|
||||
import { optimaActions, optimaOpenModels } from '~/common/layout/optima/useOptima';
|
||||
import { useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
import { useVisibleLLMs } from '~/common/stores/llms/llms.hooks';
|
||||
|
||||
import { FormLabelStart } from './FormLabelStart';
|
||||
@@ -58,6 +59,16 @@ const _styles = {
|
||||
backgroundColor: 'background.popup',
|
||||
boxShadow: 'xs',
|
||||
},
|
||||
listFooter: {
|
||||
// '--ListItem-minHeight': '2.25rem',
|
||||
borderTop: '1px solid',
|
||||
borderTopColor: 'divider',
|
||||
// pb: 0,
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
backgroundColor: 'background.surface',
|
||||
zIndex: 1,
|
||||
},
|
||||
listVendor: {
|
||||
// see OptimaBarDropdown's _styles.separator
|
||||
fontSize: 'sm',
|
||||
@@ -130,6 +141,7 @@ interface LLMSelectOptions {
|
||||
isHorizontal?: boolean;
|
||||
autoRefreshDomain?: DModelDomainId;
|
||||
appendConfigureModels?: boolean; // appends a bottom option to open the Models panel
|
||||
showStarFilter?: boolean; // show a button to filter starred models only
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -145,14 +157,18 @@ export function useLLMSelect(
|
||||
options: LLMSelectOptions,
|
||||
): [DLLM | null, React.JSX.Element | null, React.FunctionComponent<SvgIconProps> | undefined] {
|
||||
|
||||
// options
|
||||
const { label, larger = false, disabled = false, placeholder = LLM_TEXT_PLACEHOLDER, isHorizontal = false, autoRefreshDomain, appendConfigureModels = false, showStarFilter = false } = options;
|
||||
|
||||
// state
|
||||
const [controlledOpen, setControlledOpen] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const _filteredLLMs = useVisibleLLMs(llmId);
|
||||
const starredOnly = useUIPreferencesStore(state => showStarFilter && state.showModelsStarredOnly);
|
||||
// const modelsStarredOnTop = useUIPreferencesStore(state => state.modelsStarredOnTop); // unsupported, this creates some issues with groups I believe
|
||||
const { llms: _filteredLLMs, hasStarred } = useVisibleLLMs(llmId, starredOnly, false);
|
||||
|
||||
// derived state
|
||||
const { label, larger = false, disabled = false, placeholder = LLM_TEXT_PLACEHOLDER, isHorizontal = false, autoRefreshDomain, appendConfigureModels = false } = options;
|
||||
const noIcons = false; //smaller;
|
||||
const llm = !llmId ? null : _filteredLLMs.find(llm => llm.id === llmId) ?? null;
|
||||
const isReasoning = !LLM_SELECT_SHOW_REASONING_ICON ? false : llm?.interfaces?.includes(LLM_IF_OAI_Reasoning) ?? false;
|
||||
@@ -182,7 +198,7 @@ export function useLLMSelect(
|
||||
|
||||
let features = '';
|
||||
const isNotSymlink = !llm.label.startsWith('🔗');
|
||||
const seemsFree = !!llm.pricing?.chat?._isFree;
|
||||
const seemsFree = !!getLLMPricing(llm)?.chat?._isFree;
|
||||
if (isNotSymlink) {
|
||||
// check features
|
||||
if (seemsFree) features += 'free ';
|
||||
@@ -209,7 +225,7 @@ export function useLLMSelect(
|
||||
>
|
||||
{!noIcons && (
|
||||
<ListItemDecorator>
|
||||
{llm.userStarred ? <StarredNoXL2 /> : vendor?.id ? <LLMVendorIcon vendorId={vendor.id} /> : null}
|
||||
{(llm.userStarred && !starredOnly) ? <StarredNoXL2 /> : vendor?.id ? <LLMVendorIcon vendorId={vendor.id} /> : null}
|
||||
</ListItemDecorator>
|
||||
)}
|
||||
{/*<Tooltip title={llm.description}>*/}
|
||||
@@ -244,7 +260,7 @@ export function useLLMSelect(
|
||||
|
||||
return acc;
|
||||
}, [] as React.JSX.Element[]);
|
||||
}, [_filteredLLMs, llmId, noIcons, optimizeToSingleVisibleId]);
|
||||
}, [_filteredLLMs, llmId, noIcons, optimizeToSingleVisibleId, starredOnly]);
|
||||
|
||||
|
||||
const onSelectChange = React.useCallback((_event: unknown, value: DLLMId | null) => {
|
||||
@@ -297,10 +313,26 @@ export function useLLMSelect(
|
||||
</Option>
|
||||
)}
|
||||
|
||||
{/* Star Filter Toggle - shown at the top of the list only if visible */}
|
||||
{showStarFilter && hasStarred && !optimizeToSingleVisibleId && (
|
||||
<ListItem key='star-filter-toggle' sx={_styles.listFooter}>
|
||||
<ListItemButton
|
||||
variant={starredOnly ? 'soft' : 'plain'}
|
||||
onClick={useUIPreferencesStore.getState().toggleShowModelsStarredOnly}
|
||||
// sx={{ backgroundColor: 'background.surface', position: 'sticky', top: 0, zIndex: 1 }}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<StarIconUnstyled isStarred={starredOnly} />
|
||||
</ListItemDecorator>
|
||||
{starredOnly ? 'Showing: Starred' : 'Showing: All'}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
</Select>
|
||||
{/*</Box>*/}
|
||||
</FormControl>
|
||||
), [appendConfigureModels, autoRefreshDomain, controlledOpen, disabled, hasNoModels, isHorizontal, isReasoning, label, larger, llmId, onSelectChange, optimizeToSingleVisibleId, options.color, options.sx, options.variant, optionsArray, placeholder, showNoOptions]);
|
||||
), [appendConfigureModels, autoRefreshDomain, controlledOpen, disabled, hasNoModels, hasStarred, isHorizontal, isReasoning, label, larger, llmId, onSelectChange, optimizeToSingleVisibleId, options.color, options.sx, options.variant, optionsArray, placeholder, showNoOptions, showStarFilter, starredOnly]);
|
||||
|
||||
// Memo the vendor icon for the chat LLM
|
||||
const chatLLMVendorIconFC = React.useMemo(() => {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - gift
|
||||
*/
|
||||
export function PhGift(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M216,72H180.92c.39-.33.79-.65,1.17-1A29.53,29.53,0,0,0,192,49.57,32.62,32.62,0,0,0,158.44,16,29.53,29.53,0,0,0,137,25.91a54.94,54.94,0,0,0-9,14.48,54.94,54.94,0,0,0-9-14.48A29.53,29.53,0,0,0,97.56,16,32.62,32.62,0,0,0,64,49.57,29.53,29.53,0,0,0,73.91,71c.38.33.78.65,1.17,1H40A16,16,0,0,0,24,88v32a16,16,0,0,0,16,16v64a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V136a16,16,0,0,0,16-16V88A16,16,0,0,0,216,72ZM149,36.51a13.69,13.69,0,0,1,10-4.5h.49A16.62,16.62,0,0,1,176,49.08a13.69,13.69,0,0,1-4.5,10c-9.49,8.4-25.24,11.36-35,12.4C137.7,60.89,141,45.5,149,36.51Zm-64.09.36A16.63,16.63,0,0,1,96.59,32h.49a13.69,13.69,0,0,1,10,4.5c8.39,9.48,11.35,25.2,12.39,34.92-9.72-1-25.44-4-34.92-12.39a13.69,13.69,0,0,1-4.5-10A16.6,16.6,0,0,1,84.87,36.87ZM40,88h80v32H40Zm16,48h64v64H56Zm144,64H136V136h64Zm16-80H136V88h80v32Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - key
|
||||
*/
|
||||
export function PhKey(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M216.57,39.43A80,80,0,0,0,83.91,120.78L28.69,176A15.86,15.86,0,0,0,24,187.31V216a16,16,0,0,0,16,16H72a8,8,0,0,0,8-8V208H96a8,8,0,0,0,8-8V184h16a8,8,0,0,0,5.66-2.34l9.56-9.57A79.73,79.73,0,0,0,160,176h.1A80,80,0,0,0,216.57,39.43ZM224,98.1c-1.09,34.09-29.75,61.86-63.89,61.9H160a63.7,63.7,0,0,1-23.65-4.51,8,8,0,0,0-8.84,1.68L116.69,168H96a8,8,0,0,0-8,8v16H72a8,8,0,0,0-8,8v16H40V187.31l58.83-58.82a8,8,0,0,0,1.68-8.84A63.72,63.72,0,0,1,96,95.92c0-34.14,27.81-62.8,61.9-63.89A64,64,0,0,1,224,98.1ZM192,76a12,12,0,1,1-12-12A12,12,0,0,1,192,76Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - megaphone
|
||||
*/
|
||||
export function PhMegaphone(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M248,120a48.05,48.05,0,0,0-48-48H160.2c-2.91-.17-53.62-3.74-101.91-44.24A16,16,0,0,0,32,40V200a16,16,0,0,0,26.29,12.25c37.77-31.68,77-40.76,93.71-43.3v31.72A16,16,0,0,0,159.12,214l11,7.33A16,16,0,0,0,194.5,212l11.77-44.36A48.07,48.07,0,0,0,248,120ZM48,199.93V40h0c42.81,35.91,86.63,45,104,47.24v65.48C134.65,155,90.84,164.07,48,199.93Zm131,8,0,.11-11-7.33V168h21.6ZM200,152H168V88h32a32,32,0,1,1,0,64Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - pencil-simple
|
||||
*/
|
||||
export function PhPencilSimple(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M227.31,73.37,182.63,28.68a16,16,0,0,0-22.63,0L36.69,152A15.86,15.86,0,0,0,32,163.31V208a16,16,0,0,0,16,16H92.69A15.86,15.86,0,0,0,104,219.31L227.31,96a16,16,0,0,0,0-22.63ZM92.69,208H48V163.31l88-88L180.69,120ZM192,108.68,147.31,64l24-24L216,84.68Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - terminal
|
||||
*/
|
||||
export function PhTerminal(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M117.31,134l-72,64a8,8,0,1,1-10.63-12L100,128,34.69,70A8,8,0,1,1,45.32,58l72,64a8,8,0,0,1,0,12ZM216,184H120a8,8,0,0,0,0,16h96a8,8,0,0,0,0-16Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
// from https://platform.moonshot.ai/lightmode.svg - 2025-11-09
|
||||
export function MoonshotIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 465 470' width='24' height='24' strokeWidth={0} stroke='none' fill='currentColor' strokeLinecap='butt' strokeLinejoin='miter' {...props}>
|
||||
<path fillRule='evenodd' clipRule='evenodd'
|
||||
d='M418.766 93.78C388.626 53.17 345.086 22.07 292.446 7.97001C239.806 -6.12999 186.536 -0.969994 140.136 19.13L418.766 93.78ZM40.7758 100.85C61.3758 70.84 88.5858 46.43 119.776 29.15L325.036 84.15C311.566 91.92 297.136 105.31 285.966 120.03L454.736 165.25C459.526 181.08 462.646 197.49 463.956 214.24L40.7758 100.85ZM456.686 292.45C454.306 301.35 451.436 309.99 448.116 318.34L2.44584 198.93C3.74584 190.03 5.58584 181.12 7.96584 172.22C13.5658 151.31 21.8458 131.84 32.3458 114.08L250.436 172.52C241.706 185 233.756 198.56 226.756 213.07L460.516 275.71C459.446 281.3 458.166 286.88 456.676 292.47L456.686 292.45ZM13.4858 310.23C2.67584 279.96 -1.93416 247.22 0.745843 213.95L206.426 269.06C204.946 273.74 203.556 278.48 202.266 283.29C199.646 293.08 197.506 302.85 195.846 312.55L417.946 372.06C407.606 385.79 395.826 398.23 382.896 409.21L13.4858 310.23ZM172.206 456.69C102.096 437.9 48.1158 388.98 20.4758 327.59L187.846 372.44C187.576 388.05 188.546 403.26 190.676 417.85L312.286 450.43C268.876 466.39 220.286 469.57 172.206 456.69Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { hasKeys } from '~/common/util/objectUtils';
|
||||
|
||||
import type { ShortcutObject } from './useGlobalShortcuts';
|
||||
|
||||
|
||||
@@ -32,7 +34,7 @@ export const useGlobalShortcutsStore = create<ShortcutsStore>((set, get) => ({
|
||||
const { [groupId]: _, ...rest } = state.shortcutGroups;
|
||||
return {
|
||||
shortcutGroups: rest,
|
||||
hasShortcuts: Object.keys(rest).length > 0,
|
||||
hasShortcuts: hasKeys(rest),
|
||||
};
|
||||
}),
|
||||
|
||||
|
||||
@@ -158,15 +158,23 @@ export class WebSpeechApiEngine implements IRecognitionEngine {
|
||||
let chunk = result[0]?.transcript?.trim();
|
||||
if (!chunk) continue;
|
||||
|
||||
/**
|
||||
* Android Chrome bug tentative fix.
|
||||
* On Android, interim results are incorrectly marked as isFinal, causing words to accumulate and duplicate.
|
||||
* Use confidence attribute to validate truly final results (expected that
|
||||
* Interim results have confidence = 0, while truly final results have confidence > 0.
|
||||
*/
|
||||
const isTrulyFinal = result.isFinal && (result[0]?.confidence === undefined || result[0].confidence > 0);
|
||||
|
||||
// Capitalize
|
||||
if (chunk.length >= 2 && (result.isFinal || !this.results.interimTranscript))
|
||||
if (chunk.length >= 2 && (isTrulyFinal || !this.results.interimTranscript))
|
||||
chunk = chunk.charAt(0).toUpperCase() + chunk.slice(1);
|
||||
|
||||
// Punctuate
|
||||
if (result.isFinal && !/[.!?;:,\s]$/.test(chunk))
|
||||
if (isTrulyFinal && !/[.!?;:,\s]$/.test(chunk))
|
||||
chunk += '.';
|
||||
|
||||
if (result.isFinal)
|
||||
if (isTrulyFinal)
|
||||
this.results.transcript = _chunkExpressionReplaceEN(this.results.transcript + chunk + ' ');
|
||||
else
|
||||
this.results.interimTranscript = _chunkExpressionReplaceEN(this.results.interimTranscript + chunk + ' ');
|
||||
|
||||
@@ -9,6 +9,9 @@
|
||||
* - tba
|
||||
*/
|
||||
|
||||
import type { ModelVendorId } from '~/modules/llms/vendors/vendors.registry';
|
||||
import type { DModelsServiceId } from '~/common/stores/llms/llms.service.types';
|
||||
|
||||
|
||||
/// Speech Recognition (browser)
|
||||
|
||||
@@ -37,7 +40,9 @@ export { useCapability as useCapabilityElevenLabs } from '~/modules/elevenlabs/e
|
||||
|
||||
export interface TextToImageProvider {
|
||||
providerId: string; // unique ID of this provider, used for selecting in a list (e.g. 'openai-2' or 'localai')
|
||||
modelServiceId?: DModelsServiceId; // if auto-created from a model service, the service ID
|
||||
vendor: TextToImageVendor;
|
||||
priority: number; // lower is higher priority
|
||||
// UI attributes
|
||||
label: string; // e.g. 'OpenAI #2'
|
||||
painter: string; // e.g. 'GPT Image', 'DALL·E', 'Grok', ...
|
||||
@@ -45,13 +50,7 @@ export interface TextToImageProvider {
|
||||
configured: boolean;
|
||||
}
|
||||
|
||||
type TextToImageVendor =
|
||||
| 'gemini'
|
||||
| 'localai'
|
||||
| 'openai'
|
||||
| 'xai'
|
||||
;
|
||||
|
||||
type TextToImageVendor = Extract<ModelVendorId, 'azure' | 'openai' | 'localai' | 'googleai' | 'xai'>;
|
||||
|
||||
export interface CapabilityTextToImage {
|
||||
mayWork: boolean;
|
||||
|
||||
@@ -67,7 +67,7 @@ export function OptimaLayout(props: { suspendAutoModelsSetup?: boolean, children
|
||||
{ key: ',', ctrl: true, action: optimaOpenPreferences },
|
||||
{ key: 'm', ctrl: true, shift: true, action: optimaOpenModels },
|
||||
{ key: 'g', ctrl: true, shift: true, action: optimaActions().openLogger },
|
||||
{ key: 'a', ctrl: true, shift: true, action: optimaActions().openAIXDebugger },
|
||||
{ key: 'a', ctrl: true, shift: true, action: optimaActions().toggleAIXDebugger },
|
||||
// Font Scale
|
||||
{ key: '+', ctrl: true, shift: true, action: useUIPreferencesStore.getState().increaseContentScaling },
|
||||
{ key: '-', ctrl: true, shift: true, action: useUIPreferencesStore.getState().decreaseContentScaling },
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import Router from 'next/router';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Divider, Dropdown, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
|
||||
import { Divider, Dropdown, FormHelperText, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
|
||||
import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import HistoryIcon from '@mui/icons-material/History';
|
||||
@@ -222,9 +222,13 @@ export function DesktopNav(props: { component: React.ElementType, currentApp?: N
|
||||
Support
|
||||
</Typography>
|
||||
</ListItem>
|
||||
<MenuItem component="a" href={BaseProduct.SupportForm()} target="_blank">
|
||||
<ListItemDecorator><LightbulbOutlinedIcon /></ListItemDecorator>
|
||||
I Have Feedback
|
||||
<MenuItem component='a' href={BaseProduct.SupportForm()} target='_blank'>
|
||||
<ListItemDecorator>🔥</ListItemDecorator>
|
||||
<div>
|
||||
Improve Big-AGI
|
||||
<FormHelperText>AI fixes what you report</FormHelperText>
|
||||
</div>
|
||||
<ArrowOutwardRoundedIcon sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
{!!releaseNotesUrl && (
|
||||
<MenuItem onClick={handleShowReleaseNotes}>
|
||||
|
||||
@@ -93,6 +93,7 @@ export interface OptimaActions {
|
||||
|
||||
closeAIXDebugger: () => void;
|
||||
openAIXDebugger: () => void;
|
||||
toggleAIXDebugger: () => void;
|
||||
|
||||
closeKeyboardShortcuts: () => void;
|
||||
openKeyboardShortcuts: () => void;
|
||||
@@ -195,6 +196,11 @@ export const useLayoutOptimaStore = create<OptimaState & OptimaActions>((_set, _
|
||||
|
||||
closeAIXDebugger: () => _set({ showAIXDebugger: false }),
|
||||
openAIXDebugger: () => _set({ ...modalsClosedState, showAIXDebugger: true }),
|
||||
toggleAIXDebugger: () => _set((state) =>
|
||||
state.showAIXDebugger
|
||||
? { showAIXDebugger: false }
|
||||
: { ...modalsClosedState, showAIXDebugger: true }
|
||||
),
|
||||
|
||||
closeKeyboardShortcuts: () => _set({ showKeyboardShortcuts: false }),
|
||||
openKeyboardShortcuts: () => _set({ showKeyboardShortcuts: true }),
|
||||
|
||||
@@ -22,6 +22,7 @@ export type GlobalOverlayId = // string - disabled so we keep an orderliness
|
||||
| 'chat-delete-confirmation'
|
||||
| 'chat-reset-confirmation'
|
||||
| 'chat-message-delete-confirmation'
|
||||
| 'chat-message-delete-aux'
|
||||
| 'chat-message-inline-aux'
|
||||
| 'livefile-overwrite'
|
||||
| 'shortcuts-confirm-close'
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function reconfigureBackendModels(lastLlmReconfigHash: string, setL
|
||||
})
|
||||
.catch(error => {
|
||||
// catches errors and logs them, but does not stop the chain
|
||||
console.error('Auto-configuration failed for service:', service.label, error);
|
||||
console.warn('Auto-configuration failed for service:', service.label, error);
|
||||
})
|
||||
.then(() => {
|
||||
// short delay between vendors
|
||||
|
||||
@@ -33,7 +33,7 @@ export type DMessageContentFragment = _DMessageFragmentWrapper<'content',
|
||||
| DMessageTextPart // plain text or mixed content -> BlockRenderer
|
||||
| DMessageReferencePart // reference (e.g. zync entity) Content, such as a Asset (image, audio, PFD, etc.), chat, persona, etc.
|
||||
| DMessageImageRefPart // large image
|
||||
| DMessageToolInvocationPart // shown to dev only, singature of the llm function call
|
||||
| DMessageToolInvocationPart // shown to dev only, signature of the llm function call
|
||||
| DMessageToolResponsePart // shown to dev only, response of the llm
|
||||
| DMessageErrorPart // red message, e.g. non-content application issues
|
||||
| _SentinelPart
|
||||
@@ -66,6 +66,11 @@ export type DMessageVoidFragment = _DMessageFragmentWrapper<'void',
|
||||
| _SentinelPart
|
||||
>;
|
||||
|
||||
export type DVoidFragmentModelAnnotations = _NarrowFragmentToPart<DMessageVoidFragment, DVoidModelAnnotationsPart>;
|
||||
type _DVoidFragmentModelAux = _NarrowFragmentToPart<DMessageVoidFragment, DVoidModelAuxPart>;
|
||||
type _DVoidFragmentPlaceholder = _NarrowFragmentToPart<DMessageVoidFragment, DVoidPlaceholderPart>;
|
||||
type _NarrowFragmentToPart<TFragment extends DMessageFragment, TPart> = TFragment & { part: TPart };
|
||||
|
||||
|
||||
// Future Examples: up to 1 per message, containing the Rays and Merges that would be used to restore the Beam state - could be volatile (omitted at save)
|
||||
// could not be the data store itself, but only used for save/reload
|
||||
@@ -84,6 +89,19 @@ type _DMessageFragmentWrapper<TFragment, TPart extends { pt: string }> = {
|
||||
fId: DMessageFragmentId;
|
||||
part: TPart;
|
||||
originId?: string; // optional, for multi-model, identifies which actor produced this fragment
|
||||
vendorState?: DMessageFragmentVendorState; // optional vendor-specific protocol state (opaque, lossy-safe)
|
||||
}
|
||||
|
||||
/**
|
||||
* Carries opaque vendor metadata required for protocol correctness - i.e. state continuity tokens, encrypted signatures, protocol quirks.
|
||||
* - Lossy-safe: Can be dropped during conversion/export without breaking functionality.
|
||||
* - Graceful-degrade on missing.
|
||||
*/
|
||||
export type DMessageFragmentVendorState = Record<string, unknown> & {
|
||||
gemini?: {
|
||||
thoughtSignature?: string; // Gemini 3+ - echoed back to maintain reasoning context
|
||||
};
|
||||
// Future: openai?: { ... }, anthropic?: { ... }
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +137,7 @@ type DMessageDocMeta = {
|
||||
codeLanguage?: string;
|
||||
srcFileName?: string;
|
||||
srcFileSize?: number;
|
||||
srcOcrFrom?: 'image' | 'pdf';
|
||||
srcOcrFrom?: 'image' | 'pdf' | 'image-caption';
|
||||
}
|
||||
|
||||
|
||||
@@ -166,6 +184,7 @@ namespace ZYNC_Entity { export type UUID = string; }
|
||||
|
||||
export type DMessageToolInvocationPart = {
|
||||
pt: 'tool_invocation',
|
||||
/** Matches the corresponding tool_response's id for pairing - set by the LLM, unique per message, at least */
|
||||
id: string,
|
||||
invocation: {
|
||||
type: 'function_call'
|
||||
@@ -184,6 +203,7 @@ export type DMessageToolInvocationPart = {
|
||||
|
||||
export type DMessageToolResponsePart = {
|
||||
pt: 'tool_response',
|
||||
/** Set by the response (or upstream server hosted response), matches the corresponding tool_invocation's id for pairing */
|
||||
id: string,
|
||||
error: boolean | string,
|
||||
response: {
|
||||
@@ -224,13 +244,28 @@ export type DVoidModelAuxPart = {
|
||||
redactedData?: readonly string[],
|
||||
};
|
||||
|
||||
type DVoidPlaceholderPart = { pt: 'ph', pText: string, pType?: 'chat-gen-follow-up' /* 2025-02-23: added for non-pure-text placeholders */, modelOp?: DVoidPlaceholderModelOp };
|
||||
export type DVoidPlaceholderPart = {
|
||||
pt: 'ph',
|
||||
pText: string,
|
||||
pType?: 'chat-gen-follow-up', // a follow-up is being generated
|
||||
modelOp?: DVoidPlaceholderModelOp,
|
||||
aixControl?: DVoidPlaceholderAixControlRetry,
|
||||
};
|
||||
|
||||
export type DVoidPlaceholderModelOp = {
|
||||
mot: 'search-web' | 'gen-image' | 'code-exec',
|
||||
cts: number, // client-based timestamp
|
||||
};
|
||||
|
||||
type DVoidPlaceholderAixControlRetry = {
|
||||
ctl: 'ec-retry', // control type: error correction retry
|
||||
rScope: 'srv-dispatch' | 'srv-op' | 'cli-ll', // srv-dispatch: dispatch fetch, srv-op: operation-level, cli-ll: client low-level
|
||||
rAttempt?: number, // attempt number (starts from 2 to be clear it's a retry)
|
||||
rStrat?: 'cli-ll-reconnect' | 'cli-ll-resume', // strategy for cli-ll scope (reconnect: new request, resume: continue from handle)
|
||||
rCauseHttp?: number, // HTTP status code if available (e.g., 429, 503, 502)
|
||||
rCauseConn?: string, // connection error type if available (e.g., 'net-disconnected', 'timeout')
|
||||
};
|
||||
|
||||
type _SentinelPart = { pt: '_pt_sentinel' };
|
||||
|
||||
|
||||
@@ -273,11 +308,15 @@ export function isVoidFragment(fragment: DMessageFragment): fragment is DMessage
|
||||
return fragment.ft === 'void' && !!fragment.part?.pt;
|
||||
}
|
||||
|
||||
export function isVoidAnnotationsFragment(fragment: DMessageFragment): fragment is DMessageVoidFragment & { part: DVoidModelAnnotationsPart } {
|
||||
export function isVoidAnnotationsFragment(fragment: DMessageFragment): fragment is DVoidFragmentModelAnnotations {
|
||||
return fragment.ft === 'void' && fragment.part.pt === 'annotations';
|
||||
}
|
||||
|
||||
export function isVoidThinkingFragment(fragment: DMessageFragment): fragment is DMessageVoidFragment & { part: DVoidModelAuxPart } {
|
||||
export function isVoidPlaceholderFragment(fragment: DMessageFragment): fragment is _DVoidFragmentPlaceholder {
|
||||
return fragment.ft === 'void' && fragment.part.pt === 'ph';
|
||||
}
|
||||
|
||||
export function isVoidThinkingFragment(fragment: DMessageFragment): fragment is _DVoidFragmentModelAux {
|
||||
return fragment.ft === 'void' && fragment.part.pt === 'ma' && fragment.part.aType === 'reasoning';
|
||||
}
|
||||
|
||||
@@ -406,8 +445,8 @@ export function createModelAuxVoidFragment(aType: DVoidModelAuxPart['aType'], aT
|
||||
return _createVoidFragment(_create_ModelAux_Part(aType, aText, textSignature, redactedData));
|
||||
}
|
||||
|
||||
export function createPlaceholderVoidFragment(placeholderText: string, placeholderType?: DVoidPlaceholderPart['pType'], modelOp?: DVoidPlaceholderModelOp): DMessageVoidFragment {
|
||||
return _createVoidFragment(_create_Placeholder_Part(placeholderText, placeholderType, modelOp));
|
||||
export function createPlaceholderVoidFragment(placeholderText: string, placeholderType?: DVoidPlaceholderPart['pType'], modelOp?: DVoidPlaceholderModelOp, aixControl?: DVoidPlaceholderPart['aixControl']): DMessageVoidFragment {
|
||||
return _createVoidFragment(_create_Placeholder_Part(placeholderText, placeholderType, modelOp, aixControl));
|
||||
}
|
||||
|
||||
function _createVoidFragment(part: DMessageVoidFragment['part']): DMessageVoidFragment {
|
||||
@@ -428,18 +467,20 @@ export function duplicateDMessageFragments(fragments: Readonly<DMessageFragment[
|
||||
}
|
||||
|
||||
/**
|
||||
* NOTE: a duplicate fragment gets a new ID, and also loses any originId, if set (not sure why, but it's the way it is now)
|
||||
* Duplicates a fragment with a new ID while preserving content-related metadata:
|
||||
* - Preserved: originId, vendorState, mutability
|
||||
* - Cleared: fId (new ID), identity (per spec: "removed on duplication (new edit)")
|
||||
*/
|
||||
function _duplicateFragment(fragment: DMessageFragment): DMessageFragment {
|
||||
switch (fragment.ft) {
|
||||
case 'content':
|
||||
return _createContentFragment(_duplicate_Part(fragment.part));
|
||||
return _carryMeta(fragment, _createContentFragment(_duplicate_Part(fragment.part)));
|
||||
|
||||
case 'attachment':
|
||||
return _createAttachmentFragment(fragment.title, fragment.caption, _duplicate_Part(fragment.part), fragment.liveFileId);
|
||||
return _carryMeta(fragment, _createAttachmentFragment(fragment.title, fragment.caption, _duplicate_Part(fragment.part), fragment.liveFileId));
|
||||
|
||||
case 'void':
|
||||
return _createVoidFragment(_duplicate_Part(fragment.part));
|
||||
return _carryMeta(fragment, _createVoidFragment(_duplicate_Part(fragment.part)));
|
||||
|
||||
case '_ft_sentinel':
|
||||
return _createSentinelFragment();
|
||||
@@ -450,6 +491,22 @@ function _duplicateFragment(fragment: DMessageFragment): DMessageFragment {
|
||||
}
|
||||
}
|
||||
|
||||
/** Duplication: Preserves optional DMessageFragment metadata from source to target. */
|
||||
function _carryMeta<T extends DMessageFragment>(source: Readonly<DMessageFragment>, target: T): T {
|
||||
// quick-out: sentinels don't have metadata
|
||||
if (source.ft === '_ft_sentinel' || target.ft === '_ft_sentinel')
|
||||
return target;
|
||||
|
||||
let enriched = target;
|
||||
if ('originId' in source && source.originId)
|
||||
enriched = { ...enriched, originId: source.originId };
|
||||
|
||||
if ('vendorState' in source && source.vendorState)
|
||||
enriched = { ...enriched, vendorState: structuredClone(source.vendorState) };
|
||||
|
||||
return enriched;
|
||||
}
|
||||
|
||||
|
||||
/// Helpers - Parts Creation & Duplication
|
||||
|
||||
@@ -520,8 +577,8 @@ function _create_ModelAux_Part(aType: DVoidModelAuxPart['aType'], aText: string,
|
||||
};
|
||||
}
|
||||
|
||||
function _create_Placeholder_Part(placeholderText: string, pType?: DVoidPlaceholderPart['pType'], modelOp?: DVoidPlaceholderModelOp): DVoidPlaceholderPart {
|
||||
return { pt: 'ph', pText: placeholderText, ...(pType ? { pType } : undefined), ...(modelOp ? { modelOp: { ...modelOp } } : undefined) };
|
||||
function _create_Placeholder_Part(placeholderText: string, pType?: DVoidPlaceholderPart['pType'], modelOp?: DVoidPlaceholderModelOp, aixControl?: DVoidPlaceholderPart['aixControl']): DVoidPlaceholderPart {
|
||||
return { pt: 'ph', pText: placeholderText, ...(pType ? { pType } : undefined), ...(modelOp ? { modelOp: { ...modelOp } } : undefined), ...(aixControl ? { aixControl: { ...aixControl } } : undefined) };
|
||||
}
|
||||
|
||||
function _create_Sentinel_Part(): _SentinelPart {
|
||||
@@ -576,7 +633,7 @@ function _duplicate_Part<TPart extends (DMessageContentFragment | DMessageAttach
|
||||
return _create_ModelAux_Part(part.aType, part.aText, part.textSignature, part.redactedData) as TPart;
|
||||
|
||||
case 'ph':
|
||||
return _create_Placeholder_Part(part.pText, part.pType, part.modelOp) as TPart;
|
||||
return _create_Placeholder_Part(part.pText, part.pType, part.modelOp, part.aixControl) as TPart;
|
||||
|
||||
case 'text':
|
||||
return _create_Text_Part(part.text) as TPart;
|
||||
|
||||
@@ -271,6 +271,48 @@ export function messageWasInterruptedAtStart(message: Pick<DMessage, 'generator'
|
||||
// }
|
||||
|
||||
|
||||
// helpers - generators
|
||||
|
||||
export function messageSetGenerator(message: Pick<DMessage, 'generator'>, generator: undefined | DMessageGenerator): void {
|
||||
if (generator !== undefined)
|
||||
message.generator = generator;
|
||||
else
|
||||
delete message.generator;
|
||||
}
|
||||
|
||||
export function messageSetGeneratorNamed(message: Pick<DMessage, 'generator'>, label: 'web' | 'issue' | 'help' | string): void {
|
||||
message.generator = {
|
||||
mgt: 'named',
|
||||
name: label,
|
||||
};
|
||||
}
|
||||
|
||||
function _messageSetGeneratorAIX(message: Pick<DMessage, 'generator'>, modelLabel: string, modelVendorId: ModelVendorId, modelId: DLLMId): void {
|
||||
message.generator = {
|
||||
mgt: 'aix',
|
||||
name: modelLabel,
|
||||
aix: {
|
||||
vId: modelVendorId,
|
||||
mId: modelId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function messageSetGeneratorAIX_AutoLabel(message: Pick<DMessage, 'generator'>, modelVendorId: ModelVendorId, modelId: DLLMId): void {
|
||||
|
||||
// Simply strip the first part of the modelId, which is the serviceId, before the dash.
|
||||
const heuristicLabel = modelId.includes('-') ? modelId.replace(/^[^-]+-/, '') : modelId;
|
||||
|
||||
_messageSetGeneratorAIX(message, heuristicLabel, modelVendorId, modelId);
|
||||
}
|
||||
|
||||
/*export function messageUpdateGeneratorInfo(message: Pick<DMessage, 'generator'>, metrics?: DMetricsChatGenerate_Md, tokenStopReason?: DMessageGenerator['tokenStopReason']): void {
|
||||
if (!message.generator) return;
|
||||
if (metrics) message.generator.metrics = metrics;
|
||||
if (tokenStopReason) message.generator.tokenStopReason = tokenStopReason;
|
||||
}*/
|
||||
|
||||
|
||||
// helpers - user flags
|
||||
|
||||
const flag2EmojiMap: Record<DMessageUserFlag, string> = {
|
||||
|
||||
@@ -3,12 +3,17 @@ import * as React from 'react';
|
||||
import type { Immutable } from '~/common/types/immutable.types';
|
||||
import { shallowEquals } from '~/common/util/hooks/useShallowObject';
|
||||
|
||||
import { DMessageAttachmentFragment, DMessageContentFragment, DMessageFragment, DMessageVoidFragment, isContentFragment, isErrorPart, isImageRefPart, isZyncAssetImageReferencePart } from '../chat.fragments';
|
||||
import { DMessageAttachmentFragment, DMessageContentFragment, DMessageFragment, DMessageVoidFragment, DVoidFragmentModelAnnotations, isContentFragment, isErrorPart, isImageRefPart, isVoidAnnotationsFragment, isZyncAssetImageReferencePart } from '../chat.fragments';
|
||||
|
||||
/**
|
||||
* Fragments that can be interleaved: void fragments (reasoning, placeholders) and content fragments (text, code, tools).
|
||||
* Excludes annotations (rendered separately at top) and attachments (rendered separately).
|
||||
*/
|
||||
export type InterleavedFragment = DMessageVoidFragment | DMessageContentFragment;
|
||||
|
||||
interface FragmentBuckets {
|
||||
voidFragments: DMessageVoidFragment[];
|
||||
contentFragments: DMessageContentFragment[];
|
||||
annotationFragments: DVoidFragmentModelAnnotations[];
|
||||
interleavedFragments: InterleavedFragment[];
|
||||
imageAttachments: DMessageAttachmentFragment[];
|
||||
nonImageAttachments: DMessageAttachmentFragment[];
|
||||
lastFragmentIsError: boolean;
|
||||
@@ -20,16 +25,16 @@ interface FragmentBuckets {
|
||||
export function useFragmentBuckets(messageFragments: Immutable<DMessageFragment[]>): FragmentBuckets {
|
||||
|
||||
// Refs to store the last stable value for each bucket
|
||||
const voidFragmentsRef = React.useRef<DMessageVoidFragment[]>([]);
|
||||
const contentFragmentsRef = React.useRef<DMessageContentFragment[]>([]);
|
||||
const annotationFragmentsRef = React.useRef<DVoidFragmentModelAnnotations[]>([]);
|
||||
const interleavedFragmentsRef = React.useRef<InterleavedFragment[]>([]);
|
||||
const imageAttachmentsRef = React.useRef<DMessageAttachmentFragment[]>([]);
|
||||
const nonImageAttachmentsRef = React.useRef<DMessageAttachmentFragment[]>([]);
|
||||
|
||||
// Use useMemo to recalculate buckets only when messageFragments changes
|
||||
return React.useMemo(() => {
|
||||
|
||||
const voidFragments: DMessageVoidFragment[] = [];
|
||||
const contentFragments: DMessageContentFragment[] = [];
|
||||
const annotationFragments: DVoidFragmentModelAnnotations[] = [];
|
||||
const interleavedFragments: InterleavedFragment[] = [];
|
||||
const imageAttachments: DMessageAttachmentFragment[] = [];
|
||||
const nonImageAttachments: DMessageAttachmentFragment[] = [];
|
||||
|
||||
@@ -37,14 +42,20 @@ export function useFragmentBuckets(messageFragments: Immutable<DMessageFragment[
|
||||
const ft = fragment.ft;
|
||||
switch (ft) {
|
||||
case 'content':
|
||||
return contentFragments.push(fragment);
|
||||
// Content fragments go into interleaved list (in order)
|
||||
return interleavedFragments.push(fragment);
|
||||
case 'attachment':
|
||||
// Attachments stay separated for special rendering
|
||||
if (isZyncAssetImageReferencePart(fragment.part) || isImageRefPart(fragment.part))
|
||||
return imageAttachments.push(fragment);
|
||||
else
|
||||
return nonImageAttachments.push(fragment);
|
||||
case 'void':
|
||||
return voidFragments.push(fragment);
|
||||
// Use type guard to properly narrow the fragment type
|
||||
if (isVoidAnnotationsFragment(fragment))
|
||||
return annotationFragments.push(fragment);
|
||||
else
|
||||
return interleavedFragments.push(fragment);
|
||||
case '_ft_sentinel':
|
||||
break; // nothing to do here - this is a sentinel type
|
||||
default:
|
||||
@@ -54,11 +65,11 @@ export function useFragmentBuckets(messageFragments: Immutable<DMessageFragment[
|
||||
});
|
||||
|
||||
// For each bucket, return the new value if it's different, otherwise return the stable ref
|
||||
if (!shallowEquals(voidFragments, voidFragmentsRef.current))
|
||||
voidFragmentsRef.current = voidFragments;
|
||||
if (!shallowEquals(annotationFragments, annotationFragmentsRef.current))
|
||||
annotationFragmentsRef.current = annotationFragments;
|
||||
|
||||
if (!shallowEquals(contentFragments, contentFragmentsRef.current))
|
||||
contentFragmentsRef.current = contentFragments;
|
||||
if (!shallowEquals(interleavedFragments, interleavedFragmentsRef.current))
|
||||
interleavedFragmentsRef.current = interleavedFragments;
|
||||
|
||||
if (!shallowEquals(imageAttachments, imageAttachmentsRef.current))
|
||||
imageAttachmentsRef.current = imageAttachments;
|
||||
@@ -69,8 +80,8 @@ export function useFragmentBuckets(messageFragments: Immutable<DMessageFragment[
|
||||
const lastFragment: DMessageFragment | undefined = messageFragments.at(-1);
|
||||
|
||||
return {
|
||||
voidFragments: voidFragmentsRef.current,
|
||||
contentFragments: contentFragmentsRef.current,
|
||||
annotationFragments: annotationFragmentsRef.current,
|
||||
interleavedFragments: interleavedFragmentsRef.current,
|
||||
imageAttachments: imageAttachmentsRef.current,
|
||||
nonImageAttachments: nonImageAttachmentsRef.current,
|
||||
lastFragmentIsError: !!lastFragment && isContentFragment(lastFragment) && isErrorPart(lastFragment.part),
|
||||
|
||||
@@ -15,16 +15,40 @@ export function useLLMs(llmIds: ReadonlyArray<DLLMId>): ReadonlyArray<DLLM | und
|
||||
}));
|
||||
}
|
||||
|
||||
function _sortStarredFirstComparator(a: { userStarred?: boolean }, b: { userStarred?: boolean }) {
|
||||
if (a.userStarred && !b.userStarred) return -1;
|
||||
if (!a.userStarred && b.userStarred) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function useLLMsByService(serviceId: false | DModelsServiceId): DLLM[] {
|
||||
return useModelsStore(useShallow(
|
||||
state => !serviceId ? state.llms : state.llms.filter(llm => llm.sId === serviceId),
|
||||
));
|
||||
}
|
||||
|
||||
export function useVisibleLLMs(includeLlmId: undefined | DLLMId | null): ReadonlyArray<DLLM> {
|
||||
return useModelsStore(useShallow(
|
||||
({ llms }) => llms.filter(llm => isLLMVisible(llm) || (includeLlmId && llm.id === includeLlmId)),
|
||||
));
|
||||
export function useVisibleLLMs(includeLlmId: undefined | DLLMId | null, starredOnly: boolean, starredFirst: boolean): { llms: ReadonlyArray<DLLM>; hasStarred: boolean } {
|
||||
// for performance, we don't include this in the memo selector, as they'll change in tandem anyway
|
||||
let hasStarred = false;
|
||||
|
||||
const llms = useModelsStore(useShallow(({ llms }) => {
|
||||
// filter by visibility and starred status
|
||||
const filtered = llms.filter((llm) => {
|
||||
// finds out if any starred LLM exists
|
||||
if (llm.userStarred) hasStarred = true;
|
||||
|
||||
// always include the specified LLM ID if provided
|
||||
if (includeLlmId && llm.id === includeLlmId) return true;
|
||||
|
||||
// visibility filter
|
||||
return isLLMVisible(llm) && (!starredOnly || llm.userStarred);
|
||||
});
|
||||
|
||||
// sort starred first if requested
|
||||
return !starredFirst ? filtered : filtered.sort(_sortStarredFirstComparator);
|
||||
}));
|
||||
|
||||
return { llms, hasStarred };
|
||||
}
|
||||
|
||||
export function useHasLLMs(): boolean {
|
||||
|
||||
@@ -126,6 +126,24 @@ export const DModelParameterRegistry = {
|
||||
// No initial value - when undefined, the model decides the aspect ratio
|
||||
} as const,
|
||||
|
||||
llmVndGeminiCodeExecution: {
|
||||
label: 'Code Execution',
|
||||
type: 'enum' as const,
|
||||
description: 'Enable automatic Python code generation and execution by the model',
|
||||
values: ['auto'] as const,
|
||||
// No initialValue - undefined means off
|
||||
} as const,
|
||||
|
||||
llmVndGeminiComputerUse: {
|
||||
label: 'Computer Use Environment',
|
||||
type: 'enum' as const,
|
||||
description: 'Environment type for Computer Use tool (required for Computer Use model)',
|
||||
values: ['browser'] as const,
|
||||
initialValue: 'browser',
|
||||
// requiredFallback: 'browser', // See `const _requiredParamId: DModelParameterId[]` in llms.parameters.ts for why custom params don't have required values at AIX invocation...
|
||||
hidden: true,
|
||||
} as const,
|
||||
|
||||
llmVndGeminiGoogleSearch: {
|
||||
label: 'Google Search',
|
||||
type: 'enum' as const,
|
||||
@@ -134,11 +152,27 @@ export const DModelParameterRegistry = {
|
||||
// No initialValue - undefined means off
|
||||
} as const,
|
||||
|
||||
llmVndGeminiImageSize: { // [Gemini, 2025-11-20] Nano Banana launch
|
||||
label: 'Image Size',
|
||||
type: 'enum' as const,
|
||||
description: 'Controls the resolution of generated images',
|
||||
values: ['1K', '2K', '4K'] as const,
|
||||
// No initial value - when undefined, the model decides the image size
|
||||
} as const,
|
||||
|
||||
llmVndGeminiMediaResolution: {
|
||||
label: 'Media Resolution',
|
||||
type: 'enum' as const,
|
||||
description: 'Controls vision processing quality for multimodal inputs. Higher resolution improves text reading and detail identification but increases token usage.',
|
||||
values: ['mr_high', 'mr_medium', 'mr_low'] as const,
|
||||
// No initialValue - undefined: "If unspecified, the model uses optimal defaults based on the media type." (Images: high, PDFs: medium, Videos: low/medium (rec: high for OCR))
|
||||
} as const,
|
||||
|
||||
llmVndGeminiShowThoughts: {
|
||||
label: 'Show Thoughts',
|
||||
type: 'boolean' as const,
|
||||
description: 'Show Gemini\'s reasoning process',
|
||||
initialValue: true,
|
||||
// initialValue: true, // no initial value
|
||||
} as const,
|
||||
|
||||
llmVndGeminiThinkingBudget: {
|
||||
@@ -154,6 +188,35 @@ export const DModelParameterRegistry = {
|
||||
description: 'Budget for extended thinking. 0 disables thinking. If not set, the model chooses automatically.',
|
||||
} as const,
|
||||
|
||||
llmVndGeminiThinkingLevel: {
|
||||
label: 'Thinking Level',
|
||||
type: 'enum' as const,
|
||||
description: 'Controls internal reasoning depth. Replaces thinking_budget for Gemini 3 models. When unset, the model decides dynamically.',
|
||||
values: ['high', 'medium' /* not present at launch */, 'low' /* default when unset */] as const,
|
||||
// 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,
|
||||
|
||||
// NOTE: we don't have this as a parameter, as for now we use it in tandem with llmVndGeminiGoogleSearch
|
||||
// llmVndGeminiUrlContext: {
|
||||
// label: 'URL Context',
|
||||
// type: 'enum' as const,
|
||||
// description: 'Enable fetching and analyzing content from URLs provided in prompts (up to 20 URLs, 34MB each)',
|
||||
// values: ['auto'] as const,
|
||||
// // No initialValue - undefined means off
|
||||
// } as const,
|
||||
|
||||
// Moonshot-specific parameters
|
||||
|
||||
llmVndMoonshotWebSearch: {
|
||||
label: 'Web Search',
|
||||
type: 'enum' as const,
|
||||
description: 'Enable Kimi\'s $web_search builtin function for real-time web search ($0.005 per search)',
|
||||
values: ['auto'] as const,
|
||||
// No initialValue - undefined means off
|
||||
} as const,
|
||||
|
||||
// OpenAI-specific parameters
|
||||
|
||||
llmVndOaiReasoningEffort: {
|
||||
label: 'Reasoning Effort',
|
||||
type: 'enum' as const,
|
||||
@@ -340,7 +403,7 @@ export function applyModelParameterInitialValues(destValues: DModelParameterValu
|
||||
const _requiredParamId: DModelParameterId[] = [
|
||||
// 'llmRef', // disabled: we know this can't have a fallback value in the registry
|
||||
'llmResponseTokens', // DModelParameterRegistry.llmResponseTokens.requiredFallback = FALLBACK_LLM_PARAM_RESPONSE_TOKENS
|
||||
'llmTemperature' // DModelParameterRegistry.llmTemperature.requiredFallback = FALLBACK_LLM_PARAM_TEMPERATURE
|
||||
'llmTemperature', // DModelParameterRegistry.llmTemperature.requiredFallback = FALLBACK_LLM_PARAM_TEMPERATURE
|
||||
] as const;
|
||||
|
||||
export function getAllModelParameterValues(initialParameters: undefined | DModelParameterValues, userParameters?: DModelParameterValues): DModelParameterValues {
|
||||
|
||||
@@ -84,6 +84,7 @@ interface Was_DModelPricingV2 {
|
||||
}
|
||||
|
||||
export function portModelPricingV2toV3(llm: DLLM): void {
|
||||
// NOTE: direct .pricing access instead of getLLMPricing, because there was no user pricing in this generation
|
||||
if (!llm.pricing) return;
|
||||
if (typeof llm.pricing !== 'object') return;
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface DLLM {
|
||||
userParameters?: DModelParameterValues; // user has set these parameters
|
||||
userContextTokens?: DLLMContextTokens; // user override for context window
|
||||
userMaxOutputTokens?: DLLMMaxOutputTokens; // user override for max output tokens
|
||||
userPricing?: DModelPricing; // user override for model pricing
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +122,18 @@ export function getLLMMaxOutputTokens(llm: DLLM | null): DLLMMaxOutputTokens | u
|
||||
return llm.userMaxOutputTokens ?? llm.maxOutputTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective pricing for a model.
|
||||
* Checks user override first, then falls back to model default.
|
||||
*/
|
||||
export function getLLMPricing(llm: DLLM | null): DModelPricing | undefined {
|
||||
if (!llm)
|
||||
return undefined; // undefined if no model
|
||||
|
||||
// Check user override first, then fall back to model default
|
||||
return llm.userPricing ?? llm.pricing;
|
||||
}
|
||||
|
||||
|
||||
/// Interfaces ///
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ type ModelDomainSpec = {
|
||||
};
|
||||
|
||||
|
||||
export const ModelDomainsList: DModelDomainId[] = ['primaryChat', 'codeApply', 'fastUtil'] as const;
|
||||
export const ModelDomainsList: DModelDomainId[] = ['primaryChat', 'codeApply', 'fastUtil', 'imageCaption'] as const;
|
||||
|
||||
export const ModelDomainsRegistry: Record<DModelDomainId, ModelDomainSpec> = {
|
||||
primaryChat: {
|
||||
@@ -48,6 +48,15 @@ export const ModelDomainsRegistry: Record<DModelDomainId, ModelDomainSpec> = {
|
||||
autoStrategy: 'topVendorLowestCost',
|
||||
requiredInterfaces: [LLM_IF_OAI_Fn], // NOTE: we do enforce this already, although this may not be correctly set for all vendors
|
||||
},
|
||||
imageCaption: {
|
||||
label: 'Image Captioning',
|
||||
confLabel: 'Vision',
|
||||
confTooltip: 'Vision model for image captioning',
|
||||
description: 'Describes images as text',
|
||||
recommended: 'Qwen VL',
|
||||
autoStrategy: 'topVendorTopLlm',
|
||||
fallbackDomain: 'primaryChat',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -17,4 +17,9 @@ export type DModelDomainId =
|
||||
* Fast Utility model; must have function calling, but we won't enforce in the code for now until all LLMs are correctly identified as FC or not - used for quick responses and simple tasks
|
||||
*/
|
||||
'fastUtil'
|
||||
|
|
||||
/**
|
||||
* Image Captioning model - used to generate detailed text descriptions of images before sending to primary chat model
|
||||
*/
|
||||
'imageCaption'
|
||||
;
|
||||
@@ -3,7 +3,7 @@ import type { StateCreator } from 'zustand/vanilla';
|
||||
import type { ModelVendorId } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import type { DModelDomainId } from './model.domains.types';
|
||||
import { DLLM, DLLMId, isLLMHidden, isLLMVisible } from './llms.types';
|
||||
import { DLLM, DLLMId, getLLMPricing, isLLMHidden, isLLMVisible } from './llms.types';
|
||||
import { LlmsRootState, useModelsStore } from './store-llms';
|
||||
import { ModelDomainsList, ModelDomainsRegistry } from './model.domains.registry';
|
||||
import { createDModelConfiguration, DModelConfiguration } from './modelconfiguration.types';
|
||||
@@ -275,7 +275,7 @@ function _groupLlmsByVendorRankedByElo(llms: ReadonlyArray<DLLM>): PreferredRank
|
||||
const eloCostItem = {
|
||||
id: llm.id,
|
||||
cbaElo: llm.benchmark?.cbaElo,
|
||||
costRank: !llm.pricing ? undefined : _getLlmCostBenchmark(llm),
|
||||
costRank: !getLLMPricing(llm) ? undefined : _getLlmCostBenchmark(llm),
|
||||
};
|
||||
if (!group)
|
||||
acc.push({ vendorId: llm.vId, llmsByElo: [eloCostItem] });
|
||||
@@ -295,8 +295,9 @@ function _groupLlmsByVendorRankedByElo(llms: ReadonlyArray<DLLM>): PreferredRank
|
||||
|
||||
// Hypothetical cost benchmark for a model, based on total cost of 100k input tokens and 10k output tokens.
|
||||
function _getLlmCostBenchmark(llm: DLLM): number | undefined {
|
||||
if (!llm.pricing?.chat) return undefined;
|
||||
const costIn = getLlmCostForTokens(100000, 100000, llm.pricing.chat.input);
|
||||
const costOut = getLlmCostForTokens(100000, 10000, llm.pricing.chat.output);
|
||||
const pricing = getLLMPricing(llm);
|
||||
if (!pricing?.chat) return undefined;
|
||||
const costIn = getLlmCostForTokens(100000, 100000, pricing.chat.input);
|
||||
const costOut = getLlmCostForTokens(100000, 10000, pricing.chat.output);
|
||||
return (costIn !== undefined && costOut !== undefined) ? costIn + costOut : undefined;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ import type { DOpenRouterServiceSettings } from '~/modules/llms/vendors/openrout
|
||||
import type { IModelVendor } from '~/modules/llms/vendors/IModelVendor';
|
||||
import type { ModelVendorId } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import { hasKeys } from '~/common/util/objectUtils';
|
||||
|
||||
import type { DModelDomainId } from './model.domains.types';
|
||||
import type { DModelParameterId, DModelParameterValues } from './llms.parameters';
|
||||
import type { DModelsService, DModelsServiceId } from './llms.service.types';
|
||||
@@ -37,6 +39,7 @@ interface LlmsRootActions {
|
||||
removeLLM: (id: DLLMId) => void;
|
||||
rerankLLMsByServices: (serviceIdOrder: DModelsServiceId[]) => void;
|
||||
updateLLM: (id: DLLMId, partial: Partial<DLLM>) => void;
|
||||
updateLLMs: (updates: Array<{ id: DLLMId; partial: Partial<DLLM> }>) => void;
|
||||
updateLLMUserParameters: (id: DLLMId, partial: Partial<DModelParameterValues>) => void;
|
||||
deleteLLMUserParameter: (id: DLLMId, parameterId: DModelParameterId) => void;
|
||||
resetLLMUserParameters: (id: DLLMId) => void;
|
||||
@@ -78,7 +81,9 @@ export const useModelsStore = create<LlmsStore>()(persist(
|
||||
if (keepUserEdits) {
|
||||
serviceLLMs = serviceLLMs.map((llm: DLLM): DLLM => {
|
||||
const existing = existingLLMs.find(m => m.id === llm.id);
|
||||
return !existing ? llm : {
|
||||
if (!existing) return llm;
|
||||
|
||||
const result = {
|
||||
...llm,
|
||||
...(existing.userLabel !== undefined ? { userLabel: existing.userLabel } : {}),
|
||||
...(existing.userHidden !== undefined ? { userHidden: existing.userHidden } : {}),
|
||||
@@ -86,7 +91,16 @@ export const useModelsStore = create<LlmsStore>()(persist(
|
||||
...(existing.userParameters !== undefined ? { userParameters: { ...existing.userParameters } } : {}),
|
||||
...(existing.userContextTokens !== undefined ? { userContextTokens: existing.userContextTokens } : {}),
|
||||
...(existing.userMaxOutputTokens !== undefined ? { userMaxOutputTokens: existing.userMaxOutputTokens } : {}),
|
||||
...(existing.userPricing !== undefined ? { userPricing: existing.userPricing } : {}),
|
||||
};
|
||||
|
||||
// clean up stale parameters from userParameters - e.g. was in the model spec but removed in the new version
|
||||
if (result.userParameters)
|
||||
for (const key of Object.keys(result.userParameters))
|
||||
if (!llm.parameterSpecs.some(spec => spec.paramId === key))
|
||||
delete result.userParameters[key as DModelParameterId];
|
||||
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -140,6 +154,19 @@ export const useModelsStore = create<LlmsStore>()(persist(
|
||||
),
|
||||
})),
|
||||
|
||||
updateLLMs: (updates: Array<{ id: DLLMId; partial: Partial<DLLM> }>) =>
|
||||
set(state => {
|
||||
// Create a map of updates for efficient lookup
|
||||
const updatesMap = new Map(updates.map(u => [u.id, u.partial]));
|
||||
|
||||
return {
|
||||
llms: state.llms.map((llm: DLLM): DLLM => {
|
||||
const partial = updatesMap.get(llm.id);
|
||||
return partial ? { ...llm, ...partial } : llm;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
|
||||
updateLLMUserParameters: (id: DLLMId, partialUserParameters: Partial<DModelParameterValues>) =>
|
||||
set(({ llms }) => ({
|
||||
llms: llms.map((llm: DLLM): DLLM =>
|
||||
@@ -163,10 +190,10 @@ export const useModelsStore = create<LlmsStore>()(persist(
|
||||
llms: llms.map((llm: DLLM): DLLM => {
|
||||
if (llm.id !== id) return llm;
|
||||
// strip away just the user parameters
|
||||
const { userParameters /*, userContextTokens, userMaxOutputTokens*/, ...rest } = llm;
|
||||
const { userParameters /*, userContextTokens, userMaxOutputTokens, userPricing, ...*/, ...rest } = llm;
|
||||
return rest;
|
||||
}),
|
||||
})),
|
||||
})),
|
||||
|
||||
createModelsService: (vendor: IModelVendor): DModelsService => {
|
||||
|
||||
@@ -251,7 +278,7 @@ export const useModelsStore = create<LlmsStore>()(persist(
|
||||
* 2: large changes on all LLMs, and reset chat/fast/func LLMs
|
||||
* 3: big-AGI v2.x upgrade
|
||||
* 4: migrate .options to .initialParameters/.userParameters
|
||||
* 4B: we changed from .chatLLMId/.fastLLMId to modelAssignments: {}, without expicit migration (done on rehydrate, and for no particular reason)
|
||||
* 4B: we changed from .chatLLMId/.fastLLMId to modelAssignments: {}, without explicit migration (done on rehydrate, and for no particular reason)
|
||||
*/
|
||||
version: 4,
|
||||
migrate: (_state: any, fromVersion: number): LlmsStore => {
|
||||
@@ -321,7 +348,7 @@ export const useModelsStore = create<LlmsStore>()(persist(
|
||||
// Select the best LLMs automatically, if not set
|
||||
try {
|
||||
// auto-detect assignments, or re-import them from the old format
|
||||
if (!state.modelAssignments || !Object.keys(state.modelAssignments).length) {
|
||||
if (!hasKeys(state.modelAssignments)) {
|
||||
|
||||
// reimport the former chatLLMId and fastLLMId if set
|
||||
const prevState = state as { chatLLMId?: DLLMId, fastLLMId?: DLLMId };
|
||||
|
||||
@@ -48,6 +48,12 @@ interface UIPreferencesStore {
|
||||
showModelsHidden: boolean;
|
||||
setShowModelsHidden: (showModelsHidden: boolean) => void;
|
||||
|
||||
showModelsStarredOnly: boolean;
|
||||
toggleShowModelsStarredOnly: () => void;
|
||||
|
||||
modelsStarredOnTop: boolean;
|
||||
setModelsStarredOnTop: (modelsStarredOnTop: boolean) => void;
|
||||
|
||||
composerQuickButton: 'off' | 'call' | 'beam';
|
||||
setComposerQuickButton: (composerQuickButton: 'off' | 'call' | 'beam') => void;
|
||||
|
||||
@@ -117,6 +123,12 @@ export const useUIPreferencesStore = create<UIPreferencesStore>()(
|
||||
showModelsHidden: false,
|
||||
setShowModelsHidden: (showModelsHidden: boolean) => set({ showModelsHidden }),
|
||||
|
||||
showModelsStarredOnly: false,
|
||||
toggleShowModelsStarredOnly: () => set((state) => ({ showModelsStarredOnly: !state.showModelsStarredOnly })),
|
||||
|
||||
modelsStarredOnTop: true,
|
||||
setModelsStarredOnTop: (modelsStarredOnTop: boolean) => set({ modelsStarredOnTop }),
|
||||
|
||||
composerQuickButton: 'beam',
|
||||
setComposerQuickButton: (composerQuickButton: 'off' | 'call' | 'beam') => set({ composerQuickButton }),
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export namespace AudioGenerator {
|
||||
interface SoundOptions {
|
||||
volume?: number;
|
||||
roomSize?: 'small' | 'large';
|
||||
filter?: 'underwater' | null;
|
||||
}
|
||||
|
||||
// Advanced Sounds (with room acoustics)
|
||||
@@ -399,12 +400,40 @@ export namespace AudioGenerator {
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.001, now + (index + 1) * noteDuration);
|
||||
|
||||
oscillator.connect(gainNode); // .connect(agMasterGain);
|
||||
applyRoomAcoustics(ctx, gainNode, options.roomSize || 'small');
|
||||
applyRoomAcoustics(ctx, gainNode, options.roomSize || 'small', options.filter);
|
||||
oscillator.start(now + index * noteDuration);
|
||||
oscillator.stop(now + (index + 2) * noteDuration);
|
||||
});
|
||||
}
|
||||
|
||||
/** Play an error notification sound when the assistant's response fails */
|
||||
export function chatNotifyError(options: SoundOptions = {}): void {
|
||||
const ctx = singleContext();
|
||||
if (!ctx) return;
|
||||
|
||||
const now = ctx.currentTime;
|
||||
const volume = options.volume ?? 0.15;
|
||||
const noteDuration = 0.16;
|
||||
|
||||
// Descending tone with triangle wave: G4 → D4 (subtle roughness for error)
|
||||
[392.00, 293.66].forEach((freq, index) => {
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
|
||||
osc.type = 'triangle'; // Less smooth than sine
|
||||
osc.frequency.setValueAtTime(freq, now + index * noteDuration);
|
||||
|
||||
gain.gain.setValueAtTime(0, now + index * noteDuration);
|
||||
gain.gain.linearRampToValueAtTime(volume, now + index * noteDuration + 0.05);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, now + (index + 1) * noteDuration);
|
||||
|
||||
osc.connect(gain);
|
||||
applyRoomAcoustics(ctx, gain, options.roomSize || 'small', options.filter);
|
||||
osc.start(now + index * noteDuration);
|
||||
osc.stop(now + (index + 2) * noteDuration);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -575,7 +604,26 @@ export namespace AudioGenerator {
|
||||
|
||||
/// Utility Functions ///
|
||||
|
||||
function applyRoomAcoustics(ctx: AudioContext, source: AudioNode, roomSize: 'small' | 'large' = 'small'): void {
|
||||
function applyRoomAcoustics(ctx: AudioContext, source: AudioNode, roomSize: 'small' | 'large' = 'small', filter?: 'underwater' | null): void {
|
||||
// If underwater, apply muffled + echo
|
||||
if (filter === 'underwater') {
|
||||
const lowpass = ctx.createBiquadFilter();
|
||||
lowpass.type = 'lowpass';
|
||||
lowpass.frequency.value = 250;
|
||||
|
||||
const delay = ctx.createDelay();
|
||||
delay.delayTime.value = 0.08;
|
||||
|
||||
const echo = ctx.createGain();
|
||||
echo.gain.value = 0.3;
|
||||
|
||||
source.connect(lowpass);
|
||||
lowpass.connect(agMasterGain);
|
||||
lowpass.connect(delay).connect(echo).connect(agMasterGain);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply room reverb (normal case)
|
||||
const convolver = ctx.createConvolver();
|
||||
const reverbTime = roomSize === 'large' ? 2 : 0.5;
|
||||
const decayRate = roomSize === 'large' ? 0.5 : 2;
|
||||
|
||||
@@ -103,7 +103,6 @@ export function maybeDebuggerBreak(): void {
|
||||
const isBreakEnabled = process.env.NEXT_PUBLIC_DEBUG_BREAKS === 'true';
|
||||
|
||||
if (Release.IsNodeDevBuild && isBreakEnabled) {
|
||||
// eslint-disable-next-line no-debugger
|
||||
debugger; // This line will be hit only if DevTools are open.
|
||||
// Build tools often remove debugger statements in production.
|
||||
}
|
||||
|
||||
@@ -5,11 +5,17 @@
|
||||
* Also see videoUtils.ts for more image-related functions.
|
||||
*/
|
||||
|
||||
import { DEFAULT_ADRAFT_IMAGE_MIMETYPE, DEFAULT_ADRAFT_IMAGE_QUALITY } from '../attachment-drafts/attachment.pipeline';
|
||||
|
||||
import { Is } from './pwaUtils';
|
||||
import { asyncCanvasToBlobWithValidation } from './canvasUtils';
|
||||
|
||||
|
||||
// important platform values
|
||||
export const PLATFORM_IMAGE_MIMETYPE: CommonImageMimeTypes = !Is.Browser.Safari ? 'image/webp' : 'image/jpeg';
|
||||
|
||||
|
||||
// configuration
|
||||
const HQ_SMOOTHING = true;
|
||||
const DEFAULT_LOSSY_QUALITY = 0.96;
|
||||
const IMAGE_DIMENSIONS = {
|
||||
ANTHROPIC_MAX_SIDE: 1568,
|
||||
GOOGLE_MAX_SIDE: 3072,
|
||||
@@ -88,7 +94,7 @@ export async function renderSVGToPNGBlob(svgCode: string, transparentBackground:
|
||||
interface ImageTransformOptions {
|
||||
/** Resize mode for the image, if specified. */
|
||||
resizeMode?: LLMImageResizeMode,
|
||||
/** If unspecified, we'll use the DEFAULT_ADRAFT_IMAGE_MIMETYPE (webp for chrome/firefox, jpeg for safari which doesn't encode webp) */
|
||||
/** If unspecified, we'll use the PLATFORM_IMAGE_MIMETYPE (webp for chrome/firefox, jpeg for safari which doesn't encode webp) */
|
||||
convertToMimeType?: 'image/png' | 'image/jpeg' | 'image/webp',
|
||||
/** If specified, we'll use the DEFAULT_ADRAFT_IMAGE_QUALITY */
|
||||
convertToLossyQuality?: number, // 0-1, only used if convertToMimeType is lossy (jpeg or webp)
|
||||
@@ -137,8 +143,8 @@ export async function imageBlobTransform(inputImage: Blob, options: ImageTransfo
|
||||
// 1. Resize & Format-convert image if requested
|
||||
let hasResized = false;
|
||||
let hasTypeConverted = false;
|
||||
const toMimeType = options.convertToMimeType || DEFAULT_ADRAFT_IMAGE_MIMETYPE;
|
||||
const toLossyQuality = options.convertToLossyQuality ?? DEFAULT_ADRAFT_IMAGE_QUALITY;
|
||||
const toMimeType = options.convertToMimeType || PLATFORM_IMAGE_MIMETYPE;
|
||||
const toLossyQuality = options.convertToLossyQuality ?? DEFAULT_LOSSY_QUALITY;
|
||||
if (options.resizeMode) {
|
||||
|
||||
// if null, resizing was not needed or possible (size could not be a fit)
|
||||
@@ -252,6 +258,12 @@ export async function imageBlobConvertType(imageBlob: Blob, toMimeType: CommonIm
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx)
|
||||
return reject(new Error('Failed to get canvas context for conversion'));
|
||||
|
||||
if (HQ_SMOOTHING) {
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
}
|
||||
|
||||
ctx.drawImage(image, 0, 0);
|
||||
|
||||
// Convert canvas to Blob with validation
|
||||
@@ -401,17 +413,41 @@ export async function imageBlobResizeIfNeeded(imageBlob: Blob, resizeMode: LLMIm
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx)
|
||||
return reject(new Error('Failed to get canvas context for resizing'));
|
||||
// multi-pass downscaling for better quality on large downscales (>2x)
|
||||
// progressively downscale by at most 2x per pass to reduce aliasing
|
||||
const scaleRatio = Math.max(originalWidth / newWidth, originalHeight / newHeight);
|
||||
const passes = (HQ_SMOOTHING && scaleRatio > 2) ? Math.min(4, Math.ceil(Math.log2(scaleRatio))) : 1;
|
||||
|
||||
canvas.width = newWidth;
|
||||
canvas.height = newHeight;
|
||||
ctx.drawImage(image, 0, 0, newWidth, newHeight);
|
||||
let currentDest: HTMLImageElement | HTMLCanvasElement = image;
|
||||
let currentWidth = originalWidth;
|
||||
let currentHeight = originalHeight;
|
||||
|
||||
for (let pass = 0; pass < passes; pass++) {
|
||||
const isLastPass = pass === passes - 1;
|
||||
const targetWidth = isLastPass ? newWidth : Math.max(newWidth, Math.round(currentWidth / 2));
|
||||
const targetHeight = isLastPass ? newHeight : Math.max(newHeight, Math.round(currentHeight / 2));
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx)
|
||||
return reject(new Error('Failed to get canvas context for resizing'));
|
||||
|
||||
if (HQ_SMOOTHING) {
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
}
|
||||
ctx.drawImage(currentDest, 0, 0, targetWidth, targetHeight);
|
||||
|
||||
currentDest = canvas;
|
||||
currentWidth = targetWidth;
|
||||
currentHeight = targetHeight;
|
||||
}
|
||||
const finalCanvas = currentDest as HTMLCanvasElement;
|
||||
|
||||
// Convert canvas to Blob with validation
|
||||
asyncCanvasToBlobWithValidation(canvas, toMimeType, toLossyQuality, 'imageBlobResizeIfNeeded')
|
||||
asyncCanvasToBlobWithValidation(finalCanvas, toMimeType, toLossyQuality, 'imageBlobResizeIfNeeded')
|
||||
.then(({ blob }) => resolve({ blob, width: newWidth, height: newHeight }))
|
||||
.catch((reason) => reject(new Error(`Failed to resize image to '${resizeMode}' as '${toMimeType}': ${reason instanceof Error ? reason.message : String(reason)}`)));
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Object utility functions optimized for performance
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if an object has any enumerable keys (faster than Object.keys().length > 0)
|
||||
*
|
||||
* Performance: ~2ns per call vs ~10ns for Object.keys().length
|
||||
* Benchmark: /tmp/benchmark-helper-vs-inline.html
|
||||
*
|
||||
* Note: Returns a boolean, automatically narrows out null/undefined through truthiness check
|
||||
*/
|
||||
export function hasKeys(obj: object | null | undefined): boolean {
|
||||
if (!obj) return false;
|
||||
// noinspection LoopStatementThatDoesntLoopJS
|
||||
for (const _ in obj) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the number of enumerable keys in an object (faster than Object.keys().length)
|
||||
*
|
||||
* Performance: O(n) where n is number of keys, but avoids array allocation
|
||||
*/
|
||||
export function countKeys(obj: object | null | undefined): number {
|
||||
if (!obj) return 0;
|
||||
let count = 0;
|
||||
for (const _ in obj) count++;
|
||||
return count;
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import * as z from 'zod/v4';
|
||||
|
||||
import type { AixAPIChatGenerate_Request } from '~/modules/aix/server/api/aix.wiretypes';
|
||||
import { aixCGR_ChatSequence_FromDMessagesOrThrow, aixCGR_SystemMessageText } from '~/modules/aix/client/aix.client.chatGenerateRequest';
|
||||
import { aixChatGenerateContent_DMessage, aixCreateChatGenerateContext } from '~/modules/aix/client/aix.client';
|
||||
import { aixChatGenerateContent_DMessage_orThrow, aixCreateChatGenerateContext } from '~/modules/aix/client/aix.client';
|
||||
import { aixFunctionCallTool, aixRequireSingleFunctionCallInvocation } from '~/modules/aix/client/aix.client.fromSimpleFunction';
|
||||
|
||||
import { createTextContentFragment, DMessageAttachmentFragment, isImageRefPart, isZyncAssetImageReferencePart } from '~/common/stores/chat/chat.fragments';
|
||||
@@ -67,7 +67,7 @@ Analyze the provided content to determine its nature, identify any relationships
|
||||
toolsPolicy: { type: 'any' },
|
||||
} as const;
|
||||
|
||||
const { fragments } = await aixChatGenerateContent_DMessage(
|
||||
const { fragments } = await aixChatGenerateContent_DMessage_orThrow(
|
||||
llmId,
|
||||
aixChatGenerate,
|
||||
aixCreateChatGenerateContext('chat-attachment-prompts', attachmentFragments[0].fId),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as z from 'zod/v4';
|
||||
|
||||
import type { AixAPIChatGenerate_Request } from '~/modules/aix/server/api/aix.wiretypes';
|
||||
import { aixChatGenerateContent_DMessage, aixCreateChatGenerateContext } from '~/modules/aix/client/aix.client';
|
||||
import { aixChatGenerateContent_DMessage_orThrow, aixCreateChatGenerateContext } from '~/modules/aix/client/aix.client';
|
||||
import { aixCGR_FromSimpleText } from '~/modules/aix/client/aix.client.chatGenerateRequest';
|
||||
import { aixFunctionCallTool, aixRequireSingleFunctionCallInvocation } from '~/modules/aix/client/aix.client.fromSimpleFunction';
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function agiFixupCode(issueType: CodeFixType, codeToFix: string, er
|
||||
};
|
||||
|
||||
// Invoke the AI model
|
||||
const { fragments } = await aixChatGenerateContent_DMessage(
|
||||
const { fragments } = await aixChatGenerateContent_DMessage_orThrow(
|
||||
llmId,
|
||||
aixRequest,
|
||||
aixCreateChatGenerateContext('fixup-code', '_DEV_'),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user