mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
330 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e900695f8b | |||
| aacb4349e9 | |||
| 55bde68a4d | |||
| 26ae3545a7 | |||
| 0001f7392b | |||
| d7e83e578b | |||
| 901d93b5f0 | |||
| 6858b0b94a | |||
| 9d88bf9b82 | |||
| 1bf1b744b9 | |||
| ee2d7114c7 | |||
| 3b1b54b3a3 | |||
| 524029a882 | |||
| 69161d29a7 | |||
| 8a542c1af4 | |||
| fe16970624 | |||
| e21abdef45 | |||
| acdbb2fbaf | |||
| 14be134ef2 | |||
| f56f6eb3cd | |||
| d3a7b75d1c | |||
| d5d7cf5a21 | |||
| 13b928d68b | |||
| 31948a62f9 | |||
| bf2d00a936 | |||
| ed4edd7c0b | |||
| e5de61d682 | |||
| ac69c62020 | |||
| a43b6a2cf5 | |||
| e8e3366fe2 | |||
| d813810a28 | |||
| c400aa7543 | |||
| 9fc0b39730 | |||
| 194bfe23a1 | |||
| 35110480ef | |||
| 959595e33a | |||
| a960424dfb | |||
| 0df6c7d08b | |||
| 65c841e7a7 | |||
| b21b8cc982 | |||
| aa2c4f06b7 | |||
| b8d7b4ec10 | |||
| c48520255a | |||
| 0790da989d | |||
| 506d24d2fd | |||
| 1348dbf493 | |||
| ce677f3cd9 | |||
| 39203d78e3 | |||
| 2ef7daf369 | |||
| cff3d90613 | |||
| 9f89243d7f | |||
| 784ee9a4da | |||
| 678e6b8ba1 | |||
| 30e301c496 | |||
| b22904f6bb | |||
| 3f0de7ddca | |||
| 9a6f0f9202 | |||
| 4f0bae5657 | |||
| 2101f06195 | |||
| 6d54b5594c | |||
| 36b8e5b1df | |||
| 8252d671c7 | |||
| 30d97c94aa | |||
| 82654a00d4 | |||
| 9595f14ddc | |||
| 8c496074b2 | |||
| 4d097d7136 | |||
| 178619d275 | |||
| 59c8b2538d | |||
| 443b72c52a | |||
| ae13abef45 | |||
| 83ae02ef9b | |||
| 9bb178413b | |||
| d85f0ebfc4 | |||
| 8f84dc2f24 | |||
| c8b4301bcd | |||
| bd8eaf0b9f | |||
| a4148cf694 | |||
| 4cb0b493dc | |||
| e6354e9089 | |||
| 08506abaee | |||
| 078c80d572 | |||
| b1c9f6be45 | |||
| fc497e9beb | |||
| 6ad01fd981 | |||
| 44ed8664c8 | |||
| 4cb16ee715 | |||
| 2dc9b87cda | |||
| 0e587c4889 | |||
| 41d42d82fb | |||
| f703c8a8c9 | |||
| bf753eab55 | |||
| 698b67af06 | |||
| 377d61056a | |||
| 94b32c8fe3 | |||
| 1e70a59ad6 | |||
| 44d05181f4 | |||
| 996998a5cc | |||
| 98474b2721 | |||
| 198dc0e23f | |||
| 079731c573 | |||
| 492c89650a | |||
| 5b5bbb7649 | |||
| 27d1f081ab | |||
| 76183fd840 | |||
| 345165eabf | |||
| c186732b3b | |||
| 04916b700e | |||
| 013dab185c | |||
| 5ab93faccf | |||
| fa301e3675 | |||
| fa6e7dd9c5 | |||
| 01736ad5da | |||
| ce682b1f85 | |||
| 96d801f40a | |||
| 8985868f63 | |||
| 8febdcd0c0 | |||
| 4d21d5134a | |||
| 09d44a4314 | |||
| 40066e975a | |||
| 202382c80a | |||
| 6ffbb32c57 | |||
| 9b8a3ca503 | |||
| cdd7892077 | |||
| 974aa12137 | |||
| d8f8999333 | |||
| 0efd87b522 | |||
| ec76e1c5cf | |||
| 1e04efe748 | |||
| 69c135ae78 | |||
| 205fb1bb5b | |||
| c8e7315de3 | |||
| 725f3b0fd7 | |||
| 7ee3701607 | |||
| 9537ce59e8 | |||
| 6c0a60e0d1 | |||
| 436a858cb0 | |||
| 6ea6c55f65 | |||
| c477fa86ce | |||
| 08cd5ed5b6 | |||
| b5f2cd35f2 | |||
| 4cb0f6d67e | |||
| 5260ec68cc | |||
| 72ce4d2884 | |||
| ed65f989d9 | |||
| 588ebf4993 | |||
| 22969033a7 | |||
| 8b5e00480b | |||
| aaf752fa9c | |||
| 82d3b36048 | |||
| 588c81f9ad | |||
| 4013a3f997 | |||
| 5823e18904 | |||
| 31ea6863aa | |||
| f3f58f26ae | |||
| 67132f285e | |||
| 20a638a8c9 | |||
| c9174e995f | |||
| 656c507c94 | |||
| a1fb744eb1 | |||
| 28367547fd | |||
| 6610211eac | |||
| b66e3e2afa | |||
| 4bf965953a | |||
| 1bd6513d59 | |||
| 6ce457913e | |||
| ef84ca5a04 | |||
| f76524c650 | |||
| 0be676229f | |||
| 40a0ca7235 | |||
| 1563c3a9dc | |||
| 80f32be80d | |||
| eea53714cc | |||
| 148f1ec22c | |||
| b5a2a70e73 | |||
| e7667e4b7d | |||
| 9250eb9aff | |||
| 92883caaab | |||
| 6d57450efc | |||
| 5dd4c600ea | |||
| 392a3b7949 | |||
| e22c40c7e4 | |||
| c7abee6969 | |||
| 4772e63fdb | |||
| f3d7abefec | |||
| ac76b156cf | |||
| 97e65efc31 | |||
| 13dcaa0a57 | |||
| 1f42b0ae66 | |||
| 003a50f181 | |||
| 32c5849a50 | |||
| 44a8ee0593 | |||
| 1ad70c7b1b | |||
| 7413983159 | |||
| 6c3e8c6a8f | |||
| 7e3e9854ac | |||
| 41fc93345c | |||
| b9275177e3 | |||
| 5ea95e4095 | |||
| 0ea041ed5b | |||
| 037e3b62d8 | |||
| 517c18c902 | |||
| 685b5c5130 | |||
| cfdab2f900 | |||
| 1a743ff264 | |||
| 85463fafb1 | |||
| 0641b0df97 | |||
| 98825081a9 | |||
| f549c13465 | |||
| 8bf7fd7106 | |||
| d8d889c706 | |||
| 90665ed84a | |||
| dd3d10a391 | |||
| 19ebd399a8 | |||
| f21a2973e9 | |||
| 04bb8f9c12 | |||
| 5ea63c8734 | |||
| f4f4ad9373 | |||
| ba06d70c05 | |||
| 62ddd17715 | |||
| f76db1d19e | |||
| f0901dbc03 | |||
| c65a2ce387 | |||
| eaee372938 | |||
| d8836534cb | |||
| 7d2e64b458 | |||
| bc942c5581 | |||
| 4ca24f8314 | |||
| b299dec68e | |||
| b9f07d011b | |||
| 9259be8dbb | |||
| 4b0b7c4493 | |||
| 73f0760809 | |||
| db6c2b1620 | |||
| 1233e846db | |||
| 27312537a7 | |||
| 1dfd4d8395 | |||
| ccd9f0980f | |||
| 5cc48d24ec | |||
| 7929d4eb30 | |||
| 14c5c83f91 | |||
| 263412c422 | |||
| d395fa817d | |||
| 9cfc8c513b | |||
| c92a1cfcb1 | |||
| f45e45ca8f | |||
| e44d4b8b01 | |||
| c342f553db | |||
| 2fab208ccf | |||
| eab3eee19f | |||
| fcb3903b5f | |||
| 90ccb64bd0 | |||
| 1772db5e98 | |||
| a04ee4de95 | |||
| 73b6a54f9e | |||
| 52b08b407c | |||
| 269a3a9991 | |||
| 1b2050cd96 | |||
| a71dd5e3aa | |||
| 8d91ea0413 | |||
| 81b39c7f9c | |||
| a3200e1aab | |||
| 4c8fa8e477 | |||
| f64aae10c5 | |||
| bd8f484cd2 | |||
| 4c3151e3be | |||
| 4e3377f1df | |||
| f95b643a5c | |||
| 85083f323d | |||
| b884386143 | |||
| 01a8d858cf | |||
| 08fed36a61 | |||
| f8b110e108 | |||
| b78b0f1323 | |||
| 148c0b1d77 | |||
| fe501831b2 | |||
| 1862b72ba5 | |||
| a609071966 | |||
| dc2d162e6e | |||
| 07f2cd291e | |||
| a6e040e3e5 | |||
| 3e6cfc9775 | |||
| 0e2abd2615 | |||
| 394e79510e | |||
| 848977820e | |||
| c893f1969c | |||
| bb9a8b81d1 | |||
| 188b338bdc | |||
| 463ef406a7 | |||
| a916ff46dc | |||
| db3a5c0b1b | |||
| b760250da1 | |||
| b5829ac541 | |||
| fa4f2b8fcd | |||
| 333c318a62 | |||
| 5f6f7086d0 | |||
| a7495bd4cf | |||
| 76c4919e9c | |||
| 5530a0253e | |||
| 86aaa65d10 | |||
| 65bf147e04 | |||
| f76ad186f0 | |||
| e5e333db70 | |||
| ddee08c2da | |||
| 93b7686f18 | |||
| e61e9626e2 | |||
| 3c6bfe0152 | |||
| e4fc44bc9c | |||
| 51e23ad3a4 | |||
| 5ebbe45a63 | |||
| 6df276d51d | |||
| f811500b60 | |||
| 2b51605c18 | |||
| 513b840b47 | |||
| d94c8c8a3b | |||
| 3dd641a398 | |||
| 8e545f1738 | |||
| 2a12597567 | |||
| e003683040 | |||
| 0338b3d2e9 | |||
| 5d5bc403c4 | |||
| b646149980 | |||
| 1e7e8ac632 | |||
| 309786e01e | |||
| 08e3caf8c2 | |||
| 21b68d7660 | |||
| 4986c61b2a | |||
| 801479cb5c | |||
| 1d18e21018 | |||
| 4c329a8f51 |
@@ -0,0 +1,63 @@
|
||||
---
|
||||
description: Search git history for commits that introduce or remove an exact string, within a commit range
|
||||
argument-hint: "[search-string] [ancestor-commit]"
|
||||
allowed-tools: Bash(git *)
|
||||
---
|
||||
|
||||
Search git history using `git log -S` (pickaxe) to find commits that add or remove an exact string.
|
||||
This repo has 7000+ commits, so pickaxe searches can take 30-60+ seconds - this is expected.
|
||||
|
||||
## Parameters
|
||||
|
||||
- `$0` - The exact string to search for in file contents (not commit messages). Examples: `getLabsSUDO`, `EXPERIMENT_ON_SUDO`, `myFunctionName`
|
||||
- `$1` - A commit hash or unique commit message substring to identify the start of the range. Examples: `5af80b96a8`, `"Sudo Mode": 10-click`
|
||||
|
||||
## Example
|
||||
|
||||
```
|
||||
/code:grep-history EXPERIMENT_ON_SUDO "Sudo Mode": 10-click
|
||||
```
|
||||
|
||||
This searches all commits between the `"Sudo Mode": 10-click` commit and HEAD for any that add or remove the string `EXPERIMENT_ON_SUDO` in file contents.
|
||||
|
||||
## Procedure
|
||||
|
||||
### Step 1: Resolve the ancestor commit
|
||||
|
||||
If `$1` looks like a commit hash (hex string), use it directly.
|
||||
Otherwise, search for it by message, restricting to ancestors of HEAD:
|
||||
|
||||
```bash
|
||||
git log --oneline --grep='$1' HEAD | head -5
|
||||
```
|
||||
|
||||
This only walks commits reachable from HEAD, so every result is a guaranteed ancestor - no verification loop needed.
|
||||
|
||||
If multiple results, pick the oldest (last listed) since it represents the earliest matching commit.
|
||||
If none, report the error and stop.
|
||||
|
||||
### Step 2: Run pickaxe search
|
||||
|
||||
```bash
|
||||
git log -S "$0" --oneline <resolved_ancestor>..HEAD
|
||||
```
|
||||
|
||||
This finds commits where the count of `$0` in the codebase changes (i.e., it was added or removed).
|
||||
This can be slow on 7000+ commits - wait for it.
|
||||
|
||||
### Step 3: Check endpoints
|
||||
|
||||
Also check whether the string exists at HEAD and at the ancestor commit:
|
||||
|
||||
```bash
|
||||
git grep -l "$0" HEAD 2>/dev/null || echo "(not found at HEAD)"
|
||||
git grep -l "$0" <resolved_ancestor> 2>/dev/null || echo "(not found at ancestor)"
|
||||
```
|
||||
|
||||
### Step 4: Report
|
||||
|
||||
Present results concisely:
|
||||
- Number of commits found (or "none")
|
||||
- List of matching commits (hash + subject line)
|
||||
- Whether the string exists at HEAD and/or at the ancestor
|
||||
- If found, suggest next steps (e.g., `git show <hash>` to inspect specific commits)
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
description: Show a hierarchical progress tree of the current conversation thread
|
||||
---
|
||||
|
||||
Analyze this conversation thread and produce a **hierarchical progress tree** - a vertical breadcrumb of the chat and actions from the very start to now.
|
||||
|
||||
**Format:**
|
||||
|
||||
A tree, where every rabbithole that was taken adds a level.
|
||||
|
||||
```
|
||||
[ ] Brief initial phase/ask/goal description
|
||||
[x] Specific thing done or decided - "user quote if relevant"
|
||||
[x] Another step
|
||||
[ ] Sub-phase/rabbithole/etc
|
||||
[x] Done step (if important)
|
||||
[ ] Sub-sub-phase
|
||||
[ ] Current step doing <-- HERE
|
||||
[ ] Next step since this sub-sub-phase was broken out
|
||||
|
||||
[ ] Remaining step
|
||||
[ ] ...
|
||||
|
||||
[ ] Missing, back to the main goal
|
||||
[ ] ...
|
||||
|
||||
### What do we rewind the rabbithole to (once the current level is complete)?
|
||||
...
|
||||
|
||||
### What's up (towards user value) and down (towards deeper code levels) the rabbithole?
|
||||
...
|
||||
|
||||
### What's a good hyphenated title for this chat?
|
||||
...
|
||||
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- `[x]` done, `[ ]` not done. Parent is done only when ALL children on the next level are `[x]`
|
||||
- Each node: a few words, specific. Quote the user briefly when it captures the intent
|
||||
- Group by logical phases or rabbitholes (when descending to a deeper level of implementation or going off for a temporary tangent or sub-quest), not by messages
|
||||
- Earlier levels that are fully completed don't need to be expanded in subtasks
|
||||
- Root nodes/completed nodes need to show what was "wanted" from them, not being checked because they are shown as earlier phases (i.e. upper hierarchy contains more)
|
||||
- Some earlier sub-phases or even levels of rabbitholes can be marked as done as indented [x] below each other (do not add non-major bullets on already completed nodes)
|
||||
- Insert newlines in between large groups of items
|
||||
- Decisions: state what was chosen, not the alternatives
|
||||
- If a former phase produced no code change or decision, omit
|
||||
- Very important to insert incomplete `[ ]` items for things that wre mentioned and are likely useful but mentioned at higher levels of the rabbithole so they must come after, when unwinding the stack
|
||||
- Keep it short, tight (min 0 max item count below *ONE QUARTER the user messages*). This is a navigation aid, not a transcript
|
||||
|
||||
It's important for this to represent a high-level sequence of important actions and turns and pivots and rabbiholes, all focuses on trying to solve something.
|
||||
|
||||
First think through it looking at all the chat from the back to the front, then front to back, user requests, and understand the main storybeats. This is useful especially to remove already done leaves that don't add much if shown.
|
||||
So think about the full list, so you have it all in front of you when you do the last pass to show it to me.
|
||||
It's important to see the progress of what we were doing (e.g. see that we set out to do something at the beginning, but a few items of those are still incomplete, also because we took 2 detours to fix more things in the meantime...).
|
||||
|
||||
At the end anser the questions in the Format, with brief bullet points.
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
description: Update MiniMax model definitions with latest pricing and capabilities
|
||||
---
|
||||
|
||||
Update `src/modules/llms/server/openai/models/minimax.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 & Changelog: https://platform.minimax.io/docs/release-notes/models.md
|
||||
- Pricing: https://platform.minimax.io/docs/guides/pricing-paygo.md
|
||||
- Pricing Overview: https://platform.minimax.io/docs/pricing/overview.md
|
||||
- Text Generation API: https://platform.minimax.io/docs/guides/text-generation.md
|
||||
|
||||
**Note:** MiniMax is a hardcoded-only vendor (no `/v1/models` API yet). All model IDs, context windows, and pricing must be manually maintained from the docs. Pay attention to new model releases (M-series), highspeed variants, and deprecated models.
|
||||
|
||||
**Fallbacks if blocked:** Search "minimax api models pricing", "minimax m2 m3 models", "minimax api changelog" or check https://openrouter.ai models list for MiniMax entries.
|
||||
|
||||
**Important:**
|
||||
- Models are `ModelDescriptionSchema[]` objects (not ManualMappings) - match existing pattern in the file
|
||||
- Review the full model list for additions, removals, and price changes
|
||||
- Check for new `-highspeed` variants and new model families
|
||||
- Verify context window sizes and max completion tokens against docs
|
||||
- Minimize whitespace/comment changes, focus on content
|
||||
- Preserve comments to make diffs easy to review
|
||||
- Flag broken links or unexpected content
|
||||
@@ -8,14 +8,11 @@ Reference `src/modules/llms/server/llm.server.types.ts` and `src/modules/llms/se
|
||||
|
||||
**Automated Workflow:**
|
||||
```bash
|
||||
# 1. Fetch the HTML (sorted by newest for stable ordering)
|
||||
curl -s "https://ollama.com/library?sort=newest" -o /tmp/ollama-newest.html
|
||||
# 1. Fetch the HTML to a cross-platform temp path (sorted by newest for stable ordering)
|
||||
curl -s "https://ollama.com/library?sort=newest" -o "$(node -p "require('os').tmpdir()")/ollama-newest.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
|
||||
# 2. Parse it with the script (auto-finds the file in os.tmpdir())
|
||||
node .claude/scripts/parse-ollama-models.js 2>&1
|
||||
```
|
||||
|
||||
The parser outputs: `modelName|pulls|capabilities|sizes`
|
||||
|
||||
@@ -23,11 +23,12 @@ If `$ARGUMENTS` provided, verify only that dialect, which includes reading the p
|
||||
|
||||
## Task
|
||||
|
||||
The sweep data is the source of truth for allowed model parameter values or value ranges.
|
||||
The sweep data is the source of truth for allowed model parameter values or value ranges, and for the `fn` function-calling capability probe.
|
||||
|
||||
For each model in the sweep, verify the model definition exposes exactly those capabilities - no more, no less. This includes:
|
||||
- The parameter is present in parameterSpecs
|
||||
- The paramId variant covers exactly the values from the sweep, if applicable
|
||||
- `LLM_IF_OAI_Fn` in `interfaces` matches `"roundtrip"` in the sweep's `fn` array (see below)
|
||||
- etc.
|
||||
|
||||
Report models where the definition doesn't match the sweep.
|
||||
@@ -50,6 +51,14 @@ and need to be carefully updated, otherwise thousands of clients may break.
|
||||
| Gemini | `gemini-thinking-budget` | `llmVndGeminiThinkingBudget` |
|
||||
| xAI | `xai-web-search` | `llmVndXaiWebSearch` |
|
||||
|
||||
## Function-Calling Capability (`fn`)
|
||||
|
||||
The sweep `fn` array is a capability probe (not a paramId). `"roundtrip"` is the authoritative signal - full tool-call -> response -> coherent follow-up. `LLM_IF_OAI_Fn` in the model's `interfaces` must track `"roundtrip"`: present iff present.
|
||||
|
||||
Flag:
|
||||
- `"roundtrip"` in sweep but `LLM_IF_OAI_Fn` missing (or vice versa)
|
||||
- `fn` contains `"auto"`/`"required"` without `"roundtrip"` - partial capability, call it out
|
||||
|
||||
## Output
|
||||
|
||||
Report first for every model the expected values from the sweep, then the actual values from the definition, then the mismatches.
|
||||
|
||||
@@ -9,7 +9,15 @@ Execute the release process for Big-AGI. Go step-by-step, waiting for user appro
|
||||
|
||||
If `$ARGUMENTS` provided, use it. Otherwise, read `package.json` and increment patch version.
|
||||
|
||||
## Step 2: Update Files
|
||||
## Step 2: Gather Context
|
||||
|
||||
Before drafting, gather what changed:
|
||||
1. `git log --oneline` since last release tag to see all commits
|
||||
2. Fetch https://big-agi.com/changes to see what daily entries already covered
|
||||
3. `gh issue list --state closed --search "closed:>LAST_RELEASE_DATE"` to find closed issues
|
||||
4. Check auto-generated release notes (`gh release create --generate-notes --draft`) for community PRs and new contributors
|
||||
|
||||
## Step 3: Update Files
|
||||
|
||||
1. **package.json** - Update `version` field
|
||||
2. **src/common/app.release.ts** - Increment `Monotonics.NewsVersion` (e.g., 203 → 204)
|
||||
@@ -23,12 +31,13 @@ For the news entry, ask user for release name and key highlights.
|
||||
- UX items grouped, minimal bold
|
||||
- Fixes last, brief
|
||||
- Release name stays subtle - don't oversell the theme
|
||||
- Apply the draft, then let the user edit manually and re-read after - don't over-iterate
|
||||
|
||||
Use `<B>`, `<B issue={N}>`, `<B href='url'>`. Re-read file after user edits.
|
||||
|
||||
4. User runs `npm i` to update lockfile
|
||||
|
||||
## Step 3: README
|
||||
## Step 4: README
|
||||
|
||||
Update `README.md`:
|
||||
- Line ~46: Update model examples if new flagship models
|
||||
@@ -36,7 +45,7 @@ Update `README.md`:
|
||||
|
||||
**Style:** `- Open X.Y.Z: **Name** feature1, feature2, feature3`
|
||||
|
||||
## Step 4: Git Operations
|
||||
## Step 5: Git Operations
|
||||
|
||||
User commits changes, then:
|
||||
```bash
|
||||
@@ -44,21 +53,31 @@ git tag vX.Y.Z
|
||||
git push opensource vX.Y.Z
|
||||
```
|
||||
|
||||
## Step 5: GitHub Release
|
||||
## Step 6: GitHub Release
|
||||
|
||||
Create release with `gh release create`. Structure:
|
||||
Create release with `gh release create` using `--notes` (not `--body`).
|
||||
|
||||
**Structure** - discursive intro paragraph, then themed sections, not a generic "What's New" header:
|
||||
|
||||
```
|
||||
# Big-AGI X.Y.Z - Name
|
||||
|
||||
## What's New
|
||||
### Theme tagline.
|
||||
|
||||
### **Headline Feature**
|
||||
1-2 sentences explaining the main theme. Then bullet points for specifics.
|
||||
1-2 sentence discursive paragraph setting the release theme - what it means, not a feature list.
|
||||
|
||||
### **Also New**
|
||||
- Bullet list of other features
|
||||
- Keep it scannable
|
||||
### Section Name (e.g., Models & Parameters)
|
||||
- Bullet points for specifics
|
||||
- Group by theme, not by commit order
|
||||
|
||||
### Vendor/Platform Section (when enough substance)
|
||||
- Give a vendor its own section if 3+ related changes (e.g., Anthropic, AWS Bedrock)
|
||||
|
||||
### Also New
|
||||
- Remaining features, scannable
|
||||
|
||||
## New Contributors
|
||||
* @user made their first contribution (brief description) in PR_URL
|
||||
|
||||
**Full Changelog**: https://github.com/enricoros/big-AGI/compare/vPREV...vNEW
|
||||
|
||||
@@ -66,7 +85,14 @@ Create release with `gh release create`. Structure:
|
||||
Available now at [big-agi.com](https://big-agi.com), via Docker, or self-host from source.
|
||||
```
|
||||
|
||||
## Step 6: Announcements
|
||||
## Step 7: Changelog (big-agi.com/changes)
|
||||
|
||||
The Open release entry on big-agi.com/changes is lightweight - just 1-2 bullets announcing the stable release, since daily entries already covered the individual features. Use `/rel:changelog` to generate.
|
||||
|
||||
**Style:** `- Open X.Y.Z Name stable release on GitHub and Docker`
|
||||
followed by 1 bullet summarizing what landed in the final days since the last daily entry.
|
||||
|
||||
## Step 8: Announcements
|
||||
|
||||
Draft for user to post:
|
||||
|
||||
@@ -90,6 +116,16 @@ Big-AGI Open X.Y.Z is out!
|
||||
**More:** Count of commits/fixes
|
||||
```
|
||||
|
||||
## Step 9: Cover Image Prompts
|
||||
|
||||
Offer cover image prompt alternatives for the release. Read past prompts from `news.data.tsx` comments (lines ~24-37) for the pattern.
|
||||
|
||||
**Pattern:** Always a capybara sculpture made of crystal glass, wearing rayban-like oversized black sunglasses. Each release has a unique theme/activity that symbolizes the release.
|
||||
|
||||
**Shared prefix:** `High-key white scene, very clean, hero framing. A close-up photo of a capybara sculpture made of crystal glass. The capybara wears rayban-like oversized black sunglasses.`
|
||||
|
||||
**Also offer future release concepts** tied to vision vectors from `kb/vision-inlined.md` (e.g., agency, inhabitation, sculpting, safe exploration).
|
||||
|
||||
## Tone Guide
|
||||
|
||||
**Good:**
|
||||
|
||||
@@ -22,8 +22,10 @@
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const htmlPath = process.argv[2] || '/tmp/ollama-newest.html';
|
||||
const htmlPath = process.argv[2] || path.join(os.tmpdir(), 'ollama-newest.html');
|
||||
const TOP_N_ALWAYS_INCLUDE = 30;
|
||||
const MIN_PULLS_THRESHOLD = 50000;
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Build and push by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
@@ -102,7 +102,7 @@ jobs:
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -29,6 +29,18 @@ npm run build # next build runs compile+lint+types but stops at first type-erro
|
||||
The `gh` command is available to interact with GitHub from the terminal, but **NEVER PUSH TO ANY BRANCH**. The user manages all 'write' git operations.
|
||||
- `opensource` -> `enricoros/big-AGI` (public, default branch: `main`, MIT) - community issues/PRs/releases
|
||||
- `private` -> `big-agi/big-agi-private` (private, default branch: `dev`) - main dev repo with `dev`->`staging`->`prod` pipeline
|
||||
- **Always use `git mv` instead of `mv`** when renaming or moving files - preserves git history tracking
|
||||
- **NEVER run `git stash`** - it causes work loss
|
||||
|
||||
**Branch contents:**
|
||||
- `main` is the open-source build: local-first, BYO-keys, full AIX and provider coverage
|
||||
- `dev` extends `main` with the hosted/cloud layer: auth, Zync sync, Cloud Fabric, Stripe, multi-tenant, admin pages, it's the way to go for users, the best user experience of any multi-model chat application
|
||||
- Cloud/auth/sync code stays on `dev`; non-cloud improvements (UX, AIX, model support, bug fixes) can land on either branch
|
||||
|
||||
**Branch workflow:**
|
||||
- `dev` is rebased on top of `main` (never merged) - `main` changes flow into `dev` on the next rebase, no manual forward-port needed
|
||||
- Never `git merge` between the two branches - breaks the linear topology
|
||||
- Backporting `dev` -> `main` is a re-implementation, never a cherry-pick - keep `main`-side edits minimal/additive so the existing `dev` version lands cleanly on rebase; split into small commits when natural
|
||||
|
||||
### Core Directory Structure
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ It comes packed with **world-class features** like Beam, and is praised for its
|
||||
|
||||
### 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.6, Nano Banana Pro, Kimi K2.5 or GPT 5.4 -
|
||||
**Intelligence**: with [Beam & Merge](https://big-agi.com/beam) for multi-model de-hallucination, native search, and bleeding-edge AI models like Opus 4.7, Nano Banana Pro, Kimi K2.6 or GPT 5.4 -
|
||||
**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.
|
||||
|
||||
@@ -346,7 +346,6 @@ Configure 100s of AI models from 20+ providers:
|
||||
|:--------------|:---------------------------------------------------------------------------------------------------------------|
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
@@ -231,7 +231,6 @@ For Developers:
|
||||
- **[Install Mobile APP](../docs/pixels/feature_pwa.png)** 📲 looks like native (@harlanlewis)
|
||||
- **[UI language](../docs/pixels/feature_language.png)** with auto-detect, and future app language! (@tbodyston)
|
||||
- **PDF Summarization** 🧩🤯 - ask questions to a PDF! (@fredliubojin)
|
||||
- **Code Execution: [Codepen](https://codepen.io/)** 💻 (@harlanlewis)
|
||||
- **[SVG Drawing](../docs/pixels/feature_svg_drawing.png)** - draw with AI 🎨
|
||||
- Chats: multiple chats, AI titles, Import/Export, Selection mode
|
||||
- Rendering: Markdown, SVG, improved Code blocks
|
||||
|
||||
@@ -80,6 +80,27 @@ and then are send to the upstream AI services.
|
||||
|
||||

|
||||
|
||||
### Direct Connection (Browser → AI Service)
|
||||
|
||||
Most AI services offer a **Direct Connection** toggle (under a service's Advanced settings). When enabled, the browser calls the AI provider's API directly, skipping the Big-AGI server entirely.
|
||||
|
||||
Benefits:
|
||||
|
||||
- **No 4.5 MB upload limit** - the Vercel body-size cap does not apply, so larger attachments and long prompts go through.
|
||||
- **No 300-second timeout** - the Vercel function timeout does not apply, so long-running generations keep streaming.
|
||||
- **More privacy** - connection metadata (IP, timestamp, edge region, Vercel telemetry) is not observable by the Big-AGI edge server.
|
||||
|
||||
Tradeoff:
|
||||
|
||||
- **Slightly more downlink bandwidth**: when traffic passes through the Big-AGI edge, repetitive streaming frames are compacted; direct streams arrive verbatim from the provider.
|
||||
|
||||
Availability requires both:
|
||||
|
||||
1. The API key is set in your browser (client-side), not via server environment variables. Server-key deployments cannot use Direct Connection because the browser has no credential to send.
|
||||
2. The AI service allows CORS (browser-origin requests). Most major providers do; Big-AGI sets any extra headers they require.
|
||||
|
||||
Direct Connection is a net win on speed, limits, and privacy whenever the provider permits it.
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
**Basic Security**:
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
|
||||
Quick answers to common questions about Big-AGI. For detailed documentation, see our [Website Docs](https://big-agi.com/docs).
|
||||
|
||||
### Connectivity
|
||||
|
||||
<details open>
|
||||
<summary><b>What is "Direct Connection" and should I enable it?</b></summary>
|
||||
|
||||
Direct Connection lets the browser call the AI provider's API directly, skipping the Big-AGI edge server. It appears as a toggle in each AI service's Advanced settings when your API key is set client-side.
|
||||
|
||||
**When available, it is a net win**: faster, fewer restrictions, more privacy.
|
||||
|
||||
- **No 4.5 MB upload limit** (Vercel body-size cap does not apply).
|
||||
- **No 300-second timeout** (Vercel function timeout does not apply; call length is bound only by the AI service).
|
||||
- **More privacy** - connection metadata (IP, timestamp, edge region, Vercel telemetry) is not observable by the Big-AGI edge server.
|
||||
- **Slightly more downlink bandwidth** - when passing through the edge, Big-AGI sheds repetitive streaming frames; direct streams arrive verbatim.
|
||||
|
||||
**When it is unavailable**:
|
||||
|
||||
1. **Server-side keys** - if the deployment stores API keys in server environment variables, the browser has no credential to send directly.
|
||||
2. **Provider does not allow CORS** - browsers cannot call APIs that block cross-origin requests. Most major providers permit it; Big-AGI sets any required headers.
|
||||
</details>
|
||||
|
||||
### Versions
|
||||
|
||||
<details open>
|
||||
|
||||
@@ -14,4 +14,10 @@ const compat = new FlatCompat({
|
||||
|
||||
export default defineConfig([{
|
||||
extends: compat.extends("next/core-web-vitals"),
|
||||
rules: {
|
||||
//
|
||||
"react-hooks/exhaustive-deps": ["warn", {
|
||||
additionalHooks: "(useMemoShallowStable)",
|
||||
}],
|
||||
},
|
||||
}]);
|
||||
@@ -17,6 +17,13 @@ Architecture and system documentation is available in the `/kb/` knowledge base,
|
||||
#### CSF - Client-Side Fetch
|
||||
- **[CSF.md](systems/client-side-fetch.md)** - Direct browser-to-API communication for LLM requests
|
||||
|
||||
#### LLM - Language Model Metadata
|
||||
- **[LLM-editorial-control.md](modules/LLM-editorial-pubdate.md)** - Where we have editorial control over per-model metadata vs dynamic discovery; `pubDate` field semantics, propagation chain, resolution rules, per-vendor matrix
|
||||
- **[LLM-models-catalog-pipeline.md](modules/LLM-models-catalog-pipeline.md)** - Forward-looking pipeline: extraction script, snapshot artifact, website consumption, future schema extensions
|
||||
|
||||
#### LLM - Vendor APIs
|
||||
- **[LLM-gemini-interactions.md](modules/LLM-gemini-interactions.md)** - Gemini Interactions API (Deep Research): endpoints, status taxonomy, two retrieval paths (SSE replay vs JSON GET), known failure modes (10-min cuts, zombies), UI surface
|
||||
|
||||
### Systems Documentation
|
||||
|
||||
#### Core Platform Systems
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# LLM Editorial Control Surface
|
||||
|
||||
This document maps where Big-AGI has editorial control over per-model metadata (and therefore can guarantee fields like `pubDate`, curated `description`, `chatPrice`, `benchmark`, `parameterSpecs`, etc.) versus where it must rely on the vendor API's dynamic discovery (and therefore cannot guarantee them).
|
||||
|
||||
For the forward-looking pipeline (extraction script, snapshot, website consumption, future schema extensions), see [LLM-models-catalog-pipeline.md](LLM-models-catalog-pipeline.md).
|
||||
|
||||
|
||||
## The `pubDate` field
|
||||
|
||||
`pubDate?: string` (validated as `/^\d{8}$/`, e.g. `'20250929'`) is **optional** in the wire schema and on `DLLM`. It was added to:
|
||||
|
||||
- `ModelDescription_schema` in `src/modules/llms/server/llm.server.types.ts` - the canonical wire type
|
||||
- `OrtVendorLookupResult` in the same file - so OpenRouter inherits it via `llmOrt*Lookup`
|
||||
- `DLLM` in `src/common/stores/llms/llms.types.ts` - the persisted client model
|
||||
|
||||
### Where `pubDate` is guaranteed (always emitted)
|
||||
|
||||
- **Editorial entries** in 12 hybrid/editorial vendors (282 models). Hand-curated, externally corroborated. Future entries in these arrays are expected to include `pubDate`.
|
||||
- **Anthropic 0-day placeholder** (`llmsAntCreatePlaceholderModel`): when the API surfaces an Anthropic model not in the editorial list, the placeholder uses the API's `created_at` ISO date, falling back to today via `formatPubDate()`.
|
||||
- **Gemini 0-day fallback** (`geminiModelToModelDescription`): when the API returns a Gemini model not in `_knownGeminiModels`, the converter falls back to today via `formatPubDate()` (Gemini API does not expose a creation timestamp).
|
||||
|
||||
### Where `pubDate` is omitted (optional)
|
||||
|
||||
- **Symlink entries** (`KnownLink`) - inherit the target's `pubDate` via the merge logic in `fromManualMapping`.
|
||||
- **Unknown variants resolved through `super`/`fallback`** in `fromManualMapping` for non-Anthropic/non-Gemini vendors - the field is left undefined rather than fabricated.
|
||||
- **Dynamic-only vendors** (OpenRouter, TogetherAI, Novita, ChutesAI, FireworksAI, TLUS, Azure, LM Studio, LocalAI, FastAPI, ArceeAI, LLMAPI) - no editorial knob; pubDate flows in only when the underlying lookup or upstream API populates it.
|
||||
|
||||
The rationale: today's date is a defensible 0-day proxy only when we know we're seeing a brand-new model the vendor just announced (Anthropic and Gemini's "discovery via official model list" paths). For arbitrary dynamic vendors, fabricating today would mark old/well-known models as new - misleading. Better to omit.
|
||||
|
||||
### Propagation chain
|
||||
|
||||
- `fromManualMapping()` in `src/modules/llms/server/models.mappings.ts` - copies the field for OAI-style vendors when present
|
||||
- `geminiModelToModelDescription()` in `src/modules/llms/server/gemini/gemini.models.ts` - copies for Gemini, falls back to today for unknowns
|
||||
- `llmsAntCreatePlaceholderModel()` in `src/modules/llms/server/anthropic/anthropic.models.ts` - emits from API `created_at` (or today)
|
||||
- `_mergeLookup()` in `src/modules/llms/server/openai/models/openrouter.models.ts` - merges for OpenRouter cross-vendor inheritance
|
||||
- `_createDLLMFromModelDescription()` in `src/modules/llms/llm.client.ts` - copies onto the persisted DLLM when present
|
||||
- `formatPubDate()` helper in `src/modules/llms/server/models.mappings.ts` - shared `'YYYYMMDD'` formatter for the 0-day-fillable paths
|
||||
|
||||
### Semantics
|
||||
|
||||
`pubDate` is the **earliest public availability** of the model - the date on which the vendor first made this specific model usable by external users via any channel (consumer app, web, console, API, partner, open-weights upload).
|
||||
|
||||
It is **not**:
|
||||
|
||||
- The date Big-AGI added the entry to its catalog (Ollama uses `added` for that)
|
||||
- The training-data cutoff (proposed but not implemented; see `src/common/stores/llms/llms.types.next.ts:217`)
|
||||
- The date the model snapshot was built (suffixes like `-1212` may refer to build dates, but `pubDate` tracks public availability)
|
||||
|
||||
### Resolution rules (when sources conflict)
|
||||
|
||||
1. **Date-suffixed model IDs**: when the suffix matches a documented announcement, the suffix is canonical (vendor convention). xAI, OpenAI, and Mistral all use suffixes that closely track release dates.
|
||||
2. **Anthropic exception**: Anthropic's date suffixes are typically the **snapshot/training-cutoff date, not the public release date**. For example, `claude-3-7-sonnet-20250219` was released on 2025-02-24, `claude-opus-4-20250514` was released 2025-05-22, and `claude-haiku-4-5-20251001` was released 2025-10-15. Always corroborate against Anthropic's blog/press for the actual release date. Only `claude-sonnet-4-5-20250929` and `claude-opus-4-1-20250805` have suffixes that match.
|
||||
3. **Closed beta -> public beta -> GA**: use the first date *external* users could access the specific variant.
|
||||
4. **Family-headline IDs and dated snapshots** (e.g., `claude-opus-4-1` and `claude-opus-4-1-20250805`): typically share a release date.
|
||||
5. **Hosted on a third party** (Groq hosting Llama, OpenPipe mirroring others, OpenRouter aggregating): use the *underlying* model's original release date by its creator, not when the host added it.
|
||||
6. **Symlinks** (entries with `symLink:`): inherit the target's date.
|
||||
7. **Partial dates** (only month known): use the 1st of the month and tag as MEDIUM confidence in the editor's note.
|
||||
|
||||
|
||||
## Editorial control matrix
|
||||
|
||||
Three categories:
|
||||
|
||||
- **Editorial** - the vendor file contains hand-curated entries; we control descriptions, pricing, benchmarks, interfaces, parameter specs, and `pubDate`.
|
||||
- **Hybrid** - the API returns the live model list, and editorial entries (keyed by id/idPrefix) merge over the API data via `fromManualMapping`. We control everything except *which models exist*.
|
||||
- **Dynamic** - the API is the only source of model identity and metadata. Big-AGI cannot reliably populate `pubDate` here (no editorial knob).
|
||||
|
||||
| Vendor | Category | File | Array | Entries | `pubDate` populated |
|
||||
|---|---|---|---|---|---|
|
||||
| Anthropic | Hybrid | `anthropic/anthropic.models.ts` | `hardcodedAnthropicModels` | 12 | 12/12 HIGH |
|
||||
| Gemini | Hybrid | `gemini/gemini.models.ts` | `_knownGeminiModels` | 33 | 33/33 HIGH |
|
||||
| OpenAI | Hybrid | `openai/models/openai.models.ts` | `_knownOpenAIChatModels` | 96 | 95/96 HIGH/MED (`osb-120b` skipped, speculative) |
|
||||
| xAI | Hybrid | `openai/models/xai.models.ts` | `_knownXAIChatModels` | 13 | 13/13 HIGH (pilot) |
|
||||
| Mistral | Hybrid | `openai/models/mistral.models.ts` | `_knownMistralModelDetails` | 41 | 41/41 (40 HIGH, 1 MED for legacy `mistral-medium`) |
|
||||
| Moonshot (Kimi) | Hybrid | `openai/models/moonshot.models.ts` | `_knownMoonshotModels` | 13 | 13/13 (10 HIGH, 3 MED for v1 base models) |
|
||||
| Perplexity | Editorial | `openai/models/perplexity.models.ts` | `_knownPerplexityChatModels` | 4 | 4/4 HIGH |
|
||||
| MiniMax | Editorial | `openai/models/minimax.models.ts` | `_knownMiniMaxModels` | 10 | 10/10 HIGH |
|
||||
| DeepSeek | Hybrid | `openai/models/deepseek.models.ts` | `_knownDeepseekChatModels` | 4 | 4/4 HIGH |
|
||||
| Groq | Hybrid (host) | `openai/models/groq.models.ts` | `_knownGroqModels` | 11 | 11/11 HIGH (underlying-model date) |
|
||||
| Z.AI / GLM | Hybrid | `openai/models/zai.models.ts` | `_knownZAIModels` | 17 | 16/17 (`glm-5-code` UNCONFIRMED) |
|
||||
| OpenPipe | Editorial (mirror) | `openai/models/openpipe.models.ts` | `_knownOpenPipeChatModels` | 30 | 30/30 HIGH (all upstream-mirror, no OpenPipe originals) |
|
||||
| Bedrock | Reuses Anthropic | `bedrock/bedrock.models.ts` | -> `hardcodedAnthropicModels` | (12) | inherited |
|
||||
| Ollama | Editorial (catalog) | `ollama/ollama.models.ts` | `OLLAMA_BASE_MODELS` | 209 | **deferred** - see notes |
|
||||
| Arcee AI | Dynamic | `openai/models/arceeai.models.ts` | `_arceeKnownModels` | 0 | n/a (empty) |
|
||||
| LLMAPI | Dynamic | `openai/models/llmapi.models.ts` | `_llmapiKnownModels` | 0 | n/a (empty) |
|
||||
| Alibaba | Dynamic | `openai/models/alibaba.models.ts` | `_knownAlibabaChatModels` | 0 | n/a (empty) |
|
||||
| OpenRouter | Dynamic + delegated lookup | `openai/models/openrouter.models.ts` | (parser) | -- | inherited via `llmOrt*Lookup` |
|
||||
| TogetherAI | Dynamic | `openai/models/together.models.ts` | (parser) | -- | no |
|
||||
| FireworksAI | Dynamic | `openai/models/fireworksai.models.ts` | (parser) | -- | no |
|
||||
| Novita | Dynamic | `openai/models/novita.models.ts` | (parser) | -- | no |
|
||||
| ChutesAI | Dynamic | `openai/models/chutesai.models.ts` | (parser) | -- | no |
|
||||
| TLUS | Dynamic | `openai/models/tlusapi.models.ts` | (parser) | -- | no |
|
||||
| Azure | Dynamic | `openai/models/azure.models.ts` | (parser) | -- | no |
|
||||
| LM Studio | Dynamic | `openai/models/lmstudio.models.ts` | (parser) | -- | no |
|
||||
| LocalAI | Dynamic | `openai/models/localai.models.ts` | (parser) | -- | no |
|
||||
| FastAPI | Dynamic | `openai/models/fastapi.models.ts` | (parser) | -- | no |
|
||||
|
||||
**Totals**: 284 editorial entries across 12 vendors, of which **282** have corroborated `pubDate` and **2** are intentional gaps (`osb-120b` speculative, `glm-5-code` not yet announced). All 12 vendor files type-check clean.
|
||||
|
||||
### Notes
|
||||
|
||||
- **Hybrid** vendors are still effectively editorial for the models we know about: when an API id matches a hardcoded `idPrefix` (or `id`), `fromManualMapping` injects all the editorial fields. Unknown ids fall through to a default-shaped placeholder where `pubDate` is undefined.
|
||||
- **OpenRouter** delegates back to Anthropic / Gemini / OpenAI editorial lookups via `llmOrtAntLookup_ThinkingVariants`, `llmOrtGemLookup`, `llmOrtOaiLookup`. `pubDate` flows through these lookups, so OpenRouter-served Claude/Gemini/GPT models get `pubDate` automatically once the underlying editorial entry has it.
|
||||
- **Bedrock** finds Anthropic editorial via `llmBedrockFindAnthropicModel` and strips unsupported interfaces - `pubDate` inherits from Anthropic.
|
||||
- **Ollama** is deferred: 209 entries keyed by upstream model family (e.g. `qwen3.6`, `kimi-k2`, `glm-4.6`). Each entry's `pubDate` would need to be the upstream creator's release date (Meta, Alibaba, Moonshot, Z.AI, etc.). This is large-scale upstream research; better handled in a follow-up pass once cross-vendor `pubDate` data is consolidated and reusable.
|
||||
- **Dynamic-only** vendors get nothing automatic. To add `pubDate` for them we'd have to seed editorial entries (which is what `fromManualMapping`'s mapping mechanism was built for); this is a per-vendor decision and out of scope for the initial rollout.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Gemini Interactions API
|
||||
|
||||
The Interactions API powers Gemini's agent runs (Deep Research today, more agent types planned). This doc is the source of truth for protocol shape, failure modes, and the recovery model — code comments link here instead of repeating the rationale.
|
||||
|
||||
## References
|
||||
|
||||
- **GH [#1088](https://github.com/enricoros/big-AGI/issues/1088)** — Auto-resume for Deep Research; Recover button
|
||||
- **GH [#1095](https://github.com/enricoros/big-AGI/issues/1095)** — Visualizations toggle (`agent_config.visualization`)
|
||||
- **Google forum [143098](https://discuss.ai.google.dev/t/interactions-api-connection-breaks-at-the-10-minutes-mark/143098)** — 10-min SSE cut
|
||||
- **Google forum [143099](https://discuss.ai.google.dev/t/streaming-resume-broken-on-interactions-api-deep-research-often-cannot-resume/143099)** — Streaming resume re-cuts
|
||||
- **Upstream specs** — `_upstream/gemini.interactions.spec.md`, `gemini.interactions.guide.md`, `gemini.deep-research.guide.md`
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Verb | Path | Purpose |
|
||||
|--------|-------------------------------------------|-------------------------------------------------------------------|
|
||||
| POST | `/v1beta/interactions` | Start a run. We always send `stream:true, background:true, store:true` |
|
||||
| GET | `/v1beta/interactions/{id}?stream=true` | Reattach via SSE replay (full event sequence from start) |
|
||||
| GET | `/v1beta/interactions/{id}` | Fetch the resource as JSON (one-shot) |
|
||||
| POST | `/v1beta/interactions/{id}/cancel` | Stop a background run |
|
||||
| DELETE | `/v1beta/interactions/{id}` | Remove the stored record (does NOT cancel an in-flight run) |
|
||||
|
||||
Retention: 1 day free, 55 days paid.
|
||||
|
||||
## Status taxonomy
|
||||
|
||||
| Status | Meaning | Handling |
|
||||
|-------------------|-----------------------------------------------|-------------------------------------------------------|
|
||||
| `in_progress` | Live run **or** zombie (see C) | Surface diagnostics; offer Resume/Recover/Stop |
|
||||
| `completed` | Done with content in `outputs[]` | Emit fragments, `tokenStopReason='ok'` |
|
||||
| `failed` | Server-side failure | Terminating issue |
|
||||
| `cancelled` | We or another client cancelled | Close as `cg-issue` |
|
||||
| `incomplete` | Stopped early (token limit) — partial outputs | Note + `tokenStopReason='out-of-tokens'` |
|
||||
| `requires_action` | Not expected for Deep Research | Fail loudly so we notice |
|
||||
|
||||
## Two retrieval paths
|
||||
|
||||
| Path | Endpoint | Parser | Use case |
|
||||
|-----------------------|-----------------------------------|-------------------------------------------|-----------------------------------|
|
||||
| SSE replay | `GET ?stream=true` | `createGeminiInteractionsParserSSE` | Canonical resume; live deltas |
|
||||
| JSON GET (recovery) | `GET` (no `stream`) | `createGeminiInteractionsParserNS` | Recover when SSE is broken |
|
||||
|
||||
Both replay from the start — `ContentReassembler` REPLACES content on reattach, so partial replay (`last_event_id`) is intentionally NOT used. The NS parser walks `outputs[]` (thoughts, text, images, audio) and emits the same particles the SSE parser would, in one batch.
|
||||
|
||||
## Failure modes
|
||||
|
||||
### A. 10-minute SSE cut (forum 143098)
|
||||
|
||||
The SSE connection gets cut at exactly 600 s, regardless of activity. The cut is malformed (JSON error array instead of clean SSE close) and we treat it as stream-closed-early. The run typically **continues** server-side and reaches `completed`. **Recover (JSON GET)** retrieves the full report.
|
||||
|
||||
### B. Streaming resume re-cuts (forum 143099)
|
||||
|
||||
A fresh SSE replay can re-cut at the same 10-minute boundary on long runs, so Resume alone never reaches `interaction.complete`. **Recover** is the fallback.
|
||||
|
||||
### C. Zombie interactions (#1088)
|
||||
|
||||
Resource sits in `status: in_progress` for **days** with `outputs: []` — the generator crashed but the status never transitioned. **Not recoverable** (no data was ever produced). The NS parser surfaces `created`, `updated`, output count, and a "stuck for over an hour" hint so the user can decide to delete and retry.
|
||||
|
||||
### D. Connection drop mid-run
|
||||
|
||||
Network blip; resource is fine. **Resume (SSE replay)** picks up cleanly.
|
||||
|
||||
## UI
|
||||
|
||||
`BlockOpUpstreamResume` renders up to three buttons:
|
||||
|
||||
| Button | Action | Shown when |
|
||||
|----------|-----------------------------------|---------------------------------------------------------|
|
||||
| Resume | SSE replay | `onResume` provided |
|
||||
| Recover | JSON GET (one-shot) | `upstreamHandle.uht` ∈ `_NS_RECOVER_UHTS` |
|
||||
| Stop | Cancel + delete upstream resource | `onDelete` provided |
|
||||
|
||||
The Recover gate is an inline `uht === 'vnd.gem.interactions'` check in `BlockOpUpstreamResume.tsx` — extend when another vendor needs the same fallback. Stop is intentionally NOT gated by Resume/Recover busy state — it's the escape hatch for hung resumes.
|
||||
|
||||
## Visualization control (#1095)
|
||||
|
||||
Deep Research accepts `agent_config.visualization: 'auto' | 'off'`. Exposed as `llmVndGeminiAgentViz` (label "Visualizations"). Forwarded only when explicitly `'off'` so the upstream `'auto'` default stays untouched. Useful when merging multiple reports — image fragments break Beam fusion.
|
||||
|
||||
## Code map
|
||||
|
||||
| File | Role |
|
||||
|--------------------------------------------------------------------------------------|-------------------------------------------------------|
|
||||
| `aix/server/dispatch/wiretypes/gemini.interactions.wiretypes.ts` | Zod schemas (RequestBody, Interaction, StreamEvent) |
|
||||
| `aix/server/dispatch/chatGenerate/adapters/gemini.interactionsCreate.ts` | POST body (input + agent_config) |
|
||||
| `aix/server/dispatch/chatGenerate/parsers/gemini.interactions.parser.ts` | SSE parser + NS parser |
|
||||
| `aix/server/dispatch/chatGenerate/chatGenerate.dispatch.ts` (`gemini` case) | Resume dispatch: SSE vs JSON branch |
|
||||
| `apps/chat/components/message/BlockOpUpstreamResume.tsx` | Resume / Recover / Stop UI |
|
||||
| `apps/chat/components/ChatMessageList.tsx` (`handleMessageUpstreamResume`) | Wires click handler to `aixReattachContent_DMessage_orThrow` |
|
||||
@@ -0,0 +1,78 @@
|
||||
# LLM Models Catalog Pipeline (forward-looking)
|
||||
|
||||
Status: **proposal / partially implemented**. Companion to [LLM-editorial-control.md](LLM-editorial-pubdate.md) which describes the durable reference (`pubDate` semantics, editorial-vs-dynamic matrix, propagation chain).
|
||||
|
||||
This document captures the forward-looking pipeline that turns Big-AGI's editorial model metadata into website value-add (plots, decision helpers, comparison tools at big-agi.com).
|
||||
|
||||
|
||||
## Goal
|
||||
|
||||
Stand up a database/datastore that the website (`~/dev/website`) can query for plots, decision helpers, and comparison tools - without requiring the website to call our authenticated tRPC endpoints.
|
||||
|
||||
|
||||
## Stages
|
||||
|
||||
### Stage 1: source of truth (in this repo) — DONE
|
||||
|
||||
Editorial files in `src/modules/llms/server/` remain the canonical source for:
|
||||
|
||||
- Identity: id, label, vendor
|
||||
- Capabilities: `interfaces`, `parameterSpecs`, `contextWindow`, `maxCompletionTokens`
|
||||
- Pricing: `chatPrice` (input / output / cache tiers)
|
||||
- Benchmarks: `benchmark.cbaElo` (Chat Bot Arena ELO)
|
||||
- Lifecycle: `pubDate`, `isLegacy`, `isPreview`, `hidden`, deprecation comments
|
||||
|
||||
Well-typed, version-controlled, reviewed - every model edit is a code change with diff history. 282 entries currently carry `pubDate` (see editorial-control matrix).
|
||||
|
||||
### Stage 2: extraction script — IN PROGRESS
|
||||
|
||||
A build-time script (e.g. `scripts/llms/export-models.ts`) that:
|
||||
|
||||
1. Loads every editorial vendor's model array.
|
||||
2. Normalizes per-vendor shapes (array vs Record, `id` vs `idPrefix`, `KnownLink` symlinks) to a single row format.
|
||||
3. Resolves symlinks (target's `pubDate` flows through).
|
||||
4. Writes a single JSON snapshot: `data/models-catalog.json` (one row per model, with vendor + the editorial fields above).
|
||||
|
||||
Open question: do we want this committed (gives the website a stable artifact / public URL) or built on-demand in CI? **Recommend committed snapshot** under `data/` so consumers get a stable URL.
|
||||
|
||||
### Stage 3: enrichment — NOT STARTED
|
||||
|
||||
The exported snapshot gets enriched with data we don't currently track in editorial files:
|
||||
|
||||
- **Knowledge cutoff** (proposed in `llms.types.next.ts:217` but never implemented; should be added to `ModelDescription_schema` as a follow-up).
|
||||
- **MMLU / HumanEval / SWE-bench / GPQA / MATH** scores (currently only `cbaElo`; richer benchmarks belong in a separate block).
|
||||
- **Throughput / latency** numbers (per-vendor, possibly per-region).
|
||||
- **Modalities matrix** (input image, input audio, input video, input PDF, output image, output audio).
|
||||
- **Weights availability** (closed / open / restricted), license.
|
||||
|
||||
Sources for enrichment: HuggingFace cards, vendor docs, Artificial Analysis, LLM-Stats, official benchmarks. Some can be scraped on a cadence; some needs editorial review.
|
||||
|
||||
### Stage 4: website consumption — NOT STARTED
|
||||
|
||||
The website (`~/dev/website`) consumes the snapshot to render:
|
||||
|
||||
- **Timeline plot**: `pubDate` (x-axis) vs `cbaElo` (y-axis), grouped by vendor - shows the frontier and rate of progress.
|
||||
- **Cost-per-quality plot**: `chatPrice.output` vs `cbaElo` - "best model per dollar".
|
||||
- **Decision helpers**: filter by capability (`interfaces`), context window, pricing tier, vendor.
|
||||
- **Comparison cards**: side-by-side specs.
|
||||
- **Lifecycle alerts**: deprecation warnings for retiring models.
|
||||
|
||||
|
||||
## Open questions
|
||||
|
||||
1. **Where does enrichment data live?** A separate `data/models-enrichment.json` (joined by id at build time) keeps editorial files clean but introduces a join surface. Alternative: extend `ModelDescription_schema` with optional enrichment fields and treat editorial files as the only source. Recommend the separate file approach - editorial files stay focused on vendor-API integration; enrichment evolves on a different cadence.
|
||||
2. **How fresh does the website need to be?** If daily, build the snapshot in CI on push and publish to a static URL. If real-time, consume tRPC directly - more work but fewer freshness gaps.
|
||||
3. **Do we expose `pubDate` and other editorial metadata via tRPC publicly, or only via the snapshot?** The current tRPC routes require auth; the website should consume the snapshot, not live tRPC.
|
||||
4. **Schema versioning** - if `ModelDescription_schema` evolves, the snapshot consumers need to be tolerant. Include a `schemaVersion` field in the snapshot envelope.
|
||||
|
||||
|
||||
## Future extensions to `ModelDescription_schema`
|
||||
|
||||
Beyond `pubDate`, the natural follow-ups (in priority order):
|
||||
|
||||
1. **`knowledgeCutoff?: string`** (`'YYYY-MM'` or `'YYYY-MM-DD'`) - already proposed in `llms.types.next.ts`. Useful for the timeline plot and for context-aware prompts.
|
||||
2. **`deprecationDate?: string`** - currently exists informally as `deprecated?: string` on `_knownGeminiModels`; should be promoted to the schema.
|
||||
3. **`license?: string`** - especially important for open-weights models (apache-2.0, mit, llama-community, custom).
|
||||
4. **`weights?: 'closed' | 'open' | 'restricted'`** - quick filter for "can I run this myself?".
|
||||
5. **`benchmarks?: { mmlu?: number, humaneval?: number, gpqa?: number, ... }`** - richer than the current `cbaElo`-only block.
|
||||
6. **`modalities?: { in: string[], out: string[] }`** - more precise than `interfaces` for input/output capability matrices.
|
||||
@@ -1,6 +1,22 @@
|
||||
# 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.
|
||||
Client-Side Fetch (CSF), surfaced to users as **"Direct Connection"**, 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.
|
||||
|
||||
## User-facing tradeoffs (Direct Connection vs via-server)
|
||||
|
||||
Wins when Direct Connection is on:
|
||||
- **No 4.5MB upload limit** (Vercel body-size cap does not apply to direct browser-to-API requests).
|
||||
- **No 300s function timeout** (Vercel serverless/edge timeout does not apply; call duration is bound only by the AI service).
|
||||
- **More privacy**: connection metadata (IP, timestamp, edge region, Vercel telemetry) is not observable by the Big-AGI edge server.
|
||||
|
||||
Costs:
|
||||
- **Slightly more downlink bandwidth**: when traffic passes through the Big-AGI server, repetitive streaming frames are shed/compacted; direct streams arrive verbatim.
|
||||
|
||||
Availability requires both:
|
||||
1. The API key is on the **client** (localStorage), not a server-side env var. Server-key deployments cannot use CSF because the browser has no credential to send.
|
||||
2. The AI service **allows CORS** from browsers. Most major providers do; some require specific headers which Big-AGI sets.
|
||||
|
||||
Net: Direct Connection is a win on speed, limits, and privacy whenever the provider permits it. It is unavailable when keys are server-side or the provider blocks browser-origin requests.
|
||||
|
||||
## Implementation
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ 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})`);
|
||||
console.log(` 🧠 \x1b[1mbig-AGI\x1b[0m v${process.env.NEXT_PUBLIC_BUILD_PKGVER} (@${process.env.NEXT_PUBLIC_BUILD_HASH}${process.env.VERCEL_ENV ? `, \x1b[2mV:\x1b[0m${process.env.VERCEL_ENV}` : ''}, \x1b[2mN:\x1b[0m${process.env.NODE_ENV})`);
|
||||
|
||||
// Non-default build types
|
||||
const buildType =
|
||||
|
||||
Generated
+189
-183
@@ -44,10 +44,10 @@
|
||||
"next": "~15.1.12",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "5.4.54",
|
||||
"posthog-js": "^1.360.2",
|
||||
"posthog-node": "^5.28.2",
|
||||
"posthog-js": "^1.369.0",
|
||||
"posthog-node": "^5.29.2",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer-core": "^24.39.1",
|
||||
"puppeteer-core": "^24.40.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.71.2",
|
||||
@@ -69,7 +69,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@posthog/nextjs-config": "~1.6.4",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/prismjs": "^1.26.6",
|
||||
"@types/react": "^19.2.14",
|
||||
@@ -77,12 +77,12 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "~15.1.12",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier": "^3.8.2",
|
||||
"prisma": "~5.22.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^6.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.0.0 || ^22.0.0 || ^20.0.0"
|
||||
@@ -253,7 +253,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@dnd-kit/accessibility": "^3.1.1",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -395,7 +394,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -459,7 +457,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz",
|
||||
"integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
@@ -997,15 +994,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/config-array": {
|
||||
"version": "0.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
|
||||
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
|
||||
"version": "0.21.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz",
|
||||
"integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/object-schema": "^2.1.7",
|
||||
"debug": "^4.3.1",
|
||||
"minimatch": "^3.1.2"
|
||||
"minimatch": "^3.1.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -1038,20 +1035,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/eslintrc": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
|
||||
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
|
||||
"integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ajv": "^6.12.4",
|
||||
"ajv": "^6.14.0",
|
||||
"debug": "^4.3.2",
|
||||
"espree": "^10.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"ignore": "^5.2.0",
|
||||
"import-fresh": "^3.2.1",
|
||||
"js-yaml": "^4.1.1",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^3.1.5",
|
||||
"strip-json-comments": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1062,9 +1059,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz",
|
||||
"integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==",
|
||||
"version": "9.39.4",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz",
|
||||
"integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -2550,7 +2547,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
@@ -3389,9 +3385,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/types": {
|
||||
"version": "1.360.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.360.2.tgz",
|
||||
"integrity": "sha512-U48CbtmX5kETZvWjaJVlublSA1aLV99m71TQtgxWksBMXINS/3C7j+KqlMO6wH7SuaEZQnjaxh1KYGH4nRCaaA==",
|
||||
"version": "1.369.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.369.0.tgz",
|
||||
"integrity": "sha512-iFkLrg/+QRQHc8KSjO9parN6H3rftM2ld2sCJ/GeBRRO9MJYCdc6jXSFRgF7aGJPzb79syqksz+CrnBzdZnBKg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@posthog/webpack-plugin": {
|
||||
@@ -3687,7 +3683,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@svta/cml-xml/-/cml-xml-1.0.1.tgz",
|
||||
"integrity": "sha512-11LkJa5kDEcsRMWkVI1ABH3KLCxGoiSVe4kQ293ItVj8ncTTQ7htmCGiJDjS+Cmy35UgF3e/vc0ysJIiWRTx2g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
@@ -3725,7 +3720,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
|
||||
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@tanstack/query-core": "5.90.20"
|
||||
},
|
||||
@@ -3778,7 +3772,6 @@
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@trpc/server": "11.5.1",
|
||||
"typescript": ">=5.7.2"
|
||||
@@ -3819,7 +3812,6 @@
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@tanstack/react-query": "^5.80.3",
|
||||
"@trpc/client": "11.5.1",
|
||||
@@ -3837,7 +3829,6 @@
|
||||
"https://trpc.io/sponsor"
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"typescript": ">=5.7.2"
|
||||
}
|
||||
@@ -3922,12 +3913,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||
"version": "25.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
|
||||
"integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
"undici-types": "~7.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nprogress": {
|
||||
@@ -3961,7 +3952,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -3991,6 +3981,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
||||
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@@ -4026,20 +4017,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
|
||||
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.0.tgz",
|
||||
"integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/type-utils": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"@typescript-eslint/scope-manager": "8.58.0",
|
||||
"@typescript-eslint/type-utils": "8.58.0",
|
||||
"@typescript-eslint/utils": "8.58.0",
|
||||
"@typescript-eslint/visitor-keys": "8.58.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4049,9 +4040,9 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.54.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
"@typescript-eslint/parser": "^8.58.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
@@ -4065,17 +4056,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.0.tgz",
|
||||
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"@typescript-eslint/scope-manager": "8.58.0",
|
||||
"@typescript-eslint/types": "8.58.0",
|
||||
"@typescript-eslint/typescript-estree": "8.58.0",
|
||||
"@typescript-eslint/visitor-keys": "8.58.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4086,19 +4076,19 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
|
||||
"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.0.tgz",
|
||||
"integrity": "sha512-8Q/wBPWLQP1j16NxoPNIKpDZFMaxl7yWIoqXWYeWO+Bbd2mjgvoF0dxP2jKZg5+x49rgKdf7Ck473M8PC3V9lg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.54.0",
|
||||
"@typescript-eslint/types": "^8.54.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.58.0",
|
||||
"@typescript-eslint/types": "^8.58.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4109,18 +4099,18 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
|
||||
"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.0.tgz",
|
||||
"integrity": "sha512-W1Lur1oF50FxSnNdGp3Vs6P+yBRSmZiw4IIjEeYxd8UQJwhUF0gDgDD/W/Tgmh73mxgEU3qX0Bzdl/NGuSPEpQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0"
|
||||
"@typescript-eslint/types": "8.58.0",
|
||||
"@typescript-eslint/visitor-keys": "8.58.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4131,9 +4121,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
|
||||
"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.0.tgz",
|
||||
"integrity": "sha512-doNSZEVJsWEu4htiVC+PR6NpM+pa+a4ClH9INRWOWCUzMst/VA9c4gXq92F8GUD1rwhNvRLkgjfYtFXegXQF7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4144,21 +4134,21 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
|
||||
"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.0.tgz",
|
||||
"integrity": "sha512-aGsCQImkDIqMyx1u4PrVlbi/krmDsQUs4zAcCV6M7yPcPev+RqVlndsJy9kJ8TLihW9TZ0kbDAzctpLn5o+lOg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0",
|
||||
"@typescript-eslint/types": "8.58.0",
|
||||
"@typescript-eslint/typescript-estree": "8.58.0",
|
||||
"@typescript-eslint/utils": "8.58.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4168,14 +4158,14 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
|
||||
"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.0.tgz",
|
||||
"integrity": "sha512-O9CjxypDT89fbHxRfETNoAnHj/i6IpRK0CvbVN3qibxlLdo5p5hcLmUuCCrHMpxiWSwKyI8mCP7qRNYuOJ0Uww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -4187,21 +4177,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
|
||||
"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.0.tgz",
|
||||
"integrity": "sha512-7vv5UWbHqew/dvs+D3e1RvLv1v2eeZ9txRHPnEEBUgSNLx5ghdzjHa0sgLWYVKssH+lYmV0JaWdoubo0ncGYLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.54.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"@typescript-eslint/project-service": "8.58.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.58.0",
|
||||
"@typescript-eslint/types": "8.58.0",
|
||||
"@typescript-eslint/visitor-keys": "8.58.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^9.0.5",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4211,46 +4201,59 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
|
||||
"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.0.tgz",
|
||||
"integrity": "sha512-RfeSqcFeHMHlAWzt4TBjWOAtoW9lnsAGiP3GbaX9uVgTYYrMbVnGONEfUCiSss+xMHFl+eHZiipmA8WkQ7FuNA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0"
|
||||
"@typescript-eslint/scope-manager": "8.58.0",
|
||||
"@typescript-eslint/types": "8.58.0",
|
||||
"@typescript-eslint/typescript-estree": "8.58.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4260,19 +4263,19 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
|
||||
"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
|
||||
"version": "8.58.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.0.tgz",
|
||||
"integrity": "sha512-XJ9UD9+bbDo4a4epraTwG3TsNPeiB9aShrUneAVXy8q4LuwowN+qu89/6ByLMINqvIMeI9H9hOHQtg/ijrYXzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
"@typescript-eslint/types": "8.58.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
@@ -4282,6 +4285,19 @@
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
@@ -4653,7 +4669,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -4693,9 +4708,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "6.12.6",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
|
||||
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5656,6 +5671,7 @@
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
@@ -5945,15 +5961,13 @@
|
||||
"version": "0.0.1581282",
|
||||
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
|
||||
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/dexie": {
|
||||
"version": "4.0.11",
|
||||
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.11.tgz",
|
||||
"integrity": "sha512-SOKO002EqlvBYYKQSew3iymBoN2EQ4BDw/3yprjh7kAfFzjBYkaMNa/pZvcA7HSWlcKSQb9XhPe3wKyQ0x4A8A==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/dexie-react-hooks": {
|
||||
"version": "1.1.7",
|
||||
@@ -5999,6 +6013,7 @@
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -6459,26 +6474,25 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "9.39.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz",
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"version": "9.39.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz",
|
||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
"@eslint/config-array": "^0.21.1",
|
||||
"@eslint/config-array": "^0.21.2",
|
||||
"@eslint/config-helpers": "^0.4.2",
|
||||
"@eslint/core": "^0.17.0",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@eslint/eslintrc": "^3.3.5",
|
||||
"@eslint/js": "9.39.4",
|
||||
"@eslint/plugin-kit": "^0.4.1",
|
||||
"@humanfs/node": "^0.16.6",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@humanwhocodes/retry": "^0.4.2",
|
||||
"@types/estree": "^1.0.6",
|
||||
"ajv": "^6.12.4",
|
||||
"ajv": "^6.14.0",
|
||||
"chalk": "^4.0.0",
|
||||
"cross-spawn": "^7.0.6",
|
||||
"debug": "^4.3.2",
|
||||
@@ -6497,7 +6511,7 @@
|
||||
"is-glob": "^4.0.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"minimatch": "^3.1.2",
|
||||
"minimatch": "^3.1.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"optionator": "^0.9.3"
|
||||
},
|
||||
@@ -6638,7 +6652,6 @@
|
||||
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.9",
|
||||
@@ -7118,7 +7131,6 @@
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
@@ -8370,6 +8382,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/iterator.prototype": {
|
||||
@@ -9659,9 +9672,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"version": "3.1.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
|
||||
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
@@ -9778,7 +9791,6 @@
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.1.12.tgz",
|
||||
"integrity": "sha512-fClyhVCGTATGYBnETgKAi7YU5+bSwzM5rqNsY3Dg5wBoBMwE0NSvWA3fzwYj0ijl+LMeiV8P2QAnUFpeqDfTgw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@next/env": "15.1.12",
|
||||
"@swc/counter": "0.1.3",
|
||||
@@ -10661,6 +10673,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -10800,9 +10813,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.360.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.360.2.tgz",
|
||||
"integrity": "sha512-/Wed0mOuRUfyEGT/BRQaokCqBlxrEceE7MDT9A00lU5tXo443/2Pg9ZiqN5sucUluZF47hwGORpYPoVUt32UFw==",
|
||||
"version": "1.369.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.369.0.tgz",
|
||||
"integrity": "sha512-fOJzwpCb/TWxJBsUNKJ5PhHNl0+X7zRrVTuUrIH+EfsIfxJGSkOa0lWPJJGr3xzg810JHCUpuhn5sFBBxfHqIQ==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
@@ -10810,8 +10823,8 @@
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
|
||||
"@opentelemetry/resources": "^2.2.0",
|
||||
"@opentelemetry/sdk-logs": "^0.208.0",
|
||||
"@posthog/core": "1.23.4",
|
||||
"@posthog/types": "1.360.2",
|
||||
"@posthog/core": "1.25.2",
|
||||
"@posthog/types": "1.369.0",
|
||||
"core-js": "^3.38.1",
|
||||
"dompurify": "^3.3.2",
|
||||
"fflate": "^0.4.8",
|
||||
@@ -10821,21 +10834,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-js/node_modules/@posthog/core": {
|
||||
"version": "1.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.4.tgz",
|
||||
"integrity": "sha512-gSM1gnIuw5UOBUOTz0IhCTH8jOHoFr5rzSDb5m7fn9ofLHvz3boZT1L1f+bcuk+mvzNJfrJ3ByVQGKmUQnKQ8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
}
|
||||
"version": "1.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.25.2.tgz",
|
||||
"integrity": "sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-node": {
|
||||
"version": "5.28.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.28.2.tgz",
|
||||
"integrity": "sha512-a+unFAKU8Vtez1DAEgCXB/KOZbroQZE+GvnSr9B35u3uMUxtyPO5ulgLJo8AUcZ4prhv6ia8R1Xjr4BrxPfdsA==",
|
||||
"version": "5.29.2",
|
||||
"resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.29.2.tgz",
|
||||
"integrity": "sha512-rI7kkF0XqDc0G1qjx+Hb4iuY9NAlL+XQNoGOpnEpRNTUcXvjY6WlsRGZ9m2whgc39emrrYdszi/YT8wZkr2xsg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/core": "1.23.4"
|
||||
"@posthog/core": "1.25.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.20.0 || >=22.22.0"
|
||||
@@ -10850,13 +10860,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/posthog-node/node_modules/@posthog/core": {
|
||||
"version": "1.23.4",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.4.tgz",
|
||||
"integrity": "sha512-gSM1gnIuw5UOBUOTz0IhCTH8jOHoFr5rzSDb5m7fn9ofLHvz3boZT1L1f+bcuk+mvzNJfrJ3ByVQGKmUQnKQ8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6"
|
||||
}
|
||||
"version": "1.25.2",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.25.2.tgz",
|
||||
"integrity": "sha512-h2FO7ut/BbfwpAXWpwdDHTzQgUo9ibDFEs6ZO+3cI3KPWQt5XwczK1OLAuPprcjm8T/jl0SH8jSFo5XdU4RbTg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/preact": {
|
||||
"version": "10.28.3",
|
||||
@@ -10879,9 +10886,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"version": "3.8.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.2.tgz",
|
||||
"integrity": "sha512-8c3mgTe0ASwWAJK+78dpviD+A8EqhndQPUBpNUIPt6+xWlIigCwfN01lWr9MAede4uqXGTEKeQWTvzb3vjia0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -10901,7 +10908,6 @@
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.22.0"
|
||||
},
|
||||
@@ -11036,9 +11042,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/puppeteer-core": {
|
||||
"version": "24.39.1",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.39.1.tgz",
|
||||
"integrity": "sha512-AMqQIKoEhPS6CilDzw0Gd1brLri3emkC+1N2J6ZCCuY1Cglo56M63S0jOeBZDQlemOiRd686MYVMl9ELJBzN3A==",
|
||||
"version": "24.40.0",
|
||||
"resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.40.0.tgz",
|
||||
"integrity": "sha512-MWL3XbUCfVgGR0gRsidzT6oKJT2QydPLhMITU6HoVWiiv4gkb6gJi3pcdAa8q4HwjBTbqISOWVP4aJiiyUJvag==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@puppeteer/browsers": "2.13.0",
|
||||
@@ -11085,7 +11091,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -11098,7 +11103,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -11203,6 +11207,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
@@ -11677,6 +11682,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
@@ -11689,6 +11695,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -12362,7 +12369,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -12419,9 +12425,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
|
||||
"integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==",
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -12583,11 +12589,10 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
||||
"integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -12657,9 +12662,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"version": "7.19.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
|
||||
"integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
@@ -13015,6 +13020,7 @@
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
|
||||
+8
-7
@@ -12,6 +12,7 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"tsclint": "tsc --noEmit --pretty",
|
||||
"postinstall": "prisma generate --no-hints",
|
||||
"gen:icon-sprites": "node tools/develop/gen-icon-sprites/generate-llm-sprites.ts",
|
||||
"db:push": "prisma db push",
|
||||
@@ -58,10 +59,10 @@
|
||||
"next": "~15.1.12",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "5.4.54",
|
||||
"posthog-js": "^1.360.2",
|
||||
"posthog-node": "^5.28.2",
|
||||
"posthog-js": "^1.369.0",
|
||||
"posthog-node": "^5.29.2",
|
||||
"prismjs": "^1.30.0",
|
||||
"puppeteer-core": "^24.39.1",
|
||||
"puppeteer-core": "^24.40.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "^7.71.2",
|
||||
@@ -83,7 +84,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@posthog/nextjs-config": "~1.6.4",
|
||||
"@types/node": "^25.5.0",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/prismjs": "^1.26.6",
|
||||
"@types/react": "^19.2.14",
|
||||
@@ -91,12 +92,12 @@
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@types/turndown": "^5.0.6",
|
||||
"cross-env": "^10.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "~15.1.12",
|
||||
"prettier": "^3.8.1",
|
||||
"prettier": "^3.8.2",
|
||||
"prisma": "~5.22.0",
|
||||
"tsx": "^4.21.0",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^6.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^24.0.0 || ^22.0.0 || ^20.0.0"
|
||||
|
||||
@@ -45,6 +45,7 @@ export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
|
||||
{Brand.URIs.CardImage && <meta property='twitter:image' content={Brand.URIs.CardImage} />}
|
||||
<meta name='twitter:site' content={Brand.Meta.TwitterSite} />
|
||||
<meta name='twitter:creator' content='@enricoros' />
|
||||
<link rel='canonical' href={Brand.URIs.Home} />
|
||||
|
||||
{/* Author & Structured Data */}
|
||||
<meta name='author' content='Enrico Ros' />
|
||||
|
||||
@@ -250,13 +250,13 @@ export function Telephone(props: {
|
||||
if (messageWasInterruptedAtStart(status.lastDMessage))
|
||||
return;
|
||||
|
||||
// whether status.outcome === 'success' or not, we get a valid DMessage, eventually with Error Fragments inside
|
||||
// whether status.outcome === 'completed' 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
|
||||
if (status.outcome === 'success' && finalText?.length >= 1)
|
||||
if (status.outcome === 'completed' && finalText?.length >= 1)
|
||||
void speakText(finalText,
|
||||
undefined,
|
||||
{ label: 'Call', priority: 'fast' },
|
||||
|
||||
@@ -583,9 +583,11 @@ export function AppChat() {
|
||||
}, []);
|
||||
|
||||
useGlobalShortcuts('AppChat', React.useMemo(() => [
|
||||
// focused conversation
|
||||
{ key: 'z', ctrl: true, shift: true, disabled: isFocusedChatEmpty, action: handleMessageRegenerateLastInFocusedPane, description: 'Retry' },
|
||||
{ key: 'b', ctrl: true, shift: true, disabled: isFocusedChatEmpty, action: handleMessageBeamLastInFocusedPane, description: 'Beam Edit' },
|
||||
// focused conversation (excluded when Beam is open so the keystroke passes through to the browser)
|
||||
...(beamOpenStoreInFocusedPane ? [] : [
|
||||
{ key: 'z', ctrl: true, shift: true, disabled: isFocusedChatEmpty, action: handleMessageRegenerateLastInFocusedPane, description: 'Retry' },
|
||||
{ key: 'b', ctrl: true, shift: true, disabled: isFocusedChatEmpty, action: handleMessageBeamLastInFocusedPane, description: 'Beam Edit' },
|
||||
]),
|
||||
{ key: 'o', ctrl: true, action: handleConversationsImportFormFilePicker },
|
||||
{ key: 's', ctrl: true, action: () => handleFileSaveConversation(focusedPaneConversationId) },
|
||||
{ key: 'n', ctrl: true, shift: true, action: () => handleConversationNewInFocusedPane(false, false) },
|
||||
@@ -603,7 +605,7 @@ export function AppChat() {
|
||||
{ key: 'p', ctrl: true, action: () => personaDropdownRef.current?.openListbox() /*, description: 'Open Persona Dropdown'*/ },
|
||||
// focused conversation llm
|
||||
{ key: 'o', ctrl: true, shift: true, action: handleOpenChatLlmOptions },
|
||||
], [focusedPaneConversationId, handleConversationNewInFocusedPane, handleConversationReset, handleConversationsImportFormFilePicker, handleDeleteConversations, handleFileSaveConversation, handleMessageBeamLastInFocusedPane, handleMessageRegenerateLastInFocusedPane, handleMoveFocus, handleNavigateHistoryInFocusedPane, handleOpenChatLlmOptions, isFocusedChatEmpty]));
|
||||
], [beamOpenStoreInFocusedPane, focusedPaneConversationId, handleConversationNewInFocusedPane, handleConversationReset, handleConversationsImportFormFilePicker, handleDeleteConversations, handleFileSaveConversation, handleMessageBeamLastInFocusedPane, handleMessageRegenerateLastInFocusedPane, handleMoveFocus, handleNavigateHistoryInFocusedPane, handleOpenChatLlmOptions, isFocusedChatEmpty]));
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Box, List } from '@mui/joy';
|
||||
|
||||
import type { SystemPurposeExample } from '../../../data';
|
||||
|
||||
import type { AixReattachMode } from '~/modules/aix/client/aix.client';
|
||||
import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { speakText } from '~/modules/speex/speex.client';
|
||||
|
||||
@@ -15,7 +16,7 @@ import { DConversationId, excludeSystemMessages } from '~/common/stores/chat/cha
|
||||
import { ShortcutKey, useGlobalShortcuts } from '~/common/components/shortcuts/useGlobalShortcuts';
|
||||
import { clipboardInterceptCtrlCForCleanup } from '~/common/util/clipboardUtils';
|
||||
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, DMessageGenerator, DMessageId, DMessageUserFlag, DMetaReferenceItem, MESSAGE_FLAG_AIX_SKIP, messageHasUserFlag } 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';
|
||||
@@ -124,6 +125,91 @@ export function ChatMessageList(props: {
|
||||
}, [conversationHandler, conversationId, onConversationExecuteHistory]);
|
||||
|
||||
|
||||
// Resume in-flight tracking - lives at this level (NOT inside BlockOpUpstreamResume) so it
|
||||
// survives any remount of the message bubble during a long-running stream (e.g. Deep Research).
|
||||
// - `resumeInFlight` (state) drives the loading/Detach UI on BlockOpUpstreamResume via props.
|
||||
// - `resumeAbortersRef` (ref) holds the AbortController so Detach can abort even after a remount.
|
||||
// Map keyed by messageId so multiple messages could in principle resume concurrently.
|
||||
const [resumeInFlight, setResumeInFlight] = React.useState<Record<DMessageId, AixReattachMode>>({});
|
||||
const resumeAbortersRef = React.useRef<Map<DMessageId, AbortController>>(new Map());
|
||||
|
||||
const handleMessageUpstreamResume = React.useCallback(async (generator: DMessageGenerator, messageId: DMessageId, mode: AixReattachMode) => {
|
||||
if (!conversationId || !conversationHandler) return;
|
||||
if (!generator.upstreamHandle) throw new Error('No upstream handle on generator');
|
||||
|
||||
// For AIX generators the DLLMId is at .aix.mId
|
||||
const llmId = generator.mgt === 'aix' ? generator.aix.mId : undefined;
|
||||
if (!llmId) throw new Error('No model id on generator');
|
||||
|
||||
const controller = new AbortController();
|
||||
resumeAbortersRef.current.set(messageId, controller);
|
||||
setResumeInFlight(prev => ({ ...prev, [messageId]: mode }));
|
||||
|
||||
const { aixCreateChatGenerateContext, aixReattachContent_DMessage_orThrow } = await import('~/modules/aix/client/aix.client');
|
||||
try {
|
||||
await aixReattachContent_DMessage_orThrow(
|
||||
llmId,
|
||||
generator,
|
||||
aixCreateChatGenerateContext('conversation', conversationId),
|
||||
mode,
|
||||
{ abortSignal: controller.signal, throttleParallelThreads: 0 }, // Detach: aborting kills the local fetch; upstream run keeps going.
|
||||
async (update, isDone) => {
|
||||
conversationHandler.messageEdit(messageId, {
|
||||
fragments: update.fragments,
|
||||
generator: update.generator,
|
||||
pendingIncomplete: update.pendingIncomplete,
|
||||
}, isDone, isDone); // remove the pending state and update only when done
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
// Clear local tracking only if this attempt is still the current one (avoid races on rapid retry)
|
||||
if (resumeAbortersRef.current.get(messageId) === controller)
|
||||
resumeAbortersRef.current.delete(messageId);
|
||||
setResumeInFlight(prev => {
|
||||
if (prev[messageId] !== mode) return prev;
|
||||
const { [messageId]: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
}
|
||||
|
||||
// Manual reattach is one-shot: on failure (e.g. upstream 404 from expired or already-consumed handle),
|
||||
// drop the upstreamHandle so the Resume button doesn't keep luring the user into the same error.
|
||||
// On 'aborted' we keep it so the user can try again later; on 'completed' the reassembler already cleared it.
|
||||
// 2026-04-22: disabled; it was removing the connect button on a connection error (e.g. wifi drop)
|
||||
// if (result.outcome === 'failed' && result.generator?.upstreamHandle)
|
||||
// conversationHandler.messageEdit(messageId, {
|
||||
// generator: { ...result.generator, upstreamHandle: undefined },
|
||||
// }, false /* messageComplete */, true /* touch */);
|
||||
}, [conversationHandler, conversationId]);
|
||||
|
||||
const handleMessageUpstreamDetach = React.useCallback((messageId: DMessageId) => {
|
||||
resumeAbortersRef.current.get(messageId)?.abort();
|
||||
}, []);
|
||||
|
||||
|
||||
const handleMessageUpstreamDelete = React.useCallback(async (generator: DMessageGenerator, messageId: DMessageId) => {
|
||||
if (!conversationId || !conversationHandler) return;
|
||||
if (!generator.upstreamHandle) throw new Error('No upstream handle on generator');
|
||||
|
||||
// For AIX generators the DLLMId is at .aix.mId
|
||||
const llmId = generator.mgt === 'aix' ? generator.aix.mId : undefined;
|
||||
if (!llmId) throw new Error('No model id on generator');
|
||||
|
||||
const { aixDeleteUpstreamContent_orThrow } = await import('~/modules/aix/client/aix.client');
|
||||
const result = await aixDeleteUpstreamContent_orThrow(llmId, generator);
|
||||
|
||||
// On success (or 404 already-gone), clear the handle locally so the buttons disappear
|
||||
if (result.ok) {
|
||||
conversationHandler.messageEdit(messageId, {
|
||||
generator: { ...generator, upstreamHandle: undefined },
|
||||
}, false /* messageComplete */, true /* touch */);
|
||||
return;
|
||||
}
|
||||
// On failure: surface to the button's error UI
|
||||
throw new Error(result.message || `Delete failed${result.httpStatus ? ` (HTTP ${result.httpStatus})` : ''}`);
|
||||
}, [conversationHandler, conversationId]);
|
||||
|
||||
|
||||
// message menu methods proxy
|
||||
|
||||
const handleMessageAssistantFrom = React.useCallback(async (messageId: DMessageId, offset: number) => {
|
||||
@@ -340,7 +426,11 @@ export function ChatMessageList(props: {
|
||||
|
||||
{filteredMessages.map((message, idx) => {
|
||||
|
||||
// Optimization: only memo complete components, or we'd be memoizing garbage
|
||||
// Optimization: only memo complete components, or we'd be memoizing garbage (fragments
|
||||
// change every chunk during streaming, so the equality check would always fail).
|
||||
// CAVEAT: switching between memo and non-memo at the same position causes React to
|
||||
// remount the subtree (different component types). Any state that must survive that
|
||||
// boundary lives on this component (e.g. resumeInFlight, resumeAbortersRef).
|
||||
const ChatMessageMemoOrNot = !message.pendingIncomplete ? ChatMessageMemo : ChatMessage;
|
||||
|
||||
return props.isMessageSelectionMode ? (
|
||||
@@ -371,6 +461,10 @@ export function ChatMessageList(props: {
|
||||
onMessageBeam={handleMessageBeam}
|
||||
onMessageBranch={handleMessageBranch}
|
||||
onMessageContinue={handleMessageContinue}
|
||||
onMessageUpstreamResume={handleMessageUpstreamResume}
|
||||
onMessageUpstreamDetach={handleMessageUpstreamDetach}
|
||||
onMessageUpstreamDelete={handleMessageUpstreamDelete}
|
||||
upstreamResumeMode={resumeInFlight[message.id]}
|
||||
onMessageDelete={handleMessageDelete}
|
||||
onMessageFragmentAppend={handleMessageAppendFragment}
|
||||
onMessageFragmentDelete={handleMessageDeleteFragment}
|
||||
|
||||
@@ -38,7 +38,6 @@ import { getModelParameterValueWithFallback } from '~/common/stores/llms/llms.pa
|
||||
import { launchAppCall, removeQueryParam, useRouterQuery } from '~/common/app.routes';
|
||||
import { lineHeightTextareaMd, themeBgAppChatComposer } from '~/common/app.theme';
|
||||
import { optimaOpenPreferences } from '~/common/layout/optima/useOptima';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
import { supportsCameraCapture } from '~/common/components/camera/useCameraCapture';
|
||||
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
import { useAttachHandler_CameraOpen, useAttachHandler_Files, useAttachHandler_PasteIntercept, useAttachHandler_ScreenCapture, useAttachHandler_UrlWebLinks } from '~/common/attachment-drafts/attachment-sources/useAttachmentSourceHandlers';
|
||||
@@ -140,8 +139,7 @@ export function Composer(props: {
|
||||
})));
|
||||
const timeToShowTips = useLogicSherpaStore(state => state.usageCount >= SHOW_TIPS_AFTER_RELOADS);
|
||||
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);
|
||||
@@ -548,16 +546,14 @@ export function Composer(props: {
|
||||
// Alt (Windows) or Option (Mac) + Enter: append the message instead of sending it
|
||||
if (e.altKey && !e.metaKey && !e.ctrlKey) {
|
||||
if (await handleSendAction('append-user', composeText)) // 'alt+enter' -> write
|
||||
touchAltEnter();
|
||||
e.stopPropagation();
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
// Ctrl (Windows) or Command (Mac) + Enter: send for beaming
|
||||
if (e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||
if (await handleSendAction('beam-content', composeText)) { // 'ctrl+enter' -> beam
|
||||
touchCtrlEnter();
|
||||
if (await handleSendAction('beam-content', composeText)) // 'ctrl+enter' -> beam
|
||||
e.stopPropagation();
|
||||
}
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -571,7 +567,7 @@ export function Composer(props: {
|
||||
}
|
||||
}
|
||||
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatExecuteMode, composeText, enterIsNewline, handleSendAction, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatExecuteMode, composeText, enterIsNewline, handleSendAction, touchShiftEnter]);
|
||||
|
||||
|
||||
// Focus mode
|
||||
@@ -662,6 +658,7 @@ export function Composer(props: {
|
||||
|
||||
const showChatInReferenceTo = !!inReferenceTo?.length;
|
||||
const showChatExtras = isText && !showChatInReferenceTo && !assistantAbortible && composerQuickButton !== 'off';
|
||||
const speechMayWork = browserSpeechRecognitionCapability().mayWork;
|
||||
|
||||
const sendButtonVariant: VariantProp = (isAppend || (isMobile && isTextBeam)) ? 'outlined' : 'solid';
|
||||
|
||||
@@ -708,10 +705,6 @@ export function Composer(props: {
|
||||
if (isDesktop && timeToShowTips && !isDraw) {
|
||||
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');
|
||||
else if (explainCtrlEnter)
|
||||
textPlaceholder += platformAwareKeystrokes('\n\n⫷ Tip: Ctrl + Enter to beam');
|
||||
}
|
||||
|
||||
const stableGridSx: SxProps = React.useMemo(() => ({
|
||||
@@ -972,7 +965,7 @@ export function Composer(props: {
|
||||
|
||||
{/* [mobile] bottom-corner secondary button */}
|
||||
{isMobile && (showChatExtras
|
||||
? (composerQuickButton === 'call'
|
||||
? (composerQuickButton === 'call' && speechMayWork
|
||||
? <ButtonCallMemo isMobile disabled={noConversation || noLLM} onClick={handleCallClicked} />
|
||||
: <ButtonBeamMemo isMobile disabled={noConversation /*|| noLLM*/} color={beamButtonColor} hasContent={!!composeText} onClick={handleSendTextBeamClicked} />)
|
||||
: isDraw
|
||||
@@ -1063,8 +1056,8 @@ export function Composer(props: {
|
||||
{/* [desktop] secondary bottom-buttons (aligned to bottom for now, and mutually exclusive) */}
|
||||
{isDesktop && <Box sx={{ mt: 'auto', display: 'grid', gap: 1 }}>
|
||||
|
||||
{/* [desktop] Call secondary button */}
|
||||
{showChatExtras && <ButtonCallMemo disabled={noConversation || noLLM || assistantAbortible} onClick={handleCallClicked} />}
|
||||
{/* [desktop] Call secondary button - hidden when speech recognition is not available */}
|
||||
{showChatExtras && speechMayWork && <ButtonCallMemo disabled={noConversation || noLLM || assistantAbortible} onClick={handleCallClicked} />}
|
||||
|
||||
{/* [desktop] Draw Options secondary button */}
|
||||
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
|
||||
|
||||
@@ -33,7 +33,10 @@ const _styles = {
|
||||
} as const,
|
||||
'& nav > ol > li:first-of-type': {
|
||||
overflow: 'hidden',
|
||||
maxWidth: { xs: '110px', md: '140px' },
|
||||
// allow the chat title to use available space, shrinking gracefully when the bar is narrow
|
||||
// NOTE: already performed by virtue of the breadcrumb having agi-ellipsize on the crumbs
|
||||
// flexShrink: 1,
|
||||
// minWidth: '60px',
|
||||
} as const,
|
||||
|
||||
} as const,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { OptimaBarControlMethods, OptimaBarDropdownMemo, OptimaDropdownItems } from '~/common/layout/optima/bar/OptimaBarDropdown';
|
||||
import { findModelsServiceOrNull } from '~/common/stores/llms/store-llms';
|
||||
import { isDeepEqual } from '~/common/util/hooks/useDeep';
|
||||
import { sortLLMsByServiceLabel } from '~/common/stores/llms/components/llms.dropdown.utils';
|
||||
import { optimaActions, optimaOpenModels } from '~/common/layout/optima/useOptima';
|
||||
import { useAllLLMs } from '~/common/stores/llms/hooks/useAllLLMs';
|
||||
import { useModelDomain } from '~/common/stores/llms/hooks/useModelDomain';
|
||||
@@ -72,7 +73,10 @@ function LLMDropdown(props: {
|
||||
return lcFilterString ? true : isLLMVisible(llm);
|
||||
});
|
||||
|
||||
for (const llm of filteredLLMs) {
|
||||
// sort by service label so vendor groups appear alphabetically (groups remain contiguous because sort is stable on equal keys)
|
||||
const sortedLLMs = sortLLMsByServiceLabel(filteredLLMs);
|
||||
|
||||
for (const llm of sortedLLMs) {
|
||||
// add separators when changing services
|
||||
if (!prevServiceId || llm.sId !== prevServiceId) {
|
||||
const vendor = findModelVendor(llm.vId);
|
||||
|
||||
@@ -16,6 +16,7 @@ import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
|
||||
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { CloseablePopup } from '~/common/components/CloseablePopup';
|
||||
import { DFolder, useFolderStore } from '~/common/stores/folders/store-chat-folders';
|
||||
import { DebouncedInputMemo } from '~/common/components/DebouncedInput';
|
||||
@@ -89,6 +90,7 @@ function ChatDrawer(props: {
|
||||
// external state
|
||||
const {
|
||||
clearFilters,
|
||||
filterHasBeamOpen, toggleFilterHasBeamOpen,
|
||||
filterHasDocFragments, toggleFilterHasDocFragments,
|
||||
filterHasImageAssets, toggleFilterHasImageAssets,
|
||||
filterHasStars, toggleFilterHasStars,
|
||||
@@ -98,7 +100,7 @@ function ChatDrawer(props: {
|
||||
} = useChatDrawerFilters();
|
||||
const { activeFolder, allFolders, enableFolders, toggleEnableFolders } = useFolders(props.activeFolderId);
|
||||
const { filteredChatsCount, filteredChatIDs, filteredChatsAreEmpty, filteredChatsBarBasis, filteredChatsIncludeActive, renderNavItems } = useChatDrawerRenderItems(
|
||||
props.activeConversationId, props.chatPanesConversationIds, debouncedSearchQuery, activeFolder, allFolders, filterHasStars, filterHasImageAssets, filterHasDocFragments, filterIsArchived, navGrouping, searchSorting, showRelativeSize, searchDepth,
|
||||
props.activeConversationId, props.chatPanesConversationIds, debouncedSearchQuery, activeFolder, allFolders, filterHasBeamOpen, filterHasStars, filterHasImageAssets, filterHasDocFragments, filterIsArchived, navGrouping, searchSorting, showRelativeSize, searchDepth,
|
||||
);
|
||||
const [uiComplexityMode, contentScaling] = useUIPreferencesStore(useShallow((state) => [state.complexityMode, state.contentScaling]));
|
||||
const zenMode = uiComplexityMode === 'minimal';
|
||||
@@ -240,6 +242,10 @@ function ChatDrawer(props: {
|
||||
<ListItemDecorator>{filterHasDocFragments && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Has Attachments <AttachFileRoundedIcon />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={toggleFilterHasBeamOpen}>
|
||||
<ListItemDecorator>{filterHasBeamOpen && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Beam Open <ChatBeamIcon />
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider />
|
||||
<ListItem>
|
||||
@@ -288,8 +294,8 @@ function ChatDrawer(props: {
|
||||
)}
|
||||
</Dropdown>
|
||||
), [
|
||||
filterHasDocFragments, filterHasImageAssets, filterHasStars, isSearching, navGrouping, searchSorting, searchDepth, filterIsArchived, showPersonaIcons, showRelativeSize,
|
||||
toggleFilterHasDocFragments, toggleFilterHasImageAssets, toggleFilterHasStars, toggleFilterIsArchived, toggleShowPersonaIcons, toggleShowRelativeSize,
|
||||
filterHasBeamOpen, filterHasDocFragments, filterHasImageAssets, filterHasStars, isSearching, navGrouping, searchSorting, searchDepth, filterIsArchived, showPersonaIcons, showRelativeSize,
|
||||
toggleFilterHasBeamOpen, toggleFilterHasDocFragments, toggleFilterHasImageAssets, toggleFilterHasStars, toggleFilterIsArchived, toggleShowPersonaIcons, toggleShowRelativeSize,
|
||||
]);
|
||||
|
||||
const displayNavItems = React.useMemo(() => {
|
||||
@@ -304,6 +310,18 @@ function ChatDrawer(props: {
|
||||
return activeItem ? [...sliced, activeItem] : sliced;
|
||||
}, [renderNavItems, renderLimit, props.activeConversationId]);
|
||||
|
||||
|
||||
// when filters/search transition from active to inactive, the active chat may end up
|
||||
// submerged below the fold of a much longer list - scroll it back into view
|
||||
const chatsListRef = React.useRef<HTMLDivElement>(null);
|
||||
const isFiltering = isSearching || filterHasBeamOpen || filterHasDocFragments || filterHasImageAssets || filterHasStars || filterIsArchived;
|
||||
React.useLayoutEffect(() => {
|
||||
if (isFiltering) return;
|
||||
const activeEl = chatsListRef.current?.querySelector('[aria-current="true"]') as HTMLElement | null;
|
||||
activeEl?.scrollIntoView({ block: 'nearest' });
|
||||
}, [isFiltering]);
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
{/* Drawer Header */}
|
||||
@@ -390,7 +408,7 @@ function ChatDrawer(props: {
|
||||
</Box>
|
||||
|
||||
{/* Chat Titles List (shrink as half the rate as the Folders List) */}
|
||||
<Box sx={{ flexGrow: 1, flexShrink: 1, flexBasis: '20rem', overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
|
||||
<Box key='chatlist' ref={chatsListRef} sx={{ flexGrow: 1, flexShrink: 1, flexBasis: '20rem', overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
|
||||
{displayNavItems.map((item, idx) => item.type === 'nav-item-chat-data' ? (
|
||||
<ChatDrawerItemMemo
|
||||
key={'nav-chat-' + item.conversationId}
|
||||
@@ -422,7 +440,7 @@ function ChatDrawer(props: {
|
||||
{filterHasStars && <StarOutlineRoundedIcon sx={{ color: 'primary.softColor', fontSize: 'xl', mb: -0.5, mr: 1 }} />}
|
||||
{item.message}
|
||||
</Typography>
|
||||
{(filterHasStars || filterHasImageAssets || filterHasDocFragments || filterIsArchived) && (
|
||||
{(filterHasBeamOpen || filterHasStars || filterHasImageAssets || filterHasDocFragments || filterIsArchived) && (
|
||||
<Tooltip title='Clear Filters'>
|
||||
<IconButton size='sm' color='primary' onClick={clearFilters}>
|
||||
<ClearIcon />
|
||||
|
||||
@@ -282,7 +282,7 @@ function ChatDrawerItem(props: {
|
||||
{searchFrequency > 0 ? (
|
||||
// Display search frequency if it exists and is greater than 0
|
||||
<Typography level='body-sm'>
|
||||
{searchFrequency}
|
||||
{Math.round(searchFrequency * 10) / 10}
|
||||
</Typography>
|
||||
) : (props.showSymbols && (userFlagsSummary || containsDocAttachments || containsImageAssets)) ? (
|
||||
<Box sx={{
|
||||
@@ -308,6 +308,7 @@ function ChatDrawerItem(props: {
|
||||
|
||||
// Active or Also Open
|
||||
<Sheet
|
||||
aria-current={isActive ? 'true' : undefined}
|
||||
variant={isActive ? 'solid' : 'outlined'}
|
||||
invertedColors={isActive}
|
||||
onClick={!isActive ? handleConversationActivate : undefined}
|
||||
|
||||
@@ -86,6 +86,7 @@ export function useChatDrawerRenderItems(
|
||||
filterByQuery: string,
|
||||
activeFolder: DFolder | null,
|
||||
allFolders: DFolder[],
|
||||
filterHasBeamOpen: boolean,
|
||||
filterHasStars: boolean,
|
||||
filterHasImageAssets: boolean,
|
||||
filterHasDocFragments: boolean,
|
||||
@@ -146,7 +147,8 @@ export function useChatDrawerRenderItems(
|
||||
}
|
||||
|
||||
// filter for required attributes
|
||||
if ((filterHasStars && !hasStars) || (filterHasImageAssets && !hasImages) || (filterHasDocFragments && !hasDocs))
|
||||
const hasBeamOpen = openBeamConversationIds[_c.id];
|
||||
if ((filterHasBeamOpen && !hasBeamOpen) || (filterHasStars && !hasStars) || (filterHasImageAssets && !hasImages) || (filterHasDocFragments && !hasDocs))
|
||||
return null;
|
||||
|
||||
// rich properties
|
||||
@@ -186,7 +188,7 @@ export function useChatDrawerRenderItems(
|
||||
? allFolders.find(folder => folder.conversationIds.includes(_c.id)) ?? null
|
||||
: null,
|
||||
updatedAt: _c.updated || _c.created || 0,
|
||||
hasBeamOpen: !!openBeamConversationIds?.[_c.id],
|
||||
hasBeamOpen,
|
||||
messageCount,
|
||||
beingGenerated: !!_c._abortController, // FIXME: when the AbortController is moved at the message level, derive the state in the conv
|
||||
systemPurposeId: _c.systemPurposeId,
|
||||
@@ -287,19 +289,21 @@ export function useChatDrawerRenderItems(
|
||||
renderNavItems.push({
|
||||
type: 'nav-item-info-message',
|
||||
message: (filterHasStars && (filterHasImageAssets || filterHasDocFragments)) ? 'No results'
|
||||
: filterHasDocFragments ? 'No attachment results'
|
||||
: filterHasImageAssets ? 'No image results'
|
||||
: filterHasStars ? 'No starred results'
|
||||
: filterIsArchived ? 'No archived conversations'
|
||||
: isSearching ? 'Text not found'
|
||||
: 'No conversations in folder',
|
||||
: filterHasBeamOpen ? 'No beam conversations'
|
||||
: filterHasDocFragments ? 'No attachment results'
|
||||
: filterHasImageAssets ? 'No image results'
|
||||
: filterHasStars ? 'No starred results'
|
||||
: filterIsArchived ? 'No archived conversations'
|
||||
: isSearching ? 'Text not found'
|
||||
: 'No conversations in folder',
|
||||
});
|
||||
} else {
|
||||
// filtering reminder (will be rendered with a clear button too)
|
||||
if (filterHasStars || filterHasImageAssets || filterHasDocFragments || filterIsArchived) {
|
||||
if (filterHasBeamOpen || filterHasStars || filterHasImageAssets || filterHasDocFragments || filterIsArchived) {
|
||||
renderNavItems.unshift({
|
||||
type: 'nav-item-info-message',
|
||||
message: `${filterIsArchived ? 'Showing' : 'Filtering by'} ${[
|
||||
filterHasBeamOpen && 'beam',
|
||||
filterHasStars && 'stars',
|
||||
filterHasImageAssets && 'images',
|
||||
filterHasDocFragments && 'attachments',
|
||||
|
||||
@@ -1,57 +1,90 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { Box, Button, ButtonGroup, Tooltip, Typography } from '@mui/joy';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import LinkOffRoundedIcon from '@mui/icons-material/LinkOffRounded';
|
||||
import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded';
|
||||
import StopRoundedIcon from '@mui/icons-material/StopRounded';
|
||||
|
||||
import type { AixReattachMode } from '~/modules/aix/client/aix.client';
|
||||
|
||||
import type { DMessageGenerator } from '~/common/stores/chat/chat.message';
|
||||
|
||||
|
||||
const ARM_TIMEOUT_MS = 4000;
|
||||
|
||||
|
||||
/**
|
||||
* FIXME: COMPLETE THIS
|
||||
* Resume controls for an upstream-stored run.
|
||||
* - Resume: SSE replay (live deltas) - canonical path. Always offered when onResume exists.
|
||||
* - Recover: one-shot JSON GET - shown only for vendors that benefit from it (Gemini Interactions).
|
||||
* - Detach: abort the local fetch but leave the upstream run alive. Visible only when a resume
|
||||
* is in-flight (`inFlightMode != null`). Resume/Recover stay available afterwards.
|
||||
* - Stop: terminate the upstream run + delete the resource.
|
||||
*
|
||||
* IMPORTANT: in-flight state is owned by the parent (`inFlightMode` + `onDetach`) so it survives
|
||||
* remounts that happen while a long-running stream is active (e.g. Deep Research).
|
||||
*/
|
||||
export function BlockOpUpstreamResume(props: {
|
||||
upstreamHandle: Exclude<DMessageGenerator['upstreamHandle'], undefined>,
|
||||
onResume?: () => void | Promise<void>;
|
||||
onCancel?: () => void | Promise<void>;
|
||||
pending?: boolean; // true iff a local in-flight op (initial POST or resume); drives the state machine + hides the expiry footer
|
||||
inFlightMode?: AixReattachMode; // set by the parent while a resume is in flight; drives the loading/Detach UI
|
||||
onResume?: (mode: AixReattachMode) => void | Promise<void>;
|
||||
onDetach?: () => void;
|
||||
onDelete?: () => void | Promise<void>;
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [isResuming, setIsResuming] = React.useState(false);
|
||||
const [isCancelling, setIsCancelling] = React.useState(false);
|
||||
// local state - only for short-lived ops the parent doesn't own
|
||||
const [isDeleting, setIsDeleting] = React.useState(false);
|
||||
const [deleteArmed, setDeleteArmed] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
// expiration: boolean is evaluated at render (may lag briefly if nothing re-renders past expiry).
|
||||
const { expiresAt /*, runId = ''*/ } = props.upstreamHandle;
|
||||
|
||||
// State machine - mutually exclusive triplet (idle | initial-POST | resume | recover):
|
||||
// - Idle : !pending - run not active locally (incl. post-reload, since
|
||||
// chats.converters.ts clears pendingIncomplete on hydrate).
|
||||
// - Initial POST : pending && !inFlightMode - first generation streaming.
|
||||
// - Resume replay : pending && mode='replay' - we own this resume cycle.
|
||||
// - Recover snap : pending && mode='snapshot' - we own this snapshot fetch.
|
||||
//
|
||||
// Visibility matrix (see BlockOpUpstreamResume props doc):
|
||||
// Resume Recover Detach Cancel
|
||||
// Idle ✅ ✅¹ — ✅
|
||||
// Initial POST — — — ✅
|
||||
// Resume in flight — — ✅ ✅
|
||||
// Recover in flight — ✅² — —
|
||||
// ¹ only for Gemini Interactions ² with loading spinner
|
||||
const isReplaying = props.inFlightMode === 'replay';
|
||||
const isSnapshotting = props.inFlightMode === 'snapshot';
|
||||
const isIdle = !props.pending;
|
||||
|
||||
const canRecoverVendor = props.upstreamHandle.uht === 'vnd.gem.interactions';
|
||||
const showResume = isIdle && !!props.onResume;
|
||||
const showRecover = (isIdle || isSnapshotting) && !!props.onResume && canRecoverVendor;
|
||||
const showDetach = isReplaying && !!props.onDetach;
|
||||
const showCancel = !isSnapshotting && !!props.onDelete;
|
||||
|
||||
// handlers
|
||||
|
||||
const handleResume = React.useCallback(async () => {
|
||||
const handleResume = React.useCallback((mode: AixReattachMode) => {
|
||||
if (!props.onResume) return;
|
||||
setError(null);
|
||||
setIsResuming(true);
|
||||
try {
|
||||
await props.onResume();
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Resume failed');
|
||||
} finally {
|
||||
setIsResuming(false);
|
||||
}
|
||||
}, [props]);
|
||||
|
||||
const handleCancel = React.useCallback(async () => {
|
||||
if (!props.onCancel) return;
|
||||
setError(null);
|
||||
setIsCancelling(true);
|
||||
try {
|
||||
await props.onCancel();
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Cancel failed');
|
||||
} finally {
|
||||
setIsCancelling(false);
|
||||
}
|
||||
// fire-and-forget: parent owns the promise lifecycle and the abort controller.
|
||||
// If it rejects, the parent surfaces the error via its own UI; we stay silent.
|
||||
Promise.resolve(props.onResume(mode)).catch(() => { /* parent handles */ });
|
||||
}, [props]);
|
||||
|
||||
// Two-click arm: first click arms (visible red "Confirm?"), second click (within ARM_TIMEOUT_MS) executes.
|
||||
const handleDelete = React.useCallback(async () => {
|
||||
if (!props.onDelete) return;
|
||||
if (!deleteArmed) {
|
||||
setDeleteArmed(true);
|
||||
return;
|
||||
}
|
||||
setDeleteArmed(false);
|
||||
setError(null);
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
@@ -61,7 +94,14 @@ export function BlockOpUpstreamResume(props: {
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
}, [props]);
|
||||
}, [deleteArmed, props]);
|
||||
|
||||
// Auto-disarm after ARM_TIMEOUT_MS so the armed state can't leak into a later session
|
||||
React.useEffect(() => {
|
||||
if (!deleteArmed) return;
|
||||
const t = setTimeout(() => setDeleteArmed(false), ARM_TIMEOUT_MS);
|
||||
return () => clearTimeout(t);
|
||||
}, [deleteArmed]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -74,41 +114,55 @@ export function BlockOpUpstreamResume(props: {
|
||||
}}
|
||||
>
|
||||
<ButtonGroup>
|
||||
{props.onResume && (
|
||||
<Tooltip title='Resume generation from last checkpoint'>
|
||||
{showResume && (
|
||||
<Tooltip title='Resume by re-streaming from the upstream run'>
|
||||
<Button
|
||||
disabled={isResuming || isCancelling || isDeleting}
|
||||
loading={isResuming}
|
||||
startDecorator={<PlayArrowRoundedIcon sx={{ color: 'success.solidBg' }} />}
|
||||
onClick={handleResume}
|
||||
disabled={isDeleting}
|
||||
startDecorator={<PlayArrowRoundedIcon color='success' />}
|
||||
onClick={() => handleResume('replay')}
|
||||
>
|
||||
Resume
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{props.onCancel && (
|
||||
<Tooltip title='Cancel the response generation'>
|
||||
{showRecover && (
|
||||
<Tooltip title='Fetch the result without streaming - recovers stuck or hung runs'>
|
||||
<Button
|
||||
disabled={isResuming || isCancelling || isDeleting}
|
||||
loading={isCancelling}
|
||||
// startDecorator={<CancelIcon />}
|
||||
onClick={handleCancel}
|
||||
disabled={isDeleting}
|
||||
loading={isSnapshotting}
|
||||
loadingPosition='start'
|
||||
startDecorator={<DownloadIcon />}
|
||||
onClick={() => handleResume('snapshot')}
|
||||
>
|
||||
Cancel
|
||||
Recover
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{props.onDelete && (
|
||||
<Tooltip title='Delete the stored response'>
|
||||
{showDetach && (
|
||||
<Tooltip title='Close this connection only - the upstream run keeps going. Click Resume or Recover later to fetch results.'>
|
||||
<Button
|
||||
disabled={isDeleting}
|
||||
startDecorator={<LinkOffRoundedIcon />}
|
||||
onClick={props.onDetach}
|
||||
>
|
||||
Detach
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{showCancel && (
|
||||
<Tooltip title={deleteArmed ? 'Click again to confirm - cancels the upstream run and clears the handle' : 'Cancel the upstream run'}>
|
||||
<Button
|
||||
loading={isDeleting}
|
||||
// startDecorator={<DeleteIcon />}
|
||||
color={deleteArmed ? 'danger' : 'neutral'}
|
||||
variant={deleteArmed ? 'solid' : 'outlined'}
|
||||
startDecorator={<StopRoundedIcon />}
|
||||
onClick={handleDelete}
|
||||
disabled={isResuming || isCancelling || isDeleting}
|
||||
disabled={isDeleting}
|
||||
>
|
||||
Delete
|
||||
{deleteArmed ? 'Confirm?' : 'Cancel'}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
@@ -120,9 +174,11 @@ export function BlockOpUpstreamResume(props: {
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Typography level='body-xs' sx={{ fontSize: '0.65rem', opacity: 0.6 }}>
|
||||
Response ID: {props.upstreamHandle.responseId.slice(0, 12)}...
|
||||
</Typography>
|
||||
{!props.pending && !!expiresAt && <Typography level='body-xs' sx={{ fontSize: '0.65rem', opacity: 0.6 }}>
|
||||
{/*Run ID: {runId.slice(0, 12)}...*/}
|
||||
{/*{!!expiresAt && <> · Expires <TimeAgo date={expiresAt} /></>}*/}
|
||||
Expires <TimeAgo date={expiresAt} />
|
||||
</Typography>}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import TimeAgo from 'react-timeago';
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, ButtonGroup, CircularProgress, Divider, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import { ClickAwayListener, Popper } from '@mui/base';
|
||||
import AlternateEmailIcon from '@mui/icons-material/AlternateEmail';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
@@ -16,7 +15,7 @@ import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import FormatBoldIcon from '@mui/icons-material/FormatBold';
|
||||
import FormatPaintOutlinedIcon from '@mui/icons-material/FormatPaintOutlined';
|
||||
import InsertLinkIcon from '@mui/icons-material/InsertLink';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive';
|
||||
import NotificationsOutlinedIcon from '@mui/icons-material/NotificationsOutlined';
|
||||
@@ -30,17 +29,19 @@ import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
|
||||
import type { AixReattachMode } from '~/modules/aix/client/aix.client';
|
||||
import { ModelVendorAnthropic } from '~/modules/llms/vendors/anthropic/anthropic.vendor';
|
||||
|
||||
import { AnthropicIcon } from '~/common/components/icons/vendors/AnthropicIcon';
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { CloseablePopup } from '~/common/components/CloseablePopup';
|
||||
import { DMessage, DMessageId, DMessageUserFlag, DMetaReferenceItem, MESSAGE_FLAG_AIX_SKIP, MESSAGE_FLAG_NOTIFY_COMPLETE, MESSAGE_FLAG_STARRED, MESSAGE_FLAG_VND_ANT_CACHE_AUTO, MESSAGE_FLAG_VND_ANT_CACHE_USER, messageFragmentsReduceText, messageHasUserFlag } from '~/common/stores/chat/chat.message';
|
||||
import { DMessage, DMessageGenerator, DMessageId, DMessageUserFlag, DMetaReferenceItem, MESSAGE_FLAG_AIX_SKIP, MESSAGE_FLAG_NOTIFY_COMPLETE, MESSAGE_FLAG_STARRED, MESSAGE_FLAG_VND_ANT_CACHE_AUTO, MESSAGE_FLAG_VND_ANT_CACHE_USER, messageFragmentsReduceText, messageHasUserFlag } from '~/common/stores/chat/chat.message';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { MarkHighlightIcon } from '~/common/components/icons/MarkHighlightIcon';
|
||||
import { PhTreeStructure } from '~/common/components/icons/phosphor/PhTreeStructure';
|
||||
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
|
||||
import { Release } from '~/common/app.release';
|
||||
import { StarredState } from '~/common/components/StarIcons';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { adjustContentScaling, themeScalingMap, themeZIndexChatBubble } from '~/common/app.theme';
|
||||
import { avatarIconSx, makeMessageAvatarIcon, messageBackground, useMessageAvatarLabel } from '~/common/util/dMessageUtils';
|
||||
@@ -53,6 +54,7 @@ import { BlockOpContinue } from './BlockOpContinue';
|
||||
import { BlockOpOptions, optionsExtractFromFragments_dangerModifyFragment } from './BlockOpOptions';
|
||||
import { BlockOpUpstreamResume } from './BlockOpUpstreamResume';
|
||||
import { ChatMessageEditAttachments, type EditModeAttachmentsHandle } from './ChatMessageEditAttachments';
|
||||
import { ChatMessageInfoPopup } from './ChatMessageInfoPopup';
|
||||
import { ContentFragments } from './fragments-content/ContentFragments';
|
||||
import { DocumentAttachmentFragments } from './fragments-attachment-doc/DocumentAttachmentFragments';
|
||||
import { ImageAttachmentFragments } from './fragments-attachment-image/ImageAttachmentFragments';
|
||||
@@ -160,6 +162,10 @@ export function ChatMessage(props: {
|
||||
onMessageBeam?: (messageId: string) => Promise<void>,
|
||||
onMessageBranch?: (messageId: string) => void,
|
||||
onMessageContinue?: (messageId: string, continueText: null | string) => void,
|
||||
onMessageUpstreamResume?: (generator: DMessageGenerator, messageId: string, mode: AixReattachMode) => Promise<void>,
|
||||
onMessageUpstreamDetach?: (messageId: string) => void,
|
||||
onMessageUpstreamDelete?: (generator: DMessageGenerator, messageId: string) => Promise<void>,
|
||||
upstreamResumeMode?: AixReattachMode, // set by parent while a resume is in flight on this message
|
||||
onMessageDelete?: (messageId: string) => void,
|
||||
onMessageFragmentAppend?: (messageId: DMessageId, fragment: DMessageFragment) => void
|
||||
onMessageFragmentDelete?: (messageId: DMessageId, fragmentId: DMessageFragmentId) => void,
|
||||
@@ -180,6 +186,7 @@ export function ChatMessage(props: {
|
||||
const [contextMenuAnchor, setContextMenuAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [opsMenuAnchor, setOpsMenuAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [textContentEditState, setTextContentEditState] = React.useState<ChatMessageTextPartEditState | null>(null);
|
||||
const [showInfoModal, setShowInfoModal] = React.useState(false);
|
||||
const attachmentsEditRef = React.useRef<EditModeAttachmentsHandle>(null);
|
||||
|
||||
// external state
|
||||
@@ -243,7 +250,7 @@ export function ChatMessage(props: {
|
||||
// const wordsDiff = useWordsDifference(textSubject, props.diffPreviousText, showDiff);
|
||||
|
||||
|
||||
const { onMessageAssistantFrom, onMessageDelete, onMessageFragmentAppend, onMessageFragmentDelete, onMessageFragmentReplace, onMessageContinue } = props;
|
||||
const { onMessageAssistantFrom, onMessageDelete, onMessageFragmentAppend, onMessageFragmentDelete, onMessageFragmentReplace, onMessageContinue, onMessageUpstreamResume, onMessageUpstreamDetach, onMessageUpstreamDelete } = props;
|
||||
|
||||
const handleFragmentNew = React.useCallback(() => {
|
||||
onMessageFragmentAppend?.(messageId, createTextContentFragment(''));
|
||||
@@ -261,6 +268,20 @@ export function ChatMessage(props: {
|
||||
onMessageContinue?.(messageId, continueText);
|
||||
}, [messageId, onMessageContinue]);
|
||||
|
||||
const handleUpstreamResume = React.useCallback((mode: AixReattachMode) => {
|
||||
if (!messageGenerator) return;
|
||||
return onMessageUpstreamResume?.(messageGenerator, messageId, mode);
|
||||
}, [messageGenerator, messageId, onMessageUpstreamResume]);
|
||||
|
||||
const handleUpstreamDetach = React.useCallback(() => {
|
||||
onMessageUpstreamDetach?.(messageId);
|
||||
}, [messageId, onMessageUpstreamDetach]);
|
||||
|
||||
const handleUpstreamDelete = React.useCallback(() => {
|
||||
if (!messageGenerator) return;
|
||||
return onMessageUpstreamDelete?.(messageGenerator, messageId);
|
||||
}, [messageGenerator, messageId, onMessageUpstreamDelete]);
|
||||
|
||||
|
||||
// Text Editing
|
||||
|
||||
@@ -359,6 +380,13 @@ export function ChatMessage(props: {
|
||||
onMessageToggleUserFlag?.(messageId, MESSAGE_FLAG_STARRED);
|
||||
}, [messageId, onMessageToggleUserFlag]);
|
||||
|
||||
const handleOpsShowInfo = React.useCallback(() => {
|
||||
setOpsMenuAnchor(null);
|
||||
setShowInfoModal(true);
|
||||
}, []);
|
||||
|
||||
const handleInfoClose = React.useCallback(() => setShowInfoModal(false), []);
|
||||
|
||||
const handleOpsToggleNotifyComplete = React.useCallback(() => {
|
||||
// also remember the preference, for auto-setting flags by the persona
|
||||
setIsNotificationEnabledForModel(messageId, !isUserNotifyComplete);
|
||||
@@ -877,13 +905,15 @@ export function ChatMessage(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Upstream Resume... */}
|
||||
{props.isBottom && fromAssistant && lastFragmentIsError && messageGenerator?.upstreamHandle?.responseId && (
|
||||
{/* Upstream Resume - shows whenever there's a stored handle (incl. post-reload, and while streaming so Stop can cancel the upstream run) */}
|
||||
{props.isBottom && fromAssistant && messageGenerator?.upstreamHandle && (!!onMessageUpstreamResume || !!onMessageUpstreamDelete) && (
|
||||
<BlockOpUpstreamResume
|
||||
upstreamHandle={messageGenerator.upstreamHandle}
|
||||
onResume={console.error}
|
||||
onCancel={console.error}
|
||||
onDelete={console.error}
|
||||
pending={messagePendingIncomplete}
|
||||
inFlightMode={props.upstreamResumeMode}
|
||||
onResume={onMessageUpstreamResume ? handleUpstreamResume : undefined}
|
||||
onDetach={onMessageUpstreamDetach ? handleUpstreamDetach : undefined}
|
||||
onDelete={onMessageUpstreamDelete ? handleUpstreamDelete : undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -972,18 +1002,15 @@ export function ChatMessage(props: {
|
||||
{/* Starred */}
|
||||
{!!onMessageToggleUserFlag && (
|
||||
<MenuItem onClick={handleOpsToggleStarred} sx={{ flexGrow: 0, px: 1 }}>
|
||||
<Tooltip disableInteractive title={!isUserStarred ? 'Link message - use @ to refer to it from another chat' : 'Remove link'}>
|
||||
{isUserStarred
|
||||
? <AlternateEmailIcon color='primary' sx={{ fontSize: 'xl' }} />
|
||||
: <InsertLinkIcon sx={{ rotate: '45deg' }} />
|
||||
}
|
||||
{/*{isUserStarred*/}
|
||||
{/* ? <StarRoundedIcon color='primary' sx={{ fontSize: 'xl2' }} />*/}
|
||||
{/* : <StarOutlineRoundedIcon sx={{ fontSize: 'xl2' }} />*/}
|
||||
{/*}*/}
|
||||
<Tooltip disableInteractive title={!isUserStarred ? 'Star message - use @ to refer to it from another chat' : 'Remove star'}>
|
||||
<StarredState isStarred={isUserStarred} />
|
||||
</Tooltip>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Info */}
|
||||
<MenuItem onClick={handleOpsShowInfo} sx={{ flexGrow: 0, px: 1 }}>
|
||||
<InfoOutlinedIcon sx={{ fontSize: 'xl' }} />
|
||||
</MenuItem>
|
||||
</Box>
|
||||
|
||||
{/* Notify Complete */}
|
||||
@@ -1240,6 +1267,16 @@ export function ChatMessage(props: {
|
||||
</CloseablePopup>
|
||||
)}
|
||||
|
||||
|
||||
{/* Message Info Modal */}
|
||||
{showInfoModal && (
|
||||
<ChatMessageInfoPopup
|
||||
open
|
||||
onClose={handleInfoClose}
|
||||
message={props.message}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box } from '@mui/joy';
|
||||
|
||||
import { llmsGetVendorIcon } from '~/modules/llms/components/LLMVendorIcon';
|
||||
|
||||
import type { DMessage } from '~/common/stores/chat/chat.message';
|
||||
import type { Immutable } from '~/common/types/immutable.types';
|
||||
import { GoodModal } from '~/common/components/modals/GoodModal';
|
||||
import { tooltipMetricsGridSx, prettyMessageMetrics, prettyShortChatModelName, prettyTokenStopReason } from '~/common/util/dMessageUtils';
|
||||
|
||||
|
||||
const contentSx: SxProps = {
|
||||
fontSize: 'sm',
|
||||
display: 'grid',
|
||||
gap: 1.5,
|
||||
};
|
||||
|
||||
const vendorIconContainerSx: SxProps = {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
};
|
||||
|
||||
const timestampSx: SxProps = {
|
||||
fontSize: 'xs',
|
||||
color: 'text.tertiary',
|
||||
};
|
||||
|
||||
|
||||
export function ChatMessageInfoPopup(props: {
|
||||
open: boolean,
|
||||
onClose: () => void,
|
||||
message: Immutable<DMessage>,
|
||||
}) {
|
||||
|
||||
const { message } = props;
|
||||
const { generator, created, updated, tokenCount, role } = message;
|
||||
|
||||
const isAix = generator?.mgt === 'aix';
|
||||
const vendorId = isAix ? generator.aix?.vId ?? null : null;
|
||||
const VendorIcon = vendorId ? llmsGetVendorIcon(vendorId) : null;
|
||||
const metrics = generator?.metrics ? prettyMessageMetrics(generator.metrics, 'extra') : null;
|
||||
const stopReason = generator?.tokenStopReason ? prettyTokenStopReason(generator.tokenStopReason, 'extra') : null;
|
||||
|
||||
return (
|
||||
<GoodModal
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
title='Message Info'
|
||||
hideBottomClose
|
||||
sx={{ minWidth: { xs: 300, sm: 400 }, maxWidth: 480 }}
|
||||
>
|
||||
<Box sx={contentSx}>
|
||||
|
||||
{/* Model / Generator */}
|
||||
{generator && (
|
||||
<Box sx={tooltipMetricsGridSx}>
|
||||
<div>Model:</div>
|
||||
<div>
|
||||
{VendorIcon
|
||||
? <Box sx={vendorIconContainerSx}><VendorIcon />{prettyShortChatModelName(generator.name)}</Box>
|
||||
: prettyShortChatModelName(generator.name)}
|
||||
</div>
|
||||
{isAix && generator.aix?.mId && <>
|
||||
<div>ID:</div>
|
||||
<div style={{ opacity: 0.75 }}>{generator.aix.mId}</div>
|
||||
</>}
|
||||
{generator.providerInfraLabel && <>
|
||||
<div>Provider:</div>
|
||||
<div>{generator.providerInfraLabel}</div>
|
||||
</>}
|
||||
{stopReason && <>
|
||||
<div>Status:</div>
|
||||
<div>{stopReason}</div>
|
||||
</>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Metrics (tokens, speed, cost, time) */}
|
||||
{metrics}
|
||||
|
||||
{/* Message metadata */}
|
||||
<Box sx={tooltipMetricsGridSx}>
|
||||
<div>Role:</div>
|
||||
<div>{role}</div>
|
||||
{tokenCount > 0 && <>
|
||||
<div>Tokens:</div>
|
||||
<div>{tokenCount.toLocaleString()} (visible text ~approx)</div>
|
||||
</>}
|
||||
</Box>
|
||||
|
||||
{/* Timestamps */}
|
||||
<Box sx={timestampSx}>
|
||||
{!!created && <div>Created <TimeAgo date={created} /> - {new Date(created).toLocaleString()}</div>}
|
||||
{!!updated && <div>Updated <TimeAgo date={updated} /> - {new Date(updated).toLocaleString()}</div>}
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
</GoodModal>
|
||||
);
|
||||
}
|
||||
@@ -5,13 +5,13 @@ import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import ErrorIcon from '@mui/icons-material/ErrorRounded';
|
||||
import ImageIcon from '@mui/icons-material/ImageRounded';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFieldsRounded';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
|
||||
import { DMessage, MESSAGE_FLAG_AIX_SKIP, messageFragmentsReduceText, messageHasUserFlag } from '~/common/stores/chat/chat.message';
|
||||
import { DMessageAttachmentFragment, DMessageFragment, isAttachmentFragment, isContentFragment, isImageRefPart, isZyncAssetImageReferencePart } from '~/common/stores/chat/chat.fragments';
|
||||
import { PhImageSquare } from '~/common/components/icons/phosphor/PhImageSquare';
|
||||
import { makeMessageAvatarIcon, messageBackground } from '~/common/util/dMessageUtils';
|
||||
|
||||
import { TokenBadgeMemo } from '../composer/tokens/TokenBadge';
|
||||
@@ -273,7 +273,7 @@ export function CleanerMessage(props: { message: DMessage, selected: boolean, re
|
||||
</Chip>
|
||||
)}
|
||||
{analysis.imageCount > 0 && (
|
||||
<Chip size='sm' variant='solid' color='success' startDecorator={<ImageIcon />} sx={{ px: 1 }}>
|
||||
<Chip size='sm' variant='solid' color='success' startDecorator={<PhImageSquare />} sx={{ px: 1 }}>
|
||||
{analysis.imageCount} image{analysis.imageCount > 1 ? 's' : ''}
|
||||
</Chip>
|
||||
)}
|
||||
|
||||
+3
-3
@@ -5,7 +5,6 @@ import { Box, Button, ColorPaletteProp } from '@mui/joy';
|
||||
import AbcIcon from '@mui/icons-material/Abc';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
import TextureIcon from '@mui/icons-material/Texture';
|
||||
@@ -13,6 +12,7 @@ import TextureIcon from '@mui/icons-material/Texture';
|
||||
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { DMessageAttachmentFragment, DMessageFragmentId, DVMimeType, isDocPart } from '~/common/stores/chat/chat.fragments';
|
||||
import { LiveFileIcon } from '~/common/livefile/liveFile.icons';
|
||||
import { PhImageSquare } from '~/common/components/icons/phosphor/PhImageSquare';
|
||||
import { PhVoice } from '~/common/components/icons/phosphor/PhVoice';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { ellipsizeMiddle } from '~/common/util/textUtils';
|
||||
@@ -48,7 +48,7 @@ export function buttonIconForFragment(part: DMessageAttachmentFragment['part']):
|
||||
const assetType = part.assetType;
|
||||
switch (assetType) {
|
||||
case 'image':
|
||||
return ImageOutlinedIcon;
|
||||
return PhImageSquare;
|
||||
case 'audio':
|
||||
return PhVoice;
|
||||
default:
|
||||
@@ -93,7 +93,7 @@ export function buttonIconForFragment(part: DMessageAttachmentFragment['part']):
|
||||
|
||||
// [OLD-style] Image Attachment Fragment
|
||||
case 'image_ref':
|
||||
return ImageOutlinedIcon;
|
||||
return PhImageSquare;
|
||||
|
||||
case '_pt_sentinel':
|
||||
return TextureIcon; // nothing to do here - this is a sentinel type
|
||||
|
||||
@@ -21,11 +21,15 @@ export function BlockPartError(props: {
|
||||
// special error presentation, based on hints
|
||||
switch (props.errorHint) {
|
||||
case 'aix-net-disconnected':
|
||||
// determine the 2 'kinds' of disconnection errors in aix.client.ts
|
||||
// determine the 'kinds' of disconnection errors in aix.client.ts
|
||||
// - 'network error' (browser) -> client side
|
||||
// - 'connection terminated' (tRPC 'Stream closed' wrapper) -> server/edge side (CSF recovery)
|
||||
// - 'upstream dropped' (undici TypeError 'terminated') -> upstream provider socket drop (CSF recovery applies)
|
||||
const kind =
|
||||
props.errorText.includes('**network error**') ? 'net-client-closed'
|
||||
: props.errorText.includes('**connection terminated**') ? 'net-server-closed'
|
||||
: 'net-unknown-closed';
|
||||
: props.errorText.includes('**upstream dropped**') ? 'net-server-closed'
|
||||
: 'net-unknown-closed';
|
||||
|
||||
// For client-side error, we don't show the _NetDisconnected component
|
||||
if (kind === 'net-client-closed')
|
||||
|
||||
+3
-1
@@ -36,7 +36,9 @@ export function BlockPartError_RequestExceeded(props: {
|
||||
Request Too Large
|
||||
</Box>
|
||||
<div>
|
||||
Your message or attachments exceed the limit of the Vercel edge network
|
||||
Your message or attachments exceed the limit
|
||||
of the Vercel edge network
|
||||
{/* Note: Assumption here - since explaing to any 413, it could be any network */}
|
||||
</div>
|
||||
|
||||
{/* Recovery options */}
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { Box, Checkbox, CircularProgress, Dropdown, IconButton, ListDivider, ListItemDecorator, Menu, MenuButton, MenuItem, Sheet, Typography } from '@mui/joy';
|
||||
import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
|
||||
|
||||
import type { AnthropicAccessSchema } from '~/modules/llms/server/anthropic/anthropic.access';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { apiAsync, apiQuery } from '~/common/util/trpc.client';
|
||||
import { convert_Base64_To_UInt8Array } from '~/common/util/blobUtils';
|
||||
import { createTextContentFragment, DMessageContentFragment, DMessageFragmentId, DMessageHostedResourcePart } from '~/common/stores/chat/chat.fragments';
|
||||
import { copyBlobPromiseToClipboard, copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { downloadBlob } from '~/common/util/downloadUtils';
|
||||
import { humanReadableBytes } from '~/common/util/textUtils';
|
||||
import { mimeTypeIsPlainText, mimeTypeIsSupportedImage } from '~/common/attachment-drafts/attachment.mimetypes';
|
||||
import { useAIPreferencesStore } from '~/common/stores/store-ai';
|
||||
import { useLlmServiceAccess } from '~/common/stores/llms/hooks/useLlmServiceAccess';
|
||||
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
|
||||
|
||||
|
||||
// -- react-query enrichers - stable select functions --
|
||||
|
||||
function _enrichMetadataWithMimeFlags<T extends { mime_type: string }>(meta: T) {
|
||||
return {
|
||||
...meta,
|
||||
mimeIsText: mimeTypeIsPlainText(meta.mime_type),
|
||||
mimeIsImage: mimeTypeIsSupportedImage(meta.mime_type),
|
||||
};
|
||||
}
|
||||
|
||||
function _base64ResponseToBlob({ base64Data, mimeType }: { base64Data: string; mimeType: string }) {
|
||||
const bytes = convert_Base64_To_UInt8Array(base64Data, 'hosted-resource-ant-file');
|
||||
return {
|
||||
blob: new Blob([bytes], { type: mimeType }),
|
||||
httpMimeType: mimeType,
|
||||
httpMimeIsText: mimeTypeIsPlainText(mimeType),
|
||||
httpMimeIsImage: mimeTypeIsSupportedImage(mimeType),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function AnthropicFileChip(props: {
|
||||
access: AnthropicAccessSchema,
|
||||
fileId: string,
|
||||
contentScaling: ContentScaling,
|
||||
onFragmentDelete?: () => void,
|
||||
onFragmentReplace?: (newFragment: DMessageContentFragment) => void,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [busy, setBusy] = React.useState<false | 'download' | 'copy' | 'delete' | 'inline'>(false);
|
||||
const [actionError, setActionError] = React.useState<string | null>(null);
|
||||
const { showPromisedOverlay } = useOverlayComponents();
|
||||
|
||||
// props
|
||||
const { access, fileId, onFragmentDelete, onFragmentReplace } = props;
|
||||
|
||||
// external state
|
||||
const autoEmbedEnabled = useAIPreferencesStore(state => state.vndAntInlineFiles !== 'off');
|
||||
const { data: metadata, isLoading: metaLoading, error: metaError } = apiQuery.llmAnthropic.fileApiGetMetadata.useQuery({ access, fileId }, {
|
||||
staleTime: Infinity,
|
||||
select: _enrichMetadataWithMimeFlags,
|
||||
});
|
||||
const { data: fileContent, refetch: refetchFileContent } = apiQuery.llmAnthropic.fileApiDownload.useQuery({ access, fileId }, {
|
||||
enabled: false, // on-demand only
|
||||
select: _base64ResponseToBlob,
|
||||
});
|
||||
|
||||
|
||||
// derive display info from typed metadata
|
||||
const fileName = metadata?.filename || fileId;
|
||||
const displayName = fileName.length > 40 ? fileName.slice(0, 20) + '...' + fileName.slice(-15) : fileName;
|
||||
|
||||
|
||||
// handlers
|
||||
|
||||
const handleDownload = React.useCallback(async () => {
|
||||
setBusy('download');
|
||||
setActionError(null);
|
||||
try {
|
||||
const data = fileContent || (await refetchFileContent({ cancelRefetch: false, throwOnError: true })).data;
|
||||
data && downloadBlob(data.blob, fileName);
|
||||
} catch (error: any) {
|
||||
setActionError(error?.message || 'Download failed');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [fileContent, refetchFileContent, fileName]);
|
||||
|
||||
const handleCopy = React.useCallback(async () => {
|
||||
setBusy('copy');
|
||||
setActionError(null);
|
||||
try {
|
||||
const data = fileContent || (await refetchFileContent({ cancelRefetch: false, throwOnError: true })).data;
|
||||
if (!data) return;
|
||||
if (data.httpMimeIsText)
|
||||
copyToClipboard(await data.blob.text(), fileName);
|
||||
else
|
||||
copyBlobPromiseToClipboard(data.httpMimeType, Promise.resolve(data.blob), fileName);
|
||||
} catch (error: any) {
|
||||
setActionError(error?.message || 'Copy failed');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [fileContent, refetchFileContent, fileName]);
|
||||
|
||||
const handleDelete = React.useCallback(async (event: React.MouseEvent) => {
|
||||
if (!onFragmentDelete) return;
|
||||
if (!event.shiftKey && !await showPromisedOverlay('chat-message-delete-hosted-resource', { rejectWithValue: false }, ({ onResolve, onUserReject }) =>
|
||||
<ConfirmationModal
|
||||
open onClose={onUserReject} onPositive={() => onResolve(true)}
|
||||
confirmationText={<>Delete "{fileName}" from Anthropic servers?<br />This action cannot be undone.</>}
|
||||
positiveActionText='Delete'
|
||||
/>,
|
||||
)) return;
|
||||
setBusy('delete');
|
||||
setActionError(null);
|
||||
try {
|
||||
// remote deletion
|
||||
await apiAsync.llmAnthropic.fileApiDelete.mutate({ access, fileId });
|
||||
// fragment removal
|
||||
onFragmentDelete();
|
||||
} catch (error: any) {
|
||||
setActionError(error?.message || 'Delete failed');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [access, fileId, fileName, onFragmentDelete, showPromisedOverlay]);
|
||||
|
||||
|
||||
const handleInline = React.useCallback(async () => {
|
||||
if (!onFragmentReplace) return;
|
||||
setBusy('inline');
|
||||
setActionError(null);
|
||||
try {
|
||||
const data = fileContent || (await refetchFileContent({ cancelRefetch: false, throwOnError: true })).data;
|
||||
if (!data) return;
|
||||
|
||||
// text: inline as fenced code block
|
||||
if (data.httpMimeIsText) {
|
||||
const text = await data.blob.text();
|
||||
|
||||
// fence with adaptive depth (extra backticks if content contains ```)
|
||||
let fence = '```';
|
||||
while (text.includes(fence) && fence.length < 10)
|
||||
fence += '`';
|
||||
onFragmentReplace(createTextContentFragment(`${fence}${fileName}\n${text}\n${fence}\n`));
|
||||
}
|
||||
// image: get dimensions, store in DBlob, and create a Zync asset reference
|
||||
// else if (data.httpMimeIsImage) {
|
||||
//
|
||||
// const { width, height } = await imageBlobGetDimensions(data.blob).catch(() => ({ width: 0, height: 0 }));
|
||||
//
|
||||
// const dblobAssetId = await addDBImageAsset('app-chat', data.blob, {
|
||||
// label: fileName,
|
||||
// origin: { ot: 'generated', source: 'ai-text-to-image', generatorName: 'anthropic-code-execution', prompt: '', parameters: {}, generatedAt: new Date().toISOString() },
|
||||
// metadata: { width, height },
|
||||
// });
|
||||
//
|
||||
// onFragmentReplace(createZyncAssetReferenceContentFragment(
|
||||
// nanoidToUuidV4(dblobAssetId, 'convert-dblob-to-dasset'),
|
||||
// fileName,
|
||||
// 'image',
|
||||
// {
|
||||
// pt: 'image_ref',
|
||||
// dataRef: createDMessageDataRefDBlob(dblobAssetId, data.httpMimeType, data.blob.size),
|
||||
// ...(fileName ? { altText: fileName } : {}),
|
||||
// ...(width ? { width } : {}),
|
||||
// ...(height ? { height } : {}),
|
||||
// },
|
||||
// ));
|
||||
// }
|
||||
else
|
||||
return setActionError('Cannot inline this file type');
|
||||
|
||||
// fire-and-forget: delete from provider
|
||||
apiAsync.llmAnthropic.fileApiDelete.mutate({ access, fileId }).catch(console.error);
|
||||
} catch (error: any) {
|
||||
setActionError(error?.message || 'Inline failed');
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}, [fileContent, refetchFileContent, access, fileId, fileName, onFragmentReplace]);
|
||||
|
||||
|
||||
const handleToggleAutoEmbed = React.useCallback(async () => {
|
||||
if (autoEmbedEnabled)
|
||||
return useAIPreferencesStore.getState().setVndAntInlineFiles('off');
|
||||
if (await showPromisedOverlay('chat-message-auto-embed-notice', { rejectWithValue: false }, ({ onResolve, onUserReject }) =>
|
||||
<ConfirmationModal
|
||||
open onClose={onUserReject} onPositive={() => onResolve(true)}
|
||||
noTitleBar
|
||||
lowStakes
|
||||
confirmationText={<>
|
||||
From now on, files generated by Claude tools (code execution, etc.) will be automatically downloaded and embedded into messages, then removed from Anthropic's File API.
|
||||
<br /><br />
|
||||
You can change this anytime in <b>Settings > Chat AI > Anthropic File Inlining</b>.
|
||||
</>}
|
||||
positiveActionText='Enable & Embed'
|
||||
negativeActionText='Cancel'
|
||||
/>,
|
||||
)) {
|
||||
useAIPreferencesStore.getState().setVndAntInlineFiles('inline-file-and-delete');
|
||||
await handleInline();
|
||||
}
|
||||
}, [autoEmbedEnabled, handleInline, showPromisedOverlay]);
|
||||
|
||||
|
||||
const canCopy = !!metadata?.mimeIsText || !!metadata?.mimeIsImage;
|
||||
const canInline = !!onFragmentReplace && !!metadata?.mimeIsText; // for images, replace with ... && canCopy
|
||||
|
||||
const isBusy = !!busy || metaLoading;
|
||||
const hasError = !!metaError || !!actionError;
|
||||
const isFileGone = !!metaError && typeof metaError === 'object' && 'data' in metaError && (metaError.data?.httpStatus === 404 || metaError.data?.aixFHttpStatus === 404);
|
||||
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
variant='soft'
|
||||
color='primary'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
mx: 1.5,
|
||||
px: 1.125,
|
||||
py: 0.5,
|
||||
borderRadius: 'sm',
|
||||
overflow: 'hidden',
|
||||
maxWidth: '100%',
|
||||
boxShadow: 'inset 1px 2px 2px -2px rgba(0, 0, 0, 0.2)',
|
||||
}}
|
||||
>
|
||||
<AttachFileRoundedIcon sx={{ fontSize: 'lg', opacity: 0.5 }} />
|
||||
|
||||
<Box sx={{ minWidth: 0, flex: 1 }}>
|
||||
<Box className='agi-ellipsize' sx={{ fontSize: 'sm', fontWeight: 'md', color: hasError ? 'var(--joy-palette-danger-plainColor)' : undefined }}>
|
||||
{metaLoading ? 'Loading...' : isFileGone ? `${fileId} - file no longer available` : hasError ? `${displayName} - ${actionError || metaError?.message || 'Could not load file info'}` : displayName}
|
||||
</Box>
|
||||
{metadata && (
|
||||
<Box sx={{ fontSize: 'xs', opacity: 0.6 }}>
|
||||
{humanReadableBytes(metadata.size_bytes)} · <TimeAgo date={metadata.created_at} /> · {metadata.mime_type}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!isFileGone ? <>
|
||||
|
||||
{canCopy && (
|
||||
<GoodTooltip title='Copy to clipboard'>
|
||||
<IconButton variant='soft' color='primary' disabled={isBusy} onClick={handleCopy} size='sm'>
|
||||
{busy === 'copy' ? <CircularProgress size='sm' /> : <ContentCopyIcon sx={{ fontSize: 'lg' }} />}
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
)}
|
||||
{/*{canInline && (*/}
|
||||
{/* <GoodTooltip title='Embed in chat'>*/}
|
||||
{/* <IconButton variant='soft' color='primary' disabled={isBusy} onClick={handleInline} size='sm'>*/}
|
||||
{/* {busy === 'inline' ? <CircularProgress size='sm' /> : <VerticalAlignBottomIcon sx={{ fontSize: 'lg' }} />}*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </GoodTooltip>*/}
|
||||
{/*)}*/}
|
||||
<GoodTooltip title='Download file'>
|
||||
<IconButton variant='soft' color='primary' disabled={isBusy || isFileGone} onClick={handleDownload} size='sm'>
|
||||
{busy === 'download' ? <CircularProgress size='sm' /> : <DownloadIcon sx={{ fontSize: 'lg' }} />}
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
{(onFragmentDelete || onFragmentReplace) && (
|
||||
<Dropdown>
|
||||
<MenuButton slots={{ root: IconButton }} slotProps={{ root: { variant: 'soft', color: 'primary', size: 'sm', disabled: isBusy && busy !== 'inline' } }}>
|
||||
{(busy === 'delete' || busy === 'inline') ? <CircularProgress size='sm' /> : <MoreVertIcon sx={{ fontSize: 'lg' }} />}
|
||||
</MenuButton>
|
||||
<Menu placement='bottom-end' sx={{ minWidth: 220 }}>
|
||||
{/* Inline as doc attachment */}
|
||||
<MenuItem disabled={!canInline || isBusy} onClick={handleInline}>
|
||||
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
|
||||
<div>
|
||||
Embed
|
||||
{!canInline && <Typography level='body-xs' sx={{ opacity: 0.6 }}>
|
||||
File type not supported
|
||||
</Typography>}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/* Auto-embed toggle - shared global preference */}
|
||||
{!autoEmbedEnabled && <>
|
||||
<MenuItem disabled={!canInline || isBusy} onClick={handleToggleAutoEmbed}>
|
||||
<ListItemDecorator><Checkbox checked={autoEmbedEnabled} readOnly color='neutral' /></ListItemDecorator>
|
||||
<div>
|
||||
Always embed
|
||||
<Typography level='body-xs' sx={{ opacity: 0.6 }}>
|
||||
Change anytime in Settings
|
||||
</Typography>
|
||||
</div>
|
||||
</MenuItem>
|
||||
</>}
|
||||
{!!onFragmentDelete && <ListDivider />}
|
||||
{/* Delete from provider */}
|
||||
{!!onFragmentDelete && (
|
||||
<MenuItem color='danger' disabled={isBusy} onClick={handleDelete}>
|
||||
<ListItemDecorator><DeleteOutlineIcon /></ListItemDecorator>
|
||||
Delete
|
||||
</MenuItem>
|
||||
)}
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
</> : onFragmentDelete && (
|
||||
<GoodTooltip title='Remove from message'>
|
||||
<IconButton variant='plain' color='danger' onClick={onFragmentDelete} size='sm'>
|
||||
<DeleteOutlineIcon sx={{ fontSize: 'lg' }} />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
)}
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function NoAccessChip(props: { fileId: string }) {
|
||||
return (
|
||||
<Sheet variant='outlined' sx={{ display: 'inline-flex', alignItems: 'center', gap: 1, px: 1.5, py: 0.5, borderRadius: 'sm' }}>
|
||||
<AttachFileRoundedIcon sx={{ fontSize: 'lg', opacity: 0.4 }} />
|
||||
<Typography level='body-sm' sx={{ opacity: 0.5 }}>
|
||||
{props.fileId} (no credentials)
|
||||
</Typography>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function BlockPartHostedResource(props: {
|
||||
hostedResourcePart: DMessageHostedResourcePart,
|
||||
fragmentId: DMessageFragmentId,
|
||||
messageGeneratorLlmId?: string | null,
|
||||
contentScaling: ContentScaling,
|
||||
onFragmentDelete?: (fragmentId: DMessageFragmentId) => void,
|
||||
onFragmentReplace?: (fragmentId: DMessageFragmentId, newFragment: DMessageContentFragment) => void,
|
||||
}) {
|
||||
|
||||
const { resource } = props.hostedResourcePart;
|
||||
const { fragmentId, onFragmentDelete, onFragmentReplace } = props;
|
||||
|
||||
const handleFragmentDelete = React.useCallback(() => {
|
||||
onFragmentDelete?.(fragmentId);
|
||||
}, [fragmentId, onFragmentDelete]);
|
||||
|
||||
const handleFragmentReplace = React.useCallback((newFragment: DMessageContentFragment) => {
|
||||
onFragmentReplace?.(fragmentId, newFragment);
|
||||
}, [fragmentId, onFragmentReplace]);
|
||||
|
||||
// TODO: OpenAI container_file_citation support (via: 'openai' with fileId + containerId)?
|
||||
|
||||
// reactive service + access resolution
|
||||
const isAnthropic = resource.via === 'anthropic';
|
||||
const antAccess = useLlmServiceAccess(isAnthropic ? props.messageGeneratorLlmId : undefined, 'anthropic');
|
||||
|
||||
// only support Anthropic files for now
|
||||
if (!isAnthropic || !antAccess)
|
||||
return <NoAccessChip fileId={resource?.fileId || 'unknown'} />;
|
||||
|
||||
return (
|
||||
<AnthropicFileChip
|
||||
access={antAccess}
|
||||
fileId={resource.fileId}
|
||||
contentScaling={props.contentScaling}
|
||||
onFragmentDelete={onFragmentDelete ? handleFragmentDelete : undefined}
|
||||
onFragmentReplace={onFragmentReplace ? handleFragmentReplace : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -31,6 +31,7 @@ export function BlockPartText_AutoBlocks(props: {
|
||||
|
||||
showUnsafeHtmlCode?: boolean,
|
||||
optiAllowSubBlocksMemo: boolean,
|
||||
optiStreamingLastFragment?: boolean,
|
||||
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
onDoubleClick?: (event: React.MouseEvent) => void;
|
||||
@@ -77,6 +78,7 @@ export function BlockPartText_AutoBlocks(props: {
|
||||
codeRenderVariant='enhanced' // was: { props.enhanceCodeBlocks ? 'enhanced' : 'outlined' }
|
||||
textRenderVariant={props.disableMarkdownText ? 'text' : 'markdown'}
|
||||
optiAllowSubBlocksMemo={props.optiAllowSubBlocksMemo}
|
||||
optiStreamingLastFragment={props.optiStreamingLastFragment}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDoubleClick={props.onDoubleClick}
|
||||
setText={!props.setEditedText ? undefined : handleSetText}
|
||||
|
||||
@@ -14,8 +14,9 @@ import type { ChatMessageTextPartEditState } from '../ChatMessage';
|
||||
import { BlockEdit_TextFragment } from './BlockEdit_TextFragment';
|
||||
import { BlockOpEmpty } from './BlockOpEmpty';
|
||||
import { BlockPartError } from './BlockPartError';
|
||||
import { BlockPartHostedResource } from './BlockPartHostedResource';
|
||||
import { BlockPartImageRef } from './BlockPartImageRef';
|
||||
import { BlockPartModelAux } from '../fragments-void/BlockPartModelAux';
|
||||
import { BlockPartModelAux, BlockPartModelAuxMemo } from '../fragments-void/BlockPartModelAux';
|
||||
import { BlockPartPlaceholder } from '../fragments-void/BlockPartPlaceholder';
|
||||
import { BlockPartText_AutoBlocks } from './BlockPartText_AutoBlocks';
|
||||
import { BlockPartToolInvocation } from './BlockPartToolInvocation';
|
||||
@@ -86,6 +87,7 @@ export function ContentFragments(props: {
|
||||
// solo placeholder - dataStreamViz trigger
|
||||
const showDataStreamViz =
|
||||
!Release.Features.LIGHTER_ANIMATIONS
|
||||
&& !!props.messagePendingIncomplete // if generating
|
||||
&& props.uiComplexityMode !== 'minimal'
|
||||
&& props.contentFragments.length === 1
|
||||
// && props.noVoidFragments // not needed, we have all the interleaved fragments here
|
||||
@@ -133,6 +135,8 @@ export function ContentFragments(props: {
|
||||
|
||||
// simplify
|
||||
const { fId, ft } = fragment;
|
||||
const isLastFragment = fragmentIndex === props.contentFragments.length - 1;
|
||||
const optimizeMemoBeforeLastBlock = props.optiAllowSubBlocksMemo === true && !isLastFragment;
|
||||
|
||||
// VOID FRAGMENTS (reasoning, placeholders - interleaved with content)
|
||||
if (ft === 'void') {
|
||||
@@ -145,8 +149,13 @@ export function ContentFragments(props: {
|
||||
// return null;
|
||||
|
||||
case 'ma':
|
||||
// skip rendering empty reasoning fragments (created as vehicles for vendor state / reasoning continuity)
|
||||
const isActivelyStreaming = isLastFragment && !!props.messagePendingIncomplete;
|
||||
if (!part.aText && !part.redactedData?.length && !isActivelyStreaming)
|
||||
return null;
|
||||
const BlockPartModelAuxMemoOrNot = optimizeMemoBeforeLastBlock ? BlockPartModelAuxMemo : BlockPartModelAux;
|
||||
return (
|
||||
<BlockPartModelAux
|
||||
<BlockPartModelAuxMemoOrNot
|
||||
key={fId}
|
||||
fragmentId={fId}
|
||||
auxType={part.aType}
|
||||
@@ -156,7 +165,7 @@ export function ContentFragments(props: {
|
||||
messagePendingIncomplete={!!props.messagePendingIncomplete}
|
||||
zenMode={props.uiComplexityMode === 'minimal'}
|
||||
contentScaling={props.contentScaling}
|
||||
isLastFragment={fragmentIndex === props.contentFragments.length - 1}
|
||||
isLastFragment={isLastFragment}
|
||||
onFragmentDelete={props.onFragmentDelete}
|
||||
onFragmentReplace={props.onFragmentReplace}
|
||||
/>
|
||||
@@ -166,14 +175,13 @@ export function ContentFragments(props: {
|
||||
return (
|
||||
<BlockPartPlaceholder
|
||||
key={fId}
|
||||
placeholderText={part.pText}
|
||||
placeholderType={part.pType}
|
||||
placeholderModelOp={part.modelOp}
|
||||
placeholderAixControl={part.aixControl}
|
||||
messageRole={props.messageRole}
|
||||
fragmentId={fId}
|
||||
placeholderPart={part}
|
||||
contentScaling={props.contentScaling}
|
||||
showAsItalic
|
||||
messagePendingIncomplete={!!props.messagePendingIncomplete}
|
||||
showAsDataStreamViz={showDataStreamViz}
|
||||
zenMode={props.uiComplexityMode === 'minimal'}
|
||||
onFragmentDelete={props.messagePendingIncomplete ? undefined : props.onFragmentDelete}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -335,6 +343,7 @@ export function ContentFragments(props: {
|
||||
// renderWordsDiff={wordsDiff || undefined}
|
||||
showUnsafeHtmlCode={props.showUnsafeHtmlCode}
|
||||
optiAllowSubBlocksMemo={!!props.optiAllowSubBlocksMemo}
|
||||
optiStreamingLastFragment={!!props.optiAllowSubBlocksMemo && isLastFragment && props.uiComplexityMode === 'minimal'}
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDoubleClick={props.onDoubleClick}
|
||||
/>
|
||||
@@ -360,6 +369,19 @@ export function ContentFragments(props: {
|
||||
/>
|
||||
);
|
||||
|
||||
case 'hosted_resource':
|
||||
return (
|
||||
<BlockPartHostedResource
|
||||
key={fId}
|
||||
hostedResourcePart={part}
|
||||
fragmentId={fId}
|
||||
messageGeneratorLlmId={props.messageGeneratorLlmId}
|
||||
contentScaling={props.contentScaling}
|
||||
onFragmentDelete={props.onFragmentDelete}
|
||||
onFragmentReplace={props.onFragmentReplace}
|
||||
/>
|
||||
);
|
||||
|
||||
case '_pt_sentinel':
|
||||
return null;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ColorPaletteProp } from '@mui/joy/styles/types';
|
||||
import type { ColorPaletteProp, SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Chip, Typography } from '@mui/joy';
|
||||
import AllInclusiveIcon from '@mui/icons-material/AllInclusive';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
@@ -11,7 +11,7 @@ import { useScaledTypographySx } from '~/modules/blocks/blocks.styles';
|
||||
|
||||
import { ConfirmationModal } from '~/common/components/modals/ConfirmationModal';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
import { adjustContentScaling, ContentScaling } from '~/common/app.theme';
|
||||
import { adjustContentScaling, ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { animationSpinHalfPause } from '~/common/util/animUtils';
|
||||
import { createTextContentFragment, DMessageContentFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
|
||||
import { useOverlayComponents } from '~/common/layout/overlays/useOverlayComponents';
|
||||
@@ -32,35 +32,32 @@ const _styles = {
|
||||
},
|
||||
|
||||
chip: {
|
||||
px: 1.5,
|
||||
py: 0.375,
|
||||
pl: 1.5,
|
||||
pr: 1.75,
|
||||
my: '1px', // to not crop the outline on mobile, or on beam
|
||||
minHeight: '1.5rem', // similar parts, modelOps and paired tools, are 1.75rem
|
||||
'& .MuiChip-startDecorator': {
|
||||
marginRight: '0.5em',
|
||||
},
|
||||
},
|
||||
|
||||
chipActive: {
|
||||
outline: '1px solid',
|
||||
outlineColor: `${REASONING_COLOR}.solidBg`, // .outlinedBorder
|
||||
boxShadow: `1px 2px 4px -3px var(--joy-palette-${REASONING_COLOR}-solidBg)`,
|
||||
// '& > button': {
|
||||
// boxShadow: `inset 1px 2px 4px -3px var(--joy-palette-${REASONING_COLOR}-solidBg)`,
|
||||
// },
|
||||
},
|
||||
|
||||
chipDisabled: {
|
||||
px: 1.5,
|
||||
py: 0.375,
|
||||
my: '1px', // to not crop the outline on mobile, or on beam
|
||||
},
|
||||
|
||||
chipIcon: {
|
||||
fontSize: '1rem',
|
||||
mr: 0.5,
|
||||
},
|
||||
|
||||
chipIcon: undefined, // { fontSize: '1rem', },
|
||||
chipIconPending: {
|
||||
fontSize: '1rem',
|
||||
mr: 0.5,
|
||||
// fontSize: '1rem',
|
||||
animation: `${animationSpinHalfPause} 2s ease-in-out infinite`,
|
||||
},
|
||||
|
||||
chipExpanded: {
|
||||
mt: '1px', // need to copy the `chip` mt
|
||||
px: 1.5,
|
||||
py: 0.375,
|
||||
// borderRadius: 'sm',
|
||||
// transition: 'border-radius 0.2s ease-in-out',
|
||||
},
|
||||
@@ -94,13 +91,12 @@ const _styles = {
|
||||
// borderRadius: 'sm',
|
||||
// fontSize: 'xs',
|
||||
},
|
||||
|
||||
} as const;
|
||||
|
||||
|
||||
/** Detect if content is potentially markdown based on common markdown patterns */
|
||||
function _maybeMarkdownReasoning(trimmed: string): boolean {
|
||||
// const trimmed = text.trimStart();
|
||||
function _maybeMarkdownReasoning(text: string): boolean {
|
||||
const trimmed = text.trimStart();
|
||||
return trimmed.startsWith('**')
|
||||
|| trimmed.startsWith('# ')
|
||||
// || trimmed.startsWith('* ')
|
||||
@@ -109,6 +105,8 @@ function _maybeMarkdownReasoning(trimmed: string): boolean {
|
||||
}
|
||||
|
||||
|
||||
export const BlockPartModelAuxMemo = React.memo(BlockPartModelAux);
|
||||
|
||||
export function BlockPartModelAux(props: {
|
||||
fragmentId: DMessageFragmentId,
|
||||
auxType: 'reasoning' | string,
|
||||
@@ -130,17 +128,28 @@ export function BlockPartModelAux(props: {
|
||||
// external state
|
||||
const { showPromisedOverlay } = useOverlayComponents();
|
||||
|
||||
// derived
|
||||
const isActive = props.isLastFragment && props.messagePendingIncomplete;
|
||||
const contentScaling = adjustContentScaling(props.contentScaling, -1);
|
||||
const typeText = props.auxType === 'reasoning' ? 'Reasoning' : 'Auxiliary';
|
||||
|
||||
// memo
|
||||
const scaledTypographySx = useScaledTypographySx(adjustContentScaling(props.contentScaling, -1), false, false);
|
||||
const maybeMarkdown = React.useMemo(() => !ENABLE_MARKDOWN_DETECTION || neverExpanded ? false : _maybeMarkdownReasoning(props.auxText), [neverExpanded, props.auxText]);
|
||||
|
||||
// memo style
|
||||
const chipSx: SxProps = React.useMemo(() => ({
|
||||
..._styles.chip,
|
||||
...(isActive && _styles.chipActive),
|
||||
...(expanded && _styles.chipExpanded),
|
||||
fontSize: themeScalingMap[contentScaling]?.blockFontSize ?? undefined,
|
||||
}), [contentScaling, expanded, isActive]);
|
||||
const scaledTypographySx = useScaledTypographySx(contentScaling, false, false);
|
||||
const textSx = React.useMemo(() => ({
|
||||
..._styles.text,
|
||||
...scaledTypographySx,
|
||||
...(maybeMarkdown ? _styles.textUndoWhitespace : {}),
|
||||
}), [maybeMarkdown, scaledTypographySx]);
|
||||
|
||||
let typeText = props.auxType === 'reasoning' ? 'Reasoning' : 'Auxiliary';
|
||||
|
||||
|
||||
// handlers
|
||||
|
||||
@@ -196,20 +205,21 @@ export function BlockPartModelAux(props: {
|
||||
{/* Chip to expand/collapse */}
|
||||
<Box data-agi-no-copy /* do not copy these buttons */ sx={{ display: 'flex', flexWrap: 'wrap', gap: 1, alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Chip
|
||||
color={props.isLastFragment ? REASONING_COLOR : 'neutral'}
|
||||
variant={expanded ? 'solid' : 'soft'}
|
||||
size='sm'
|
||||
color={isActive || expanded ? REASONING_COLOR : 'neutral'}
|
||||
variant={expanded ? 'solid' : 'soft'}
|
||||
onClick={handleToggleExpanded}
|
||||
sx={expanded ? _styles.chipExpanded : props.isLastFragment ? _styles.chip : _styles.chipDisabled}
|
||||
sx={chipSx}
|
||||
startDecorator={
|
||||
<AllInclusiveIcon
|
||||
sx={(props.messagePendingIncomplete && !expanded && props.isLastFragment) ? _styles.chipIconPending : _styles.chipIcon}
|
||||
sx={!expanded && isActive ? _styles.chipIconPending : _styles.chipIcon}
|
||||
/* sx={{ color: expanded ? undefined : REASONING_COLOR }} */
|
||||
/>
|
||||
}
|
||||
// startDecorator='🧠'
|
||||
>
|
||||
Show {typeText}
|
||||
{/*Show {typeText}*/}
|
||||
{isActive && !expanded && typeText === 'Reasoning' ? `${typeText}...` : `Show ${typeText}`}
|
||||
</Chip>
|
||||
|
||||
{expanded && !props.messagePendingIncomplete && (showInline || showDelete) && !!props.auxText && (
|
||||
@@ -223,7 +233,8 @@ export function BlockPartModelAux(props: {
|
||||
disabled={!onFragmentReplace /* || props.messagePendingIncomplete */}
|
||||
onClick={!onFragmentReplace ? undefined : handleInline}
|
||||
endDecorator={<TextFieldsIcon />}
|
||||
sx={(!onFragmentReplace /* || props.messagePendingIncomplete */) ? _styles.chipDisabled : _styles.chip}
|
||||
sx={_styles.chip}
|
||||
// sx={(!onFragmentReplace /* || props.messagePendingIncomplete */) ? _styles.chipDisabled : _styles.chip}
|
||||
>
|
||||
Make Regular Text
|
||||
</Chip>}
|
||||
@@ -236,7 +247,8 @@ export function BlockPartModelAux(props: {
|
||||
disabled={!onFragmentDelete /* || props.messagePendingIncomplete */}
|
||||
onClick={!onFragmentDelete ? undefined : handleDelete}
|
||||
endDecorator={<DeleteOutlineIcon />}
|
||||
sx={(!onFragmentDelete /* || props.messagePendingIncomplete */) ? _styles.chipDisabled : _styles.chip}
|
||||
sx={_styles.chip}
|
||||
// sx={(!onFragmentDelete /* || props.messagePendingIncomplete */) ? _styles.chipDisabled : _styles.chip}
|
||||
>
|
||||
Delete
|
||||
</Chip>}
|
||||
|
||||
@@ -1,27 +1,46 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Chip, ColorPaletteProp } from '@mui/joy';
|
||||
import { Box, Chip, ColorPaletteProp, Divider, Tooltip } from '@mui/joy';
|
||||
import BrushRoundedIcon from '@mui/icons-material/BrushRounded';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import ClearAllRoundedIcon from '@mui/icons-material/ClearAllRounded';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
|
||||
import RepeatIcon from '@mui/icons-material/Repeat';
|
||||
import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
|
||||
|
||||
import { BlocksContainer } from '~/modules/blocks/BlocksContainers';
|
||||
import { RenderCodeMemo } from '~/modules/blocks/code/RenderCode';
|
||||
import { ScaledTextBlockRenderer } from '~/modules/blocks/ScaledTextBlockRenderer';
|
||||
|
||||
import type { DMessageRole } from '~/common/stores/chat/chat.message';
|
||||
import type { DVoidPlaceholderModelOp, DVoidPlaceholderPart } from '~/common/stores/chat/chat.fragments';
|
||||
import { adjustContentScaling, ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import type { DMessageFragmentId, DVoidPlaceholderMOp, DVoidPlaceholderPart } from '~/common/stores/chat/chat.fragments';
|
||||
import { DataStreamViz } from '~/common/components/DataStreamViz';
|
||||
import { adjustContentScaling, ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { animationSpinHalfPause } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
// configuration
|
||||
const DATASTREAM_VISUALIZATION_DELAY = Math.round(2 * Math.PI * 1000);
|
||||
const MODELOP_TIMEOUT_DELAY = 5; // seconds
|
||||
const MODELOP_TIMEOUT_LIMIT = 300; // seconds
|
||||
const MODELOP_TIMEOUT_LIMIT = 7 * 24 * 60 * 60; // seconds - 1 week for long ops, such as Gemini Deep Research
|
||||
|
||||
const modelOperationConfig: Record<DVoidPlaceholderMOp['mot'], { Icon: React.ElementType, color: ColorPaletteProp }> = {
|
||||
'search-web': { Icon: SearchRoundedIcon, color: 'neutral' },
|
||||
'gen-image': { Icon: BrushRoundedIcon, color: 'success' },
|
||||
'code-exec': { Icon: CodeIcon, color: 'primary' },
|
||||
} as const;
|
||||
|
||||
function _formatElapsed(seconds: number): string {
|
||||
if (seconds < 60) return `${seconds}s`;
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = seconds % 60;
|
||||
if (m < 60) return s ? `${m}m ${s}s` : `${m}m`;
|
||||
const h = Math.floor(m / 60);
|
||||
const rm = m % 60;
|
||||
return rm ? `${h}h ${rm}m` : `${h}h`;
|
||||
}
|
||||
|
||||
|
||||
const _styles = {
|
||||
@@ -36,61 +55,230 @@ const _styles = {
|
||||
// wrap text if needed - introduced for retry error messages
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
} as const,
|
||||
},
|
||||
|
||||
followUpChipIcon: {
|
||||
fontSize: '1rem',
|
||||
mr: 0.5,
|
||||
animation: `${animationSpinHalfPause} 2s ease-in-out infinite`,
|
||||
} as const,
|
||||
},
|
||||
|
||||
opList: {
|
||||
// backgroundColor: 'red',
|
||||
px: 1.5,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
|
||||
opChipTooltip: {
|
||||
borderRadius: 'xs',
|
||||
boxShadow: 'md',
|
||||
fontSize: 'xs',
|
||||
whiteSpace: 'pre-wrap',
|
||||
maxWidth: '96vw',
|
||||
p: 2,
|
||||
},
|
||||
opChip: {
|
||||
maxWidth: '100%', // fundamental for the ellipsize to work
|
||||
// width: '100%', // would have way less 'jumpy-ness'
|
||||
minWidth: 200, // would work on mobile, but no clear advantage
|
||||
minWidth: 100, // safety floor, constant across active/done states
|
||||
// fontWeight: 500,
|
||||
minHeight: '2rem',
|
||||
minHeight: '1.75rem',
|
||||
// replaced by Box with px: 2
|
||||
// mx: 1.5, // example: RenderPlainText has _styles.typography.mx = 1.5
|
||||
pl: 1.5,
|
||||
pr: 1.75,
|
||||
borderRadius: 'sm',
|
||||
boxShadow: 'inset 1px 1px 4px -2px rgba(0, 0, 0, 0.2)',
|
||||
transition: 'all 0.2s ease',
|
||||
'& .MuiChip-startDecorator': {
|
||||
marginRight: '0.5em',
|
||||
},
|
||||
},
|
||||
opChipDone: {
|
||||
boxShadow: undefined, // reset
|
||||
color: 'text.tertiary',
|
||||
background: 'transparent',
|
||||
// done chips are rendered in 'plain' only, so the following works, otherwise it would remove the bg even in 'soft' for instance
|
||||
'& > button': {
|
||||
background: 'transparent',
|
||||
},
|
||||
},
|
||||
} as const satisfies Record<string, SxProps>;
|
||||
|
||||
|
||||
const modelOperationConfig: Record<DVoidPlaceholderModelOp['mot'], { Icon: React.ElementType, color: ColorPaletteProp }> = {
|
||||
'search-web': { Icon: SearchRoundedIcon, color: 'neutral' },
|
||||
'gen-image': { Icon: BrushRoundedIcon, color: 'success' },
|
||||
'code-exec': { Icon: CodeIcon, color: 'primary' },
|
||||
'flow-cont': { Icon: SearchRoundedIcon, color: 'warning' },
|
||||
} as const;
|
||||
// --- Render Follow-Up ---
|
||||
|
||||
function RenderChipFollowUp(props: {
|
||||
text: string
|
||||
}) {
|
||||
return (
|
||||
<Chip
|
||||
size='sm'
|
||||
color='primary'
|
||||
variant='soft'
|
||||
sx={_styles.followUpChip}
|
||||
startDecorator={<HourglassEmptyIcon sx={_styles.followUpChipIcon} />}
|
||||
>
|
||||
{props.text}
|
||||
</Chip>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// --- Render AIX Control ---
|
||||
|
||||
function RenderChipAixControl({ aixControl, text }: {
|
||||
text: string,
|
||||
aixControl: Exclude<DVoidPlaceholderPart['aixControl'], undefined>, // DVoidPlaceholderAixControlRetry
|
||||
}) {
|
||||
|
||||
// derived
|
||||
let startText: number | string | undefined;
|
||||
let color: ColorPaletteProp;
|
||||
let Icon: React.ElementType | undefined;
|
||||
if (aixControl.ctl === 'ac-info')
|
||||
color = 'primary';
|
||||
else if (aixControl.ctl === 'ec-retry') {
|
||||
const { rCauseConn, rCauseHttp, rScope } = aixControl;
|
||||
startText = rCauseHttp || rCauseConn || rScope;
|
||||
color = rScope === 'srv-dispatch' ? 'primary'
|
||||
: rScope === 'srv-op' ? 'warning'
|
||||
: 'danger';
|
||||
Icon = RepeatIcon;
|
||||
} else
|
||||
color = 'danger';
|
||||
|
||||
return (
|
||||
<Chip
|
||||
size='sm'
|
||||
color={color}
|
||||
variant='soft'
|
||||
startDecorator={startText ? <div style={{ opacity: 0.75, textWrap: 'nowrap' }}>{startText}</div> : Icon ? <Icon style={{ opacity: 0.75 }} /> : undefined}
|
||||
sx={{
|
||||
mx: 1.5, // usual, esp for the looks into Beam
|
||||
gap: 1.5,
|
||||
px: 1.5,
|
||||
py: 0.375,
|
||||
my: '1px', // to not crop the outline on mobile, or on beam
|
||||
boxShadow: `inset 1px 2px 2px -1px var(--joy-palette-${color}-outlinedBorder)`,
|
||||
// outline: `1px solid var(--joy-palette-${color}-outlinedBorder)`,
|
||||
// wrap text if needed - introduced for retry error messages
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{text || 'Unknown Stream Control'}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// --- Render Model Operations ---
|
||||
|
||||
function RenderChipListModelOps(props: {
|
||||
opLog: Exclude<DVoidPlaceholderPart['opLog'], undefined>,
|
||||
contentScaling: ContentScaling,
|
||||
messagePendingIncomplete: boolean,
|
||||
fragmentId: DMessageFragmentId,
|
||||
onFragmentDelete?: (fragmentId: DMessageFragmentId) => void,
|
||||
}) {
|
||||
|
||||
// destructure
|
||||
const { contentScaling, opLog, fragmentId, onFragmentDelete } = props;
|
||||
|
||||
// memo ordering - children right after their parent (recursive, for PFC nesting)
|
||||
const ordered = React.useMemo(() => {
|
||||
|
||||
// fast path: no nesting -> keep insertion order
|
||||
if (!opLog.some(e => e.parentOpId)) return opLog;
|
||||
|
||||
// collect children by parent
|
||||
const roots: DVoidPlaceholderMOp[] = [];
|
||||
const childrenOf = new Map<string, DVoidPlaceholderMOp[]>();
|
||||
for (const e of opLog)
|
||||
if (e.parentOpId) (childrenOf.get(e.parentOpId) ?? childrenOf.set(e.parentOpId, []).get(e.parentOpId)!).push(e);
|
||||
else roots.push(e);
|
||||
|
||||
// recursively emit entry + descendants, then orphans
|
||||
const result: DVoidPlaceholderMOp[] = [];
|
||||
const placed = new Set<DVoidPlaceholderMOp>();
|
||||
const emit = (entry: DVoidPlaceholderMOp) => {
|
||||
result.push(entry);
|
||||
placed.add(entry);
|
||||
if (entry.opId)
|
||||
for (const child of childrenOf.get(entry.opId) ?? [])
|
||||
emit(child);
|
||||
};
|
||||
for (const root of roots) emit(root);
|
||||
for (const e of opLog) if (!placed.has(e)) result.push(e);
|
||||
|
||||
return result;
|
||||
}, [opLog]);
|
||||
|
||||
if (!ordered.length) return null;
|
||||
|
||||
return (
|
||||
<BlocksContainer sx={_styles.opList}>
|
||||
|
||||
{/* Operations list, with indentations */}
|
||||
{ordered.map((entry, i) => (
|
||||
<Box
|
||||
key={entry.opId}
|
||||
sx={!entry.level ? undefined : {
|
||||
ml: 2.125 * entry.level,
|
||||
borderLeft: '1px solid var(--joy-palette-neutral-outlinedBorder)',
|
||||
pl: 0.5,
|
||||
}}
|
||||
>
|
||||
<ModelOperationChip
|
||||
op={entry}
|
||||
contentScaling={contentScaling}
|
||||
messagePendingIncomplete={props.messagePendingIncomplete}
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
|
||||
{/* Harakiri chip, if possible (the div avoids x-stretching) */}
|
||||
{!!onFragmentDelete && <div>
|
||||
<OperationsHarakiriChip
|
||||
label='Clear steps'
|
||||
fragmentId={fragmentId}
|
||||
contentScaling={contentScaling}
|
||||
onFragmentDelete={onFragmentDelete}
|
||||
/>
|
||||
</div>}
|
||||
|
||||
</BlocksContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function ModelOperationChip(props: {
|
||||
mot: DVoidPlaceholderModelOp['mot'],
|
||||
cts: number,
|
||||
text: string,
|
||||
op: DVoidPlaceholderMOp,
|
||||
contentScaling: ContentScaling,
|
||||
messagePendingIncomplete: boolean,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [elapsedSeconds, setElapsedSeconds] = React.useState(0);
|
||||
|
||||
// derived
|
||||
const { Icon, color } = modelOperationConfig[props.mot] ?? {};
|
||||
const timerActive = Math.floor((Date.now() - props.cts) / 1000) < MODELOP_TIMEOUT_LIMIT;
|
||||
const { mot, cts, text, state, iTexts, oTexts } = props.op;
|
||||
const { Icon, color } = modelOperationConfig[mot] ?? {};
|
||||
const isDone = state === 'done';
|
||||
const isError = state === 'error';
|
||||
const isFinished = isDone || isError;
|
||||
|
||||
const iText = iTexts?.join('\n\n').trimStart() ?? null;
|
||||
const oText = oTexts?.join('\n') ?? null;
|
||||
const hasDetails = !!iText || !!oText;
|
||||
|
||||
const timerIsActive = props.messagePendingIncomplete && !isFinished && Math.floor((Date.now() - cts) / 1000) < MODELOP_TIMEOUT_LIMIT;
|
||||
|
||||
// [effect] show the elapsed time
|
||||
React.useEffect(() => {
|
||||
if (!timerActive) return; // prevent long-past timers to show
|
||||
if (!timerIsActive) return; // prevent long-past timers to show
|
||||
const timerId = setInterval(() => {
|
||||
const elapsed = Math.floor((Date.now() - props.cts) / 1000);
|
||||
const elapsed = Math.floor((Date.now() - cts) / 1000);
|
||||
if (elapsed >= MODELOP_TIMEOUT_DELAY)
|
||||
setElapsedSeconds(elapsed);
|
||||
}, 1000);
|
||||
@@ -98,123 +286,171 @@ function ModelOperationChip(props: {
|
||||
clearInterval(timerId);
|
||||
setElapsedSeconds(0);
|
||||
};
|
||||
}, [props.cts, timerActive]);
|
||||
}, [cts, timerIsActive]);
|
||||
|
||||
|
||||
// memo style
|
||||
const chipSx: SxProps = React.useMemo(() => ({
|
||||
..._styles.opChip,
|
||||
...(isFinished && _styles.opChipDone),
|
||||
...(isError && { color: undefined /* we inherit 'warning' */ }),
|
||||
...(hasDetails && { cursor: 'pointer' }),
|
||||
fontSize: themeScalingMap[props.contentScaling]?.blockFontSize ?? undefined,
|
||||
}), [isFinished, isError, hasDetails, props.contentScaling]);
|
||||
|
||||
const chipElement = (
|
||||
<Chip
|
||||
size='sm'
|
||||
color={isError ? 'warning' : isFinished ? 'neutral' : color}
|
||||
variant={isFinished ? 'plain' : 'soft'}
|
||||
onClick={!hasDetails ? undefined : () => false}
|
||||
startDecorator={isError ? <CloseRoundedIcon /> : isDone ? <CheckRoundedIcon /> : <Icon />}
|
||||
sx={chipSx}
|
||||
>
|
||||
<span className='agi-ellipsize'>
|
||||
{text}
|
||||
{elapsedSeconds >= MODELOP_TIMEOUT_DELAY && (
|
||||
<span style={{ opacity: 0.6 }}>
|
||||
{' · '}<span style={{ display: 'inline-block', minWidth: elapsedSeconds >= 60 ? '6ch' : '3ch' }}>{_formatElapsed(elapsedSeconds)}</span>
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</Chip>
|
||||
);
|
||||
|
||||
return !hasDetails ? chipElement : (
|
||||
<Tooltip variant='outlined' placement='top' arrow sx={_styles.opChipTooltip} title={
|
||||
<div>
|
||||
{/* Input: rendered as code if */}
|
||||
{!!iText && mot === 'code-exec' ? (
|
||||
<RenderCodeMemo
|
||||
code={iText}
|
||||
semiStableId={`model-op-input-${props.op.opId}`}
|
||||
title=''
|
||||
isPartial={false}
|
||||
renderHideTitle={true}
|
||||
sx={{ m: -1.5, fontSize: props.contentScaling }}
|
||||
/>
|
||||
) : iText}
|
||||
|
||||
{!!iTexts?.length && !!oTexts?.length && <Divider sx={{ my: 2 }} />}
|
||||
|
||||
{!!oTexts?.length && oTexts.map((t, i) => (
|
||||
<span key={i} style={t.startsWith('exit code:') ? { color: 'var(--joy-palette-warning-plainColor)', fontWeight: 600 } : undefined}>
|
||||
{i > 0 && '\n'}{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
}>
|
||||
{chipElement}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function OperationsHarakiriChip(props: {
|
||||
label: string,
|
||||
fragmentId: DMessageFragmentId, // used for self deletion
|
||||
contentScaling: ContentScaling,
|
||||
onFragmentDelete: (fragmentId: DMessageFragmentId) => void,
|
||||
}) {
|
||||
|
||||
const { fragmentId, onFragmentDelete } = props;
|
||||
|
||||
// handler
|
||||
|
||||
const handleDeleteSelf = React.useCallback(() => {
|
||||
onFragmentDelete(fragmentId);
|
||||
}, [fragmentId, onFragmentDelete]);
|
||||
|
||||
|
||||
// memo style
|
||||
const chipSx: SxProps = React.useMemo(() => ({
|
||||
..._styles.opChip,
|
||||
..._styles.opChipDone,
|
||||
fontSize: themeScalingMap[props.contentScaling]?.blockFontSize ?? undefined,
|
||||
}), [props.contentScaling]);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
size='sm'
|
||||
color={color}
|
||||
variant='soft'
|
||||
startDecorator={<Icon />}
|
||||
sx={{
|
||||
..._styles.opChip,
|
||||
fontSize: themeScalingMap[props.contentScaling]?.blockFontSize ?? undefined,
|
||||
}}
|
||||
variant='plain'
|
||||
onClick={handleDeleteSelf}
|
||||
startDecorator={<ClearAllRoundedIcon /* sx={{ opacity: 0 }} */ />}
|
||||
sx={chipSx}
|
||||
>
|
||||
<span className='agi-ellipsize'>{props.text}{elapsedSeconds >= MODELOP_TIMEOUT_DELAY && <span style={{ opacity: 0.6 }}> · {elapsedSeconds}s</span>}</span>
|
||||
{props.label}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function BlockPartPlaceholder(props: {
|
||||
placeholderText: string,
|
||||
placeholderType?: DVoidPlaceholderPart['pType'],
|
||||
placeholderModelOp?: DVoidPlaceholderModelOp,
|
||||
placeholderAixControl?: DVoidPlaceholderPart['aixControl'],
|
||||
messageRole: DMessageRole,
|
||||
interface BlockPartPlaceholderProps {
|
||||
placeholderPart: DVoidPlaceholderPart,
|
||||
contentScaling: ContentScaling,
|
||||
showAsItalic?: boolean,
|
||||
messagePendingIncomplete: boolean,
|
||||
showAsDataStreamViz?: boolean,
|
||||
}) {
|
||||
zenMode?: boolean,
|
||||
|
||||
// used for self deletion
|
||||
fragmentId: DMessageFragmentId,
|
||||
onFragmentDelete?: (fragmentId: DMessageFragmentId) => void,
|
||||
// onFragmentReplace?: (fragmentId: DMessageFragmentId, newFragment: DMessageContentFragment) => void,
|
||||
}
|
||||
|
||||
/**
|
||||
* Transient placeholder: follow-ups, retries, model-op progress (with PFC nesting), or italic text.
|
||||
*/
|
||||
export function BlockPartPlaceholder({ placeholderPart, contentScaling, messagePendingIncomplete, showAsDataStreamViz, zenMode, fragmentId, onFragmentDelete }: BlockPartPlaceholderProps){
|
||||
|
||||
// state
|
||||
const [showVisualization, setShowVisualization] = React.useState(false);
|
||||
|
||||
// derived state
|
||||
const shouldShowViz = props.showAsDataStreamViz && !props.placeholderModelOp;
|
||||
const { pText, pType, opLog, aixControl } = placeholderPart;
|
||||
const shouldShowViz = showAsDataStreamViz && !opLog?.length && !aixControl;
|
||||
|
||||
|
||||
// [effect] if allowed trigger the viz effect in 6.28 seconds, otherwise clear it
|
||||
React.useEffect(() => {
|
||||
let timerId: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
if (shouldShowViz)
|
||||
timerId = setTimeout(() => setShowVisualization(true), DATASTREAM_VISUALIZATION_DELAY);
|
||||
else
|
||||
setShowVisualization(false);
|
||||
|
||||
return () => timerId && clearTimeout(timerId);
|
||||
if (!shouldShowViz) return setShowVisualization(false);
|
||||
const timerId = setTimeout(() => setShowVisualization(true), DATASTREAM_VISUALIZATION_DELAY);
|
||||
return () => clearTimeout(timerId);
|
||||
}, [shouldShowViz]);
|
||||
|
||||
|
||||
// rendering switchboard
|
||||
|
||||
// Alternative placeholder visualization
|
||||
if (shouldShowViz && showVisualization)
|
||||
return <DataStreamViz height={1 + 8 * 4} />;
|
||||
|
||||
// 1. autoChatFollowUps's 'Follow Up' notices
|
||||
if (pType === 'chat-gen-follow-up')
|
||||
return <RenderChipFollowUp text={pText} />;
|
||||
|
||||
// Type-based visualization
|
||||
const isFollowUp = props.placeholderType === 'chat-gen-follow-up';
|
||||
if (isFollowUp) return (
|
||||
<Chip
|
||||
color='primary'
|
||||
variant='soft'
|
||||
size='sm'
|
||||
sx={_styles.followUpChip}
|
||||
startDecorator={<HourglassEmptyIcon sx={_styles.followUpChipIcon} />}
|
||||
>
|
||||
{props.placeholderText}
|
||||
</Chip>
|
||||
// 2. AIX Control renderer - only for error correction retry
|
||||
if (aixControl?.ctl)
|
||||
return <RenderChipAixControl text={pText} aixControl={aixControl} />;
|
||||
|
||||
// 3. Model operation render - stacked list when multiple operations, single chip otherwise
|
||||
if (opLog?.length) return zenMode ? null : (
|
||||
<RenderChipListModelOps
|
||||
opLog={opLog}
|
||||
contentScaling={adjustContentScaling(contentScaling, -1)}
|
||||
messagePendingIncomplete={messagePendingIncomplete}
|
||||
fragmentId={fragmentId}
|
||||
onFragmentDelete={onFragmentDelete}
|
||||
/>
|
||||
);
|
||||
|
||||
// AIX Control renderer (e.g., error correction retry)
|
||||
if (props.placeholderAixControl?.ctl === 'ec-retry') {
|
||||
const { rScope, rCauseHttp, rCauseConn } = props.placeholderAixControl;
|
||||
const color = rScope === 'srv-dispatch' ? 'primary' : rScope === 'srv-op' ? 'warning' : 'danger';
|
||||
return (
|
||||
<Chip
|
||||
// size='sm'
|
||||
color={color}
|
||||
variant='soft'
|
||||
startDecorator={<div style={{ opacity: 0.75 }}>{rCauseHttp || rCauseConn || rScope}</div>}
|
||||
endDecorator={<RepeatIcon style={{ opacity: 0.5 }} />}
|
||||
onClick={() => console.log({ props })}
|
||||
sx={{
|
||||
gap: 1.5,
|
||||
px: 1.5,
|
||||
py: 0.375,
|
||||
my: '1px', // to not crop the outline on mobile, or on beam
|
||||
boxShadow: `1px 2px 4px -3px var(--joy-palette-${color}-solidBg)`,
|
||||
// wrap text if needed - introduced for retry error messages
|
||||
whiteSpace: 'normal',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{props.placeholderText}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
||||
// Model operation renderer
|
||||
if (props.placeholderModelOp)
|
||||
return (
|
||||
<BlocksContainer>
|
||||
<Box sx={{ px: 1.5 }}>
|
||||
<ModelOperationChip
|
||||
text={props.placeholderText}
|
||||
mot={props.placeholderModelOp.mot}
|
||||
cts={props.placeholderModelOp.cts}
|
||||
contentScaling={adjustContentScaling(props.contentScaling, -1)}
|
||||
/>
|
||||
</Box>
|
||||
</BlocksContainer>
|
||||
);
|
||||
|
||||
// 4. 'placeholder text' in italic - used in various places in the app
|
||||
return (
|
||||
<ScaledTextBlockRenderer
|
||||
text={props.placeholderText}
|
||||
contentScaling={props.contentScaling}
|
||||
text={pText}
|
||||
contentScaling={contentScaling}
|
||||
textRenderVariant='text'
|
||||
showAsItalic={props.showAsItalic}
|
||||
// showAsDanger={false}
|
||||
showAsItalic={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -353,7 +353,8 @@ export function PersonaSelector(props: {
|
||||
|
||||
{/* [row -3] Example incipits */}
|
||||
{systemPurposeId !== 'Custom' && (
|
||||
<ExpanderControlledBox expanded={showExamples || (!isCustomPurpose && showPrompt)} sx={{ gridColumn: '1 / -1', pt: 1 }}>
|
||||
<Box sx={{ gridColumn: '1 / -1', pt: 1 }}>
|
||||
<ExpanderControlledBox expanded={showExamples || (!isCustomPurpose && showPrompt)}>
|
||||
{showExamples && (
|
||||
<List
|
||||
aria-label='Persona Conversation Starters'
|
||||
@@ -419,6 +420,7 @@ export function PersonaSelector(props: {
|
||||
</Card>
|
||||
)}
|
||||
</ExpanderControlledBox>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* [row -1] Custom Prompt box */}
|
||||
|
||||
@@ -78,15 +78,14 @@ export async function runPersonaOnConversationHead(
|
||||
// if (abortController.signal.aborted)
|
||||
// console.warn('runPersonaOnConversationHead: Aborted', { conversationId, assistantLlmId, messageOverwrite });
|
||||
|
||||
// deep copy the object to avoid partial updates
|
||||
let deepCopy = structuredClone(messageOverwrite);
|
||||
// fragments and generator are already immutable (new refs per update) - no deep clone needed
|
||||
const { fragments, ...rest } = messageOverwrite;
|
||||
|
||||
// [Cosmetic Logic] if the content hasn't come yet, don't replace the fragments to still show the placeholder
|
||||
if (!messageComplete && deepCopy.pendingIncomplete && deepCopy.fragments?.length === 0)
|
||||
delete (deepCopy as any).fragments;
|
||||
const includeFragments = !!fragments?.length || messageComplete || !messageOverwrite.pendingIncomplete;
|
||||
|
||||
// update the message
|
||||
cHandler.messageEdit(assistantMessageId, deepCopy, messageComplete, false);
|
||||
cHandler.messageEdit(assistantMessageId, { ...(includeFragments && { fragments }), ...rest }, messageComplete, false);
|
||||
|
||||
// if requested, speak the message
|
||||
autoSpeaker?.handleMessage(messageOverwrite, messageComplete);
|
||||
@@ -97,12 +96,12 @@ export async function runPersonaOnConversationHead(
|
||||
);
|
||||
|
||||
// final message update (needed only in case of error)
|
||||
const lastDeepCopy = structuredClone(messageStatus.lastDMessage);
|
||||
if (messageStatus.outcome === 'errored')
|
||||
cHandler.messageEdit(assistantMessageId, lastDeepCopy, true, false);
|
||||
const lastDMessage = messageStatus.lastDMessage;
|
||||
if (messageStatus.outcome === 'failed')
|
||||
cHandler.messageEdit(assistantMessageId, lastDMessage, true, false);
|
||||
|
||||
// special case: if the last message was aborted and had no content, delete it
|
||||
if (messageWasInterruptedAtStart(lastDeepCopy)) {
|
||||
if (messageWasInterruptedAtStart(lastDMessage)) {
|
||||
cHandler.messagesDelete([assistantMessageId]);
|
||||
// NOTE: ok to exit here, as the abort was already done
|
||||
return false;
|
||||
@@ -136,5 +135,5 @@ export async function runPersonaOnConversationHead(
|
||||
cHandler.historyStripThinking(0);
|
||||
|
||||
// return true if this succeeded
|
||||
return messageStatus.outcome === 'success';
|
||||
return messageStatus.outcome === 'completed';
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export const NewsItems: NewsItem[] = [
|
||||
{ text: <>Major <B href='https://x.com/enricoros/status/1756553038293303434'>performance optimizations</B>: runs faster, saves power, saves memory</> },
|
||||
{ text: <>Improvements: auto-size charts, search and folder experience</> },
|
||||
{ text: <>Perfect chat scaling, with rapid keyboard shortcuts</> },
|
||||
{ text: <>Also: diagrams auto-resize, open code with StackBlitz and JSFiddle, quick model visibility toggle, open links externally, docs on the web</> },
|
||||
{ text: <>Also: diagrams auto-resize, quick model visibility toggle, open links externally, docs on the web</> },
|
||||
{ text: <>Fixes: standalone LaTeX blocks, close views by dragging, knowledge cutoff dates, crashes on Google translate (thanks dad)</> },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
|
||||
import type { DModelDomainId } from '~/common/stores/llms/model.domains.types';
|
||||
import { AIVndAntInlineFilesPolicy, useAIPreferencesStore } from '~/common/stores/store-ai';
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormSelectControl, FormSelectOption } from '~/common/components/forms/FormSelectControl';
|
||||
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
|
||||
@@ -33,6 +34,12 @@ const _keepThinkingBlocksOptions: FormSelectOption<ChatThinkingPolicy>[] = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
const _vndAntInlineFilesOptions: FormSelectOption<AIVndAntInlineFilesPolicy>[] = [
|
||||
{ value: 'off', label: 'Show', description: 'Keep as links' },
|
||||
{ value: 'inline-file', label: 'Embed', description: 'Default, embed in chat' },
|
||||
{ value: 'inline-file-and-delete', label: 'Embed + Free', description: 'Embed, then free' },
|
||||
] as const;
|
||||
|
||||
const _tokenCountingMethodOptions: FormSelectOption<TokenCountingMethod>[] = [
|
||||
{
|
||||
value: 'approximate',
|
||||
@@ -82,6 +89,7 @@ export function AppChatSettingsAI() {
|
||||
chatThinkingPolicy, setChatThinkingPolicy,
|
||||
tokenCountingMethod, setTokenCountingMethod,
|
||||
} = useChatAutoAI();
|
||||
const vndAntInlineFiles = useAIPreferencesStore(state => state.vndAntInlineFiles);
|
||||
|
||||
const showModelIcons = false; // useUIComplexityMode() === 'extra';
|
||||
|
||||
@@ -153,6 +161,22 @@ export function AppChatSettingsAI() {
|
||||
onChange={setChatThinkingPolicy}
|
||||
/>
|
||||
|
||||
<FormSelectControl<AIVndAntInlineFilesPolicy>
|
||||
title='Anthropic Files'
|
||||
tooltip={<>
|
||||
When Claude uses tools like code execution, it may produce text and image files stored in Anthropic's File API. This setting controls whether Big-AGI should automatically download and embed them in the chat.
|
||||
<ul>
|
||||
<li><b>Show</b>: keep as references.</li>
|
||||
<li><b>Embed</b>: download and embed text/images (default).</li>
|
||||
<li><b>Embed + Free</b>: embed, then delete from Anthropic to free storage.</li>
|
||||
</ul>
|
||||
Only affects Anthropic models.
|
||||
</>}
|
||||
options={_vndAntInlineFilesOptions}
|
||||
value={vndAntInlineFiles}
|
||||
onChange={useAIPreferencesStore.getState().setVndAntInlineFiles}
|
||||
/>
|
||||
|
||||
<ListDivider inset='gutter'>Automatic AI Functions</ListDivider>
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between' }}>
|
||||
|
||||
@@ -4,12 +4,12 @@ import { FormControl, Typography } from '@mui/joy';
|
||||
import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||
import AttachFileRoundedIcon from '@mui/icons-material/AttachFileRounded';
|
||||
import ShortcutIcon from '@mui/icons-material/Shortcut';
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
|
||||
import SpeedIcon from '@mui/icons-material/Speed';
|
||||
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { PhImageSquare } from '~/common/components/icons/phosphor/PhImageSquare';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useUXLabsStore } from '~/common/stores/store-ux-labs';
|
||||
|
||||
@@ -29,7 +29,7 @@ export function UxLabsSettings() {
|
||||
return <>
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><ImageOutlinedIcon sx={{ fontSize: 'lg', mr: 0.5, mb: 0.25 }} />Lossless Images</>} description={labsLosslessImages ? 'Large storage use' : 'Compress'}
|
||||
title={<><PhImageSquare sx={{ fontSize: 'lg', mr: 0.5, mb: 0.25 }} />Lossless Images</>} description={labsLosslessImages ? 'Large storage use' : 'Compress'}
|
||||
tooltipWarning={labsLosslessImages}
|
||||
tooltip={<>
|
||||
Preserves the original lossless PNG format for AI-generated images instead of compressing them to WebP/JPEG.
|
||||
|
||||
@@ -13,7 +13,7 @@ export const Brand = {
|
||||
},
|
||||
Meta: {
|
||||
Description: 'Launch the open-source AI workspace for experts. BYO API keys. Compare and tune models, use personas, voice and vision - your data stays local.',
|
||||
SiteName: 'Big-AGI | AI for power-users',
|
||||
SiteName: 'Big-AGI | The Expert\'s AI Workspace',
|
||||
ThemeColor: '#32383E',
|
||||
TwitterSite: '@enricoros',
|
||||
},
|
||||
|
||||
+9
-11
@@ -8,8 +8,6 @@ import Diversity2Icon from '@mui/icons-material/Diversity2';
|
||||
import EventNoteIcon from '@mui/icons-material/EventNote';
|
||||
import EventNoteOutlinedIcon from '@mui/icons-material/EventNoteOutlined';
|
||||
import GrainIcon from '@mui/icons-material/Grain';
|
||||
import ImageIcon from '@mui/icons-material/Image';
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import IosShareOutlinedIcon from '@mui/icons-material/IosShareOutlined';
|
||||
// Link icons
|
||||
@@ -189,15 +187,15 @@ export const navItems: {
|
||||
hideIcon: true,
|
||||
isDev: true,
|
||||
},
|
||||
{
|
||||
name: 'Media Library',
|
||||
icon: ImageOutlinedIcon,
|
||||
iconActive: ImageIcon,
|
||||
type: 'app',
|
||||
route: '/media',
|
||||
isDev: true,
|
||||
_delete: true,
|
||||
},
|
||||
// {
|
||||
// name: 'Media Library',
|
||||
// icon: ImageOutlinedIcon,
|
||||
// iconActive: ImageIcon,
|
||||
// type: 'app',
|
||||
// route: '/media',
|
||||
// isDev: true,
|
||||
// _delete: true,
|
||||
// },
|
||||
{
|
||||
name: 'Shared Chats',
|
||||
barTitle: 'Shared Chat',
|
||||
|
||||
@@ -23,7 +23,7 @@ export const Release = {
|
||||
|
||||
// this is here to trigger revalidation of data, e.g. models refresh
|
||||
Monotonics: {
|
||||
Aix: 62,
|
||||
Aix: 70,
|
||||
NewsVersion: 204,
|
||||
},
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import AbcIcon from '@mui/icons-material/Abc';
|
||||
import CodeIcon from '@mui/icons-material/Code';
|
||||
import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
|
||||
import HtmlIcon from '@mui/icons-material/Html';
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
|
||||
import PermMediaOutlinedIcon from '@mui/icons-material/PermMediaOutlined';
|
||||
import PhotoSizeSelectLargeOutlinedIcon from '@mui/icons-material/PhotoSizeSelectLargeOutlined';
|
||||
import PhotoSizeSelectSmallOutlinedIcon from '@mui/icons-material/PhotoSizeSelectSmallOutlined';
|
||||
@@ -24,6 +23,7 @@ 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 { LiveFileIcon } from '~/common/livefile/liveFile.icons';
|
||||
import { PhImageSquare } from '~/common/components/icons/phosphor/PhImageSquare';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { ellipsizeFront, ellipsizeMiddle } from '~/common/util/textUtils';
|
||||
|
||||
@@ -97,10 +97,10 @@ const converterTypeToIconMap: { [key in AttachmentDraftConverterType]: React.Com
|
||||
'rich-text-cleaner': CodeIcon,
|
||||
'rich-text-markdown': TextFieldsIcon,
|
||||
'rich-text-table': PivotTableChartIcon,
|
||||
'image-original': ImageOutlinedIcon,
|
||||
'image-original': PhImageSquare,
|
||||
'image-resized-high': PhotoSizeSelectLargeOutlinedIcon,
|
||||
'image-resized-low': PhotoSizeSelectSmallOutlinedIcon,
|
||||
'image-to-default': ImageOutlinedIcon,
|
||||
'image-to-default': PhImageSquare,
|
||||
'image-caption': AbcIcon,
|
||||
'image-ocr': AbcIcon,
|
||||
'pdf-auto': PictureAsPdfIcon,
|
||||
@@ -113,7 +113,7 @@ const converterTypeToIconMap: { [key in AttachmentDraftConverterType]: React.Com
|
||||
'url-page-markdown': CodeIcon, // was LanguageIcon
|
||||
'url-page-html': HtmlIcon, // was LanguageIcon
|
||||
'url-page-null': TextureIcon,
|
||||
'url-page-image': ImageOutlinedIcon,
|
||||
'url-page-image': PhImageSquare,
|
||||
'youtube-transcript': YouTubeIcon,
|
||||
'youtube-transcript-simple': YouTubeIcon,
|
||||
'ego-fragments-inlined': TelegramIcon,
|
||||
|
||||
@@ -30,11 +30,13 @@ const GuessedMimeLookupTable: Record<string, GuessedMimeInfo> = {
|
||||
// Code (including various programming languages)
|
||||
'text/css': { ext: ['css', 'scss', 'less', 'sass'], dt: 'code' },
|
||||
'text/javascript': { ext: ['js', 'mjs', 'jsx'], dt: 'code' },
|
||||
'application/javascript': { ext: null, dt: 'code' }, // [Anthropic 2026-04-09] non-standard variant returned by the Anthropic Files API
|
||||
'application/x-javascript': { ext: null, dt: 'code' },
|
||||
'text/x-typescript': { ext: ['ts', 'tsx', 'd.ts'], dt: 'code' }, // TypeScript files (recommended is application/typescript, but we standardize to text/x-typescript instead as per Gemini's standard)
|
||||
'application/x-typescript': { ext: null, dt: 'code' },
|
||||
'text/csv': { ext: ['csv', 'tsv'], dt: 'code' },
|
||||
'text/x-python': { ext: ['py', 'pyw'], dt: 'code' },
|
||||
'text/x-script.python': { ext: null, dt: 'code' }, // [Anthropic 2026-04-09]
|
||||
'application/x-python-code': { ext: null, dt: 'code' },
|
||||
'application/x-ipynb+json': { ext: ['ipynb'], dt: 'code' },
|
||||
'application/json': { ext: ['json', 'jsonld'], dt: 'code' },
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import type { DLLM } from '~/common/stores/llms/llms.types';
|
||||
import type { DMessageAttachmentFragment } from '~/common/stores/chat/chat.fragments';
|
||||
import { estimateTokensForFragments } from '~/common/stores/chat/chat.tokens';
|
||||
import { useShallowStable } from '~/common/util/hooks/useShallowObject';
|
||||
import { useMemoShallowStable } from '~/common/util/hooks/useShallowObject';
|
||||
|
||||
import type { AttachmentDraft } from '../attachment.types';
|
||||
import type { AttachmentEnrichmentSummary, IAttachmentEnrichment } from './attachment.enrichment';
|
||||
@@ -64,7 +64,7 @@ class LLMAttachmentEnrichment implements IAttachmentEnrichment {
|
||||
* Hook that creates an LLM-specific IAttachmentEnrichment and computes
|
||||
* collection-level summary for the given attachment drafts.
|
||||
*/
|
||||
export function useAttachmentDraftsEnrichment(attachmentDrafts: AttachmentDraft[], chatLLM: DLLM | null, chatLLMSupportsImages: boolean): {
|
||||
export function useAttachmentDraftsEnrichment(attachmentDraftsStable: AttachmentDraft[], chatLLM: DLLM | null, chatLLMSupportsImages: boolean): {
|
||||
enrichment: IAttachmentEnrichment;
|
||||
summary: AttachmentEnrichmentSummary;
|
||||
} {
|
||||
@@ -76,12 +76,12 @@ export function useAttachmentDraftsEnrichment(attachmentDrafts: AttachmentDraft[
|
||||
);
|
||||
|
||||
// Collection-level summary - shallow-stabilized to avoid unnecessary re-renders
|
||||
const summary = useShallowStable<AttachmentEnrichmentSummary>({
|
||||
allCompatible: attachmentDrafts.every(enrichment.isCompatible),
|
||||
anyImages: attachmentDrafts.some(enrichment.hasImages),
|
||||
anyInlinable: attachmentDrafts.some(enrichment.supportsTextInline),
|
||||
totalTokensApprox: enrichment.estimateTotalTokens(attachmentDrafts),
|
||||
});
|
||||
const summary = useMemoShallowStable<AttachmentEnrichmentSummary>(() => ({
|
||||
allCompatible: attachmentDraftsStable.every(enrichment.isCompatible),
|
||||
anyImages: attachmentDraftsStable.some(enrichment.hasImages),
|
||||
anyInlinable: attachmentDraftsStable.some(enrichment.supportsTextInline),
|
||||
totalTokensApprox: enrichment.estimateTotalTokens(attachmentDraftsStable),
|
||||
}), [attachmentDraftsStable, enrichment]);
|
||||
|
||||
return { enrichment, summary };
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
import { SystemPurposes } from '../../data';
|
||||
|
||||
import { BeamStore, createBeamVanillaStore } from '~/modules/beam/store-beam_vanilla';
|
||||
import { autoConversationTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
import { useModuleBeamStore } from '~/modules/beam/store-module-beam';
|
||||
|
||||
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
|
||||
@@ -275,6 +276,10 @@ export class ConversationHandler {
|
||||
|
||||
// close beam
|
||||
terminateKeepingSettings();
|
||||
|
||||
// auto-title the conversation if enabled (parity with chat-persona flow — fixes #1078)
|
||||
if (getChatAutoAI().autoTitleChat)
|
||||
void autoConversationTitle(this.conversationId, false);
|
||||
};
|
||||
|
||||
beamOpen(viewHistory, getChatLLMId(), !!destReplaceMessageId, onBeamSuccess);
|
||||
|
||||
@@ -54,6 +54,10 @@ export class ConversationsManager {
|
||||
const instance = ConversationsManager._instance || (ConversationsManager._instance = new ConversationsManager());
|
||||
let handler = instance.handlers.get(conversationId);
|
||||
if (!handler) {
|
||||
if (!conversationId) {
|
||||
// this shall not happen, so we check here just in case
|
||||
debugger;
|
||||
}
|
||||
handler = new ConversationHandler(conversationId);
|
||||
instance.handlers.set(conversationId, handler);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { StoreApi, useStore } from 'zustand';
|
||||
import { createStore as createVanillaStore } from 'zustand/vanilla';
|
||||
|
||||
import { AttachmentDraftsStoreApi, AttachmentsDraftsStore, createAttachmentDraftsStoreSlice } from '~/common/attachment-drafts/store-attachment-drafts_slice';
|
||||
import { Release } from '~/common/app.release';
|
||||
|
||||
import { ComposerOverlayStore, createComposerOverlayStoreSlice } from './store-perchat-composer_slice';
|
||||
import { createEphemeralsOverlayStoreSlice, EphemeralsOverlayStore } from './store-perchat-ephemerals_slice';
|
||||
@@ -34,16 +35,29 @@ export const createPerChatVanillaStore = (): StoreApi<PerChatOverlayStore> => cr
|
||||
}));
|
||||
|
||||
|
||||
const fallbackStoreApi = createPerChatVanillaStore();
|
||||
|
||||
// usages: ChatMessagesList
|
||||
export const useChatOverlayStore = <T, >(vanillaStore: Readonly<StoreApi<PerChatOverlayStore>> | null, selector: (store: PerChatOverlayStore) => T): T =>
|
||||
useStore(vanillaStore || fallbackStoreApi, selector);
|
||||
useStore(vanillaStore || _getFallbackStoreApi('store'), selector);
|
||||
|
||||
// usages: useAttachmentDrafts
|
||||
export const useChatAttachmentsStore = <T, >(vanillaStore: Readonly<AttachmentDraftsStoreApi> | null, selector: (store: AttachmentsDraftsStore) => T): T =>
|
||||
useStore(vanillaStore || fallbackStoreApi, selector);
|
||||
useStore(vanillaStore || _getFallbackStoreApi('attachments'), selector);
|
||||
|
||||
// usages: Composer
|
||||
export const useChatComposerOverlayStore = <T, >(vanillaStore: Readonly<StoreApi<ComposerOverlayStore>> | null, selector: (store: ComposerOverlayStore) => T): T =>
|
||||
useStore(vanillaStore || fallbackStoreApi, selector);
|
||||
useStore(vanillaStore || _getFallbackStoreApi('composer'), selector);
|
||||
|
||||
|
||||
// -- Lazy fallback store --
|
||||
|
||||
let _fallbackStoreApi: StoreApi<PerChatOverlayStore> | null = null;
|
||||
|
||||
function _getFallbackStoreApi(caller: string): StoreApi<PerChatOverlayStore> {
|
||||
if (!_fallbackStoreApi) {
|
||||
console[Release.IsNodeDevBuild ? 'warn' : 'log'](`[DEV] Requiring fallback Session ${caller} store`);
|
||||
if (Release.IsNodeDevBuild)
|
||||
debugger; // We want to see the backtrace
|
||||
_fallbackStoreApi = createPerChatVanillaStore();
|
||||
}
|
||||
return _fallbackStoreApi;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Button, IconButton, Tooltip } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
|
||||
|
||||
const _btnSx: SxProps = { borderColor: 'neutral.outlinedBorder' } as const;
|
||||
|
||||
export function ButtonServiceAdd(props: {
|
||||
isMobile: boolean;
|
||||
isEmpty: boolean;
|
||||
emptyHint: string;
|
||||
onClick: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
menuOpen?: boolean;
|
||||
label?: string;
|
||||
}) {
|
||||
|
||||
const { isMobile, isEmpty, emptyHint, onClick, menuOpen, label = 'Add' } = props;
|
||||
|
||||
const disabled = !!menuOpen;
|
||||
const variant = isEmpty ? 'solid' : 'outlined';
|
||||
|
||||
// Mobile + populated = icon-only (no hint needed - user already knows)
|
||||
if (isMobile && !isEmpty)
|
||||
return (
|
||||
<IconButton color="primary" variant={variant} onClick={onClick} disabled={disabled} sx={_btnSx}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
);
|
||||
|
||||
// Full button with controlled empty-state hint tooltip
|
||||
return (
|
||||
<Tooltip open={isEmpty && !menuOpen} color='primary' variant='outlined' title={emptyHint} arrow disableInteractive placement={isMobile ? 'bottom-end' : 'top-start'}>
|
||||
<Button
|
||||
variant={variant}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
startDecorator={<AddIcon />}
|
||||
sx={_btnSx}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, BoxProps, styled } from '@mui/joy';
|
||||
|
||||
|
||||
@@ -28,6 +29,13 @@ const BoxCollapser = styled(Box)({
|
||||
contain: 'layout',
|
||||
});
|
||||
|
||||
// Fix for RenderCode losing fixed OverlayButtons positioning system: drops the `contain` which would create a
|
||||
// containing block for fixed-positioned descendants and trap things like position:fixed sticky overlays inside this collapser).
|
||||
// `overflow: clip` alone keeps the collapse animation clipping without the trap.
|
||||
const collapserNoContainSx = {
|
||||
contain: 'none',
|
||||
} as const satisfies SxProps;
|
||||
|
||||
const BoxCollapsee = styled(Box)({
|
||||
/**
|
||||
* FIX: the absence of this made the ChatPanelModelParameters content overflow on the horizontal
|
||||
@@ -37,9 +45,9 @@ const BoxCollapsee = styled(Box)({
|
||||
});
|
||||
|
||||
|
||||
export function ExpanderControlledBox({ expanded, children, ...rest }: BoxProps & { expanded: boolean }) {
|
||||
export function ExpanderControlledBox({ expanded, noContain, children, ...rest }: BoxProps & { expanded: boolean, noContain?: boolean, sx?: never }) {
|
||||
return (
|
||||
<BoxCollapser aria-hidden={!expanded ? true : undefined} data-agi-no-copy={!expanded || undefined} {...rest}>
|
||||
<BoxCollapser aria-hidden={!expanded ? true : undefined} data-agi-no-copy={!expanded || undefined} {...rest} sx={noContain ? collapserNoContainSx : undefined}>
|
||||
<BoxCollapsee>
|
||||
{children}
|
||||
</BoxCollapsee>
|
||||
|
||||
@@ -76,6 +76,7 @@ export function ExpanderSection(props: {
|
||||
expandRequest?: boolean, // the internal expanded state will track this on change
|
||||
initialExpanded: boolean, // only read at first mount
|
||||
startDecorator?: React.ReactNode,
|
||||
persistentDivider?: boolean, // keep the header divider line visible even when expanded
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
|
||||
@@ -114,7 +115,7 @@ export function ExpanderSection(props: {
|
||||
{!!description && <FormHelperText>{description}</FormHelperText>}
|
||||
</Box>
|
||||
|
||||
{isCollapsible && !isExpanded && <Box sx={_styles.aeDivider} />}
|
||||
{isCollapsible && (!isExpanded || props.persistentDivider) && <Box sx={_styles.aeDivider} />}
|
||||
|
||||
{isCollapsible && (isExpanded
|
||||
? <UnfoldMoreIcon sx={_styles.aeHeaderIcon} />
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import * as React from 'react';
|
||||
|
||||
|
||||
/**
|
||||
* Can be extended to make an object useSyncExternalStore-compatible,
|
||||
* or used as field in such an object to avoid boilerplate.
|
||||
*/
|
||||
export class SyncExternalStorable<TSnapshot extends object> {
|
||||
|
||||
#listeners: Set<() => void> | null = null;
|
||||
#snapshot: TSnapshot;
|
||||
|
||||
constructor(initialSnapshot: TSnapshot) {
|
||||
this.#snapshot = initialSnapshot;
|
||||
}
|
||||
|
||||
// subscribe/getSnapshot - stable arrow functions for useSyncExternalStore
|
||||
getSnapshot = (): TSnapshot => this.#snapshot;
|
||||
subscribe = (listener: () => void): (() => void) => {
|
||||
const listeners = (this.#listeners ??= new Set());
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
};
|
||||
|
||||
/** Patch fields and notify listeners. */
|
||||
protected _snapshotPatch(patch: Partial<TSnapshot>): void {
|
||||
this.#snapshot = { ...this.#snapshot, ...patch };
|
||||
this.#notifyListeners();
|
||||
}
|
||||
|
||||
/** Full replace and notify listeners. */
|
||||
protected _snapshotSet(snapshot: TSnapshot): void {
|
||||
this.#snapshot = snapshot;
|
||||
this.#notifyListeners();
|
||||
}
|
||||
|
||||
/** Full replace, no notification. For dispose/reset. */
|
||||
protected _snapshotSetSilent(snapshot: TSnapshot): void {
|
||||
this.#snapshot = snapshot;
|
||||
}
|
||||
|
||||
#notifyListeners() {
|
||||
if (this.#listeners) for (const fn of this.#listeners) fn();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// --- Managed Lifecycle Helper hook ---
|
||||
|
||||
export abstract class SyncExternalDisposableStorable<TSnapshot extends object> extends SyncExternalStorable<TSnapshot> {
|
||||
abstract dispose(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook: creates a SyncExternalStorable instance via factory (once, in a ref),
|
||||
* subscribes to snapshot changes, and disposes on unmount.
|
||||
* Returns `[snapshot, instance]`.
|
||||
*/
|
||||
export function useManagedSyncStorable<Tds extends SyncExternalDisposableStorable<object>>(disposableStoreFactory: () => Tds): [
|
||||
snapshot: _SnapshotOf<Tds>,
|
||||
instance: Tds,
|
||||
] {
|
||||
// create the store object
|
||||
const ref = React.useRef<Tds | null>(null);
|
||||
if (!ref.current) ref.current = disposableStoreFactory();
|
||||
const ds = ref.current;
|
||||
|
||||
// subscribe to snapshot changes
|
||||
const snapshot = React.useSyncExternalStore(ds.subscribe, ds.getSnapshot) as _SnapshotOf<Tds>;
|
||||
|
||||
// lifecycle: dispose on unmount
|
||||
React.useEffect(() => () => {
|
||||
// [HMR] reset the ref so that a new instance will be created on next render after hot reload
|
||||
ref.current = null;
|
||||
ds.dispose();
|
||||
}, [ds]);
|
||||
|
||||
return [snapshot, ds];
|
||||
}
|
||||
|
||||
type _SnapshotOf<T> = T extends SyncExternalDisposableStorable<infer S> ? S : never;
|
||||
@@ -2,8 +2,8 @@ import * as React from 'react';
|
||||
|
||||
import { Box, FormControl, FormHelperText, FormLabel, IconButton, Input, InputSlotsAndSlotProps } from '@mui/joy';
|
||||
import KeyIcon from '@mui/icons-material/Key';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
|
||||
import { getIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
@@ -20,6 +20,7 @@ const slotPropsInputSx: InputSlotsAndSlotProps['slotProps'] = {
|
||||
|
||||
export function FormInputKey(props: {
|
||||
autoCompleteId: string, // introduced to avoid clashes
|
||||
size?: 'sm' | 'md' | 'lg',
|
||||
label?: string, rightLabel?: string | React.JSX.Element,
|
||||
tooltip?: string,
|
||||
description?: string | React.JSX.Element,
|
||||
@@ -40,7 +41,7 @@ export function FormInputKey(props: {
|
||||
|
||||
const endDecorator = React.useMemo(() => !!props.value && !props.noKey && (
|
||||
<IconButton onClick={() => setIsVisible(!isVisible)}>
|
||||
{isVisible ? <VisibilityIcon sx={{ fontSize: 'lg' }} /> : <VisibilityOffIcon sx={{ fontSize: 'md' }} />}
|
||||
{isVisible ? <VisibilityOutlinedIcon sx={{ fontSize: 'lg' }} /> : <VisibilityOffOutlinedIcon sx={{ fontSize: 'md' }} />}
|
||||
</IconButton>
|
||||
), [props.value, props.noKey, isVisible]);
|
||||
|
||||
@@ -48,7 +49,7 @@ export function FormInputKey(props: {
|
||||
const ghostUsername = props.noKey ? null : props.autoCompleteId.replace('-key', '').replace('-', ' ');
|
||||
|
||||
return (
|
||||
<FormControl id={acId}>
|
||||
<FormControl size={props.size} id={acId}>
|
||||
|
||||
{!!props.label && <Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'baseline', flexWrap: 'wrap', justifyContent: 'space-between' }}>
|
||||
{props.tooltip ? (
|
||||
@@ -73,6 +74,7 @@ export function FormInputKey(props: {
|
||||
name={acId}
|
||||
autoComplete={props.noKey ? 'on' /* e.g. host names */ : 'new-password' /* tells password managers this is a 'new password' entry */}
|
||||
autoFocus={disableAutoFocus ? undefined : !props.required ? undefined : props.value ? undefined : true}
|
||||
// size={props.size}
|
||||
variant={props.required ? 'outlined' : 'outlined' /* 'soft */}
|
||||
value={props.value} onChange={handleChange}
|
||||
placeholder={props.required ? props.placeholder ? 'required: ' + props.placeholder : 'required' : props.placeholder || '...'}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { StarIconUnstyled, StarredNoXL2 } from '~/common/components/StarIcons';
|
||||
import { TooltipOutlined } from '~/common/components/TooltipOutlined';
|
||||
import { findModelsServiceOrNull, getChatLLMId, llmsStoreActions } from '~/common/stores/llms/store-llms';
|
||||
import { optimaActions, optimaOpenModels } from '~/common/layout/optima/useOptima';
|
||||
import { sortLLMsByServiceLabel } from '~/common/stores/llms/components/llms.dropdown.utils';
|
||||
import { useToggleableStringSet } from '~/common/util/hooks/useToggleableStringSet';
|
||||
import { useUIPreferencesStore } from '~/common/stores/store-ui';
|
||||
import { useVisibleLLMs } from '~/common/stores/llms/llms.hooks';
|
||||
@@ -202,12 +203,15 @@ export function useLLMSelect(
|
||||
const optimizeToSingleVisibleId = (!controlledOpen && _filteredLLMs.length > LLM_SELECT_REDUCE_OPTIONS) ? llmId : null; // id to keep visible when optimizing
|
||||
|
||||
const optionsArray = React.useMemo(() => {
|
||||
// sort LLMs alphabetically by service label so vendor groups appear in a stable order (groups remain contiguous because sort is stable on equal keys)
|
||||
const sortedLLMs = sortLLMsByServiceLabel(_filteredLLMs);
|
||||
|
||||
// check if we have multiple services (to show collapsible headers)
|
||||
const hasMultipleServices = _filteredLLMs.some((llm, i, arr) => i > 0 && llm.sId !== arr[i - 1].sId);
|
||||
const hasMultipleServices = sortedLLMs.some((llm, i, arr) => i > 0 && llm.sId !== arr[i - 1].sId);
|
||||
|
||||
// create the option items
|
||||
let prevServiceId: DModelsServiceId | null = null;
|
||||
return _filteredLLMs.reduce((acc, llm, _index) => {
|
||||
return sortedLLMs.reduce((acc, llm, _index) => {
|
||||
|
||||
if (optimizeToSingleVisibleId && llm.id !== optimizeToSingleVisibleId)
|
||||
return acc;
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/**
|
||||
* This is taken from the Codepen website - all rights reserved to them.
|
||||
* This is the code of the public facing website, we embed it to send traffic to them.
|
||||
*/
|
||||
export function CodePenIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 100 100' width='100' height='100' stroke='none' fill='currentColor' {...props}>
|
||||
<path d='M100 34.2c-.4-2.6-3.3-4-5.3-5.3-3.6-2.4-7.1-4.7-10.7-7.1-8.5-5.7-17.1-11.4-25.6-17.1-2-1.3-4-2.7-6-4-1.4-1-3.3-1-4.8 0-5.7 3.8-11.5 7.7-17.2 11.5L5.2 29C3 30.4.1 31.8 0 34.8c-.1 3.3 0 6.7 0 10v16c0 2.9-.6 6.3 2.1 8.1 6.4 4.4 12.9 8.6 19.4 12.9 8 5.3 16 10.7 24 16 2.2 1.5 4.4 3.1 7.1 1.3 2.3-1.5 4.5-3 6.8-4.5 8.9-5.9 17.8-11.9 26.7-17.8l9.9-6.6c.6-.4 1.3-.8 1.9-1.3 1.4-1 2-2.4 2-4.1V37.3c.1-1.1.2-2.1.1-3.1 0-.1 0 .2 0 0zM54.3 12.3 88 34.8 73 44.9 54.3 32.4V12.3zm-8.6 0v20L27.1 44.8 12 34.8l33.7-22.5zM8.6 42.8 19.3 50 8.6 57.2V42.8zm37.1 44.9L12 65.2l15-10.1 18.6 12.5v20.1zM50 60.2 34.8 50 50 39.8 65.2 50 50 60.2zm4.3 27.5v-20l18.6-12.5 15 10.1-33.6 22.4zm37.1-30.5L80.7 50l10.8-7.2-.1 14.4z' />
|
||||
{/*<path d='M75 0L100 0L100 25Z' />*/}
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/**
|
||||
* This is taken from wikipedia: https://upload.wikimedia.org/wikipedia/commons/d/d0/Google_Colaboratory_SVG_Logo.svg
|
||||
*/
|
||||
export function GoogleColabIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 24 24' width='977' height='602' stroke='none' fill='currentColor' {...props}>
|
||||
<path d='M4.54,9.46,2.19,7.1a6.93,6.93,0,0,0,0,9.79l2.36-2.36A3.59,3.59,0,0,1,4.54,9.46Z' fill='#E8710A' />
|
||||
<path d='M2.19,7.1,4.54,9.46a3.59,3.59,0,0,1,5.08,0l1.71-2.93h0l-.1-.08h0A6.93,6.93,0,0,0,2.19,7.1Z' fill='#F9AB00' />
|
||||
<path d='M11.34,17.46h0L9.62,14.54a3.59,3.59,0,0,1-5.08,0L2.19,16.9a6.93,6.93,0,0,0,9,.65l.11-.09' fill='#F9AB00' />
|
||||
<path d='M12,7.1a6.93,6.93,0,0,0,0,9.79l2.36-2.36a3.59,3.59,0,1,1,5.08-5.08L21.81,7.1A6.93,6.93,0,0,0,12,7.1Z' fill='#F9AB00' />
|
||||
<path d='M21.81,7.1,19.46,9.46a3.59,3.59,0,0,1-5.08,5.08L12,16.9A6.93,6.93,0,0,0,21.81,7.1Z' fill='#E8710A' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/**
|
||||
* This is taken from the JSFiddle website - all rights reserved to them.
|
||||
* This is the code of the public facing website, we embed it to send traffic to them.
|
||||
*/
|
||||
export function JSFiddleIcon(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox="0 0 46 33" width="46" height="33" strokeWidth={4} stroke="currentColor" {...props}>
|
||||
<g fill="none" fillRule="evenodd">
|
||||
<path
|
||||
d="M23.4888889,20.543316 C21.4404656,18.4187374 19.0750303,15.6666667 16.4832014,15.6666667 C13.8721947,15.6666667 11.7555556,17.6366138 11.7555556,20.0666667 C11.7555556,22.4967196 13.8721947,24.4666667 16.4832014,24.4666667 C18.8347252,24.4666667 19.9845474,23.0125628 20.6429148,22.312473"
|
||||
strokeLinecap="round"
|
||||
></path>
|
||||
<path
|
||||
d="M22.5111111,19.5900174 C24.5595344,21.7145959 26.9249697,24.4666667 29.5167986,24.4666667 C32.1278053,24.4666667 34.2444444,22.4967196 34.2444444,20.0666667 C34.2444444,17.6366138 32.1278053,15.6666667 29.5167986,15.6666667 C27.1652748,15.6666667 26.0154526,17.1207706 25.3570852,17.8208603"
|
||||
strokeLinecap="round"
|
||||
></path>
|
||||
<path
|
||||
d="M45,22.7331459 C45,19.1499462 42.7950446,16.079593 39.6628004,14.7835315 C39.6774469,14.5246474 39.7003932,14.2674038 39.7003932,14.0035978 C39.7003932,6.82243304 33.8412885,1 26.611593,1 C21.3985635,1 16.9102123,4.03409627 14.8051788,8.41527616 C13.7828502,7.62878013 12.503719,7.15547161 11.1134367,7.15547161 C7.77825654,7.15547161 5.07450503,9.84159999 5.07450503,13.1544315 C5.07450503,13.7760488 5.16938207,14.3779791 5.3477444,14.9418479 C2.74863428,16.4787471 1,19.2867709 1,22.5105187 C1,27.3287502 4.89630545,31.2367856 9.72803666,31.31094 L36.3341301,31.3109406 C41.1201312,31.3406346 45,27.4870665 45,22.7331459 L45,22.7331459 Z"
|
||||
strokeLinejoin="round"
|
||||
></path>
|
||||
</g>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/**
|
||||
* This is taken from the StackBlitz website - all rights reserved to them.
|
||||
* This is the code of the public facing website, we embed it to send traffic to them.
|
||||
*/
|
||||
export function StackBlitzIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 28 28' width='28' height='28' stroke='none' fill='currentColor' {...props}>
|
||||
<path d='M12.747 16.273h-7.46L18.925 1.5l-3.671 10.227h7.46L9.075 26.5l3.671-10.227z' />
|
||||
{/*<path d='M21 0L28 0L28 7Z' />*/}
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - image-square
|
||||
*/
|
||||
export function PhImageSquare(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M208,32H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM48,48H208v77.38l-24.69-24.7a16,16,0,0,0-22.62,0L53.37,208H48ZM208,208H76l96-96,36,36v60ZM96,120A24,24,0,1,0,72,96,24,24,0,0,0,96,120Zm0-32a8,8,0,1,1-8,8A8,8,0,0,1,96,88Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - list-checks (regular)
|
||||
*/
|
||||
export function PhListChecks(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M128,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16h80A8,8,0,0,1,128,128ZM40,72H184a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16Zm80,112H40a8,8,0,0,0,0,16h80a8,8,0,0,0,0-16Zm133.66-50.34a8,8,0,0,0-11.32,0L208,171.31l-10.34-10.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32,0l40-40A8,8,0,0,0,253.66,131.66Zm0,64a8,8,0,0,0-11.32,0L208,235.31l-10.34-10.34a8,8,0,0,0-11.32,11.32l16,16a8,8,0,0,0,11.32,0l40-40A8,8,0,0,0,253.66,195.66Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - paint-brush-household
|
||||
* Source: 'https://phosphoricons.com/' - paint-brush
|
||||
*/
|
||||
export function PhPaintBrush(props: SvgIconProps) {
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: 'https://phosphoricons.com/' - wrench
|
||||
*/
|
||||
export function PhWrench(props: SvgIconProps) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 256 256' stroke='none' fill='currentColor' width='24' height='24' {...props}>
|
||||
<path d='M226.76,69a8,8,0,0,0-12.84-2.88l-40.3,37.19-17.23-3.7-3.7-17.23,37.19-40.3A8,8,0,0,0,187,29.24,72,72,0,0,0,88,96,72.34,72.34,0,0,0,94,124.94L33.79,177c-.15.12-.29.26-.43.39a32,32,0,0,0,45.26,45.26c.13-.13.27-.28.39-.42L131.06,162A72,72,0,0,0,232,96,71.56,71.56,0,0,0,226.76,69ZM160,152a56.14,56.14,0,0,1-27.07-7,8,8,0,0,0-9.92,1.77L67.11,211.51a16,16,0,0,1-22.62-22.62L109.18,133a8,8,0,0,0,1.77-9.93,56,56,0,0,1,58.36-82.31l-31.2,33.81a8,8,0,0,0-1.94,7.1L141.83,108a8,8,0,0,0,6.14,6.14l26.35,5.66a8,8,0,0,0,7.1-1.94l33.81-31.2A56.06,56.06,0,0,1,160,152Z' />
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function ArceeAIIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='-0.64 -2.13 24 24' width='24' height='24' fill='currentColor' stroke='none' {...props}>
|
||||
<path d='M11.3728 0L0 19.7457H2.63715L12.6896 2.29149L11.3728 0Z' />
|
||||
<path d='M14.8716 6.091L3.96021 19.7458H6.8881L16.0971 8.22193L14.8716 6.091Z' />
|
||||
<path d='M19.4405 14.0409L18.2907 12.0391L8.13135 19.7442H11.9207L19.4405 14.0409Z' />
|
||||
<path d='M21.5276 17.6735L13.4695 19.7452H22.7194L21.5276 17.6735Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function ChutesAIIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 -10.5 62 62' width='24' height='24' fill='currentColor' stroke='none' {...props}>
|
||||
<path d='M38 39.7C37.1 41.1 35.3 41.4 34 40.3L28.7 35.2C27.5 34 27.5 32 28.7 30.9L34.1 26C40.1 20.5 48.1 18.4 55.6 20.2L59.7 21.2C59.8 21.2 60 21.3 60.1 21.4C60.7 22 60.3 23 59.6 23.1L59.5 23.1C52 23.6 45.2 28 41 34.9L38 39.7Z' />
|
||||
<path d='M15.3 36.6C14.2 37.8 12.3 37.7 11.3 36.3L0.5 21.4C-0.4 20 -0.1 18 1.3 17.2L22.8 4.3C29.6 0.2 37.7-0.3 44.8 3.1L60.3 10.5C60.6 10.6 60.8 10.8 61 11C61.8 12.2 60.9 13.9 59.5 13.7L50.2 12.4C42.9 11.4 35.4 14.3 30.1 20.2L15.3 36.6Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function CloudflareIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 -17.5 75 75' width='24' height='24' fill='currentColor' stroke='none' {...props}>
|
||||
<path d='M51 36.4L51.4 35.1C51.9 33.5 51.7 32.1 50.9 31C50.2 30 49.1 29.4 47.6 29.4L20.7 29C20.5 29 20.4 28.9 20.3 28.8C20.2 28.7 20.2 28.5 20.3 28.3C20.3 28 20.6 27.8 20.9 27.8L48 27.5C51.2 27.3 54.7 24.7 56 21.5L57.5 17.4C57.5 17.3 57.6 17.2 57.6 17.1V16.9C55.8 9 48.7 3 40.3 3C32.5 3 25.9 8.1 23.6 15.1C22 13.9 20.1 13.3 18 13.5C14.2 13.9 11.2 16.9 10.9 20.7C10.8 21.6 10.8 22.6 11.1 23.4C5 23.6 0.1 28.6 0.1 34.8C0.1 35.4 0.1 35.9 0.2 36.5C0.2 36.7 0.5 36.9 0.7 36.9H50.4C50.7 36.9 50.9 36.7 51 36.4Z' />
|
||||
<path d='M60 17.7C59.7 17.7 59.5 17.7 59.2 17.8H59.1C59 17.8 58.9 17.9 58.9 18.1L57.8 21.7C57.3 23.3 57.5 24.8 58.3 25.8C59 26.8 60.1 27.4 61.6 27.5L67.3 27.8C67.5 27.8 67.6 27.9 67.7 28.1C67.8 28.2 67.8 28.4 67.8 28.5C67.7 28.8 67.4 29 67.1 29L61.2 29.4C58 29.5 54.5 32.1 53.2 35.3L52.8 36.5C52.7 36.7 52.9 36.9 53.1 36.9H73.6C73.9 36.9 74.1 36.7 74.1 36.5C74.5 35.2 74.7 33.9 74.7 32.5C74.7 24.4 68.1 17.7 60 17.7Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
+2
-2
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function DeepseekIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox="0 0 56.2 41.3594" width="56.2" height="41.359375" strokeWidth={0} stroke='none' fill='currentColor' strokeLinecap='butt' strokeLinejoin='miter' {...props}>
|
||||
<path id="path" d="M55.6128 3.47119C55.0175 3.17944 54.7611 3.73535 54.413 4.01782C54.2939 4.10889 54.1932 4.22729 54.0924 4.33667C53.2223 5.26587 52.2057 5.87646 50.8776 5.80347C48.9359 5.69409 47.2781 6.30469 45.8126 7.78979C45.5012 5.9585 44.4663 4.86499 42.8909 4.16357C42.0667 3.79907 41.2332 3.43457 40.6561 2.64185C40.2532 2.07715 40.1432 1.44849 39.9418 0.828857C39.8135 0.455322 39.6853 0.0725098 39.2548 0.00878906C38.7877 -0.0639648 38.6045 0.327637 38.4213 0.655762C37.6886 1.99512 37.4047 3.47119 37.4321 4.96533C37.4962 8.32739 38.9159 11.0059 41.7369 12.9102C42.0575 13.1289 42.1399 13.3474 42.0392 13.6665C41.8468 14.3225 41.6178 14.9602 41.4164 15.6162C41.2881 16.0354 41.0957 16.1265 40.647 15.9441C39.0991 15.2974 37.7618 14.3406 36.5803 13.1836C34.5745 11.2429 32.761 9.10181 30.4988 7.42529C29.9675 7.03345 29.4363 6.66919 28.8867 6.32275C26.5786 4.08154 29.189 2.24097 29.7935 2.02246C30.4254 1.79468 30.0133 1.01099 27.9708 1.02026C25.9283 1.0293 24.0599 1.71265 21.6786 2.62378C21.3306 2.7605 20.9641 2.8606 20.5886 2.94263C18.4271 2.53271 16.1831 2.44141 13.8384 2.70581C9.42371 3.19775 5.89758 5.28418 3.30554 8.84668C0.191406 13.1289 -0.54126 17.9941 0.356323 23.0691C1.29968 28.4172 4.02905 32.8452 8.22388 36.3076C12.5745 39.8972 17.5845 41.6558 23.2997 41.3186C26.771 41.1182 30.6361 40.6536 34.9958 36.9636C36.0948 37.5103 37.2489 37.7288 39.1632 37.8928C40.6378 38.0295 42.0575 37.8201 43.1565 37.5923C44.8784 37.2278 44.7594 35.6333 44.1366 35.3418C39.09 32.9912 40.1981 33.9478 39.1907 33.1733C41.7552 30.1394 45.6204 26.9868 47.1316 16.7732C47.2506 15.9624 47.1499 15.4521 47.1316 14.7961C47.1224 14.3953 47.214 14.2405 47.672 14.1948C48.9359 14.0491 50.1632 13.7029 51.2898 13.0833C54.5596 11.2976 55.8784 8.36377 56.1898 4.84692C56.2357 4.30933 56.1807 3.75342 55.6128 3.47119ZM27.119 35.123C22.2281 31.2783 19.856 30.0117 18.8759 30.0664C17.96 30.1211 18.1249 31.1689 18.3263 31.8523C18.537 32.5264 18.8118 32.9912 19.1964 33.5833C19.462 33.9751 19.6453 34.5581 18.9309 34.9956C17.3555 35.9705 14.6169 34.6675 14.4886 34.6038C11.3014 32.7268 8.63611 30.2485 6.75842 26.8594C4.94495 23.5974 3.89172 20.0989 3.71765 16.3633C3.67188 15.4614 3.9375 15.1423 4.83508 14.9785C6.0166 14.7598 7.23474 14.7141 8.41626 14.8872C13.408 15.6162 17.6577 17.8484 21.2206 21.3835C23.2539 23.397 24.7926 25.8025 26.3772 28.1531C28.0624 30.6494 29.8759 33.0276 32.184 34.9773C32.9991 35.6606 33.6494 36.1799 34.2722 36.5627C32.3947 36.7722 29.2622 36.8179 27.119 35.123ZM29.4637 20.0442C29.4637 19.6433 29.7843 19.3245 30.1874 19.3245C30.2789 19.3245 30.3613 19.3425 30.4346 19.3699C30.5354 19.4065 30.627 19.4612 30.7002 19.543C30.8285 19.6707 30.9017 19.8528 30.9017 20.0442C30.9017 20.4451 30.5812 20.7639 30.1782 20.7639C29.7751 20.7639 29.4637 20.4451 29.4637 20.0442ZM36.7452 23.7798C36.2781 23.9712 35.811 24.135 35.3622 24.1533C34.6661 24.1897 33.9059 23.9072 33.4938 23.561C32.8527 23.0234 32.3947 22.7229 32.2023 21.7844C32.1199 21.3835 32.1656 20.7639 32.239 20.4087C32.4038 19.6433 32.2206 19.1514 31.6803 18.7048C31.2406 18.3403 30.6819 18.2402 30.0682 18.2402C29.8392 18.2402 29.6287 18.1399 29.4729 18.0579C29.2164 17.9304 29.0059 17.6116 29.2073 17.2197C29.2714 17.0923 29.5829 16.7825 29.6561 16.7278C30.4896 16.2539 31.4513 16.4089 32.3397 16.7642C33.1641 17.1013 33.7869 17.7209 34.6844 18.5955C35.6003 19.6523 35.7651 19.9441 36.2872 20.7366C36.6995 21.3562 37.075 21.9939 37.3314 22.7229C37.4871 23.1785 37.2856 23.552 36.7452 23.7798Z"/>
|
||||
return <SvgIcon viewBox='0 0 56.2 41.4' width='24' height='24' stroke='none' fill='currentColor' {...props}>
|
||||
<path d='M55.6 3.5C55 3.2 54.8 3.7 54.4 4C54.3 4.1 54.2 4.2 54.1 4.3C53.2 5.3 52.2 5.9 50.9 5.8C48.9 5.7 47.3 6.3 45.8 7.8C45.5 6 44.5 4.9 42.9 4.2C42.1 3.8 41.2 3.4 40.7 2.6C40.3 2.1 40.1 1.4 39.9 0.8C39.8 0.5 39.7 0.1 39.3 0C38.8-0.1 38.6 0.3 38.4 0.7C37.7 2 37.4 3.5 37.4 5C37.5 8.3 38.9 11 41.7 12.9C42.1 13.1 42.1 13.3 42 13.7C41.8 14.3 41.6 15 41.4 15.6C41.3 16 41.1 16.1 40.6 15.9C39.1 15.3 37.8 14.3 36.6 13.2C34.6 11.2 32.8 9.1 30.5 7.4C30 7 29.4 6.7 28.9 6.3C26.6 4.1 29.2 2.2 29.8 2C30.4 1.8 30 1 28 1C25.9 1 24.1 1.7 21.7 2.6C21.3 2.8 21 2.9 20.6 2.9C18.4 2.5 16.2 2.4 13.8 2.7C9.4 3.2 5.9 5.3 3.3 8.8C0.2 13.1-0.5 18 0.4 23.1C1.3 28.4 4 32.8 8.2 36.3C12.6 39.9 17.6 41.7 23.3 41.3C26.8 41.1 30.6 40.7 35 37C36.1 37.5 37.2 37.7 39.2 37.9C40.6 38 42.1 37.8 43.2 37.6C44.9 37.2 44.8 35.6 44.1 35.3C39.1 33 40.2 33.9 39.2 33.2C41.8 30.1 45.6 27 47.1 16.8C47.3 16 47.1 15.5 47.1 14.8C47.1 14.4 47.2 14.2 47.7 14.2C48.9 14 50.2 13.7 51.3 13.1C54.6 11.3 55.9 8.4 56.2 4.8C56.2 4.3 56.2 3.8 55.6 3.5ZM27.1 35.1C22.2 31.3 19.9 30 18.9 30.1C18 30.1 18.1 31.2 18.3 31.9C18.5 32.5 18.8 33 19.2 33.6C19.5 34 19.6 34.6 18.9 35C17.4 36 14.6 34.7 14.5 34.6C11.3 32.7 8.6 30.2 6.8 26.9C4.9 23.6 3.9 20.1 3.7 16.4C3.7 15.5 3.9 15.1 4.8 15C6 14.8 7.2 14.7 8.4 14.9C13.4 15.6 17.7 17.8 21.2 21.4C23.3 23.4 24.8 25.8 26.4 28.2C28.1 30.6 29.9 33 32.2 35C33 35.7 33.6 36.2 34.3 36.6C32.4 36.8 29.3 36.8 27.1 35.1ZM29.5 20C29.5 19.6 29.8 19.3 30.2 19.3C30.3 19.3 30.4 19.3 30.4 19.4C30.5 19.4 30.6 19.5 30.7 19.5C30.8 19.7 30.9 19.9 30.9 20C30.9 20.4 30.6 20.8 30.2 20.8C29.8 20.8 29.5 20.4 29.5 20ZM36.7 23.8C36.3 24 35.8 24.1 35.4 24.2C34.7 24.2 33.9 23.9 33.5 23.6C32.9 23 32.4 22.7 32.2 21.8C32.1 21.4 32.2 20.8 32.2 20.4C32.4 19.6 32.2 19.2 31.7 18.7C31.2 18.3 30.7 18.2 30.1 18.2C29.8 18.2 29.6 18.1 29.5 18.1C29.2 17.9 29 17.6 29.2 17.2C29.3 17.1 29.6 16.8 29.7 16.7C30.5 16.3 31.5 16.4 32.3 16.8C33.2 17.1 33.8 17.7 34.7 18.6C35.6 19.7 35.8 19.9 36.3 20.7C36.7 21.4 37.1 22 37.3 22.7C37.5 23.2 37.3 23.6 36.7 23.8Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function FireworksAIIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='-0.5 -12 48 48' width='24' height='24' fill='currentColor' stroke='none' {...props}>
|
||||
<path fillRule='evenodd' clipRule='evenodd' d='M29 .4L23.5 13.5 18 .4H14.5L20.5 14.8C21 16 22.2 16.7 23.5 16.7 24.8 16.7 26 16 26.5 14.8L32.5.4H29ZM31.4 20.4L41.4 10.3 40 7 29.1 18.2C28.2 19.1 27.9 20.5 28.4 21.7 28.9 22.8 30.1 23.6 31.4 23.6H47.1L45.7 20.4H31.4ZM5.6 10.3L7 7 18 18.1C18.9 19.1 19.2 20.5 18.7 21.6 18.2 22.8 17 23.6 15.7 23.6H0L1.4 20.3H15.7L5.6 10.3Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function HeliconeIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='30 30 252 252' width='24' height='24' fill='none' stroke='currentColor' strokeWidth='9' strokeLinecap='round' strokeLinejoin='round' {...props}>
|
||||
<path d='M36 89L155 37C156 36.6 157 36.6 158 37L276 89M36 89V197M36 89L129 130M276 89L244 103M276 89V117M156 278V238M156 278L183 266M156 278L47 232M156 141L244 103M156 141L129 130M156 141V153M156 238L244 103M156 238V153M183 266L274 228C275 228 276 226 276 224V117M183 266L276 117M36 197V224C36 226 37 228 39 228L47 232M36 197L129 130M47 232L156 153' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
+2
-2
@@ -4,7 +4,7 @@ import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function InworldIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 141 181' width='24' height='24' fill='currentColor' {...props}>
|
||||
<path d='M48.2616 34.7993C47.9981 34.8585 47.9766 34.6058 48.1379 34.5144C53.3155 31.4874 60.6866 30.767 61.235 30.353C61.7297 29.6594 57.864 29.697 56.5199 29.7024C46.74 30.0411 38.2989 33.681 31.288 40.2941C20.5242 49.4664 16.9973 64.5582 18.4866 78.1285C20.2285 92.1611 27.9384 105.344 39.385 113.635C63.2405 129.673 96.9242 122.011 114.774 100.183C136.753 73.537 130.194 35.6004 107.215 18.2613C101.252 14.1214 94.7359 10.8524 87.7035 8.98143C84.4238 8.00828 79.8592 7.07277 77.7624 7.00288C74.5472 6.90073 75.6171 9.54596 75.0472 10.1965C74.4773 10.8471 68.5901 11.1105 75.2515 13.4493C79.1925 14.8311 76.7946 14.9117 73.7891 17.6322C71.1278 20.0409 73.4504 22.7399 72.531 23.6861C68.5847 27.7508 74.6279 29.5358 77.7462 31.31C92.8542 39.3747 101.177 53.8913 98.4242 70.9831C94.5585 93.5644 60.9286 103.317 46.0464 85.462C34.5999 71.924 38.928 49.3858 55.2241 42.0361C57.4822 40.9447 62.5792 39.3532 60.3909 39.536C51.9552 40.2403 45.369 44.7297 43.2399 46.3588C43.0571 46.4986 42.8152 46.2835 42.9281 46.0846C44.4711 43.3695 53.4122 37.036 71.0041 34.8101C74.1494 34.1327 62.3157 31.6541 48.2616 34.7993Z' />
|
||||
<path d='M55.4704 170.577C56.4274 170.566 57.3791 170.706 58.2716 170.797C60.5673 170.867 63.148 170.604 65.6696 170.932C71.1536 171.244 76.6807 172.475 82.1808 172.609C85.9712 172.889 89.5896 172.717 93.4607 172.862C96.3801 173.238 99.848 173.007 102.466 173.4C104.101 173.588 105.601 173.507 107.241 173.749C111.579 174.367 115.902 174.459 120.295 174.4C121.967 174.453 123.682 174.486 125.349 174.378C127.128 174.276 129.069 174.351 130.499 173.212C131.349 172.711 131.306 171.695 131.483 170.996C131.704 170.615 132.166 170.41 132.37 170.023C132.596 169.357 132.741 168.663 132.752 167.98V167.937C132.8 165.615 131.688 163.33 130.838 161.174C129.424 158.05 127.763 155.13 125.967 152.206C123.962 149.372 122.956 145.506 119.547 143.974C113.402 141.281 107.085 141.565 100.283 140.743C87.482 139.608 73.8849 138.775 61.9867 138.614C50.8843 138.775 31.2708 139.501 21.1738 140.399C3.2378 141.996 11.5875 144.84 11.023 149.872C10.8187 151.727 8.79172 151.576 9.01753 153.394C9.39388 156.028 14.3349 159.324 16.8188 159.991C27.76 162.916 13.2435 163.351 14.8672 166.276C16.2166 168.711 22.2383 168.012 24.6954 168.98L55.4597 170.577H55.4704Z' />
|
||||
<path d='M48.3 34.8C48 34.9 48 34.6 48.1 34.5C53.3 31.5 60.7 30.8 61.2 30.4C61.7 29.7 57.9 29.7 56.5 29.7C46.7 30 38.3 33.7 31.3 40.3C20.5 49.5 17 64.6 18.5 78.1C20.2 92.2 27.9 105.3 39.4 113.6C63.2 129.7 96.9 122 114.8 100.2C136.8 73.5 130.2 35.6 107.2 18.3C101.3 14.1 94.7 10.9 87.7 9C84.4 8 79.9 7.1 77.8 7C74.5 6.9 75.6 9.5 75 10.2C74.5 10.8 68.6 11.1 75.3 13.4C79.2 14.8 76.8 14.9 73.8 17.6C71.1 20 73.5 22.7 72.5 23.7C68.6 27.8 74.6 29.5 77.7 31.3C92.9 39.4 101.2 53.9 98.4 71C94.6 93.6 60.9 103.3 46 85.5C34.6 71.9 38.9 49.4 55.2 42C57.5 40.9 62.6 39.4 60.4 39.5C52 40.2 45.4 44.7 43.2 46.4C43.1 46.5 42.8 46.3 42.9 46.1C44.5 43.4 53.4 37 71 34.8C74.1 34.1 62.3 31.7 48.3 34.8Z' />
|
||||
<path d='M55.5 170.6C56.4 170.6 57.4 170.7 58.3 170.8C60.6 170.9 63.1 170.6 65.7 170.9C71.2 171.2 76.7 172.5 82.2 172.6C86 172.9 89.6 172.7 93.5 172.9C96.4 173.2 99.8 173 102.5 173.4C104.1 173.6 105.6 173.5 107.2 173.7C111.6 174.4 115.9 174.5 120.3 174.4C122 174.5 123.7 174.5 125.3 174.4C127.1 174.3 129.1 174.4 130.5 173.2C131.3 172.7 131.3 171.7 131.5 171C131.7 170.6 132.2 170.4 132.4 170C132.6 169.4 132.7 168.7 132.8 168V167.9C132.8 165.6 131.7 163.3 130.8 161.2C129.4 158.1 127.8 155.1 126 152.2C124 149.4 123 145.5 119.5 144C113.4 141.3 107.1 141.6 100.3 140.7C87.5 139.6 73.9 138.8 62 138.6C50.9 138.8 31.3 139.5 21.2 140.4C3.2 142 11.6 144.8 11 149.9C10.8 151.7 8.8 151.6 9 153.4C9.4 156 14.3 159.3 16.8 160C27.8 162.9 13.2 163.4 14.9 166.3C16.2 168.7 22.2 168 24.7 169L55.5 170.6H55.5Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function MiniMaxIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 36.9 31.1' width='24' height='24' stroke='none' fill='currentColor' {...props}>
|
||||
<path d='M17.6 3.1C17.6 2.4 17 1.8 16.3 1.8C15.5 1.8 14.9 2.4 14.9 3.1V25C14.9 26.7 13.6 28.1 11.9 28.1C10.2 28.1 8.8 26.7 8.8 25V11C8.8 10.2 8.2 9.6 7.5 9.6C6.7 9.6 6.1 10.2 6.1 11V16.9C6.1 18.6 4.8 20 3.1 20C1.4 20 0 18.6 0 16.9V14.8C0 14.3 0.4 13.9 0.9 13.9C1.4 13.9 1.8 14.3 1.8 14.8V16.9C1.8 17.7 2.3 18.2 3.1 18.2C3.8 18.2 4.4 17.6 4.4 16.9V11C4.4 9.3 5.8 7.9 7.5 7.9C9.2 7.9 10.5 9.3 10.5 11V25C10.5 25.8 11.1 26.4 11.9 26.4C12.6 26.4 13.2 25.8 13.2 25V16.4V3.1C13.2 1.4 14.6 0 16.3 0C18 0 19.3 1.4 19.3 3.1V21.3C19.3 21.7 18.9 22.1 18.5 22.1C18 22.1 17.6 21.7 17.6 21.3V3.1Z' />
|
||||
<path d='M33.8 7.9C32.1 7.9 30.8 9.3 30.8 11V22.7C30.8 23.4 30.2 24 29.4 24C28.7 24 28.1 23.4 28.1 22.7V3.1C28.1 1.4 26.7 0 25.1 0C23.4 0 22 1.4 22 3.1V28C22 28.7 21.4 29.3 20.7 29.3C19.9 29.3 19.3 28.7 19.3 28V24.9C19.3 24.4 18.9 24 18.5 24C18 24 17.6 24.4 17.6 24.9V28C17.6 29.7 19 31.1 20.7 31.1C22.4 31.1 23.7 29.7 23.7 28V3.1C23.7 2.4 24.3 1.8 25.1 1.8C25.8 1.8 26.4 2.4 26.4 3.1V22.7C26.4 24.4 27.8 25.8 29.4 25.8C31.1 25.8 32.5 24.4 32.5 22.7V11C32.5 10.2 33.1 9.6 33.8 9.6C34.6 9.6 35.2 10.2 35.2 11V21.3C35.2 21.7 35.6 22.1 36 22.1C36.5 22.1 36.9 21.7 36.9 21.3V11C36.9 9.3 35.5 7.9 33.8 7.9Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function NovitaAIIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 24 24' width='24' height='24' fill='currentColor' stroke='none' {...props}>
|
||||
<path d='M24 18.8H14.3L9.2 13.7V18.8H0V18.8L9.2 9.7V4L24 18.8Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+9
-9
@@ -5,34 +5,34 @@ import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
export function OpenPipeIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 24 24' width='24' height='24' strokeWidth={0} stroke='none' fill='currentColor' strokeLinecap='butt' strokeLinejoin='miter' {...props}>
|
||||
<path
|
||||
d='m 6.181795,6.5454213 h 11.63641 V 21.276364 A 0.7236348,0.7236348 0 0 1 17.09457,21.999999 H 6.90543 A 0.7236348,0.7236348 0 0 1 6.181795,21.276364 Z'
|
||||
d='m 6.2,6.5 h 11.6 V 21.3 A 0.7,0.7 0 0 1 17.1,22 H 6.9 A 0.7,0.7 0 0 1 6.2,21.3 Z'
|
||||
stroke='currentColor' strokeWidth={1}
|
||||
/>
|
||||
<path
|
||||
d='M 4.763652,2.7236346 A 0.7236348,0.7236348 0 0 1 5.487287,1.9999998 h 13.025426 a 0.7236348,0.7236348 0 0 1 0.723635,0.7236348 V 6.185413 A 0.7236348,0.7236348 0 0 1 18.512713,6.9090478 H 5.487287 A 0.7236348,0.7236348 0 0 1 4.763652,6.185413 Z'
|
||||
d='M 4.8,2.7 A 0.7,0.7 0 0 1 5.5,2 h 13 a 0.7,0.7 0 0 1 0.7,0.7 V 6.2 A 0.7,0.7 0 0 1 18.5,6.9 H 5.5 A 0.7,0.7 0 0 1 4.8,6.2 Z'
|
||||
stroke='currentColor' strokeWidth={1}
|
||||
/>
|
||||
{/* This is the orange part - comment? */}
|
||||
<path
|
||||
d='M 6.5817845,6.9091382 H 17.418216 V 21.238193 A 0.3618174,0.3618174 0 0 1 17.056398,21.60001 H 6.9436019 A 0.3618174,0.3618174 0 0 1 6.5817845,21.238193 Z M 5.1636412,2.7254436 A 0.3618174,0.3618174 0 0 1 5.5254586,2.3636262 H 18.510904 a 0.3618174,0.3618174 0 0 1 0.361818,0.3618174 V 6.1473317 A 0.3618174,0.3618174 0 0 1 18.510904,6.5091491 H 5.5254586 A 0.3618174,0.3618174 0 0 1 5.1636412,6.1473317 Z'
|
||||
d='M 6.6,6.9 H 17.4 V 21.2 A 0.4,0.4 0 0 1 17.1,21.6 H 6.9 A 0.4,0.4 0 0 1 6.6,21.2 Z M 5.2,2.7 A 0.4,0.4 0 0 1 5.5,2.4 H 18.5 a 0.4,0.4 0 0 1 0.4,0.4 V 6.1 A 0.4,0.4 0 0 1 18.5,6.5 H 5.5 A 0.4,0.4 0 0 1 5.2,6.1 Z'
|
||||
fill='#ff5733' />
|
||||
<path
|
||||
d='M 8.6909086,7.1635863 H 10.436406 V 21.509646 H 8.6908181 V 7.1635863 Z'
|
||||
d='M 8.7,7.2 H 10.4 V 21.5 H 8.7 V 7.2 Z'
|
||||
fill='#ffffff' />
|
||||
<path
|
||||
d='m 15.672718,6.9091382 h 1.745498 V 21.238193 a 0.3618174,0.3618174 0 0 1 -0.361818,0.361817 h -1.38368 z'
|
||||
d='m 15.7,6.9 h 1.7 V 21.2 a 0.4,0.4 0 0 1 -0.4,0.4 h -1.4 z'
|
||||
fill='#ffffff' fillOpacity={0.25} />
|
||||
<path
|
||||
d='M 8.2545568,7.1635863 H 8.6909086 V 21.509646 H 8.2545568 Z'
|
||||
d='M 8.3,7.2 H 8.7 V 21.5 H 8.3 Z'
|
||||
fill='#ffffff' fillOpacity={0.5} />
|
||||
<path
|
||||
d='M 7.818205,2.3636262 H 9.5999748 V 6.434072 H 7.8182954 V 2.3636262 Z'
|
||||
d='M 7.8,2.4 H 9.6 V 6.4 H 7.8 V 2.4 Z'
|
||||
fill='#ffffff' />
|
||||
<path
|
||||
d='m 17.090861,2.3636262 h 1.420043 a 0.3618174,0.3618174 0 0 1 0.361818,0.3618174 v 3.4218881 a 0.3618174,0.3618174 0 0 1 -0.361818,0.3618174 h -1.420043 z'
|
||||
d='m 17.1,2.4 h 1.4 a 0.4,0.4 0 0 1 0.4,0.4 v 3.4 a 0.4,0.4 0 0 1 -0.4,0.4 h -1.4 z'
|
||||
fill='#ffffff' fillOpacity={0.25} />
|
||||
<path
|
||||
d='M 7.3818532,2.3636262 H 7.818205 V 6.434072 H 7.3818532 Z'
|
||||
d='M 7.4,2.4 H 7.8 V 6.4 H 7.4 Z'
|
||||
fill='#ffffff' fillOpacity={0.25} />
|
||||
</SvgIcon>;
|
||||
}
|
||||
+7
-7
@@ -4,12 +4,12 @@ import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function PerplexityIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 24 24' width='24' height='24' fill='none' stroke='currentColor' strokeWidth={1.5} strokeLinecap='round' strokeLinejoin='round' {...props}>
|
||||
<path d='M 11.977248,8.1395615 5.6952727,2.280012 v 5.8595495 z' fill='none' />
|
||||
<path d='M 12.103816,8.1395615 18.385789,2.280012 v 5.8595495 z' fill='none' />
|
||||
<path d='M 11.984325,1.333998 V 22.797381' fill='none' />
|
||||
<path d='M 18.259392,13.921732 11.977418,8.1453013 v 8.0095097 l 6.281974,5.601423 z' fill='none' />
|
||||
<path d='M 5.70235,13.921732 11.984325,8.1453013 V 16.154811 L 5.70235,21.756234 Z' fill='none' />
|
||||
<path d='M 3.1231247,8.1395615 V 16.515528 H 5.7008663 V 13.915992 L 11.984301,8.1395615 Z' fill='none' />
|
||||
<path d='m 11.977418,8.2166075 6.281974,5.7764065 v 2.599535 h 2.617484 V 8.2166075 Z' fill='none' />
|
||||
<path d='M 12,8.1 5.7,2.3 v 5.9 z' fill='none' />
|
||||
<path d='M 12.1,8.1 18.4,2.3 v 5.9 z' fill='none' />
|
||||
<path d='M 12,1.3 V 22.8' fill='none' />
|
||||
<path d='M 18.3,13.9 12,8.1 v 8 l 6.3,5.6 z' fill='none' />
|
||||
<path d='M 5.7,13.9 12,8.1 V 16.2 L 5.7,21.8 Z' fill='none' />
|
||||
<path d='M 3.1,8.1 V 16.5 H 5.7 V 13.9 L 12,8.1 Z' fill='none' />
|
||||
<path d='m 12,8.2 6.3,5.8 v 2.6 h 2.6 V 8.2 Z' fill='none' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
+5
-16
@@ -3,20 +3,9 @@ import * as React from 'react';
|
||||
import { SvgIcon, SvgIconProps } from '@mui/joy';
|
||||
|
||||
export function TogetherIcon(props: SvgIconProps) {
|
||||
return <SvgIcon viewBox='0 0 976 180' width='24' height='24' strokeWidth={0} stroke='none' fill='currentColor' strokeLinecap='butt' strokeLinejoin='miter' {...props}>
|
||||
<path d='M973,3 C971,1 968,0 965,0 C962,0 960,1 958,3 C956,5 955,7 955,10 C955,13 956,16 958,18 C960,20 962,21 965,21 C968,21 971,20 973,18 C975,16 975,13 975,10 C975,7 975,5 973,3' />
|
||||
<polygon points='957 139 973 139 973 39 957 39' />
|
||||
<path d='M571,48 C564,41 554,37 542,37 C534,37 527,39 522,42 C516,46 512,50 509,55 L509,1 L493,1 L493,139 L509,139 L509,89 C509,77 511,68 517,62 C523,56 530,52 539,52 C548,52 554,55 559,60 C564,66 566,74 566,84 L566,139 L582,139 L582,83 C582,67 579,56 571,48' />
|
||||
<path d='M629,59 C635,54 641,52 649,52 C657,52 664,54 670,59 C675,63 679,70 679,78 L619,78 C620,70 623,64 629,59 Z M696,92 C696,89 696,86 696,85 C696,75 694,67 690,60 C686,53 680,47 673,43 C666,39 658,37 649,37 C640,37 632,40 626,44 C619,48 611,54 607,62 C603,69 601,78 601,89 C601,99 603,108 607,116 C611,123 617,129 625,134 C633,138 641,140 651,140 C662,140 672,137 680,130 C688,124 693,115 695,105 L679,105 C677,111 673,116 668,120 C663,124 657,125 650,125 C641,125 634,122 628,117 C622,111 619,103 619,93 L619,92 L696,92 Z' />
|
||||
<path d='M767,39 L767,54 L759,54 C750,54 743,57 739,64 C735,70 733,78 733,88 L733,139 L717,139 L717,39 L731,39 L733,54 C736,49 739,45 744,43 C748,40 754,39 763,39 L767,39' />
|
||||
<path d='M431,53 L413,53 L413,39 L431,39 L431,11 L447,11 L447,39 L472,39 L472,53 L447,53 L447,112 C447,116 448,119 449,121 C451,123 454,124 458,124 L475,124 L475,139 L457,139 C448,139 441,137 437,133 C433,129 431,123 431,112 L431,53' />
|
||||
<path d='M337,59 C343,54 349,52 357,52 C365,52 372,54 378,59 C383,63 386,70 387,78 L327,78 C328,70 331,64 337,59 Z M404,92 C404,89 404,86 404,85 C404,75 402,67 398,60 C394,53 388,47 381,43 C374,39 366,37 357,37 C348,37 340,40 333,44 C326,48 319,54 315,62 C311,69 309,78 309,89 C309,99 311,108 315,116 C319,123 325,129 333,134 C341,138 349,140 359,140 C370,140 380,137 388,130 C396,124 401,115 403,105 L387,105 C385,111 382,116 377,120 C372,124 366,125 359,125 C350,125 343,122 337,117 C332,111 329,103 329,93 L329,92 L404,92 Z' />
|
||||
<path d='M269,108 C266,113 262,118 257,121 C252,124 246,125 240,125 C230,125 222,122 216,115 C210,108 207,99 207,89 C207,78 210,69 216,62 C222,55 230,52 240,52 C246,52 252,54 257,57 C262,60 266,64 269,70 C272,75 273,82 273,89 C273,96 272,102 269,108 Z M275,39 L273,56 C270,50 265,45 259,42 C253,39 246,38 238,38 C229,38 221,40 214,44 C207,48 201,54 197,62 C193,69 191,78 191,89 C191,99 193,108 197,116 C201,123 207,129 214,134 C222,138 230,140 240,140 C254,140 265,134 273,122 L273,133 C273,154 262,165 240,165 C232,165 225,164 220,160 C215,157 211,152 210,146 L193,146 C195,157 199,165 207,171 C216,177 226,179 239,179 C272,179 289,164 289,134 L289,39 L275,39' />
|
||||
<g transform='translate(0, 11)'>
|
||||
<path d='M153,97 C150,103 146,107 141,110 C136,113 130,115 124,115 C118,115 112,113 107,110 C102,107 98,103 95,97 C92,91 91,85 91,78 C91,71 92,65 95,59 C98,53 102,49 107,46 C112,43 118,41 124,41 C130,41 136,43 141,46 C146,49 150,53 153,59 C156,65 157,71 157,78 C157,85 156,91 153,97 Z M167,51 C163,43 157,37 150,33 C142,29 134,27 124,27 C115,27 106,29 99,33 C91,37 85,43 81,51 C77,59 75,68 75,78 C75,88 77,97 81,105 C85,112 91,118 99,123 C106,127 115,129 124,129 C134,129 142,127 150,123 C157,118 163,112 167,105 C171,97 173,88 173,78 C173,68 171,59 167,51' />
|
||||
<path d='M18,43 L-0,43 L-0,28 L18,28 L18,0 L34,0 L34,28 L59,28 L59,43 L34,43 L34,102 C34,106 35,109 36,111 C38,113 41,114 45,114 L62,114 L62,129 L44,129 C35,129 28,127 24,123 C20,119 18,113 18,102 L18,43' />
|
||||
</g>
|
||||
<path d='M911 98 C911 106 908 113 903 118 C897 123 889 126 880 126 C873 126 868 125 864 122 C860 119 858 115 858 110 C858 98 865 93 880 93 L911 93 L911 98 Z M934 124 C929 124 927 121 927 117 L927 74 C927 62 924 53 917 47 C910 41 900 37 887 37 C875 37 865 40 858 46 C850 51 846 59 845 69 L861 69 C862 64 865 59 869 56 C874 53 879 52 886 52 C894 52 900 54 905 57 C909 61 911 66 911 73 L911 79 L882 79 C869 79 859 82 852 87 C845 93 842 100 842 111 C842 120 845 127 852 132 C859 137 868 140 878 140 C893 140 904 135 912 123 C912 128 914 132 916 135 C919 137 924 139 930 139 L939 139 L939 124 L934 124 Z' />
|
||||
<path d='M796 138 C804 138 811 131 811 123 C811 114 804 108 796 108 C787 108 780 114 780 123 C780 131 787 138 796 138' />
|
||||
return <SvgIcon viewBox='-0.4 -0.4 27 27' width='24' height='24' stroke='none' fill='currentColor' {...props}>
|
||||
<path d='M26.1 3.4C24.2 0.1 20.1-1 16.9 0.9C14.8 2.1 13.6 4.2 13.5 6.4L20.3 6.4V7H13.5C13.5 8.1 13.8 9.1 14.4 10.1C16.3 13.3 20.4 14.4 23.6 12.6C26.9 10.7 28 6.6 26.1 3.4Z' />
|
||||
<path d='M0.9 3.4C-1 6.6 0.1 10.7 3.4 12.6C5.5 13.8 7.9 13.7 9.9 12.7L6.5 6.9L7 6.6L10.4 12.4C11.3 11.8 12 11.1 12.6 10.1C14.5 6.9 13.4 2.8 10.1 0.9C6.9-1 2.8 0.1 0.9 3.4Z' />
|
||||
<path d='M13.5 25.1C17.2 25.1 20.2 22.1 20.2 18.4C20.2 16 19 13.9 17.1 12.7L13.7 18.5L13.2 18.2L16.6 12.4C15.7 11.9 14.6 11.7 13.5 11.7C9.8 11.7 6.7 14.7 6.7 18.4C6.7 22.1 9.8 25.1 13.5 25.1Z' />
|
||||
</SvgIcon>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,13 +18,17 @@ let cachedCapability: CapabilityBrowserSpeechRecognition | null = null;
|
||||
|
||||
export const browserSpeechRecognitionCapability = (): CapabilityBrowserSpeechRecognition => {
|
||||
if (!cachedCapability) {
|
||||
const isApiAvailable = !!getSpeechRecognitionClass();
|
||||
const isBraveBlocked = Is.Browser.Brave; // Brave exposes the API but silently blocks results
|
||||
const isApiAvailable = !!getSpeechRecognitionClass() && !isBraveBlocked;
|
||||
const isDeviceNotSupported = false;
|
||||
cachedCapability = {
|
||||
mayWork: isApiAvailable && !isDeviceNotSupported,
|
||||
isApiAvailable,
|
||||
isDeviceNotSupported,
|
||||
warnings: Is.OS.iOS ? ['Not tested on this browser/device.'] : [],
|
||||
warnings: [
|
||||
...Is.OS.iOS ? ['Not tested on this browser/device.'] : [],
|
||||
...isBraveBlocked ? ['Speech recognition is not supported in Brave. Please use Chrome, Edge, or Safari.'] : [],
|
||||
],
|
||||
};
|
||||
}
|
||||
return cachedCapability;
|
||||
|
||||
@@ -24,6 +24,8 @@ export type GlobalOverlayId = // string - disabled so we keep an orderliness
|
||||
| 'chat-reset-confirmation'
|
||||
| 'chat-message-delete-confirmation'
|
||||
| 'chat-message-delete-aux'
|
||||
| 'chat-message-delete-hosted-resource'
|
||||
| 'chat-message-auto-embed-notice'
|
||||
| 'chat-message-inline-aux'
|
||||
| 'livefile-overwrite'
|
||||
| 'shortcuts-confirm-close'
|
||||
|
||||
@@ -26,7 +26,10 @@ export function setupClientUncaughtErrorsLogging(): () => void {
|
||||
|
||||
// Handle unhandled promise rejections
|
||||
const handleRejection = (event: PromiseRejectionEvent) => {
|
||||
logger.error('Unhandled promise rejection', {
|
||||
// skip if already handled by a component-level listener
|
||||
if (event.defaultPrevented) return;
|
||||
|
||||
logger.error('Unhandled promise rejection', {
|
||||
reason: event.reason,
|
||||
message: event.reason?.message,
|
||||
stack: event.reason?.stack,
|
||||
|
||||
@@ -35,6 +35,7 @@ export type DMessageContentFragment = _DMessageFragmentWrapper<'content',
|
||||
| DMessageImageRefPart // large image
|
||||
| DMessageToolInvocationPart // shown to dev only, signature of the llm function call
|
||||
| DMessageToolResponsePart // shown to dev only, response of the llm
|
||||
| DMessageHostedResourcePart // provider-hosted resource (e.g. Anthropic file from Skills) with download affordance
|
||||
| DMessageErrorPart // red message, e.g. non-content application issues
|
||||
| _SentinelPart
|
||||
>;
|
||||
@@ -101,7 +102,17 @@ export type DMessageFragmentVendorState = Record<string, unknown> & {
|
||||
gemini?: {
|
||||
thoughtSignature?: string; // Gemini 3+ - echoed back to maintain reasoning context
|
||||
};
|
||||
// Future: openai?: { ... }, anthropic?: { ... }
|
||||
openai?: {
|
||||
// Responses API reasoning item continuity handle.
|
||||
// IMPORTANT: OpenAI-private encryption + server-side item id; never round-trip to xAI.
|
||||
reasoningItem?: { id?: string; encryptedContent?: string; };
|
||||
};
|
||||
xai?: {
|
||||
// xAI Responses API reasoning item continuity handle.
|
||||
// IMPORTANT: xAI-private encryption + server-side item id; never round-trip to OpenAI.
|
||||
reasoningItem?: { id?: string; encryptedContent?: string; };
|
||||
};
|
||||
// Future: anthropic?: { ... }
|
||||
}
|
||||
|
||||
|
||||
@@ -199,7 +210,7 @@ export type DMessageToolInvocationPart = {
|
||||
id: string,
|
||||
invocation: {
|
||||
type: 'function_call'
|
||||
name: string; // Name of the function as passed from the definition
|
||||
name: string; // REQUIRED. Name of the function as passed from the definition
|
||||
args: string /*| null*/; // JSON-encoded object (only objects are supported), if null there are no args and it's just a plain invocation
|
||||
// temporary, not stored
|
||||
_description?: string; // Description from the definition
|
||||
@@ -231,6 +242,15 @@ export type DMessageToolResponsePart = {
|
||||
type DMessageToolEnvironment = 'upstream' | 'server' | 'client';
|
||||
type DMessageToolCodeExecutor = 'gemini_auto_inline' | 'code_interpreter';
|
||||
|
||||
|
||||
/** Hosted resource - a provider-hosted resource (e.g. Anthropic container file from Skills/code execution). */
|
||||
export type DMessageHostedResourcePart = {
|
||||
pt: 'hosted_resource';
|
||||
resource:
|
||||
| { via: 'anthropic', fileId: string, containerId?: string };
|
||||
};
|
||||
|
||||
|
||||
type DVoidModelAnnotationsPart = {
|
||||
pt: 'annotations',
|
||||
annotations: readonly DVoidWebCitation[],
|
||||
@@ -255,17 +275,34 @@ export type DVoidModelAuxPart = {
|
||||
redactedData?: readonly string[],
|
||||
};
|
||||
|
||||
|
||||
export type DVoidPlaceholderPart = {
|
||||
pt: 'ph',
|
||||
pText: string,
|
||||
pType?: 'chat-gen-follow-up', // a follow-up is being generated
|
||||
modelOp?: DVoidPlaceholderModelOp,
|
||||
aixControl?: DVoidPlaceholderAixControlRetry,
|
||||
|
||||
// render type
|
||||
pType?:
|
||||
| 'chat-gen-follow-up', // a follow-up is being generated
|
||||
|
||||
// operation history for stacked progress UI
|
||||
opLog?: readonly DVoidPlaceholderMOp[],
|
||||
|
||||
// NOTE: the following should be extracted as its own part over time
|
||||
aixControl?:
|
||||
| { ctl: 'ac-info', ait: 'flow-cont' }
|
||||
| DVoidPlaceholderAixControlRetry,
|
||||
};
|
||||
|
||||
export type DVoidPlaceholderModelOp = {
|
||||
mot: 'search-web' | 'gen-image' | 'code-exec' | 'flow-cont',
|
||||
cts: number, // client-based timestamp
|
||||
export type DVoidPlaceholderMOp = {
|
||||
readonly opId: string, // upstream operation ID (srvtoolu_*, item_id, etc.)
|
||||
readonly mot: 'search-web' | 'gen-image' | 'code-exec',
|
||||
text: string, // latest status text
|
||||
state: 'active' | 'done' | 'error', // lifecycle state
|
||||
iTexts?: readonly string[], // decorative input context (e.g., search queries, code snippets, gen image prompt)
|
||||
oTexts?: readonly string[], // decorative output context (e.g., result urls, code exec outputs, file IDs, error details)
|
||||
readonly parentOpId?: string, // parent operation ID for nesting (e.g., code_execution that triggered this web_search)
|
||||
readonly level: number, // nesting depth (0 = root, inferred from parentOpId)
|
||||
readonly cts: number, // client timestamp (first seen)
|
||||
};
|
||||
|
||||
type DVoidPlaceholderAixControlRetry = {
|
||||
@@ -277,6 +314,7 @@ type DVoidPlaceholderAixControlRetry = {
|
||||
rCauseConn?: string, // connection error type if available (e.g., 'net-disconnected', 'timeout')
|
||||
};
|
||||
|
||||
|
||||
type _SentinelPart = { pt: '_pt_sentinel' };
|
||||
|
||||
|
||||
@@ -306,6 +344,10 @@ export function isTextContentFragment(fragment: DMessageFragment): fragment is D
|
||||
return fragment.ft === 'content' && fragment.part.pt === 'text';
|
||||
}
|
||||
|
||||
export function isErrorContentFragment(fragment: DMessageFragment): fragment is DMessageContentFragment & { part: DMessageErrorPart } {
|
||||
return fragment.ft === 'content' && fragment.part.pt === 'error';
|
||||
}
|
||||
|
||||
export function isAttachmentFragment(fragment: DMessageFragment): fragment is DMessageAttachmentFragment {
|
||||
return fragment.ft === 'attachment' && !!fragment.part?.pt;
|
||||
}
|
||||
@@ -372,6 +414,10 @@ export function isToolResponseFunctionCallPart(part: DMessageContentFragment['pa
|
||||
return part.pt === 'tool_response' && part.response.type === 'function_call';
|
||||
}
|
||||
|
||||
export function isHostedResourcePart(part: DMessageContentFragment['part']): part is DMessageHostedResourcePart {
|
||||
return part.pt === 'hosted_resource';
|
||||
}
|
||||
|
||||
export function isAnnotationsPart(part: DMessageVoidFragment['part']) {
|
||||
return part.pt === 'annotations';
|
||||
}
|
||||
@@ -415,6 +461,10 @@ export function create_CodeExecutionResponse_ContentFragment(id: string, error:
|
||||
return _createContentFragment(_create_CodeExecutionResponse_Part(id, error, result, executor, environment));
|
||||
}
|
||||
|
||||
export function createHostedResourceContentFragment(resource: DMessageHostedResourcePart['resource']): DMessageContentFragment {
|
||||
return _createContentFragment({ pt: 'hosted_resource', resource });
|
||||
}
|
||||
|
||||
function _createContentFragment(part: DMessageContentFragment['part']): DMessageContentFragment {
|
||||
return { ft: 'content', fId: agiId('chat-dfragment' /* -content */), part };
|
||||
}
|
||||
@@ -456,8 +506,8 @@ export function createModelAuxVoidFragment(aType: DVoidModelAuxPart['aType'], aT
|
||||
return _createVoidFragment(_create_ModelAux_Part(aType, aText, textSignature, redactedData));
|
||||
}
|
||||
|
||||
export function createPlaceholderVoidFragment(placeholderText: string, placeholderType?: DVoidPlaceholderPart['pType'], modelOp?: DVoidPlaceholderModelOp, aixControl?: DVoidPlaceholderPart['aixControl']): DMessageVoidFragment {
|
||||
return _createVoidFragment(_create_Placeholder_Part(placeholderText, placeholderType, modelOp, aixControl));
|
||||
export function createPlaceholderVoidFragment(placeholderText: string, placeholderType?: DVoidPlaceholderPart['pType'], aixControl?: DVoidPlaceholderPart['aixControl'], opLog?: readonly DVoidPlaceholderMOp[]): DMessageVoidFragment {
|
||||
return _createVoidFragment(_create_Placeholder_Part(placeholderText, placeholderType, aixControl, opLog));
|
||||
}
|
||||
|
||||
function _createVoidFragment(part: DMessageVoidFragment['part']): DMessageVoidFragment {
|
||||
@@ -588,8 +638,8 @@ function _create_ModelAux_Part(aType: DVoidModelAuxPart['aType'], aText: string,
|
||||
};
|
||||
}
|
||||
|
||||
function _create_Placeholder_Part(placeholderText: string, pType?: DVoidPlaceholderPart['pType'], modelOp?: DVoidPlaceholderModelOp, aixControl?: DVoidPlaceholderPart['aixControl']): DVoidPlaceholderPart {
|
||||
return { pt: 'ph', pText: placeholderText, ...(pType ? { pType } : undefined), ...(modelOp ? { modelOp: { ...modelOp } } : undefined), ...(aixControl ? { aixControl: { ...aixControl } } : undefined) };
|
||||
function _create_Placeholder_Part(placeholderText: string, pType?: DVoidPlaceholderPart['pType'], aixControl?: DVoidPlaceholderPart['aixControl'], opLog?: readonly DVoidPlaceholderMOp[]): DVoidPlaceholderPart {
|
||||
return { pt: 'ph', pText: placeholderText, ...(pType ? { pType } : undefined), ...(opLog ? { opLog: opLog.map(e => ({ ...e })) } : undefined), ...(aixControl ? { aixControl: { ...aixControl } } : undefined) };
|
||||
}
|
||||
|
||||
function _create_Sentinel_Part(): _SentinelPart {
|
||||
@@ -644,7 +694,7 @@ function _duplicate_Part<TPart extends (DMessageContentFragment | DMessageAttach
|
||||
return _create_ModelAux_Part(part.aType, part.aText, part.textSignature, part.redactedData) as TPart;
|
||||
|
||||
case 'ph':
|
||||
return _create_Placeholder_Part(part.pText, part.pType, part.modelOp, part.aixControl) as TPart;
|
||||
return _create_Placeholder_Part(part.pText, part.pType, part.aixControl, part.opLog) as TPart;
|
||||
|
||||
case 'text':
|
||||
return _create_Text_Part(part.text) as TPart;
|
||||
@@ -659,6 +709,9 @@ function _duplicate_Part<TPart extends (DMessageContentFragment | DMessageAttach
|
||||
? _create_FunctionCallResponse_Part(part.id, part.error, part.response.name, part.response.result, part.environment) as TPart
|
||||
: _create_CodeExecutionResponse_Part(part.id, part.error, part.response.result, part.response.executor, part.environment) as TPart;
|
||||
|
||||
case 'hosted_resource':
|
||||
return { pt: 'hosted_resource', resource: { ...part.resource } } as TPart;
|
||||
|
||||
case '_pt_sentinel':
|
||||
return _create_Sentinel_Part() as TPart;
|
||||
|
||||
@@ -855,6 +908,7 @@ export function updateFragmentWithEditedText(
|
||||
break;
|
||||
|
||||
case 'image_ref':
|
||||
case 'hosted_resource':
|
||||
case '_pt_sentinel':
|
||||
// nothing to do here - not editable
|
||||
break;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { agiUuid } from '~/common/util/idUtils';
|
||||
|
||||
import { createPlaceholderVoidFragment, createTextContentFragment, DMessageFragment, duplicateDMessageFragments, isAttachmentFragment, isContentFragment, isVoidFragment } from './chat.fragments';
|
||||
import { createPlaceholderVoidFragment, createTextContentFragment, DMessageFragment, duplicateDMessageFragments } from './chat.fragments';
|
||||
|
||||
import type { ModelVendorId } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import type { DLLMId } from '~/common/stores/llms/llms.types';
|
||||
import type { DMetricsChatGenerate_Md } from '~/common/stores/metrics/metrics.chatgenerate';
|
||||
import type { Immutable } from '~/common/types/immutable.types';
|
||||
|
||||
|
||||
// Message
|
||||
@@ -26,21 +27,9 @@ export interface DMessage {
|
||||
|
||||
generator?: DMessageGenerator; // Assistant generator info, and metrics
|
||||
|
||||
/**
|
||||
* Session metadata for multi-turn agentic sessions.
|
||||
*
|
||||
* Enables stateful time-monotonic multi-turn interactions in a stateless architecture:
|
||||
* - Parsers accumulate session values (container IDs, response handles, etc.)
|
||||
* - Request builders traverse history for latest non-expired values
|
||||
* - Child messages inherit parent session, new values override
|
||||
*
|
||||
* Pattern:
|
||||
* 1. Parser extracts vendor session data → stores in sessionMetadata
|
||||
* 2. Request builder finds latest value per key → includes in next request
|
||||
* 3. Vendor reuses session (e.g., Anthropic container for file access, OpenAI response for reconnection)
|
||||
*
|
||||
* Keys namespaced by vendor: 'anthropic.container.id', 'openai.response.id'
|
||||
*/
|
||||
// Session metadata for multi-turn agentic sessions was considered here (see commit 3cd38f47)
|
||||
// but vendor session state (container IDs, response handles) is stored on DMessageGenerator
|
||||
// fields instead (upstreamContainer, upstreamHandle) - simpler plumbing, no persistence migration.
|
||||
// sessionMetadata?: DMessageSessionMetadata;
|
||||
|
||||
userFlags?: DMessageUserFlag[]; // (UI) user-set per-message flags
|
||||
@@ -59,13 +48,7 @@ export type DMessageId = string;
|
||||
|
||||
export type DMessageRole = 'user' | 'assistant' | 'system';
|
||||
|
||||
/**
|
||||
* Session metadata carrying vendor-specific state across multi-turn agentic sessions.
|
||||
* Namespaced keys (e.g., 'anthropic.container.id'), child inherits parent, new values override.
|
||||
*
|
||||
* NOTE: may use some typescript module augmentation to plug new keys and value types here.
|
||||
* NOTE2: may add references to the parent sessions/unique Ids, although they may be the message itself
|
||||
*/
|
||||
// Superseded by DMessageGenerator.upstreamContainer / .upstreamHandle - see note above.
|
||||
// export type DMessageSessionMetadata = Record<string, string | number | boolean | null>;
|
||||
|
||||
|
||||
@@ -142,11 +125,17 @@ export type DMessageGenerator = ({
|
||||
}) & {
|
||||
metrics?: DMetricsChatGenerate_Md; // medium-sized metrics stored in the message
|
||||
providerInfraLabel?: string; // upstream provider that served the request (e.g., OpenRouter provider routing)
|
||||
upstreamHandle?: {
|
||||
uht: 'vnd.oai.responses',
|
||||
responseId: string,
|
||||
expiresAt: number | null, // null = never expires
|
||||
upstreamContainer?: {
|
||||
uct: 'vnd.ant.container',
|
||||
containerId: string,
|
||||
expiresAt: string, // ISO 8601 UTC timestamp (e.g., "2026-04-07T05:59:32Z")
|
||||
},
|
||||
upstreamHandle?:
|
||||
// unified `runId` across variants - vendor-specific id lives behind it; `uht` is consulted only for dispatch routing
|
||||
// createdAt/expiresAt: server-clock (ms) at the FIRST observation for this runId - preserved across reattaches
|
||||
// (reassembler ignores re-emissions for the same runId so retention is measured from creation, not last reattach)
|
||||
| { uht: 'vnd.oai.responses', runId: string /* OpenAI `response.id` */, createdAt: number | null, expiresAt: number | null /* null = never expires */ }
|
||||
| { uht: 'vnd.gem.interactions', runId: string /* Gemini `interaction.id` */, createdAt: number | null, expiresAt: number | null },
|
||||
tokenStopReason?:
|
||||
| 'client-abort' // if the generator stopped due to a client abort signal
|
||||
| 'filter' // (inline filter message injected) if the generator stopped due to a filter
|
||||
@@ -206,7 +195,9 @@ export function duplicateDMessage(message: Readonly<DMessage>, skipVoid: boolean
|
||||
role: message.role,
|
||||
fragments: duplicateDMessageFragments(message.fragments, skipVoid), // [*] full message duplication (see downstream)
|
||||
|
||||
...(message.pendingIncomplete ? { pendingIncomplete: true } : {}),
|
||||
// 2026-03-27: Not porting pendingIncomplete anymore - if a message is duplicated, we consider it detached
|
||||
// IMPORTANT: a duplicate message is never 'being generated' (anymore) - we may as well finalize it
|
||||
// ...(message.pendingIncomplete ? { pendingIncomplete: true } : {}),
|
||||
|
||||
purposeId: message.purposeId,
|
||||
|
||||
@@ -246,6 +237,7 @@ export function duplicateDMessageGenerator(generator: Readonly<DMessageGenerator
|
||||
// ...(generator.xeOpCode ? { xeOpCode: generator.xeOpCode } : {}),
|
||||
...(generator.metrics ? { metrics: { ...generator.metrics } } : {}),
|
||||
...(generator.providerInfraLabel ? { providerInfraLabel: generator.providerInfraLabel } : {}),
|
||||
...(generator.upstreamContainer ? { upstreamContainer: { ...generator.upstreamContainer } } : {}),
|
||||
...(generator.upstreamHandle ? { upstreamHandle: { ...generator.upstreamHandle } } : {}),
|
||||
...(generator.tokenStopReason ? { tokenStopReason: generator.tokenStopReason } : {}),
|
||||
};
|
||||
@@ -256,6 +248,7 @@ export function duplicateDMessageGenerator(generator: Readonly<DMessageGenerator
|
||||
aix: { ...generator.aix },
|
||||
...(generator.metrics ? { metrics: { ...generator.metrics } } : {}),
|
||||
...(generator.providerInfraLabel ? { providerInfraLabel: generator.providerInfraLabel } : {}),
|
||||
...(generator.upstreamContainer ? { upstreamContainer: { ...generator.upstreamContainer } } : {}),
|
||||
...(generator.upstreamHandle ? { upstreamHandle: { ...generator.upstreamHandle } } : {}),
|
||||
...(generator.tokenStopReason ? { tokenStopReason: generator.tokenStopReason } : {}),
|
||||
};
|
||||
@@ -277,44 +270,18 @@ export function messageWasInterruptedAtStart(message: Pick<DMessage, 'generator'
|
||||
|
||||
// helpers - generators
|
||||
|
||||
export function messageSetGenerator(message: Pick<DMessage, 'generator'>, generator: undefined | DMessageGenerator): void {
|
||||
if (generator !== undefined)
|
||||
message.generator = generator;
|
||||
else
|
||||
delete message.generator;
|
||||
}
|
||||
|
||||
export function messageSetGeneratorNamed(message: Pick<DMessage, 'generator'>, label: 'web' | 'issue' | 'help' | string): void {
|
||||
message.generator = {
|
||||
mgt: 'named',
|
||||
name: label,
|
||||
};
|
||||
}
|
||||
|
||||
function _messageSetGeneratorAIX(message: Pick<DMessage, 'generator'>, modelLabel: string, modelVendorId: ModelVendorId, modelId: DLLMId): void {
|
||||
message.generator = {
|
||||
mgt: 'aix',
|
||||
name: modelLabel,
|
||||
aix: {
|
||||
vId: modelVendorId,
|
||||
mId: modelId,
|
||||
},
|
||||
};
|
||||
message.generator = { mgt: 'named', name: label };
|
||||
}
|
||||
|
||||
export function messageSetGeneratorAIX_AutoLabel(message: Pick<DMessage, 'generator'>, modelVendorId: ModelVendorId, modelId: DLLMId): void {
|
||||
|
||||
// Strip the serviceId prefix: 'vendor-' or 'vendor-N-' (when multiple providers of same vendor)
|
||||
const heuristicLabel = modelId.includes('-') ? modelId.replace(/^[^-]+-(\d-)?/, '') : modelId;
|
||||
|
||||
_messageSetGeneratorAIX(message, heuristicLabel, modelVendorId, modelId);
|
||||
message.generator = createGeneratorAIX_AutoLabel(modelVendorId, modelId);
|
||||
}
|
||||
|
||||
/*export function messageUpdateGeneratorInfo(message: Pick<DMessage, 'generator'>, metrics?: DMetricsChatGenerate_Md, tokenStopReason?: DMessageGenerator['tokenStopReason']): void {
|
||||
if (!message.generator) return;
|
||||
if (metrics) message.generator.metrics = metrics;
|
||||
if (tokenStopReason) message.generator.tokenStopReason = tokenStopReason;
|
||||
}*/
|
||||
export function createGeneratorAIX_AutoLabel(modelVendorId: ModelVendorId, modelId: DLLMId): DMessageGenerator {
|
||||
const heuristicLabel = modelId.includes('-') ? modelId.replace(/^[^-]+-(\d-)?/, '') : modelId;
|
||||
return { mgt: 'aix', name: heuristicLabel, aix: { vId: modelVendorId, mId: modelId } };
|
||||
}
|
||||
|
||||
|
||||
// helpers - user flags
|
||||
@@ -350,60 +317,77 @@ export function messageSetUserFlag(message: Pick<DMessage, 'userFlags'>, flag: D
|
||||
|
||||
// helpers during the transition from V3
|
||||
|
||||
export function messageFragmentsReduceText(fragments: DMessageFragment[], fragmentSeparator: string = '\n\n', excludeAttachmentFragments?: boolean): string {
|
||||
export function messageFragmentsReduceText(fragments: Immutable<DMessageFragment[]>, fragmentSeparator: string = '\n\n', excludeAttachmentFragments?: boolean): string {
|
||||
|
||||
// This function is used frequently - so this is the optimized version with low allocations
|
||||
|
||||
// quick path for empty fragments
|
||||
if (!fragments?.length)
|
||||
return '';
|
||||
|
||||
return fragments
|
||||
.map(fragment => {
|
||||
switch (true) {
|
||||
case isContentFragment(fragment):
|
||||
const cPt = fragment.part.pt;
|
||||
switch (cPt) {
|
||||
case 'text':
|
||||
return fragment.part.text;
|
||||
case 'error':
|
||||
return fragment.part.error;
|
||||
case 'reference':
|
||||
case 'image_ref':
|
||||
return '';
|
||||
case 'tool_invocation':
|
||||
case 'tool_response':
|
||||
// Ignore tools for the text reduction
|
||||
return '';
|
||||
case '_pt_sentinel':
|
||||
return '';
|
||||
default:
|
||||
const _exhaustiveCheck: never = cPt;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case isAttachmentFragment(fragment):
|
||||
if (excludeAttachmentFragments)
|
||||
return '';
|
||||
// fast path: single text content fragment (most common case)
|
||||
if (fragments.length === 1 && fragments[0].ft === 'content' && fragments[0].part.pt === 'text')
|
||||
return fragments[0].part.text;
|
||||
|
||||
// single-pass accumulation (avoids intermediate arrays from .map/.filter/.join)
|
||||
let result = '';
|
||||
for (const fragment of fragments) {
|
||||
let text: string | undefined;
|
||||
|
||||
switch (fragment.ft) {
|
||||
case 'content': {
|
||||
const cPt = fragment.part.pt;
|
||||
switch (cPt) {
|
||||
case 'text':
|
||||
text = fragment.part.text;
|
||||
break;
|
||||
case 'error':
|
||||
text = fragment.part.error;
|
||||
break;
|
||||
case 'reference':
|
||||
case 'image_ref':
|
||||
case 'tool_invocation':
|
||||
case 'tool_response':
|
||||
case 'hosted_resource':
|
||||
case '_pt_sentinel':
|
||||
break;
|
||||
default:
|
||||
const _exhaustiveCheck: never = cPt;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'attachment':
|
||||
if (!excludeAttachmentFragments) {
|
||||
const aPt = fragment.part.pt;
|
||||
switch (aPt) {
|
||||
case 'doc':
|
||||
return fragment.part.data.text;
|
||||
text = fragment.part.data.text;
|
||||
break;
|
||||
case 'reference':
|
||||
case 'image_ref':
|
||||
return '';
|
||||
case '_pt_sentinel':
|
||||
return '';
|
||||
break;
|
||||
default:
|
||||
const _exhaustiveCheck: never = aPt;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case isVoidFragment(fragment):
|
||||
// all void fragments are ignored by definition when doing a text reduction
|
||||
return '';
|
||||
}
|
||||
console.warn(`[DEV] messageFragmentsReduceText: unexpected '${fragment.ft}' fragment with '${(fragment as any)?.part?.pt}' part`);
|
||||
return '';
|
||||
})
|
||||
.filter(text => !!text)
|
||||
.join(fragmentSeparator);
|
||||
}
|
||||
break;
|
||||
case 'void': // all void fragments (including reasoning) are ignored by definition when doing a text reduction
|
||||
case '_ft_sentinel':
|
||||
break;
|
||||
default:
|
||||
const _exhaustiveCheck: never = fragment;
|
||||
console.warn(`[DEV] messageFragmentsReduceText: unexpected '${(fragment as any)?.ft}' fragment with '${(fragment as any)?.part?.pt}' part`);
|
||||
break;
|
||||
}
|
||||
|
||||
if (text) {
|
||||
if (result) result += fragmentSeparator;
|
||||
result += text;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ function _fragmentTokens(llm: DLLM, role: DMessageRole, fragment: DMessageFragme
|
||||
return estimateTextTokens(cPart.text, llm, debugFrom);
|
||||
case 'tool_invocation':
|
||||
case 'tool_response':
|
||||
case 'hosted_resource':
|
||||
break; // warn
|
||||
default:
|
||||
const _exhaustiveCheck: never = cPt;
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { DModelsService } from '~/common/stores/llms/llms.service.types';
|
||||
|
||||
import { createDConversation, DConversation, type DConversationId } from './chat.conversation';
|
||||
import { createDMessageTextContent, DMessage, MESSAGE_FLAG_NOTIFY_COMPLETE, messageSetUserFlag } from './chat.message';
|
||||
import { createDMessageZyncAssetReferencePart, createErrorContentFragment, isAttachmentFragment, isContentOrAttachmentFragment, isDocPart, isImageRefPart, isPlaceholderPart, isTextContentFragment, isVoidFragment } from './chat.fragments';
|
||||
import { createDMessageZyncAssetReferencePart, createErrorContentFragment, isAttachmentFragment, isContentOrAttachmentFragment, isDocPart, isImageRefPart, isTextContentFragment, isVoidPlaceholderFragment } from './chat.fragments';
|
||||
|
||||
|
||||
// configuration
|
||||
@@ -64,9 +64,14 @@ export namespace V4ToHeadConverters {
|
||||
if (!validLiveFileIDs.includes(fragment.liveFileId))
|
||||
delete fragment.liveFileId;
|
||||
|
||||
// show the aborted ops: convert a Placeholder fragment [part.pt='ph'] to an Error fragment
|
||||
if (isVoidFragment(fragment) && isPlaceholderPart(fragment.part))
|
||||
m.fragments[i] = createErrorContentFragment(`${fragment.part.pText || '(did not complete)'}`);
|
||||
// [Show Terminated] convert a Placeholder fragment [part.pt='ph'] to an Error fragment
|
||||
const isLastFragment = (i === m.fragments.length - 1);
|
||||
if (isLastFragment && isVoidPlaceholderFragment(fragment)) // [PH-LIFECYCLE]
|
||||
m.fragments[i] = createErrorContentFragment(
|
||||
'opLog' in fragment.part ? '(message incomplete)'
|
||||
: fragment.part.pText ? `(did not complete: ${fragment.part.pText})`
|
||||
: '(did not complete)'
|
||||
);
|
||||
|
||||
// [ASSET] [MIGRATION] Convert DBlob image references to Asset references - converts legacy image_ref parts with dblob references to the new reference system
|
||||
if (isContentOrAttachmentFragment(fragment) && isImageRefPart(fragment.part) && fragment.part.dataRef?.reftype === 'dblob') {
|
||||
|
||||
@@ -63,14 +63,13 @@ export interface ChatActions {
|
||||
|
||||
type ConversationsStore = ChatState & ChatActions;
|
||||
|
||||
const defaultConversations: DConversation[] = [createDConversation()];
|
||||
|
||||
export const useChatStore = create<ConversationsStore>()(/*devtools(*/
|
||||
persist(
|
||||
(_set, _get) => ({
|
||||
|
||||
// default state
|
||||
conversations: defaultConversations,
|
||||
conversations: [], // we used to have a default conversation here for zero-state, but we moved it to the merge function
|
||||
|
||||
prependNewConversation: (personaId: SystemPurposeId | undefined, isIncognito: boolean): DConversationId => {
|
||||
const newConversation = createDConversation(personaId);
|
||||
@@ -484,14 +483,18 @@ export const useChatStore = create<ConversationsStore>()(/*devtools(*/
|
||||
*/
|
||||
merge: (persistedState, currentState: ConversationsStore): ConversationsStore => {
|
||||
|
||||
// concatenate-merge conversations reloaded from storage
|
||||
// insert all stored conversations [ current (if any), ...stored ]
|
||||
const mergedConversations = [...(currentState?.conversations || [])];
|
||||
if (persistedState && typeof persistedState === 'object' && 'conversations' in persistedState) {
|
||||
const storedConversations = persistedState.conversations as ChatState['conversations'];
|
||||
if (storedConversations.length)
|
||||
if (storedConversations?.length)
|
||||
mergedConversations.push(...storedConversations);
|
||||
}
|
||||
|
||||
// zero-state: prepend an empty conversations if there are none
|
||||
if (!mergedConversations.length)
|
||||
mergedConversations.unshift(createDConversation(undefined));
|
||||
|
||||
return {
|
||||
// default shallow merge
|
||||
...currentState,
|
||||
|
||||
@@ -42,17 +42,44 @@ export interface LLMServiceGroup {
|
||||
}
|
||||
|
||||
/**
|
||||
* Group LLMs by service, resolving service display labels.
|
||||
* Resolve display label for each unique service in the input.
|
||||
* Fallback chain: service.label -> vendor.name -> service.id.
|
||||
*/
|
||||
function _resolveServiceLabels(llms: ReadonlyArray<DLLM>): Map<DModelsServiceId, string> {
|
||||
const labelById = new Map<DModelsServiceId, string>();
|
||||
for (const llm of llms) {
|
||||
if (labelById.has(llm.sId)) continue;
|
||||
const vendor = findModelVendor(llm.vId);
|
||||
labelById.set(llm.sId, findModelsServiceOrNull(llm.sId)?.label || vendor?.name || llm.sId);
|
||||
}
|
||||
return labelById;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stably sort LLMs by their service label (alphabetical, locale-aware).
|
||||
* Preserves intra-service order (e.g. starred-first), since JS sort is stable.
|
||||
*/
|
||||
export function sortLLMsByServiceLabel<T extends DLLM>(llms: ReadonlyArray<T>): T[] {
|
||||
if (llms.length < 2) return [...llms];
|
||||
const labelById = _resolveServiceLabels(llms);
|
||||
return [...llms].sort((a, b) => labelById.get(a.sId)!.localeCompare(labelById.get(b.sId)!));
|
||||
}
|
||||
|
||||
/**
|
||||
* Group LLMs by service, alphabetically sorted by service label.
|
||||
* Preserves intra-service order.
|
||||
*/
|
||||
export function groupLLMsByService(llms: ReadonlyArray<DLLM>): LLMServiceGroup[] {
|
||||
const labelById = _resolveServiceLabels(llms);
|
||||
if (llms.length >= 2)
|
||||
llms = [...llms].sort((a, b) => labelById.get(a.sId)!.localeCompare(labelById.get(b.sId)!));
|
||||
|
||||
const groups: LLMServiceGroup[] = [];
|
||||
let currentGroup: LLMServiceGroup | null = null;
|
||||
|
||||
for (const llm of llms) {
|
||||
if (!currentGroup || currentGroup.serviceId !== llm.sId) {
|
||||
const vendor = findModelVendor(llm.vId);
|
||||
const serviceLabel = findModelsServiceOrNull(llm.sId)?.label || vendor?.name || llm.sId;
|
||||
currentGroup = { serviceId: llm.sId, serviceLabel, models: [] };
|
||||
currentGroup = { serviceId: llm.sId, serviceLabel: labelById.get(llm.sId)!, models: [] };
|
||||
groups.push(currentGroup);
|
||||
}
|
||||
currentGroup.models.push(llm);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { findModelVendor, type ModelVendorAccessOf, type ModelVendorId, type ModelVendorOf } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import type { DLLMId } from '../llms.types';
|
||||
import { useModelsStore } from '../store-llms';
|
||||
|
||||
|
||||
/**
|
||||
* Reactively resolve the vendor-specific transport access for a given LLM ID.
|
||||
*
|
||||
* - `llmId` nullish: hook is disabled, returns null.
|
||||
* - `llmId` set: resolves the LLM's own service (if it matches `fallbackFirstOf`),
|
||||
* or falls back to the first configured service for that vendor.
|
||||
*
|
||||
* NOTE: when the LLM's vendor doesn't match `fallbackFirstOf`, the LLM's own
|
||||
* service is ignored but the fallback still resolves - so a non-null access may
|
||||
* be returned even for unrelated LLMs. Callers should gate accordingly.
|
||||
*/
|
||||
export function useLlmServiceAccess<V extends ModelVendorId>(llmId: undefined | null | DLLMId, fallbackFirstOf: V) {
|
||||
|
||||
// reactive: resolve stable service object reference
|
||||
const service = useModelsStore(({ llms, sources }) => {
|
||||
if (!llmId) return null;
|
||||
|
||||
// prefer the LLM's own service
|
||||
const llm = llms.find(m => m.id === llmId);
|
||||
if (llm && llm.vId === fallbackFirstOf) {
|
||||
const svc = sources.find(s => s.id === llm.sId);
|
||||
if (svc) return svc;
|
||||
}
|
||||
|
||||
// fallback: first service for the given vendor
|
||||
return sources.find(s => s.vId === fallbackFirstOf) ?? null;
|
||||
});
|
||||
|
||||
// derive transport access (stable: only recomputes when service changes)
|
||||
return React.useMemo(() => {
|
||||
if (!service) return null;
|
||||
const vendor = findModelVendor(service.vId) as ModelVendorOf<V> | null;
|
||||
return vendor?.getTransportAccess(service.setup) as ModelVendorAccessOf<V> ?? null;
|
||||
}, [service]);
|
||||
}
|
||||
@@ -151,7 +151,7 @@ export const DModelParameterRegistry = {
|
||||
label: 'Effort',
|
||||
type: 'enum',
|
||||
description: 'Controls reasoning depth. Works alongside thinking budget.',
|
||||
values: ['low', 'medium', 'high', 'max'],
|
||||
values: ['low', 'medium', 'high', 'xhigh', 'max'],
|
||||
// undefined means high effort (default)
|
||||
}),
|
||||
|
||||
@@ -175,7 +175,8 @@ export const DModelParameterRegistry = {
|
||||
label: 'Thinking',
|
||||
type: 'enum',
|
||||
description: 'Enable or disable extended thinking mode.',
|
||||
values: ['none', 'high'],
|
||||
values: ['none', 'high', 'max'],
|
||||
// 'max' is for now DeepSeek V4-specific (reasoning_effort=max); other vendors restrict via enumValues
|
||||
// undefined means vendor default (usually 'high', i.e. thinking enabled)
|
||||
}),
|
||||
|
||||
@@ -348,6 +349,15 @@ export const DModelParameterRegistry = {
|
||||
// when undefined, the model chooses automatically
|
||||
},
|
||||
|
||||
// Gemini Interactions API agent_config - per-agent knobs (Deep Research only today)
|
||||
llmVndGeminiAgentViz: _enumDef({
|
||||
label: 'Visualizations',
|
||||
type: 'enum',
|
||||
description: 'Charts and images in Deep Research reports. Disable for text-only output (helpful when merging multiple reports).',
|
||||
values: ['auto', 'off'],
|
||||
// undefined means upstream default ('auto'); we only forward when explicitly 'off'
|
||||
}),
|
||||
|
||||
// NOTE: we don't have this as a parameter, as for now we use it in tandem with llmVndGeminiGoogleSearch
|
||||
// llmVndGeminiUrlContext: {
|
||||
// label: 'URL Context',
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface DLLM {
|
||||
label: string;
|
||||
created: number | 0;
|
||||
updated?: number | 0;
|
||||
pubDate?: string; // official release date in 'YYYYMMDD'
|
||||
description: string;
|
||||
hidden: boolean;
|
||||
|
||||
@@ -97,6 +98,10 @@ export function isLLMVisible(llm: DLLM): boolean {
|
||||
return !(llm.userHidden ?? llm.hidden ?? false);
|
||||
}
|
||||
|
||||
export function isLLMCustomUserParameters(llm: DLLM): boolean {
|
||||
return !!(llm.userParameters && Object.keys(llm.userParameters).length > 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the effective context token limit for a model.
|
||||
* Checks user override first, then vendor-specific parameters, then falls back to model default.
|
||||
@@ -133,6 +138,20 @@ export function getLLMMaxOutputTokens(llm: DLLM | null): DLLMMaxOutputTokens | u
|
||||
return llm.userMaxOutputTokens ?? llm.maxOutputTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the model's editorial `pubDate` ('YYYYMMDD') into a Date, or null if missing/malformed.
|
||||
* Date is constructed at local midnight - pubDate is day-precision, no time component.
|
||||
*/
|
||||
export function getLLMPubDate(llm: DLLM | null | undefined): Date | null {
|
||||
const p = llm?.pubDate;
|
||||
if (!p || !/^\d{8}$/.test(p)) return null;
|
||||
const y = parseInt(p.slice(0, 4), 10);
|
||||
const m = parseInt(p.slice(4, 6), 10) - 1; // JS Date months are 0-indexed
|
||||
const d = parseInt(p.slice(6, 8), 10);
|
||||
const date = new Date(y, m, d);
|
||||
return Number.isFinite(date.getTime()) ? date : null;
|
||||
}
|
||||
|
||||
/// Interfaces ///
|
||||
|
||||
// do not change anything below! those will be persisted in data
|
||||
@@ -145,6 +164,7 @@ export type DModelInterfaceV1 =
|
||||
| 'oai-chat-reasoning'
|
||||
| 'ant-prompt-caching'
|
||||
| 'gem-code-execution'
|
||||
| 'gem-interactions'
|
||||
| 'oai-prompt-caching'
|
||||
| 'oai-realtime'
|
||||
| 'oai-responses'
|
||||
@@ -164,6 +184,7 @@ export type DModelInterfaceV1 =
|
||||
// FIXME: keep this in sync with the server side on modules/llms/server/llm.server.types.ts
|
||||
export const LLM_IF_OAI_Chat: DModelInterfaceV1 = 'oai-chat';
|
||||
export const LLM_IF_OAI_Fn: DModelInterfaceV1 = 'oai-chat-fn';
|
||||
/** @deprecated we don't se this one anymore 2026-04-19; suspended until we have a reason or per-model continuos validation of this */
|
||||
export const LLM_IF_OAI_Json: DModelInterfaceV1 = 'oai-chat-json'; // for Structured Outputs (or JSON mode at worst)
|
||||
export const LLM_IF_ANT_ToolsSearch: DModelInterfaceV1 = 'ant-tools-search';
|
||||
// export const LLM_IF_OAI_JsonSchema: ... future?
|
||||
@@ -175,6 +196,7 @@ export const LLM_IF_Outputs_NoText: DModelInterfaceV1 = 'outputs-no-text';
|
||||
export const LLM_IF_Tools_WebSearch: DModelInterfaceV1 = 'tools-web-search';
|
||||
export const LLM_IF_ANT_PromptCaching: DModelInterfaceV1 = 'ant-prompt-caching';
|
||||
export const LLM_IF_GEM_CodeExecution: DModelInterfaceV1 = 'gem-code-execution';
|
||||
export const LLM_IF_GEM_Interactions: DModelInterfaceV1 = 'gem-interactions';
|
||||
export const LLM_IF_OAI_PromptCaching: DModelInterfaceV1 = 'oai-prompt-caching';
|
||||
export const LLM_IF_OAI_Responses: DModelInterfaceV1 = 'oai-responses';
|
||||
export const LLM_IF_HOTFIX_NoStream: DModelInterfaceV1 = 'hotfix-no-stream';
|
||||
@@ -202,6 +224,7 @@ export const LLMS_ALL_INTERFACES = [
|
||||
// Vendor-specific capabilities
|
||||
LLM_IF_ANT_PromptCaching, // [Anthropic] model supports anthropic-specific caching
|
||||
LLM_IF_GEM_CodeExecution, // [Gemini] Tool: code execution
|
||||
LLM_IF_GEM_Interactions, // [Gemini] Interactions API (required by Deep Research agents)
|
||||
LLM_IF_OAI_PromptCaching, // [OpenAI] model supports OpenAI prompt caching
|
||||
LLM_IF_OAI_Responses, // [OpenAI] Responses API (new) support
|
||||
// Hotfixes to patch specific model quirks
|
||||
|
||||
@@ -266,8 +266,8 @@ export const useModelsStore = create<LlmsStore>()(persist(
|
||||
resetServiceUserParameters: (serviceId: DModelsServiceId) =>
|
||||
set(({ llms }) => ({
|
||||
llms: llms.map((llm: DLLM): DLLM => {
|
||||
if (llm.sId !== serviceId) return llm;
|
||||
// strip away user parameters and user label
|
||||
if (llm.sId !== serviceId || llm.isUserClone) return llm;
|
||||
// strip away user parameters and user label (skip user-cloned models)
|
||||
const {
|
||||
userParameters,
|
||||
userLabel, // service-wide reset includes resetting the name
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
|
||||
/// Global AI Preferences ///
|
||||
|
||||
|
||||
export type AIVndAntInlineFilesPolicy = 'off' | 'inline-file' | 'inline-file-and-delete';
|
||||
|
||||
|
||||
interface AIPreferencesState {
|
||||
|
||||
// Anthropic
|
||||
vndAntInlineFiles: AIVndAntInlineFilesPolicy;
|
||||
|
||||
}
|
||||
|
||||
interface AIPreferencesActions {
|
||||
|
||||
// Anthropic
|
||||
setVndAntInlineFiles: (policy: AIVndAntInlineFilesPolicy) => void;
|
||||
|
||||
// Maintenance
|
||||
resetToDefaults: () => void;
|
||||
|
||||
}
|
||||
|
||||
|
||||
const createAIPreferencesDefaults = (): AIPreferencesState => ({
|
||||
vndAntInlineFiles: 'inline-file',
|
||||
});
|
||||
|
||||
|
||||
export const useAIPreferencesStore = create<AIPreferencesState & AIPreferencesActions>()(persist((_set) => ({
|
||||
|
||||
...createAIPreferencesDefaults(),
|
||||
|
||||
// Anthropic
|
||||
setVndAntInlineFiles: (vndAntInlineFiles: AIVndAntInlineFilesPolicy) => _set({ vndAntInlineFiles }),
|
||||
|
||||
// Maintenance
|
||||
resetToDefaults: () => _set(createAIPreferencesDefaults()),
|
||||
|
||||
}), {
|
||||
name: 'app-ai-preferences',
|
||||
}));
|
||||
|
||||
|
||||
// Imperative getters/actions (for use outside React)
|
||||
|
||||
export function getVndAntInlineFiles(): AIVndAntInlineFilesPolicy {
|
||||
return useAIPreferencesStore.getState().vndAntInlineFiles;
|
||||
}
|
||||
|
||||
// export function resetAIPreferencesToDefaults(): void {
|
||||
// useAIPreferencesStore.getState().resetToDefaults();
|
||||
// }
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user