Compare commits

..

1 Commits

Author SHA1 Message Date
Enrico Ros 6ba1afa540 ChatDrawer: virtualize scroll (for very numerous chats) 2025-01-15 00:49:43 -08:00
691 changed files with 14699 additions and 52842 deletions
-20
View File
@@ -1,20 +0,0 @@
---
description: Increment the AIX monotonic version number
allowed-tools: Bash(git add:*),Bash(git status:*),Bash(git commit:*),Edit,Write
model: haiku
disable-model-invocation: true
---
Increment `Monotonics.Aix` in `src/common/app.release.ts` and commit it.
**Pre-flight checks (MUST pass or abort):**
1. Run `git branch --show-current` - MUST be on `main` branch
2. Run `git status src/common/app.release.ts` - file MUST be unmodified (no changes on this specific file)
**Execute:**
1. Read current `Monotonics.Aix` value from `src/common/app.release.ts`
2. Increment by 1
3. Update ONLY that line
4. Run: `git add src/common/app.release.ts && git commit -m "Roll AIX"`
Confirm new version number.
@@ -1,31 +0,0 @@
---
description: Sync Anthropic API implementation with latest upstream documentation
argument-hint: specific feature to check
---
Please take a look at my API code for Anthropic: message wire types `src/modules/aix/server/dispatch/wiretypes/anthropic.wiretypes.ts`, assembly of the request messages (adapters) `src/modules/aix/server/dispatch/chatGenerate/adapters/anthropic.messageCreate.ts`, and parsing of the response in streaming or not `src/modules/aix/server/dispatch/chatGenerate/parsers/anthropic.parser.ts`.
IMPORTANT: we only support the Messages API (message create). We do NOT support other APIs such as the older Completions API.
We support Anthropic caching natively, and want to make sure tools and state (crafting the history) are also done well.
Then take a look at the newest API information available. Try these sources, and be creative if some are blocked:
**Primary Sources:**
- Docs API: https://docs.claude.com/en/api/messages
- Release notes: https://docs.claude.com/en/release-notes/api
- Tools use: https://docs.claude.com/en/docs/agents-and-tools/tool-use/overview
- Handling stop reasons: https://docs.claude.com/en/api/handling-stop-reasons
**Alternative Sources if primary blocked:**
- Anthropic TypeScript SDK: https://github.com/anthropics/anthropic-sdk-typescript
- Anthropic Python SDK: https://github.com/anthropics/anthropic-sdk-python
- Recent news and announcements: Web Search for "anthropic api changelog" or "new claude api" or "new claude api pricing"
**If all blocked:** Explain what you attempted and ask user to provide documentation manually.
$ARGUMENTS
Check carefully and look if there are any discrepancies in the protocols, the available API surface, the structure of the messages, functionality, logic, etc.
Make sure you look deep in the fields of the requests and responses, especially required fields, streaming event types, and any new response shapes.
Please point out all of the differences in the API whether it's in the final parsing and reassembly of the streaming message, or the protocol changed, etc.
Prioritize breaking changes and new capabilities that would improve the user experience.
-30
View File
@@ -1,30 +0,0 @@
---
description: Sync Google Gemini API implementation with latest upstream documentation
argument-hint: specific feature to check
---
Please take a look at my API code for Google Gemini: message wire types `src/modules/aix/server/dispatch/wiretypes/gemini.wiretypes.ts`, assembly of the request messages (adapters) `src/modules/aix/server/dispatch/chatGenerate/adapters/gemini.generateContent.ts`, and parsing of the response in streaming or not `src/modules/aix/server/dispatch/chatGenerate/parsers/gemini.parser.ts`.
IMPORTANT: we only support the generateContent API, not other Gemini APIs such as embeddings, etc.
Caching is only supported when implicit, we do not explicitly manage Gemini Caches. Same for file uploads and other systems.
Image generation happens through models, i.e. 'Gemini 2.5 Flash - Nano Banana' generates images using AIX from generateContent (chat input).
Then take a look at the newest API information available. Try these sources, and be creative if some are blocked:
**Primary Sources:**
- Docs API 1/2: https://ai.google.dev/api/generate-content
- Docs API 2/2: https://ai.google.dev/api/caching#Content
- Release notes: https://ai.google.dev/gemini-api/docs/changelog
**Alternative Sources if primary blocked:**
- Google AI JavaScript SDK: https://github.com/googleapis/js-genai (check latest commits, README, type definitions)
Recent news and announcements: Web Search for "gemini api changelog" or "nwe gemini api updates" or "new gemini api pricing"
**If all blocked:** Explain what you attempted and ask user to provide documentation manually.
$ARGUMENTS
Check carefully and look if there are any discrepancies in the protocols, the available API surface, the structure of the messages, functionality, logic, etc.
Make sure you look deep in the fields of the requests and responses, especially required fields, streaming event types, and any new response shapes.
Please point out all of the differences in the API whether it's in the final parsing and reassembly of the streaming message, or the protocol changed, etc.
Prioritize breaking changes and new capabilities that would improve the user experience.
-34
View File
@@ -1,34 +0,0 @@
---
description: Sync OpenAI API implementation with latest upstream documentation
argument-hint: specific feature to check
---
Please take a look at my API code for OpenAI: message wire types `src/modules/aix/server/dispatch/wiretypes/openai.wiretypes.ts`, assembly of the request messages (adapters) `src/modules/aix/server/dispatch/chatGenerate/adapters/openai.chatCompletions.ts`, and parsing of the response in streaming or not `src/modules/aix/server/dispatch/chatGenerate/parsers/openai.parser.ts`.
IMPORTANT: we prioritize the new Responses API, while Chat Completions is still supported but legacy.
We do NOT support other APIs such as Realtime (incl. websockets), etc.
We also do not support Agentic APIs (Agent SDK, AgentKit, ChatKit, Assistants API etc), as we perform similar functionality in AIX (server or client side).
Then take a look at the newest API information available. Try these sources, and be creative if some are blocked:
**Primary Sources:**
- Responses API (AIX prioritizes it): https://platform.openai.com/docs/api-reference/responses/create
- Chat Completions API: https://platform.openai.com/docs/api-reference/chat/create
- Changelog: https://platform.openai.com/docs/changelog
- Models: https://platform.openai.com/docs/models
- Pricing (use Copy Page button to download markdown): https://platform.openai.com/docs/pricing
**Alternative Sources if primary blocked:**
- OpenAI Node.js SDK: https://github.com/openai/openai-node
- OpenAI Python SDK: https://github.com/openai/openai-python
- OpenAI OpenAPI spec: https://github.com/openai/openai-openapi
Recent news and announcements: Web Search for "openai api changelog" or "openai new models" or "openai new prices"
**If all blocked:** Explain what you attempted and ask user to provide documentation manually.
$ARGUMENTS
Check carefully and look if there are any discrepancies in the protocols, the available API surface, the structure of the messages, functionality, logic, etc.
Make sure you look deep in the fields of the requests and responses, especially required fields, streaming event types, and any new response shapes.
Please point out all of the differences in the API whether it's in the final parsing and reassembly of the streaming message, or the protocol changed, etc.
Prioritize breaking changes and new capabilities that would improve the user experience.
@@ -1,49 +0,0 @@
---
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`
@@ -1,20 +0,0 @@
---
description: Update Alibaba model definitions with latest pricing and capabilities
---
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.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
- Billing Guide: https://www.alibabacloud.com/help/en/model-studio/billing-for-model-studio
**Fallbacks if blocked:**
- Search "alibaba model studio latest pricing", "alibaba latest models", "qwen models pricing", 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
@@ -1,20 +0,0 @@
---
description: Update Anthropic model definitions with latest pricing and capabilities
---
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.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
- Pricing: https://claude.com/pricing#api
- Deprecations: https://docs.claude.com/en/docs/about-claude/model-deprecations
**Fallbacks if blocked:** Check Anthropic TypeScript SDK at https://github.com/anthropics/anthropic-sdk-typescript, search "anthropic models latest pricing", "anthropic latest 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
@@ -1,22 +0,0 @@
---
description: Update DeepSeek model definitions with latest pricing and capabilities
---
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.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
- Model List: https://api-docs.deepseek.com/api/list-models
- Release Notes: https://api-docs.deepseek.com/updates (check for version updates like V3.2-Exp)
**Note:** DeepSeek frequently releases new versions with significant pricing changes. Always check release notes first.
**Fallbacks if blocked:** Search "deepseek api latest pricing", "deepseek latest models", "deepseek models list" 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
@@ -1,21 +0,0 @@
---
description: Update Gemini model definitions with latest pricing and capabilities
---
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.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
- Pricing: https://ai.google.dev/gemini-api/docs/pricing
- Changelog: https://ai.google.dev/gemini-api/docs/changelog
**Fallbacks if blocked:** Check Google AI JS SDK at https://github.com/googleapis/js-genai, search "gemini models latest pricing", "gemini latest models", or search GitHub for latest model prices and context windows
**Important:**
- Ignore context windows (auto-determined at runtime) and training cutoffs (not supported)
- 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, do NOT remove comments
- Flag broken links or unexpected content
@@ -1,19 +0,0 @@
---
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.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
- Pricing: https://groq.com/pricing/
**Fallbacks if blocked:** Search "groq models latest pricing", "groq latest models", "groq 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
@@ -1,19 +0,0 @@
---
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
@@ -1,24 +0,0 @@
---
description: Update Mistral model definitions with latest pricing and capabilities
---
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.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/
- Pricing: https://mistral.ai/pricing#api-pricing
- Changelog: https://docs.mistral.ai/getting-started/changelog/
**Fallbacks if blocked:**
- Search "mistral [model-name] latest pricing", "mistral api latest pricing", "mistral latest models", or search GitHub for latest model prices and context windows
- Cross-reference: pricepertoken.com, helicone.ai, artificialanalysis.ai
- Check Mistral API list models response
- As last resort: Use Chrome DevTools MCP to render pricing table
**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
@@ -1,41 +0,0 @@
---
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.mappings.ts` for context only. Focus on the model file, do not descend into other code.
**Automated Workflow:**
```bash
# 1. Fetch the HTML
curl -s "https://ollama.com/library?sort=featured" -o /tmp/ollama-featured.html
# 2. Parse it with the script
node .claude/scripts/parse-ollama-models.js > /tmp/ollama-parsed.txt 2>&1
# 3. Review the parsed output
cat /tmp/ollama-parsed.txt
```
The parser outputs: `modelName|pulls|capabilities|sizes`
- Example: `deepseek-r1|66200000|tools,thinking|1.5b,7b,8b,14b,32b,70b,671b`
**Primary Sources:**
- Model Library: https://ollama.com/library?sort=featured
- Parser script: `.claude/scripts/parse-ollama-models.js`
**Fallbacks if blocked:** Check https://github.com/ollama/ollama, search "ollama featured models", "ollama latest models", or search GitHub for latest model info
**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
- Set today's date (YYYYMMDD format) for newly added models only
- Update OLLAMA_LAST_UPDATE constant to today's date
- Do NOT change dates of existing models
- Review the full model list for additions, removals, and changes
- Minimize whitespace/comment changes, focus on content
- Preserve comments and newlines to make diffs easy to review
@@ -1,26 +0,0 @@
---
description: Update OpenAI model definitions with latest pricing and capabilities
---
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.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.
**Primary Sources:**
- Models: https://platform.openai.com/docs/models (use Copy Page button)
- Pricing: https://platform.openai.com/docs/pricing (expand tables first)
**Known Issue:** OpenAI docs block automated access (403 Forbidden). Manual browser access required.
**Fallbacks if blocked:**
- Search "openai models latest pricing", "openai latest models" for third-party aggregators, or search GitHub for latest model prices and context windows
- OpenAI Node SDK (https://github.com/openai/openai-node) has limited model metadata only
- As last resort: Use Chrome DevTools MCP to navigate and extract from official docs
**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
@@ -1,19 +0,0 @@
---
description: Update OpenPipe model definitions with latest pricing and capabilities
---
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.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
- Pricing: https://docs.openpipe.ai/pricing/pricing
**Fallbacks if blocked:** Search "openpipe models latest pricing", "openpipe latest models", "openpipe base 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
@@ -1,20 +0,0 @@
---
description: Update Perplexity model definitions with latest pricing and capabilities
---
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.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
- Pricing: https://docs.perplexity.ai/getting-started/pricing
- Changelog: https://docs.perplexity.ai/changelog/changelog
**Fallbacks if blocked:** Search "perplexity api latest pricing", "perplexity latest 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
@@ -1,23 +0,0 @@
---
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.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
**Known Issue:** docs.x.ai blocks automated access (403 Forbidden). Use fallbacks below.
**Fallbacks if blocked:**
- Search "xai grok latest pricing", "xai latest models", "xai api models", or search GitHub for latest model prices and context windows
- Random sites? https://the-rogue-marketing.github.io/grok-api-latest-llms-pricing-october-2025/ (find a newer version), https://langdb.ai/app/providers/xai/ (browse by model, limited coverage)
- As last resort: Use Chrome DevTools MCP to access docs.x.ai
**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
-81
View File
@@ -1,81 +0,0 @@
#!/usr/bin/env node
/**
* Parse Ollama featured models from HTML
*
* Usage:
* 1. Fetch HTML: curl -s "https://ollama.com/library?sort=featured" -o /tmp/ollama-featured.html
* 2. Parse: node .claude/scripts/parse-ollama-models.js
*
* Outputs: pipe-delimited format: modelName|pulls|capabilities|sizes
* Example: deepseek-r1|66200000|tools,thinking|1.5b,7b,8b,14b,32b,70b,671b
*/
const fs = require('fs');
const htmlPath = process.argv[2] || '/tmp/ollama-featured.html';
if (!fs.existsSync(htmlPath)) {
console.error(`Error: HTML file not found at ${htmlPath}`);
console.error('Please fetch it first with:');
console.error(' curl -s "https://ollama.com/library?sort=featured" -o /tmp/ollama-featured.html');
process.exit(1);
}
const html = fs.readFileSync(htmlPath, 'utf8');
// Split into model sections - each starts with <a href="/library/
const modelSections = html.split(/<a href="\/library\//);
const models = [];
for (let i = 1; i < modelSections.length; i++) {
const section = modelSections[i].substring(0, 5000); // Large enough window to capture all data
// Extract model name (first quoted string)
const nameMatch = section.match(/^([^"]+)"/);
if (!nameMatch) continue;
const name = nameMatch[1];
// Extract pulls using x-test-pull-count
const pullsMatch = section.match(/x-test-pull-count>([^<]+)</);
let pulls = 0;
if (pullsMatch) {
const pullStr = pullsMatch[1].replace(/,/g, '');
if (pullStr.includes('M')) {
pulls = Math.floor(parseFloat(pullStr) * 1000000);
} else if (pullStr.includes('K')) {
pulls = Math.floor(parseFloat(pullStr) * 1000);
} else {
pulls = parseInt(pullStr);
}
}
// Extract capabilities (tools, vision, embedding, thinking, cloud)
const capabilities = [];
const capabilityRegex = /x-test-capability[^>]*>([^<]+)</g;
let capMatch;
while ((capMatch = capabilityRegex.exec(section)) !== null) {
capabilities.push(capMatch[1].trim());
}
// Extract sizes (1.5b, 7b, etc.)
const sizes = [];
const sizeRegex = /x-test-size[^>]*>([^<]+)</g;
let sizeMatch;
while ((sizeMatch = sizeRegex.exec(section)) !== null) {
sizes.push(sizeMatch[1].trim());
}
// Only include models with 50K+ pulls
if (pulls >= 50000) {
models.push({ name, pulls, capabilities, sizes });
}
}
// Output in pipe-delimited format (in the order they appear on the page)
models.forEach(m => {
const caps = m.capabilities.join(',');
const tags = m.sizes.join(',');
console.log(`${m.name}|${m.pulls}|${caps}|${tags}`);
});
console.error(`\nTotal models with 50K+ pulls: ${models.length}`);
-40
View File
@@ -1,40 +0,0 @@
{
"permissions": {
"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:*)",
"Bash(grep:*)",
"Bash(ls:*)",
"Bash(mkdir:*)",
"Bash(node:*)",
"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",
"mcp__chrome-devtools",
"mcp__github",
"mcp__ide__getDiagnostics"
],
"deny": [
"Read(node_modules)",
"Read(node_modules/**)"
]
}
}
+3
View File
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
-70
View File
@@ -1,70 +0,0 @@
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
+2 -19
View File
@@ -5,29 +5,14 @@ labels: [ 'type: bug' ]
body:
- type: markdown
attributes:
value: Thank you for reporting a bug. Please help us by providing accurate environment information.
- type: dropdown
attributes:
label: Environment
description: (required) Where are you experiencing this issue?
options:
- Big-AGI Pro (big-agi.com)
- Self-deployed from GitHub
- Docker container (specify in description)
- Local development
- Other
validations:
required: true
value: Thank you for reporting a bug.
- type: textarea
attributes:
label: Description
description: (required) Please provide a clear description and **steps to reproduce**.
description: (required) Please provide a clear description. Please also provide the steps to reproduce.
placeholder: 'Concise description + steps to reproduce.'
validations:
required: true
- type: textarea
attributes:
label: Device and browser
@@ -35,12 +20,10 @@ body:
placeholder: 'Device: (e.g., iPhone 16, Pixel 9, PC, Macbook...), OS: (e.g., iOS 17, Windows 12), Browser: (e.g., Chrome 119, Safari 18, Firefox..)'
validations:
required: true
- type: textarea
attributes:
label: Screenshots and more
placeholder: 'Attach screenshots, or add any additional context here.'
- type: checkboxes
attributes:
label: Willingness to Contribute
@@ -32,6 +32,7 @@ assignees: enricoros
- [ ] verify deployment on Vercel
- [ ] verify container on GitHub Packages
- [ ] update the GitHub release
- [ ] push as stable `git push opensource main:main-stable`
- Announce:
- [ ] Discord announcement
- [ ] Twitter announcement
@@ -50,7 +51,7 @@ To familiarize yourself with the application, the following are the Website and
```
- paste the URL: https://big-agi.com
- drag & drop: [README.md](https://raw.githubusercontent.com/enricoros/big-AGI/main/README.md)
- drag & drop: [README.md](https://raw.githubusercontent.com/enricoros/big-AGI/v2-dev/README.md)
```markdown
I am announcing a new version, 1.2.3.
-57
View File
@@ -1,57 +0,0 @@
name: Claude Code DM
on:
issues:
types: [opened, assigned]
issue_comment:
types: [created]
pull_request_review:
types: [submitted]
pull_request_review_comment:
types: [created]
jobs:
claude-dm:
if: |
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) ||
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude'))
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
pull-requests: write
issues: write
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude Code DM Response
id: claude
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Security: Only users with write access can trigger (DMs allow code execution)
# This is an optional setting that allows Claude to read CI results on PRs
additional_permissions: |
actions: read
# Optional: Add claude_args to customize behavior and configuration
# 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: '--allowed-tools Bash(gh pr:*)'
# disabling opus for now claude-opus-4-1-20250805
claude_args: |
--model claude-sonnet-4-5-20250929
--max-turns 100
--allowedTools "Edit,Read,Write,WebFetch,WebSearch,Bash(cat:*),Bash(cp:*),Bash(find:*),Bash(git branch:*),Bash(grep:*),Bash(ls:*),Bash(mkdir:*),Bash(npm install),Bash(npm install:*),Bash(npm run:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh label:*),Bash(gh pr:*),mcp__chrome-devtools,SlashCommand"
-77
View File
@@ -1,77 +0,0 @@
name: Claude Code Auto-Triage Issues
on:
issues:
types: [ opened, assigned ]
jobs:
claude-issue-triage:
# Optional: Skip for bot users and direct mentions in the body (handled by claude-dm.yml)
if: |
github.event.issue.user.type != 'Bot' &&
!contains(github.event.issue.body, '@claude')
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
issues: write
pull-requests: write
id-token: write
actions: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Analyze issue and provide help
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Security: Allow any user to trigger triage (automated issue help is safe)
github_token: ${{ secrets.GITHUB_TOKEN }}
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 }}
A user has reported an issue. Please help them by:
1. Deep think about the issue:
**Understand the problem**: Analyze the issue description and any error messages
**Search for context**:
- Use the repository's CLAUDE.md for high level guidance and especially kb/ documentation
- Look in relevant code files, including kb/ documentation
**Use web search**: When potentially outside Big-AGI (e.g. user configuration), search the web for similar errors or related issues
**Provide a solution**:
- Provide multiple solutions if uncertain, and say so
- If you can fix it in code, propose the fix
- If possible also suggest fixes or workarounds for immediate relief
- Reference specific files and line numbers
- Test selectively and even npm install and run build if needed to verify the solution
2. Always add the 'claude-triage' issue label to indicate this issue was triaged by Claude
3. Comment with:
- Very brief thank you note, if applicable
- Initial assessment
- Next steps or clarification needed
- 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 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"
-77
View File
@@ -1,77 +0,0 @@
name: Claude Code PR Review
on:
pull_request:
types: [ opened, synchronize, ready_for_review ]
# Limit branches
branches: [ main, dev, v1 ]
# Optional: Only run on specific file changes
# paths:
# - "src/**/*.ts"
# - "src/**/*.tsx"
jobs:
claude-pr-review:
# Skip draft PRs
# Optional: filter authors: github.event.pull_request.user.login != 'enricoros'
if: |
github.event.pull_request.draft == false
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
pull-requests: write
issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run PR Review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Security: Allow any user to trigger reviews (read-only PR analysis is safe)
github_token: ${{ secrets.GITHUB_TOKEN }}
allowed_non_write_users: '*'
# track_progress: true # Enables tracking comments
# This setting allows Claude to read CI results on PRs
additional_permissions: |
actions: read
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
Please review this pull request and provide feedback on:
- Potential bugs or issues
- Adherence to Big-AGI architecture and design patterns
- Code quality and best practices, including TypeScript types, error handling, and edge cases
- Performance considerations: bundle size, React patterns, streaming efficiency
- Security concerns if applicable
Use the repository's CLAUDE.md for guidance on style and conventions.
Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.
Use `gh pr review comment` for inline suggestions on specific lines.
IMPORTANT: After completing your review, always add the 'claude-review' label to the PR to indicate it was reviewed by Claude:
gh pr edit ${{ github.event.pull_request.number }} --add-label "claude-review"
Be constructive, helpful, no-BS, and specific with file:line references.
# 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 100
--allowedTools "Edit,Read,Write,WebFetch,WebSearch,Bash(cat:*),Bash(cp:*),Bash(find:*),Bash(git branch:*),Bash(grep:*),Bash(ls:*),Bash(mkdir:*),Bash(npm install),Bash(npm install:*),Bash(npm run:*),Bash(gh issue:*),Bash(gh search:*),Bash(gh label:*),Bash(gh pr:*),mcp__chrome-devtools"
+11 -19
View File
@@ -12,9 +12,11 @@ name: Create and publish Docker images
on:
push:
branches:
- main # Primary branch (Big-AGI Open)
- v2-dev
#- v1-dev # Disabled because this is not needed anymore
#- v1-stable # Disabled as the v* tag is used for stable releases
tags:
- 'v2.*' # Stable releases (v2.0.0, v2.1.0, etc.)
- 'v*' # Trigger on version tags (e.g., v1.7.0)
env:
REGISTRY: ghcr.io
@@ -23,7 +25,6 @@ env:
jobs:
build-and-push-image:
runs-on: ubuntu-latest
timeout-minutes: 60 # Max 1 hour (expected: ~25min)
permissions:
contents: read
packages: write
@@ -54,21 +55,14 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
# Development: main branch
type=raw,value=development,enable=${{ github.ref == 'refs/heads/main' }}
# Latest: v2.x releases (safe default)
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v2.') }}
# Stable: v2.x releases (alias)
type=raw,value=stable,enable=${{ startsWith(github.ref, 'refs/tags/v2.') }}
# Version tags (v2.0.0, 2.0.0)
type=ref,event=tag
type=semver,pattern={{version}}
type=raw,value=development,enable=${{ github.ref == 'refs/heads/v2-dev' }} # For v2-dev branch
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/v1-stable' }}
type=ref,event=tag # Use the tag name as a tag for tag builds
type=semver,pattern={{version}} # Generate semantic versioning tags for tag builds
type=sha,format=short,prefix=sha- # Just in case none of the above applies
labels: |
org.opencontainers.image.title=Big-AGI Open
org.opencontainers.image.description=Big-AGI Open - Multi-model AI workspace for experts who need to think broader, decide smarter, and build with confidence.
org.opencontainers.image.title=Big-AGI
org.opencontainers.image.description=Generative AI suite powered by state-of-the-art models
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.documentation=https://big-agi.com
@@ -83,8 +77,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
build-args: |
NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
NEXT_PUBLIC_BUILD_HASH=${{ github.sha }}
NEXT_PUBLIC_BUILD_REF_NAME=${{ github.ref_name }}
# Enable build cache (future)
#cache-from: type=gha
#cache-to: type=gha,mode=max
+3
View File
@@ -0,0 +1,3 @@
overrides=@mui/material@^5.0.0:
dependencies:
@mui/material: replaced-by=@mui/joy
-242
View File
@@ -1,242 +0,0 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
```bash
# Targeted Code Quality (safe while dev server runs)
npx tsc --noEmit # Type check without building
npx eslint src/path/to/file.ts # Lint specific file
npm run lint # Lint entire project
```
## Architecture Overview
Big-AGI is a Next.js 15 application with a modular architecture built for advanced AI interactions. The codebase follows a three-layer structure with distinct separation of concerns.
### Core Directory Structure
```
/app/api/ # Next.js App Router (API routes only, mostly -> /src/server/)
/pages/ # Next.js Pages Router (file-based, mostly -> /src/apps/)
/src/
├── apps/ # Feature applications (self-contained modules)
├── modules/ # Reusable business logic and integrations
├── common/ # Shared infrastructure and utilities
└── server/ # Backend API layer with tRPC
/kb/ # Knowledge base for modules, architectures
```
### Key Technologies
- **Frontend**: Next.js 15, React 18, Material-UI Joy, Emotion (CSS-in-JS)
- **State Management**: Zustand with localStorge/IndexedDB (single cell) persistence
- **API Layer**: tRPC with React Query for type-safe communication
- **Runtime**: Edge Runtime for AI operations, Node.js for data processing
### Apps Architecture Pattern
Each app in `/src/apps/` is a self-contained feature module:
- Main component (`App*.tsx`)
- Local state store (`store-app-*.ts`)
- Feature-specific components and layouts
- Runtime configurations
Example apps: `chat/`, `call/`, `beam/`, `draw/`, `personas/`, `settings-modal/`
### Modules Architecture Pattern
Modules in `/src/modules/` provide reusable business logic:
- **`aix/`** - AI communication framework for real-time streaming
- **`beam/`** - Multi-model AI reasoning system (scatter/gather pattern)
- **`blocks/`** - Content rendering (markdown, code, images, etc.)
- **`llms/`** - Language model abstraction supporting 16 vendors
### Key Subsystems & Their Patterns
#### 1. AIX - Real-time AI Communication
**Location**: `/src/modules/aix/`
**Pattern**: Client-server streaming architecture with provider abstraction
- **Client** → tRPC → **Server****AI Providers**
- Handles streaming/non-streaming responses with batching and error recovery
- Particle-based streaming: `AixWire_Particles``ContentReassembler``DMessage`
- Provider-agnostic through adapter pattern (OpenAI, Anthropic, Gemini protocols)
#### 3. Beam - Multi-Model Reasoning
**Location**: `/src/modules/beam/`
**Pattern**: Scatter/Gather for parallel AI processing
- **Scatter**: Multiple models (rays) process input in parallel
- **Gather**: Fusion algorithms combine outputs
- Real-time UI updates via vanilla Zustand stores
- BeamStore per conversation via ConversationHandler
#### 4. Conversation Management
**Location**: `/src/common/stores/chat/` and `/src/common/chat-overlay/`
**Pattern**: Overlay architecture with handler per conversation
- `ConversationHandler` orchestrates chat, beam, ephemerals
- Per-chat stores: `PerChatOverlayStore` + `BeamStore`
- Message structure: `DMessage``DMessageFragment[]`
- Supports multi-pane with independent conversation states
### Storage System
Big-AGI uses a local-first architecture with Zustand + IndexedDB:
- **Zustand** stores for in-memory state management
- **localStorage** for persistent settings/all storage (via Zustand persist middleware)
- **IndexedDB** for persistent chat-only storage (via Zustand persist middleware) on a single key-val cell
- **Local-first** architecture with offline capability
- **Migration system** for upgrading data structures across versions
Key storage patterns:
- Stores use `createIDBPersistStorage()` for IndexedDB persistence
- Version-based migrations handle data structure changes
- Partialize/merge functions control what gets persisted
- Rehydration logic repairs and upgrades data on load
Located in `/src/common/stores/` with stores like:
- `chat/store-chats.ts`: Conversations and messages
- `llms/store-llms.ts`: Model configurations
### Layout System ("Optima")
The Optima layout system provides:
- **Responsive design** adapting desktop/mobile
- **Drawer/Panel/Toolbar** composition
- **Split-pane support** for multi-conversation views
- **Portal-based rendering** for flexible component placement
Located in `/src/common/layout/optima/`
### State Management Patterns
1. **Global Stores** (Zustand with IndexedDB persistence)
- `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
- `store-perchat_vanilla`: Chat overlay state
- High-performance, no React integration
3. **Module Stores**
- Feature-specific configuration and state
- Example: `store-module-beam`, `store-module-t2i`
### User Flows & Interdependencies
#### Chat Message Flow
1. User input → `Composer``DMessage` creation
2. `ConversationHandler.messageAppend()` → Store update
3. `_handleExecute()` / `ConversationHandler.executeChatMessages()` → AIX client request
4. AIX streaming → `ContentReassembler` → UI updates
5. Zustand auto-persistence → IndexedDB
#### Beam Multi-Model Flow
1. User triggers Beam → `BeamStore.open()` state update
2. Scatter: Parallel `aixChatGenerateContent()` to N models
3. Real-time ray updates → UI progress
4. Gather: User selects fusion → Combined output
5. Result → New message in conversation
### Development Patterns
#### Module Integration
- Each module exports its functionality through index files
- Modules register with central registries (e.g., `vendors.registry.ts`)
- Configuration objects define module behavior
- Type-safe integration through strict TypeScript interfaces
#### Component Patterns
- **Controlled components** with clear prop interfaces
- **Hook-based logic** extraction for reusability
- **Portal rendering** for overlays and modals
- **Suspense boundaries** for async operations
#### API Patterns
- **tRPC routers** for type-safe API endpoints
- **Zod schemas** for runtime validation
- **Middleware** for request/response processing
- **Edge functions** for performance-critical AI operations
## Security Considerations
- API keys stored client-side in localStorage (user-provided)
- Server-side API keys in environment variables only
- XSS protection through proper content escaping
- No credential transmission to third parties
## Knowledge Base
Architecture and system documentation is available in the `/kb/` knowledge base:
@kb/KB.md
## Common Development Tasks
### Testing & Quality
- Run `npm run lint` before committing
- Type-check with `npx tsc --noEmit`
- Test critical user flows manually
### Adding a New LLM Vendor
1. Create vendor in `/src/modules/llms/vendors/[vendor]/`
2. Implement `IModelVendor` interface
3. Register in `vendors.registry.ts`
4. Add environment variables to `env.ts` (if server-side keys needed)
### Debugging Storage Issues
- Check IndexedDB: DevTools → Application → IndexedDB → `app-chats`
- Monitor Zustand state: Use Zustand DevTools
- Check migration logs in console during rehydration
## Code Examples
### AIX Streaming Pattern
```typescript
// Efficient streaming with decimation
aixChatGenerateContent_DMessage(
llmId,
request,
{ abortSignal, throttleParallelThreads: 1 },
async (update, isDone) => {
// Real-time UI updates
}
);
```
### Model Registry Pattern
```typescript
// Registry pattern for extensibility
const MODEL_VENDOR_REGISTRY: Record<ModelVendorId, IModelVendor> = {
openai: ModelVendorOpenAI,
anthropic: ModelVendorAnthropic,
// ... 14 more vendors
};
```
## Server Architecture
The server uses a split architecture with two tRPC routers:
### Edge Network (`trpc.router-edge`)
Distributed edge runtime for low-latency AI operations:
- **AIX** - AI streaming and communication
- **LLM Routers** - Direct vendor integrations (OpenAI, Anthropic, Gemini, Ollama)
- **External Services** - ElevenLabs (TTS), Google Search, YouTube transcripts
Located at `/src/server/trpc/trpc.router-edge.ts`
### Cloud Network (`trpc.router-cloud`)
Centralized server for data processing operations:
- **Browse** - Web scraping and content extraction
- **Trade** - Import/export functionality (ChatGPT, markdown, JSON)
Located at `/src/server/trpc/trpc.router-cloud.ts`
**Key Pattern**: Edge runtime for AI (fast, distributed), Cloud runtime for data ops (centralized, Node.js)
+5 -21
View File
@@ -1,6 +1,6 @@
# Base
FROM node:22-alpine AS base
ENV NEXT_TELEMETRY_DISABLED=1
ENV NEXT_TELEMETRY_DISABLED 1
# Dependencies
FROM base AS deps
@@ -14,7 +14,7 @@ COPY src/server/prisma ./src/server/prisma
RUN sh -c '[ ! -e /lib/libssl.so.3 ] && ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3 || echo "Link already exists"'
# Install dependencies, including dev (release builds should use npm ci)
ENV NODE_ENV=development
ENV NODE_ENV development
RUN npm ci
@@ -22,32 +22,16 @@ RUN npm ci
FROM base AS builder
WORKDIR /app
# Deployment type marker
ENV NEXT_PUBLIC_DEPLOYMENT_TYPE=docker
# Optional build version arguments at build time
ARG NEXT_PUBLIC_BUILD_HASH
ENV NEXT_PUBLIC_BUILD_HASH=${NEXT_PUBLIC_BUILD_HASH}
ARG NEXT_PUBLIC_BUILD_REF_NAME
ENV NEXT_PUBLIC_BUILD_REF_NAME=${NEXT_PUBLIC_BUILD_REF_NAME}
# Optional argument to configure GA4 at build time (see: docs/deploy-analytics.md)
ARG NEXT_PUBLIC_GA4_MEASUREMENT_ID
ENV NEXT_PUBLIC_GA4_MEASUREMENT_ID=${NEXT_PUBLIC_GA4_MEASUREMENT_ID}
# Optional argument to configure PostHog at build time (see: docs/deploy-analytics.md)
ARG NEXT_PUBLIC_POSTHOG_KEY
ENV NEXT_PUBLIC_POSTHOG_KEY=${NEXT_PUBLIC_POSTHOG_KEY}
# Copy development deps and source
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# link ssl3 for latest Alpine
RUN sh -c '[ ! -e /lib/libssl.so.3 ] && ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3 || echo "Link already exists"'
# Build the application
ENV NODE_ENV=production
ENV NODE_ENV production
RUN npm run build
# Reduce installed packages to production-only
@@ -69,8 +53,8 @@ COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/src/server/prisma ./src/server/prisma
# Minimal ENV for production
ENV NODE_ENV=production
ENV PATH=$PATH:/app/node_modules/.bin
ENV NODE_ENV production
ENV PATH $PATH:/app/node_modules/.bin
# Run as non-root user
USER nextjs
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023-2025 Enrico Ros
Copyright (c) 2023-2024 Enrico Ros
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+91 -238
View File
@@ -1,191 +1,41 @@
<div align="center">
# BIG-AGI 🧠✨
<img width="256" height="256" alt="Big-AGI Logo" src="https://big-agi.com/assets/logo-bright-github.svg" />
Welcome to big-AGI, the AI suite for professionals that need function, form,
simplicity, and speed. Powered by the latest models from 12 vendors and
open-source servers, `big-AGI` offers best-in-class Chats,
[Beams](https://github.com/enricoros/big-AGI/issues/470),
and [Calls](https://github.com/enricoros/big-AGI/issues/354) with AI personas,
visualizations, coding, drawing, side-by-side chatting, and more -- all wrapped in a polished UX.
<h1><a href="https://big-agi.com">Big-AGI</a></h1>
Stay ahead of the curve with big-AGI. 🚀 Pros & Devs love big-AGI. 🤖
[![Use Free ⋅ Go Pro](https://img.shields.io/badge/Use_Free-Get_Pro-d5ec31?style=for-the-badge&logo=rocket&logoColor=white&labelColor=000)](https://big-agi.com)
[![Deploy on Docker](https://img.shields.io/badge/Self--Host-Docker-blue?style=for-the-badge&logo=docker&logoColor=white&labelColor=000)](https://github.com/enricoros/big-AGI/pkgs/container/big-agi)
[![Deploy on Vercel](https://img.shields.io/badge/Vercel-Deploy-blue?style=for-the-badge&logo=vercel&logoColor=white&labelColor=000)](https://vercel.com/new/clone?repository-url=https://github.com/enricoros/big-agi)
[![Discord](https://img.shields.io/discord/1098796266906980422?style=for-the-badge&label=Discord&logo=discord&logoColor=white&labelColor=000000&color=purple)](https://discord.gg/MkH4qj2Jp9)
<br/>
[![GitHub Monthly Commits](https://img.shields.io/github/commit-activity/m/enricoros/big-agi?style=for-the-badge&x=3&logo=github&logoColor=white&label=commits&labelColor=000&color=green)](https://github.com/enricoros/big-agi/commits)
[![GHCR Pulls](https://img.shields.io/badge/ghcr.io-767k_dl-12b76a?style=for-the-badge&logo=Xdocker&logoColor=white&labelColor=000&color=A8E6CF)](https://github.com/enricoros/big-AGI/pkgs/container/big-agi)
[![Contributors](https://img.shields.io/github/contributors/enricoros/big-agi?style=for-the-badge&x=2&logo=Xgithub&logoColor=white&label=cooks&labelColor=000&color=A8E6CF)](https://github.com/enricoros/big-AGI/graphs/contributors)
[![License: MIT](https://img.shields.io/badge/License-MIT-A8E6CF?style=for-the-badge&labelColor=000)](https://opensource.org/licenses/MIT)
<br/>
[![Official Website](https://img.shields.io/badge/BIG--AGI.com-%23096bde?style=for-the-badge&logo=vercel&label=launch)](https://big-agi.com)
[![Open an Issue](https://img.shields.io/badge/Open_Issue-AI_Will_Help-ff8c00?style=for-the-badge&logo=fireship&logoColor=fff&labelColor=8b0000)](https://github.com/enricoros/big-agi/issues/new?template=ai-triage.yml)
> 🚀 Big-AGI 2 is launching Q4 2024. Be the first to experience it before the public release.
>
> 👉 [Apply for Early Access](https://y2rjg0zillz.typeform.com/to/ZSADpr5u?utm_source=gh-2&utm_medium=readme&utm_campaign=ea2)
[//]: # ([![Uptime Robot ratio &#40;30 days&#41;]&#40;https://img.shields.io/uptimerobot/ratio/m801796948-868b22ed7ceaa0acac4dc765?style=for-the-badge&labelColor=000&color=green&#41;]&#40;https://stats.uptimerobot.com/59MXcnmjrM&#41;)
[//]: # ([![Open Version]&#40;https://img.shields.io/github/v/release/enricoros/big-AGI?label=Open+Release&style=flat-square&logo=github&logoColor=white&labelColor=000&#41;]&#40;https://github.com/enricoros/big-AGI/releases/latest&#41;)
[//]: # (![GitHub Stars]&#40;https://img.shields.io/github/stars/enricoros/big-agi?style=flat-square&logo=github&logoColor=white&labelColor=000&color=yellow&#41;)
[//]: # ([![GitHub Forks]&#40;https://img.shields.io/github/forks/enricoros/big-agi?style=flat-square&logo=github&logoColor=white&labelColor=000&#41;]&#40;#&#41;)
[//]: # ([![Follow on X]&#40;https://img.shields.io/twitter/follow/enricoros?style=flat-square&logo=X&logoColor=white&labelColor=000&color=000&#41;]&#40;https://x.com/enricoros&#41;)
Or fork & run on Vercel
</div>
[![Deploy with Vercel](https://vercel.com/button)](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)
<br/>
### New Version
# Big-AGI Open 🧠
This repository contains two main versions:
This is the open-source foundation of **Big-AGI**, ___the multi-model AI workspace for experts___.
- Big-AGI 2: next-generation, bringing the most advanced AI experience
- `v2-dev`: V2 development branch, the exciting one, future default
- Big-AGI Stable: as deployed on big-agi.com
- `v1-dev`: V1 development branch (this branch)
- `v1-stable`: Current stable version
Big-AGI is the multi-model AI workspace for experts: Engineers architecting systems. Founders making decisions. Researchers validating hypotheses.
You need to think broader, decide faster, and build with confidence, then you need Big-AGI.
Note: After the V2 release in Q4, `v2-dev` will become the default branch and `v1-dev` will reach EOL.
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.**
### Quick links: 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [installation](docs/installation.md) 👉 [documentation](docs/README.md)
![LLM Vendors](https://img.shields.io/badge/18+_LLM_Services-500+_Models-black?style=for-the-badge&logo=anthropic&logoColor=white&labelColor=purple)&nbsp;
[![Feature Beam](https://img.shields.io/badge/AI--Validation-BEAM-000?style=for-the-badge&labelColor=purple)](https://big-agi.com/beam)&nbsp;
[![Feature Inspector](https://img.shields.io/badge/Expert_Mode-AI_Inspector-000?style=for-the-badge&labelColor=purple)](https://big-agi.com/inspector)
### What's New in 1.16.1...1.16.8 · Sep 13, 2024 (patch releases)
### 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 Opus 4.5, Nano Banana, Kimi K2 or GPT 5.1 -
**Control**: with personas, data ownership, requests inspection, unlimited usage with API keys, and *no vendor lock-in* -
and **Speed**: with a local-first, over-powered, zero-latency, madly optimized web app.
<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 |
|------------------------------------------------------|-------------------|---------------------------------------------------------------|-------------|
| Big-AGI Open (self-host) | **IT** | First to get new models support. Maximum control and privacy. | 5-30 min |
| [big-agi.com](https://big-agi.com) Free | **Everyone** | Full core experience, improved Beam, new Personas, best UX. | **2 min**\* |
| **[big-agi.com](https://big-agi.com) Pro** $10.99/mo | **Professionals** | Everything + **Sync** across unlimited devices + 1GB storage | **2 min**\* |
\*: **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.
[![Use Free ⋅ Go Pro](https://img.shields.io/badge/Use_Free-Get_Pro-d5ec31?style=for-the-badge&logo=rocket&logoColor=white&labelColor=000)](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:
[![Deploy on Vercel](https://img.shields.io/badge/Deploy-black?style=for-the-badge&logo=vercel&logoColor=white&labelColor=000)](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:**)
[//]: # (- [**Big-AGI Open**]&#40;https://github.com/enricoros/big-AGI/tree/main&#41; - Open Source, latest models and features &#40;main branch&#41;)
[//]: # (- [**Big-AGI Pro**]&#40;https://big-agi.com&#41; - Hosted with Cloud Sync)
---
## Our Philosophy
We're an independent, non-VC-funded project with a simple belief: **AI should elevate you, not replace you**.
This is why we built Big-AGI to be **local-first**, madly optimized to 0-latency, launched multi-model first to
defeat hallucinations, designed Beam around the **humans in the loop**, re-wrote frameworks and abstractions
so you **are not vendor locked-in**, and obsessed over a powerful UI that works, just works.
NOTE: this is a powerful tool - if you need a toy UI or clone, this ain't it.
---
## Release Notes
👉 **[See the Live Release Notes](https://big-agi.com/changes)**
- Open 2.0.1: **Opus 4.5** full support, **Gemini 3 Pro** w/ code exec, **Nano Banana Pro**, **Grok 4.1**, **GPT-5.1**, **Kimi K2 Thinking** + 280 fixes
### What's New in 2.0 · Oct 31, 2025 · Open
- **Big-AGI Open** is ready and more productive and faster than ever, with:
- **Beam 2**: multi-modal, program-based, follow-ups, save presets
- Top-notch AI models support including **agentic models** and **reasoning models**
- **Image Generation** and editing with Nano Banana and gpt-image-1
- **Web Search** with citations for supported models
- **UI** & Mobile UI overhaul with peeking and side panels
- And all of the [Big-AGI 2 changes](https://github.com/enricoros/big-AGI/issues/567#issuecomment-2262187617) and more
- Built for the future, madly optimized
<img width="830" height="385" alt="image" src="https://github.com/user-attachments/assets/ad52761d-7e3f-44d8-b41e-947ce8b4faa1" />
#### **Open** links: 👉 [changelog](https://big-agi.com/changes) 👉 [installation](docs/installation.md) 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [documentation](docs/README.md)
**For teams and institutions:** Need shared prompts, SSO, or managed deployments? Reach out at enrico@big-agi.com. We're actively collecting requirements from research groups and IT departments.
<details>
<summary>5,000 Commits Milestone</summary>
Hit 5k commits last week. That's a lot of code.
Recent work has been intense:
- Chain of thought reasoning across multiple LLMs: **OpenAI o3** and o1, **DeepSeek R1**, **Gemini 2.0 Flash Thinking**, and more
- Beam is real - ~35% of our users run it daily to compare models
- New AIX framework lets us scale features we couldn't before
- UI is faster than ever. Like, terminal-fast
The new architecture is solid and the speed improvements are real.
![5000e-830px](https://github.com/user-attachments/assets/42f7420b-9331-421b-9a18-2e653aaa7d9b)
</details>
<details>
<summary>What's New in 1.16.1...1.16.10 · 2024-2025 (patch releases)</summary>
- 1.16.10: OpenRouter models support
- 1.16.9: Docker Gemini fix, R1 models support
- 1.16.8: OpenAI ChatGPT-4o Latest, o1 models support
- 1.16.8: OpenAI ChatGPT-4o Latest (o1-preview and o1-mini are supported in Big-AGI 2)
- 1.16.7: OpenAI support for GPT-4o 2024-08-06
- 1.16.6: Groq support for Llama 3.1 models
- 1.16.5: GPT-4o Mini support
@@ -198,10 +48,7 @@ The new architecture is solid and the speed improvements are real.
- 1.16.2: Updates to Beam
- 1.16.1: Support for the new OpenAI GPT-4o 2024-05-13 model
</details>
<details>
<summary>What's New in 1.16.0 · May 9, 2024 · Crystal Clear</summary>
### What's New in 1.16.0 · May 9, 2024 · Crystal Clear
- [Beam](https://big-agi.com/blog/beam-multi-model-ai-reasoning) core and UX improvements based on user feedback
- Chat cost estimation 💰 (enable it in Labs / hover the token counter)
@@ -212,20 +59,14 @@ The new architecture is solid and the speed improvements are real.
- Models update: **Anthropic**, **Groq**, **Ollama**, **OpenAI**, **OpenRouter**, **Perplexity**
- Code soft-wrap, chat text selection toolbar, 3x faster on Apple silicon, and more [#517](https://github.com/enricoros/big-AGI/issues/517), [507](https://github.com/enricoros/big-AGI/pull/507)
</details>
<details>
<summary>3,000 Commits Milestone · April 7, 2024</summary>
#### 3,000 Commits Milestone · April 7, 2024
![big-AGI Milestone](https://github.com/enricoros/big-AGI/assets/32999/47fddbb1-9bd6-4b58-ace4-781dfcb80923)
- 🥇 Today we <b>celebrate commit 3000</b> in just over one year, and going stronger 🚀
- 📢️ Thanks everyone for your support and words of love for Big-AGI, we are committed to creating the best AI experiences for everyone.
</details>
<details>
<summary>What's New in 1.15.0 · April 1, 2024 · Beam</summary>
### What's New in 1.15.0 · April 1, 2024 · Beam
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
- Managed Deployments **Auto-Configuration**: simplify the UI models setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
@@ -235,8 +76,6 @@ The new architecture is solid and the speed improvements are real.
- 1.15.1: Support for Gemini Pro 1.5 and OpenAI Turbo models
- Beast release, over 430 commits, 10,000+ lines changed: [release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.15.0), and changes [v1.14.1...v1.15.0](https://github.com/enricoros/big-AGI/compare/v1.14.1...v1.15.0)
</details>
<details>
<summary>What's New in 1.14.1 · March 7, 2024 · Modelmorphic</summary>
@@ -307,85 +146,99 @@ https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cf
</details>
For full details and former releases, check out the [archived versions changelog](docs/changelog.md).
For full details and former releases, check out the [changelog](docs/changelog.md).
## 👉 Supported Models & Integrations
## 👉 Key Features ✨
Delightful UX with latest models exclusive features like Beam for **multi-model AI validation**.
> ![LLM Vendors](https://img.shields.io/badge/18_LLM_Services-500+_Models-black?style=for-the-badge&logo=openai&logoColor=white&labelColor=purple)&nbsp;
> [![Feature Beam](https://img.shields.io/badge/AI--Validation-BEAM-000?style=for-the-badge&logo=anthropic&labelColor=purple)](https://big-agi.com/beam)
| ![Advanced AI](https://img.shields.io/badge/Advanced%20AI-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![500+ AI Models](https://img.shields.io/badge/500%2B%20AI%20Models-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![Flow-state UX](https://img.shields.io/badge/Flow--state%20UX-32383e?style=for-the-badge&logo=flow&logoColor=white) | ![Privacy First](https://img.shields.io/badge/Privacy%20First-32383e?style=for-the-badge&logo=privacy&logoColor=white) | ![Advanced Tools](https://img.shields.io/badge/Fun%20To%20Use-f22a85?style=for-the-badge&logo=tools&logoColor=white) |
| ![Advanced AI](https://img.shields.io/badge/Advanced%20AI-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![100+ AI Models](https://img.shields.io/badge/100%2B%20AI%20Models-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![Flow-state UX](https://img.shields.io/badge/Flow--state%20UX-32383e?style=for-the-badge&logo=flow&logoColor=white) | ![Privacy First](https://img.shields.io/badge/Privacy%20First-32383e?style=for-the-badge&logo=privacy&logoColor=white) | ![Advanced Tools](https://img.shields.io/badge/Fun%20To%20Use-f22a85?style=for-the-badge&logo=tools&logoColor=white) |
|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| **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 |
![big-AGI screenshot](docs/pixels/big-AGI-compo-20240201_small.png)
### AI Models & Vendors
You can easily configure 100s of AI models in big-AGI:
Configure 100s of AI models from 18+ providers:
| **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) · [Google Gemini](https://ai.google.dev/) · [OpenAI](https://platform.openai.com/docs/overview) |
| Language services | [Anthropic](https://anthropic.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/) |
| Image services | [Prodia](https://prodia.com/) (SDXL) |
| Speech services | [ElevenLabs](https://elevenlabs.io) (Voice synthesis / cloning) |
| **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) |
Add extra functionality with these integrations:
### Additional Integrations
| **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/) |
| Sharing | [Paste.gg](https://paste.gg/) (Paste chats) |
| Tracking | [Helicone](https://www.helicone.ai) (LLM Observability) |
| **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) |
[//]: # (- [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
Self-host with Docker, deploy on Vercel, or develop locally. Full setup guide:
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.
[![Installation Guide](https://img.shields.io/badge/Installation%20Guide-blue?style=for-the-badge&logo=read-the-docs&logoColor=white)](docs/installation.md)
Or use the hosted version at [big-agi.com](https://big-agi.com) with your API keys.
Or bring your API keys and jump straight into our free instance on [big-AGI.com](https://big-agi.com).
---
<br/>
## 👋 Community & Contributing
### Connect
# 🌟 Get Involved!
[//]: # ([![Official Discord]&#40;https://img.shields.io/discord/1098796266906980422?label=discord&logo=discord&logoColor=%23fff&style=for-the-badge&#41;]&#40;https://discord.gg/MkH4qj2Jp9&#41;)
[![Official Discord](https://discordapp.com/api/guilds/1098796266906980422/widget.png?style=banner2)](https://discord.gg/MkH4qj2Jp9)
⭐ [Star the repo](https://github.com/enricoros/big-agi) if Big-AGI is useful to you
- [ ] 📢️ [**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)
### Contribute
<br/>
**🤖 AI-Powered Issue Assistance**
[//]: # ([![GitHub stars]&#40;https://img.shields.io/github/stars/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/stargazers&#41;)
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!
[//]: # ([![GitHub forks]&#40;https://img.shields.io/github/forks/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/network&#41;)
[![Open an Issue](https://img.shields.io/badge/Open_Issue-AI_Will_Help-ff8c00?style=for-the-badge&logo=fireship&logoColor=fff&labelColor=8b0000)](https://github.com/enricoros/big-agi/issues/new?template=ai-triage.yml)
[![Request Feature](https://img.shields.io/badge/Request_Feature-Roadmap_Idea-orange?style=for-the-badge&logo=lightbulb&logoColor=white)](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
[//]: # ([![GitHub pull requests]&#40;https://img.shields.io/github/issues-pr/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/pulls&#41;)
[![Good First Issues](https://img.shields.io/badge/Good_First_Issues-Start-blue?style=for-the-badge&logo=github&logoColor=white)](https://github.com/users/enricoros/projects/4/views/4)
[![Customization](https://img.shields.io/badge/Fork_&_Customize-Your_Own-purple?style=for-the-badge&logo=git&logoColor=white)](docs/customizations.md)
[![Roadmap](https://img.shields.io/badge/Open_Roadmap-View-0366d6?style=for-the-badge&logo=github&logoColor=white)](https://github.com/users/enricoros/projects/4/views/2)
[//]: # ([![License]&#40;https://img.shields.io/github/license/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/LICENSE&#41;)
#### Contributors
## 📜 Licensing
<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>
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).
---
## License
MIT License · [Third-Party Notices](src/modules/3rdparty/THIRD_PARTY_NOTICES.md)
**2023-2025** · Enrico Ros × [Big-AGI](https://big-agi.com)
2023-2024 · Enrico Ros x [Big-AGI](https://big-agi.com) · Like this project? Leave a star! 💫⭐
+7 -22
View File
@@ -2,38 +2,23 @@ import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouterCloud } from '~/server/trpc/trpc.router-cloud';
import { createTRPCFetchContext } from '~/server/trpc/trpc.server';
import { posthogServerSendException } from '~/server/posthog/posthog.server';
const handlerNodeRoutes = (req: Request) => fetchRequestHandler({
endpoint: '/api/cloud',
router: appRouterCloud,
req,
createContext: createTRPCFetchContext,
onError: async function({ path, error, type, ctx }) {
// -> DEV error logging
if (process.env.NODE_ENV === 'development')
console.error(`❌ tRPC-cloud failed on ${path ?? 'unk-path'}: ${error.message}`);
// -> Capture node errors
await posthogServerSendException(error, undefined, {
domain: 'trpc-onerror',
runtime: 'nodejs',
endpoint: path ?? 'unknown',
method: req.method,
url: req.url,
additionalProperties: {
error_code: error.code,
error_type: type,
},
});
},
onError:
process.env.NODE_ENV === 'development'
? ({ path, error }) => console.error(`❌ tRPC-cloud failed on ${path ?? 'unk-path'}: ${error.message}`)
: undefined,
});
// NOTE: the following statement breaks the build on non-pro deployments, and conditionals don't work either
// so we resorted to raising the timeout from 10s to 60s in the vercel.json file instead
// export const maxDuration = 60;
// so we resorted to raising the timeout from 10s to 25s in the vercel.json file instead
// export const maxDuration = 25;
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export { handlerNodeRoutes as GET, handlerNodeRoutes as POST };
+1 -3
View File
@@ -10,11 +10,9 @@ const handlerEdgeRoutes = (req: Request) => fetchRequestHandler({
createContext: createTRPCFetchContext,
onError:
process.env.NODE_ENV === 'development'
? ({ path, error }) => console.error(`\n❌ tRPC-edge failed on ${path ?? 'unk-path'}: ${error.message}`)
? ({ path, error }) => console.error(`❌ tRPC-edge failed on ${path ?? 'unk-path'}: ${error.message}`)
: undefined,
});
// NOTE: we don't set maxDuration explicitly here - however we set it in the Vercel project settings, raising to the limit of 300s
// export const maxDuration = 60;
export const runtime = 'edge';
export { handlerEdgeRoutes as GET, handlerEdgeRoutes as POST };
+1 -1
View File
@@ -1,6 +1,6 @@
# Very simple docker-compose file to run the app on http://localhost:3000 (or http://127.0.0.1:3000).
#
# For more examples, such running big-AGI alongside a web browsing service, see the `docs/docker` folder.
# For more examples, such runnin big-AGI alongside a web browsing service, see the `docs/docker` folder.
version: '3.9'
+21 -32
View File
@@ -2,47 +2,35 @@
Information you need to get started, configure, and use big-AGI productively.
👉 **[Changelog](https://big-agi.com/changes)** - See what's new
## Getting Started
Essential guides:
Guides for basic big-AGI features:
- **[FAQ](help-faq.md)**: Common questions and answers
- **[Enabling Microphone](help-feature-microphone.md)**: Configure speech recognition in your browser
- **[Enabling Microphone for Speech Recognition](help-feature-microphone.md)**: Instructions to
allow speech recognition in browsers and apps.
## AI Services
## AI Model Configuration
How to set up AI models and features in big-AGI.
Detailed guides to configure AI models and advanced features in big-AGI.
> 👉 The following applies to users of big-AGI.com, as the public instance is empty and requires user configuration.
- **Cloud AI Services**:
- Easy API key configuration:
[Alibaba](https://bailian.console.alibabacloud.com/?apiKey=1#/api-key),
[Anthropic](https://console.anthropic.com/settings/keys),
[Deepseek](https://platform.deepseek.com/api_keys),
[Google Gemini](https://aistudio.google.com/app/apikey),
[Groq](https://console.groq.com/keys),
[Mistral](https://console.mistral.ai/api-keys/),
[OpenAI](https://platform.openai.com/api-keys),
[OpenPipe](https://app.openpipe.ai/settings),
[Perplexity](https://www.perplexity.ai/settings/api),
[TogetherAI](https://api.together.xyz/settings/api-keys),
[xAI](http://x.ai/api)
- **[Azure OpenAI](config-azure-openai.md)** guide
- **FireworksAI** ([API keys](https://fireworks.ai/account/api-keys), via custom OpenAI endpoint: https://api.fireworks.ai/inference)
- **[OpenRouter](config-openrouter.md)** guide
- **[Azure OpenAI](config-azure-openai.md)**
- **[OpenRouter](config-openrouter.md)**
- Easy API key setup: **Anthropic**, **Deepseek**, **Google AI**, **Groq**, **Mistral**, **OpenAI**, **OpenPipe**, **Perplexity**, **TogetherAI**, **xAI**
- **Local AI Integrations**:
- [LocalAI](config-local-localai.md), [LM Studio](config-local-lmstudio.md), [Ollama](config-local-ollama.md)
- **[LocalAI](config-local-localai.md)**
- **[LM Studio](config-local-lmstudio.md)**
- **[Ollama](config-local-ollama.md)**
- **Enhanced AI Features**:
- **[Web Browsing](config-feature-browse.md)**: Enable web page download through third-party services or your own cloud
- **[Web Browsing](config-feature-browse.md)**: Enable web page download through third-party services or your own cloud (advanced)
- **Web Search**: Google Search API (see '[Environment Variables](environment-variables.md)')
- **Image Generation**: GPT Image (gpt-image-1), DALL·E 3 and 2
- **Image Generation**: DALL·E 3 and 2, or Prodia API for Stable Diffusion XL
- **Voice Synthesis**: ElevenLabs API for voice generation
## Deployment & Customization
@@ -51,14 +39,13 @@ How to set up AI models and features in big-AGI.
For deploying a custom big-AGI instance:
- **[Installation Guide](installation.md)**, including:
- Set up your own big-AGI instance
- **[Installation Guide](installation.md)**: Set up your own big-AGI instance
- Source build or pre-built options
- Local, cloud, or on-premises deployment
- **Advanced Setup**:
- **[Source Code Customization](customizations.md)**: Modify the source code
- **[Source Code Customization Guide](customizations.md)**: Modify the source code
- **[Access Control](deploy-authentication.md)**: Optional, add basic user authentication
- **[Database Setup](deploy-database.md)**: Optional, enables "Chat Link Sharing"
- **[Reverse Proxy](deploy-reverse-proxy.md)**: Optional, enables custom domains and SSL
@@ -66,8 +53,10 @@ For deploying a custom big-AGI instance:
## Community & Support
- Check the [changelog](https://big-agi.com/changes) for the latest updates
- Visit our [GitHub repository](https://github.com/enricoros/big-AGI) for source code and issue tracking
- Join our [Discord](https://discord.gg/MkH4qj2Jp9) for discussions and help
Connect with the growing big-AGI community:
Let's build something great.
- Visit our [GitHub repository](https://github.com/enricoros/big-AGI) for source code and issue tracking
- Check the latest updates and features on [Changelog](changelog.md) or the in-app [News](https://get.big-agi.com/news)
- Connect with us and other users on [Discord](https://discord.gg/MkH4qj2Jp9) for discussions, help, and sharing your experiences with big-AGI
Thank you for choosing big-AGI. We're excited to give you the best tools to amplify yourself.
+7 -19
View File
@@ -1,30 +1,18 @@
## Archived Versions - Changelog
## Changelog
This is a high-level changelog. Calls out some of the high level features batched
by release.
- For the live changelog, see [big-agi.com/changes](https://big-agi.com/changes)
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
> NOTE: with the release of 2.0.0 we switching to [big-agi.com/changes](https://big-agi.com/changes) for the
> continuously updated changelog.
### 1.17.0 - Jun 2024
### What's New in 2 · Oct 31, 2025 · Open
- milestone: [1.17.0](https://github.com/enricoros/big-agi/milestone/17)
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
- **Big-AGI Open** is ready and more productive and faster than ever, with:
- **Beam 2**: multi-modal, program-based, follow-ups, save presets
- Top-notch AI models support including **agentic models** and **reasoning models**
- **Image Generation** and editing with Nano Banana and gpt-image-1
- **Web Search** with citations for supported models
- **UI** & Mobile UI overhaul with peeking and side panels
- And all of the [Big-AGI 2 changes](https://github.com/enricoros/big-AGI/issues/567#issuecomment-2262187617) and more
- Built for the future, madly optimized
### What's New in 1.16.1...1.16.8 · Sep 13, 2024 (patch releases)
### What's New in 1.16.1...1.16.9 · Jan 21, 2025 (patch releases)
- 1.16.10: OpenRouter models support
- 1.16.9: Docker Gemini fix, R1 models support
- 1.16.8: OpenAI ChatGPT-4o Latest, o1 models support
- 1.16.8: OpenAI ChatGPT-4o Latest (o1-preview and o1-mini are supported in Big-AGI 2)
- 1.16.7: OpenAI support for GPT-4o 2024-08-06
- 1.16.6: Groq support for Llama 3.1 models
- 1.16.5: GPT-4o Mini support
@@ -58,7 +46,7 @@ by release.
### What's New in 1.15.0 · April 1, 2024 · Beam
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
- Managed Deployments **Auto-Configuration**: simplify the UI models setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
- Managed Deployments **Auto-Configuration**: simplify the UI mdoels setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
- Message **Starring ⭐**: star important messages within chats, to attach them later. [#476](https://github.com/enricoros/big-AGI/issues/476)
- Enhanced the default Persona
- Fixes to Gemini models and SVGs, improvements to UI and icons
+28 -48
View File
@@ -14,7 +14,7 @@ If you have an `API Endpoint` and `API Key`, you can configure big-AGI as follow
1. Launch the `big-AGI` application
2. Go to the **Models** settings
3. Add a Vendor and select **Azure OpenAI**
- Enter the Endpoint (e.g., 'https://your-resource-name.openai.azure.com')
- Enter the Endpoint (e.g., 'https://your-openai-api-1234.openai.azure.com/')
- Enter the API Key (e.g., 'fd5...........................ba')
The deployed models are now available in the application. If you don't have a configured
@@ -23,36 +23,6 @@ Azure OpenAI service instance, continue with the next section.
In addition to using the UI, configuration can also be done using
[environment variables](environment-variables.md).
## Server Configuration
For server deployments, set these environment variables:
```bash
AZURE_OPENAI_API_ENDPOINT=https://your-resource-name.openai.azure.com
AZURE_OPENAI_API_KEY=your-api-key
```
This enables Azure OpenAI for all users without requiring individual API keys. For more details, see [environment-variables.md](environment-variables.md).
## Azure OpenAI API Versions
Azure OpenAI supports both traditional deployment-based API and the next-generation v1 API:
### Next-Generation v1 API (Default)
- **Enabled by default** for GPT-5-like models (GPT-5, GPT-6, o3, o4, etc.)
- Uses direct `/openai/v1/responses` endpoint without deployment IDs
- Optimized for advanced reasoning models and new features
- Can be disabled by setting `AZURE_OPENAI_DISABLE_V1=true`
### Traditional Deployment-Based API
- Uses `/openai/deployments/{deployment-name}/...` endpoints
- Required for older models and when v1 API is disabled
- Needs deployment ID for all API calls
### Known Limitations
- **Web Search Tool**: Azure OpenAI does not support the `web_search_preview` tool that's available in OpenAI's API
- Models with web search capabilities will have this feature automatically disabled on Azure
## Setting Up Azure
### Step 1: Azure Account & Subscription
@@ -64,7 +34,18 @@ Azure OpenAI supports both traditional deployment-based API and the next-generat
- Fill in the required fields and click on **Create**
- Note down the **Subscription ID** (e.g., `12345678-1234-1234-1234-123456789012`)
### Step 2: Create Azure OpenAI Resource
### Step 2: Apply for Azure OpenAI Service
We'll now be creating "OpenAI"-specific resources on Azure. This requires to 'apply',
and acceptance should be quick (even as low as minutes).
1. Visit [Azure OpenAI Service](https://aka.ms/azure-openai)
2. Click on **Apply for access**
- Fill in the required fields (including the subscription ID) and click on **Apply**
Once your application is accepted, you can create OpenAI resources on Azure.
### Step 3: Create Azure OpenAI Resource
For more information, see [Azure: Create and deploy OpenAI](https://learn.microsoft.com/en-us/azure/ai-services/openai/how-to/create-resource?pivots=web-portal)
@@ -74,32 +55,31 @@ For more information, see [Azure: Create and deploy OpenAI](https://learn.micros
![Creating an OpenAI service](pixels/config-azure-openai-create.png)
- Select the subscription
- Select a resource group or create a new one
- Select the region. **Important**: The region determines which models are available.
> Popular regions like **East US**, **West Europe**, and **Australia East** typically have the best model availability. For the latest model availability by region, see [Azure OpenAI Model Availability](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models)
- Select the region. Note that the region determines the available models.
> For instance, **Canada East** offers GPT-4-32k models, For the full list, see [GPT-4 models](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models)
- Name the service (e.g., `your-openai-api-1234`)
- Select a pricing tier (e.g., `S0` for standard)
- Select: "All networks, including the internet, can access this resource."
- Click on **Review + create** and then **Create**
After creating the resource, you can access the API Keys and Endpoints:
After creating the resource, you can access the API Keys and Endpoints. At any point, you can go to
the OpenAI Service instance page to get this information.
1. Click on **Go to resource** (or navigate to your Azure OpenAI resource)
2. In the left sidebar, under **Resource Management**, click on **Keys and Endpoint**
3. Copy the required information:
- **Endpoint**: e.g., 'https://your-resource-name.openai.azure.com/'
- **Key**: Copy either KEY 1 or KEY 2 (both work identically)
- Click on **Go to resource**
- Click on **Develop**
- Copy the `Endpoint`, called "Language API", e.g. 'https://your-openai-api-1234.openai.azure.com/'
- Copy `KEY 1`
### Step 3: Deploy Models
### Step 4: Deploy Models
By default, Azure OpenAI resource instances don't have models available. You need to deploy the models you want to use.
1. In your Azure OpenAI resource, click on **Model deployments** in the left sidebar
2. Click on **Create new deployment**
3. Fill in the deployment details:
- **Select a model**: Choose from available models
- **Model version**: Select the latest version or a specific one
- **Deployment name**: Give it a meaningful name
4. Click **Deploy**
1. Click on **Model Deployments > Manage Deployments**
2. Click on **+Create New Deployment**
![Deploying a model](pixels/config-azure-openai-deploy.png)
- Select the model you want to deploy
- Optionally select a version
- name the model, e.g., `gpt4-32k-0613`
Repeat as necessary for each model you want to deploy.
+1 -1
View File
@@ -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/server/models.mappings.ts](../src/modules/llms/server/models.mappings.ts)
Please update the [src/modules/llms/transports/server/openai/models/models.data.ts](../src/modules/llms/server/openai/models/models.data.ts)
file with the mapping information between LocalAI model IDs and names/descriptions/tokens, etc.
# 🤝 Support
+6 -27
View File
@@ -31,14 +31,17 @@ At time of writing, big-AGI has only 2 operations that run on Node.js Functions:
browsing (fetching web pages) and sharing. They both can exceed 10 seconds, especially
when fetching large pages or waiting for websites to be completed.
We provide `vercel_PRODUCTION.json` to raise the duration to 25 seconds (from a default of 10), to use it,
make sure to rename it to `vercel.json` before build.
From the Vercel Project > Settings > General > Build & Development Settings,
you can for instance set the build command to:
```bash
next build
mv vercel_PRODUCTION.json vercel.json; next build
```
### Change the Personas (v1.x only)
### Change the Personas
Edit the `src/data.ts` file to customize personas. This file houses the default personas. You can add, remove, or modify these to meet your project's needs.
@@ -52,21 +55,6 @@ Adapt the UI to match your project's aesthetic, incorporate new features, or exc
- [ ] Modify `src/common/app.config.tsx` to alter the application's name
- [ ] Update `src/common/app.nav.tsx` to revise the navigation bar
### Add a Message of the Day
You can display a temporary announcement banner at the top of the app using the `NEXT_PUBLIC_MOTD` environment variable.
- Set this variable in your deployment environment
- The message supports template variables:
- `{{app_build_hash}}`: Current git commit hash
- `{{app_build_pkgver}}`: Package version
- `{{app_build_time}}`: Build timestamp as date
- `{{app_deployment_type}}`: Deployment type (local, docker, vercel, etc.)
- Users can dismiss the message (until next page refresh)
- Use it for version announcements, maintenance notices, or feature highlights
Example: `NEXT_PUBLIC_MOTD=🚀 New features available in {{app_build_pkgver}}! Try the improved Beam.`
## Testing & Deployment
Test your application thoroughly using local development (refer to README.md for local build instructions). Deploy using your preferred hosting service. big-AGI supports deployment on platforms like Vercel, Docker, or any Node.js-compatible service, especially those supporting NextJS's "Edge Runtime."
@@ -77,16 +65,7 @@ Test your application thoroughly using local development (refer to README.md for
## Debugging
The application includes a client-side logging system. You can view recent logs via the UI (Settings > Tools > Logs).
For deeper debugging during development:
1. **Debug Page**: Access the `/info/debug` page for an overview of the application's environment, configuration, API status, and environment variables available to the client.
2. **Conditional Breakpoints**: To automatically pause execution in your browser's developer tools when critical errors (`error`, `critical`, `DEV` levels) are logged to the console, set the following environment variable in your local `.env.local` file and restart your development server:
```bash
NEXT_PUBLIC_DEBUG_BREAKS=true
```
This allows you to inspect the application state at the exact moment an important error occurs. This feature only works in development mode (`npm run dev`) and requires the environment variable to be explicitly set to `true`.
We introduced the `/info/debug` page that provides a detailed overview of the application's environment, including the API keys, environment variables, and other configuration settings.
<br/>
+23 -40
View File
@@ -2,9 +2,8 @@
The open-source big-AGI project provides support for the following analytics services:
- **Google Analytics 4**: manual setup required
- **PostHog Analytics**: manual setup required
- **Vercel Analytics**: automatic when deployed to Vercel
- **Google Analytics 4**: manual setup required
The following is a quick overview of the Analytics options for the deployers of this open-source project.
big-AGI is deployed to many large-scale and enterprise though various ways (custom builds, Docker, Vercel, Cloudflare, etc.),
@@ -12,36 +11,6 @@ and this guide is for its customization.
## Service Configuration
### Google Analytics 4
- Why: user engagement and retention, performance insights, personalization, content optimization
- What: https://support.google.com/analytics/answer/11593727
Google Analytics 4 (GA4) is a powerful tool for understanding user behavior and engagement.
This can help optimize big-AGI, understanding which features are needed/users and which aren't.
To enable Google Analytics 4, you need to set the `NEXT_PUBLIC_GA4_MEASUREMENT_ID` environment variable
before starting the local build or the docker build (i.e. at build time), at which point the
server/container will be able to report analytics to your Google Analytics 4 property.
As of Feb 27, 2024, this feature is in development.
### PostHog Analytics
- Why: feature usage tracking, user journeys, conversion optimization, product analytics
- What: page views, page leave events, user interactions, and deployment context
PostHog provides comprehensive product analytics with privacy controls. It helps understand how users interact with big-AGI's features, identify opportunities for improvement, and optimize the user experience.
To enable PostHog, set the `NEXT_PUBLIC_POSTHOG_KEY` environment variable at build time. PostHog is configured with tracking optimization and privacy in mind:
- Uses a proxy endpoint (`/a/ph`) to avoid ad blockers
- Respects user opt-out preferences via local storage
- Tracks only essential information without PII
- Adds deployment context for better segmentation
The implementation follows PostHog's best practices for Next.js applications and includes manual page view tracking for proper single-page application support.
### Vercel Analytics
- Why: understand coarse traction, and identify deployment issues - all without tracking individual users
@@ -62,19 +31,33 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) => <>
</>;
```
When big-AGI is served on Vercel hosts, the `process.env.NEXT_PUBLIC_VERCEL_URL` environment variable is trueish, and
When big-AGI is served on Vercel hosts, the ```process.env.NEXT_PUBLIC_VERCEL_URL``` environment variable is trueish, and
analytics will be sent by default to the Vercel Analytics service which is deployed by Vercel IF configured from the
Vercel project dashboard.
In summary: to turn it on: activate the `Analytics` service in the Vercel project dashboard.
### Google Analytics 4
- Why: user engagement and retention, performance insights, personalization, content optimization
- What: https://support.google.com/analytics/answer/11593727
Google Analytics 4 (GA4) is a powerful tool for understanding user behavior and engagement.
This can help optimize big-AGI, understanding which features are needed/users and which aren't.
To enable Google Analytics 4, you need to set the `NEXT_PUBLIC_GA4_MEASUREMENT_ID` environment variable
before starting the local build or the docker build (i.e. at build time), at which point the
server/container will be able to report analytics to your Google Analytics 4 property.
As of Feb 27, 2024, this feature is in development.
## Configurations
| Scope | Default | Description / Instructions |
|-------------------------------------------------------------------------------------------------------------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Your **Source** builds of big-AGI | None | **Google Analytics**: set environment variable at build time · **PostHog**: set environment variable at build time · **Vercel**: enable Vercel Analytics from the dashboard |
| Your **Docker** builds of big-AGI | None | (**Vercel**: n/a) · **Google Analytics**: set environment variable at `docker build` time · **PostHog**: set environment variable at `docker build` time. |
| [get.big-agi.com](https://get.big-agi.com) (**Big-AGI 1.x Legacy**) | Vercel + Google + PostHog | The main website ([privacy policy](https://big-agi.com/privacy)) hosted for free for anyone. |
| [prebuilt Docker packages](https://github.com/enricoros/big-AGI/pkgs/container/big-agi) (**Big-AGI 1.x**, 'latest' tag) | Google Analytics | **Vercel**: n/a · **Google Analytics**: set to the big-agi.com Google Analytics for analytics and improvements · **PostHog**: n/a |
| Scope | Default | Description / Instructions |
|-----------------------------------------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------|
| Your source builds of big-AGI | None | **Vercel**: enable Vercel Analytics from the dashboard. · **Google Analytics**: set environment variable at build time. |
| Your docker builds of big-AGI | None | **Vercel**: n/a. · **Google Analytics**: set environment variable at `docker build` time. |
| [big-agi.com](https://big-agi.com) | Vercel + Google | The main website ([privacy policy](https://big-agi.com/privacy)) hosted for free for anyone. |
| [official Docker packages](https://github.com/enricoros/big-AGI/pkgs/container/big-agi) | Google Analytics | **Vercel**: n/a · **Google Analytics**: set to the big-agi.com Google Analytics for analytics and improvements. |
Note: this information is updated as of March 3, 2025 and can change at any time.
Note: this information is updated as of Feb 27, 2024 and can change at any time.
-6
View File
@@ -31,12 +31,6 @@ file.
### Official Images: [ghcr.io/enricoros/big-agi](https://github.com/enricoros/big-agi/pkgs/container/big-agi)
#### Available Tags
- **`:latest`** / **`:stable`** - Latest stable release (recommended)
- **`:development`** - Main branch (bleeding edge)
- **`:v2.0.0`** - Specific versions
#### Run using *docker* 🚀
```bash
+10 -20
View File
@@ -3,7 +3,7 @@
This document provides an explanation of the environment variables used in the big-AGI application.
**All variables are optional**; and _UI options_ take precedence over _backend environment variables_,
which take place over _defaults_. This file is kept in sync with [`../src/server/env.ts`](../src/server/env.ts).
which take place over _defaults_. This file is kept in sync with [`../src/server/env.mjs`](../src/server/env.mjs).
### Setting Environment Variables
@@ -23,8 +23,6 @@ MDB_URI=
OPENAI_API_KEY=
OPENAI_API_HOST=
OPENAI_API_ORG_ID=
ALIBABA_API_HOST=
ALIBABA_API_KEY=
AZURE_OPENAI_API_ENDPOINT=
AZURE_OPENAI_API_KEY=
ANTHROPIC_API_KEY=
@@ -35,7 +33,6 @@ GROQ_API_KEY=
LOCALAI_API_HOST=
LOCALAI_API_KEY=
MISTRAL_API_KEY=
MOONSHOT_API_KEY=
OLLAMA_API_HOST=
OPENPIPE_API_KEY=
OPENROUTER_API_KEY=
@@ -57,16 +54,16 @@ GOOGLE_CSE_ID=
ELEVENLABS_API_KEY=
ELEVENLABS_API_HOST=
ELEVENLABS_VOICE_ID=
# Text-To-Image: Prodia
PRODIA_API_KEY=
# Backend HTTP Basic Authentication (see `deploy-authentication.md` for turning on authentication)
HTTP_BASIC_AUTH_USERNAME=
HTTP_BASIC_AUTH_PASSWORD=
# Frontend variables
NEXT_PUBLIC_MOTD=
# Frontend variables
NEXT_PUBLIC_GA4_MEASUREMENT_ID=
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_PLANTUML_SERVER_URL=
```
@@ -91,13 +88,8 @@ requiring the user to enter an API key
| `OPENAI_API_KEY` | API key for OpenAI | Recommended |
| `OPENAI_API_HOST` | Changes the backend host for the OpenAI vendor, to enable platforms such as Helicone and CloudFlare AI Gateway | Optional |
| `OPENAI_API_ORG_ID` | Sets the "OpenAI-Organization" header field to support organization users | Optional |
| `ALIBABA_API_HOST` | The Alibaba AI OpenAI-compatible endpoint | Optional |
| `ALIBABA_API_KEY` | The API key for Alibaba AI | Optional |
| `AZURE_OPENAI_API_ENDPOINT` | Azure OpenAI endpoint - host only, without the path | Optional, but if set `AZURE_OPENAI_API_KEY` must also be set |
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key, see [config-azure-openai.md](config-azure-openai.md) | Optional, but if set `AZURE_OPENAI_API_ENDPOINT` must also be set |
| `AZURE_OPENAI_DISABLE_V1` | Disables the next-generation v1 API for GPT-5-like models (set to 'true' to disable) | Optional, defaults to enabled |
| `AZURE_OPENAI_API_VERSION` | API version for traditional deployment-based endpoints | Optional, defaults to '2025-04-01-preview' |
| `AZURE_DEPLOYMENTS_API_VERSION` | API version for the deployments listing endpoint | Optional, defaults to '2023-03-15-preview' |
| `ANTHROPIC_API_KEY` | The API key for Anthropic | Optional |
| `ANTHROPIC_API_HOST` | Changes the backend host for the Anthropic vendor, to enable platforms such as AWS Bedrock | Optional |
| `DEEPSEEK_API_KEY` | The API key for Deepseek AI | Optional |
@@ -106,7 +98,6 @@ 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 |
@@ -136,6 +127,8 @@ Enable the app to Talk, Draw, and Google things up.
| `ELEVENLABS_API_KEY` | ElevenLabs API Key - used for calls, etc. |
| `ELEVENLABS_API_HOST` | Custom host for ElevenLabs |
| `ELEVENLABS_VOICE_ID` | Default voice ID for ElevenLabs |
| **Text-To-Image** | [Prodia](https://prodia.com/) is a reliable image generation service |
| `PRODIA_API_KEY` | Prodia API Key - used with '/imagine ...' |
| **Google Custom Search** | [Google Programmable Search Engine](https://programmablesearchengine.google.com/about/) produces links to pages |
| `GOOGLE_CLOUD_API_KEY` | Google Cloud API Key, used with the '/react' command - [Link to GCP](https://console.cloud.google.com/apis/credentials) |
| `GOOGLE_CSE_ID` | Google Custom/Programmable Search Engine ID - [Link to PSE](https://programmablesearchengine.google.com/) |
@@ -149,13 +142,10 @@ Enable the app to Talk, Draw, and Google things up.
The value of these variables are passed to the frontend (Web UI) - make sure they do not contain secrets.
| Variable | Description |
|:----------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `NEXT_PUBLIC_DEBUG_BREAKS` | (optional, development) When set to 'true', enables automatic debugger breaks on DEV/error/critical logs in development builds |
| `NEXT_PUBLIC_MOTD` | Message of the Day - displays a dismissible banner at the top of the app (see [customizations](customizations.md) for the template variables). Example: 🔔 Welcome to our deployment! Version {{app_build_pkgver}} built on {{app_build_time}}. |
| `NEXT_PUBLIC_GA4_MEASUREMENT_ID` | (optional) The measurement ID for Google Analytics 4. (see [deploy-analytics](deploy-analytics.md)) |
| `NEXT_PUBLIC_POSTHOG_KEY` | (optional) Key for PostHog analytics. (see [deploy-analytics](deploy-analytics.md)) |
| `NEXT_PUBLIC_PLANTUML_SERVER_URL` | The URL of the PlantUML server, used for rendering UML diagrams. Allows using custom local servers. |
| Variable | Description |
|:----------------------------------|:-----------------------------------------------------------------------------------------|
| `NEXT_PUBLIC_GA4_MEASUREMENT_ID` | The measurement ID for Google Analytics 4. (see [deploy-analytics](deploy-analytics.md)) |
| `NEXT_PUBLIC_PLANTUML_SERVER_URL` | The URL of the PlantUML server, used for rendering UML diagrams. (code in RederCode.tsx) |
> Important: these variables must be set at build time, which is required by Next.js to pass them to the frontend.
> This is in contrast to the backend variables, which can be set when starting the local server/container.
-99
View File
@@ -1,99 +0,0 @@
# Big-AGI Data Ownership Guide
Big-AGI is a **client-first** web application, which means it prioritizes speed and data ownership compared to cloud apps.
Your *API keys*, *chat history*, and *settings* live in your
browser's [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), not
on cloud servers.
You can use Big-AGI in two ways:
1. Run it yourself (open-source)
2. Use big-agi.com (hosted service)
This guide explains how the open-source version handles your data. You can verify everything in [the source code](https://github.com/enricoros/big-agi).
## Client-Side Storage
Within Big-AGI almost all chat/keys data is handled client-side in your browser using two
standard browser storage mechanisms:
- **Local Storage**: API keys, settings, and configurations ([learn more](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage))
- **IndexedDB**: Chat history and larger files ([learn more](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API))
The Big-AGI backend mainly passes requests to AI services (OpenAI, Anthropic, etc.). It doesn't store your data, except for the chat-sharing function if used.
You can see your data in your browser's local storage and IndexedDB - try it yourself:
1. In Chrome: Open DevTools (press F12 on Windows, ⌘ + ⌥ + I on Mac)
2. Click 'Application' > 'Local Storage'
3. See your settings and API keys
![Browser local storage showing API keys and chat data](pixels/data_ownership_local_storage.png)
### What This Means For You
Storing data in your browser means:
- Your data stays on **one device/browser only**
- Clearing browser data **erases your chats** - make backups
- Anyone using your browser can see your chats and keys
- Running your own server needs technical skills
### Local Device Identifier
Big-AGI generates a _device identifier_ that combines timestamp and random components, stored only on your device. This identifier:
- Is used only for the **optional sync functionality** between your devices (not yet ready)
- Helps maintain data consistency when using Big-AGI across multiple devices
- Remains completely local unless you explicitly enable sync
- Is not used for tracking, analytics, or telemetry
- Can be deleted anytime by clearing local storage
- Is fully transparent - see the implementation in `src/common/stores/store-client.ts`
## How Data Flows
AI interactions in Big-AGI, such as chats, AI titles, text to speech, browsing, flow through three components:
1. **Browser** (client/installed App) - Stores your keys & data locally
2. **Backend** (routing server) - Passes requests to AI services
3. **AI Services** - Where the actual AI processing happens
### Self-Deployed Version: Your Infrastructure
You run the server. Your data only leaves when making AI requests.
The keys and chats are under your control and pass through your code, and are sent to
the upstream AI services on a per-request basis.
![data_ownership_local.png](pixels/data_ownership_deployed.png)
### Web Version: Using big-agi.com
Your data passes through the hosted Big-AGI edge network to reach AI services. The keys
and chats pass through Big-AGI's edge network to reach the AI services on a per-request basis,
and then are send to the upstream AI services.
![data_ownership_hosted.png](pixels/data_ownership_hosted.png)
## Security Best Practices
**Basic Security**:
- **Never share API keys**
- **Don't use shared computers**
- Use private browsing for one-off sessions
- Use trusted networks
- Back up your data
**When Running Your Own Server**:
- Use [environment variables](environment-variables.md) for API keys
- Run on trusted infrastructure
- Keep your installation updated
## TL;DR
Your API keys and chats stay in your browser. The server only passes requests to AI services.
Use big-agi.com for convenience, or [run it yourself](installation.md) for full control.
Need help? Join our [Discord](https://discord.gg/MkH4qj2Jp9) or open a [GitHub issue](https://github.com/enricoros/big-agi/issues).
-28
View File
@@ -1,28 +0,0 @@
# Frequently Asked Questions
Quick answers to common questions about Big-AGI. For detailed documentation, see our [Website Docs](https://big-agi.com/docs).
### Versions
<details open>
<summary><b>How do I check my Big-AGI version?</b></summary>
You can see the version in the _News_ section of the app, as per the image below.
![Version location in Big-AGI](https://github.com/user-attachments/assets/cd295094-0114-420f-a5b9-0d762e59b506)
</details>
<details open>
<summary><b>How do I verify my Vercel deployment version?</b></summary>
You can go in the **deployments** section of your Vercel project, and at a quick glance see
what is the latest deployment status, time, and link to the source code.
![Vercel deployments view](https://github.com/user-attachments/assets/664b8c3d-496e-4595-ad5e-898bdb82507c)
Each deployment links directly to its source code commit.
</details>
---
Missing something? [Open an issue](https://github.com/enricoros/big-agi/issues/new) or [join our Discord](https://discord.gg/MkH4qj2Jp9).
+1 -1
View File
@@ -151,6 +151,6 @@ Enjoy all the features of big-AGI without the hassle of infrastructure managemen
Join our vibrant community of developers, researchers, and AI enthusiasts. Share your projects, get help, and collaborate with others.
- [Discord Community](https://discord.gg/MkH4qj2Jp9)
- [Twitter](https://twitter.com/enricoros)
- [Twitter](https://twitter.com/yourusername)
For any questions or inquiries, please don't hesitate to [reach out to our team](mailto:hello@big-agi.com).
+3 -3
View File
@@ -16,8 +16,6 @@ stringData:
OPENAI_API_KEY: ""
OPENAI_API_HOST: ""
OPENAI_API_ORG_ID: ""
ALIBABA_API_HOST: ""
ALIBABA_API_KEY: ""
AZURE_OPENAI_API_ENDPOINT: ""
AZURE_OPENAI_API_KEY: ""
ANTHROPIC_API_KEY: ""
@@ -28,7 +26,6 @@ stringData:
LOCALAI_API_HOST: ""
LOCALAI_API_KEY: ""
MISTRAL_API_KEY: ""
MOONSHOT_API_KEY: ""
OLLAMA_API_HOST: ""
OPENPIPE_API_KEY: ""
OPENROUTER_API_KEY: ""
@@ -47,3 +44,6 @@ stringData:
ELEVENLABS_API_KEY: ""
ELEVENLABS_API_HOST: ""
ELEVENLABS_VOICE_ID: ""
# Text-To-Image: Prodia
PRODIA_API_KEY: ""
Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

-17
View File
@@ -1,17 +0,0 @@
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"),
}]);
-38
View File
@@ -1,38 +0,0 @@
# Knowledge Base
Internal documentation for Big-AGI architecture and systems, for use by AI agents and developers.
**Structure:**
- `/kb/modules/` - Core business logic (e.g. AIX)
- `/kb/systems/` - Infrastructure (routing, startup)
## Index
### Modules Documentation
#### AIX - AI Communication Framework
- **[AIX.md](modules/AIX.md)** - AIX streaming architecture documentation
- **[AIX-callers-analysis.md](modules/AIX-callers-analysis.md)** - Analysis of AIX entry points, call chains, common and different rendering, error handling, etc.
#### CSF - Client-Side Fetch
- **[CSF.md](systems/client-side-fetch.md)** - Direct browser-to-API communication for LLM requests
### Systems Documentation
#### Core Platform Systems
- **[app-routing.md](systems/app-routing.md)** - Next.js routing, provider stack, and display state hierarchy
- **[LLM-parameters-system.md](systems/LLM-parameters-system.md)** - Language model parameter flow across the system
## Guidelines
### Writing Style
- **Direct and factual** - No marketing language
- **Present tense** - "AIX handles streaming" not "AIX will handle"
- **Active voice** - "The system processes" not "Processing is done by"
- **Concrete examples** - Show actual code/config when helpful, briefly
### Maintenance
- Remove outdated information when detected!
- Keep cross-references current when files move
-144
View File
@@ -1,144 +0,0 @@
# AIX Chat Generation Calls Analysis
This document analyzes all AIX function callers and their patterns for message removal, placeholder handling, and error management.
## AIX Function Architecture
### Three-Tier Call Hierarchy
**Core AIX Functions** (Direct tRPC API callers):
- `aixChatGenerateContent_DMessage_FromConversation` - 8 callers (conversation streaming)
- `aixChatGenerateContent_DMessage` - 6 callers (direct request/response)
- `aixChatGenerateText_Simple` - 12 callers (text-only utilities)
**Utility Layer** (Hooks & Functions):
- Conversation management, persona processing, content generation utilities
**UI Layer** (React Components):
- User-facing interfaces with rich error states and fallback mechanisms
## Core Function Callers Analysis
### Conversation-Based Callers (`_FromConversation`)
| **Caller** | **Context** | **Message Removal** | **Placeholder** | **Error Handling** |
|------------|-------------|-------------------|----------------|-------------------|
| **Chat Persona** | `'conversation'` | `messageWasInterruptedAtStart()``removeMessage()` | None | Error fragments |
| **Beam Scatter** | `'beam-scatter'` | `messageWasInterruptedAtStart()` → empty message | `SCATTER_PLACEHOLDER` | Ray status update |
| **Beam Gather** | `'beam-gather'` | `messageWasInterruptedAtStart()` → clear fragments | `GATHER_PLACEHOLDER` | Re-throw errors |
| **Beam Follow-up** | `'beam-followup'` | `messageWasInterruptedAtStart()` → remove message | `FOLLOWUP_PLACEHOLDER` | Status updates |
| **ScratchChat** | `'scratch-chat'` | `aborted && !fragments` → array removal | `SCRATCH_CHAT_PLACEHOLDER` | Error fragments |
| **Telephone** | `'call'` | None | None | Basic handling |
| **ReAct Agent** | `'chat-react-turn'` | None | None | Append errors |
| **Variform** | `'_DEV_'` | None | None | Throw errors |
### Direct Request Callers (`aixChatGenerateContent_DMessage`)
| **Caller** | **Context** | **Message Removal** | **Error Handling** |
|------------|-------------|-------------------|-------------------|
| **Auto Follow-ups** | `'chat-followup-*'` | `fragmentDelete()` on failure | `fragmentReplace()` with error |
| **Gen CR Diffs** | `'aifn-gen-cr-diffs'` | None | State-based handling |
| **Code Fixup** | `'fixup-code'` | None | Throw errors |
| **Attachment Prompts** | `'chat-attachment-prompts'` | None | Throw errors |
### Text-Only Utilities (`aixChatGenerateText_Simple`)
| **Utility** | **Purpose** | **Error Strategy** | **Called By** |
|-------------|-------------|-------------------|---------------|
| **conversationTitle** | Auto-generate chat titles | Try/catch with fallback | UI components |
| **conversationSummary** | Generate summaries | Try/catch with fallback | Chat drawer |
| **useStreamChatText** | Generic text streaming | Error state management | FlattenerModal |
| **useLLMChain** | Multi-step processing | Step-by-step handling | Persona creation |
| **imaginePromptFromText** | Text → image prompts | Simple propagation | Image generation |
| **aifnBeamGenerateBriefing** | Beam summaries | Null return on error | Beam completion |
| **useAifnPersonaGenIdentity** | Extract persona identity | Query error handling | Persona flows |
| **DiagramsModal** | Generate diagrams | Component error state | Manual generation |
## Message Removal Patterns
### 1. Complete Message Removal
- **Chat Persona**: `messageWasInterruptedAtStart()``messageEditor.removeMessage()`
- **ScratchChat**: `outcome === 'aborted' && !fragments?.length` → array removal
- **Trigger**: Message aborted before any content generated
### 2. Fragment-Level Management
- **Beam Gather**: Clear fragments array but keep message structure
- **Auto Follow-ups**: Delete specific placeholder fragments on failure
- **Purpose**: Maintain message structure while removing failed content
### 3. Empty Message Replacement
- **Beam Scatter**: Replace with `createDMessageEmpty()` but preserve ray structure
- **Purpose**: Keep UI structure intact while indicating failure
### 4. No Removal Strategy
- **Text-only functions**: Use fallback values, error states, or null returns
- **Simple callers**: Propagate errors upstream for handling
## Error Handling by Layer
### UI Layer (Components)
- **Pattern**: Rich error states with user-facing messages
- **Examples**: DiagramsModal, FlattenerModal
- **Features**: Retry mechanisms, fallback UI, loading states
### Utility Layer (Hooks/Functions)
- **Pattern**: Graceful degradation with fallbacks
- **Examples**: conversationTitle, conversationSummary
- **Features**: Silent failures, default values, try/catch blocks
### Core Layer (Direct API)
- **Pattern**: Minimal handling, error propagation
- **Examples**: Code Fixup, Attachment Prompts
- **Features**: Assumes upstream error handling
## Key Implementation Details
### Message Removal Detection
```typescript
// Core detection logic
function messageWasInterruptedAtStart(message: Pick<DMessage, 'generator' | 'fragments'>): boolean {
return message.generator?.tokenStopReason === 'client-abort' && message.fragments.length === 0;
}
```
### Placeholder Management
- **Initialization**: `createPlaceholderVoidFragment(placeholderText)`
- **Replacement**: During streaming updates or on completion
- **Cleanup**: Delete on error to avoid stale content
### Context Patterns
- **Production**: `'conversation'`, `'beam-scatter'`, `'scratch-chat'`
- **Features**: `'chat-followup-*'`, `'fixup-code'`, `'ai-diagram'`
- **Development**: `'_DEV_'`
## Best Practices
### Message Removal
- Use `messageWasInterruptedAtStart()` for consistent detection
- Only remove messages with no content that were client-aborted
- Consider UI context when choosing removal vs. clearing strategy
### Error Handling
- **Fragment-level**: Use `messageEditor.fragmentReplace()` with error fragments
- **Message-level**: Use `messageEditor.removeMessage()` or array removal
- **Status-level**: Update component state for UI feedback
### Placeholder Management
- Initialize with descriptive placeholders using `createPlaceholderVoidFragment()`
- Replace during streaming updates
- Clean up on error to prevent stale content
## Architectural Insights
1. **Layered Error Handling**: Sophistication increases closer to UI
2. **Context Specialization**: Different contexts for different use cases
3. **Streaming vs Non-Streaming**: Conversation functions stream, utilities typically don't
4. **Message vs Fragment Management**: Different strategies for different UI needs
The most sophisticated handling is in **Beam modules** and **Chat Persona** with comprehensive removal logic, while simpler callers rely on upstream error handling.
## Code References
- **Core function**: `src/modules/aix/client/aix.client.ts:aixChatGenerateContent_DMessage_FromConversation`
- **Removal check**: `src/common/stores/chat/chat.message.ts:388:messageWasInterruptedAtStart()`
- **Placeholder creation**: `src/common/stores/chat/chat.fragments.ts:createPlaceholderVoidFragment()`
-189
View File
@@ -1,189 +0,0 @@
# AIX
AIX is a client/server library for integrating advanced AI capabilities into web applications.
## Overview
AIX provides real-time, type-safe communication between a Typescript application and AI providers.
Built with tRPC, it manages the lifecycle of AI-generated content from request to rendering, supporting both streaming and non-streaming AI providers.
## Features
- Content Generation
- Multi-Modal streaming/non-streaming
- Throttled batching and error handling
- Server-side timeout/retry
- Function Calling and Code Execution
- Complex AI Workflows (future)
- Embeddings / Information Retrieval / Image Manipulation (future)
## AIX Providers support
| Service | Chat | Function Calling | Multi-Modal Input | Cont. (1) | Streaming | Idiosyncratic |
|------------|------------|------------------|-------------------|-----------|-----------|---------------|
| Alibaba | ✅ | ✅ | | ✅ | Yes + 📦 | |
| Anthropic | ✅ | ✅ + Parallel | Img: ✅ | ✅ | Yes + 📦 | |
| Azure | ✅ | ✅ | | ✅ | Yes + 📦 | |
| Deepseek | ✅ | ❌ (rejected) | | ✅ | Yes + 📦 | |
| Gemini | ✅ | ✅ + Parallel | Img: ✅ | ✅ | Yes + 📦 | Code ex.: ✅ |
| Groq | ✅ | ✅ + Parallel | | ✅ | Yes + 📦 | |
| LM Studio | ✅ | ❌ (not working) | | ❌ | Yes + 📦 | |
| Local AI | ✅ | ✅ | | ❌ | Yes + 📦 | |
| Mistral | ✅ | ✅ | | ✅ | Yes + 📦 | |
| OpenAI | ✅ | ✅ + Parallel | Img: ✅ | ✅ | Yes + 📦 | |
| OpenPipe | ✅ | ✅ | Img: ✅ | ✅ | Yes + 📦 | |
| OpenRouter | ✅ | ❌ (inconsistent) | | ✅ | Yes + 📦 | |
| Perplexity | ✅ | ❌ (rejected) | | ✅ | Yes + 📦 | |
| TogetherAI | ✅ | ✅ | | ✅ | Yes + 📦 | |
| xAI | | | | | | |
| Ollama (2) | ❌ (broken) | ? | | | | |
Notes:
- 1: Continuation marks: a. sends reason=max-tokens (streaming/non-streaming), b. TBA
- 2: Ollama has not been ported to AIX yet due to the custom APIs.
## 1. System Architecture
The subsystem comprises three main components:
1. **Client (e.g. Next.js Frontend)**
- Initiates requests
- Renders AI-generated content in real-time
- Reconstructs streamed data
2. **Server (e.g. Next.js Backend)**
- Acts as an intermediary between client and AI providers
- Handles request preparation, dispatching, and response processing
- Streams responses back to the client
3. **Upstream AI Providers**
- Generate AI content based on requests
### ChatGenerate workflow:
1. Request Initialization: AIX Client prepares and sends request (systemInstruction, messages=AixWire_Parts[], etc.) to AIX Server
2. Dispatch Preparation: AIX Server prepares for upstream communication
3. AI Provider Interaction: AIX Server communicates with AI Provider (streaming or non-streaming)
4. Data Decoding, Transformation and Transmission: AIX Server sends AixWire_Particles to AIX Client
5. Client-side Processing: Client's ContentReassembler processes AixWire_Particles into a list (likely a single) of multi-fragment (DMessageContentFragment[]) messages
6. Completion: AIX Server sends 'done' control message, AIX Client finalizes data update
7. Error Handling: AIX Server sends specific error messages when necessary
## 2. Files and Folders
AIX is organized into the following files and folders:
1. Client-Side (`/client/`):
- `aix.client.ts`: Main client-side entry point for AIX operations.
- `aix.client.chatGenerateRequest.ts`: Handles conversion of chat messages to AIX-compatible format (AixWire_Content, AixWire_Parts, etc.).
2. Server-Side (`/server/`):
- API (`/server/api/`) - Client to Server communication:
- `aix.router.ts`: Defines the tRPC router for AIX operations.
- `aix.wiretypes.ts`: Contains Zod schemas for types and calls incoming from the client (AixWire_Parts, AixWire_Content, AixWire_Tooling, AixWire_API, ...), and outgoing (AixWire_Particles)
- Dispatch (`/server/dispatch/`) - Server to AI Provider communication:
- `/server/dispatch/chatGenerate/`: Content Generation with chat-style inputs:
- `./adapters/`: Adapters for creating API requests for different AI protocols (Anthropic, Gemini, OpenAI).
- `./parsers/`: Parsers for parsing streaming/non-streamin responses from different AI protocols (same 3).
- `chatGenerate.dispatch.ts`: Creates a pipeline to execute Chat Generation to a specific provider.
- `ChatGenerateTransmitter.ts`: Used to serialize and transmit AixWire_Particles to the client.
- `/server/dispatch/wiretypes/`: AI provider Wire Types:
- Type definitions for different AI providers/protocols (Anthropic, Gemini, OpenAI).
- `stream.demuxers.ts`: Handles demuxing of different stream formats.
## 3. Architecture Diagram
```mermaid
sequenceDiagram
participant AIX Client
participant AIX Server
participant PartTransmitter
participant AI Provider
AIX Client ->> AIX Client: Initialize ContentReassembler
AIX Client ->> AIX Client: Convert DMessage*Part to AixWire_Parts
AIX Client ->> AIX Server: Send messages (arrays of AixWire_Parts)
AIX Server ->> AIX Server: Prepare Dispatch (Upstream request, demux, parsing)
alt Dispatch Preparation Error
AIX Server ->> AIX Client: Send `dispatch-prepare` error message
else Dispatch Fetch
AIX Server ->> AI Provider: Send AI-provider specific stream/non-stream request
AIX Server ->> AIX Client: Send 'start' control message
AIX Server ->> PartTransmitter: Initialize part particle serialization
alt Streaming AI Provider
loop Until stream end or error
AI Provider ->> AIX Server: Stream response chunk
AIX Server ->> AIX Server: Demux chunk into DispatchEvents
loop For each AI-provider specific DispatchEvent
AIX Server ->> AIX Server: Parse DispatchEvent
AIX Server ->> PartTransmitter: (Parser) Calls serialization functions
PartTransmitter ->> PartTransmitter: Generate and throttle AixWire_PartParticles
PartTransmitter -->> AIX Server: Yield AixWire_PartParticle
end
AIX Server ->> AIX Client: Send accumulated AixWire_PartParticles
end
AIX Server ->> PartTransmitter: Request any remaining particles
PartTransmitter -->> AIX Server: Yield any final AixWire_PartParticles
AIX Server ->> AIX Client: Send final AixWire_PartParticles (if any)
else Non-Streaming AI Provider
AI Provider ->> AIX Server: Send AI-provider specific complete response
alt AI-provider specific full-response parser
AIX Server ->> AIX Server: Parse full response
AIX Server ->> PartTransmitter: Call particle serialization functions
PartTransmitter ->> PartTransmitter: Generate AixWire_PartParticle
PartTransmitter -->> AIX Server: Yield ALL AixWire_PartParticle
end
AIX Server ->> AIX Client: Send all AixWire_PartParticles
end
AIX Server ->> AIX Client: Send 'done' control message
loop For each received batch of particles
AIX Client ->> AIX Client: ContentReassembler processes particles into DMessage*Part
alt DMessageTextPart
AIX Client ->> AIX Client: Update UI with text content
else DMessageImageRefPart
AIX Client ->> AIX Client: Load and display image
else DMessageToolInvocationPart
AIX Client ->> AIX Client: Process tool invocation (dev only)
else DMessageToolResponsePart
AIX Client ->> AIX Client: Process tool response (dev only)
else DMessageErrorPart
AIX Client ->> AIX Client: Display error message
else DMessageDocPart
AIX Client ->> AIX Client: Process and display document
else DMetaPlaceholderPart
AIX Client ->> AIX Client: Handle placeholder (non-submitted)
end
end
AIX Client ->> AIX Client: Finalize data update
end
alt Error Handling
AIX Server ->> AIX Client: Send 'error' specific control messages
end
note over AIX Server, AI Provider: Server-side Timeout/Retry mechanism
loop Retry on timeout (server-side)
AIX Server ->> AI Provider: Retry request
end
note over AIX Client: Client-side Timeout mechanism
AIX Client ->> AIX Client: Timeout if no response received within set time
```
---
### 2025-03-14 Update
AIX is used in production in Big-AGI and is stable and performant.
The code is tightly coupled with the tRPC framework and the rest of our codebase,
so it is not recommended to use it outside of our ecosystem.
For a great Typescript alternative we recommend the Vercel AI SDK.
-131
View File
@@ -1,131 +0,0 @@
# LLM Parameters System
This document describes how parameters flow through Big-AGI's LLM parameters system, from definition to API invocation.
## System Overview
The LLM parameters system operates across five layers that transform parameters from global definitions to vendor-specific API calls. Each layer serves a specific purpose in the parameter resolution pipeline.
## Parameter Flow Architecture
### Layer 1: Parameter Registry
**File**: `src/common/stores/llms/llms.parameters.ts`
The `DModelParameterRegistry` defines all available parameters with their constraints and metadata. Each parameter includes type information, validation rules, and default behavior.
**Example**: `llmVndOaiReasoningEffort4` defines a 4-value enum with 'medium' as the required fallback.
**Default Value System**: The registry supports multiple default mechanisms:
- `initialValue` - Parameter's base default (e.g., `llmVndOaiRestoreMarkdown: true`)
- `requiredFallback` - Fallback for required parameters (e.g., `llmTemperature: 0.5`)
- `nullable` - Parameters that can be explicitly null to skip API transmission
### Layer 2: Model Specifications
**File**: `src/modules/llms/server/llm.server.types.ts`
Models declare which parameters they support through `parameterSpecs` arrays. Each spec can override registry defaults:
```typescript
parameterSpecs: [
{ paramId: 'llmVndOaiReasoningEffort4' },
{ paramId: 'llmVndAntThinkingBudget', initialValue: 1024 }, // Override default
{ paramId: 'llmVndGeminiThinkingBudget', rangeOverride: [0, 8192] }, // Custom range
]
```
**Parameter Visibility**: The `hidden` flag removes parameters from the UI while keeping them functional. Models can also mark parameters as `required`.
### Layer 3: Client Configuration
The system provides two UI configurators with different scopes:
#### Full Model Configuration Dialog
**File**: `src/modules/llms/models-modal/LLMParametersEditor.tsx`
Shows all non-hidden parameters from model's `parameterSpecs`. Used in the models modal for complete configuration.
#### ChatPanel Quick Controls
**File**: `src/apps/chat/components/layout-panel/ChatPanelModelParameters.tsx`
Shows only parameters that are:
- In model's `parameterSpecs`
- Listed in `_interestingParameters` array
- Not marked as `hidden`
**Value Resolution**: Both UIs use `getAllModelParameterValues()` to merge:
1. **Fallback values** - Required parameters get their `requiredFallback` values
2. **Initial values** - Model's `initialParameters` (populated during model creation)
3. **User values** - User's `userParameters` (highest priority)
### Layer 4: AIX Translation
**File**: `src/modules/aix/client/aix.client.ts`
The AIX client transforms DLLM parameters to wire protocol format. This layer handles parameter precedence rules and name transformations:
```
// Parameter precedence: newer 4-value version takes priority over 3-value
...((llmVndOaiReasoningEffort4 || llmVndOaiReasoningEffort) ?
{ vndOaiReasoningEffort: llmVndOaiReasoningEffort4 || llmVndOaiReasoningEffort } : {})
```
**Client Options**: The system supports parameter overrides through `llmOptionsOverride` and complete replacement via `llmUserParametersReplacement`.
### Layer 5: Vendor Adaptation
**Files**: `src/modules/aix/server/dispatch/chatGenerate/adapters/*.ts`
Server-side adapters translate AIX parameters to vendor APIs. Each vendor may interpret parameters differently:
- **OpenAI**: `vndOaiReasoningEffort``reasoning_effort`
- **Perplexity**: Reuses OpenAI parameter format
- **OpenAI Responses API**: Maps to structured reasoning config with additional logic
## Parameter Initialization Process
When a model is loaded:
1. **Model Creation**: `modelDescriptionToDLLM()` creates the DLLM with empty `initialParameters`
2. **Initial Value Application**: `applyModelParameterInitialValues()` populates initial values from:
- Model spec `initialValue` (highest priority)
- Registry `initialValue` (fallback)
3. **Runtime Resolution**: `getAllModelParameterValues()` creates final parameter set:
- Required fallbacks (for missing required parameters)
- Initial parameters (model defaults)
- User parameters (user overrides)
## Special Parameter Behaviors
**Hidden Parameters**: Parameters like `llmRef` are marked `hidden: true` in the registry and never appear in the UI, but remain functional for system use.
**Nullable Parameters**: Parameters with `nullable` configuration can be explicitly set to `null` to prevent transmission to the API, distinct from being undefined.
**Range Overrides**: Models can override parameter ranges (e.g., different Gemini models support different thinking budget ranges).
**Parameter Interactions**: The UI implements business logic like disabling web search when reasoning effort is 'minimal'.
## Type Safety Mechanisms
The system maintains type safety through:
- `DModelParameterId` union from registry keys
- `DModelParameterValue<T>` conditional types for values
- `DModelParameterSpec<T>` interfaces for specifications
- Runtime validation via Zod schemas at API boundaries
## Model Variant Pattern
Some vendors use model variants to enable features, for instance:
- **Anthropic**: Creates separate `idVariant: 'thinking'` entries forcing value of hidden parameters
- **Google/OpenAI**: Parameters directly on base models
## Migration and Compatibility
The architecture supports parameter evolution:
- **Version Coexistence**: Both `llmVndOaiReasoningEffort` and `llmVndOaiReasoningEffort4` exist simultaneously
- **Precedence Rules**: Newer parameters take priority during AIX translation
- **Graceful Degradation**: Unknown parameters log warnings but don't break functionality
## Key Implementation Files
- **Registry**: `src/common/stores/llms/llms.parameters.ts`
- **Specifications**: `src/modules/llms/server/llm.server.types.ts`
- **UI Controls**: `src/modules/llms/models-modal/LLMParametersEditor.tsx`
- **AIX Translation**: `src/modules/aix/client/aix.client.ts`
- **Wire Types**: `src/modules/aix/server/api/aix.wiretypes.ts`
- **Vendor Adapters**: `src/modules/aix/server/dispatch/chatGenerate/adapters/*.ts`
-151
View File
@@ -1,151 +0,0 @@
# Big-AGI Routing & Display States
This document describes the routing architecture and display state hierarchy in Big-AGI, from top-level providers down to component-level states.
## Overview
Big-AGI uses Next.js Pages Router with a provider stack that determines what users see based on application state and configuration.
## Quick Reference: Route Configurations
| Route | Purpose | Key Features |
|-------|---------|--------------|
| `/` | Main chat app | Default application |
| `/call` | Voice interface | Voice-to-voice AI conversations |
| `/personas` | Persona management | Create and manage AI personas |
| ... | | |
## Decision Flow Diagram
The routing decisions follow a hierarchy from system-level provider configuration down to component-level states.
```mermaid
flowchart TD
Start([Navigate to Route]) --> Root[_app.tsx]
Root --> Theme[ProviderTheming]
Theme --> Error[ErrorBoundary]
Error --> Bootstrap[ProviderBootstrapLogic]
Bootstrap --> BootCheck{Bootstrap Checks}
BootCheck -->|News| News[↗️ /news]
BootCheck -->|Continue| Router{Router}
Router -->|/| Chat[Chat App]
Router -->|/personas,/call,/beam...| OtherApps[Other Apps]
Router -->|/news| NewsApp[News App]
Chat --> ChatStates{Chat States}
ChatStates -->|No Models| ZeroModels[🟡 Setup Models]
ChatStates -->|No Conv| ZeroConv[🟡 Select Chat]
ChatStates -->|No Msgs| PersonaGrid[Choose Persona]
ChatStates -->|Ready| Active[🟢 Active Chat]
Active --> Features[Features:<br/>• Chat Bar<br/>• Beam Mode<br/>• Attachments]
style ZeroModels fill:#fff4cc
style ZeroConv fill:#fff4cc
style Active fill:#ccffcc
style Chat fill:#f0f8ff
style OtherApps fill:#f0f8ff
style NewsApp fill:#f0f8ff
```
## Display State Hierarchy
```
_app.tsx (Root)
├── ProviderTheming ← Always Applied
├── ErrorBoundary ← Always Applied
├── ProviderBootstrapLogic ← Always Applied
│ ├── Tiktoken preload & Model auto-config
│ ├── Storage maintenance & cleanup
│ └── News Redirect (if conditions met)
└── Page Component
├── AppChat (/) → Default app
│ ├── CMLZeroModels → If no models configured
│ ├── CMLZeroConversation → If no conversation selected
│ └── PersonaGrid → If conversation empty
└── Other Apps → Personas, Call, Draw, News, Beam
```
## Provider Stack
| Provider | Purpose | Key Functions |
|----------|---------|---------------|
| **ProviderTheming** | UI theme management | Theme switching, CSS variables |
| **ErrorBoundary** | Error handling | Catches and displays errors gracefully |
| **ProviderBootstrapLogic** | App initialization | • Tiktoken preload<br>• Model auto-config<br>• Storage cleanup<br>• News redirect logic |
For detailed initialization sequence and provider functions, see [app-startup-sequence.md](app-startup-sequence.md), if present.
## Application Routes
### Primary Apps
- `/` → AppChat (default)
- `/call` → Voice call interface
- `/beam` → Multi-model reasoning
- `/draw` → Image generation
- `/personas` → Personas app
- `/news` → News/updates
### Zero States
#### Chat App Zero States
**CMLZeroModels**
- **Location**: `/src/apps/chat/components/messages-list/CMLZeroModels.tsx`
- **Triggered**: No LLM sources configured
- **Shows**: Welcome screen with "Setup Models" button
**CMLZeroConversation**
- **Location**: `/src/apps/chat/components/messages-list/CMLZeroConversation.tsx`
- **Triggered**: No conversation selected
- **Shows**: "Select/create conversation" prompt
**PersonaGrid**
- **App**: Chat (when conversation is empty)
- **Triggered**: Conversation exists but has no messages
- **Shows**: Persona selector interface
#### Feature-Specific Zero States
**Beam Tutorial**
- **Feature**: Beam (multi-model reasoning)
- **Component**: `ExplainerCarousel`
- **Triggered**: First-time Beam usage
- **Shows**: Interactive feature walkthrough
## Common Scenarios
### New User First Visit
1. Navigates to `/` → Provider stack loads
2. Bootstrap runs → No news redirect (first visit)
3. Chat loads → **CMLZeroModels** (no models configured)
4. User clicks "Setup Models" → Configuration flow
### Returning User with Saved State
1. Navigates to `/` → Provider stack loads
2. IndexedDB restores state → Previous conversation loaded
3. Chat loads → **Active chat interface** (bypasses all zero states)
4. All messages and context preserved from last session
### Shared Chat Viewer
1. Navigates to `/link/chat/[id]` → Full provider stack
2. Views read-only chat → May see "Import" option
3. If importing → Checks for duplicates, creates new local conversation
## Storage System
Big-AGI uses a local-first architecture:
- **Zustand** for reactive state management
- **IndexedDB** for persistent storage via Zustand persist middleware
- **Version-based migrations** for data structure upgrades
Key stores:
- `app-chats`: Conversations and messages (IndexedDB)
- `app-llms`: Model configurations (IndexedDB)
- `app-ui`: UI preferences (localStorage)
-13
View File
@@ -1,13 +0,0 @@
# CSF - Client-Side Fetch
Client-Side Fetch (CSF) enables direct browser-to-API communication, bypassing the server for LLM requests. When enabled, the browser makes requests directly to vendor APIs (e.g., `api.openai.com`, `api.groq.com`) instead of routing through the Next.js server. This reduces latency, decreases server load, and is particularly useful for local models where the browser can communicate directly with Ollama or LM Studio.
## Implementation
CSF is implemented as an opt-in setting stored as `csf: boolean` in each vendor's service settings. The vendor interface exposes `csfAvailable?: (setup) => boolean` to determine if CSF can be enabled (typically checking if an API key or host is configured). The actual execution happens in `aix.client.direct-chatGenerate.ts` which dynamically imports when CSF is active, making direct fetch calls using the same wire protocols as the server.
All 16 supported vendors (OpenAI, Anthropic, Gemini, Ollama, LocalAI, Deepseek, Groq, Mistral, xAI, OpenRouter, Perplexity, Together AI, Alibaba, Moonshot, OpenPipe, LM Studio) support CSF. Cloud vendors require CORS support from the API provider (all tested vendors return `access-control-allow-origin: *`). Local vendors (Ollama, LocalAI, LM Studio) require CORS to be enabled on the local server.
## UI
The CSF toggle appears in each vendor's setup panel under "Advanced" settings, labeled "Direct Connection". It becomes visible when the prerequisites are met (API key present for cloud vendors, host configured for local vendors). The setting is managed through `useModelServiceClientSideFetch` hook which provides `csfAvailable`, `csfActive`, `csfToggle`, and `csfReset` for UI consumption.
+85
View File
@@ -0,0 +1,85 @@
import { readFile } from 'node:fs/promises';
// Build information
process.env.NEXT_PUBLIC_BUILD_HASH = 'big-agi-2-dev';
process.env.NEXT_PUBLIC_BUILD_PKGVER = JSON.parse('' + await readFile(new URL('./package.json', import.meta.url))).version;
process.env.NEXT_PUBLIC_BUILD_TIMESTAMP = new Date().toISOString();
console.log(` 🧠 \x1b[1mbig-AGI\x1b[0m v${process.env.NEXT_PUBLIC_BUILD_PKGVER} (@${process.env.NEXT_PUBLIC_BUILD_HASH})`);
// Non-default build types
const buildType =
process.env.BIG_AGI_BUILD === 'standalone' ? 'standalone'
: process.env.BIG_AGI_BUILD === 'static' ? 'export'
: undefined;
buildType && console.log(` 🧠 big-AGI: building for ${buildType}...\n`);
/** @type {import('next').NextConfig} */
let nextConfig = {
reactStrictMode: true,
// [exports] https://nextjs.org/docs/advanced-features/static-html-export
...buildType && {
output: buildType,
distDir: 'dist',
// disable image optimization for exports
images: { unoptimized: true },
// Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
// trailingSlash: true,
},
// [puppeteer] https://github.com/puppeteer/puppeteer/issues/11052
// NOTE: we may not be needing this anymore, as we use '@cloudflare/puppeteer'
serverExternalPackages: ['puppeteer-core'],
webpack: (config, { isServer }) => {
// @mui/joy: anything material gets redirected to Joy
config.resolve.alias['@mui/material'] = '@mui/joy';
// @dqbd/tiktoken: enable asynchronous WebAssembly
config.experiments = {
asyncWebAssembly: true,
layers: true,
};
// fix warnings for async functions in the browser (https://github.com/vercel/next.js/issues/64792)
if (!isServer) {
config.output.environment = { ...config.output.environment, asyncFunction: true };
}
// prevent too many small chunks (40kb min) on 'client' packs (not 'server' or 'edge-server')
// noinspection JSUnresolvedReference
if (typeof config.optimization.splitChunks === 'object' && config.optimization.splitChunks.minSize) {
// noinspection JSUnresolvedReference
config.optimization.splitChunks.minSize = 40 * 1024;
}
return config;
},
// Note: disabled to check whether the project becomes slower with this
// modularizeImports: {
// '@mui/icons-material': {
// transform: '@mui/icons-material/{{member}}',
// },
// },
// Uncomment the following leave console messages in production
// compiler: {
// removeConsole: false,
// },
};
// Validate environment variables, if set at build time. Will be actually read and used at runtime.
// This is the reason both this file and the servr/env.mjs files have this extension.
await import('./src/server/env.mjs');
// conditionally enable the nextjs bundle analyzer
if (process.env.ANALYZE_BUNDLE) {
const { default: withBundleAnalyzer } = await import('@next/bundle-analyzer');
nextConfig = withBundleAnalyzer({ openAnalyzer: true })(nextConfig);
}
export default nextConfig;
-160
View File
@@ -1,160 +0,0 @@
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';
// Build information: from CI, or git commit hash
let buildHash = process.env.NEXT_PUBLIC_BUILD_HASH || process.env.GITHUB_SHA || process.env.VERCEL_GIT_COMMIT_SHA; // Docker or custom, GitHub Actions, Vercel
try {
// fallback to local git commit hash
if (!buildHash)
buildHash = execSync('git rev-parse --short HEAD').toString().trim();
} catch {
// final fallback
buildHash = '2-dev';
}
// The following are used by/available to Release.buildInfo(...)
process.env.NEXT_PUBLIC_BUILD_HASH = (buildHash || '').slice(0, 10);
process.env.NEXT_PUBLIC_BUILD_PKGVER = JSON.parse('' + readFileSync(new URL('./package.json', import.meta.url))).version;
process.env.NEXT_PUBLIC_BUILD_TIMESTAMP = new Date().toISOString();
process.env.NEXT_PUBLIC_DEPLOYMENT_TYPE = process.env.NEXT_PUBLIC_DEPLOYMENT_TYPE || (process.env.VERCEL_ENV ? `vercel-${process.env.VERCEL_ENV}` : 'local'); // Docker or custom, Vercel
console.log(` 🧠 \x1b[1mbig-AGI\x1b[0m v${process.env.NEXT_PUBLIC_BUILD_PKGVER} (@${process.env.NEXT_PUBLIC_BUILD_HASH})`);
// Non-default build types
const buildType =
process.env.BIG_AGI_BUILD === 'standalone' ? 'standalone' as const
: process.env.BIG_AGI_BUILD === 'static' ? 'export' as const
: undefined;
buildType && console.log(` 🧠 big-AGI: building for ${buildType}...\n`);
/** @type {import('next').NextConfig} */
let nextConfig: NextConfig = {
reactStrictMode: !process.env.NO_STRICT_MODE, // default: enabled
// [exports] https://nextjs.org/docs/advanced-features/static-html-export
...(buildType && {
output: buildType,
distDir: 'dist',
// disable image optimization for exports
images: { unoptimized: true },
// Optional: Change links `/me` -> `/me/` and emit `/me.html` -> `/me/index.html`
// trailingSlash: true,
}),
// [puppeteer] https://github.com/puppeteer/puppeteer/issues/11052
// NOTE: we may not be needing this anymore, as we use '@cloudflare/puppeteer'
serverExternalPackages: ['puppeteer-core'],
webpack: (config: any, { isServer, webpack /*, dev, nextRuntime*/ }: WebpackConfigContext) => {
// @mui/joy: anything material gets redirected to Joy
config.resolve.alias['@mui/material'] = '@mui/joy';
// @dqbd/tiktoken: enable asynchronous WebAssembly
config.experiments = {
asyncWebAssembly: true,
layers: true,
};
// 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 };
}
// prevent too many small chunks (40kb min) on 'client' packs (not 'server' or 'edge-server')
// noinspection JSUnresolvedReference
if (typeof config.optimization.splitChunks === 'object' && config.optimization.splitChunks.minSize) {
// noinspection JSUnresolvedReference
config.optimization.splitChunks.minSize = 40 * 1024;
}
return config;
},
// Optional Analytics > PostHog
skipTrailingSlashRedirect: true, // required to support PostHog trailing slash API requests
async rewrites() {
return [
{
source: '/a/ph/static/:path*',
destination: 'https://us-assets.i.posthog.com/static/:path*',
},
{
source: '/a/ph/:path*',
destination: 'https://us.i.posthog.com/:path*',
},
{
source: '/a/ph/decide',
destination: 'https://us.i.posthog.com/decide',
},
{
source: '/a/ph/flags',
destination: 'https://us.i.posthog.com/flags',
},
];
},
// Note: disabled to check whether the project becomes slower with this
// modularizeImports: {
// '@mui/icons-material': {
// transform: '@mui/icons-material/{{member}}',
// },
// },
// Uncomment the following leave console messages in production
// compiler: {
// removeConsole: false,
// },
};
// 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';
if (process.env.POSTHOG_API_KEY && process.env.POSTHOG_ENV_ID) {
console.log(' 🧠 \x1b[1mbig-AGI\x1b[0m: building with PostHog issue reporting and source maps...');
nextConfig = withPostHogConfig(nextConfig, {
personalApiKey: process.env.POSTHOG_API_KEY,
envId: process.env.POSTHOG_ENV_ID,
host: 'https://us.i.posthog.com', // backtrace upload host
logLevel: 'error', // lowered, too noisy
sourcemaps: {
enabled: process.env.NODE_ENV === 'production',
project: 'big-agi',
version: process.env.NEXT_PUBLIC_BUILD_HASH,
deleteAfterUpload: false, // false: leave them in the tree, which would also help debugging of open-source installs
},
});
}
// conditionally enable the nextjs bundle analyzer
import withBundleAnalyzer from '@next/bundle-analyzer';
if (process.env.ANALYZE_BUNDLE) {
nextConfig = withBundleAnalyzer({ openAnalyzer: true })(nextConfig) as NextConfig;
}
export default nextConfig;
+1492 -3739
View File
File diff suppressed because it is too large Load Diff
+67 -55
View File
@@ -1,6 +1,6 @@
{
"name": "big-agi",
"version": "2.0.2",
"version": "1.91.0",
"private": true,
"author": "Enrico Ros <enrico.ros@gmail.com>",
"repository": "https://github.com/enricoros/big-agi",
@@ -14,8 +14,7 @@
"postinstall": "prisma generate --no-hints",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"vercel:env:pull": "npx vercel env pull .env.development.local",
"sharp:win32_x64": "npm install --os=win32 --cpu=x64 sharp"
"vercel:env:pull": "npx vercel env pull .env.development.local"
},
"prisma": {
"schema": "src/server/prisma/schema.prisma"
@@ -28,72 +27,85 @@
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^5.18.0",
"@mui/joy": "^5.0.0-beta.52",
"@next/bundle-analyzer": "~15.1.8",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^5.16.14",
"@mui/joy": "^5.0.0-beta.51",
"@mui/material": "^5.16.14",
"@next/bundle-analyzer": "^15.1.4",
"@next/third-parties": "^15.1.4",
"@prisma/client": "~5.22.0",
"@tanstack/react-query": "5.90.10",
"@tanstack/react-virtual": "^3.13.12",
"@trpc/client": "11.5.1",
"@trpc/next": "11.5.1",
"@trpc/react-query": "11.5.1",
"@trpc/server": "11.5.1",
"@vercel/analytics": "^1.5.0",
"@vercel/speed-insights": "^1.2.0",
"browser-fs-access": "^0.38.0",
"cheerio": "^1.1.2",
"csv-stringify": "^6.6.0",
"dexie": "~4.0.11",
"dexie-react-hooks": "~1.1.7",
"diff": "^8.0.2",
"eventemitter3": "^5.0.1",
"idb-keyval": "^6.2.2",
"mammoth": "^1.11.0",
"nanoid": "^5.1.6",
"next": "~15.1.8",
"@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.63.0",
"@tanstack/react-virtual": "^3.11.2",
"@trpc/client": "11.0.0-rc.688",
"@trpc/next": "11.0.0-rc.688",
"@trpc/react-query": "11.0.0-rc.688",
"@trpc/server": "11.0.0-rc.688",
"@vercel/analytics": "^1.4.1",
"@vercel/speed-insights": "^1.1.0",
"browser-fs-access": "^0.35.0",
"cheerio": "^1.0.0",
"dexie": "^4.0.10",
"dexie-react-hooks": "^1.1.7",
"diff": "^7.0.0",
"eventsource-parser": "^3.0.0",
"idb-keyval": "^6.2.1",
"mammoth": "^1.9.0",
"nanoid": "^5.0.9",
"next": "^15.1.4",
"nprogress": "^0.2.0",
"pdfjs-dist": "5.4.54",
"posthog-js": "^1.298.1",
"posthog-node": "^5.14.0",
"prismjs": "^1.30.0",
"puppeteer-core": "^24.31.0",
"pdfjs-dist": "4.10.38",
"plantuml-encoder": "^1.4.0",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-csv": "^2.2.2",
"react-dom": "^18.3.1",
"react-hook-form": "^7.66.1",
"react-markdown": "^10.1.0",
"react-player": "^3.4.0",
"react-resizable-panels": "^3.0.6",
"react-timeago": "^8.3.0",
"react-hook-form": "^7.54.2",
"react-katex": "^3.0.1",
"react-markdown": "^9.0.3",
"react-player": "^2.16.0",
"react-resizable-panels": "^2.1.7",
"react-timeago": "^7.2.0",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-gfm": "^4.0.0",
"remark-mark-highlight": "^0.1.1",
"remark-math": "^6.0.0",
"sharp": "^0.34.5",
"superjson": "^2.2.6",
"tesseract.js": "^6.0.1",
"tiktoken": "^1.0.22",
"turndown": "^7.2.2",
"zod": "^4.1.13",
"zustand": "5.0.7"
"sharp": "^0.33.5",
"superjson": "^2.2.2",
"tesseract.js": "^6.0.0",
"tiktoken": "^1.0.18",
"turndown": "^7.2.0",
"zod": "^3.24.1",
"zod-to-json-schema": "^3.24.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@posthog/nextjs-config": "^1.6.0",
"@types/node": "^24.10.1",
"@types/diff": "^7.0.0",
"@types/node": "^22.10.5",
"@types/nprogress": "^0.2.3",
"@types/plantuml-encoder": "^1.4.2",
"@types/prismjs": "^1.26.5",
"@types/react": "^19.2.7",
"@types/react": "^18.3.18",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-csv": "^1.1.10",
"@types/react-dom": "^19.2.3",
"@types/turndown": "^5.0.6",
"cross-env": "^10.1.0",
"eslint": "^9.39.1",
"eslint-config-next": "~15.1.8",
"prettier": "^3.6.2",
"@types/react-dom": "^18.3.5",
"@types/react-katex": "^3.0.4",
"@types/react-timeago": "^4.1.7",
"@types/turndown": "^5.0.5",
"cross-env": "^7.0.3",
"eslint": "^9.17.0",
"eslint-config-next": "^15.1.4",
"prettier": "^3.4.2",
"prisma": "~5.22.0",
"typescript": "^5.9.3"
"puppeteer-core": "^23.11.1",
"typescript": "^5.7.3"
},
"engines": {
"node": "^26.0.0 || ^24.0.0 || ^22.0.0 || ^20.0.0"
"node": "^22.0.0 || ^20.0.0"
},
"overrides": {
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"uri-js": "npm:uri-js-replace"
}
}
+9 -19
View File
@@ -1,17 +1,12 @@
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';
@@ -19,7 +14,6 @@ import '~/common/styles/NProgress.css';
import '~/common/styles/agi.effects.css';
import '~/common/styles/app.styles.css';
import { ErrorBoundary } from '~/common/components/ErrorBoundary';
import { Is } from '~/common/util/pwaUtils';
import { OverlaysInsert } from '~/common/layout/overlays/OverlaysInsert';
import { ProviderBackendCapabilities } from '~/common/providers/ProviderBackendCapabilities';
@@ -27,8 +21,7 @@ import { ProviderBootstrapLogic } from '~/common/providers/ProviderBootstrapLogi
import { ProviderSingleTab } from '~/common/providers/ProviderSingleTab';
import { ProviderTheming } from '~/common/providers/ProviderTheming';
import { SnackbarInsert } from '~/common/components/snackbar/SnackbarInsert';
import { hasGoogleAnalytics, OptionalGoogleAnalytics } from '~/common/components/3rdparty/GoogleAnalytics';
import { hasPostHogAnalytics, OptionalPostHogAnalytics } from '~/common/components/3rdparty/PostHogAnalytics';
import { hasGoogleAnalytics, OptionalGoogleAnalytics } from '~/common/components/GoogleAnalytics';
const Big_AGI_App = ({ Component, emotionCache, pageProps }: MyAppProps) => {
@@ -49,21 +42,18 @@ const Big_AGI_App = ({ Component, emotionCache, pageProps }: MyAppProps) => {
<ProviderSingleTab>
<ProviderBackendCapabilities>
{/* ^ Backend capabilities & SSR boundary */}
<ErrorBoundary outer>
<ProviderBootstrapLogic>
<SnackbarInsert />
{getLayout(<Component {...pageProps} />)}
<OverlaysInsert />
</ProviderBootstrapLogic>
</ErrorBoundary>
<ProviderBootstrapLogic>
<SnackbarInsert />
{getLayout(<Component {...pageProps} />)}
<OverlaysInsert />
</ProviderBootstrapLogic>
</ProviderBackendCapabilities>
</ProviderSingleTab>
</ProviderTheming>
{hasGoogleAnalytics && <OptionalGoogleAnalytics />}
{hasPostHogAnalytics && <OptionalPostHogAnalytics />}
{Is.Deployment.VercelFromFrontend && <VercelAnalytics debug={false} />}
{Is.Deployment.VercelFromFrontend && <VercelSpeedInsights debug={false} sampleRate={1 / 2} />}
{hasGoogleAnalytics && <OptionalGoogleAnalytics />}
</>;
};
+1 -4
View File
@@ -100,10 +100,6 @@ MyDocument.getInitialProps = async (ctx: DocumentContext) => {
});
const initialProps = await Document.getInitialProps(ctx);
// Inject the comment before the HTML tag
initialProps.html = `<!-- ❤ Built with Big-AGI -->\n${initialProps.html}`;
// This is important. It prevents Emotion to render invalid HTML.
// See https://github.com/mui/material-ui/issues/26561#issuecomment-855286153
const emotionStyles = extractCriticalToChunks(initialProps.html);
@@ -111,6 +107,7 @@ 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 }}
/>
));
+5 -3
View File
@@ -18,18 +18,18 @@ import { ROUTE_APP_CHAT, ROUTE_INDEX } from '~/common/app.routes';
import { Release } from '~/common/app.release';
// capabilities access
import { useCapabilityBrowserSpeechRecognition, useCapabilityTextToImage } from '~/common/components/useCapabilities';
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs, useCapabilityTextToImage } from '~/common/components/useCapabilities';
// stores access
import { getLLMsDebugInfo } from '~/common/stores/llms/store-llms';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { useFolderStore } from '~/common/stores/folders/store-chat-folders';
import { useLogicSherpaStore } from '~/common/logic/store-logic-sherpa';
import { useUXLabsStore } from '~/common/stores/store-ux-labs';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
// utils access
import { BrowserLang, clientHostName, Is, isPwa } from '~/common/util/pwaUtils';
import { getGA4MeasurementId } from '~/common/components/3rdparty/GoogleAnalytics';
import { getGA4MeasurementId } from '~/common/components/GoogleAnalytics';
import { prettyTimestampForFilenames } from '~/common/util/timeUtils';
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
@@ -95,6 +95,7 @@ function AppDebug() {
const cProduct = {
capabilities: {
mic: useCapabilityBrowserSpeechRecognition(),
elevenLabs: useCapabilityElevenLabs(),
textToImage: useCapabilityTextToImage(),
},
models: getLLMsDebugInfo(),
@@ -108,6 +109,7 @@ function AppDebug() {
reloads: usageCount,
},
release: {
app: Release.App,
build: frontendBuild,
},
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -10,6 +10,7 @@ import { createBeamVanillaStore } from '~/modules/beam/store-beam_vanilla';
import { OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { createDConversation, DConversation } from '~/common/stores/chat/chat.conversation';
import { createDMessageTextContent, DMessage } from '~/common/stores/chat/chat.message';
import { getChatLLMId } from '~/common/stores/llms/store-llms';
import { useIsMobile } from '~/common/components/useMatchMedia';
@@ -20,8 +21,8 @@ function initTestConversation(): DConversation {
return conversation;
}
function initTestBeamStore(messages: DMessage[], beamStore: BeamStoreApi): BeamStoreApi {
beamStore.getState().open(messages, null, false, (content) => alert(content));
function initTestBeamStore(messages: DMessage[], beamStore: BeamStoreApi = createBeamVanillaStore()): BeamStoreApi {
beamStore.getState().open(messages, getChatLLMId(), false, (content) => alert(content));
return beamStore;
}
+13 -16
View File
@@ -6,17 +6,15 @@ import ChatIcon from '@mui/icons-material/Chat';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import MicIcon from '@mui/icons-material/Mic';
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
import { useSpeexGlobalEngine } from '~/modules/speex/store-module-speex';
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
import { animationColorRainbow } from '~/common/util/animUtils';
import { navigateBack } from '~/common/app.routes';
import { optimaOpenPreferences } from '~/common/layout/optima/useOptima';
import { useCapabilityBrowserSpeechRecognition } from '~/common/components/useCapabilities';
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { useUICounter } from '~/common/stores/store-ui';
import { useUICounter } from '~/common/state/store-ui';
function StatusCard(props: { icon: React.JSX.Element, hasIssue: boolean, text: string, button?: React.JSX.Element }) {
@@ -47,7 +45,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
// external state
const recognition = useCapabilityBrowserSpeechRecognition();
const speexGlobalEngine = useSpeexGlobalEngine();
const synthesis = useCapabilityElevenLabs();
const chatIsEmpty = useChatStore(state => {
if (!props.conversationId)
return false;
@@ -60,16 +58,15 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
const outOfTheBlue = !props.conversationId;
const overriddenEmptyChat = chatEmptyOverride || !chatIsEmpty;
const overriddenRecognition = recognitionOverride || recognition.mayWork;
const synthesisShallWork = !!speexGlobalEngine;
const allGood = overriddenEmptyChat && overriddenRecognition && synthesisShallWork;
const fatalGood = overriddenRecognition && synthesisShallWork;
const allGood = overriddenEmptyChat && overriddenRecognition && synthesis.mayWork;
const fatalGood = overriddenRecognition && synthesis.mayWork;
const handleOverrideChatEmpty = React.useCallback(() => setChatEmptyOverride(true), []);
const handleOverrideRecognition = React.useCallback(() => setRecognitionOverride(true), []);
const handleConfigureVoice = React.useCallback(() => optimaOpenPreferences('voice'), []);
const handleConfigureElevenLabs = React.useCallback(() => optimaOpenPreferences('voice'), []);
const handleFinishButton = React.useCallback(() => {
if (!allGood)
@@ -131,17 +128,17 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
{/* Text to Speech status */}
<StatusCard
icon={<PhVoice />}
icon={<RecordVoiceOverTwoToneIcon />}
text={
(synthesisShallWork ? 'Voice synthesis should be ready.' : 'There might be an issue with voice synthesis.')
// + (synthesis.isConfiguredServerSide ? '' : (synthesis.isConfiguredClientSide ? '' : ' Please add your API key in the settings.'))
(synthesis.mayWork ? 'Voice synthesis should be ready.' : 'There might be an issue with ElevenLabs voice synthesis.')
+ (synthesis.isConfiguredServerSide ? '' : (synthesis.isConfiguredClientSide ? '' : ' Please add your API key in the settings.'))
}
button={synthesisShallWork ? undefined : (
<Button variant='outlined' onClick={handleConfigureVoice} sx={{ mx: 1 }}>
button={synthesis.mayWork ? undefined : (
<Button variant='outlined' onClick={handleConfigureElevenLabs} sx={{ mx: 1 }}>
Configure
</Button>
)}
hasIssue={!synthesisShallWork}
hasIssue={!synthesis.mayWork}
/>
{/*<Typography>*/}
+28 -35
View File
@@ -5,11 +5,11 @@ import { Avatar, Box, Card, CardContent, Chip, IconButton, Link as MuiLink, List
import CallIcon from '@mui/icons-material/Call';
import { GitHubProjectIssueCard } from '~/common/components/GitHubProjectIssueCard';
import { OptimaPanelGroupedList } from '~/common/layout/optima/panel/OptimaPanelGroupedList';
import { OptimaPanelIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { OptimaPanelGroup } from '~/common/layout/optima/panel/OptimaPanelGroup';
import { animationShadowRingLimey } from '~/common/util/animUtils';
import { conversationTitle, DConversation, DConversationId } from '~/common/stores/chat/chat.conversation';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { useSetOptimaAppMenu } from '~/common/layout/optima/useOptima';
import type { AppCallIntent } from './AppCall';
import { MockPersona, useMockPersonas } from './state/useMockPersonas';
@@ -210,7 +210,7 @@ function useConversationsByPersona() {
}
function ContactsMenuItems() {
export function Contacts(props: { setCallIntent: (intent: AppCallIntent) => void }) {
// external state
const {
@@ -218,42 +218,35 @@ function ContactsMenuItems() {
showConversations, toggleShowConversations,
showSupport, toggleShowSupport,
} = useAppCallStore();
return (
<OptimaPanelGroupedList title='Contacts Settings'>
<MenuItem onClick={toggleGrayUI}>
Grayed UI
<Switch checked={grayUI} sx={{ ml: 'auto' }} />
</MenuItem>
<MenuItem onClick={toggleShowConversations}>
Conversations
<Switch checked={showConversations} sx={{ ml: 'auto' }} />
</MenuItem>
<MenuItem onClick={toggleShowSupport}>
Show Support
<Switch checked={showSupport} sx={{ ml: 'auto' }} />
</MenuItem>
</OptimaPanelGroupedList>
);
}
export function Contacts(props: { setCallIntent: (intent: AppCallIntent) => void }) {
// external state
const { personas } = useMockPersonas();
const { grayUI, showConversations, showSupport } = useAppCallStore();
const conversationsByPersona = useConversationsByPersona();
return <>
// pluggable UI
{/* -> Panel */}
<OptimaPanelIn><ContactsMenuItems /></OptimaPanelIn>
const menuItems = React.useMemo(() => <OptimaPanelGroup title='Contacts Settings'>
<MenuItem onClick={toggleGrayUI}>
Grayed UI
<Switch checked={grayUI} sx={{ ml: 'auto' }} />
</MenuItem>
<MenuItem onClick={toggleShowConversations}>
Conversations
<Switch checked={showConversations} sx={{ ml: 'auto' }} />
</MenuItem>
<MenuItem onClick={toggleShowSupport}>
Show Support
<Switch checked={showSupport} sx={{ ml: 'auto' }} />
</MenuItem>
</OptimaPanelGroup>, [grayUI, showConversations, showSupport, toggleGrayUI, toggleShowConversations, toggleShowSupport]);
useSetOptimaAppMenu(menuItems, 'CallUI-Contacts');
return <>
{/* Header "Call AGI" */}
<Box sx={{
@@ -317,7 +310,7 @@ export function Contacts(props: { setCallIntent: (intent: AppCallIntent) => void
issue={354}
text='Call App: Support thread and compatibility matrix'
note={<>
Voice input uses the HTML Web Speech API.
Voice input uses the HTML Web Speech API, and speech output requires an ElevenLabs API Key.
</>}
// note2='Please report any issues you encounter'
sx={{
+56 -44
View File
@@ -7,30 +7,31 @@ import CallEndIcon from '@mui/icons-material/CallEnd';
import CallIcon from '@mui/icons-material/Call';
import MicIcon from '@mui/icons-material/Mic';
import MicNoneIcon from '@mui/icons-material/MicNone';
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
import { useChatLLMDropdown } from '../chat/components/layout-bar/useLLMDropdown';
import { SystemPurposeId, SystemPurposes } from '../../data';
import { aixChatGenerateContent_DMessage_FromConversation, AixChatGenerateContent_DMessageGuts } from '~/modules/aix/client/aix.client';
import { speakText } from '~/modules/speex/speex.client';
import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client';
import { AixChatGenerateContent_DMessage, aixChatGenerateContent_DMessage_FromConversation } from '~/modules/aix/client/aix.client';
import { useElevenLabsVoiceDropdown } from '~/modules/elevenlabs/useElevenLabsVoiceDropdown';
import type { OptimaBarControlMethods } from '~/common/layout/optima/bar/OptimaBarDropdown';
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
import { Link } from '~/common/components/Link';
import { OptimaPanelGroupedList } from '~/common/layout/optima/panel/OptimaPanelGroupedList';
import { OptimaPanelIn, OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
import { OptimaPanelGroup } from '~/common/layout/optima/panel/OptimaPanelGroup';
import { OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { SpeechResult, useSpeechRecognition } from '~/common/components/speechrecognition/useSpeechRecognition';
import { conversationTitle, remapMessagesSysToUsr } from '~/common/stores/chat/chat.conversation';
import { createDMessageFromFragments, createDMessageTextContent, DMessage, messageFragmentsReduceText, messageWasInterruptedAtStart } from '~/common/stores/chat/chat.message';
import { createDMessageFromFragments, createDMessageTextContent, DMessage, messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
import { createErrorContentFragment } from '~/common/stores/chat/chat.fragments';
import { launchAppChat, navigateToIndex } from '~/common/app.routes';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts';
import { usePlayUrl } from '~/common/util/audio/usePlayUrl';
import { useSetOptimaAppMenu } from '~/common/layout/optima/useOptima';
import type { AppCallIntent } from './AppCall';
import { CallAvatar } from './components/CallAvatar';
@@ -40,17 +41,22 @@ import { CallStatus } from './components/CallStatus';
import { useAppCallStore } from './state/store-app-call';
function CallMenu(props: {
function CallMenuItems(props: {
pushToTalk: boolean,
setPushToTalk: (pushToTalk: boolean) => void,
override: boolean,
setOverride: (overridePersonaVoice: boolean) => void,
}) {
// external state
const { grayUI, toggleGrayUI } = useAppCallStore();
const { voicesDropdown } = useElevenLabsVoiceDropdown(false, !props.override);
const handlePushToTalkToggle = () => props.setPushToTalk(!props.pushToTalk);
return <OptimaPanelGroupedList title='Call'>
const handleChangeVoiceToggle = () => props.setOverride(!props.override);
return <OptimaPanelGroup title='Call'>
<MenuItem onClick={handlePushToTalkToggle}>
<ListItemDecorator>{props.pushToTalk ? <MicNoneIcon /> : <MicIcon />}</ListItemDecorator>
@@ -58,6 +64,17 @@ function CallMenu(props: {
<Switch checked={props.pushToTalk} onChange={handlePushToTalkToggle} sx={{ ml: 'auto' }} />
</MenuItem>
<MenuItem onClick={handleChangeVoiceToggle}>
<ListItemDecorator><RecordVoiceOverTwoToneIcon /></ListItemDecorator>
Change Voice
<Switch checked={props.override} onChange={handleChangeVoiceToggle} sx={{ ml: 'auto' }} />
</MenuItem>
<MenuItem>
<ListItemDecorator>{' '}</ListItemDecorator>
{voicesDropdown}
</MenuItem>
<ListDivider />
<MenuItem onClick={toggleGrayUI}>
@@ -69,7 +86,7 @@ function CallMenu(props: {
Voice Calls Feedback
</MenuItem>
</OptimaPanelGroupedList>;
</OptimaPanelGroup>;
}
@@ -82,6 +99,7 @@ export function Telephone(props: {
const [avatarClickCount, setAvatarClickCount] = React.useState<number>(0);// const [micMuted, setMicMuted] = React.useState(false);
const [callElapsedTime, setCallElapsedTime] = React.useState<string>('00:00');
const [callMessages, setCallMessages] = React.useState<DMessage[]>([]);
const [overridePersonaVoice, setOverridePersonaVoice] = React.useState<boolean>(false);
const [personaTextInterim, setPersonaTextInterim] = React.useState<string | null>(null);
const [pushToTalk, setPushToTalk] = React.useState(true);
const [stage, setStage] = React.useState<'ring' | 'declined' | 'connected' | 'ended'>('ring');
@@ -89,7 +107,7 @@ export function Telephone(props: {
const responseAbortController = React.useRef<AbortController | null>(null);
// external state
const { chatLLMId: modelId, chatLLMDropdown: modelDropdown } = useChatLLMDropdown(llmDropdownRef);
const { chatLLMId, chatLLMDropdown } = useChatLLMDropdown(llmDropdownRef);
const { chatTitle, reMessages } = useChatStore(useShallow(state => {
const conversation = props.callIntent.conversationId
? state.conversations.find(conversation => conversation.id === props.callIntent.conversationId) ?? null
@@ -101,7 +119,7 @@ export function Telephone(props: {
}));
const persona = SystemPurposes[props.callIntent.personaId as SystemPurposeId] ?? undefined;
const personaCallStarters = persona?.call?.starters ?? undefined;
// const personaVoiceSelector = React.useMemo(() => personaGetVoiceSelector(persona), [persona]);
const personaVoiceId = overridePersonaVoice ? undefined : (persona?.voices?.elevenLabs?.voiceId ?? undefined);
const personaSystemMessage = persona?.systemMessage ?? undefined;
// hooks and speech
@@ -148,6 +166,7 @@ export function Telephone(props: {
};
// [E] pickup -> seed message and call timer
// FIXME: Overriding the voice will reset the call - not a desired behavior
React.useEffect(() => {
if (!isConnected) return;
@@ -167,14 +186,11 @@ export function Telephone(props: {
setCallMessages([createDMessageTextContent('assistant', firstMessage)]); // [state] set assistant:hello message
// fire/forget - use 'fast' priority for real-time conversation
void speakText(firstMessage,
undefined,
{ label: 'Call', priority: 'fast' },
);
// fire/forget
void elevenLabsSpeakText(firstMessage, personaVoiceId, true, true);
return () => clearInterval(interval);
}, [isConnected, personaCallStarters]);
}, [isConnected, personaCallStarters, personaVoiceId]);
// [E] persona streaming response - upon new user message
React.useEffect(() => {
@@ -210,7 +226,7 @@ export function Telephone(props: {
}
// bail if no llm selected
if (!modelId) return;
if (!chatLLMId) return;
// Call Message Generation Prompt
@@ -233,40 +249,33 @@ export function Telephone(props: {
setPersonaTextInterim('💭...');
aixChatGenerateContent_DMessage_FromConversation(
modelId,
chatLLMId,
callSystemInstruction,
callGenerationInputHistory,
'call',
callMessages[0].id,
{ abortSignal: responseAbortController.current.signal },
(update: AixChatGenerateContent_DMessageGuts, _isDone: boolean) => {
(update: AixChatGenerateContent_DMessage, _isDone: boolean) => {
const updatedText = messageFragmentsReduceText(update.fragments).trim();
if (updatedText)
setPersonaTextInterim(finalText = updatedText);
},
).then((status) => {
// don't add the message to conversation if it was interrupted with no content
if (messageWasInterruptedAtStart(status.lastDMessage))
return;
// whether status.outcome === 'success' or not, we get a valid DMessage, eventually with Error Fragments inside
const fullMessage = createDMessageFromFragments('assistant', status.lastDMessage.fragments);
fullMessage.generator = status.lastDMessage.generator;
setCallMessages(messages => [...messages, fullMessage]); // [state] append assistant:call_response
// fire/forget - use 'fast' priority for real-time conversation
// fire/forget
if (status.outcome === 'success' && finalText?.length >= 1)
void speakText(finalText,
undefined,
{ label: 'Call', priority: 'fast' },
);
void elevenLabsSpeakText(finalText, personaVoiceId, true, true);
}).catch((err: DOMException) => {
if (err?.name !== 'AbortError') {
// create an error message to explain the exception
const errorMessage = createDMessageFromFragments('assistant', [createErrorContentFragment(err.message || err.toString())]);
setCallMessages(messages => [...messages, errorMessage]); // [state] append assistant:call_response-ERROR
const errorMesage = createDMessageFromFragments('assistant', [createErrorContentFragment(err.message || err.toString())]);
setCallMessages(messages => [...messages, errorMesage]); // [state] append assistant:call_response-ERROR
}
}).finally(() => {
setPersonaTextInterim(null);
@@ -276,7 +285,7 @@ export function Telephone(props: {
responseAbortController.current?.abort();
responseAbortController.current = null;
};
}, [callMessages, isConnected, modelId, personaSystemMessage, reMessages]);
}, [isConnected, callMessages, chatLLMId, personaVoiceId, personaSystemMessage, reMessages]);
// [E] Message interrupter
const abortTrigger = isConnected && recognitionState.hasSpeech;
@@ -302,19 +311,22 @@ export function Telephone(props: {
const isMicEnabled = recognitionState.isAvailable;
const isTTSEnabled = true;
const isEnabled = isMicEnabled && isTTSEnabled;
const micErrorMessage = recognitionState.errorMessage;
// pluggable UI
const menuItems = React.useMemo(() =>
<CallMenuItems
pushToTalk={pushToTalk} setPushToTalk={setPushToTalk}
override={overridePersonaVoice} setOverride={setOverridePersonaVoice} />
, [overridePersonaVoice, pushToTalk],
);
useSetOptimaAppMenu(menuItems, 'CallUI-Call');
return <>
{/* -> Toolbar */}
<OptimaToolbarIn>{modelDropdown}</OptimaToolbarIn>
{/* -> Panel */}
<OptimaPanelIn>
<CallMenu
pushToTalk={pushToTalk} setPushToTalk={setPushToTalk}
/>
</OptimaPanelIn>
<OptimaToolbarIn>{chatLLMDropdown}</OptimaToolbarIn>
<Typography
level='h1'
@@ -338,7 +350,7 @@ export function Telephone(props: {
callerName={isConnected ? undefined : personaName}
statusText={isRinging ? '' /*'is calling you'*/ : isDeclined ? 'call declined' : isEnded ? 'call ended' : callElapsedTime}
regardingText={chatTitle}
micError={!isMicEnabled} micErrorMessage={micErrorMessage} speakError={!isTTSEnabled}
micError={!isMicEnabled} speakError={!isTTSEnabled}
/>
{/* Live Transcript, w/ streaming messages, audio indication, etc. */}
+2 -2
View File
@@ -16,7 +16,7 @@ export function CallStatus(props: {
callerName?: string,
statusText: string,
regardingText: string | null,
micError: boolean, micErrorMessage: string | null, speakError: boolean,
micError: boolean, speakError: boolean,
// llmComponent?: React.JSX.Element,
}) {
return (
@@ -37,7 +37,7 @@ export function CallStatus(props: {
</Typography>}
{props.micError && <InlineError
severity='danger' error={props.micErrorMessage || 'Looks like this Browser may not support speech recognition. You can try Chrome on Windows or Android instead.'} />}
severity='danger' error='Looks like this Browser may not support speech recognition. You can try Chrome on Windows or Android instead.' />}
{props.speakError && <InlineError
severity='danger' error='Text-to-speech does not appear to be configured. Please set it up in Preferences > Voice.' />}
+88 -159
View File
@@ -2,14 +2,15 @@ import * as React from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, useTheme } from '@mui/joy';
import { useTheme } from '@mui/joy';
import { DEV_MODE_SETTINGS } from '../settings-modal/UxLabsSettings';
import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
import type { TradeConfig } from '~/modules/trade/TradeModal';
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
import { downloadSingleChat, importConversationsFromFilesAtRest, openConversationsAtRestPicker } from '~/modules/trade/trade.client';
import { imaginePromptFromTextOrThrow } from '~/modules/aifn/imagine/imaginePromptFromText';
import { elevenLabsSpeakText } from '~/modules/elevenlabs/elevenlabs.client';
import { useAreBeamsOpen } from '~/modules/beam/store-beam.hooks';
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
@@ -17,10 +18,9 @@ import type { DConversation, DConversationId } from '~/common/stores/chat/chat.c
import type { OptimaBarControlMethods } from '~/common/layout/optima/bar/OptimaBarDropdown';
import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal';
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/PanelResizeInset';
import { LLM_IF_ANT_PromptCaching, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
import { OptimaDrawerIn, OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
import { Release } from '~/common/app.release';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
@@ -28,31 +28,28 @@ import { ShortcutKey, useGlobalShortcuts } from '~/common/components/shortcuts/u
import { WorkspaceIdProvider } from '~/common/stores/workspace/WorkspaceIdProvider';
import { addSnackbar, removeSnackbar } from '~/common/components/snackbar/useSnackbarsStore';
import { createDMessageFromFragments, createDMessagePlaceholderIncomplete, DMessageMetadata, duplicateDMessageMetadata } from '~/common/stores/chat/chat.message';
import { createErrorContentFragment, createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragments } from '~/common/stores/chat/chat.fragments';
import { createErrorContentFragment, createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragmentsNoVoid } from '~/common/stores/chat/chat.fragments';
import { gcChatImageAssets } from '~/common/stores/chat/chat.gc';
import { getChatLLMId } from '~/common/stores/llms/store-llms';
import { getConversation, getConversationSystemPurposeId, useConversation } from '~/common/stores/chat/store-chats';
import { optimaActions, optimaOpenModels, optimaOpenPreferences } from '~/common/layout/optima/useOptima';
import { optimaActions, optimaOpenModels, optimaOpenPreferences, useSetOptimaAppMenu } from '~/common/layout/optima/useOptima';
import { themeBgAppChatComposer } from '~/common/app.theme';
import { useChatLLM } from '~/common/stores/llms/llms.hooks';
import { useFolderStore } from '~/common/stores/folders/store-chat-folders';
import { useIsMobile, useIsTallScreen } from '~/common/components/useMatchMedia';
import { useLLM } from '~/common/stores/llms/llms.hooks';
import { useModelDomain } from '~/common/stores/llms/hooks/useModelDomain';
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
import { useRouterQuery } from '~/common/app.routes';
import { useUIComplexityIsMinimal } from '~/common/stores/store-ui';
import { useUXLabsStore } from '~/common/stores/store-ux-labs';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { ChatPane } from './components/layout-pane/ChatPane';
import { ChatBarBeam } from './components/layout-bar/ChatBarBeam';
import { ChatBarAltBeam } from './components/layout-bar/ChatBarAltBeam';
import { ChatBarAltTitle } from './components/layout-bar/ChatBarAltTitle';
import { ChatBarChat } from './components/layout-bar/ChatBarChat';
import { ChatBarDropdowns } from './components/layout-bar/ChatBarDropdowns';
import { ChatBeamWrapper } from './components/ChatBeamWrapper';
import { ChatDrawerMemo } from './components/layout-drawer/ChatDrawer';
import { ChatMessageList } from './components/ChatMessageList';
import { Composer } from './components/composer/Composer';
import { PaneTitleOverlay } from './components/PaneTitleOverlay';
import { useComposerAutoHide } from './components/composer/useComposerAutoHide';
import { usePanesManager } from './components/panes/store-panes-manager';
import { usePanesManager } from './components/panes/usePanesManager';
import type { ChatExecuteMode } from './execute-mode/execute-mode.types';
@@ -77,52 +74,24 @@ const chatMessageListSx: SxProps = {
flexGrow: 1,
};
/*const chatMessageListBrandedSx: SxProps = {
flexGrow: 1,
backgroundBlendMode: 'soft-light',
backgroundColor: themeBgApp,
backgroundImage: 'url(https://...)',
backgroundPosition: 'center',
backgroundRepeat: 'no-repeat',
backgroundSize: 'contain',
} as const;*/
const chatBeamWrapperSx: SxProps = {
flexGrow: 1,
// we added these after removing the minSize={20} (%) from the containing panel.
minWidth: '18rem',
// minHeight: 'calc(100vh - 69px - var(--AGI-Nav-width))',
};
const composerOpenSx: SxProps = {
// NOTE: disabled on 2025-03-05: conflicts with the GlobalDragOverlay's
// zIndex: 21, // just to allocate a surface, and potentially have a shadow
zIndex: 21, // just to allocate a surface, and potentially have a shadow
minWidth: { md: 480 }, // don't get compresses too much on desktop
// backgroundColor: themeBgAppChatComposer, // inlined in the Composer
transition: 'background-color 0.5s ease-out',
backgroundColor: themeBgAppChatComposer,
borderTop: `1px solid`,
borderTopColor: 'rgba(var(--joy-palette-neutral-mainChannel, 99 107 116) / 0.4)',
// hack: eats the bottom of the last message (as it has a 1px divider)
// NOTE: commented on 2024-05-13, as other content was stepping on the border due to it and missing zIndex
// mt: '-1px',
} as const;
mt: '-1px',
};
const composerOpenMobileSx: SxProps = {
zIndex: 21, // allocates the surface, possibly enables shadow if we like
py: 0.5, // have some breathing room
// boxShadow: '0px -1px 8px -2px rgba(0, 0, 0, 0.4)',
...composerOpenSx,
} as const;
// const composerClosedSx: SxProps = {
// display: 'none',
// };
// Lazy-loaded Modals
const DiagramsModalLazy = React.lazy(() => import('~/modules/aifn/digrams/DiagramsModal').then(module => ({ default: module.DiagramsModal })));
const FlattenerModalLazy = React.lazy(() => import('~/modules/aifn/flatten/FlattenerModal').then(module => ({ default: module.FlattenerModal })));
const TradeModalLazy = React.lazy(() => import('~/modules/trade/TradeModal').then(module => ({ default: module.TradeModal })));
const composerClosedSx: SxProps = {
display: 'none',
};
export function AppChat() {
@@ -142,25 +111,21 @@ export function AppChat() {
// external state
const theme = useTheme();
const [composerHasContent, setComposerHasContent] = React.useState(false);
const isMobile = useIsMobile();
const isTallScreen = useIsTallScreen();
const isZenMode = useUIComplexityIsMinimal();
const intent = useRouterQuery<Partial<AppChatIntent>>();
const showAltTitleBar = useUXLabsStore(state => DEV_MODE_SETTINGS && state.labsChatBarAlt === 'title');
const { domainModelId: chatLLMId } = useModelDomain('primaryChat');
const chatLLM = useLLM(chatLLMId) ?? null;
const { chatLLM } = useChatLLM();
const {
// state
chatPanes,
focusedPaneConversationId, // <-- key
focusedPaneIndex,
focusedPaneConversationId,
// actions
navigateHistoryInFocusedPane,
openConversationInFocusedPane,
@@ -182,10 +147,10 @@ export function AppChat() {
}, [chatPanes]);
const beamsOpens = useAreBeamsOpen(paneBeamStores);
const beamOpenStoreInFocusedPane = focusedPaneIndex === null ? null
: !beamsOpens?.[focusedPaneIndex] ? null
: paneBeamStores?.[focusedPaneIndex] ?? null;
const focusedChatBeamOpen = focusedPaneIndex !== null && !!beamsOpens?.[focusedPaneIndex];
const beamOpenStoreInFocusedPane = React.useMemo(() => {
const open = focusedPaneIndex !== null ? (beamsOpens?.[focusedPaneIndex] ?? false) : false;
return open ? paneBeamStores?.[focusedPaneIndex!] ?? null : null;
}, [beamsOpens, focusedPaneIndex, paneBeamStores]);
const {
// focused
@@ -206,7 +171,7 @@ export function AppChat() {
// const focusedConversationWorkspaceId = workspaceForConversationIdentity(focusedPaneConversationId);
//// const focusedConversationWorkspace = useWorkspaceIdForConversation(focusedPaneConversationId);
const { mayWork: capabilityHasT2I, mayEdit: capabilityHasT2IEdit } = useCapabilityTextToImage();
const { mayWork: capabilityHasT2I } = useCapabilityTextToImage();
const activeFolderId = useFolderStore(({ enableFolders, folders }) => {
const activeFolderId = enableFolders ? _activeFolderId : null;
@@ -214,9 +179,6 @@ export function AppChat() {
return activeFolder?.id ?? null;
});
// Composer Auto-hiding
const forceComposerHide = !!beamOpenStoreInFocusedPane /* || !focusedPaneConversationId */; // auto-hide when no chat (the 'please select a conversation...' state) doesn't feel good
const composerAutoHide = useComposerAutoHide(forceComposerHide, composerHasContent);
// Window actions
@@ -249,7 +211,7 @@ export function AppChat() {
else if (outcome === 'err-t2i-unconfigured')
optimaOpenPreferences('draw');
else if (outcome === 'err-no-persona')
addSnackbar({ key: 'chat-no-persona', message: 'No persona selected.', type: 'issue', overrides: { autoHideDuration: 4000 } });
addSnackbar({ key: 'chat-no-persona', message: 'No persona selected.', type: 'issue' });
else if (outcome === 'err-no-conversation')
addSnackbar({ key: 'chat-no-conversation', message: 'No active conversation.', type: 'issue' });
else if (outcome === 'err-no-last-message')
@@ -275,7 +237,7 @@ export function AppChat() {
// create the user:message
// NOTE: this can lead to multiple chat messages with data refs that are referring to the same dblobs,
// however, we already got transferred ownership of the dblobs at this point.
const userMessage = createDMessageFromFragments('user', duplicateDMessageFragments(fragments, true)); // [chat] create user:message to send per-chat
const userMessage = createDMessageFromFragments('user', duplicateDMessageFragmentsNoVoid(fragments)); // [chat] create user:message to send per-chat
if (metadata) userMessage.metadata = duplicateDMessageMetadata(metadata);
ConversationsManager.getHandler(conversation.id).messageAppend(userMessage); // [chat] append user message in each conversation
@@ -345,6 +307,11 @@ export function AppChat() {
});
}, [handleExecuteAndOutcome]);
const handleTextSpeak = React.useCallback(async (text: string): Promise<void> => {
await elevenLabsSpeakText(text, undefined, true, true);
}, []);
// Chat actions
const handleConversationNewInFocusedPane = React.useCallback((forceNoRecycle: boolean, isIncognito: boolean) => {
@@ -362,10 +329,9 @@ export function AppChat() {
useFolderStore.getState().addConversationToFolder(activeFolderId, conversationId);
// focus the composer
if (!isMobile)
composerTextAreaRef.current?.focus();
composerTextAreaRef.current?.focus();
}, [activeFolderId, focusedPaneConversationId, handleOpenConversationInFocusedPane, isMobile, prependNewConversation, recycleNewConversationId]);
}, [activeFolderId, focusedPaneConversationId, handleOpenConversationInFocusedPane, prependNewConversation, recycleNewConversationId]);
const handleConversationImportDialog = React.useCallback(() => setTradeConfig({ dir: 'import' }), []);
@@ -466,15 +432,15 @@ export function AppChat() {
const barAltTitle = showAltTitleBar ? focusedChatTitle ?? 'No Chat' : null;
const focusedBarContent = React.useMemo(() => beamOpenStoreInFocusedPane
? <ChatBarBeam conversationTitle={focusedChatTitle ?? 'No Chat'} beamStore={beamOpenStoreInFocusedPane} isMobile={isMobile} />
? <ChatBarAltBeam beamStore={beamOpenStoreInFocusedPane} isMobile={isMobile} />
: (barAltTitle === null)
? <ChatBarChat conversationId={focusedPaneConversationId} llmDropdownRef={llmDropdownRef} personaDropdownRef={personaDropdownRef} />
? <ChatBarDropdowns conversationId={focusedPaneConversationId} llmDropdownRef={llmDropdownRef} personaDropdownRef={personaDropdownRef} />
: <ChatBarAltTitle conversationId={focusedPaneConversationId} conversationTitle={barAltTitle} />
, [barAltTitle, beamOpenStoreInFocusedPane, focusedChatTitle, focusedPaneConversationId, isMobile],
, [barAltTitle, beamOpenStoreInFocusedPane, focusedPaneConversationId, isMobile],
);
// Disabled by default, as it lags the opening of the drawer and immediately vanishes during the closing animation
// Disabled by default, as it lags the opening of the drawer and immediatly vanishes during the closing animation
const isDrawerOpen = true; // useOptimaDrawerOpen();
const drawerContent = React.useMemo(() => !isDrawerOpen ? null :
@@ -484,7 +450,6 @@ export function AppChat() {
activeFolderId={activeFolderId}
chatPanesConversationIds={paneUniqueConversationIds}
disableNewButton={disableNewButton}
focusedChatBeamOpen={focusedChatBeamOpen}
onConversationActivate={handleOpenConversationInFocusedPane}
onConversationBranch={handleConversationBranch}
onConversationNew={handleConversationNewInFocusedPane}
@@ -493,10 +458,10 @@ export function AppChat() {
onConversationsImportDialog={handleConversationImportDialog}
setActiveFolderId={setActiveFolderId}
/>,
[activeFolderId, disableNewButton, focusedChatBeamOpen, focusedPaneConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNewInFocusedPane, handleDeleteConversations, handleOpenConversationInFocusedPane, isDrawerOpen, paneUniqueConversationIds],
[activeFolderId, disableNewButton, focusedPaneConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNewInFocusedPane, handleDeleteConversations, handleOpenConversationInFocusedPane, isDrawerOpen, paneUniqueConversationIds],
);
const focusedChatPanelContent = React.useMemo(() => !focusedPaneConversationId ? null :
const focusedMenuItems = React.useMemo(() =>
<ChatPane
conversationId={focusedPaneConversationId}
disableItems={!focusedPaneConversationId || isFocusedChatEmpty}
@@ -512,6 +477,8 @@ export function AppChat() {
[focusedPaneConversationId, handleConversationBranch, handleConversationFlatten, handleConversationReset, hasConversations, isFocusedChatEmpty, isMessageSelectionMode, isMobile, isTallScreen],
);
useSetOptimaAppMenu(focusedMenuItems, 'AppChat');
// Effects
@@ -519,7 +486,7 @@ export function AppChat() {
React.useEffect(() => {
// Debug: open a null chat
if (Release.IsNodeDevBuild && intent.initialConversationId === 'null')
openConversationInFocusedPane(null! /* for debugging purpose */);
openConversationInFocusedPane(null! /* for debugging purporse */);
// Open the initial conversation if set
else if (intent.initialConversationId)
openConversationInFocusedPane(intent.initialConversationId);
@@ -611,11 +578,8 @@ export function AppChat() {
return <>
{/* -> Toolbar, -> Drawer, -> Panel*/}
<OptimaToolbarIn>{focusedBarContent}</OptimaToolbarIn>
<OptimaDrawerIn>{drawerContent}</OptimaDrawerIn>
<OptimaPanelIn>{focusedChatPanelContent}</OptimaPanelIn>
<OptimaToolbarIn>{focusedBarContent}</OptimaToolbarIn>
<PanelGroup
direction={(isMobile || isTallScreen) ? 'vertical' : 'horizontal'}
@@ -632,22 +596,20 @@ export function AppChat() {
const _panesCount = chatPanes.length;
const _keyAndId = `chat-pane-${pane.paneId}`;
const _sepId = `sep-pane-${idx}`;
return <WorkspaceIdProvider conversationId={_paneIsFocused ? _paneConversationId : null} key={_keyAndId}><ErrorBoundary>
return <WorkspaceIdProvider conversationId={_paneIsFocused ? _paneConversationId : null} key={_keyAndId}>
<Panel
id={_keyAndId}
order={idx}
collapsible={chatPanes.length === 2}
defaultSize={(_panesCount === 3 && idx === 1) ? 34 : Math.round(100 / _panesCount)}
// minSize={20 /* IMPORTANT: this forces a reflow even on a simple on hover */}
minSize={20}
onClick={(event) => {
// Alt + Click: undocumented feature to clear focus
if (event.altKey && chatPanes.length > 1)
return setFocusedPaneIndex(-1);
setFocusedPaneIndex(idx);
const setFocus = chatPanes.length < 2 || !event.altKey;
setFocusedPaneIndex(setFocus ? idx : -1);
}}
onCollapse={() => {
// NOTE: despite the delay to try to let the dragging settle, there seems to be an issue with the Pane locking the screen
// NOTE: despite the delay to try to let the draggin 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);
@@ -656,45 +618,28 @@ export function AppChat() {
// for anchoring the scroll button in place
position: 'relative',
...(isMultiPane ? {
marginBottom: '1px', // compensates for the -1px in `composerOpenSx` for the Composer offset
borderRadius: '0.375rem',
borderStyle: 'solid',
borderColor: _paneIsFocused
border: `2px solid ${_paneIsFocused
? ((willMulticast || !isMultiConversationId) ? theme.palette.primary.solidBg : theme.palette.primary.solidBg)
: ((willMulticast || !isMultiConversationId) ? theme.palette.primary.softActiveBg : theme.palette.divider),
borderWidth: '2px',
// borderBottomWidth: '3px',
: ((willMulticast || !isMultiConversationId) ? theme.palette.primary.softActiveBg : theme.palette.background.level1)}`,
// DISABLED on 2024-03-13, it gets in the way quite a lot
// filter: (!willMulticast && !_paneIsFocused)
// ? (!isMultiConversationId ? 'grayscale(66.67%)' /* clone of the same */ : 'grayscale(66.67%)')
// : undefined,
// 2025-02-27: didn't try, here's another version
// filter: _paneIsFocused ? 'none' : 'brightness(0.94) saturate(0.9)',
} : {
// 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 Drag state not being reset properly, however the pointerEvents has been set since 0.0.56 while
// The root cause seems to be a Dragstate 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',
}),
...((_paneIsIncognito && {
backgroundColor: theme.palette.background.level3,
backgroundImage: 'repeating-linear-gradient(45deg, rgba(0,0,0,0.03), rgba(0,0,0,0.03) 10px, transparent 10px, transparent 20px)',
})),
}}
>
{isMultiPane && !isZenMode && (
<PaneTitleOverlay
paneIdx={idx}
conversationId={_paneConversationId}
isFocused={_paneIsFocused}
isIncognito={_paneIsIncognito}
onConversationDelete={handleDeleteConversations}
/>
)}
<ScrollToBottom
bootToBottom
stickToBottomInitial
@@ -708,7 +653,7 @@ export function AppChat() {
conversationHandler={_paneChatHandler}
capabilityHasT2I={capabilityHasT2I}
chatLLMAntPromptCaching={chatLLM?.interfaces?.includes(LLM_IF_ANT_PromptCaching) ?? false}
chatLLMContextTokens={getLLMContextTokens(chatLLM) ?? null}
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
chatLLMSupportsImages={chatLLM?.interfaces?.includes(LLM_IF_OAI_Vision) ?? false}
fitScreen={isMobile || isMultiPane}
isMobile={isMobile}
@@ -719,6 +664,7 @@ export function AppChat() {
onConversationNew={handleConversationNewInFocusedPane}
onTextDiagram={handleTextDiagram}
onTextImagine={handleImagineFromText}
onTextSpeak={handleTextSpeak}
sx={chatMessageListSx}
/>
)}
@@ -745,67 +691,50 @@ export function AppChat() {
</PanelResizeHandle>
)}
</ErrorBoundary></WorkspaceIdProvider>;
</WorkspaceIdProvider>;
})}
</PanelGroup>
{/* Composer with auto-hide */}
<Box {...composerAutoHide.compressorProps}>
<div style={composerAutoHide.compressibleStyle}>
<Composer
isMobile={isMobile}
chatLLM={chatLLM}
composerTextAreaRef={composerTextAreaRef}
targetConversationId={focusedPaneConversationId}
capabilityHasT2I={capabilityHasT2I}
capabilityHasT2IEdit={capabilityHasT2IEdit}
isMulticast={!isMultiConversationId ? null : isComposerMulticast}
isDeveloperMode={isFocusedChatDeveloper}
onAction={handleComposerAction}
onConversationBeamEdit={handleMessageBeamLastInFocusedPane}
onConversationsImportFromFiles={handleConversationsImportFromFiles}
onTextImagine={handleImagineFromText}
setIsMulticast={setIsComposerMulticast}
onComposerHasContent={setComposerHasContent}
sx={isMobile ? composerOpenMobileSx : composerOpenSx}
/>
</div>
</Box>
{/* Hover zone for auto-hide */}
{!forceComposerHide && composerAutoHide.isHidden && <Box {...composerAutoHide.detectorProps} />}
<Composer
isMobile={isMobile}
chatLLM={chatLLM}
composerTextAreaRef={composerTextAreaRef}
targetConversationId={focusedPaneConversationId}
capabilityHasT2I={capabilityHasT2I}
isMulticast={!isMultiConversationId ? null : isComposerMulticast}
isDeveloperMode={isFocusedChatDeveloper}
onAction={handleComposerAction}
onConversationsImportFromFiles={handleConversationsImportFromFiles}
onTextImagine={handleImagineFromText}
setIsMulticast={setIsComposerMulticast}
sx={beamOpenStoreInFocusedPane ? composerClosedSx : composerOpenSx}
/>
{/* Diagrams */}
{!!diagramConfig && (
<React.Suspense fallback={null}>
<DiagramsModalLazy
config={diagramConfig}
onClose={() => setDiagramConfig(null)}
/>
</React.Suspense>
<DiagramsModal
config={diagramConfig}
onClose={() => setDiagramConfig(null)}
/>
)}
{/* Flatten */}
{!!flattenConversationId && (
<React.Suspense fallback={null}>
<FlattenerModalLazy
conversationId={flattenConversationId}
onConversationBranch={handleConversationBranch}
onClose={() => setFlattenConversationId(null)}
/>
</React.Suspense>
<FlattenerModal
conversationId={flattenConversationId}
onConversationBranch={handleConversationBranch}
onClose={() => setFlattenConversationId(null)}
/>
)}
{/* Import / Export */}
{!!tradeConfig && (
<React.Suspense fallback={null}>
<TradeModalLazy
config={tradeConfig}
onConversationActivate={handleOpenConversationInFocusedPane}
onClose={() => setTradeConfig(null)}
/>
</React.Suspense>
<TradeModal
config={tradeConfig}
onConversationActivate={handleOpenConversationInFocusedPane}
onClose={() => setTradeConfig(null)}
/>
)}
</>;
+12 -41
View File
@@ -1,41 +1,19 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, IconButton, Modal } from '@mui/joy';
import CloseFullscreenIcon from '@mui/icons-material/CloseFullscreen';
import { Box, Modal, ModalClose } from '@mui/joy';
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
import { BeamView } from '~/modules/beam/BeamView';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
import { themeZIndexBeamView } from '~/common/app.theme';
const beamWrapperStyles = {
wrapper: {
position: 'absolute',
inset: 0,
backgroundColor: 'background.level2', // darker than the expected Level1, for a change
} as const,
closeContainer: {
position: 'absolute',
top: '0.25rem',
// left: '0.25rem',
left: { xs: 'calc(50% - 3rem)', md: '50%' }, // center on desktop, a bit left (for the islands) on mobile
// transform: 'translate(-50%, 0)',
zIndex: themeZIndexBeamView, // stay on top of Message > Chips (:1), and Overlays (:2) - note: Desktop Drawer (:26)
} as const,
closeButton: {
// color: 'white',
// borderRadius: '25%',
boxShadow: 'md',
} as const,
} as const;
/*const overlaySx: SxProps = {
position: 'absolute',
inset: 0,
zIndex: themeZIndexBeamView, // stay on top of Message > Chips (:1), and Overlays (:2) - note: Desktop Drawer (:26)
}*/
export function ChatBeamWrapper(props: {
@@ -62,22 +40,15 @@ export function ChatBeamWrapper(props: {
return isMaximized ? (
<Modal open onClose={handleUnMaximize}>
<Box sx={beamWrapperStyles.wrapper}>
<Box sx={{
backgroundColor: 'background.level1',
position: 'absolute',
inset: 0,
}}>
<ScrollToBottom disableAutoStick>
{beamView}
</ScrollToBottom>
{/* Modal-Close-alike */}
<Box sx={beamWrapperStyles.closeContainer}>
<GoodTooltip title='Exit maximized mode'>
<IconButton variant='solid' onClick={handleUnMaximize} sx={beamWrapperStyles.closeButton}>
<CloseFullscreenIcon />
{/*<CloseRoundedIcon />*/}
</IconButton>
</GoodTooltip>
</Box>
<ModalClose sx={{ color: 'white', backgroundColor: 'background.surface', boxShadow: 'xs', mr: 2 }} />
</Box>
</Modal>
) : (
+43 -56
View File
@@ -7,17 +7,17 @@ import { Box, List } from '@mui/joy';
import type { SystemPurposeExample } from '../../../data';
import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
import { speakText } from '~/modules/speex/speex.client';
import type { ConversationHandler } from '~/common/chat-overlay/ConversationHandler';
import type { DLLMContextTokens } from '~/common/stores/llms/llms.types';
import { DConversationId, excludeSystemMessages } from '~/common/stores/chat/chat.conversation';
import { ShortcutKey, useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts';
import { convertFilesToDAttachmentFragments } from '~/common/attachment-drafts/attachment.pipeline';
import { createDMessageFromFragments, createDMessageTextContent, DMessage, DMessageId, DMessageUserFlag, DMetaReferenceItem, MESSAGE_FLAG_AIX_SKIP, messageHasUserFlag } from '~/common/stores/chat/chat.message';
import { createDMessageFromFragments, createDMessageTextContent, DMessage, DMessageId, DMessageUserFlag, DMetaReferenceItem, MESSAGE_FLAG_AIX_SKIP } from '~/common/stores/chat/chat.message';
import { createTextContentFragment, DMessageFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
import { openFileForAttaching } from '~/common/components/ButtonAttachFiles';
import { optimaOpenPreferences } from '~/common/layout/optima/useOptima';
import { useBrowserTranslationWarning } from '~/common/components/useIsBrowserTranslating';
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
import { useChatOverlayStore } from '~/common/chat-overlay/store-perchat_vanilla';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { useScrollToBottom } from '~/common/scroll-to-bottom/useScrollToBottom';
@@ -40,7 +40,7 @@ export function ChatMessageList(props: {
conversationHandler: ConversationHandler | null,
capabilityHasT2I: boolean,
chatLLMAntPromptCaching: boolean,
chatLLMContextTokens: DLLMContextTokens,
chatLLMContextTokens: number | null,
chatLLMSupportsImages: boolean,
fitScreen: boolean,
isMobile: boolean,
@@ -50,6 +50,7 @@ export function ChatMessageList(props: {
onConversationNew: (forceNoRecycle: boolean, isIncognito: boolean) => void,
onTextDiagram: (diagramConfig: DiagramConfig | null) => void,
onTextImagine: (conversationId: DConversationId, selectedText: string) => Promise<void>,
onTextSpeak: (selectedText: string) => Promise<void>,
setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
sx?: SxProps,
}) {
@@ -63,6 +64,7 @@ export function ChatMessageList(props: {
const { notifyBooting } = useScrollToBottom();
const danger_experimentalHtmlWebUi = useChatAutoSuggestHTMLUI();
const [showSystemMessages] = useChatShowSystemMessages();
const optionalTranslationWarning = useBrowserTranslationWarning();
const { conversationMessages, historyTokenCount } = useChatStore(useShallow(({ conversations }) => {
const conversation = conversations.find(conversation => conversation.id === props.conversationId);
return {
@@ -74,9 +76,10 @@ export function ChatMessageList(props: {
_composerInReferenceToCount: state.inReferenceTo?.length ?? 0,
ephemerals: state.ephemerals?.length ? state.ephemerals : null,
})));
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
// derived state
const { conversationHandler, conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine } = props;
const { conversationHandler, conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
const composerCanAddInReferenceTo = _composerInReferenceToCount < 5;
const composerHasInReferenceto = _composerInReferenceToCount > 0;
@@ -115,9 +118,9 @@ export function ChatMessageList(props: {
}
}, [conversationHandler, conversationId, onConversationExecuteHistory, props.chatLLMSupportsImages]);
const handleMessageContinue = React.useCallback(async (_messageId: DMessageId /* Ignored for now */, continueText: null | string) => {
const handleMessageContinue = React.useCallback(async (_messageId: DMessageId /* Ignored for now */) => {
if (conversationId && conversationHandler) {
conversationHandler.messageAppend(createDMessageTextContent('user', continueText || 'Continue')); // [chat] append user:Continue (or custom text, likely from an 'option')
conversationHandler.messageAppend(createDMessageTextContent('user', 'Continue')); // [chat] append user:Continue
await onConversationExecuteHistory(conversationId);
}
}, [conversationHandler, conversationId, onConversationExecuteHistory]);
@@ -134,8 +137,8 @@ export function ChatMessageList(props: {
const handleMessageBeam = React.useCallback(async (messageId: DMessageId) => {
// Message option menu Beam
if (!conversationId || !conversationHandler || !conversationHandler.isValid()) return;
const inputHistory = conversationHandler.historyViewHeadOrThrow('chat-beam-message');
if (!conversationId || !props.conversationHandler || !props.conversationHandler.isValid()) return;
const inputHistory = props.conversationHandler.historyViewHeadOrThrow('chat-beam-message');
if (!inputHistory.length) return;
// TODO: replace the Persona and Auto-Cache-hint in the history?
@@ -148,52 +151,52 @@ export function ChatMessageList(props: {
// assistant: do an in-place beam
if (lastTruncatedMessage.role === 'assistant') {
if (truncatedHistory.length >= 2)
conversationHandler.beamInvoke(truncatedHistory.slice(0, -1), [lastTruncatedMessage], lastTruncatedMessage.id);
props.conversationHandler.beamInvoke(truncatedHistory.slice(0, -1), [lastTruncatedMessage], lastTruncatedMessage.id);
} else if (lastTruncatedMessage.role === 'user') {
// user: truncate and append (but if the next message is an assistant message, import it)
const possibleNextMessage = inputHistory[truncatedHistory.length];
if (possibleNextMessage?.role === 'assistant')
conversationHandler.beamInvoke(truncatedHistory, [possibleNextMessage], null);
props.conversationHandler.beamInvoke(truncatedHistory, [possibleNextMessage], null);
else
conversationHandler.beamInvoke(truncatedHistory, [], null);
props.conversationHandler.beamInvoke(truncatedHistory, [], null);
}
}, [conversationHandler, conversationId]);
}, [conversationId, props.conversationHandler]);
const handleMessageBranch = React.useCallback((messageId: DMessageId) => {
conversationId && onConversationBranch(conversationId, messageId, true);
}, [conversationId, onConversationBranch]);
const handleMessageTruncate = React.useCallback((messageId: DMessageId) => {
conversationHandler?.historyTruncateTo(messageId, 0);
}, [conversationHandler]);
props.conversationHandler?.historyTruncateTo(messageId, 0);
}, [props.conversationHandler]);
const handleMessageDelete = React.useCallback((messageId: DMessageId) => {
conversationHandler?.messagesDelete([messageId]);
}, [conversationHandler]);
props.conversationHandler?.messagesDelete([messageId]);
}, [props.conversationHandler]);
const handleMessageAppendFragment = React.useCallback((messageId: DMessageId, fragment: DMessageFragment) => {
conversationHandler?.messageFragmentAppend(messageId, fragment, false, false);
}, [conversationHandler]);
props.conversationHandler?.messageFragmentAppend(messageId, fragment, false, false);
}, [props.conversationHandler]);
const handleMessageDeleteFragment = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId) => {
conversationHandler?.messageFragmentDelete(messageId, fragmentId, false, true);
}, [conversationHandler]);
props.conversationHandler?.messageFragmentDelete(messageId, fragmentId, false, true);
}, [props.conversationHandler]);
const handleMessageReplaceFragment = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => {
conversationHandler?.messageFragmentReplace(messageId, fragmentId, newFragment, true);
}, [conversationHandler]);
props.conversationHandler?.messageFragmentReplace(messageId, fragmentId, newFragment, false);
}, [props.conversationHandler]);
const handleMessageToggleUserFlag = React.useCallback((messageId: DMessageId, userFlag: DMessageUserFlag, _maxPerConversation?: number) => {
conversationHandler?.messageToggleUserFlag(messageId, userFlag, true /* touch */);
props.conversationHandler?.messageToggleUserFlag(messageId, userFlag, true /* touch */);
// Note: we don't support 'maxPerConversation' yet, which is supposed to turn off the flag from the beginning if it's too numerous
// if (_maxPerConversation) {
// ...
// }
}, [conversationHandler]);
}, [props.conversationHandler]);
const handleAddInReferenceTo = React.useCallback((item: DMetaReferenceItem) => {
conversationHandler?.overlayActions.addInReferenceTo(item);
}, [conversationHandler]);
props.conversationHandler?.overlayActions.addInReferenceTo(item);
}, [props.conversationHandler]);
const handleTextDiagram = React.useCallback(async (messageId: DMessageId, text: string) => {
conversationId && onTextDiagram({ conversationId: conversationId, messageId, text });
@@ -210,29 +213,16 @@ export function ChatMessageList(props: {
}, [capabilityHasT2I, conversationId, onTextImagine]);
const handleTextSpeak = React.useCallback(async (text: string) => {
// sandwich the speaking with the indicator
if (!isSpeakable)
return optimaOpenPreferences('voice');
setIsSpeaking(true);
const result = await speakText(text, undefined, { label: 'Chat speak' });
await onTextSpeak(text);
setIsSpeaking(false);
// open voice preferences
if (!result.success && (result.errorType === 'tts-no-engine' || result.errorType === 'tts-unconfigured'))
optimaOpenPreferences('voice');
}, []);
}, [isSpeakable, onTextSpeak]);
// operate on the local selection set
const areAllSelectedMessagesHidden = React.useMemo(() => {
if (selectedMessages.size === 0) return false;
for (const messageId of selectedMessages) {
const message = conversationMessages.find(m => m.id === messageId);
if (message && !messageHasUserFlag(message, MESSAGE_FLAG_AIX_SKIP))
return false;
}
return true;
}, [selectedMessages, conversationMessages]);
const handleSelectAll = (selected: boolean) => {
const newSelected = new Set<string>();
if (selected)
@@ -248,15 +238,15 @@ export function ChatMessageList(props: {
};
const handleSelectionDelete = React.useCallback(() => {
conversationHandler?.messagesDelete(Array.from(selectedMessages));
props.conversationHandler?.messagesDelete(Array.from(selectedMessages));
setSelectedMessages(new Set());
}, [conversationHandler, selectedMessages]);
}, [props.conversationHandler, selectedMessages]);
const handleSelectionToggleVisibility = React.useCallback(() => {
const handleSelectionHide = React.useCallback(() => {
for (let selectedMessage of Array.from(selectedMessages))
conversationHandler?.messageSetUserFlag(selectedMessage, MESSAGE_FLAG_AIX_SKIP, !areAllSelectedMessagesHidden, true);
props.conversationHandler?.messageSetUserFlag(selectedMessage, MESSAGE_FLAG_AIX_SKIP, true, true);
setSelectedMessages(new Set());
}, [conversationHandler, selectedMessages, areAllSelectedMessagesHidden]);
}, [props.conversationHandler, selectedMessages]);
const { isMessageSelectionMode, setIsMessageSelectionMode } = props;
@@ -292,10 +282,6 @@ export function ChatMessageList(props: {
p: 0,
...props.sx,
// we added these after removing the minSize={20} (%) from the containing panel.
minWidth: '18rem',
// minHeight: '180px', // not need for this, as it's already an overflow scrolling container, so one can reduce it to a pixel
// fix for the double-border on the last message (one by the composer, one to the bottom of the message)
// marginBottom: '-1px',
@@ -325,6 +311,8 @@ export function ChatMessageList(props: {
return (
<List role='chat-messages-list' sx={listSx}>
{optionalTranslationWarning}
{props.isMessageSelectionMode && (
<MessagesSelectionHeader
hasSelected={selectedMessages.size > 0}
@@ -332,8 +320,7 @@ export function ChatMessageList(props: {
onClose={() => props.setIsMessageSelectionMode(false)}
onSelectAll={handleSelectAll}
onDeleteMessages={handleSelectionDelete}
onToggleVisibility={handleSelectionToggleVisibility}
areAllMessagesHidden={areAllSelectedMessagesHidden}
onHideMessages={handleSelectionHide}
/>
)}
@@ -378,7 +365,7 @@ export function ChatMessageList(props: {
onMessageTruncate={handleMessageTruncate}
onTextDiagram={handleTextDiagram}
onTextImagine={capabilityHasT2I ? handleTextImagine : undefined}
onTextSpeak={handleTextSpeak}
onTextSpeak={isSpeakable ? handleTextSpeak : undefined}
/>
);
+1 -1
View File
@@ -13,7 +13,7 @@ import { ScaledTextBlockRenderer } from '~/modules/blocks/ScaledTextBlockRendere
import type { DEphemeral } from '~/common/chat-overlay/store-perchat-ephemerals_slice';
import { ConversationHandler } from '~/common/chat-overlay/ConversationHandler';
import { adjustContentScaling, ContentScaling, lineHeightChatTextMd } from '~/common/app.theme';
import { useUIPreferencesStore } from '~/common/stores/store-ui';
import { useUIPreferencesStore } from '~/common/state/store-ui';
// State Pane
@@ -1,194 +0,0 @@
import * as React from 'react';
import { Box, IconButton, Sheet } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import OpenInFullIcon from '@mui/icons-material/OpenInFull';
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
import { InlineTextarea } from '~/common/components/InlineTextarea';
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
import { useConversationTitle } from '~/common/stores/chat/hooks/useConversationTitle';
import { panesManagerActions } from './panes/store-panes-manager';
// configuration
const ENABLE_DELETE = false;
const _styles = {
tileBar: {
position: 'absolute',
top: 0,
left: '50%',
transform: 'translateX(-50%)',
zIndex: 10,
padding: '0 0.125rem 0.125rem',
fontSize: 'sm',
fontWeight: 'md',
borderBottomLeftRadius: '8px',
borderBottomRightRadius: '8px',
// boxShadow: 'xs',
// border: '1px solid',
// borderColor: 'background.popup',
borderTop: 'none',
maxWidth: '78%',
display: 'flex',
alignItems: 'center',
gap: 1,
} as const,
titleBarIncognito: {
backgroundImage: 'repeating-linear-gradient(45deg, rgba(0,0,0,0.1), rgba(0,0,0,0.1) 10px, transparent 10px, transparent 20px)',
backgroundColor: 'neutral.solidBg',
} as const,
title: {
flex: 1,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
cursor: 'pointer',
minWidth: '2.75rem',
textAlign: 'center',
} as const,
toolButton: {
'--IconButton-size': '1.5rem',
backgroundColor: 'transparent',
opacity: 0.5,
transition: 'opacity 0.1s',
'&:hover': {
opacity: 1,
},
} as const,
toolIcon: {} as const,
toolIconLg: {
fontSize: 'lg',
} as const,
} as const;
export function PaneTitleOverlay(props: {
paneIdx: number,
conversationId: DConversationId | null,
isFocused: boolean,
isIncognito: boolean,
onConversationDelete: (conversationIds: DConversationId[], bypassConfirmation: boolean) => void,
}) {
// state
const [editingTitle, setEditingTitle] = React.useState(false);
// external state
const { title, setUserTitle } = useConversationTitle(props.conversationId);
// if (!title || title?.length < 3)
// return null;
// close tabs handlers
const handleCloseThis = React.useCallback(() => {
panesManagerActions().removePane(props.paneIdx);
}, [props.paneIdx]);
const handleCloseOthers = React.useCallback(() => {
panesManagerActions().removeOtherPanes(props.paneIdx);
}, [props.paneIdx]);
// title handles
const handleTitleEditBegin = React.useCallback(() => {
setEditingTitle(true);
}, []);
const handleTitleEditChange = React.useCallback((newTitle: string) => {
setUserTitle(newTitle);
setEditingTitle(false);
}, [setUserTitle]);
const handleTitleEditEnd = React.useCallback(() => {
setEditingTitle(false);
}, []);
// delete handlers
const { onConversationDelete } = props;
const handleDeleteClicked = React.useCallback((event: React.MouseEvent) => {
event.stopPropagation();
if (props.conversationId)
onConversationDelete([props.conversationId], event.shiftKey);
}, [onConversationDelete, props.conversationId]);
// don't render if not focused
// if (!props.isFocused)
// return null;
const hasTitle = title && title.length > 0;
const color = props.isFocused ? 'primary' : 'neutral';
const variantO = props.isFocused ? 'solid' : 'outlined';
const variantP = props.isFocused ? 'solid' : 'plain';
return (
<Sheet
color={color}
variant={variantO}
sx={!props.isIncognito ? _styles.tileBar : { ..._styles.tileBar, ..._styles.titleBarIncognito }}
>
{/* Close Others*/}
{/*<TooltipOutlined title='Close Other Tabs'>*/}
{!editingTitle && <IconButton title='Close Other Tabs' size='sm' color={color} variant={variantP} onClick={handleCloseOthers} sx={_styles.toolButton}>
<OpenInFullIcon sx={_styles.toolIcon} />
</IconButton>}
{/*</TooltipOutlined>*/}
{/* Title */}
{editingTitle ? (
<InlineTextarea
initialText={title || ''}
placeholder='Chat title...'
invertedColors
centerText
onEdit={handleTitleEditChange}
onCancel={handleTitleEditEnd}
sx={{
// flexGrow: 1,
// minWidth: 120,
mx: { md: 1 },
}}
/>
) : !!props.conversationId && <>
{hasTitle && <Box sx={_styles.title} onClick={handleTitleEditBegin}>
{title}
</Box>}
{!hasTitle && <Box fontStyle='italic' onClick={handleTitleEditBegin}>
untitled
</Box>}
{!hasTitle && <TooltipOutlined title='Edit Chat Title'>
<IconButton title='' size='sm' color={color} variant={variantP} onClick={handleTitleEditBegin} sx={_styles.toolButton}>
<EditRoundedIcon sx={_styles.toolIcon} />
</IconButton>
</TooltipOutlined>}
</>}
{/* Delete This */}
{ENABLE_DELETE && hasTitle && !!props.conversationId && (
<TooltipOutlined title='Delete Chat (Shift+Click to bypass confirmation)'>
<IconButton size='sm' variant={variantP} onClick={handleDeleteClicked} sx={_styles.toolButton}>
<DeleteForeverIcon />
</IconButton>
</TooltipOutlined>
)}
{/* Close This */}
{/*<TooltipOutlined title='Close'>*/}
{!editingTitle && <IconButton title='Close Tab' size='sm' color={color} variant={variantP} onClick={handleCloseThis} sx={_styles.toolButton}>
<ClearIcon sx={_styles.toolIconLg} />
</IconButton>}
{/*</TooltipOutlined>*/}
</Sheet>
);
}
+88 -136
View File
@@ -1,17 +1,19 @@
import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { Box, IconButton, Typography } from '@mui/joy';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, IconButton, styled, Typography } from '@mui/joy';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import MinimizeIcon from '@mui/icons-material/Minimize';
// import { isMacUser } from '~/common/util/pwaUtils';
import { ShortcutKey, ShortcutObject } from '~/common/components/shortcuts/useGlobalShortcuts';
import type { ShortcutObject } from '~/common/components/shortcuts/useGlobalShortcuts';
import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { useGlobalShortcutsStore } from '~/common/components/shortcuts/store-global-shortcuts';
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
import { useUXLabsStore } from '~/common/stores/store-ux-labs';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
// configuration
@@ -25,92 +27,12 @@ const hideButtonTooltip = (
</Box>
);
const _styles = {
bar: {
borderBottom: '1px solid',
// borderBottomColor: 'var(--joy-palette-divider)',
borderBottomColor: 'rgba(var(--joy-palette-neutral-mainChannel) / 0.1)',
// borderTopColor: 'rgba(var(--joy-palette-neutral-mainChannel, 99 107 116) / 0.4)',
// backgroundColor: 'var(--joy-palette-background-surface)',
// paddingBlock: '0.25rem',
paddingInline: '0.5rem',
// layout
display: 'flex',
flexFlow: 'row nowrap',
columnGap: '1.5rem', // space between shortcuts
lineHeight: '1em',
// animation: `${animateAppear} 0.3s ease-out`,
// transition: 'all 0.2s ease',
// '&:hover': {
// backgroundColor: 'var(--joy-palette-background-level1)',
// },
} as const,
hideButton: {
'--IconButton-size': '28px',
'--Icon-fontSize': '16px',
'--Icon-color': 'var(--joy-palette-text-tertiary)',
mr: -0.5,
} as const,
shortcut: {
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
gap: '2px', // space between modifiers
marginBlock: '0.25rem',
// transition: 'transform 0.2s ease',
// '&:hover': {
// transform: 'scale(1.05)',
// },
'&:hover > div': {
backgroundColor: 'background.level1',
} as const,
cursor: 'pointer',
[`&[aria-disabled="true"]`]: {
opacity: 0.5,
pointerEvents: 'none',
} as const,
} as const,
itemKeyGroup: {
fontSize: 'xs',
fontWeight: 'md',
outline: '1px solid',
outlineColor: 'neutral.outlinedBorder',
borderRadius: 'xs',
// backgroundColor: 'var(--joy-palette-neutral-outlinedBorder)',
backgroundColor: 'background.popup',
// boxShadow: 'inset 2px 0px 4px -2px var(--joy-palette-background-backdrop)',
boxShadow: 'xs',
// minWidth: '1rem',
paddingBlock: '2px',
paddingInline: '1px',
// pointerEvents: 'none',
cursor: 'pointer',
transition: 'background-color 1s ease',
display: 'flex',
textAlign: 'center',
// Remove the gap and use dividers instead
gap: 0,
'& > span': {
position: 'relative',
paddingInline: '4px',
minWidth: '1rem',
'&:not(:last-child)': {
borderRight: '1px solid',
borderRightColor: 'neutral.outlinedBorder',
},
},
} as const,
itemIcon: {
fontSize: 'md',
} as const,
} as const;
const hideButtonSx: SxProps = {
'--IconButton-size': '28px',
'--Icon-fontSize': '16px',
'--Icon-color': 'var(--joy-palette-text-tertiary)',
mr: -0.5,
};
// const animateAppear = keyframes`
// from {
@@ -123,6 +45,64 @@ const _styles = {
// }
// `;
const StatusBarContainer = styled(Box)({
borderBottom: '1px solid',
// borderBottomColor: 'var(--joy-palette-divider)',
borderBottomColor: 'rgba(var(--joy-palette-neutral-mainChannel) / 0.1)',
// borderTopColor: 'rgba(var(--joy-palette-neutral-mainChannel, 99 107 116) / 0.4)',
// backgroundColor: 'var(--joy-palette-background-surface)',
// paddingBlock: '0.25rem',
paddingInline: '0.5rem',
// layout
display: 'flex',
flexFlow: 'row nowrap',
columnGap: '1.5rem', // space between shortcuts
lineHeight: '1em',
// animation: `${animateAppear} 0.3s ease-out`,
// transition: 'all 0.2s ease',
// '&:hover': {
// backgroundColor: 'var(--joy-palette-background-level1)',
// },
});
const ShortcutContainer = styled(Box)({
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
gap: '2px', // space between modifiers
marginBlock: '0.25rem',
// transition: 'transform 0.2s ease',
// '&:hover': {
// transform: 'scale(1.05)',
// },
'&:hover > div': {
backgroundColor: 'var(--joy-palette-background-level1)',
},
cursor: 'pointer',
[`&[aria-disabled="true"]`]: {
opacity: 0.5,
pointerEvents: 'none',
}
});
const ShortcutKey = styled(Box)({
fontSize: 'var(--joy-fontSize-xs)',
fontWeight: 'var(--joy-fontWeight-md)',
border: '1px solid',
borderColor: 'var(--joy-palette-neutral-outlinedBorder)',
borderRadius: 'var(--joy-radius-xs)',
// backgroundColor: 'var(--joy-palette-neutral-outlinedBorder)',
backgroundColor: 'var(--joy-palette-background-popup)',
// boxShadow: 'inset 2px 0px 4px -2px var(--joy-palette-background-backdrop)',
boxShadow: 'var(--joy-shadow-xs)',
// minWidth: '1rem',
paddingBlock: '1px',
paddingInline: '4px',
// pointerEvents: 'none',
cursor: 'pointer',
transition: 'background-color 1s ease',
});
// Display mac-style shortcuts on windows as well
const displayMacModifiers = true;
@@ -138,8 +118,6 @@ function _platformAwareModifier(symbol: 'Ctrl' | 'Alt' | 'Shift') {
}
}
const ShortcutItemMemo = React.memo(ShortcutItem);
function ShortcutItem(props: { shortcut: ShortcutObject }) {
const handleClicked = React.useCallback(() => {
@@ -148,24 +126,17 @@ function ShortcutItem(props: { shortcut: ShortcutObject }) {
}, [props.shortcut]);
return (
<Box
onClick={!props.shortcut.disabled ? handleClicked : undefined}
aria-disabled={props.shortcut.disabled}
sx={_styles.shortcut}
>
<Box sx={_styles.itemKeyGroup}>
{!!props.shortcut.ctrl && <span>{_platformAwareModifier('Ctrl')}</span>}
{!!props.shortcut.shift && <span>{_platformAwareModifier('Shift')}</span>}
{/*{!!props.shortcut.altForNonMac && <span>{_platformAwareModifier('Alt')}</span>}*/}
<span>{props.shortcut.key === 'Escape' ? 'Esc' : props.shortcut.key === 'Enter' ? '↵' : props.shortcut.key.toUpperCase()}</span>
</Box>
<ShortcutContainer onClick={!props.shortcut.disabled ? handleClicked : undefined} aria-disabled={props.shortcut.disabled}>
{!!props.shortcut.ctrl && <ShortcutKey>{_platformAwareModifier('Ctrl')}</ShortcutKey>}
{!!props.shortcut.shift && <ShortcutKey>{_platformAwareModifier('Shift')}</ShortcutKey>}
{/*{!!props.shortcut.altForNonMac && <ShortcutKey onClick={handleClicked}>{_platformAwareModifier('Alt')}</ShortcutKey>}*/}
<ShortcutKey>{props.shortcut.key === 'Escape' ? 'Esc' : props.shortcut.key === 'Enter' ? '↵' : props.shortcut.key.toUpperCase()}</ShortcutKey>
&nbsp;<Typography level='body-xs'>{props.shortcut.description}</Typography>
{!!props.shortcut.endDecoratorIcon && <props.shortcut.endDecoratorIcon sx={_styles.itemIcon} />}
</Box>
{props.shortcut.endDecoratorIcon && <props.shortcut.endDecoratorIcon sx={{ fontSize: 'md' }} />}
</ShortcutContainer>
);
}
export const StatusBarMemo = React.memo(StatusBar);
export function StatusBar(props: { toggleMinimized?: () => void, isMinimized?: boolean }) {
@@ -177,34 +148,18 @@ export function StatusBar(props: { toggleMinimized?: () => void, isMinimized?: b
// external state
const labsShowShortcutBar = useUXLabsStore(state => state.labsShowShortcutBar);
const shortcuts = useGlobalShortcutsStore(useShallow(state => {
// get visible shortcuts
let visibleShortcuts = !labsShowShortcutBar ? [] : state.getAllShortcuts().filter(shortcut => !!shortcut.description);
// filter by highest level if levels are present
const maxLevel = Math.max(...visibleShortcuts.map(s => s.level ?? 0));
if (maxLevel > 0)
visibleShortcuts = visibleShortcuts.filter(s => s.level === maxLevel);
visibleShortcuts.sort((a, b) => {
// 1. First by level
if ((a.level ?? 0) !== (b.level ?? 0))
return (b.level ?? 0) - (a.level ?? 0);
// 2. Then by modifiers presence (no modifiers first)
const aModifiers = (a.ctrl ? 1 : 0) + (a.shift ? 1 : 0);
const bModifiers = (b.ctrl ? 1 : 0) + (b.shift ? 1 : 0);
if (aModifiers !== bModifiers)
return aModifiers - bModifiers;
// 3a. Special case for ShortcutKey.Esc, at the beginning
if (a.key === ShortcutKey.Esc) return -1;
if (b.key === ShortcutKey.Esc) return 1;
// 3. Special case for 'Beam Edit'
if (a.description === 'Beam Edit') return 1;
if (b.description === 'Beam Edit') return -1;
// 4. Finally alphabetically by key
// if they don't have a 'shift', they are sorted first
if (a.shift !== b.shift)
return a.shift ? 1 : -1;
// (Hack) If the description is 'Beam', it goes last
if (a.description === 'Beam Edit')
return 1;
// alphabetical for the rest
return a.key.localeCompare(b.key);
});
return visibleShortcuts;
@@ -247,30 +202,27 @@ export function StatusBar(props: { toggleMinimized?: () => void, isMinimized?: b
return null;
return (
<Box
aria-label='Shortcuts and status bar'
sx={_styles.bar}
>
<StatusBarContainer aria-label='Status bar'>
{(!props.toggleMinimized || !COMPOSER_ENABLE_MINIMIZE) && !props.isMinimized ? (
// Close Button
<GoodTooltip variantOutlined arrow placement='top' title={hideButtonTooltip}>
<IconButton size='sm' onClick={handleHideShortcuts} sx={_styles.hideButton}>
<IconButton size='sm' sx={hideButtonSx} onClick={handleHideShortcuts}>
<CloseRoundedIcon />
</IconButton>
</GoodTooltip>
) : (
// Minimize / Maximize Button - note the Maximize icon would be more correct, but also less discoverable
<IconButton size='sm' onClick={props.toggleMinimized} sx={_styles.hideButton}>
<IconButton size='sm' sx={hideButtonSx} onClick={props.toggleMinimized}>
{props.isMinimized ? <ExpandLessIcon /> : <MinimizeIcon />}
</IconButton>
)}
{/* Show all shortcuts */}
{shortcuts.map((shortcut, idx) => (
<ShortcutItemMemo key={shortcut.key + idx} shortcut={shortcut} />
<ShortcutItem key={shortcut.key + idx} shortcut={shortcut} />
))}
</Box>
</StatusBarContainer>
);
}
@@ -127,7 +127,7 @@ export function CameraCaptureModal(props: {
const handleVideoDownloadClicked = React.useCallback(async () => {
if (!videoRef.current) return;
await downloadVideoFrame(videoRef.current, 'camera', 'image/jpeg', 0.98).catch(alert);
await downloadVideoFrame(videoRef.current, 'camera', 'image/jpeg', 0.98);
}, [videoRef]);
+70 -113
View File
@@ -2,10 +2,12 @@ import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import type { FileWithHandle } from 'browser-fs-access';
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Typography } from '@mui/joy';
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Tooltip, Typography } from '@mui/joy';
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
import PsychologyIcon from '@mui/icons-material/Psychology';
import SendIcon from '@mui/icons-material/Send';
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
@@ -17,34 +19,35 @@ import { useChatAutoSuggestAttachmentPrompts, useChatMicTimeoutMsValue } from '.
import { useAgiAttachmentPrompts } from '~/modules/aifn/agiattachmentprompts/useAgiAttachmentPrompts';
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
import { DLLM, getLLMContextTokens, getLLMPricing, LLM_IF_OAI_Vision } from '~/common/stores/llms/llms.types';
import { DLLM, 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';
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal';
import { ConversationsManager } from '~/common/chat-overlay/ConversationsManager';
import { DMessageId, DMessageMetadata, DMetaReferenceItem, messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
import { DMessageMetadata, DMetaReferenceItem, messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
import { ShortcutKey, ShortcutObject, useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts';
import { addSnackbar } from '~/common/components/snackbar/useSnackbarsStore';
import { animationEnterBelow } from '~/common/util/animUtils';
import { browserSpeechRecognitionCapability, PLACEHOLDER_INTERIM_TRANSCRIPT, SpeechResult, useSpeechRecognition } from '~/common/components/speechrecognition/useSpeechRecognition';
import { DConversationId } from '~/common/stores/chat/chat.conversation';
import { copyToClipboard, supportsClipboardRead } from '~/common/util/clipboardUtils';
import { createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragments } from '~/common/stores/chat/chat.fragments';
import { glueForMessageTokens, marshallWrapDocFragments } from '~/common/stores/chat/chat.tokens';
import { createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragmentsNoVoid } from '~/common/stores/chat/chat.fragments';
import { estimateTextTokens, glueForMessageTokens, marshallWrapDocFragments } from '~/common/stores/chat/chat.tokens';
import { isValidConversation, useChatStore } from '~/common/stores/chat/store-chats';
import { getModelParameterValueOrThrow } from '~/common/stores/llms/llms.parameters';
import { launchAppCall, removeQueryParam, useRouterQuery } from '~/common/app.routes';
import { lineHeightTextareaMd, themeBgAppChatComposer } from '~/common/app.theme';
import { lineHeightTextareaMd } from '~/common/app.theme';
import { optimaOpenPreferences } from '~/common/layout/optima/useOptima';
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
import { useChatComposerOverlayStore } from '~/common/chat-overlay/store-perchat_vanilla';
import { useComposerStartupText, useLogicSherpaStore } from '~/common/logic/store-logic-sherpa';
import { useDebouncer } from '~/common/components/useDebouncer';
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
import { useUICounter, useUIPreferencesStore } from '~/common/stores/store-ui';
import { useUXLabsStore } from '~/common/stores/store-ux-labs';
import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import type { ActileItem } from './actile/ActileProvider';
import { providerAttachmentLabels } from './actile/providerAttachmentLabels';
@@ -54,7 +57,6 @@ import { useActileManager } from './actile/useActileManager';
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import { LLMAttachmentDraftsAction, LLMAttachmentsList } from './llmattachments/LLMAttachmentsList';
import { PhPaintBrush } from '~/common/components/icons/phosphor/PhPaintBrush';
import { useAttachmentDrafts } from '~/common/attachment-drafts/useAttachmentDrafts';
import { useLLMAttachmentDrafts } from './llmattachments/useLLMAttachmentDrafts';
@@ -67,24 +69,19 @@ import { ButtonAttachScreenCaptureMemo } from './buttons/ButtonAttachScreenCaptu
import { ButtonAttachWebMemo } from './buttons/ButtonAttachWeb';
import { ButtonBeamMemo } from './buttons/ButtonBeam';
import { ButtonCallMemo } from './buttons/ButtonCall';
import { ButtonGroupDrawRepeat } from './buttons/ButtonGroupDrawRepeat';
import { ButtonMicContinuationMemo } from './buttons/ButtonMicContinuation';
import { ButtonMicMemo } from './buttons/ButtonMic';
import { ButtonMultiChatMemo } from './buttons/ButtonMultiChat';
import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
import { ComposerTextAreaActions } from './textarea/ComposerTextAreaActions';
import { ComposerTextAreaDrawActions } from './textarea/ComposerTextAreaDrawActions';
import { StatusBarMemo } from '../StatusBar';
import { StatusBar } from '../StatusBar';
import { TokenBadgeMemo } from './tokens/TokenBadge';
import { TokenProgressbarMemo } from './tokens/TokenProgressbar';
import { useComposerDragDrop } from './useComposerDragDrop';
import { useTextTokenCount } from './tokens/useTextTokenCounter';
import { useWebInputModal } from './WebInputModal';
// configuration
const zIndexComposerOverlayMic = 10;
const SHOW_TIPS_AFTER_RELOADS = 25;
const paddingBoxSx: SxProps = {
@@ -104,24 +101,20 @@ const minimizedSx: SxProps = {
export function Composer(props: {
isMobile: boolean;
chatLLM: DLLM | null;
composerTextAreaRef: React.RefObject<HTMLTextAreaElement | null>;
composerTextAreaRef: React.RefObject<HTMLTextAreaElement>;
targetConversationId: DConversationId | null;
capabilityHasT2I: boolean;
capabilityHasT2IEdit: boolean;
isMulticast: boolean | null;
isDeveloperMode: boolean;
onAction: (conversationId: DConversationId, chatExecuteMode: ChatExecuteMode, fragments: (DMessageContentFragment | DMessageAttachmentFragment)[], metadata?: DMessageMetadata) => boolean;
onConversationBeamEdit: (conversationId: DConversationId, editMessageId?: DMessageId) => Promise<void>;
onConversationsImportFromFiles: (files: File[]) => Promise<void>;
onTextImagine: (conversationId: DConversationId, text: string) => void;
setIsMulticast: (on: boolean) => void;
onComposerHasContent: (hasContent: boolean) => void;
sx?: SxProps;
}) {
// state
const [composeText, setComposeText] = React.useState('');
const [drawRepeat, setDrawRepeat] = React.useState(1);
const [composeText, debouncedText, setComposeText] = useDebouncer('', 300, 1200, true);
const [micContinuation, setMicContinuation] = React.useState(false);
const [speechInterimResult, setSpeechInterimResult] = React.useState<SpeechResult | null>(null);
const [sendStarted, setSendStarted] = React.useState(false);
@@ -142,13 +135,12 @@ export function Composer(props: {
labsShowCost: state.labsShowCost,
labsShowShortcutBar: state.labsShowShortcutBar,
})));
const timeToShowTips = useLogicSherpaStore(state => state.usageCount >= SHOW_TIPS_AFTER_RELOADS);
const timeToShowTips = useLogicSherpaStore(state => state.usageCount >= 5);
const { novel: explainShiftEnter, touch: touchShiftEnter } = useUICounter('composer-shift-enter');
const { novel: explainAltEnter, touch: touchAltEnter } = useUICounter('composer-alt-enter');
const { novel: explainCtrlEnter, touch: touchCtrlEnter } = useUICounter('composer-ctrl-enter');
const [startupText, setStartupText] = useComposerStartupText();
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
const composerQuickButton = useUIPreferencesStore(state => state.composerQuickButton);
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, abortConversationTemp } = useChatStore(useShallow(state => {
const conversation = state.conversations.find(_c => _c.id === props.targetConversationId);
@@ -178,7 +170,7 @@ export function Composer(props: {
const enableLoadURLsInComposer = hasComposerBrowseCapability && !composeText.startsWith('/');
// user message for attachments
const { onConversationBeamEdit, onConversationsImportFromFiles } = props;
const { onConversationsImportFromFiles } = props;
const handleFilterAGIFile = React.useCallback(async (file: File): Promise<boolean> =>
await showPromisedOverlay('composer-open-or-attach', { rejectWithValue: false }, ({ onResolve, onUserReject }) => (
<ConfirmationModal
@@ -194,12 +186,11 @@ export function Composer(props: {
)), [onConversationsImportFromFiles, showPromisedOverlay]);
// attachments-overlay: comes from the attachments slice of the conversation overlay
const showChatAttachments = chatExecuteModeCanAttach(chatExecuteMode, props.capabilityHasT2IEdit);
const {
/* items */ attachmentDrafts,
/* append */ attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoFragments, attachAppendFile, attachAppendUrl,
/* take */ attachmentsRemoveAll, attachmentsTakeAllFragments, attachmentsTakeFragmentsByType,
} = useAttachmentDrafts(conversationOverlayStore, enableLoadURLsInComposer, chatLLMSupportsImages, handleFilterAGIFile, showChatAttachments === 'only-images');
} = useAttachmentDrafts(conversationOverlayStore, enableLoadURLsInComposer, chatLLMSupportsImages, handleFilterAGIFile);
// attachments derived state
const llmAttachmentDraftsCollection = useLLMAttachmentDrafts(attachmentDrafts, props.chatLLM, chatLLMSupportsImages);
@@ -217,8 +208,7 @@ export function Composer(props: {
const isMobile = props.isMobile;
const isDesktop = !props.isMobile;
const noConversation = !targetConversationId;
const composerTextSuffix = chatExecuteMode === 'generate-image' && isDesktop && drawRepeat > 1 ? ` x${drawRepeat}` : '';
const showChatAttachments = chatExecuteModeCanAttach(chatExecuteMode);
const micIsRunning = !!speechInterimResult;
// more mic way below, as we use complex hooks
@@ -226,14 +216,18 @@ export function Composer(props: {
// tokens derived state
const tokensComposerTextDebounced = useTextTokenCount(composeText, props.chatLLM, 800, 1600);
let tokensComposer = (tokensComposerTextDebounced ?? 0) + (llmAttachmentDraftsCollection.llmTokenCountApprox || 0);
const tokensComposerTextDebounced = React.useMemo(() => {
return (debouncedText && props.chatLLM)
? estimateTextTokens(debouncedText, props.chatLLM, 'composer text')
: 0;
}, [props.chatLLM, debouncedText]);
let tokensComposer = tokensComposerTextDebounced + (llmAttachmentDraftsCollection.llmTokenCountApprox || 0);
if (props.chatLLM && tokensComposer > 0)
tokensComposer += glueForMessageTokens(props.chatLLM);
const tokensHistory = _historyTokenCount;
const tokensResponseMax = getModelParameterValueOrThrow('llmResponseTokens', props.chatLLM?.initialParameters, props.chatLLM?.userParameters, 0) ?? 0;
const tokenLimit = getLLMContextTokens(props.chatLLM) ?? 0;
const tokenChatPricing = getLLMPricing(props.chatLLM)?.chat;
const tokenLimit = props.chatLLM?.contextTokens || 0;
const tokenChatPricing = props.chatLLM?.pricing?.chat;
// Effect: load initial text if queued up (e.g. by /link/share_targetF)
@@ -244,13 +238,6 @@ export function Composer(props: {
}
}, [setComposeText, setStartupText, startupText]);
// Effect: notify the parent of presence/absence of content
const isContentful = composeText.length > 0 || !!attachmentDrafts.length;
const { onComposerHasContent } = props;
React.useEffect(() => {
onComposerHasContent?.(isContentful);
}, [isContentful, onComposerHasContent]);
// Overlay actions
@@ -311,9 +298,9 @@ export function Composer(props: {
// prepare the fragments: content (if any) and attachments (if allowed, and any)
const fragments: (DMessageContentFragment | DMessageAttachmentFragment)[] = [];
if (composerText)
fragments.push(createTextContentFragment(composerText + composerTextSuffix));
fragments.push(createTextContentFragment(composerText));
const canAttach = chatExecuteModeCanAttach(_chatExecuteMode, props.capabilityHasT2IEdit);
const canAttach = chatExecuteModeCanAttach(_chatExecuteMode);
if (canAttach) {
const attachmentFragments = await attachmentsTakeAllFragments('global', 'app-chat');
fragments.push(...attachmentFragments);
@@ -332,7 +319,7 @@ export function Composer(props: {
if (enqueued)
_handleClearText();
return enqueued;
}, [targetConversationId, confirmProceedIfAttachmentsNotSupported, composerTextSuffix, props.capabilityHasT2IEdit, inReferenceTo, onAction, _handleClearText, attachmentsTakeAllFragments]);
}, [attachmentsTakeAllFragments, confirmProceedIfAttachmentsNotSupported, _handleClearText, inReferenceTo, onAction, targetConversationId]);
const handleSendAction = React.useCallback(async (chatExecuteMode: ChatExecuteMode, composerText: string): Promise<boolean> => {
setSendStarted(true);
@@ -458,13 +445,8 @@ export function Composer(props: {
addSnackbar({ key: 'chat-mic-running', message: 'Please wait for the microphone to finish.', type: 'info' });
return;
}
if (composeText) {
await handleSendAction('beam-content', composeText); // 'beam' button
} else {
if (targetConversationId)
void onConversationBeamEdit(targetConversationId); // beam-edit conversation
}
}, [composeText, handleSendAction, micIsRunning, onConversationBeamEdit, targetConversationId]);
await handleSendAction('beam-content', composeText); // 'beam' button
}, [composeText, handleSendAction, micIsRunning]);
const handleStopClicked = React.useCallback(() => {
targetConversationId && abortConversationTemp(targetConversationId);
@@ -511,7 +493,7 @@ export function Composer(props: {
const cHandler = ConversationsManager.getHandler(conversationId);
const messageToEmbed = cHandler.historyFindMessageOrThrow(messageId);
if (messageToEmbed) {
const fragmentsCopy = duplicateDMessageFragments(messageToEmbed.fragments, true); // [attach] deep copy a message's fragments to attach to ego
const fragmentsCopy = duplicateDMessageFragmentsNoVoid(messageToEmbed.fragments); // [attach] deep copy a message's fragments to attach to ego
if (fragmentsCopy.length) {
const chatTitle = cHandler.title() ?? '';
const messageText = messageFragmentsReduceText(fragmentsCopy);
@@ -618,7 +600,7 @@ export function Composer(props: {
links.forEach(link => void attachAppendUrl('input-link', link.url));
}, [attachAppendUrl]);
const { openWebInputDialog, webInputDialogComponent } = useWebInputModal(handleAttachWebLinks, composeText);
const { openWebInputDialog, webInputDialogComponent } = useWebInputModal(handleAttachWebLinks);
// Attachments Down
@@ -648,12 +630,8 @@ export function Composer(props: {
const composerShortcuts: ShortcutObject[] = [];
if (showChatAttachments) {
composerShortcuts.push({ key: 'f', ctrl: true, shift: true, action: () => openFileForAttaching(true, handleAttachFiles), description: 'Attach File' });
composerShortcuts.push({ key: 'l', ctrl: true, shift: true, action: openWebInputDialog, description: 'Attach Link' });
if (supportsClipboardRead())
composerShortcuts.push({ key: 'v', ctrl: true, shift: true, action: attachAppendClipboardItems, description: 'Attach Clipboard' });
// Future: keep reactive state here to support Live Screen Capture and more
// if (labsAttachScreenCapture && supportsScreenCapture)
// composerShortcuts.push({ key: 's', ctrl: true, shift: true, action: openScreenCaptureDialog, description: 'Attach Screen Capture' });
}
if (recognitionState.isActive) {
composerShortcuts.push({ key: 'm', ctrl: true, action: handleFinishMicAndSend, description: 'Mic · Send', disabled: !recognitionState.hasSpeech || sendStarted, endDecoratorIcon: TelegramIcon as any, level: 4 });
@@ -672,7 +650,7 @@ export function Composer(props: {
}, description: 'Microphone',
});
return composerShortcuts;
}, [attachAppendClipboardItems, handleAttachFiles, handleFinishMicAndSend, openWebInputDialog, recognitionState.hasSpeech, recognitionState.isActive, sendStarted, showChatAttachments, toggleRecognition]));
}, [attachAppendClipboardItems, handleAttachFiles, handleFinishMicAndSend, recognitionState.hasSpeech, recognitionState.isActive, sendStarted, showChatAttachments, toggleRecognition]));
// ...
@@ -684,7 +662,7 @@ export function Composer(props: {
const isDraw = chatExecuteMode === 'generate-image';
const showChatInReferenceTo = !!inReferenceTo?.length;
const showChatExtras = isText && !showChatInReferenceTo && !assistantAbortible && composerQuickButton !== 'off';
const showChatExtras = isText && !showChatInReferenceTo;
const sendButtonVariant: VariantProp = (isAppend || (isMobile && isTextBeam)) ? 'outlined' : 'solid';
@@ -700,15 +678,13 @@ export function Composer(props: {
: isAppend ? <SendIcon sx={{ fontSize: 18 }} />
: isReAct ? <PsychologyIcon />
: isTextBeam ? <ChatBeamIcon /> /* <GavelIcon /> */
: isDraw ? <PhPaintBrush />
: isDraw ? <FormatPaintTwoToneIcon />
: <TelegramIcon />;
const beamButtonColor: ColorPaletteProp | undefined =
!llmAttachmentDraftsCollection.canAttachAllFragments ? 'warning'
: undefined;
const showTint: ColorPaletteProp | undefined = isDraw ? 'warning' : isReAct ? 'success' : undefined;
// stable randomization of the /verb, between '/draw', '/react'
const placeholderAction = React.useMemo(() => {
const actions: string[] = ['/react'];
@@ -728,13 +704,13 @@ export function Composer(props: {
+ (recognitionState.isAvailable ? ' · ramble' : '')
+ '...';
if (isDesktop && timeToShowTips && !isDraw) {
if (isDesktop && timeToShowTips) {
if (explainShiftEnter)
textPlaceholder += !enterIsNewline ? '\n\n Shift + Enter to add a new line' : '\n\n Shift + Enter to send';
// else if (explainAltEnter)
// textPlaceholder += platformAwareKeystrokes('\n\n Tip: Alt + Enter to just append the message');
textPlaceholder += !enterIsNewline ? '\n\n💡 Shift + Enter to add a new line' : '\n\n💡 Shift + Enter to send';
else if (explainAltEnter)
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Alt + Enter to just append the message');
else if (explainCtrlEnter)
textPlaceholder += platformAwareKeystrokes('\n\n Tip: Ctrl + Enter to beam');
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Ctrl + Enter to beam');
}
const stableGridSx: SxProps = React.useMemo(() => ({
@@ -745,14 +721,9 @@ export function Composer(props: {
}), [dragContainerSx]);
return (
<Box
aria-label='New Message'
component='section'
bgcolor={showTint ? `var(--joy-palette-${showTint}-softBg)` : themeBgAppChatComposer}
sx={props.sx}
>
<Box aria-label='User Message' component='section' sx={props.sx}>
{!isMobile && labsShowShortcutBar && <StatusBarMemo toggleMinimized={handleToggleMinimized} isMinimized={isMinimized} />}
{!isMobile && labsShowShortcutBar && <StatusBar toggleMinimized={handleToggleMinimized} isMinimized={isMinimized} />}
{/* This container is here just to let the potential statusbar fill the whole space, so we moved the padding here and not in the parent */}
<Box sx={(!isMinimized || isMobile || !labsShowShortcutBar) ? paddingBoxSx : minimizedSx}>
@@ -773,16 +744,13 @@ export function Composer(props: {
<Box sx={{ flexGrow: 0, display: 'grid', gap: 1, alignSelf: 'flex-start' }}>
{/* [mobile] Mic button */}
{recognitionState.isAvailable && <ButtonMicMemo variant={micVariant} color={micColor === 'danger' ? 'danger' : showTint || micColor} errorMessage={recognitionState.errorMessage} onClick={handleToggleMic} />}
{recognitionState.isAvailable && <ButtonMicMemo variant={micVariant} color={micColor} errorMessage={recognitionState.errorMessage} onClick={handleToggleMic} />}
{/* Responsive Camera OCR button */}
{showChatAttachments && <ButtonAttachCameraMemo color={showTint} isMobile onOpenCamera={openCamera} />}
{/* [mobile] Attach file button (in draw with image mode) */}
{showChatAttachments === 'only-images' && <ButtonAttachFilesMemo color={showTint} isMobile onAttachFiles={handleAttachFiles} fullWidth multiple />}
{showChatAttachments && <ButtonAttachCameraMemo isMobile onOpenCamera={openCamera} />}
{/* [mobile] [+] button */}
{showChatAttachments === true && (
{showChatAttachments && (
<Dropdown>
<MenuButton slots={{ root: IconButton }}>
<AddCircleOutlineIcon />
@@ -823,19 +791,19 @@ export function Composer(props: {
{/*</FormHelperText>*/}
{/* Responsive Open Files button */}
<ButtonAttachFilesMemo color={showTint} onAttachFiles={handleAttachFiles} fullWidth multiple />
<ButtonAttachFilesMemo onAttachFiles={handleAttachFiles} fullWidth multiple />
{/* Responsive Web button */}
{showChatAttachments !== 'only-images' && <ButtonAttachWebMemo color={showTint} disabled={!hasComposerBrowseCapability} onOpenWebInput={openWebInputDialog} />}
<ButtonAttachWebMemo disabled={!hasComposerBrowseCapability} onOpenWebInput={openWebInputDialog} />
{/* Responsive Paste button */}
{supportsClipboardRead() && showChatAttachments !== 'only-images' && <ButtonAttachClipboardMemo color={showTint} onAttachClipboard={attachAppendClipboardItems} />}
{supportsClipboardRead() && <ButtonAttachClipboardMemo onAttachClipboard={attachAppendClipboardItems} />}
{/* Responsive Screen Capture button */}
{labsAttachScreenCapture && supportsScreenCapture && <ButtonAttachScreenCaptureMemo color={showTint} onAttachScreenCapture={handleAttachScreenCapture} />}
{labsAttachScreenCapture && supportsScreenCapture && <ButtonAttachScreenCaptureMemo onAttachScreenCapture={handleAttachScreenCapture} />}
{/* Responsive Camera OCR button */}
{labsCameraDesktop && <ButtonAttachCameraMemo color={showTint} onOpenCamera={openCamera} />}
{labsCameraDesktop && <ButtonAttachCameraMemo onOpenCamera={openCamera} />}
</Box>)}
@@ -859,8 +827,8 @@ export function Composer(props: {
<Textarea
variant='outlined'
color={isDraw ? 'warning' : isReAct ? 'success' : undefined}
autoFocus={isDesktop}
minRows={isMobile ? 3.5 : isDraw ? 4 : agiAttachmentPrompts.hasData ? 3 : showChatInReferenceTo ? 4 : 5}
autoFocus
minRows={isMobile ? 4 : agiAttachmentPrompts.hasData ? 3 : showChatInReferenceTo ? 4 : 5}
maxRows={isMobile ? 8 : 10}
placeholder={textPlaceholder}
value={composeText}
@@ -869,12 +837,8 @@ export function Composer(props: {
onPasteCapture={handleAttachCtrlV}
// onFocusCapture={handleFocusModeOn}
// onBlurCapture={handleFocusModeOff}
endDecorator={isDraw
? <ComposerTextAreaDrawActions
composerText={composeText}
onReplaceText={setComposeText}
/>
: <ComposerTextAreaActions
endDecorator={
<ComposerTextAreaActions
agiAttachmentPrompts={agiAttachmentPrompts}
inReferenceTo={inReferenceTo}
onAppendAndSend={handleAppendTextAndSend}
@@ -883,7 +847,6 @@ export function Composer(props: {
}
slotProps={{
textarea: {
tabIndex: !recognitionState.isActive ? undefined : -1,
height: '100%',
enterKeyHint: enterIsNewline ? 'enter' : 'send',
sx: {
@@ -895,17 +858,17 @@ export function Composer(props: {
}}
sx={{
height: '100%',
backgroundColor: showTint ? undefined : 'background.level1',
backgroundColor: 'background.level1',
'&:focus-within': { backgroundColor: 'background.popup', '.within-composer-focus': { backgroundColor: 'background.popup' } },
lineHeight: lineHeightTextareaMd,
}} />
{!showChatInReferenceTo && !isDraw && tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensResponseMax) > 0) && (
{!showChatInReferenceTo && tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensResponseMax) > 0) && (
<TokenProgressbarMemo chatPricing={tokenChatPricing} direct={tokensComposer} history={tokensHistory} responseMax={tokensResponseMax} limit={tokenLimit} />
)}
{!showChatInReferenceTo && !isDraw && tokenLimit > 0 && (
<TokenBadgeMemo hideBelowDollars={0.01} chatPricing={tokenChatPricing} direct={tokensComposer} history={tokensHistory} responseMax={tokensResponseMax} limit={tokenLimit} showCost={labsShowCost} enableHover={!isMobile} showExcess absoluteBottomRight />
{!showChatInReferenceTo && tokenLimit > 0 && (
<TokenBadgeMemo hideBelowDollars={0.0001} chatPricing={tokenChatPricing} direct={tokensComposer} history={tokensHistory} responseMax={tokensResponseMax} limit={tokenLimit} showCost={labsShowCost} enableHover={!isMobile} showExcess absoluteBottomRight />
)}
</Box>
@@ -973,7 +936,7 @@ export function Composer(props: {
fontStyle: 'italic',
},
}}>
{!!composeText && <span className='preceding'>{composeText.endsWith(' ') ? composeText : composeText + ' '}</span>}
{!!debouncedText && <span className='preceding'>{debouncedText.endsWith(' ') ? debouncedText : debouncedText + ' '}</span>}
{speechInterimResult.transcript}
<span className={speechInterimResult.interimTranscript === PLACEHOLDER_INTERIM_TRANSCRIPT ? 'placeholder' : 'interim'}>{speechInterimResult.interimTranscript}</span>
</Typography>
@@ -1008,9 +971,7 @@ export function Composer(props: {
{/* [mobile] bottom-corner secondary button */}
{isMobile && (showChatExtras
? (composerQuickButton === 'call'
? <ButtonCallMemo isMobile disabled={noConversation || noLLM} onClick={handleCallClicked} />
: <ButtonBeamMemo isMobile disabled={noConversation /*|| noLLM*/} color={beamButtonColor} hasContent={!!composeText} onClick={handleSendTextBeamClicked} />)
? <ButtonCallMemo isMobile disabled={noConversation || noLLM} onClick={handleCallClicked} />
: isDraw
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
: <IconButton disabled sx={{ mr: { xs: 1, md: 2 } }} />
@@ -1030,7 +991,7 @@ export function Composer(props: {
<Button
key='composer-act'
fullWidth
disabled={noConversation /* || noLLM*/}
disabled={noConversation || noLLM}
loading={sendStarted}
loadingPosition='end'
onClick={handleSendClicked}
@@ -1061,17 +1022,16 @@ export function Composer(props: {
{/*</Tooltip>}*/}
{/* [Draw] Imagine */}
{/* NOTE: disabled: as we have prompt enhancement in the TextArea (Draw Mode) already */}
{/*{isDraw && !!composeText && <Tooltip title='Generate an image prompt'>*/}
{/* <IconButton variant='outlined' disabled={noConversation || noLLM} onClick={handleTextImagineClicked}>*/}
{/* <AutoAwesomeIcon />*/}
{/* </IconButton>*/}
{/*</Tooltip>}*/}
{isDraw && !!composeText && <Tooltip title='Generate an image prompt'>
<IconButton variant='outlined' disabled={noConversation || noLLM} onClick={handleTextImagineClicked}>
<AutoAwesomeIcon />
</IconButton>
</Tooltip>}
{/* Mode expander */}
<IconButton
variant={chatExecuteMenuShown ? 'outlined' : assistantAbortible ? 'soft' : isDraw ? undefined : undefined}
disabled={noConversation /*|| chatExecuteMenuShown*/}
variant={assistantAbortible ? 'soft' : isDraw ? undefined : undefined}
disabled={noConversation || noLLM || chatExecuteMenuShown}
onClick={showChatExecuteMenu}
>
<ExpandLessIcon />
@@ -1082,7 +1042,7 @@ export function Composer(props: {
{isDesktop && showChatExtras && !assistantAbortible && (
<ButtonBeamMemo
color={beamButtonColor}
disabled={noConversation /*|| noLLM*/}
disabled={noConversation || noLLM}
hasContent={!!composeText}
onClick={handleSendTextBeamClicked}
/>
@@ -1090,9 +1050,6 @@ export function Composer(props: {
</Box>
{/* [desktop] Draw mode N buttons */}
{isDesktop && isDraw && <ButtonGroupDrawRepeat drawRepeat={drawRepeat} setDrawRepeat={setDrawRepeat} />}
{/* [desktop] Multicast switch (under the Chat button) */}
{isDesktop && props.isMulticast !== null && <ButtonMultiChatMemo multiChat={props.isMulticast} onSetMultiChat={props.setIsMulticast} />}
@@ -1,10 +1,8 @@
import * as React from 'react';
import { Controller, useFieldArray, useForm } from 'react-hook-form';
import { Box, Button, Chip, FormControl, FormHelperText, IconButton, Input, Stack, Typography } from '@mui/joy';
import AddCircleOutlineRoundedIcon from '@mui/icons-material/AddCircleOutlineRounded';
import { Box, Button, FormControl, FormHelperText, IconButton, Input, Stack, Typography } from '@mui/joy';
import AddIcon from '@mui/icons-material/Add';
import BrowserUpdatedOutlinedIcon from '@mui/icons-material/BrowserUpdatedOutlined';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded';
import YouTubeIcon from '@mui/icons-material/YouTube';
@@ -13,7 +11,7 @@ import { extractYoutubeVideoIDFromURL } from '~/modules/youtube/youtube.utils';
import { GoodModal } from '~/common/components/modals/GoodModal';
import { addSnackbar } from '~/common/components/snackbar/useSnackbarsStore';
import { asValidURL, extractUrlsFromText } from '~/common/util/urlUtils';
import { asValidURL } from '~/common/util/urlUtils';
// configuration
@@ -28,25 +26,8 @@ type WebInputModalInputs = {
links: WebInputData[];
}
const _styles = {
ytIcon: {
color: 'red',
} as const,
chipLink: {
ml: 'auto',
pr: 1.125,
// '--Chip-radius': '4px',
// whiteSpace: 'break-spaces',
// gap: 1.5,
} as const,
} as const;
function WebInputModal(props: {
composerText?: string,
onClose: () => void,
onWebLinks: (urls: WebInputData[]) => void,
}) {
@@ -54,31 +35,13 @@ function WebInputModal(props: {
// state
const { control: formControl, handleSubmit: formHandleSubmit, formState: { isValid: formIsValid, isDirty: formIsDirty } } = useForm<WebInputModalInputs>({
values: { links: [{ url: '' }] },
mode: 'onChange', // validate on change
// mode: 'onChange', // validate on change
});
const { fields: formFields, append: formFieldsAppend, remove: formFieldsRemove, update: formFieldsUpdate } = useFieldArray({ control: formControl, name: 'links' });
const firstInputRef = React.useRef<HTMLInputElement>(null);
const { fields: formFields, append: formFieldsAppend, remove: formFieldsRemove } = useFieldArray({ control: formControl, name: 'links' });
// derived
const urlFieldCount = formFields.length;
const canAddMoreUrls = urlFieldCount < MAX_URLS;
// [effect] auto-focus first input
React.useEffect(() => {
setTimeout(() => {
if (firstInputRef.current)
firstInputRef.current.focus();
}, 0);
}, []);
// memos
const extractedComposerUrls = React.useMemo(() => {
return !props.composerText ? null : extractUrlsFromText(props.composerText);
}, [props.composerText]);
const extractedUrlsCount = extractedComposerUrls?.length ?? 0;
// handlers
@@ -107,46 +70,6 @@ function WebInputModal(props: {
}, [handleClose, onWebLinks]);
// const handleAddUrl = React.useCallback((newUrl: string) => {
// // bail if can't add
// if (!canAddMoreUrls)
// return addSnackbar({ key: 'max-urls', message: `Maximum ${MAX_URLS} URLs allowed`, type: 'precondition-fail' });
//
// // bail if already in
// const exists = formFields.some(({ url }) => url === newUrl);
// if (exists)
// return addSnackbar({ key: 'duplicate-url', message: 'URL already added', type: 'info' });
//
// // replace the first empty field, or append
// const emptyFieldIndex = formFields.findIndex(field => !field.url.trim());
// if (emptyFieldIndex >= 0)
// formFieldsUpdate(emptyFieldIndex, { url: newUrl });
// else
// formFieldsAppend({ url: newUrl });
// }, [canAddMoreUrls, formFields, formFieldsAppend, formFieldsUpdate]);
const handleAddAllUrls = React.useCallback(() => {
if (!extractedComposerUrls) return;
// new URLs that are not already in the form
const newURLs = extractedComposerUrls.filter(url => !formFields.some(field => field.url.trim() === url));
if (!newURLs.length) return;
// find empty fields first
for (let i = 0; i < formFields.length; i++) {
const field = formFields[i];
if (!field.url.trim()) {
formFieldsUpdate(i, { url: newURLs.shift()! });
if (!newURLs.length) break;
}
}
// append remaining
newURLs.forEach(url => formFieldsAppend({ url }));
}, [extractedComposerUrls, formFields, formFieldsAppend, formFieldsUpdate]);
return (
<GoodModal
open
@@ -166,26 +89,6 @@ function WebInputModal(props: {
{/*You can add up to {MAX_URLS} URLs.*/}
</Typography>
{/* Modified URLs section */}
{!!extractedUrlsCount && (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography level='title-sm' startDecorator={<BrowserUpdatedOutlinedIcon />}>
{extractedUrlsCount} URL{extractedUrlsCount > 1 ? 's' : ''} in your message
{/*{extractedUrlsCount} URL{extractedUrlsCount > 1 ? 's' : ''} found in your message*/}
</Typography>
<Chip
variant='soft'
onClick={handleAddAllUrls}
startDecorator={<AddCircleOutlineRoundedIcon />}
sx={_styles.chipLink}
>
Add
</Chip>
</Box>
)}
<form onSubmit={formHandleSubmit(handleSubmit)}>
<Stack spacing={1}>
{formFields.map((field, index) => (
@@ -198,16 +101,12 @@ function WebInputModal(props: {
<FormControl error={!!error}>
<Box sx={{ display: 'flex', gap: 1 }}>
<Input
autoFocus={index === 0}
required={index === 0}
placeholder='https://...'
endDecorator={extractYoutubeVideoIDFromURL(value) ? <YouTubeIcon sx={_styles.ytIcon} /> : undefined}
endDecorator={extractYoutubeVideoIDFromURL(value) ? <YouTubeIcon sx={{ color: 'red' }} /> : undefined}
value={value}
onChange={onChange}
slotProps={index !== 0 ? undefined : {
input: {
ref: firstInputRef,
},
}}
sx={{ flex: 1 }}
/>
{urlFieldCount > 1 && (
@@ -234,7 +133,7 @@ function WebInputModal(props: {
{formIsDirty && <Button
color='neutral'
variant='soft'
disabled={!canAddMoreUrls}
disabled={urlFieldCount >= MAX_URLS}
onClick={() => formFieldsAppend({ url: '' })}
startDecorator={<AddIcon />}
>
@@ -248,7 +147,7 @@ function WebInputModal(props: {
disabled={!formIsValid || !formIsDirty}
sx={{ minWidth: 160, ml: 'auto' }}
>
Import {urlFieldCount > 1 ? `(${urlFieldCount})` : ''}
Add {urlFieldCount > 1 ? `(${urlFieldCount})` : ''}
</Button>
</Box>
@@ -259,20 +158,15 @@ function WebInputModal(props: {
}
export function useWebInputModal(onAttachWebLinks: (urls: WebInputData[]) => void, composerText?: string) {
export function useWebInputModal(onAttachWebLinks: (urls: WebInputData[]) => void) {
// state
const [open, setOpen] = React.useState(false);
const composerTextRef = React.useRef(composerText);
// copy the text to a ref, constantly - we just care about a recent snapshot, but don't want to invalidate hooks
composerTextRef.current = composerText;
const openWebInputDialog = React.useCallback(() => setOpen(true), []);
const webInputDialogComponent = React.useMemo(() => open && (
<WebInputModal
composerText={composerTextRef.current}
onClose={() => setOpen(false)}
onWebLinks={onAttachWebLinks}
/>
@@ -38,7 +38,6 @@ export function ActilePopup(props: {
maxHeightGapPx={320}
minWidth={320}
noBottomPadding
noAutoFocus={true /* we control keyboard navigation */}
noTopPadding
>
@@ -4,7 +4,7 @@ import type { ActileItem, ActileProvider } from './ActileProvider';
import { ActilePopup } from './ActilePopup';
export const useActileManager = (providers: ActileProvider[], anchorRef: React.RefObject<HTMLElement | null>) => {
export const useActileManager = (providers: ActileProvider[], anchorRef: React.RefObject<HTMLElement>) => {
// state
const [popupOpen, setPopupOpen] = React.useState(false);
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Box, Button, ColorPaletteProp, IconButton, Tooltip } from '@mui/joy';
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
import CameraAltOutlinedIcon from '@mui/icons-material/CameraAltOutlined';
@@ -12,7 +12,6 @@ import { CameraCaptureModal } from '../CameraCaptureModal';
export const ButtonAttachCameraMemo = React.memo(ButtonAttachCamera);
function ButtonAttachCamera(props: {
color?: ColorPaletteProp,
isMobile?: boolean,
disabled?: boolean,
fullWidth?: boolean,
@@ -20,7 +19,7 @@ function ButtonAttachCamera(props: {
onOpenCamera: () => void,
}) {
return props.isMobile ? (
<IconButton color={props.color} disabled={props.disabled} onClick={props.onOpenCamera}>
<IconButton disabled={props.disabled} onClick={props.onOpenCamera}>
<AddAPhotoIcon />
</IconButton>
) : (
@@ -31,8 +30,8 @@ function ButtonAttachCamera(props: {
</Box>
)}>
<Button
variant={props.color ? 'soft' : 'plain'}
color={props.color || 'neutral'}
variant='plain'
color='neutral'
disabled={props.disabled}
fullWidth={props.fullWidth}
startDecorator={<CameraAltOutlinedIcon />}
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Box, Button, ColorPaletteProp, IconButton, Tooltip } from '@mui/joy';
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo';
import { KeyStroke } from '~/common/components/KeyStroke';
@@ -10,7 +10,6 @@ import { buttonAttachSx } from '~/common/components/ButtonAttachFiles';
export const ButtonAttachClipboardMemo = React.memo(ButtonAttachClipboard);
function ButtonAttachClipboard(props: {
color?: ColorPaletteProp,
isMobile?: boolean,
disabled?: boolean,
fullWidth?: boolean,
@@ -18,7 +17,7 @@ function ButtonAttachClipboard(props: {
onAttachClipboard: () => void,
}) {
return props.isMobile ? (
<IconButton color={props.color} disabled={props.disabled} onClick={props.onAttachClipboard}>
<IconButton disabled={props.disabled} onClick={props.onAttachClipboard}>
<ContentPasteGoIcon />
</IconButton>
) : (
@@ -30,8 +29,8 @@ function ButtonAttachClipboard(props: {
</Box>
)}>
<Button
variant={props.color ? 'soft' : 'plain'}
color={props.color || 'neutral'}
variant='plain'
color='neutral'
disabled={props.disabled}
fullWidth={props.fullWidth}
startDecorator={<ContentPasteGoIcon />}
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Box, Button, ColorPaletteProp, IconButton, Tooltip } from '@mui/joy';
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
import AddRoundedIcon from '@mui/icons-material/AddRounded';
import { buttonAttachSx } from '~/common/components/ButtonAttachFiles';
@@ -9,7 +9,6 @@ import { buttonAttachSx } from '~/common/components/ButtonAttachFiles';
export const ButtonAttachNewMemo = React.memo(ButtonAttachNew);
function ButtonAttachNew(props: {
color?: ColorPaletteProp,
isMobile?: boolean,
disabled?: boolean,
fullWidth?: boolean,
@@ -17,7 +16,7 @@ function ButtonAttachNew(props: {
onAttachNew: () => void,
}) {
return props.isMobile ? (
<IconButton color={props.color} disabled={props.disabled} onClick={props.onAttachNew}>
<IconButton disabled={props.disabled} onClick={props.onAttachNew}>
<AddRoundedIcon />
</IconButton>
) : (
@@ -30,15 +29,15 @@ function ButtonAttachNew(props: {
</Box>
)}>
<Button
variant={props.color ? 'soft' : 'plain'}
color={props.color || 'neutral'}
variant='plain'
color='neutral'
disabled={props.disabled}
fullWidth={props.fullWidth}
startDecorator={<AddRoundedIcon />}
onClick={props.onAttachNew}
sx={buttonAttachSx.desktop}
>
Note
New
</Button>
</Tooltip>
);
@@ -1,6 +1,6 @@
import * as React from 'react';
import { Box, Button, ColorPaletteProp, IconButton, Tooltip } from '@mui/joy';
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
import ScreenshotMonitorIcon from '@mui/icons-material/ScreenshotMonitor';
import { Is } from '~/common/util/pwaUtils';
@@ -11,7 +11,6 @@ import { takeScreenCapture } from '~/common/util/screenCaptureUtils';
export const ButtonAttachScreenCaptureMemo = React.memo(ButtonAttachScreenCapture);
function ButtonAttachScreenCapture(props: {
color?: ColorPaletteProp,
isMobile?: boolean,
disabled?: boolean,
fullWidth?: boolean,
@@ -42,7 +41,7 @@ function ButtonAttachScreenCapture(props: {
return props.isMobile ? (
<IconButton color={props.color} disabled={props.disabled} onClick={handleTakeScreenCapture}>
<IconButton disabled={props.disabled} onClick={handleTakeScreenCapture}>
<ScreenshotMonitorIcon />
</IconButton>
) : (
@@ -56,8 +55,8 @@ function ButtonAttachScreenCapture(props: {
</Box>
)}>
<Button
variant={capturing ? 'solid' : props.color ? 'soft' : 'plain'}
color={!!error ? 'danger' : props.color || 'neutral'}
variant={capturing ? 'solid' : 'plain'}
color={!!error ? 'danger' : 'neutral'}
disabled={props.disabled}
fullWidth={props.fullWidth}
loading={capturing}
@@ -1,16 +1,14 @@
import * as React from 'react';
import { Box, Button, ColorPaletteProp, IconButton, Tooltip } from '@mui/joy';
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
import LanguageRoundedIcon from '@mui/icons-material/LanguageRounded';
import { buttonAttachSx } from '~/common/components/ButtonAttachFiles';
import { KeyStroke } from '~/common/components/KeyStroke';
export const ButtonAttachWebMemo = React.memo(ButtonAttachWeb);
function ButtonAttachWeb(props: {
color?: ColorPaletteProp,
isMobile?: boolean,
disabled?: boolean,
fullWidth?: boolean,
@@ -19,13 +17,13 @@ function ButtonAttachWeb(props: {
}) {
const button = props.isMobile ? (
<IconButton color={props.color} disabled={props.disabled} onClick={props.onOpenWebInput}>
<IconButton disabled={props.disabled} onClick={props.onOpenWebInput}>
<LanguageRoundedIcon />
</IconButton>
) : (
<Button
variant={props.color ? 'soft' : 'plain'}
color={props.color || 'neutral'}
variant='plain'
color='neutral'
disabled={props.disabled}
fullWidth={props.fullWidth}
startDecorator={<LanguageRoundedIcon />}
@@ -37,13 +35,12 @@ function ButtonAttachWeb(props: {
);
return (props.noToolTip || props.isMobile) ? button : (
<Tooltip arrow disableInteractive placement='top-start' title={
<Tooltip arrow disableInteractive placement='top-start' title={(
<Box sx={buttonAttachSx.tooltip}>
<b>Add Web Content 🌐</b><br />
Import from websites and YouTube
<KeyStroke combo='Ctrl + Shift + L' sx={{ mt: 1, mb: 0.5 }} />
</Box>
}>
)}>
{button}
</Tooltip>
);
@@ -43,7 +43,7 @@ function ButtonBeam(props: {
onClick: () => void,
}) {
return props.isMobile ? (
<IconButton variant='outlined' color={props.color ?? 'primary'} disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
<IconButton variant='soft' color={props.color ?? 'primary'} disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
<ChatBeamIcon />
</IconButton>
) : (
@@ -1,77 +0,0 @@
import * as React from 'react';
import { Box, FormControl, IconButton } from '@mui/joy';
const _styles = {
control: {
gap: 1,
mt: 1,
} as const,
buttonGroup: {
display: 'flex',
justifyContent: 'space-evenly',
// overflowX: 'hidden',
flexWrap: 'wrap',
minWidth: '131px',
} as const,
buttonActive: {
'--IconButton-size': { xs: '1.75rem', lg: '2rem' },
} as const,
button: {
'--IconButton-size': { xs: '1.75rem', lg: '2rem' },
border: '1px solid',
borderColor: 'warning.outlinedBorder',
backgroundColor: 'background.popup',
// boxShadow: drawRepeat === n ? '0px 2px 8px 0px rgb(var(--joy-palette-warning-mainChannel) / 40%)' : 'none',
// fontWeight: drawRepeat === n ? 'xl' : 400, /* reset, from 600 */
transition: 'transform 0.14s, box-shadow 0.14s',
'&:hover': {
transform: 'translateY(-1px)',
// backgroundColor: drawRepeat === n ? 'background.popup' : 'background.surface',
// boxShadow: '0 0 8px 1px rgb(var(--joy-palette-warning-mainChannel) / 40%)',
} as const,
} as const,
text: {
mx: 'auto',
fontSize: 'xs',
opacity: '0.5',
} as const,
} as const;
export function ButtonGroupDrawRepeat(props: {
drawRepeat: number,
setDrawRepeat: (n: number) => void,
}) {
const { drawRepeat, setDrawRepeat } = props;
return (
<FormControl sx={_styles.control}>
<Box sx={_styles.buttonGroup}>
{[1, 2, 4, 5, 10].map((n) => (
<IconButton
key={n}
size='sm'
color='warning'
variant={drawRepeat === n ? 'solid' : 'soft'}
onClick={() => setDrawRepeat(n)}
sx={drawRepeat === n ? _styles.buttonActive : _styles.button}
>
{n}
</IconButton>
))}
</Box>
<Box sx={_styles.text}>
{drawRepeat > 1
? `Create ${drawRepeat} Images`
: 'Number of Images'}
</Box>
</FormControl>
);
}
@@ -7,7 +7,6 @@ import MicIcon from '@mui/icons-material/Mic';
import { ExternalDocsLink } from '~/common/components/ExternalDocsLink';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { KeyStroke } from '~/common/components/KeyStroke';
import { useDontBlurTextarea } from '~/common/components/useDontBlurTextarea';
const micLegend = (errorMessage: string | null) =>
@@ -36,7 +35,12 @@ function ButtonMic(props: {
}) {
// Mobile: don't blur the textarea when clicking the mic button
const handleDontBlurTextArea = useDontBlurTextarea();
const handleDontBlurTextArea = React.useCallback((event: React.MouseEvent) => {
const isTextAreaFocused = document.activeElement?.tagName === 'TEXTAREA';
// If a textarea is focused, prevent the default blur behavior
if (isTextAreaFocused)
event.preventDefault();
}, []);
return (
<GoodTooltip placement='top' arrow enableInteractive title={micLegend(props.errorMessage)}>
@@ -16,7 +16,7 @@ export function ButtonMultiChat(props: { isMobile?: boolean, multiChat: boolean,
color={multiChat ? 'warning' : undefined}
onClick={() => props.onSetMultiChat(!multiChat)}
>
{multiChat ? <ChatMulticastOnIcon /> : <ChatMulticastOnIcon />}
{multiChat ? <ChatMulticastOnIcon /> : <ChatMulticastOffIcon />}
</IconButton>
) : (
<FormControl orientation='horizontal' sx={{ minHeight: '2.25rem', justifyContent: 'space-between' }}>
@@ -4,8 +4,6 @@ import { Button, IconButton } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
import { PhSlidersHorizontalIcon } from '~/common/components/icons/phosphor/PhSlidersHorizontalIcon';
export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => void, sx?: SxProps }) {
return props.isMobile ? (
@@ -13,8 +11,8 @@ export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => vo
<FormatPaintTwoToneIcon />
</IconButton>
) : (
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx} endDecorator={<PhSlidersHorizontalIcon />}>
Image Settings
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
Options
</Button>
);
}
@@ -22,7 +22,7 @@ import { RenderImageRefDBlob } from '~/modules/blocks/image/RenderImageRefDBlob'
import { RenderImageURL } from '~/modules/blocks/image/RenderImageURL';
import type { AttachmentDraft, AttachmentDraftConverterType, AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import { DMessageDataRef, DMessageImageRefPart, isImageRefPart, isZyncAssetImageReferencePartWithLegacyDBlob } from '~/common/stores/chat/chat.fragments';
import { DMessageDataRef, DMessageImageRefPart, isImageRefPart } from '~/common/stores/chat/chat.fragments';
import { LiveFileIcon } from '~/common/livefile/liveFile.icons';
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
import { ellipsizeFront, ellipsizeMiddle } from '~/common/util/textUtils';
@@ -98,7 +98,6 @@ 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,
@@ -116,8 +115,8 @@ const converterTypeToIconMap: { [key in AttachmentDraftConverterType]: React.Com
};
function attachmentIcons(attachmentDraft: AttachmentDraft, noTooltips: boolean, onViewImageRefPart: (imageRefPart: DMessageImageRefPart) => void) {
const activeConverters = attachmentDraft.converters.filter(c => c.isActive);
if (activeConverters.length === 0)
const activeConterters = attachmentDraft.converters.filter(c => c.isActive);
if (activeConterters.length === 0)
return null;
// Alternate icon for the Web Page Screenshot
@@ -128,21 +127,15 @@ function attachmentIcons(attachmentDraft: AttachmentDraft, noTooltips: boolean,
let outputSingleImageRefDBlobs: Extract<DMessageDataRef, { reftype: 'dblob' }>[] = [];
if (!urlImageData && attachmentDraft.outputFragments.length === 1) {
const fragment = attachmentDraft.outputFragments[0];
if (isZyncAssetImageReferencePartWithLegacyDBlob(fragment.part))
outputSingleImageRefDBlobs = [fragment.part._legacyImageRefPart!.dataRef];
else if (isImageRefPart(fragment.part) && fragment.part.dataRef && fragment.part.dataRef.reftype === 'dblob')
if (isImageRefPart(fragment.part) && fragment.part.dataRef && fragment.part.dataRef.reftype === 'dblob')
outputSingleImageRefDBlobs = [fragment.part.dataRef];
}
const handleViewFirstImage = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const fragment = attachmentDraft.outputFragments[0];
if (!fragment) return;
if (isZyncAssetImageReferencePartWithLegacyDBlob(fragment.part))
onViewImageRefPart(fragment.part._legacyImageRefPart!);
else if (isImageRefPart(fragment.part))
onViewImageRefPart(fragment.part);
if (attachmentDraft.outputFragments[0] && isImageRefPart(attachmentDraft.outputFragments[0].part))
onViewImageRefPart(attachmentDraft.outputFragments[0].part);
};
// Whether to render the converters
@@ -169,13 +162,12 @@ function attachmentIcons(attachmentDraft: AttachmentDraft, noTooltips: boolean,
)}
{/* Render DBlob referred images in place of converter icons */}
{outputSingleImageRefDBlobs.map((dataRef, _i) => dataRef && (
{outputSingleImageRefDBlobs.map((dataRef, i) => dataRef && (
<TooltipOutlined key={`image-${dataRef.dblobAssetId}`} title={noTooltips ? null : <>View converted image{/* <br/>{dataRef?.bytesSize?.toLocaleString()} bytes */}</>} placement='top-start'>
<div>
<RenderImageRefDBlob
dataRefDBlobAssetId={dataRef.dblobAssetId}
dataRefMimeType={dataRef.mimeType}
dataRefBytesSize={dataRef.bytesSize}
variant='attachment-button'
scaledImageSx={attachmentIconSx}
onClick={handleViewFirstImage}
@@ -184,8 +176,8 @@ function attachmentIcons(attachmentDraft: AttachmentDraft, noTooltips: boolean,
</TooltipOutlined>
))}
{/*{activeConverters.some(c => c.id.startsWith('url-page-')) ? <LanguageIcon sx={{ opacity: 0.2, ml: -2.5 }} /> : null}*/}
{renderConverterIcons && activeConverters.map((_converter, idx) => {
{/*{activeConterters.some(c => c.id.startsWith('url-page-')) ? <LanguageIcon sx={{ opacity: 0.2, ml: -2.5 }} /> : null}*/}
{renderConverterIcons && activeConterters.map((_converter, idx) => {
const Icon = converterTypeToIconMap[_converter.id] ?? null;
return !Icon ? null : (
<TooltipOutlined key={`${_converter.id}-${idx}`} title={noTooltips ? null : `Attached as ${_converter.name}`} placement='top-start'>
@@ -1,7 +1,7 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Checkbox, Chip, CircularProgress, LinearProgress, ListDivider, ListItem, ListItemDecorator, MenuItem, Radio, Typography } from '@mui/joy';
import { Box, Checkbox, Chip, CircularProgress, LinearProgress, Link, ListDivider, ListItem, ListItemDecorator, MenuItem, Radio, Typography } from '@mui/joy';
import AttachmentIcon from '@mui/icons-material/Attachment';
import ClearIcon from '@mui/icons-material/Clear';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
@@ -10,16 +10,17 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import LaunchIcon from '@mui/icons-material/Launch';
import ReadMoreIcon from '@mui/icons-material/ReadMore';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import VisibilityIcon from '@mui/icons-material/Visibility';
import { CloseablePopup } from '~/common/components/CloseablePopup';
import { DMessageAttachmentFragment, DMessageDocPart, DMessageImageRefPart, isDocPart, isImageRefPart, isZyncAssetImageReferencePartWithLegacyDBlob } from '~/common/stores/chat/chat.fragments';
import { DMessageAttachmentFragment, DMessageDocPart, DMessageImageRefPart, isDocPart, isImageRefPart } from '~/common/stores/chat/chat.fragments';
import { LiveFileIcon } from '~/common/livefile/liveFile.icons';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { themeZIndexOverMobileDrawer } from '~/common/app.theme';
import { useUIPreferencesStore } from '~/common/stores/store-ui';
import { showImageDataURLInNewTab } from '~/common/util/imageUtils';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import type { AttachmentDraftsStoreApi } from '~/common/attachment-drafts/store-attachment-drafts_slice';
@@ -157,7 +158,6 @@ export function LLMAttachmentMenu(props: {
minWidth={260}
noTopPadding
placement='top'
zIndex={themeZIndexOverMobileDrawer /* was not set, but the Attachment Menu can be used from the Personas Modal */}
>
{/* Move Arrows */}
@@ -311,23 +311,13 @@ export function LLMAttachmentMenu(props: {
<Typography level='body-sm' sx={indicatorGapSx}>
{draftInput.urlImage.mimeType} · {draftInput.urlImage.width} x {draftInput.urlImage.height} · {draftInput.urlImage.imgDataUrl?.length.toLocaleString()}
{' · '}
<Chip component='span' size='sm' color='primary' variant='outlined' startDecorator={<VisibilityIcon />} onClick={(event) => {
if (draftInput?.urlImage?.imgDataUrl) {
// Invoke the viewer but with a virtual 'temp' part description to see this preview image
handleViewImageRefPart(event, {
pt: 'image_ref',
dataRef: {
reftype: 'url',
url: draftInput.urlImage.imgDataUrl,
},
altText: draft.label || 'URL Image Preview',
width: draftInput.urlImage.width || undefined,
height: draftInput.urlImage.height || undefined,
});
}
<Link onClick={(event) => {
event.preventDefault();
event.stopPropagation();
showImageDataURLInNewTab(draftInput?.urlImage?.imgDataUrl || '');
}}>
view
</Chip>
open <LaunchIcon sx={{ mx: 0.5, fontSize: 16 }} />
</Link>
</Typography>
)}
@@ -353,17 +343,13 @@ export function LLMAttachmentMenu(props: {
</Chip>
</Typography>
);
} else if (isZyncAssetImageReferencePartWithLegacyDBlob(part) || isImageRefPart(part)) {
// Unified Image Reference handling (both Zync Asset References with legacy fallback and legacy image_ref)
const legacyImageRefPart = isZyncAssetImageReferencePartWithLegacyDBlob(part) ? part._legacyImageRefPart! : part;
const { dataRef, width, height } = legacyImageRefPart;
const resolution = width && height ? `${width} x ${height}` : 'no resolution';
const mime = dataRef.reftype === 'dblob' ? dataRef.mimeType : 'unknown image';
} else if (isImageRefPart(part)) {
const resolution = part.width && part.height ? `${part.width} x ${part.height}` : 'no resolution';
const mime = part.dataRef.reftype === 'dblob' ? part.dataRef.mimeType : 'unknown image';
return (
<Typography key={index} level='body-sm' sx={{ color: 'text.primary' }} startDecorator={<ReadMoreIcon sx={indicatorSx} />}>
<span>{mime /*.replace('image/', 'img: ')*/} · {resolution} · {dataRef.reftype === 'dblob' ? (dataRef.bytesSize?.toLocaleString() || 'no size') : '(remote)'} ·&nbsp;</span>
<Chip component='span' size={isOutputMultiple ? 'sm' : 'md'} color='primary' variant='outlined' startDecorator={<VisibilityIcon />}
onClick={(event) => handleViewImageRefPart(event, legacyImageRefPart)}>
<span>{mime /*.replace('image/', 'img: ')*/} · {resolution} · {part.dataRef.reftype === 'dblob' ? (part.dataRef.bytesSize?.toLocaleString() || 'no size') : '(remote)'} ·&nbsp;</span>
<Chip component='span' size={isOutputMultiple ? 'sm' : 'md'} color='primary' variant='outlined' startDecorator={<VisibilityIcon />} onClick={(event) => handleViewImageRefPart(event, part)}>
view
</Chip>
{isOutputMultiple && <Chip component='span' size={isOutputMultiple ? 'sm' : 'md'} color='danger' variant='outlined' startDecorator={<DeleteForeverIcon />} onClick={(event) => handleDeleteOutputFragment(event, index)}>
@@ -193,7 +193,7 @@ export function LLMAttachmentsList(props: {
</Box>
{/* Overall Menu button */}
{!props.buttonsCanWrap && (
{!_style.barWraps && (
<IconButton
onClick={handleOverallMenuToggle}
onContextMenu={handleOverallMenuToggle}
@@ -1,13 +1,12 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, CircularProgress, IconButton } from '@mui/joy';
import { Box, CircularProgress, IconButton, Tooltip } from '@mui/joy';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import type { AgiAttachmentPromptsData } from '~/modules/aifn/agiattachmentprompts/useAgiAttachmentPrompts';
import { BigAgiSquircleIcon } from '~/common/components/icons/big-agi/BigAgiSquircleIcon';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { AgiSquircleIcon } from '~/common/components/icons/AgiSquircleIcon';
import { AGI_SUGGESTIONS_COLOR } from '../textarea/ComposerTextAreaActions';
@@ -43,7 +42,7 @@ function LLMAttachmentsPromptsButton({ data }: { data: AgiAttachmentPromptsData
const tooltipTitle =
data.error ? (data.error.message || 'Error guessing actions')
: data.isFetching ? null
: data.isPending ? <Box sx={{ display: 'flex', gap: 1 }}><BigAgiSquircleIcon inverted sx={{ color: 'white', borderRadius: '1rem' }} /> What can I do?</Box>
: data.isPending ? <Box sx={{ display: 'flex', gap: 1 }}><AgiSquircleIcon inverted sx={{ color: 'white', borderRadius: '1rem' }} /> What can I do?</Box>
: 'Give me more ideas';
const button = (
@@ -65,8 +64,8 @@ function LLMAttachmentsPromptsButton({ data }: { data: AgiAttachmentPromptsData
);
return !tooltipTitle ? button : (
<GoodTooltip variantOutlined arrow title={tooltipTitle}>
<Tooltip variant='outlined' disableInteractive placement='left' arrow title={tooltipTitle}>
{button}
</GoodTooltip>
</Tooltip>
);
}
@@ -11,7 +11,6 @@ export interface LLMAttachmentDraftsCollection {
canAttachAllFragments: boolean;
canInlineSomeFragments: boolean;
llmTokenCountApprox: number | null;
hasImageFragments: boolean;
}
@@ -20,7 +19,6 @@ export interface LLMAttachmentDraft {
llmSupportsAllFragments: boolean;
llmSupportsTextFragments: boolean;
llmTokenCountApprox: number | null;
hasImageFragments: boolean;
}
@@ -46,10 +44,7 @@ export function useLLMAttachmentDrafts(attachmentDrafts: AttachmentDraft[], chat
const equalChatLLM = chatLLM === prevStateRef.current.chatLLM;
// LLM-dependent multi-modal enablement
// TODO: consider also Audio inputs, maybe PDF binary inputs
// FIXME: reference fragments could refer to non-image as well
const imageTypes: DMessageAttachmentFragment['part']['pt'][] = ['reference', 'image_ref'];
const supportedTypes: DMessageAttachmentFragment['part']['pt'][] = chatLLMSupportsImages ? [...imageTypes, 'doc'] : ['doc'];
const supportedTypes: DMessageAttachmentFragment['part']['pt'][] = chatLLMSupportsImages ? ['image_ref', 'doc'] : ['doc'];
const supportedTextTypes: DMessageAttachmentFragment['part']['pt'][] = supportedTypes.filter(pt => pt === 'doc');
// Add LLM-specific properties to each attachment draft
@@ -71,7 +66,6 @@ export function useLLMAttachmentDrafts(attachmentDrafts: AttachmentDraft[], chat
llmTokenCountApprox: chatLLM
? estimateTokensForFragments(chatLLM, 'user', a.outputFragments, true, 'useLLMAttachmentDrafts')
: null,
hasImageFragments: !a.outputFragments ? false : a.outputFragments.some(op => imageTypes.includes(op.part.pt)),
};
});
@@ -81,7 +75,6 @@ export function useLLMAttachmentDrafts(attachmentDrafts: AttachmentDraft[], chat
const llmTokenCountApprox = chatLLM
? llmAttachmentDrafts.reduce((acc, a) => acc + (a.llmTokenCountApprox || 0), 0)
: null;
const hasImageFragments = llmAttachmentDrafts.some(a => a.hasImageFragments);
// [Optimization] Update the ref with the new state
prevStateRef.current = { llmAttachmentDrafts, chatLLM };
@@ -91,7 +84,6 @@ export function useLLMAttachmentDrafts(attachmentDrafts: AttachmentDraft[], chat
canAttachAllFragments,
canInlineSomeFragments,
llmTokenCountApprox,
hasImageFragments,
};
}, [attachmentDrafts, chatLLM, chatLLMSupportsImages]); // Dependencies for the outer useMemo
@@ -15,7 +15,7 @@ export const AGI_SUGGESTIONS_COLOR: ColorPaletteProp = 'success';
// Styles
export const composerTextAreaSx: SxProps = {
const textAreaSx: SxProps = {
flex: 1,
// layout
@@ -29,8 +29,8 @@ export const composerTextAreaSx: SxProps = {
'--Button-gap': '1.2rem',
transition: 'background-color 0.2s, color 0.2s',
// minWidth: 160,
} as const,
} as const;
},
};
const promptButtonSx: SxProps = {
@@ -75,7 +75,7 @@ export function ComposerTextAreaActions(props: {
return null;
return (
<Box sx={composerTextAreaSx}>
<Box sx={textAreaSx}>
{/* In-Reference-To bubbles */}
{props.inReferenceTo?.map((item, index) => (
@@ -1,76 +0,0 @@
import * as React from 'react';
import { Box, Button } from '@mui/joy';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import { composerTextAreaSx } from './ComposerTextAreaActions';
import { imaginePromptFromTextOrThrow } from '~/modules/aifn/imagine/imaginePromptFromText';
const _style = {
enhance: {
minWidth: 170,
mx: 0.625,
pr: 2,
border: '1px solid',
borderColor: 'warning.outlinedBorder',
boxShadow: '0px 4px 4px -4px rgb(var(--joy-palette-warning-darkChannel) / 20%)',
transition: 'background-color 0.14s',
justifyContent: 'space-between',
} as const,
gone: {
visibility: 'hidden',
} as const,
} as const;
export function ComposerTextAreaDrawActions(props: {
composerText: string,
onReplaceText: (text: string) => void,
}) {
// state
const [isSimpleEnhancing, setIsSimpleEnhancing] = React.useState(false);
// derived
const trimmedPrompt = props.composerText.trim();
const userHasText = trimmedPrompt.length >= 3;
const { onReplaceText } = props;
const handleSimpleEnhance = React.useCallback(async () => {
if (!trimmedPrompt || isSimpleEnhancing) return;
setIsSimpleEnhancing(true);
const improvedPrompt = await imaginePromptFromTextOrThrow(trimmedPrompt, 'DEV')
.catch(console.error);
if (improvedPrompt)
onReplaceText(improvedPrompt);
setIsSimpleEnhancing(false);
}, [isSimpleEnhancing, onReplaceText, trimmedPrompt]);
return (
<Box sx={composerTextAreaSx}>
{/* Enhance button */}
<Box sx={{ ml: 'auto' }}>
<Button
size='sm'
variant={isSimpleEnhancing ? 'soft' : 'soft'}
color='warning'
disabled={!userHasText}
loading={isSimpleEnhancing}
loadingPosition='end'
// className={promptButtonClass}
endDecorator={<AutoFixHighIcon sx={{ fontSize: '20px' }} />}
onClick={handleSimpleEnhance}
sx={!userHasText ? _style.gone : _style.enhance}
>
{isSimpleEnhancing ? 'Enhancing...' : 'Enhance Prompt'}
</Button>
</Box>
</Box>
);
}
@@ -47,9 +47,9 @@ function TokenBadge(props: {
const showAltCosts = !!props.showCost && !!costMax && costMin !== undefined;
if (showAltCosts) {
// Note: switched to 'min cost (>= ...)' on mobile as well, to restore the former behavior, just uncomment the !props.enableHover (a proxy for isMobile)
badgeValue =
// (/*!props.enableHover ||*/ isHovering) ? '< ' + formatModelsCost(costMax) :
'> ' + formatModelsCost(costMin);
badgeValue = (/*!props.enableHover ||*/ isHovering)
? '< ' + formatModelsCost(costMax)
: '> ' + formatModelsCost(costMin);
} else {
// show the direct tokens, unless we exceed the limit and 'showExcess' is enabled
@@ -77,7 +77,7 @@ function TokenBadge(props: {
slotProps={{
root: {
sx: {
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: '1rem' }),
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: 8 }),
cursor: 'help',
...(shallInvisible && {
opacity: 0,
@@ -92,13 +92,6 @@ function TokenBadge(props: {
fontFamily: 'code',
fontSize: 'xs',
...((props.absoluteBottomRight || props.inline) && { position: 'static', transform: 'none' }),
// make it transparent over text
// backgroundColor: `rgb(var(--joy-palette-${color}-lightChannel) / 15%)`, // similar to success.50
background: 'transparent',
boxShadow: 'none', // outline
'&:hover': {
backgroundColor: `${color}.softHoverBg`,
},
},
},
}}

Some files were not shown because too many files have changed in this diff Show More