mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-11 14:10:15 -07:00
Compare commits
477 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78e3a57857 | |||
| 79d0c96b20 | |||
| 21ed38a20e | |||
| d8b1f99114 | |||
| b0fb1b9890 | |||
| a63932cff2 | |||
| 0b22165d2a | |||
| 41b1951abe | |||
| 353431e54c | |||
| 7b232dd7d8 | |||
| d32adf9dbf | |||
| 940d490217 | |||
| 46e41e38cf | |||
| 276ff8f995 | |||
| 030837fccf | |||
| a7d38aefb1 | |||
| 230a0d7caf | |||
| 6e14e43c78 | |||
| e6389f08be | |||
| a4edeb098e | |||
| 093c536415 | |||
| 7479b50fea | |||
| ebce36d043 | |||
| 77bab1aa74 | |||
| ebcac3405c | |||
| d2781a6f87 | |||
| f5954f5bb3 | |||
| 6baf694d6f | |||
| cb3b586d4d | |||
| f68789ab20 | |||
| 0c6a3f1917 | |||
| 05fccaf982 | |||
| 7340b9ecc2 | |||
| 78eb4ebe0b | |||
| b1453a34ec | |||
| c357e9e2f5 | |||
| 98717bf8a9 | |||
| d7077ada0e | |||
| 64f63ed1d3 | |||
| 2a27f6c30d | |||
| 9fdddeaba8 | |||
| 2cfa5e93e4 | |||
| 778ac14344 | |||
| 85fcf8be61 | |||
| b31eb09015 | |||
| 5154dd1740 | |||
| 274f11ef1d | |||
| aeb1acf458 | |||
| a204f4a58e | |||
| 8e4a57aa01 | |||
| 797ed0a553 | |||
| 663bc0d471 | |||
| 8d7e2d2c46 | |||
| 19d96bb30b | |||
| 47f2f20d9c | |||
| 12c7c634c0 | |||
| 9a322c150a | |||
| 1a3bc4f666 | |||
| d4881b1ce5 | |||
| a2ad2df473 | |||
| 541c5bd1c3 | |||
| b744e9673b | |||
| bb94b7c5c6 | |||
| e9ff57d5e1 | |||
| 179245457c | |||
| 1493f74691 | |||
| 4857503ed3 | |||
| a0e38b4f0c | |||
| 1d62cad9e9 | |||
| 855761020c | |||
| 0950d06dfb | |||
| 1496402325 | |||
| 77e2c4babb | |||
| a465082984 | |||
| 025fdac686 | |||
| 6bde5ec64c | |||
| f099a9ec39 | |||
| 5bfcef92ee | |||
| 79a8fbd881 | |||
| 7f96a14cf6 | |||
| 5fe6d70713 | |||
| dcba4dd4bc | |||
| ccbe77913b | |||
| 2844cb81c2 | |||
| d86e8e5920 | |||
| 9665fa1eb4 | |||
| 2788ef679b | |||
| e1a88e1fd8 | |||
| 32163c5302 | |||
| 2d3d5efe87 | |||
| e1bbba392c | |||
| ed642c856b | |||
| 927e462f7a | |||
| e250499a3b | |||
| 91d96a6639 | |||
| 104ec4c87c | |||
| 0a7e8436c3 | |||
| 9e597e0a28 | |||
| 01fbb5d47c | |||
| 6517d16337 | |||
| 0e636adf28 | |||
| 0bb281237b | |||
| 2b224376c2 | |||
| e510b369d7 | |||
| a0de1f7230 | |||
| 4591132269 | |||
| a03de8d490 | |||
| 27bcfec17e | |||
| f6dbec3e1d | |||
| aebc45f705 | |||
| 310c60b9d9 | |||
| bcba67c209 | |||
| fc013aed52 | |||
| 8ad41c059b | |||
| 8eaf8db850 | |||
| 896883766c | |||
| 258dacf3ed | |||
| 242243f485 | |||
| a18436dce1 | |||
| 5323cbc00e | |||
| ddd3b137ac | |||
| 94550088e5 | |||
| 1375ca6f5c | |||
| e4c4fe0495 | |||
| 2fa5277e56 | |||
| b73ad8fdc1 | |||
| 9cc281e65e | |||
| d62107d39b | |||
| 4a8d20ad72 | |||
| 5acb72c39b | |||
| 67e8236a60 | |||
| 18b8853f82 | |||
| 65c7df7938 | |||
| 15678cdfa2 | |||
| 6cd6c62046 | |||
| dbf92805a2 | |||
| 11fc9a7b85 | |||
| 8bc970ff57 | |||
| a16eefd97b | |||
| ca5e5b820c | |||
| f73ad52441 | |||
| 729ec1d1bf | |||
| 4adb30b861 | |||
| 999f6de45f | |||
| 70686502b4 | |||
| d17a980151 | |||
| 7fa5947030 | |||
| de8f120fd4 | |||
| 9b54603264 | |||
| 698c77d7ba | |||
| 18d83a4d18 | |||
| 8e849d93b2 | |||
| 4ca42f028b | |||
| 3118337879 | |||
| db4490affb | |||
| 51ab79384e | |||
| 3ee30a252d | |||
| b883566ebb | |||
| ac78fb85b8 | |||
| 0d2b11d0c4 | |||
| 5b610c88c1 | |||
| bf444ce043 | |||
| c91c027dab | |||
| 81fd87c510 | |||
| 9da174a962 | |||
| 84f54a7e65 | |||
| baeecf1464 | |||
| f2fdd39c96 | |||
| 53b074d78e | |||
| f4fc1e6775 | |||
| dba791b8db | |||
| 750fa02621 | |||
| 7a67816111 | |||
| 613625644e | |||
| 0e25071ef0 | |||
| ed1932cd26 | |||
| 67b89213d0 | |||
| 814f142c5f | |||
| 16cd3e7d5a | |||
| c5dcb8faef | |||
| 6b46c022f9 | |||
| 88ef05fc72 | |||
| 445ea367fc | |||
| c819554f43 | |||
| bbc8a79ded | |||
| 3d181bc10d | |||
| ba5478f382 | |||
| 136c993c8d | |||
| 6cf18ea4e8 | |||
| fe7f56c82e | |||
| 6c580f1e43 | |||
| f171cd4f03 | |||
| ea109e6c30 | |||
| f514eed226 | |||
| 274ba80149 | |||
| 46b4dfc458 | |||
| 4af8f4ff6a | |||
| df5810d695 | |||
| d9ad96c374 | |||
| 06cc93fd82 | |||
| 41da63765f | |||
| 3975411c78 | |||
| fc2e75ef61 | |||
| ef0f2dd3d0 | |||
| 548c3c5d72 | |||
| d2e3a0cb8e | |||
| 9cdace6f81 | |||
| 12f020570e | |||
| bef2551eec | |||
| 7e20f8c189 | |||
| 56e8390e55 | |||
| 89fff16385 | |||
| 2cf15a24eb | |||
| 512e867034 | |||
| ce8c55c3c7 | |||
| 8e0d904d9a | |||
| 6c846a8ae7 | |||
| 5004469fe9 | |||
| 14d0af74ed | |||
| 5a76cf9486 | |||
| 82901ccd02 | |||
| 1dc9d66673 | |||
| a0cbfaf390 | |||
| 9a01ae61ef | |||
| 91837d5acd | |||
| 1b9ebdda22 | |||
| b6f6177af3 | |||
| d35486196b | |||
| 1603637e3b | |||
| 8f20840169 | |||
| 4fff2394de | |||
| afb74e68ee | |||
| d5fa7844c5 | |||
| b8470cd640 | |||
| 9a23f573a6 | |||
| efe8fa0fda | |||
| 2d16e8bb4f | |||
| bbd95eebff | |||
| ceb00b4e93 | |||
| cc60d26d1c | |||
| ba3ff739f6 | |||
| 6062647705 | |||
| 070c1c2de9 | |||
| d3aaa69409 | |||
| 0ac7753e35 | |||
| eba9d53d2e | |||
| d04d4ec8e7 | |||
| c7c3efcbe7 | |||
| 2b8d53a44c | |||
| ef6b573e08 | |||
| 61eedd41df | |||
| b265bcda20 | |||
| d703d32a1f | |||
| aab9334404 | |||
| c2570f6955 | |||
| 8e936a6334 | |||
| 46bfc22869 | |||
| db1620dd56 | |||
| e59f8a42a3 | |||
| 17d18bd85d | |||
| fb256cf578 | |||
| 1b6b5db76d | |||
| 41647ca83a | |||
| 07d2a17a87 | |||
| 6d744dfb7e | |||
| b9b946c35f | |||
| 17adfe2117 | |||
| 1e5e21102d | |||
| 4af992222f | |||
| a9447c6a11 | |||
| db71323313 | |||
| b9b2748e05 | |||
| 387231f743 | |||
| 2216a89aa3 | |||
| 4faa6326fa | |||
| cb22b3d9a1 | |||
| 152a3873bd | |||
| adc2760a89 | |||
| dde64acb06 | |||
| 008adbd8bc | |||
| 0e4866a5a2 | |||
| 5cb96cae3a | |||
| 8cbb82a67f | |||
| 848ddbe477 | |||
| 083c1cde8b | |||
| b792971062 | |||
| 07dde8f4b1 | |||
| 01f94127dd | |||
| 4d457b4e9e | |||
| 8ac93ff2da | |||
| ef33a4b08e | |||
| fdd3b25a27 | |||
| 4dc979da08 | |||
| 8f426e03c4 | |||
| 40cd085bf8 | |||
| 6aa75fc5d1 | |||
| eae5920f9d | |||
| 2f6bfa37cc | |||
| 9d6fd9b9b8 | |||
| 260cd67c96 | |||
| aff76e2d18 | |||
| 52e4343045 | |||
| 1ffbb135c6 | |||
| c3ec522261 | |||
| 4538839376 | |||
| 834edd3a71 | |||
| 581c3d9593 | |||
| 0c672fbaa5 | |||
| 6d96b9a312 | |||
| 691791ccd0 | |||
| f4299121d5 | |||
| 1adfb7eedd | |||
| 33ad583d15 | |||
| a7e2fe2277 | |||
| 5a479d5863 | |||
| 873ff034d2 | |||
| 61d3537617 | |||
| ae068a3f64 | |||
| f7402cd6f5 | |||
| c53f9c8020 | |||
| 798b4d57f4 | |||
| 98d428fb34 | |||
| 3ac5ace216 | |||
| 444a1a7ab9 | |||
| 43ea4bd4b5 | |||
| 6a9272e40a | |||
| 10589a11aa | |||
| a88f898bc0 | |||
| 7a84038b04 | |||
| 111c40732d | |||
| 69bb78c8be | |||
| ad3b327d69 | |||
| dc27f38534 | |||
| 5b0816cb92 | |||
| 57f6955303 | |||
| 78915f878d | |||
| 6ced6d626b | |||
| ee3cb819b4 | |||
| cc17b1d19d | |||
| 2c83240d47 | |||
| 54f18ff120 | |||
| 5e1fe363c3 | |||
| 3d2ec507e1 | |||
| 1dd7af3c8b | |||
| 06ec1fcebf | |||
| 86cb863fd4 | |||
| d5ef1288d8 | |||
| f3354c498d | |||
| 9557141b38 | |||
| 3144b66e73 | |||
| 6dbefa3d2f | |||
| c8f3b139e8 | |||
| 288663325d | |||
| 49947ee01d | |||
| fa7a45ebc7 | |||
| 9a074c222f | |||
| 4e0d7b6ed9 | |||
| 1f3defb04c | |||
| 6c52c43460 | |||
| deae2879f1 | |||
| 5b255a7d8b | |||
| 6e06c24b7a | |||
| 2fde1efdd3 | |||
| aeb29d983a | |||
| c8a7123da9 | |||
| 5c22061415 | |||
| 9a0fda8c02 | |||
| 2f9a17c44a | |||
| 50559015d8 | |||
| a8d4e143c2 | |||
| 2a6c69538d | |||
| 0ba5d61353 | |||
| d436ec5790 | |||
| 759b822b92 | |||
| 9df45af698 | |||
| 3474e81446 | |||
| e1f07eb957 | |||
| 71ff1b98be | |||
| 9b370dfa88 | |||
| 0be0661750 | |||
| eaa7230af7 | |||
| 11cb000481 | |||
| 8ae3554a58 | |||
| dfd4736386 | |||
| feb793c9fa | |||
| ee962fde08 | |||
| c08dd96de3 | |||
| b52f771133 | |||
| 4631232551 | |||
| df7f5047aa | |||
| 467d14324d | |||
| cbdce08e96 | |||
| d6bf8f8854 | |||
| 4599da3ded | |||
| 6d50952b2e | |||
| 7066947809 | |||
| e2924aacab | |||
| 1e86d2503f | |||
| eb67eee53a | |||
| dfdad45963 | |||
| 4735508d87 | |||
| c43c47eab8 | |||
| fafb2dc6b9 | |||
| 140e99c465 | |||
| 7ba1974390 | |||
| 51b8510f17 | |||
| 5d6949d471 | |||
| 8e9d0c1fd1 | |||
| 3852a3b779 | |||
| 8b4ba96936 | |||
| 0c17e18491 | |||
| 2bdbab3afc | |||
| b97499a95e | |||
| a70ac57872 | |||
| a9cf457024 | |||
| e5c938ac37 | |||
| edad54efa2 | |||
| f88426758f | |||
| 77a28eb810 | |||
| f834b27562 | |||
| 984e257cc5 | |||
| 729e7612bc | |||
| 59fadeae57 | |||
| bfbf7a298a | |||
| aad5d3bd65 | |||
| 504f19c445 | |||
| 19c47eb442 | |||
| ab6043df60 | |||
| 3305549a0f | |||
| c24c3cb571 | |||
| 952999258b | |||
| 0713eaa52c | |||
| 8fee689f60 | |||
| 75ddb17fed | |||
| 0c6a74626c | |||
| 41e3d0eaf9 | |||
| 8b9cfebd42 | |||
| 16badee259 | |||
| 9d5171dd36 | |||
| e0c0e81b7d | |||
| fd4e8985fc | |||
| 1d9b8503c0 | |||
| b3ef7b914d | |||
| 2f59e12e20 | |||
| 30e8652c2a | |||
| 5ee6aceb60 | |||
| 6940b6a6d1 | |||
| 4e33ce9415 | |||
| 944e22bde6 | |||
| 6054fa0a26 | |||
| 4db13cfed4 | |||
| 6a6adda2e0 | |||
| 4afa55c0db | |||
| bc120bfb2b | |||
| 88966699e7 | |||
| 9a5db3dcfb | |||
| 392aa1e654 | |||
| f2b32e47ff | |||
| 58136d0181 | |||
| 02733e55cb | |||
| 60df8456a7 | |||
| 6d0ecc805c | |||
| a0e9dd24a3 | |||
| d1eb89057d | |||
| 161c6dc83a | |||
| 54848b8a7e | |||
| 990563c604 | |||
| 8489ca8c8d | |||
| b57e2c89e3 | |||
| 66bedf78ac | |||
| 592c5cce60 | |||
| 2ccf9a4e92 | |||
| ed333c0513 | |||
| 89b65b7009 | |||
| 0cc2d346af | |||
| 5f81e78bc4 | |||
| 554b5fd4b5 |
@@ -21,8 +21,9 @@ assignees: enricoros
|
||||
- [ ] Create a temporary tag `git tag v1.2.3 && git push opensource --tags`
|
||||
- [ ] Create a [New Draft GitHub Release](https://github.com/enricoros/big-agi/releases/new), and generate the automated changelog (for new contributors)
|
||||
- [ ] Update the release version in package.json, and `npm i`
|
||||
- [ ] Update in-app News [src/apps/news/news.data.tsx](/src/apps/news/news.data.tsx)
|
||||
- [ ] Update the in-app News version number
|
||||
- [ ] Update in-app News [src/apps/news/news.data.tsx](/src/apps/news/news.data.tsx)
|
||||
- [ ] Update in-app Cover graphics
|
||||
- [ ] Update the README.md with the new release
|
||||
- [ ] Copy the highlights to the [docs/changelog.md](/docs/changelog.md)
|
||||
- Release:
|
||||
@@ -79,11 +80,32 @@ I need the following from you:
|
||||
|
||||
1. a table summarizing all the new features in 1.2.3 with the following columns: 4 words description (exactly what it is), short description, usefulness (what it does for the user), significance, link to the issue number (not the commit)), which will be used for the artifacts later
|
||||
2. then double-check the git log to see if there are any features of significance that are not in the table
|
||||
3. then score each feature in terms of importance for users (1-10), relative impact of the feature (1-10, where 10 applies to the broadest user base), and novelty and uniqueness (1-10, where 10 is truly unique and novel from what exists already)
|
||||
3. then score each feature in terms of importance for users (1-10), relative impact of the feature (1-10, where 10 applies to the broadest user base), and novelty and uniqueness (1-10, where 10 is truly unique and novel from what exists already)
|
||||
4. then improve the table, in decreasing order of importance for features, fixing any detail that's missing, in particular check if there are commits of significance from a user or developer point of view, which are not contained in the table
|
||||
5. then I want you then to update the news.data.tsx for the new release
|
||||
```
|
||||
|
||||
### release name
|
||||
|
||||
```markdown
|
||||
please brainstorm 10 different names for this release. see the former names here: https://big-agi.com/blog
|
||||
```
|
||||
|
||||
You can follow with 'What do you think of Modelmorphic?' or other selected name
|
||||
|
||||
### cover images
|
||||
|
||||
```markdown
|
||||
Great, now I need to generate images for this. Before I used the following prompts (2 releases before).
|
||||
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is using a computer with split screen made of origami, split keyboard and is wearing origami sunglasses with very different split reflections. Split halves are very contrasting. Close up photography, bokeh, white background.
|
||||
import coverV113 from '../../../public/images/covers/release-cover-v1.13.0.png';
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is calling on a 3D origami old-school pink telephone and the camera is zooming on the telephone. Close up photography, bokeh, white background.
|
||||
import coverV112 from '../../../public/images/covers/release-cover-v1.12.0.png';
|
||||
|
||||
What can I do now as far as images? Give me 4 prompt ideas with the same style as looks as the former, but different scene or action
|
||||
```
|
||||
|
||||
### Readme (and Changelog)
|
||||
|
||||
```markdown
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
# BIG-AGI 🧠✨
|
||||
|
||||
Welcome to big-AGI 👋, the GPT application for professionals that need function, form,
|
||||
Welcome to big-AGI, the AI suite for professionals that need function, form,
|
||||
simplicity, and speed. Powered by the latest models from 12 vendors and
|
||||
open-source model servers, `big-AGI` offers best-in-class Voice and Chat with AI Personas,
|
||||
visualizations, coding, drawing, calling, and quite more -- all in a polished UX.
|
||||
open-source servers, `big-AGI` offers best-in-class Chats,
|
||||
[Beams](https://github.com/enricoros/big-AGI/issues/470),
|
||||
and [Calls](https://github.com/enricoros/big-AGI/issues/354) with AI personas,
|
||||
visualizations, coding, drawing, side-by-side chatting, and more -- all wrapped in a polished UX.
|
||||
|
||||
Pros use big-AGI. 🚀 Developers love big-AGI. 🤖
|
||||
Stay ahead of the curve with big-AGI. 🚀 Pros & Devs love big-AGI. 🤖
|
||||
|
||||
[](https://big-agi.com)
|
||||
|
||||
@@ -17,6 +19,25 @@ Or fork & run on Vercel
|
||||
|
||||
big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap
|
||||
|
||||
### What's New in 1.15.0 · April 1, 2024 · Beam
|
||||
|
||||
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
|
||||
- Managed Deployments **Auto-Configuration**: simplify the UI models setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
|
||||
- Message **Starring ⭐**: star important messages within chats, to attach them later. [#476](https://github.com/enricoros/big-AGI/issues/476)
|
||||
- Enhanced the default Persona
|
||||
- Fixes to Gemini models and SVGs, improvements to UI and icons
|
||||
- Beast release, over 430 commits, 10,000+ lines changed: [release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.15.0), and changes [v1.14.1...v1.15.0](https://github.com/enricoros/big-AGI/compare/v1.14.1...v1.15.0)
|
||||
|
||||
### What's New in 1.14.1 · March 7, 2024 · Modelmorphic
|
||||
|
||||
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
|
||||
- New **[Perplexity](https://www.perplexity.ai/)** and **[Groq](https://groq.com/)** integration (thanks @Penagwin). [#407](https://github.com/enricoros/big-AGI/issues/407), [#427](https://github.com/enricoros/big-AGI/issues/427)
|
||||
- **[LocalAI](https://localai.io/models/)** deep integration, including support for [model galleries](https://github.com/enricoros/big-AGI/issues/411)
|
||||
- **Mistral** Large and Google **Gemini 1.5** support
|
||||
- Performance optimizations: runs [much faster](https://twitter.com/enricoros/status/1756553038293303434?utm_source=localhost:3000&utm_medium=big-agi), saves lots of power, reduces memory usage
|
||||
- Enhanced UX with auto-sizing charts, refined search and folder functionalities, perfected scaling
|
||||
- And with more UI improvements, documentation, bug fixes (20 tickets), and developer enhancements
|
||||
|
||||
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
|
||||
@@ -73,11 +94,11 @@ https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cf
|
||||
|
||||
For full details and former releases, check out the [changelog](docs/changelog.md).
|
||||
|
||||
## ✨ Key Features 👊
|
||||
## 👉 Key Features ✨
|
||||
|
||||
|  |  |  |  |  |
|
||||
|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
|
||||
| **Chat**<br/>**Call** AGI<br/>**Draw** images<br/>**Agents**, ... | Local & Cloud<br/>Open & Closed<br/>Cheap & Heavy<br/>Google, Mistral, ... | Attachments<br/>Diagrams<br/>Multi-Chat<br/>Mobile-first UI | Stored Locally<br/>Easy self-Host<br/>Local actions<br/>Data = Gold | AI Personas<br/>Voice Modes<br/>Screen Capture<br/>Camera + OCR |
|
||||
| **Chat**<br/>**Call**<br/>**Beam**<br/>**Draw**, ... | Local & Cloud<br/>Open & Closed<br/>Cheap & Heavy<br/>Google, Mistral, ... | Attachments<br/>Diagrams<br/>Multi-Chat<br/>Mobile-first UI | Stored Locally<br/>Easy self-Host<br/>Local actions<br/>Data = Gold | AI Personas<br/>Voice Modes<br/>Screen Capture<br/>Camera + OCR |
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -16,4 +16,8 @@ const handlerNodeRoutes = (req: Request) =>
|
||||
});
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
// NOTE: the following statement breaks the build on non-pro deployments, and conditionals don't work either
|
||||
// so we resorted to raising the timeout from 10s to 25s in the vercel.json file instead
|
||||
// export const maxDuration = 25;
|
||||
export const dynamic = 'force-dynamic';
|
||||
export { handlerNodeRoutes as GET, handlerNodeRoutes as POST };
|
||||
+1
-1
@@ -28,7 +28,7 @@ Detailed guides to configure your big-AGI interface and models.
|
||||
- **Advanced Feature Configuration**:
|
||||
- **[Browse](config-feature-browse.md)**: Enable web page download through third-party services or your own cloud (advanced)
|
||||
- **ElevenLabs API**: Voice and cutom voice generation, only requires their API key
|
||||
- **Google Search API**: guide not yet available, see the Google options in 'Environment Variables'
|
||||
- **Google Search API**: guide not yet available, see the Google options in '[Environment Variables](environment-variables.md)'
|
||||
- **Prodia API**: Stable Diffusion XL image generation, only requires their API key, alternative to DALL·E
|
||||
|
||||
## Deployment
|
||||
|
||||
+24
-4
@@ -5,12 +5,32 @@ by release.
|
||||
|
||||
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
|
||||
|
||||
### 1.13.0 - Feb 2024
|
||||
### 1.16.0 - Mar 2024
|
||||
|
||||
- milestone: [1.13.0](https://github.com/enricoros/big-agi/milestone/13)
|
||||
- milestone: [1.16.0](https://github.com/enricoros/big-agi/milestone/16)
|
||||
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
|
||||
|
||||
## What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
### What's New in 1.15.0 · April 1, 2024 · Beam
|
||||
|
||||
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
|
||||
- Managed Deployments **Auto-Configuration**: simplify the UI mdoels setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
|
||||
- Message **Starring ⭐**: star important messages within chats, to attach them later. [#476](https://github.com/enricoros/big-AGI/issues/476)
|
||||
- Enhanced the default Persona
|
||||
- Fixes to Gemini models and SVGs, improvements to UI and icons
|
||||
- Beast release, over 430 commits, 10,000+ lines changed: [release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.15.0), and changes [v1.14.1...v1.15.0](https://github.com/enricoros/big-AGI/compare/v1.14.1...v1.15.0)
|
||||
|
||||
### What's New in 1.14.1 · March 7, 2024 · Modelmorphic
|
||||
|
||||
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
|
||||
- New **[Perplexity](https://www.perplexity.ai/)** and **[Groq](https://groq.com/)** integration (thanks @Penagwin). [#407](https://github.com/enricoros/big-AGI/issues/407), [#427](https://github.com/enricoros/big-AGI/issues/427)
|
||||
- **[LocalAI](https://localai.io/models/)** deep integration, including support for [model galleries](https://github.com/enricoros/big-AGI/issues/411)
|
||||
- **Mistral** Large and Google **Gemini 1.5** support
|
||||
- Performance optimizations: runs [much faster](https://twitter.com/enricoros/status/1756553038293303434?utm_source=localhost:3000&utm_medium=big-agi), saves lots of power, reduces memory usage
|
||||
- Enhanced UX with auto-sizing charts, refined search and folder functionalities, perfected scaling
|
||||
- And with more UI improvements, documentation, bug fixes (20 tickets), and developer enhancements
|
||||
- [Release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.14.0), and changes [v1.13.1...v1.14.0](https://github.com/enricoros/big-AGI/compare/v1.13.1...v1.14.0) (233 commits, 8,000+ lines changed)
|
||||
|
||||
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
|
||||
|
||||
@@ -22,7 +42,7 @@ https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385
|
||||
- Better looking chats with improved spacing, fonts, and menus
|
||||
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-lmstudio.md) (thanks @aj47), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/deploy-database.md) (thanks @ranfysvalle02), and speedups
|
||||
|
||||
## What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline
|
||||
### What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/95ceb03c-945d-4fdd-9a9f-3317beb54f3f
|
||||
|
||||
|
||||
+26
-3
@@ -22,6 +22,25 @@ Understand the Architecture: big-AGI uses Next.js, React for the front end, and
|
||||
|
||||
This necessitates a code change (file renaming) before build initiation, detailed in [deploy-authentication.md](deploy-authentication.md).
|
||||
|
||||
### Increase Vercel Functions Timeout
|
||||
|
||||
For long-running operations, Vercel allows paid deployments to increase the timeout on Functions.
|
||||
Note that this applies to old-style Vercel Functions (based on Node.js) and not the new Edge Functions.
|
||||
|
||||
At time of writing, big-AGI has only 2 operations that run on Node.js Functions:
|
||||
browsing (fetching web pages) and sharing. They both can exceed 10 seconds, especially
|
||||
when fetching large pages or waiting for websites to be completed.
|
||||
|
||||
We provide `vercel_PRODUCTION.json` to raise the duration to 25 seconds (from a default of 10), to use it,
|
||||
make sure to rename it to `vercel.json` before build.
|
||||
|
||||
From the Vercel Project > Settings > General > Build & Development Settings,
|
||||
you can for instance set the build command to:
|
||||
|
||||
```bash
|
||||
mv vercel_PRODUCTION.json vercel.json; next build
|
||||
```
|
||||
|
||||
### Change the Personas
|
||||
|
||||
Edit the `src/data.ts` file to customize personas. This file houses the default personas. You can add, remove, or modify these to meet your project's needs.
|
||||
@@ -43,16 +62,20 @@ Test your application thoroughly using local development (refer to README.md for
|
||||
- [deploy-cloudflare.md](deploy-cloudflare.md): for Cloudflare Workers deployment
|
||||
- [deploy-docker.md](deploy-docker.md): for Docker deployment instructions and examples
|
||||
|
||||
## Debugging
|
||||
|
||||
We introduced the `/info/debug` page that provides a detailed overview of the application's environment, including the API keys, environment variables, and other configuration settings.
|
||||
|
||||
<br/>
|
||||
|
||||
## Community Projects - Share Your Project
|
||||
|
||||
After deployment, share your project with the community. We will link to your project to help others discover and learn from your work.
|
||||
|
||||
| Project | Features | GitHub |
|
||||
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| Project | Features | GitHub |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| 🚀 CoolAGI: Where AI meets Imagination<br/> | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
|
||||
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
|
||||
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
|
||||
|
||||
For public projects, update your README.md with your modifications and submit a pull request to add your project to our list, aiding in its discovery.
|
||||
|
||||
|
||||
@@ -147,5 +147,5 @@ The value of these variables are passed to the frontend (Web UI) - make sure the
|
||||
|
||||
---
|
||||
|
||||
For a higher level overview of backend code and environemnt customization,
|
||||
For a higher level overview of backend code and environment customization,
|
||||
see the [big-AGI Customization](customizations.md) guide.
|
||||
|
||||
Generated
+549
-489
File diff suppressed because it is too large
Load Diff
+18
-18
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.13.0",
|
||||
"version": "1.15.0",
|
||||
"private": true,
|
||||
"author": "Enrico Ros <enrico.ros@gmail.com>",
|
||||
"repository": "https://github.com/enricoros/big-agi",
|
||||
@@ -19,14 +19,14 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.11",
|
||||
"@mui/joy": "^5.0.0-beta.29",
|
||||
"@next/bundle-analyzer": "^14.1.0",
|
||||
"@next/third-parties": "^14.1.0",
|
||||
"@prisma/client": "^5.10.2",
|
||||
"@mui/icons-material": "^5.15.14",
|
||||
"@mui/joy": "^5.0.0-beta.32",
|
||||
"@next/bundle-analyzer": "^14.1.4",
|
||||
"@next/third-parties": "^14.1.4",
|
||||
"@prisma/client": "^5.11.0",
|
||||
"@sanity/diff-match-patch": "^3.1.1",
|
||||
"@t3-oss/env-nextjs": "^0.9.2",
|
||||
"@tanstack/react-query": "~4.36.1",
|
||||
@@ -39,7 +39,7 @@
|
||||
"browser-fs-access": "^0.35.0",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"next": "^14.1.0",
|
||||
"next": "^14.1.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "4.0.379",
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
@@ -50,8 +50,8 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-player": "^2.14.1",
|
||||
"react-resizable-panels": "^2.0.11",
|
||||
"react-player": "^2.15.1",
|
||||
"react-resizable-panels": "^2.0.13",
|
||||
"react-timeago": "^7.2.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sharp": "^0.33.2",
|
||||
@@ -60,26 +60,26 @@
|
||||
"tiktoken": "^1.0.13",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.1"
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/puppeteer": "^0.0.5",
|
||||
"@types/node": "^20.11.20",
|
||||
"@cloudflare/puppeteer": "0.0.5",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.59",
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-csv": "^1.1.10",
|
||||
"@types/react-dom": "^18.2.19",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"eslint-config-next": "^14.1.4",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.10.2",
|
||||
"typescript": "^5.3.3"
|
||||
"prisma": "^5.11.0",
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^18.0.0"
|
||||
|
||||
+11
-10
@@ -13,11 +13,11 @@ import '~/common/styles/GithubMarkdown.css';
|
||||
import '~/common/styles/NProgress.css';
|
||||
import '~/common/styles/app.styles.css';
|
||||
|
||||
import { ProviderBackendAndNoSSR } from '~/common/providers/ProviderBackendAndNoSSR';
|
||||
import { ProviderBackendCapabilities } from '~/common/providers/ProviderBackendCapabilities';
|
||||
import { ProviderBootstrapLogic } from '~/common/providers/ProviderBootstrapLogic';
|
||||
import { ProviderSingleTab } from '~/common/providers/ProviderSingleTab';
|
||||
import { ProviderSnacks } from '~/common/providers/ProviderSnacks';
|
||||
import { ProviderTRPCQueryClient } from '~/common/providers/ProviderTRPCQueryClient';
|
||||
import { ProviderTRPCQuerySettings } from '~/common/providers/ProviderTRPCQuerySettings';
|
||||
import { ProviderTheming } from '~/common/providers/ProviderTheming';
|
||||
import { hasGoogleAnalytics, OptionalGoogleAnalytics } from '~/common/components/GoogleAnalytics';
|
||||
import { isVercelFromFrontend } from '~/common/util/pwaUtils';
|
||||
@@ -33,15 +33,16 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
|
||||
|
||||
<ProviderTheming emotionCache={emotionCache}>
|
||||
<ProviderSingleTab>
|
||||
<ProviderBootstrapLogic>
|
||||
<ProviderTRPCQueryClient>
|
||||
<ProviderSnacks>
|
||||
<ProviderBackendAndNoSSR>
|
||||
<ProviderTRPCQuerySettings>
|
||||
<ProviderBackendCapabilities>
|
||||
{/* ^ SSR boundary */}
|
||||
<ProviderBootstrapLogic>
|
||||
<ProviderSnacks>
|
||||
<Component {...pageProps} />
|
||||
</ProviderBackendAndNoSSR>
|
||||
</ProviderSnacks>
|
||||
</ProviderTRPCQueryClient>
|
||||
</ProviderBootstrapLogic>
|
||||
</ProviderSnacks>
|
||||
</ProviderBootstrapLogic>
|
||||
</ProviderBackendCapabilities>
|
||||
</ProviderTRPCQuerySettings>
|
||||
</ProviderSingleTab>
|
||||
</ProviderTheming>
|
||||
|
||||
|
||||
+1
-1
@@ -26,7 +26,7 @@ export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
|
||||
<link rel='icon' type='image/png' sizes='16x16' href='/icons/favicon-16x16.png' />
|
||||
<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
<meta name='mobile-web-app-capable' content='yes' />
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='black' />
|
||||
|
||||
{/* Opengraph */}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppBeam } from '../../src/apps/beam/AppBeam';
|
||||
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function BeamPage() {
|
||||
return withLayout({ type: 'optima' }, <AppBeam />);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import DownloadIcon from '@mui/icons-material/Download';
|
||||
|
||||
import { AppPlaceholder } from '../../src/apps/AppPlaceholder';
|
||||
|
||||
import { backendCaps } from '~/modules/backend/state-backend';
|
||||
import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities';
|
||||
import { getPlantUmlServerUrl } from '~/modules/blocks/code/RenderCode';
|
||||
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
@@ -76,7 +76,7 @@ function AppDebug() {
|
||||
const [saved, setSaved] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const backendCapabilities = backendCaps();
|
||||
const backendCaps = getBackendCapabilities();
|
||||
const chatsCount = useChatStore.getState().conversations?.length;
|
||||
const uxLabsExperiments = Object.entries(useUXLabsStore.getState()).filter(([_k, v]) => v === true).map(([k, _]) => k).join(', ');
|
||||
const { folders, enableFolders } = useFolderStore.getState();
|
||||
@@ -112,7 +112,7 @@ function AppDebug() {
|
||||
},
|
||||
};
|
||||
const cBackend = {
|
||||
configuration: backendCapabilities,
|
||||
configuration: backendCaps,
|
||||
deployment: {
|
||||
home: Brand.URIs.Home,
|
||||
hostName: clientHostName(),
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 348 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 180 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
+17
-15
@@ -10,7 +10,7 @@ import { useRouterRoute } from '~/common/app.routes';
|
||||
* https://github.com/enricoros/big-AGI/issues/299
|
||||
*/
|
||||
export function AppPlaceholder(props: {
|
||||
title?: string,
|
||||
title?: string | null,
|
||||
text?: React.ReactNode,
|
||||
children?: React.ReactNode,
|
||||
}) {
|
||||
@@ -29,23 +29,25 @@ export function AppPlaceholder(props: {
|
||||
border: '1px solid blue',
|
||||
}}>
|
||||
|
||||
<Box sx={{
|
||||
my: 'auto',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
gap: 4,
|
||||
border: '1px solid red',
|
||||
}}>
|
||||
{(props.title !== null || !!props.text) && (
|
||||
<Box sx={{
|
||||
my: 'auto',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
gap: 4,
|
||||
border: '1px solid red',
|
||||
}}>
|
||||
|
||||
<Typography level='h1'>
|
||||
{placeholderAppName}
|
||||
</Typography>
|
||||
{!!props.text && (
|
||||
<Typography>
|
||||
{props.text}
|
||||
<Typography level='h1'>
|
||||
{placeholderAppName}
|
||||
</Typography>
|
||||
)}
|
||||
{!!props.text && (
|
||||
<Typography>
|
||||
{props.text}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{props.children}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, Button, Typography } from '@mui/joy';
|
||||
|
||||
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
|
||||
import { BeamView } from '~/modules/beam/BeamView';
|
||||
import { createBeamVanillaStore } from '~/modules/beam/store-beam-vanilla';
|
||||
import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { createDConversation, createDMessage, DConversation, DMessage } from '~/common/state/store-chats';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
|
||||
function initTestConversation(): DConversation {
|
||||
const conversation = createDConversation();
|
||||
conversation.messages.push(createDMessage('system', 'You are a helpful assistant.'));
|
||||
conversation.messages.push(createDMessage('user', 'Hello, who are you? (please expand...)'));
|
||||
return conversation;
|
||||
}
|
||||
|
||||
function initTestBeamStore(messages: DMessage[], beamStore: BeamStoreApi = createBeamVanillaStore()): BeamStoreApi {
|
||||
beamStore.getState().open(messages, useModelsStore.getState().chatLLMId, (text) => alert(text));
|
||||
return beamStore;
|
||||
}
|
||||
|
||||
|
||||
export function AppBeam() {
|
||||
|
||||
// state
|
||||
const [showDebug, setShowDebug] = React.useState(false);
|
||||
const conversation = React.useRef<DConversation>(initTestConversation());
|
||||
const beamStoreApi = React.useRef(initTestBeamStore(conversation.current.messages)).current;
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const { isOpen, beamState } = useBeamStore(beamStoreApi, useShallow(state => {
|
||||
return {
|
||||
isOpen: state.isOpen,
|
||||
beamState: showDebug ? state : null,
|
||||
};
|
||||
}));
|
||||
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
beamStoreApi.getState().terminate();
|
||||
}, [beamStoreApi]);
|
||||
|
||||
|
||||
// layout
|
||||
usePluggableOptimaLayout(null, React.useMemo(() => <>
|
||||
{/* button to toggle debug info */}
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={() => setShowDebug(on => !on)}>
|
||||
{showDebug ? 'Hide' : 'Show'} debug
|
||||
</Button>
|
||||
|
||||
{/* 'open' */}
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={() => {
|
||||
conversation.current = initTestConversation();
|
||||
initTestBeamStore(conversation.current.messages, beamStoreApi);
|
||||
}}>
|
||||
.open
|
||||
</Button>
|
||||
|
||||
{/* 'close' */}
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={handleClose}>
|
||||
.close
|
||||
</Button>
|
||||
</>, [beamStoreApi, handleClose, showDebug]), null, 'AppBeam');
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ flexGrow: 1, overflowY: 'auto', position: 'relative' }}>
|
||||
|
||||
{isOpen && (
|
||||
<BeamView
|
||||
beamStore={beamStoreApi}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showDebug && (
|
||||
<Typography level='body-xs' sx={{
|
||||
whiteSpace: 'pre',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1 /* debug on top of BeamView */,
|
||||
backdropFilter: 'blur(4px)',
|
||||
padding: '1rem',
|
||||
}}>
|
||||
{JSON.stringify(beamState, null, 2)
|
||||
// add an extra newline between first level properties (space, space, double quote) to make it more readable
|
||||
.split('\n').map(line => line.replace(/^\s\s"/g, '\n ')).join('\n')}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,60 +1,22 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, Card, CardContent, IconButton, ListItemDecorator, Typography } from '@mui/joy';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import ChatIcon from '@mui/icons-material/Chat';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import MicIcon from '@mui/icons-material/Mic';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { cssRainbowColorKeyframes } from '~/common/app.theme';
|
||||
import { animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { navigateBack } from '~/common/app.routes';
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
import { useUICounter } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
/*export const cssRainbowBackgroundKeyframes = keyframes`
|
||||
100%, 0% {
|
||||
background-color: rgb(128, 0, 0);
|
||||
}
|
||||
8% {
|
||||
background-color: rgb(102, 51, 0);
|
||||
}
|
||||
16% {
|
||||
background-color: rgb(64, 64, 0);
|
||||
}
|
||||
25% {
|
||||
background-color: rgb(38, 76, 0);
|
||||
}
|
||||
33% {
|
||||
background-color: rgb(0, 89, 0);
|
||||
}
|
||||
41% {
|
||||
background-color: rgb(0, 76, 41);
|
||||
}
|
||||
50% {
|
||||
background-color: rgb(0, 64, 64);
|
||||
}
|
||||
58% {
|
||||
background-color: rgb(0, 51, 102);
|
||||
}
|
||||
66% {
|
||||
background-color: rgb(0, 0, 128);
|
||||
}
|
||||
75% {
|
||||
background-color: rgb(63, 0, 128);
|
||||
}
|
||||
83% {
|
||||
background-color: rgb(76, 0, 76);
|
||||
}
|
||||
91% {
|
||||
background-color: rgb(102, 0, 51);
|
||||
}`;*/
|
||||
|
||||
function StatusCard(props: { icon: React.JSX.Element, hasIssue: boolean, text: string, button?: React.JSX.Element }) {
|
||||
return (
|
||||
<Card sx={{ width: '100%' }}>
|
||||
@@ -67,7 +29,7 @@ function StatusCard(props: { icon: React.JSX.Element, hasIssue: boolean, text: s
|
||||
{props.button}
|
||||
</Typography>
|
||||
<ListItemDecorator>
|
||||
{props.hasIssue ? <WarningRoundedIcon color='warning' /> : <CheckIcon color='success' />}
|
||||
{props.hasIssue ? <WarningRoundedIcon color='warning' /> : <CheckRoundedIcon color='success' />}
|
||||
</ListItemDecorator>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -124,7 +86,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
|
||||
<Typography level='title-lg' sx={{ fontSize: '3rem', fontWeight: 'sm', textAlign: 'center' }}>
|
||||
Welcome to<br />
|
||||
<Box component='span' sx={{ animation: `${cssRainbowColorKeyframes} 15s linear infinite` }}>
|
||||
<Box component='span' sx={{ animation: `${animationColorRainbow} 15s linear infinite` }}>
|
||||
your first call
|
||||
</Box>
|
||||
</Typography>
|
||||
@@ -167,7 +129,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
|
||||
{/* Text to Speech status */}
|
||||
<StatusCard
|
||||
icon={<RecordVoiceOverIcon />}
|
||||
icon={<RecordVoiceOverTwoToneIcon />}
|
||||
text={
|
||||
(synthesis.mayWork ? 'Voice synthesis should be ready.' : 'There might be an issue with ElevenLabs voice synthesis.')
|
||||
+ (synthesis.isConfiguredServerSide ? '' : (synthesis.isConfiguredClientSide ? '' : ' Please add your API key in the settings.'))
|
||||
@@ -208,7 +170,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
|
||||
// boxShadow: allGood ? 'md' : 'none',
|
||||
}}
|
||||
>
|
||||
{allGood ? <ArrowForwardIcon sx={{ fontSize: '1.5em' }} /> : <CloseRoundedIcon sx={{ fontSize: '1.5em' }} />}
|
||||
{allGood ? <ArrowForwardRoundedIcon sx={{ fontSize: '1.5em' }} /> : <CloseRoundedIcon sx={{ fontSize: '1.5em' }} />}
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Avatar, Box, Card, CardContent, Chip, IconButton, Link as MuiLink, ListDivider, MenuItem, Sheet, Switch, Typography } from '@mui/joy';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
|
||||
import { GitHubProjectIssueCard } from '~/common/components/GitHubProjectIssueCard';
|
||||
import { animationShadowRingLimey } from '~/common/util/animUtils';
|
||||
import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
@@ -19,27 +19,6 @@ import { useAppCallStore } from './state/store-app-call';
|
||||
const COLLAPSED_COUNT = 2;
|
||||
|
||||
|
||||
export const niceShadowKeyframes = keyframes`
|
||||
100%, 0% {
|
||||
//background-color: rgb(102, 0, 51);
|
||||
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(183, 255, 0);
|
||||
}
|
||||
25% {
|
||||
//background-color: rgb(76, 0, 76);
|
||||
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(255, 251, 0);
|
||||
//scale: 1.2;
|
||||
}
|
||||
50% {
|
||||
//background-color: rgb(63, 0, 128);
|
||||
box-shadow: 1px 1px 0 white, 2px 2px 12px rgba(0, 255, 81);
|
||||
//scale: 0.8;
|
||||
}
|
||||
75% {
|
||||
//background-color: rgb(0, 0, 128);
|
||||
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(255, 153, 0);
|
||||
}`;
|
||||
|
||||
|
||||
const ContactCardAvatar = (props: { size: string, symbol?: string, imageUrl?: string, onClick?: () => void, sx?: SxProps }) =>
|
||||
<Avatar
|
||||
// variant='outlined'
|
||||
@@ -125,7 +104,6 @@ function CallContactCard(props: {
|
||||
sx={{
|
||||
mx: 'auto',
|
||||
mt: '-2.5rem',
|
||||
zIndex: 1,
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -282,7 +260,7 @@ export function Contacts(props: { setCallIntent: (intent: AppCallIntent) => void
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: 'background.popup',
|
||||
animation: `${niceShadowKeyframes} 5s infinite`,
|
||||
animation: `${animationShadowRingLimey} 5s infinite`,
|
||||
}}>
|
||||
<CallIcon />
|
||||
</IconButton>
|
||||
|
||||
@@ -7,10 +7,10 @@ import CallEndIcon from '@mui/icons-material/CallEnd';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
import MicIcon from '@mui/icons-material/Mic';
|
||||
import MicNoneIcon from '@mui/icons-material/MicNone';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
|
||||
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '../chat/components/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { useChatLLMDropdown } from '../chat/components/useLLMDropdown';
|
||||
|
||||
import { EXPERIMENTAL_speakTextStream } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
@@ -57,7 +57,7 @@ function CallMenuItems(props: {
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem onClick={handleChangeVoiceToggle}>
|
||||
<ListItemDecorator><RecordVoiceOverIcon /></ListItemDecorator>
|
||||
<ListItemDecorator><RecordVoiceOverTwoToneIcon /></ListItemDecorator>
|
||||
Change Voice
|
||||
<Switch checked={props.override} onChange={handleChangeVoiceToggle} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
@@ -331,22 +331,9 @@ export function Telephone(props: {
|
||||
padding: 0, // move this to the ScrollToBottom component
|
||||
}}>
|
||||
|
||||
<ScrollToBottom
|
||||
// bootToBottom
|
||||
stickToBottom
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
<ScrollToBottom stickToBottomInitial>
|
||||
|
||||
// content
|
||||
display: 'grid',
|
||||
padding: 1,
|
||||
}}
|
||||
>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Box sx={{ minHeight: '100%', p: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
{/* Call Messages [] */}
|
||||
{callMessages.map((message) =>
|
||||
|
||||
@@ -1,19 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import { Avatar, Box } from '@mui/joy';
|
||||
|
||||
|
||||
const cssScaleKeyframes = keyframes`
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}`;
|
||||
import { animationScalePulse } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
export function CallAvatar(props: { symbol: string, imageUrl?: string, isRinging?: boolean, onClick: () => void }) {
|
||||
@@ -34,7 +23,7 @@ export function CallAvatar(props: { symbol: string, imageUrl?: string, isRinging
|
||||
<Box
|
||||
sx={{
|
||||
...(props.isRinging
|
||||
? { animation: `${cssScaleKeyframes} 1.4s ease-in-out infinite` }
|
||||
? { animation: `${animationScalePulse} 1.4s ease-in-out infinite` }
|
||||
: {}),
|
||||
}}
|
||||
>
|
||||
|
||||
+207
-152
@@ -9,13 +9,15 @@ import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
|
||||
import { getChatLLMId, useChatLLM } from '~/modules/llms/store-llms';
|
||||
import { imaginePromptFromText } from '~/modules/aifn/imagine/imaginePromptFromText';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { useAreBeamsOpen } from '~/modules/beam/store-beam.hooks';
|
||||
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, getConversationSystemPurposeId, useConversation } from '~/common/state/store-chats';
|
||||
import { getUXLabsHighPerformance, useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
@@ -23,20 +25,21 @@ import { themeBgAppChatComposer } from '~/common/app.theme';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useRouterQuery } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
|
||||
import { Beam } from './components/beam/Beam';
|
||||
import { ChatBarAltBeam } from './components/ChatBarAltBeam';
|
||||
import { ChatBarAltTitle } from './components/ChatBarAltTitle';
|
||||
import { ChatBarDropdowns } from './components/ChatBarDropdowns';
|
||||
import { ChatBeamWrapper } from './components/ChatBeamWrapper';
|
||||
import { ChatDrawerMemo } from './components/ChatDrawer';
|
||||
import { ChatDropdowns } from './components/ChatDropdowns';
|
||||
import { ChatMessageList } from './components/ChatMessageList';
|
||||
import { ChatPageMenuItems } from './components/ChatPageMenuItems';
|
||||
import { ChatTitle } from './components/ChatTitle';
|
||||
import { Composer } from './components/composer/Composer';
|
||||
import { ScrollToBottom } from './components/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from './components/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { getInstantAppChatPanesCount, usePanesManager } from './components/panes/usePanesManager';
|
||||
|
||||
import { DEV_MODE_SETTINGS } from '../settings-modal/UxLabsSettings';
|
||||
import { extractChatCommand, findAllChatCommands } from './commands/commands.registry';
|
||||
import { runAssistantUpdatingState } from './editors/chat-stream';
|
||||
import { runBrowseGetPageUpdatingState } from './editors/browse-load';
|
||||
@@ -59,6 +62,11 @@ export type ChatModeId =
|
||||
| 'generate-react';
|
||||
|
||||
|
||||
export interface AppChatIntent {
|
||||
initialConversationId: string | null;
|
||||
}
|
||||
|
||||
|
||||
export function AppChat() {
|
||||
|
||||
// state
|
||||
@@ -78,35 +86,55 @@ export function AppChat() {
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const showAltTitleBar = useUXLabsStore(state => state.labsChatBarAlt === 'title');
|
||||
const intent = useRouterQuery<Partial<AppChatIntent>>();
|
||||
|
||||
const showAltTitleBar = useUXLabsStore(state => DEV_MODE_SETTINGS && state.labsChatBarAlt === 'title');
|
||||
|
||||
const { openLlmOptions } = useOptimaLayout();
|
||||
|
||||
const { chatLLM } = useChatLLM();
|
||||
|
||||
const {
|
||||
// state
|
||||
chatPanes,
|
||||
focusedConversationId,
|
||||
focusedPaneIndex,
|
||||
focusedPaneConversationId,
|
||||
// actions
|
||||
navigateHistoryInFocusedPane,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
focusedPaneIndex,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
setFocusedPaneIndex,
|
||||
} = usePanesManager();
|
||||
|
||||
const chatHandlers = React.useMemo(() => chatPanes.map(pane => {
|
||||
return pane.conversationId ? ConversationsManager.getHandler(pane.conversationId) : null;
|
||||
}), [chatPanes]);
|
||||
|
||||
const beamsStores = React.useMemo(() => chatHandlers.map(handler => {
|
||||
return handler?.getBeamStore() ?? null;
|
||||
}), [chatHandlers]);
|
||||
|
||||
const beamsOpens = useAreBeamsOpen(beamsStores);
|
||||
const beamOpenStoreInFocusedPane = React.useMemo(() => {
|
||||
const open = focusedPaneIndex !== null ? (beamsOpens?.[focusedPaneIndex] ?? false) : false;
|
||||
return open ? beamsStores?.[focusedPaneIndex!] ?? null : null;
|
||||
}, [beamsOpens, beamsStores, focusedPaneIndex]);
|
||||
|
||||
const {
|
||||
// focused
|
||||
title: focusedChatTitle,
|
||||
isChatEmpty: isFocusedChatEmpty,
|
||||
isEmpty: isFocusedChatEmpty,
|
||||
isDeveloper: isFocusedChatDeveloper,
|
||||
areChatsEmpty,
|
||||
conversationIdx: focusedChatNumber,
|
||||
newConversationId,
|
||||
// all
|
||||
hasConversations,
|
||||
recycleNewConversationId,
|
||||
// actions
|
||||
prependNewConversation,
|
||||
branchConversation,
|
||||
deleteConversations,
|
||||
setMessages,
|
||||
} = useConversation(focusedConversationId);
|
||||
} = useConversation(focusedPaneConversationId);
|
||||
|
||||
const { mayWork: capabilityHasT2I } = useCapabilityTextToImage();
|
||||
|
||||
@@ -127,23 +155,25 @@ export function AppChat() {
|
||||
const willMulticast = isComposerMulticast && isMultiConversationId;
|
||||
const disableNewButton = isFocusedChatEmpty && !isMultiPane;
|
||||
|
||||
const chatHandlers = React.useMemo(() => chatPanes.map(pane => {
|
||||
return pane.conversationId ? ConversationManager.getHandler(pane.conversationId) : null;
|
||||
}), [chatPanes]);
|
||||
|
||||
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
|
||||
const handleOpenConversationInFocusedPane = React.useCallback((conversationId: DConversationId | null) => {
|
||||
conversationId && openConversationInFocusedPane(conversationId);
|
||||
}, [openConversationInFocusedPane]);
|
||||
|
||||
const openSplitConversationId = React.useCallback((conversationId: DConversationId | null) => {
|
||||
const handleOpenConversationInSplitPane = React.useCallback((conversationId: DConversationId | null) => {
|
||||
conversationId && openConversationInSplitPane(conversationId);
|
||||
}, [openConversationInSplitPane]);
|
||||
|
||||
const handleNavigateHistory = React.useCallback((direction: 'back' | 'forward') => {
|
||||
const handleNavigateHistoryInFocusedPane = React.useCallback((direction: 'back' | 'forward') => {
|
||||
if (navigateHistoryInFocusedPane(direction))
|
||||
showNextTitleChange.current = true;
|
||||
}, [navigateHistoryInFocusedPane]);
|
||||
|
||||
// [effect] Handle the initial conversation intent
|
||||
React.useEffect(() => {
|
||||
intent.initialConversationId && handleOpenConversationInFocusedPane(intent.initialConversationId);
|
||||
}, [handleOpenConversationInFocusedPane, intent.initialConversationId]);
|
||||
|
||||
// [effect] Show snackbar with the focused chat title after a history navigation in focused pane
|
||||
React.useEffect(() => {
|
||||
if (showNextTitleChange.current) {
|
||||
showNextTitleChange.current = false;
|
||||
@@ -160,95 +190,110 @@ export function AppChat() {
|
||||
const chatLLMId = getChatLLMId();
|
||||
if (!chatModeId || !conversationId || !chatLLMId) return;
|
||||
|
||||
// "/command ...": overrides the chat mode
|
||||
// Update the system message from the active persona to the history
|
||||
// NOTE: this does NOT call setMessages anymore (optimization). make sure to:
|
||||
// 1. all the callers need to pass a new array
|
||||
// 2. all the exit points need to call setMessages
|
||||
const cHandler = ConversationsManager.getHandler(conversationId);
|
||||
cHandler.inlineUpdatePurposeInHistory(history, chatLLMId);
|
||||
|
||||
// Valid /commands are intercepted here, and override chat modes, generally for mechanics or sidebars
|
||||
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
|
||||
if (lastMessage?.role === 'user') {
|
||||
const chatCommand = extractChatCommand(lastMessage.text)[0];
|
||||
if (chatCommand && chatCommand.type === 'cmd') {
|
||||
switch (chatCommand.providerId) {
|
||||
case 'ass-beam':
|
||||
return ConversationManager.getHandler(conversationId).beamStore.create(history);
|
||||
|
||||
case 'ass-browse':
|
||||
setMessages(conversationId, history);
|
||||
return await runBrowseGetPageUpdatingState(conversationId, chatCommand.params!);
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runBrowseGetPageUpdatingState(cHandler, chatCommand.params);
|
||||
|
||||
case 'ass-t2i':
|
||||
setMessages(conversationId, history);
|
||||
return await runImageGenerationUpdatingState(conversationId, chatCommand.params!);
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runImageGenerationUpdatingState(cHandler, chatCommand.params);
|
||||
|
||||
case 'ass-react':
|
||||
setMessages(conversationId, history);
|
||||
return await runReActUpdatingState(conversationId, chatCommand.params!, chatLLMId);
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runReActUpdatingState(cHandler, chatCommand.params, chatLLMId);
|
||||
|
||||
case 'chat-alter':
|
||||
// /clear
|
||||
if (chatCommand.command === '/clear') {
|
||||
if (chatCommand.params === 'all')
|
||||
return setMessages(conversationId, []);
|
||||
const helpMessage = createDMessage('assistant', 'This command requires the \'all\' parameter to confirm the operation.');
|
||||
helpMessage.originLLM = Brand.Title.Base;
|
||||
return setMessages(conversationId, [...history, helpMessage]);
|
||||
return cHandler.messagesReplace([]);
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Issue: this command requires the \'all\' parameter to confirm the operation.', undefined, 'issue', false);
|
||||
return;
|
||||
}
|
||||
// /assistant, /system
|
||||
Object.assign(lastMessage, {
|
||||
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
|
||||
sender: 'Bot',
|
||||
text: chatCommand.params || '',
|
||||
} satisfies Partial<DMessage>);
|
||||
return setMessages(conversationId, history);
|
||||
return cHandler.messagesReplace(history);
|
||||
|
||||
case 'cmd-help':
|
||||
const chatCommandsText = findAllChatCommands()
|
||||
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
|
||||
.join('\n');
|
||||
const helpMessage = createDMessage('assistant', 'Available Chat Commands:\n' + chatCommandsText);
|
||||
helpMessage.originLLM = Brand.Title.Base;
|
||||
return setMessages(conversationId, [...history, helpMessage]);
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Available Chat Commands:\n' + chatCommandsText, undefined, 'help', false);
|
||||
return;
|
||||
|
||||
case 'mode-beam':
|
||||
if (chatCommand.isError)
|
||||
return cHandler.messagesReplace(history);
|
||||
// remove '/beam ', as we want to be a user chat message
|
||||
Object.assign(lastMessage, { text: chatCommand.params || '' });
|
||||
cHandler.messagesReplace(history);
|
||||
return ConversationsManager.getHandler(conversationId).beamInvoke(history, [], null);
|
||||
|
||||
default:
|
||||
return setMessages(conversationId, [...history, createDMessage('assistant', 'This command is not supported.')]);
|
||||
return cHandler.messagesReplace([...history, createDMessage('assistant', 'This command is not supported.')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// get the focused system purpose (note: we don't react to it, or it would invalidate half UI components..)
|
||||
const conversationSystemPurposeId = getConversationSystemPurposeId(conversationId);
|
||||
if (!conversationSystemPurposeId)
|
||||
return setMessages(conversationId, [...history, createDMessage('assistant', 'No persona selected.')]);
|
||||
|
||||
// get the system purpose (note: we don't react to it, or it would invalidate half UI components..)
|
||||
if (!getConversationSystemPurposeId(conversationId)) {
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Issue: no Persona selected.', undefined, 'issue', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// synchronous long-duration tasks, which update the state as they go
|
||||
if (chatLLMId) {
|
||||
switch (chatModeId) {
|
||||
case 'generate-text':
|
||||
return await runAssistantUpdatingState(conversationId, history, chatLLMId, conversationSystemPurposeId, getUXLabsHighPerformance() ? 0 : getInstantAppChatPanesCount());
|
||||
switch (chatModeId) {
|
||||
case 'generate-text':
|
||||
cHandler.messagesReplace(history);
|
||||
return await runAssistantUpdatingState(conversationId, history, chatLLMId, getUXLabsHighPerformance() ? 0 : getInstantAppChatPanesCount());
|
||||
|
||||
case 'generate-text-beam':
|
||||
return ConversationManager.getHandler(conversationId).beamStore.create(history);
|
||||
case 'generate-text-beam':
|
||||
cHandler.messagesReplace(history);
|
||||
return cHandler.beamInvoke(history, [], null);
|
||||
|
||||
case 'append-user':
|
||||
return setMessages(conversationId, history);
|
||||
case 'append-user':
|
||||
return cHandler.messagesReplace(history);
|
||||
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
// also add a 'fake' user message with the '/draw' command
|
||||
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
|
||||
...message,
|
||||
text: `/draw ${lastMessage.text}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(conversationId, lastMessage.text);
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text) break;
|
||||
// also add a 'fake' user message with the '/draw' command
|
||||
cHandler.messagesReplace(history.map(message => (message.id !== lastMessage.id) ? message : {
|
||||
...message,
|
||||
text: `/draw ${lastMessage.text}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(cHandler, lastMessage.text);
|
||||
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
setMessages(conversationId, history);
|
||||
return await runReActUpdatingState(conversationId, lastMessage.text, chatLLMId);
|
||||
}
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text) break;
|
||||
cHandler.messagesReplace(history);
|
||||
return await runReActUpdatingState(cHandler, lastMessage.text, chatLLMId);
|
||||
}
|
||||
|
||||
// ISSUE: if we're here, it means we couldn't do the job, at least sync the history
|
||||
console.log('handleExecuteConversation: issue running', chatModeId, conversationId, lastMessage);
|
||||
setMessages(conversationId, history);
|
||||
}, [setMessages]);
|
||||
console.log('Chat execute: issue running', chatModeId, conversationId, lastMessage);
|
||||
cHandler.messagesReplace(history);
|
||||
}, []);
|
||||
|
||||
const handleComposerAction = React.useCallback((chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart): boolean => {
|
||||
// validate inputs
|
||||
@@ -283,20 +328,30 @@ export function AppChat() {
|
||||
return enqueued;
|
||||
}, [chatPanes, willMulticast, _handleExecute]);
|
||||
|
||||
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[], chatEffectBeam: boolean): Promise<void> => {
|
||||
await _handleExecute(!chatEffectBeam ? 'generate-text' : 'generate-text-beam', conversationId, history);
|
||||
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[]): Promise<void> => {
|
||||
await _handleExecute('generate-text', conversationId, history);
|
||||
}, [_handleExecute]);
|
||||
|
||||
const handleMessageRegenerateLast = React.useCallback(async () => {
|
||||
const focusedConversation = getConversation(focusedConversationId);
|
||||
const handleMessageRegenerateLastInFocusedPane = React.useCallback(async () => {
|
||||
const focusedConversation = getConversation(focusedPaneConversationId);
|
||||
if (focusedConversation?.messages?.length) {
|
||||
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
|
||||
return await _handleExecute('generate-text', focusedConversation.id, lastMessage.role === 'assistant'
|
||||
? focusedConversation.messages.slice(0, -1)
|
||||
: [...focusedConversation.messages],
|
||||
);
|
||||
const history = lastMessage.role === 'assistant' ? focusedConversation.messages.slice(0, -1) : [...focusedConversation.messages];
|
||||
return await _handleExecute('generate-text', focusedConversation.id, history);
|
||||
}
|
||||
}, [focusedConversationId, _handleExecute]);
|
||||
}, [_handleExecute, focusedPaneConversationId]);
|
||||
|
||||
const handleMessageBeamLastInFocusedPane = React.useCallback(async () => {
|
||||
// Ctrl + Shift + B
|
||||
const focusedConversation = getConversation(focusedPaneConversationId);
|
||||
if (focusedConversation?.messages?.length) {
|
||||
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
|
||||
if (lastMessage.role === 'assistant')
|
||||
ConversationsManager.getHandler(focusedConversation.id).beamInvoke(focusedConversation.messages.slice(0, -1), [lastMessage], lastMessage.id);
|
||||
else if (lastMessage.role === 'user')
|
||||
ConversationsManager.getHandler(focusedConversation.id).beamInvoke(focusedConversation.messages, [], null);
|
||||
}
|
||||
}, [focusedPaneConversationId]);
|
||||
|
||||
const handleTextDiagram = React.useCallback((diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig), []);
|
||||
|
||||
@@ -318,13 +373,15 @@ export function AppChat() {
|
||||
|
||||
// Chat actions
|
||||
|
||||
const handleConversationNew = React.useCallback((forceNoRecycle?: boolean) => {
|
||||
const handleConversationNewInFocusedPane = React.useCallback((forceNoRecycle?: boolean) => {
|
||||
|
||||
// activate an existing new conversation if present, or create another
|
||||
const conversationId = (newConversationId && !forceNoRecycle)
|
||||
? newConversationId
|
||||
: prependNewConversation(getConversationSystemPurposeId(focusedConversationId) ?? undefined);
|
||||
setFocusedConversationId(conversationId);
|
||||
// create conversation (or recycle the existing top-of-stack empty conversation)
|
||||
const conversationId = (recycleNewConversationId && !forceNoRecycle)
|
||||
? recycleNewConversationId
|
||||
: prependNewConversation(getConversationSystemPurposeId(focusedPaneConversationId) ?? undefined);
|
||||
|
||||
// switch the focused pane to the new conversation
|
||||
handleOpenConversationInFocusedPane(conversationId);
|
||||
|
||||
// if a folder is active, add the new conversation to the folder
|
||||
if (activeFolderId && conversationId)
|
||||
@@ -333,7 +390,7 @@ export function AppChat() {
|
||||
// focus the composer
|
||||
composerTextAreaRef.current?.focus();
|
||||
|
||||
}, [activeFolderId, focusedConversationId, newConversationId, prependNewConversation, setFocusedConversationId]);
|
||||
}, [activeFolderId, focusedPaneConversationId, handleOpenConversationInFocusedPane, prependNewConversation, recycleNewConversationId]);
|
||||
|
||||
const handleConversationImportDialog = React.useCallback(() => setTradeConfig({ dir: 'import' }), []);
|
||||
|
||||
@@ -351,22 +408,22 @@ export function AppChat() {
|
||||
|
||||
// replace/open a new pane with this
|
||||
showNextTitleChange.current = true;
|
||||
if (isMultiAddable)
|
||||
openSplitConversationId(branchedConversationId);
|
||||
if (!isMultiAddable)
|
||||
handleOpenConversationInFocusedPane(branchedConversationId);
|
||||
else
|
||||
setFocusedConversationId(branchedConversationId);
|
||||
handleOpenConversationInSplitPane(branchedConversationId);
|
||||
|
||||
return branchedConversationId;
|
||||
}, [activeFolderId, branchConversation, isMultiAddable, openSplitConversationId, setFocusedConversationId]);
|
||||
}, [activeFolderId, branchConversation, handleOpenConversationInFocusedPane, handleOpenConversationInSplitPane, isMultiAddable]);
|
||||
|
||||
const handleConversationFlatten = React.useCallback((conversationId: DConversationId) => setFlattenConversationId(conversationId), []);
|
||||
|
||||
const handleConfirmedClearConversation = React.useCallback(() => {
|
||||
if (clearConversationId) {
|
||||
setMessages(clearConversationId, []);
|
||||
ConversationsManager.getHandler(clearConversationId).messagesReplace([]);
|
||||
setClearConversationId(null);
|
||||
}
|
||||
}, [clearConversationId, setMessages]);
|
||||
}, [clearConversationId]);
|
||||
|
||||
const handleConversationClear = React.useCallback((conversationId: DConversationId) => setClearConversationId(conversationId), []);
|
||||
|
||||
@@ -374,13 +431,14 @@ export function AppChat() {
|
||||
if (!bypassConfirmation)
|
||||
return setDeleteConversationIds(conversationIds);
|
||||
|
||||
// perform deletion
|
||||
// perform deletion, and return the next (or a new) conversation
|
||||
const nextConversationId = deleteConversations(conversationIds, /*focusedSystemPurposeId ??*/ undefined);
|
||||
|
||||
setFocusedConversationId(nextConversationId);
|
||||
// switch the focused pane to the new conversation - NOTE: this makes the assumption that deletion had impact on the focused pane
|
||||
handleOpenConversationInFocusedPane(nextConversationId);
|
||||
|
||||
setDeleteConversationIds(null);
|
||||
}, [deleteConversations, setFocusedConversationId]);
|
||||
}, [deleteConversations, handleOpenConversationInFocusedPane]);
|
||||
|
||||
const handleConfirmedDeleteConversations = React.useCallback(() => {
|
||||
!!deleteConversationIds?.length && handleDeleteConversations(deleteConversationIds, true);
|
||||
@@ -396,17 +454,20 @@ export function AppChat() {
|
||||
}, [openLlmOptions]);
|
||||
|
||||
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
|
||||
// focused conversation
|
||||
['b', true, true, false, handleMessageBeamLastInFocusedPane],
|
||||
['r', true, true, false, handleMessageRegenerateLastInFocusedPane],
|
||||
['n', true, false, true, handleConversationNewInFocusedPane],
|
||||
['b', true, false, true, () => isFocusedChatEmpty || (focusedPaneConversationId && handleConversationBranch(focusedPaneConversationId, null))],
|
||||
['x', true, false, true, () => isFocusedChatEmpty || (focusedPaneConversationId && handleConversationClear(focusedPaneConversationId))],
|
||||
['d', true, false, true, () => focusedPaneConversationId && handleDeleteConversations([focusedPaneConversationId], false)],
|
||||
[ShortcutKeyName.Left, true, false, true, () => handleNavigateHistoryInFocusedPane('back')],
|
||||
[ShortcutKeyName.Right, true, false, true, () => handleNavigateHistoryInFocusedPane('forward')],
|
||||
// global
|
||||
['o', true, true, false, handleOpenChatLlmOptions],
|
||||
['r', true, true, false, handleMessageRegenerateLast],
|
||||
['n', true, false, true, handleConversationNew],
|
||||
['b', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationBranch(focusedConversationId, null))],
|
||||
['x', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationClear(focusedConversationId))],
|
||||
['d', true, false, true, () => focusedConversationId && handleDeleteConversations([focusedConversationId], false)],
|
||||
['+', true, true, false, useUIPreferencesStore.getState().increaseContentScaling],
|
||||
['-', true, true, false, useUIPreferencesStore.getState().decreaseContentScaling],
|
||||
[ShortcutKeyName.Left, true, false, true, () => handleNavigateHistory('back')],
|
||||
[ShortcutKeyName.Right, true, false, true, () => handleNavigateHistory('forward')],
|
||||
], [focusedConversationId, handleConversationBranch, handleConversationClear, handleConversationNew, handleDeleteConversations, handleMessageRegenerateLast, handleNavigateHistory, handleOpenChatLlmOptions, isFocusedChatEmpty]);
|
||||
], [focusedPaneConversationId, handleConversationBranch, handleConversationClear, handleConversationNewInFocusedPane, handleDeleteConversations, handleMessageBeamLastInFocusedPane, handleMessageRegenerateLastInFocusedPane, handleNavigateHistoryInFocusedPane, handleOpenChatLlmOptions, isFocusedChatEmpty]);
|
||||
useGlobalShortcuts(shortcuts);
|
||||
|
||||
|
||||
@@ -414,48 +475,50 @@ export function AppChat() {
|
||||
|
||||
const barAltTitle = showAltTitleBar ? focusedChatTitle ?? 'No Chat' : null;
|
||||
|
||||
const barContent = React.useMemo(() =>
|
||||
(barAltTitle === null)
|
||||
? <ChatDropdowns conversationId={focusedConversationId} />
|
||||
: <ChatTitle conversationId={focusedConversationId} conversationTitle={barAltTitle} />
|
||||
, [focusedConversationId, barAltTitle],
|
||||
const focusedBarContent = React.useMemo(() => beamOpenStoreInFocusedPane
|
||||
? <ChatBarAltBeam beamStore={beamOpenStoreInFocusedPane} isMobile={isMobile} />
|
||||
: (barAltTitle === null)
|
||||
? <ChatBarDropdowns conversationId={focusedPaneConversationId} />
|
||||
: <ChatBarAltTitle conversationId={focusedPaneConversationId} conversationTitle={barAltTitle} />
|
||||
, [barAltTitle, beamOpenStoreInFocusedPane, focusedPaneConversationId, isMobile],
|
||||
);
|
||||
|
||||
const drawerContent = React.useMemo(() =>
|
||||
<ChatDrawerMemo
|
||||
isMobile={isMobile}
|
||||
activeConversationId={focusedConversationId}
|
||||
activeConversationId={focusedPaneConversationId}
|
||||
activeFolderId={activeFolderId}
|
||||
chatPanesConversationIds={chatPanes.map(pane => pane.conversationId).filter(Boolean) as DConversationId[]}
|
||||
disableNewButton={disableNewButton}
|
||||
onConversationActivate={setFocusedConversationId}
|
||||
onConversationActivate={handleOpenConversationInFocusedPane}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationNew={handleConversationNew}
|
||||
onConversationNew={handleConversationNewInFocusedPane}
|
||||
onConversationsDelete={handleDeleteConversations}
|
||||
onConversationsExportDialog={handleConversationExport}
|
||||
onConversationsImportDialog={handleConversationImportDialog}
|
||||
setActiveFolderId={setActiveFolderId}
|
||||
/>,
|
||||
[activeFolderId, chatPanes, disableNewButton, focusedConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNew, handleDeleteConversations, isMobile, setFocusedConversationId],
|
||||
[activeFolderId, chatPanes, disableNewButton, focusedPaneConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNewInFocusedPane, handleDeleteConversations, handleOpenConversationInFocusedPane, isMobile],
|
||||
);
|
||||
|
||||
const menuItems = React.useMemo(() =>
|
||||
const focusedMenuItems = React.useMemo(() =>
|
||||
<ChatPageMenuItems
|
||||
isMobile={isMobile}
|
||||
conversationId={focusedConversationId}
|
||||
disableItems={!focusedConversationId || isFocusedChatEmpty}
|
||||
hasConversations={!areChatsEmpty}
|
||||
conversationId={focusedPaneConversationId}
|
||||
disableItems={!focusedPaneConversationId || isFocusedChatEmpty}
|
||||
hasConversations={hasConversations}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationClear={handleConversationClear}
|
||||
onConversationFlatten={handleConversationFlatten}
|
||||
// onConversationNew={handleConversationNew}
|
||||
// onConversationNew={handleConversationNewInFocusedPane}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
/>,
|
||||
[areChatsEmpty, focusedConversationId, handleConversationBranch, handleConversationClear, handleConversationFlatten, /*handleConversationNew,*/ isFocusedChatEmpty, isMessageSelectionMode, isMobile],
|
||||
[focusedPaneConversationId, handleConversationBranch, handleConversationClear, handleConversationFlatten, hasConversations, isFocusedChatEmpty, isMessageSelectionMode, isMobile],
|
||||
);
|
||||
|
||||
usePluggableOptimaLayout(drawerContent, barContent, menuItems, 'AppChat');
|
||||
usePluggableOptimaLayout(drawerContent, focusedBarContent, focusedMenuItems, 'AppChat');
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
@@ -465,11 +528,14 @@ export function AppChat() {
|
||||
>
|
||||
|
||||
{chatPanes.map((pane, idx) => {
|
||||
const _paneIsFocused = idx === focusedPaneIndex;
|
||||
const _paneConversationId = pane.conversationId;
|
||||
const _paneChatHandler = chatHandlers[idx] ?? null;
|
||||
const _paneChatBeamStore = beamsStores[idx] ?? null;
|
||||
const _paneChatBeamIsOpen = !!beamsOpens?.[idx];
|
||||
const _panesCount = chatPanes.length;
|
||||
const _keyAndId = `chat-pane-${idx}-${_paneConversationId}`;
|
||||
const _sepId = `sep-pane-${idx}-${_paneConversationId}`;
|
||||
const _keyAndId = `chat-pane-${pane.paneId}`;
|
||||
const _sepId = `sep-pane-${idx}`;
|
||||
return <React.Fragment key={_keyAndId}>
|
||||
|
||||
<Panel
|
||||
@@ -480,7 +546,7 @@ export function AppChat() {
|
||||
minSize={20}
|
||||
onClick={(event) => {
|
||||
const setFocus = chatPanes.length < 2 || !event.altKey;
|
||||
setFocusedPane(setFocus ? idx : -1);
|
||||
setFocusedPaneIndex(setFocus ? idx : -1);
|
||||
}}
|
||||
onCollapse={() => {
|
||||
// NOTE: despite the delay to try to let the draggin settle, there seems to be an issue with the Pane locking the screen
|
||||
@@ -493,12 +559,13 @@ export function AppChat() {
|
||||
position: 'relative',
|
||||
...(isMultiPane ? {
|
||||
borderRadius: '0.375rem',
|
||||
border: `2px solid ${idx === focusedPaneIndex
|
||||
border: `2px solid ${_paneIsFocused
|
||||
? ((willMulticast || !isMultiConversationId) ? theme.palette.primary.solidBg : theme.palette.primary.solidBg)
|
||||
: ((willMulticast || !isMultiConversationId) ? theme.palette.warning.softActiveBg : theme.palette.background.level1)}`,
|
||||
filter: (!willMulticast && idx !== focusedPaneIndex)
|
||||
? (!isMultiConversationId ? 'grayscale(66.67%)' /* clone of the same */ : 'grayscale(66.67%)')
|
||||
: undefined,
|
||||
: ((willMulticast || !isMultiConversationId) ? theme.palette.primary.softActiveBg : theme.palette.background.level1)}`,
|
||||
// DISABLED on 2024-03-13, it gets in the way quite a lot
|
||||
// filter: (!willMulticast && !_paneIsFocused)
|
||||
// ? (!isMultiConversationId ? 'grayscale(66.67%)' /* clone of the same */ : 'grayscale(66.67%)')
|
||||
// : undefined,
|
||||
} : {
|
||||
// NOTE: this is a workaround for the 'stuck-after-collapse-close' issue. We will collapse the 'other' pane, which
|
||||
// will get it removed (onCollapse), and somehow this pane will be stuck with a pointerEvents: 'none' style, which de-facto
|
||||
@@ -512,13 +579,8 @@ export function AppChat() {
|
||||
|
||||
<ScrollToBottom
|
||||
bootToBottom
|
||||
stickToBottom
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
}}
|
||||
stickToBottomInitial
|
||||
sx={_paneChatBeamIsOpen ? { display: 'none' } : undefined}
|
||||
>
|
||||
|
||||
<ChatMessageList
|
||||
@@ -555,18 +617,9 @@ export function AppChat() {
|
||||
|
||||
</ScrollToBottom>
|
||||
|
||||
{/* Best-Of Mode */}
|
||||
<Beam
|
||||
conversationHandler={_paneChatHandler}
|
||||
isMobile={isMobile}
|
||||
sx={{
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'background.level2',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1, // stay on top of Chips :shrug:
|
||||
}}
|
||||
/>
|
||||
{(_paneChatBeamIsOpen && !!_paneChatBeamStore) && (
|
||||
<ChatBeamWrapper beamStore={_paneChatBeamStore} isMobile={isMobile} />
|
||||
)}
|
||||
|
||||
</Panel>
|
||||
|
||||
@@ -586,15 +639,17 @@ export function AppChat() {
|
||||
isMobile={isMobile}
|
||||
chatLLM={chatLLM}
|
||||
composerTextAreaRef={composerTextAreaRef}
|
||||
conversationId={focusedConversationId}
|
||||
conversationId={focusedPaneConversationId}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
isMulticast={!isMultiConversationId ? null : isComposerMulticast}
|
||||
isDeveloperMode={isFocusedChatDeveloper}
|
||||
onAction={handleComposerAction}
|
||||
onTextImagine={handleTextImagine}
|
||||
setIsMulticast={setIsComposerMulticast}
|
||||
sx={{
|
||||
zIndex: 21, // position: 'sticky', bottom: 0,
|
||||
sx={beamOpenStoreInFocusedPane ? {
|
||||
display: 'none',
|
||||
} : {
|
||||
zIndex: 21, // just to allocate a surface, and potentially have a shadow
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
borderTop: `1px solid`,
|
||||
borderTopColor: 'divider',
|
||||
@@ -618,7 +673,7 @@ export function AppChat() {
|
||||
{!!tradeConfig && (
|
||||
<TradeModal
|
||||
config={tradeConfig}
|
||||
onConversationActivate={setFocusedConversationId}
|
||||
onConversationActivate={handleOpenConversationInFocusedPane}
|
||||
onClose={() => setTradeConfig(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { getUXLabsChatBeam } from '~/common/state/store-ux-labs';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsBeam: ICommandsProvider = {
|
||||
id: 'ass-beam',
|
||||
id: 'mode-beam',
|
||||
rank: 9,
|
||||
|
||||
getCommands: () => getUXLabsChatBeam() ? [{
|
||||
getCommands: () => useUXLabsStore.getState().labsBeam ? [{
|
||||
primary: '/beam',
|
||||
arguments: ['prompt'],
|
||||
description: 'Best of multiple replies',
|
||||
description: 'Combine the smarts of models',
|
||||
Icon: ChatBeamIcon,
|
||||
}] : [],
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
@@ -11,7 +11,7 @@ export const CommandsDraw: ICommandsProvider = {
|
||||
alternatives: ['/imagine', '/img'],
|
||||
arguments: ['prompt'],
|
||||
description: 'Assistant will draw the text',
|
||||
Icon: FormatPaintIcon,
|
||||
Icon: FormatPaintTwoToneIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import { CommandsHelp } from './CommandsHelp';
|
||||
import { CommandsReact } from './CommandsReact';
|
||||
|
||||
|
||||
export type CommandsProviderId = 'ass-beam' | 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help';
|
||||
export type CommandsProviderId = 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help' | 'mode-beam';
|
||||
|
||||
type TextCommandPiece =
|
||||
| { type: 'text'; value: string; }
|
||||
@@ -16,12 +16,12 @@ type TextCommandPiece =
|
||||
|
||||
|
||||
const ChatCommandsProviders: Record<CommandsProviderId, ICommandsProvider> = {
|
||||
'ass-beam': CommandsBeam,
|
||||
'ass-browse': CommandsBrowse,
|
||||
'ass-react': CommandsReact,
|
||||
'ass-t2i': CommandsDraw,
|
||||
'chat-alter': CommandsAlter,
|
||||
'cmd-help': CommandsHelp,
|
||||
'mode-beam': CommandsBeam,
|
||||
};
|
||||
|
||||
export function findAllChatCommands(): ChatCommand[] {
|
||||
@@ -40,7 +40,10 @@ export function extractChatCommand(input: string): TextCommandPiece[] {
|
||||
|
||||
// Find the first space to separate the command from its parameters (if any)
|
||||
const firstSpaceIndex = inputTrimmed.indexOf(' ');
|
||||
const potentialCommand = inputTrimmed.substring(0, firstSpaceIndex >= 0 ? firstSpaceIndex : inputTrimmed.length);
|
||||
const commandMatch = inputTrimmed.match(/^\/\S+/);
|
||||
const potentialCommand = commandMatch ? commandMatch[0] : inputTrimmed;
|
||||
|
||||
const textAfterCommand = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
|
||||
// Check if the potential command is an actual command
|
||||
for (const provider of Object.values(ChatCommandsProviders)) {
|
||||
@@ -48,22 +51,33 @@ export function extractChatCommand(input: string): TextCommandPiece[] {
|
||||
if (cmd.primary === potentialCommand || cmd.alternatives?.includes(potentialCommand)) {
|
||||
|
||||
// command needs arguments: take the rest of the input as parameters
|
||||
if (cmd.arguments?.length) {
|
||||
const params = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
return [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: params || undefined, isError: !params || undefined }];
|
||||
}
|
||||
if (cmd.arguments?.length) return [{
|
||||
type: 'cmd',
|
||||
providerId: provider.id,
|
||||
command: potentialCommand,
|
||||
params: textAfterCommand || undefined,
|
||||
isError: !textAfterCommand || undefined,
|
||||
}];
|
||||
|
||||
// command without arguments, treat any text after as a separate text piece
|
||||
const pieces: TextCommandPiece[] = [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: undefined }];
|
||||
const textAfterCommand = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
if (textAfterCommand)
|
||||
pieces.push({ type: 'text', value: textAfterCommand });
|
||||
const pieces: TextCommandPiece[] = [{
|
||||
type: 'cmd',
|
||||
providerId: provider.id,
|
||||
command: potentialCommand,
|
||||
params: undefined,
|
||||
}];
|
||||
textAfterCommand && pieces.push({
|
||||
type: 'text',
|
||||
value: textAfterCommand,
|
||||
});
|
||||
return pieces;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No command found, return the entire input as text
|
||||
return [{ type: 'text', value: input }];
|
||||
return [{
|
||||
type: 'text',
|
||||
value: input,
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
import * as React from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, IconButton, Typography } from '@mui/joy';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import FullscreenRoundedIcon from '@mui/icons-material/FullscreenRounded';
|
||||
|
||||
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
|
||||
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { animationBackgroundBeamGather, animationColorBeamScatterINV, animationEnterBelow } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
export function ChatBarAltBeam(props: {
|
||||
beamStore: BeamStoreApi,
|
||||
isMobile?: boolean
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [showCloseConfirmation, setShowCloseConfirmation] = React.useState(false);
|
||||
|
||||
|
||||
// external beam state
|
||||
const { isScattering, isGatheringAny, requiresConfirmation, setIsMaximized, terminateBeam } = useBeamStore(props.beamStore, useShallow((store) => ({
|
||||
// state
|
||||
isScattering: store.isScattering,
|
||||
isGatheringAny: store.isGatheringAny,
|
||||
requiresConfirmation: store.isScattering || store.isGatheringAny || store.raysReady > 0,
|
||||
// actions
|
||||
setIsMaximized: store.setIsMaximized,
|
||||
terminateBeam: store.terminate,
|
||||
})));
|
||||
|
||||
|
||||
// closure handlers
|
||||
|
||||
const handleCloseBeam = React.useCallback(() => {
|
||||
if (requiresConfirmation)
|
||||
setShowCloseConfirmation(true);
|
||||
else
|
||||
terminateBeam();
|
||||
}, [requiresConfirmation, terminateBeam]);
|
||||
|
||||
const handleCloseConfirmation = React.useCallback(() => {
|
||||
terminateBeam();
|
||||
setShowCloseConfirmation(false);
|
||||
}, [terminateBeam]);
|
||||
|
||||
const handleCloseDenial = React.useCallback(() => {
|
||||
setShowCloseConfirmation(false);
|
||||
}, []);
|
||||
|
||||
const handleMaximizeBeam = React.useCallback(() => {
|
||||
setIsMaximized(true);
|
||||
}, [setIsMaximized]);
|
||||
|
||||
|
||||
// intercept esc this beam is focused
|
||||
useGlobalShortcut(ShortcutKeyName.Esc, false, false, false, handleCloseBeam);
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: { xs: 1, md: 3 }, alignItems: 'center' }}>
|
||||
|
||||
{/* [desktop] maximize button, or a disabled spacer */}
|
||||
{props.isMobile ? null : (
|
||||
<GoodTooltip title='Maximize'>
|
||||
<IconButton size='sm' onClick={handleMaximizeBeam}>
|
||||
<FullscreenRoundedIcon />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
)}
|
||||
|
||||
{/* Title & Status */}
|
||||
<Typography level='title-md'>
|
||||
<Box
|
||||
component='span'
|
||||
sx={
|
||||
isGatheringAny ? { animation: `${animationBackgroundBeamGather} 3s infinite, ${animationEnterBelow} 0.6s`, px: 1.5, py: 0.5 }
|
||||
: isScattering ? { animation: `${animationColorBeamScatterINV} 5s infinite, ${animationEnterBelow} 0.6s` }
|
||||
: { fontWeight: 'lg' }
|
||||
}>
|
||||
{isGatheringAny ? 'Merging...' : isScattering ? 'Beaming...' : 'Beam'}
|
||||
</Box>
|
||||
{(!isGatheringAny && !isScattering) && ' Mode'}
|
||||
</Typography>
|
||||
|
||||
{/* Right Close Icon */}
|
||||
<GoodTooltip usePlain title={<Box sx={{ p: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>Close Beam Mode <KeyStroke combo='Esc' /></Box>}>
|
||||
<IconButton aria-label='Close' size='sm' onClick={handleCloseBeam}>
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
{showCloseConfirmation && (
|
||||
<ConfirmationModal
|
||||
open
|
||||
onClose={handleCloseDenial}
|
||||
onPositive={handleCloseConfirmation}
|
||||
lowStakes
|
||||
noTitleBar
|
||||
confirmationText='Are you sure you want to close Beam Mode? Unsaved text will be lost.'
|
||||
positiveActionText='Yes, close'
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -13,7 +13,7 @@ import { CHAT_NOVEL_TITLE } from '../AppChat';
|
||||
import { FadeInButton } from './ChatDrawerItem';
|
||||
|
||||
|
||||
export function ChatTitle(props: {
|
||||
export function ChatBarAltTitle(props: {
|
||||
conversationId: DConversationId | null,
|
||||
conversationTitle: string,
|
||||
}) {
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { usePersonaIdDropdown } from './usePersonaDropdown';
|
||||
import { useFolderDropdown } from './folders/useFolderDropdown';
|
||||
|
||||
|
||||
export function ChatDropdowns(props: {
|
||||
export function ChatBarDropdowns(props: {
|
||||
conversationId: DConversationId | null
|
||||
}) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Modal, ModalClose } from '@mui/joy';
|
||||
|
||||
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
|
||||
import { BeamView } from '~/modules/beam/BeamView';
|
||||
|
||||
import { themeZIndexBeamView } from '~/common/app.theme';
|
||||
|
||||
|
||||
export function ChatBeamWrapper(props: {
|
||||
beamStore: BeamStoreApi,
|
||||
isMobile: boolean,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const isMaximized = useBeamStore(props.beamStore, state => state.isMaximized);
|
||||
|
||||
const handleUnMaximize = React.useCallback(() => {
|
||||
props.beamStore.getState().setIsMaximized(false);
|
||||
}, [props.beamStore]);
|
||||
|
||||
// memo the beamview
|
||||
const beamView = React.useMemo(() => (
|
||||
<BeamView
|
||||
beamStore={props.beamStore}
|
||||
isMobile={props.isMobile}
|
||||
showExplainer
|
||||
/>
|
||||
), [props.beamStore, props.isMobile]);
|
||||
|
||||
return isMaximized ? (
|
||||
<Modal open onClose={handleUnMaximize}>
|
||||
<Box sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
}}>
|
||||
{beamView}
|
||||
<ModalClose sx={{ color: 'white', backgroundColor: 'background.surface', boxShadow: 'xs', mr: 2 }} />
|
||||
</Box>
|
||||
</Modal>
|
||||
) : (
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: themeZIndexBeamView, // stay on top of Message > Chips (:1), and Overlays (:2) - note: Desktop Drawer (:26)
|
||||
}}>
|
||||
{beamView}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
|
||||
import { Box, Button, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
@@ -26,9 +27,9 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { ChatDrawerItemMemo, FolderChangeRequest } from './ChatDrawerItem';
|
||||
import { ChatFolderList } from './folders/ChatFolderList';
|
||||
import { ChatNavGrouping, useChatNavRenderItems } from './useChatNavRenderItems';
|
||||
import { ChatNavGrouping, ChatSearchSorting, isDrawerSearching, useChatDrawerRenderItems } from './useChatDrawerRenderItems';
|
||||
import { ClearFolderText } from './folders/useFolderDropdown';
|
||||
import { useChatShowRelativeSize } from '../store-app-chat';
|
||||
import { useChatDrawerFilters } from '../store-app-chat';
|
||||
|
||||
|
||||
// this is here to make shallow comparisons work on the next hook
|
||||
@@ -37,7 +38,7 @@ const noFolders: DFolder[] = [];
|
||||
/*
|
||||
* Lists folders and returns the active folder
|
||||
*/
|
||||
export const useFolders = (activeFolderId: string | null) => useFolderStore(({ enableFolders, folders, toggleEnableFolders }) => {
|
||||
export const useFolders = (activeFolderId: string | null) => useFolderStore(useShallow(({ enableFolders, folders, toggleEnableFolders }) => {
|
||||
|
||||
// finds the active folder if any
|
||||
const activeFolder = (enableFolders && activeFolderId)
|
||||
@@ -50,7 +51,7 @@ export const useFolders = (activeFolderId: string | null) => useFolderStore(({ e
|
||||
enableFolders,
|
||||
toggleEnableFolders,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
|
||||
|
||||
export const ChatDrawerMemo = React.memo(ChatDrawer);
|
||||
@@ -74,20 +75,25 @@ function ChatDrawer(props: {
|
||||
|
||||
// local state
|
||||
const [navGrouping, setNavGrouping] = React.useState<ChatNavGrouping>('date');
|
||||
const [searchSorting, setSearchSorting] = React.useState<ChatSearchSorting>('frequency');
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = React.useState('');
|
||||
const [folderChangeRequest, setFolderChangeRequest] = React.useState<FolderChangeRequest | null>(null);
|
||||
|
||||
// external state
|
||||
const { closeDrawer, closeDrawerOnMobile } = useOptimaDrawers();
|
||||
const { showRelativeSize, toggleRelativeSize } = useChatShowRelativeSize();
|
||||
const {
|
||||
filterHasStars, toggleFilterHasStars,
|
||||
showPersonaIcons, toggleShowPersonaIcons,
|
||||
showRelativeSize, toggleShowRelativeSize,
|
||||
} = useChatDrawerFilters();
|
||||
const { activeFolder, allFolders, enableFolders, toggleEnableFolders } = useFolders(props.activeFolderId);
|
||||
const { filteredChatsCount, filteredChatIDs, filteredChatsAreEmpty, filteredChatsBarBasis, filteredChatsIncludeActive, renderNavItems } = useChatNavRenderItems(
|
||||
props.activeConversationId, props.chatPanesConversationIds, debouncedSearchQuery, activeFolder, allFolders, navGrouping, showRelativeSize,
|
||||
const { filteredChatsCount, filteredChatIDs, filteredChatsAreEmpty, filteredChatsBarBasis, filteredChatsIncludeActive, renderNavItems } = useChatDrawerRenderItems(
|
||||
props.activeConversationId, props.chatPanesConversationIds, debouncedSearchQuery, activeFolder, allFolders, filterHasStars, navGrouping, searchSorting, showRelativeSize,
|
||||
);
|
||||
const { contentScaling, showSymbols } = useUIPreferencesStore(state => ({
|
||||
const { contentScaling, showSymbols } = useUIPreferencesStore(useShallow(state => ({
|
||||
contentScaling: state.contentScaling,
|
||||
showSymbols: state.zenMode !== 'cleaner',
|
||||
}), shallow);
|
||||
})));
|
||||
|
||||
|
||||
// New/Activate/Delete Conversation
|
||||
@@ -140,6 +146,7 @@ function ChatDrawer(props: {
|
||||
|
||||
|
||||
// memoize the group dropdown
|
||||
const { isSearching } = isDrawerSearching(debouncedSearchQuery);
|
||||
const groupingComponent = React.useMemo(() => (
|
||||
<Dropdown>
|
||||
<MenuButton
|
||||
@@ -147,34 +154,67 @@ function ChatDrawer(props: {
|
||||
slots={{ root: IconButton }}
|
||||
slotProps={{ root: { size: 'sm' } }}
|
||||
>
|
||||
<MoreVertIcon sx={{ fontSize: 'xl' }} />
|
||||
<MoreVertIcon />
|
||||
</MenuButton>
|
||||
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Group By</Typography>
|
||||
</ListItem>
|
||||
{(['date', 'persona'] as const).map(_gName => (
|
||||
<MenuItem
|
||||
key={'group-' + _gName}
|
||||
aria-label={`Group by ${_gName}`}
|
||||
selected={navGrouping === _gName}
|
||||
onClick={() => setNavGrouping(grouping => grouping === _gName ? false : _gName)}
|
||||
>
|
||||
<ListItemDecorator>{navGrouping === _gName && <CheckIcon />}</ListItemDecorator>
|
||||
{capitalizeFirstLetter(_gName)}
|
||||
|
||||
{!isSearching ? (
|
||||
// Search/Filter default menu: Grouping, Filtering, ...
|
||||
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Group By</Typography>
|
||||
</ListItem>
|
||||
{(['date', 'persona'] as const).map(_gName => (
|
||||
<MenuItem
|
||||
key={'group-' + _gName}
|
||||
aria-label={`Group by ${_gName}`}
|
||||
selected={navGrouping === _gName}
|
||||
onClick={() => setNavGrouping(grouping => grouping === _gName ? false : _gName)}
|
||||
>
|
||||
<ListItemDecorator>{navGrouping === _gName && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
{capitalizeFirstLetter(_gName)}
|
||||
</MenuItem>
|
||||
))}
|
||||
|
||||
<ListDivider />
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Filter</Typography>
|
||||
</ListItem>
|
||||
<MenuItem onClick={toggleFilterHasStars}>
|
||||
<ListItemDecorator>{filterHasStars && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Starred <StarOutlineRoundedIcon />
|
||||
</MenuItem>
|
||||
))}
|
||||
<ListDivider />
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Show</Typography>
|
||||
</ListItem>
|
||||
<MenuItem onClick={toggleRelativeSize}>
|
||||
<ListItemDecorator>{showRelativeSize && <CheckIcon />}</ListItemDecorator>
|
||||
Relative Size
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<ListDivider />
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Show</Typography>
|
||||
</ListItem>
|
||||
<MenuItem onClick={toggleShowPersonaIcons}>
|
||||
<ListItemDecorator>{showPersonaIcons && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Icons
|
||||
</MenuItem>
|
||||
<MenuItem onClick={toggleShowRelativeSize}>
|
||||
<ListItemDecorator>{showRelativeSize && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Relative Size
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
) : (
|
||||
// While searching, show the sorting options
|
||||
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>Sort By</Typography>
|
||||
</ListItem>
|
||||
<MenuItem selected={searchSorting === 'frequency'} onClick={() => setSearchSorting('frequency')}>
|
||||
<ListItemDecorator>{searchSorting === 'frequency' && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Matches
|
||||
</MenuItem>
|
||||
<MenuItem selected={searchSorting === 'date'} onClick={() => setSearchSorting('date')}>
|
||||
<ListItemDecorator>{searchSorting === 'date' && <CheckRoundedIcon />}</ListItemDecorator>
|
||||
Date
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)}
|
||||
</Dropdown>
|
||||
), [navGrouping, showRelativeSize, toggleRelativeSize]);
|
||||
), [filterHasStars, isSearching, navGrouping, searchSorting, showPersonaIcons, showRelativeSize, toggleFilterHasStars, toggleShowPersonaIcons, toggleShowRelativeSize]);
|
||||
|
||||
|
||||
return <>
|
||||
@@ -182,13 +222,13 @@ function ChatDrawer(props: {
|
||||
{/* Drawer Header */}
|
||||
<PageDrawerHeader title='Chats' onClose={closeDrawer}>
|
||||
<Tooltip title={enableFolders ? 'Hide Folders' : 'Use Folders'}>
|
||||
<IconButton onClick={toggleEnableFolders}>
|
||||
<IconButton size='sm' onClick={toggleEnableFolders}>
|
||||
{enableFolders ? <FoldersToggleOn /> : <FoldersToggleOff />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</PageDrawerHeader>
|
||||
|
||||
{/* Folders List */}
|
||||
{/* Folders List (shrink at twice the rate as the Titles) */}
|
||||
{/*<Box sx={{*/}
|
||||
{/* display: 'grid',*/}
|
||||
{/* gridTemplateRows: !enableFolders ? '0fr' : '1fr',*/}
|
||||
@@ -205,6 +245,12 @@ function ChatDrawer(props: {
|
||||
contentScaling={contentScaling}
|
||||
activeFolderId={props.activeFolderId}
|
||||
onFolderSelect={props.setActiveFolderId}
|
||||
sx={{
|
||||
// shrink this at twice the rate as the Titles list
|
||||
flexGrow: 0, flexShrink: 2, overflow: 'hidden',
|
||||
minHeight: '7.5rem',
|
||||
p: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/*</Box>*/}
|
||||
@@ -214,67 +260,58 @@ function ChatDrawer(props: {
|
||||
|
||||
{enableFolders && <ListDivider sx={{ mb: 0 }} />}
|
||||
|
||||
{/* Search Input Field */}
|
||||
<DebounceInputMemo
|
||||
minChars={2}
|
||||
onDebounce={setDebouncedSearchQuery}
|
||||
debounceTimeout={300}
|
||||
placeholder='Search...'
|
||||
aria-label='Search'
|
||||
endDecorator={groupingComponent}
|
||||
sx={{ m: 2 }}
|
||||
/>
|
||||
{/* Search / New Chat */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', m: 2, gap: 2 }}>
|
||||
|
||||
{/* New Chat Button */}
|
||||
<ListItem sx={{ mx: '0.25rem', mb: 0.5 }}>
|
||||
<ListItemButton
|
||||
{/* Search Input Field */}
|
||||
<DebounceInputMemo
|
||||
minChars={2}
|
||||
onDebounce={setDebouncedSearchQuery}
|
||||
debounceTimeout={300}
|
||||
placeholder='Search...'
|
||||
aria-label='Search'
|
||||
endDecorator={groupingComponent}
|
||||
/>
|
||||
|
||||
{/* New Chat Button */}
|
||||
<Button
|
||||
// variant='outlined'
|
||||
variant={disableNewButton ? undefined : 'outlined'}
|
||||
variant={disableNewButton ? undefined : 'soft'}
|
||||
color='primary'
|
||||
disabled={disableNewButton}
|
||||
onClick={handleButtonNew}
|
||||
sx={{
|
||||
// ...PageDrawerTallItemSx,
|
||||
px: 'calc(var(--ListItem-paddingX) - 0.25rem)',
|
||||
justifyContent: 'flex-start',
|
||||
padding: '0px 0.75rem',
|
||||
|
||||
// text size
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'lg',
|
||||
|
||||
// style
|
||||
borderRadius: 'md',
|
||||
boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'sm',
|
||||
backgroundColor: 'background.popup',
|
||||
transition: 'box-shadow 0.2s',
|
||||
// backgroundColor: 'background.popup',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
borderRadius: 'sm',
|
||||
'--ListItemDecorator-size': 'calc(2.5rem - 1px)', // compensate for the border
|
||||
// boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'xs',
|
||||
// transition: 'box-shadow 0.2s',
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator><AddIcon sx={{ '--Icon-fontSize': 'var(--joy-fontSize-xl)', pl: '0.125rem' }} /></ListItemDecorator>
|
||||
<ListItemDecorator><AddIcon sx={{ fontSize: '' }} /></ListItemDecorator>
|
||||
New chat
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Button>
|
||||
|
||||
{/*<ListDivider sx={{ mt: 0 }} />*/}
|
||||
|
||||
{/* List of Chat Titles (and actions) */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
|
||||
{/*<ListItem sticky sx={{ justifyContent: 'space-between', boxShadow: 'sm' }}>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Conversations*/}
|
||||
{/* </Typography>*/}
|
||||
{/* <ToggleButtonGroup variant='soft' size='sm' value={grouping} onChange={(_event, newValue) => newValue && setGrouping(newValue)}>*/}
|
||||
{/* <IconButton value='off'>*/}
|
||||
{/* <AccessTimeIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* <IconButton value='persona'>*/}
|
||||
{/* <PersonIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </ToggleButtonGroup>*/}
|
||||
{/*</ListItem>*/}
|
||||
</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 }}>
|
||||
{renderNavItems.map((item, idx) => item.type === 'nav-item-chat-data' ? (
|
||||
<ChatDrawerItemMemo
|
||||
key={'nav-chat-' + item.conversationId}
|
||||
item={item}
|
||||
showSymbols={showSymbols}
|
||||
showSymbols={showPersonaIcons && showSymbols}
|
||||
bottomBarBasis={filteredChatsBarBasis}
|
||||
onConversationActivate={handleConversationActivate}
|
||||
onConversationBranch={onConversationBranch}
|
||||
@@ -283,12 +320,26 @@ function ChatDrawer(props: {
|
||||
onConversationFolderChange={handleConversationFolderChange}
|
||||
/>
|
||||
) : item.type === 'nav-item-group' ? (
|
||||
<Typography key={'nav-divider-' + idx} level='body-xs' sx={{ textAlign: 'center', my: 'calc(var(--ListItem-minHeight) / 4)' }}>
|
||||
<Typography key={'nav-divider-' + idx} level='body-xs' sx={{
|
||||
textAlign: 'center',
|
||||
my: 'calc(var(--ListItem-minHeight) / 4)',
|
||||
// keeps the group header sticky to the top
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
backgroundColor: 'background.popup',
|
||||
zIndex: 1,
|
||||
}}>
|
||||
{item.title}
|
||||
</Typography>
|
||||
) : item.type === 'nav-item-info-message' ? (
|
||||
<Typography key={'nav-info-' + idx} level='body-xs' sx={{ textAlign: 'center', my: 'calc(var(--ListItem-minHeight) / 2)' }}>
|
||||
<Typography key={'nav-info-' + idx} level='body-xs' sx={{ textAlign: 'center', color: 'primary.softColor', my: 'calc(var(--ListItem-minHeight) / 4)' }}>
|
||||
{filterHasStars && <StarOutlineRoundedIcon sx={{ color: 'primary.softColor', fontSize: 'xl', mb: -0.5, mr: 1 }} />}
|
||||
{item.message}
|
||||
{filterHasStars && <>
|
||||
<Button variant='soft' size='sm' onClick={toggleFilterHasStars} sx={{ display: 'block', mt: 2, mx: 'auto' }}>
|
||||
remove filters
|
||||
</Button>
|
||||
</>}
|
||||
</Typography>
|
||||
) : null,
|
||||
)}
|
||||
@@ -296,7 +347,8 @@ function ChatDrawer(props: {
|
||||
|
||||
<ListDivider sx={{ my: 0 }} />
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{/* Bottom commands */}
|
||||
<Box sx={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
|
||||
<ListItemButton onClick={props.onConversationsImportDialog} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator>
|
||||
<FileUploadOutlinedIcon />
|
||||
|
||||
@@ -5,7 +5,7 @@ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
@@ -21,6 +21,7 @@ import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { isDeepEqual } from '~/common/util/jsUtils';
|
||||
|
||||
import { CHAT_NOVEL_TITLE } from '../AppChat';
|
||||
import { STREAM_TEXT_INDICATOR } from '../editors/chat-stream';
|
||||
|
||||
|
||||
// set to true to display the conversation IDs
|
||||
@@ -53,6 +54,7 @@ export interface ChatNavigationItemData {
|
||||
isAlsoOpen: string | false;
|
||||
isEmpty: boolean;
|
||||
title: string;
|
||||
userFlagsSummary: string | undefined;
|
||||
folder: DFolder | null | undefined; // null: 'All', undefined: do not show folder select
|
||||
updatedAt: number;
|
||||
messageCount: number;
|
||||
@@ -86,7 +88,7 @@ function ChatDrawerItem(props: {
|
||||
|
||||
// derived state
|
||||
const { onConversationBranch, onConversationExport, onConversationFolderChange } = props;
|
||||
const { conversationId, isActive, isAlsoOpen, title, folder, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
|
||||
const { conversationId, isActive, isAlsoOpen, title, userFlagsSummary, folder, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
|
||||
const isNew = messageCount === 0;
|
||||
|
||||
|
||||
@@ -204,7 +206,7 @@ function ChatDrawerItem(props: {
|
||||
}}
|
||||
>
|
||||
{/*{DEBUG_CONVERSATION_IDS && `${conversationId} - `}*/}
|
||||
{title.trim() ? title : CHAT_NOVEL_TITLE}{assistantTyping && '...'}
|
||||
{title.trim() ? title : CHAT_NOVEL_TITLE}{assistantTyping && STREAM_TEXT_INDICATOR}
|
||||
</Box>
|
||||
) : (
|
||||
<InlineTextarea
|
||||
@@ -219,16 +221,19 @@ function ChatDrawerItem(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Display search frequency if it exists and is greater than 0 */}
|
||||
{searchFrequency > 0 && (
|
||||
<Box sx={{ ml: 1 }}>
|
||||
<Typography level='body-sm'>
|
||||
{searchFrequency}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{/* Right text */}
|
||||
{searchFrequency > 0 ? (
|
||||
// Display search frequency if it exists and is greater than 0
|
||||
<Typography level='body-sm'>
|
||||
{searchFrequency}
|
||||
</Typography>
|
||||
) : (userFlagsSummary && props.showSymbols) ? (
|
||||
<Typography sx={{ mr: '5px' }}>
|
||||
{userFlagsSummary}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
</>, [assistantTyping, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title]);
|
||||
</>, [assistantTyping, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title, userFlagsSummary]);
|
||||
|
||||
const progressBarFixedComponent = React.useMemo(() =>
|
||||
progress > 0 && (
|
||||
@@ -278,7 +283,7 @@ function ChatDrawerItem(props: {
|
||||
{/* buttons row */}
|
||||
{isActive && (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, minHeight: '2.25rem', alignItems: 'center' }}>
|
||||
<ListItemDecorator />
|
||||
{props.showSymbols && <ListItemDecorator />}
|
||||
|
||||
{/* Current Folder color, and change initiator */}
|
||||
{!deleteArmed && <>
|
||||
@@ -300,7 +305,7 @@ function ChatDrawerItem(props: {
|
||||
|
||||
<Tooltip disableInteractive title='Rename'>
|
||||
<FadeInButton size='sm' disabled={isEditingTitle || isAutoEditingTitle} onClick={handleTitleEditBegin}>
|
||||
<EditIcon />
|
||||
<EditRoundedIcon />
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, List } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
|
||||
@@ -10,17 +10,17 @@ import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { createDMessage, DConversationId, DMessage, DMessageUserFlag, getConversation, messageToggleUserFlag, useChatStore } from '~/common/state/store-chats';
|
||||
import { useBrowserTranslationWarning } from '~/common/components/useIsBrowserTranslating';
|
||||
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useEphemerals } from '~/common/chats/EphemeralsStore';
|
||||
import { useScrollToBottom } from '~/common/scroll-to-bottom/useScrollToBottom';
|
||||
|
||||
import { ChatMessage, ChatMessageMemo } from './message/ChatMessage';
|
||||
import { CleanerMessage, MessagesSelectionHeader } from './message/CleanerMessage';
|
||||
import { Ephemerals } from './Ephemerals';
|
||||
import { PersonaSelector } from './persona-selector/PersonaSelector';
|
||||
import { useChatShowSystemMessages } from '../store-app-chat';
|
||||
import { useScrollToBottom } from './scroll-to-bottom/useScrollToBottom';
|
||||
|
||||
|
||||
/**
|
||||
@@ -34,7 +34,7 @@ export function ChatMessageList(props: {
|
||||
fitScreen: boolean,
|
||||
isMessageSelectionMode: boolean,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
|
||||
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[], chatEffectBeam: boolean) => Promise<void>,
|
||||
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[]) => Promise<void>,
|
||||
onTextDiagram: (diagramConfig: DiagramConfig | null) => void,
|
||||
onTextImagine: (conversationId: DConversationId, selectedText: string) => Promise<void>,
|
||||
onTextSpeak: (selectedText: string) => Promise<void>,
|
||||
@@ -52,7 +52,7 @@ export function ChatMessageList(props: {
|
||||
const { openPreferencesTab } = useOptimaLayout();
|
||||
const [showSystemMessages] = useChatShowSystemMessages();
|
||||
const optionalTranslationWarning = useBrowserTranslationWarning();
|
||||
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
|
||||
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(useShallow(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
|
||||
return {
|
||||
conversationMessages: conversation ? conversation.messages : [],
|
||||
@@ -61,7 +61,7 @@ export function ChatMessageList(props: {
|
||||
editMessage: state.editMessage,
|
||||
setMessages: state.setMessages,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
const ephemerals = useEphemerals(props.conversationHandler);
|
||||
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
|
||||
|
||||
@@ -71,26 +71,50 @@ export function ChatMessageList(props: {
|
||||
|
||||
// text actions
|
||||
|
||||
const handleRunExample = React.useCallback(async (text: string) => {
|
||||
conversationId && await onConversationExecuteHistory(conversationId, [...conversationMessages, createDMessage('user', text)], false);
|
||||
const handleRunExample = React.useCallback(async (examplePrompt: string) => {
|
||||
conversationId && await onConversationExecuteHistory(conversationId, [...conversationMessages, createDMessage('user', examplePrompt)]);
|
||||
}, [conversationId, conversationMessages, onConversationExecuteHistory]);
|
||||
|
||||
|
||||
// message menu methods proxy
|
||||
|
||||
const handleConversationBranch = React.useCallback((messageId: string) => {
|
||||
conversationId && onConversationBranch(conversationId, messageId);
|
||||
}, [conversationId, onConversationBranch]);
|
||||
|
||||
const handleConversationRestartFrom = React.useCallback(async (messageId: string, offset: number, chatEffectBeam: boolean) => {
|
||||
const handleMessageAssistantFrom = React.useCallback(async (messageId: string, offset: number) => {
|
||||
const messages = getConversation(conversationId)?.messages;
|
||||
if (messages) {
|
||||
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + offset + 1);
|
||||
conversationId && await onConversationExecuteHistory(conversationId, truncatedHistory, chatEffectBeam);
|
||||
conversationId && await onConversationExecuteHistory(conversationId, truncatedHistory);
|
||||
}
|
||||
}, [conversationId, onConversationExecuteHistory]);
|
||||
|
||||
const handleConversationTruncate = React.useCallback((messageId: string) => {
|
||||
const handleMessageBeam = React.useCallback(async (messageId: string) => {
|
||||
// Right-click menu Beam
|
||||
if (!conversationId || !props.conversationHandler) return;
|
||||
const messages = getConversation(conversationId)?.messages;
|
||||
if (messages?.length) {
|
||||
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + 1);
|
||||
const lastMessage = truncatedHistory[truncatedHistory.length - 1];
|
||||
if (lastMessage) {
|
||||
// assistant: do an in-place beam
|
||||
if (lastMessage.role === 'assistant') {
|
||||
if (truncatedHistory.length >= 2)
|
||||
props.conversationHandler.beamInvoke(truncatedHistory.slice(0, -1), [lastMessage], lastMessage.id);
|
||||
} else {
|
||||
// user: truncate and append (but if the next message is an assistant message, import it)
|
||||
const nextMessage = messages[truncatedHistory.length];
|
||||
if (nextMessage?.role === 'assistant')
|
||||
props.conversationHandler.beamInvoke(truncatedHistory, [nextMessage], null);
|
||||
else
|
||||
props.conversationHandler.beamInvoke(truncatedHistory, [], null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [conversationId, props.conversationHandler]);
|
||||
|
||||
const handleMessageBranch = React.useCallback((messageId: string) => {
|
||||
conversationId && onConversationBranch(conversationId, messageId);
|
||||
}, [conversationId, onConversationBranch]);
|
||||
|
||||
const handleMessageTruncate = React.useCallback((messageId: string) => {
|
||||
const messages = getConversation(conversationId)?.messages;
|
||||
if (conversationId && messages) {
|
||||
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + 1);
|
||||
@@ -106,6 +130,12 @@ export function ChatMessageList(props: {
|
||||
conversationId && editMessage(conversationId, messageId, { text: newText }, true);
|
||||
}, [conversationId, editMessage]);
|
||||
|
||||
const handleMessageToggleUserFlag = React.useCallback((messageId: string, userFlag: DMessageUserFlag) => {
|
||||
conversationId && editMessage(conversationId, messageId, (message) => ({
|
||||
userFlags: messageToggleUserFlag(message, userFlag),
|
||||
}), false);
|
||||
}, [conversationId, editMessage]);
|
||||
|
||||
const handleTextDiagram = React.useCallback(async (messageId: string, text: string) => {
|
||||
conversationId && onTextDiagram({ conversationId: conversationId, messageId, text });
|
||||
}, [conversationId, onTextDiagram]);
|
||||
@@ -239,11 +269,13 @@ export function ChatMessageList(props: {
|
||||
isBottom={idx === count - 1}
|
||||
isImagining={isImagining}
|
||||
isSpeaking={isSpeaking}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationRestartFrom={handleConversationRestartFrom}
|
||||
onConversationTruncate={handleConversationTruncate}
|
||||
onMessageAssistantFrom={handleMessageAssistantFrom}
|
||||
onMessageBeam={handleMessageBeam}
|
||||
onMessageBranch={handleMessageBranch}
|
||||
onMessageDelete={handleMessageDelete}
|
||||
onMessageEdit={handleMessageEdit}
|
||||
onMessageToggleUserFlag={handleMessageToggleUserFlag}
|
||||
onMessageTruncate={handleMessageTruncate}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Box, Grid, IconButton, Sheet, styled, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { DConversationId } from '~/common/state/store-chats';
|
||||
import { DEphemeral } from '~/common/chats/EphemeralsStore';
|
||||
import { lineHeightChatTextMd } from '~/common/app.theme';
|
||||
@@ -78,7 +78,7 @@ function StateRenderer(props: { state: object }) {
|
||||
function EphemeralItem({ conversationId, ephemeral }: { conversationId: string, ephemeral: DEphemeral }) {
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
ConversationManager.getHandler(conversationId).ephemeralsStore.delete(ephemeral.id);
|
||||
ConversationsManager.getHandler(conversationId).ephemeralsStore.delete(ephemeral.id);
|
||||
}, [conversationId, ephemeral.id]);
|
||||
|
||||
return <Box
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Alert, Box, Sheet, Typography } from '@mui/joy';
|
||||
|
||||
import { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
import { useBeam } from '~/common/chats/BeamStore';
|
||||
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
|
||||
|
||||
|
||||
export function Beam(props: {
|
||||
conversationHandler: ConversationHandler | null,
|
||||
isMobile: boolean,
|
||||
sx?: SxProps
|
||||
}) {
|
||||
|
||||
// state
|
||||
const { config, candidates } = useBeam(props.conversationHandler);
|
||||
|
||||
// external state
|
||||
const [allChatLlm, allChatLlmComponent] = useLLMSelect(true, 'Beam LLM');
|
||||
|
||||
if (!config)
|
||||
return null;
|
||||
|
||||
const lastMessage = config.history.slice(-1)[0] ?? null;
|
||||
|
||||
return (
|
||||
<Box sx={{ ...props.sx, px: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
|
||||
{/* Issues */}
|
||||
{!!config.configError && (
|
||||
<Alert>
|
||||
{config.configError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Models, [x] all same, */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 2 }}>
|
||||
<Box sx={{ minWidth: 200 }}>
|
||||
{allChatLlmComponent}
|
||||
</Box>
|
||||
|
||||
{!!lastMessage && (
|
||||
<Box sx={{
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'xs',
|
||||
borderRadius: 'lg',
|
||||
borderTopRightRadius: 0,
|
||||
borderTopLeftRadius: 0,
|
||||
py: 1,
|
||||
px: 1,
|
||||
mb: 'auto',
|
||||
|
||||
|
||||
flex: 1,
|
||||
}}>
|
||||
{lastMessage.text}
|
||||
</Box>
|
||||
// <ChatMessageMemo
|
||||
// message={lastMessage}
|
||||
// fitScreen={props.isMobile}
|
||||
// sx={{
|
||||
// borderRadius: 'lg',
|
||||
// borderBottomRightRadius: lastMessage.role === 'assistant' ? undefined : 0,
|
||||
// borderBottomLeftRadius: lastMessage.role === 'user' ? undefined : 0,
|
||||
// boxShadow: 'xs',
|
||||
// my: 2,
|
||||
// px: 0,
|
||||
// py: 1,
|
||||
// alignSelf: 'self-end',
|
||||
// flex: 1,
|
||||
// maxHeight: '5rem',
|
||||
// overflow: 'hidden',
|
||||
// }}
|
||||
// />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Grid */}
|
||||
<Box sx={{
|
||||
// my: 'auto',
|
||||
// display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
border: '1px solid purple',
|
||||
minHeight: '300px',
|
||||
|
||||
// layout
|
||||
display: 'grid',
|
||||
gridTemplateColumns: props.isMobile ? 'repeat(auto-fit, minmax(320px, 1fr))' : 'repeat(auto-fit, minmax(400px, 1fr))',
|
||||
gap: { xs: 2, md: 2 },
|
||||
}}>
|
||||
<Sheet sx={{ minHeight: '50%' }}>
|
||||
b
|
||||
</Sheet>
|
||||
<Sheet>
|
||||
a
|
||||
</Sheet>
|
||||
<Sheet>
|
||||
a
|
||||
</Sheet>
|
||||
<Sheet>
|
||||
a
|
||||
</Sheet>
|
||||
</Box>
|
||||
|
||||
{/* Auto-Gatherer: All-in-one, Best-Of */}
|
||||
<Box>
|
||||
Gatherer
|
||||
</Box>
|
||||
|
||||
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='body-sm' sx={{ whiteSpace: 'break-spaces' }}>
|
||||
{/*{JSON.stringify(config, null, 2)}*/}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box sx={{
|
||||
height: '100%',
|
||||
borderRadius: 'lg',
|
||||
borderBottomLeftRadius: 0,
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'lg',
|
||||
m: 2,
|
||||
p: '0.25rem 1rem',
|
||||
}}>
|
||||
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
a
|
||||
</Box>
|
||||
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export function CameraCaptureModal(props: {
|
||||
}}>
|
||||
|
||||
{/* Top bar */}
|
||||
<Sheet variant='solid' invertedColors sx={{ zIndex: 10, display: 'flex', justifyContent: 'space-between', p: 1 }}>
|
||||
<Sheet variant='solid' invertedColors sx={{ display: 'flex', justifyContent: 'space-between', p: 1 }}>
|
||||
<Select
|
||||
variant='solid' color='neutral'
|
||||
value={cameraIdx} onChange={(_event: any, value: number | null) => setCameraIdx(value === null ? -1 : value)}
|
||||
@@ -116,7 +116,7 @@ export function CameraCaptureModal(props: {
|
||||
|
||||
{showInfo && !!info && <Typography
|
||||
sx={{
|
||||
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 1,
|
||||
position: 'absolute', inset: 0, zIndex: 1, /* camera info on top of video */
|
||||
background: 'rgba(0,0,0,0.5)', color: 'white',
|
||||
whiteSpace: 'pre', overflowY: 'scroll',
|
||||
}}>
|
||||
@@ -127,7 +127,7 @@ export function CameraCaptureModal(props: {
|
||||
</Box>
|
||||
|
||||
{/* Bottom controls (zoom, ocr, download) & progress */}
|
||||
<Sheet variant='soft' sx={{ display: 'flex', flexDirection: 'column', zIndex: 20, gap: 1, p: 1 }}>
|
||||
<Sheet variant='soft' sx={{ display: 'flex', flexDirection: 'column', gap: 1, p: 1 }}>
|
||||
|
||||
{!!error && <InlineError error={error} />}
|
||||
|
||||
@@ -137,7 +137,7 @@ export function CameraCaptureModal(props: {
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'space-between' }}>
|
||||
{/* Info */}
|
||||
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
|
||||
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)}>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
{/*<Button disabled={ocrProgress !== null} fullWidth variant='solid' size='lg' onClick={handleVideoOCRClicked} sx={{ flex: 1, maxWidth: 260 }}>*/}
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { Box, MenuItem, Radio, Typography } from '@mui/joy';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { KeyStroke, platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { ChatModeId } from '../../AppChat';
|
||||
@@ -13,7 +13,9 @@ import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
interface ChatModeDescription {
|
||||
label: string;
|
||||
description: string | React.JSX.Element;
|
||||
highlight?: boolean;
|
||||
shortcut?: string;
|
||||
hideOnDesktop?: boolean;
|
||||
requiresTTI?: boolean;
|
||||
}
|
||||
|
||||
@@ -22,9 +24,15 @@ const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
|
||||
label: 'Chat',
|
||||
description: 'Persona replies',
|
||||
},
|
||||
'generate-text-beam': {
|
||||
label: 'Beam', // Best of, Auto-Prime, Top Pick, Select Best
|
||||
description: 'Combine multiple models', // Smarter: combine...
|
||||
shortcut: 'Ctrl + Enter',
|
||||
hideOnDesktop: true,
|
||||
},
|
||||
'append-user': {
|
||||
label: 'Write',
|
||||
description: 'Appends a message',
|
||||
description: 'Append a message',
|
||||
shortcut: 'Alt + Enter',
|
||||
},
|
||||
'generate-image': {
|
||||
@@ -32,13 +40,9 @@ const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
|
||||
description: 'AI Image Generation',
|
||||
requiresTTI: true,
|
||||
},
|
||||
'generate-text-beam': {
|
||||
label: 'Best-Of', // Best of, Auto-Prime, Top Pick, Select Best
|
||||
description: 'Smarter: best of multiple replies',
|
||||
},
|
||||
'generate-react': {
|
||||
label: 'Reason + Act', // · α
|
||||
description: 'Answers questions in multiple steps',
|
||||
description: 'Answer questions in multiple steps',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -50,13 +54,16 @@ function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
|
||||
}
|
||||
|
||||
export function ChatModeMenu(props: {
|
||||
anchorEl: HTMLAnchorElement | null, onClose: () => void,
|
||||
chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void
|
||||
isMobile: boolean,
|
||||
anchorEl: HTMLAnchorElement | null,
|
||||
onClose: () => void,
|
||||
chatModeId: ChatModeId,
|
||||
onSetChatModeId: (chatMode: ChatModeId) => void,
|
||||
capabilityHasTTI: boolean,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const labsChatBeam = useUXLabsStore(state => state.labsChatBeam);
|
||||
const labsBeam = useUXLabsStore(state => state.labsBeam);
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
|
||||
return (
|
||||
@@ -74,17 +81,18 @@ export function ChatModeMenu(props: {
|
||||
|
||||
{/* ChatMode items */}
|
||||
{Object.entries(ChatModeItems)
|
||||
.filter(([key, data]) => key !== 'generate-text-beam' || labsChatBeam)
|
||||
.filter(([key, _data]) => key !== 'generate-text-beam' || labsBeam)
|
||||
.filter(([_key, data]) => !data.hideOnDesktop || props.isMobile)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Radio checked={key === props.chatModeId} />
|
||||
<Radio color={data.highlight ? 'success' : undefined} checked={key === props.chatModeId} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography>{data.label}</Typography>
|
||||
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
|
||||
</Box>
|
||||
{(key === props.chatModeId || !!data.shortcut) && (
|
||||
<KeyStroke combo={fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline)} />
|
||||
<KeyStroke combo={platformAwareKeystrokes(fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline))} />
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>)}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { fileOpen, FileWithHandle } from 'browser-fs-access';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
|
||||
@@ -10,7 +9,7 @@ import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
@@ -24,22 +23,27 @@ import type { LLMOptionsOpenAI } from '~/modules/llms/vendors/openai/openai.vend
|
||||
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
import { conversationTitle, DConversationId, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { countModelTokens } from '~/common/util/token-counter';
|
||||
import { isMacUser } from '~/common/util/pwaUtils';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
import { lineHeightTextareaMd } from '~/common/app.theme';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
import { playSoundUrl } from '~/common/util/audioUtils';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
import { useDebouncer } from '~/common/components/useDebouncer';
|
||||
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ActileItem, ActileProvider } from './actile/ActileProvider';
|
||||
import type { ActileItem } from './actile/ActileProvider';
|
||||
import { providerCommands } from './actile/providerCommands';
|
||||
import { providerStarredMessage, StarredMessageItem } from './actile/providerStarredMessage';
|
||||
import { useActileManager } from './actile/useActileManager';
|
||||
|
||||
import type { AttachmentId } from './attachments/store-attachments';
|
||||
@@ -52,6 +56,7 @@ import { ButtonAttachCameraMemo, useCameraCaptureModal } from './buttons/ButtonA
|
||||
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
|
||||
import { ButtonAttachScreenCaptureMemo } from './buttons/ButtonAttachScreenCapture';
|
||||
import { ButtonBeamMemo } from './buttons/ButtonBeam';
|
||||
import { ButtonCallMemo } from './buttons/ButtonCall';
|
||||
import { ButtonMicContinuationMemo } from './buttons/ButtonMicContinuation';
|
||||
import { ButtonMicMemo } from './buttons/ButtonMic';
|
||||
@@ -63,16 +68,8 @@ import { TokenProgressbarMemo } from './TokenProgressbar';
|
||||
import { useComposerStartupText } from './store-composer';
|
||||
|
||||
|
||||
export const animationStopEnter = keyframes`
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px)
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0)
|
||||
}
|
||||
`;
|
||||
const zIndexComposerOverlayDrop = 10;
|
||||
const zIndexComposerOverlayMic = 20;
|
||||
|
||||
const dropperCardSx: SxProps = {
|
||||
display: 'none',
|
||||
@@ -81,7 +78,7 @@ const dropperCardSx: SxProps = {
|
||||
border: '2px dashed',
|
||||
borderRadius: 'xs',
|
||||
boxShadow: 'none',
|
||||
zIndex: 10,
|
||||
zIndex: zIndexComposerOverlayDrop,
|
||||
} as const;
|
||||
|
||||
const dropppedCardDraggingSx: SxProps = {
|
||||
@@ -108,6 +105,7 @@ export function Composer(props: {
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
|
||||
const [composeText, debouncedText, setComposeText] = useDebouncer('', 300, 1200, true);
|
||||
const [micContinuation, setMicContinuation] = React.useState(false);
|
||||
const [speechInterimResult, setSpeechInterimResult] = React.useState<SpeechResult | null>(null);
|
||||
@@ -116,12 +114,15 @@ export function Composer(props: {
|
||||
|
||||
// external state
|
||||
const { openPreferencesTab /*, setIsFocusedMode*/ } = useOptimaLayout();
|
||||
const { labsAttachScreenCapture, labsCameraDesktop } = useUXLabsStore(state => ({
|
||||
const { labsAttachScreenCapture, labsBeam, labsCameraDesktop } = useUXLabsStore(state => ({
|
||||
labsAttachScreenCapture: state.labsAttachScreenCapture,
|
||||
labsBeam: state.labsBeam,
|
||||
labsCameraDesktop: state.labsCameraDesktop,
|
||||
}), shallow);
|
||||
const timeToShowTips = useAppStateStore(state => state.usageCount > 2);
|
||||
const { novel: explainShiftEnter, touch: touchShiftEnter } = useUICounter('composer-shift-enter');
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
|
||||
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 chatMicTimeoutMs = useChatMicTimeoutMsValue();
|
||||
@@ -135,7 +136,7 @@ export function Composer(props: {
|
||||
};
|
||||
}, shallow);
|
||||
const { inComposer: browsingInComposer } = useBrowseCapability();
|
||||
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
|
||||
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoMessage, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
|
||||
useAttachments(browsingInComposer && !composeText.startsWith('/'));
|
||||
|
||||
|
||||
@@ -200,6 +201,10 @@ export function Composer(props: {
|
||||
handleSendAction(chatModeId, composeText);
|
||||
}, [chatModeId, composeText, handleSendAction]);
|
||||
|
||||
const handleSendTextBeamClicked = React.useCallback(() => {
|
||||
labsBeam && handleSendAction('generate-text-beam', composeText);
|
||||
}, [composeText, handleSendAction, labsBeam]);
|
||||
|
||||
const handleStopClicked = React.useCallback(() => {
|
||||
!!props.conversationId && stopTyping(props.conversationId);
|
||||
}, [props.conversationId, stopTyping]);
|
||||
@@ -241,7 +246,7 @@ export function Composer(props: {
|
||||
|
||||
// Actiles
|
||||
|
||||
const onActileCommandSelect = React.useCallback((item: ActileItem) => {
|
||||
const onActileCommandPaste = React.useCallback((item: ActileItem) => {
|
||||
if (props.composerTextAreaRef.current) {
|
||||
const textArea = props.composerTextAreaRef.current;
|
||||
const currentText = textArea.value;
|
||||
@@ -262,9 +267,22 @@ export function Composer(props: {
|
||||
}
|
||||
}, [props.composerTextAreaRef, setComposeText]);
|
||||
|
||||
const actileProviders: ActileProvider[] = React.useMemo(() => {
|
||||
return [providerCommands(onActileCommandSelect)];
|
||||
}, [onActileCommandSelect]);
|
||||
const onActileMessageAttach = React.useCallback((item: StarredMessageItem) => {
|
||||
// get the message
|
||||
const conversation = getConversation(item.conversationId);
|
||||
const messageToAttach = conversation?.messages.find(m => m.id === item.messageId);
|
||||
if (conversation && messageToAttach && messageToAttach.text) {
|
||||
// Testing with this serialization for LLM. Note it will still be within a multi-part message,
|
||||
// this could be in a titled markdown block. Don't know yet how this fares with different LLMs.
|
||||
const chatTitle = conversationTitle(conversation);
|
||||
const textPlain = `---\nitem id: ${messageToAttach.id}\ncontext title: ${chatTitle}\n---\n${messageToAttach.text.trim()}\n`;
|
||||
void attachAppendEgoMessage('context-item', textPlain, `${chatTitle} > ${messageToAttach.text.slice(0, 10)}...`);
|
||||
}
|
||||
}, [attachAppendEgoMessage]);
|
||||
|
||||
const actileProviders = React.useMemo(() => {
|
||||
return [providerCommands(onActileCommandPaste), providerStarredMessage(onActileMessageAttach)];
|
||||
}, [onActileCommandPaste, onActileMessageAttach]);
|
||||
|
||||
const { actileComponent, actileInterceptKeydown, actileInterceptTextChange } = useActileManager(actileProviders, props.composerTextAreaRef);
|
||||
|
||||
@@ -284,12 +302,20 @@ export function Composer(props: {
|
||||
// Enter: primary action
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
// Alt: append the message instead
|
||||
// Alt (Windows) or Option (Mac) + Enter: append the message instead of sending it
|
||||
if (e.altKey) {
|
||||
touchAltEnter();
|
||||
handleSendAction('append-user', composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
// Ctrl (Windows) or Command (Mac) + Enter: send for beaming
|
||||
if (labsBeam && ((isMacUser && e.metaKey && !e.ctrlKey) || (!isMacUser && e.ctrlKey && !e.metaKey))) {
|
||||
touchCtrlEnter();
|
||||
handleSendAction('generate-text-beam', composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
// Shift: toggles the 'enter is newline'
|
||||
if (e.shiftKey)
|
||||
touchShiftEnter();
|
||||
@@ -300,7 +326,7 @@ export function Composer(props: {
|
||||
}
|
||||
}
|
||||
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchShiftEnter]);
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, labsBeam, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
|
||||
|
||||
|
||||
// Focus mode
|
||||
@@ -469,19 +495,21 @@ export function Composer(props: {
|
||||
const isReAct = chatModeId === 'generate-react';
|
||||
const isDraw = chatModeId === 'generate-image';
|
||||
|
||||
const showCall = isText || isAppend;
|
||||
const showChatExtras = isText;
|
||||
|
||||
const buttonVariant: VariantProp = (isAppend || (isMobile && isTextBeam)) ? 'outlined' : 'solid';
|
||||
|
||||
const buttonColor: ColorPaletteProp =
|
||||
assistantAbortible ? 'warning'
|
||||
: isReAct ? 'success'
|
||||
: isTextBeam ? 'success'
|
||||
: isTextBeam ? 'primary'
|
||||
: isDraw ? 'warning'
|
||||
: 'primary';
|
||||
|
||||
const buttonText =
|
||||
isAppend ? 'Write'
|
||||
: isReAct ? 'ReAct'
|
||||
: isTextBeam ? 'Best-Of'
|
||||
: isTextBeam ? 'Beam'
|
||||
: isDraw ? 'Draw'
|
||||
: 'Chat';
|
||||
|
||||
@@ -490,18 +518,24 @@ export function Composer(props: {
|
||||
: isAppend ? <SendIcon sx={{ fontSize: 18 }} />
|
||||
: isReAct ? <PsychologyIcon />
|
||||
: isTextBeam ? <ChatBeamIcon /> /* <GavelIcon /> */
|
||||
: isDraw ? <FormatPaintIcon />
|
||||
: isDraw ? <FormatPaintTwoToneIcon />
|
||||
: <TelegramIcon />;
|
||||
|
||||
let textPlaceholder: string =
|
||||
isDraw ? 'Describe an idea or a drawing...'
|
||||
: isReAct ? 'Multi-step reasoning question...'
|
||||
: isTextBeam ? 'Multi-chat with this persona...'
|
||||
: isTextBeam ? 'Beam: combine the smarts of models...'
|
||||
: props.isDeveloperMode ? 'Chat with me' + (isDesktop ? ' · drop source' : '') + ' · attach code...'
|
||||
: props.capabilityHasT2I ? 'Chat · /react · /draw · drop files...'
|
||||
: props.capabilityHasT2I ? 'Chat · /beam · /draw · drop files...'
|
||||
: 'Chat · /react · drop files...';
|
||||
if (isDesktop && explainShiftEnter)
|
||||
textPlaceholder += !enterIsNewline ? '\nShift+Enter to add a new line' : '\nShift+Enter to send';
|
||||
if (isDesktop && timeToShowTips) {
|
||||
if (explainShiftEnter)
|
||||
textPlaceholder += !enterIsNewline ? '\n\n💡 Shift + Enter to add a new line' : '\n\n💡 Shift + Enter to send';
|
||||
else if (explainAltEnter)
|
||||
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Alt + Enter to just append the message');
|
||||
else if (labsBeam && explainCtrlEnter)
|
||||
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Ctrl + Enter to beam');
|
||||
}
|
||||
|
||||
return (
|
||||
<Box aria-label='User Message' component='section' sx={props.sx}>
|
||||
@@ -625,7 +659,7 @@ export function Composer(props: {
|
||||
{isSpeechEnabled && (
|
||||
<Box sx={{
|
||||
position: 'absolute', top: 0, right: 0,
|
||||
zIndex: 21,
|
||||
zIndex: zIndexComposerOverlayMic + 1,
|
||||
mt: isDesktop ? 1 : 0.25,
|
||||
mr: isDesktop ? 1 : 0.25,
|
||||
display: 'flex', flexDirection: 'column', gap: isDesktop ? 1 : 0.25,
|
||||
@@ -652,7 +686,7 @@ export function Composer(props: {
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.solidBg',
|
||||
borderRadius: 'sm',
|
||||
zIndex: 20,
|
||||
zIndex: zIndexComposerOverlayMic,
|
||||
px: 1.5, py: 1,
|
||||
}}>
|
||||
<Typography>
|
||||
@@ -696,11 +730,12 @@ export function Composer(props: {
|
||||
<Grid xs={12} md={3}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, height: '100%' } as const}>
|
||||
|
||||
{/* This row is here only for the [mobile] bottom-start corner item */}
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
{/* [mobile] This row is here only for the [mobile] bottom-start corner item */}
|
||||
{/* [desktop] This column arrangement will have the [desktop] beam button right under call */}
|
||||
<Box sx={isMobile ? { display: 'flex' } : { display: 'grid', gap: 1 }}>
|
||||
|
||||
{/* [mobile] bottom-corner secondary button */}
|
||||
{isMobile && (showCall
|
||||
{isMobile && (showChatExtras
|
||||
? <ButtonCallMemo isMobile disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />
|
||||
: isDraw
|
||||
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
@@ -709,11 +744,12 @@ export function Composer(props: {
|
||||
|
||||
{/* Responsive Send/Stop buttons */}
|
||||
<ButtonGroup
|
||||
variant={isAppend ? 'outlined' : 'solid'}
|
||||
variant={buttonVariant}
|
||||
color={buttonColor}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
boxShadow: isMobile ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
|
||||
backgroundColor: (isMobile && buttonVariant === 'outlined') ? 'background.popup' : undefined,
|
||||
boxShadow: (isMobile && buttonVariant !== 'outlined') ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
|
||||
}}
|
||||
>
|
||||
{!assistantAbortible ? (
|
||||
@@ -732,12 +768,19 @@ export function Composer(props: {
|
||||
fullWidth variant='soft' disabled={!props.conversationId}
|
||||
onClick={handleStopClicked}
|
||||
endDecorator={<StopOutlinedIcon sx={{ fontSize: 18 }} />}
|
||||
sx={{ animation: `${animationStopEnter} 0.1s ease-out` }}
|
||||
sx={{ animation: `${animationEnterBelow} 0.1s ease-out` }}
|
||||
>
|
||||
Stop
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* [Beam] Open Beam */}
|
||||
{/*{isText && <Tooltip title='Open Beam'>*/}
|
||||
{/* <IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleSendTextBeamClicked}>*/}
|
||||
{/* <ChatBeamIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/*</Tooltip>}*/}
|
||||
|
||||
{/* [Draw] Imagine */}
|
||||
{isDraw && !!composeText && <Tooltip title='Imagine a drawing prompt'>
|
||||
<IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleTextImagineClicked}>
|
||||
@@ -755,6 +798,14 @@ export function Composer(props: {
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
{/* [desktop] secondary-top buttons */}
|
||||
{labsBeam && isDesktop && showChatExtras && !assistantAbortible && (
|
||||
<ButtonBeamMemo
|
||||
disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
|
||||
onClick={handleSendTextBeamClicked}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* [desktop] Multicast switch (under the Chat button) */}
|
||||
@@ -764,7 +815,7 @@ export function Composer(props: {
|
||||
{isDesktop && <Box sx={{ mt: 'auto', display: 'grid', gap: 1 }}>
|
||||
|
||||
{/* [desktop] Call secondary button */}
|
||||
{showCall && <ButtonCallMemo disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
|
||||
{showChatExtras && <ButtonCallMemo disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
|
||||
|
||||
{/* [desktop] Draw Options secondary button */}
|
||||
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
|
||||
@@ -779,6 +830,7 @@ export function Composer(props: {
|
||||
{/* Mode selector */}
|
||||
{!!chatModeMenuAnchor && (
|
||||
<ChatModeMenu
|
||||
isMobile={isMobile}
|
||||
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
|
||||
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
|
||||
capabilityHasTTI={props.capabilityHasT2I}
|
||||
|
||||
@@ -49,7 +49,7 @@ export function ActilePopup(props: {
|
||||
const labelNormal = item.label.slice(props.activePrefixLength);
|
||||
return (
|
||||
<ListItem
|
||||
key={item.id}
|
||||
key={item.key}
|
||||
variant={isActive ? 'soft' : undefined}
|
||||
color={isActive ? 'primary' : undefined}
|
||||
onClick={() => props.onItemClick(item)}
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
import type { FunctionComponent } from 'react';
|
||||
|
||||
export interface ActileItem {
|
||||
id: string;
|
||||
key: string;
|
||||
label: string;
|
||||
argument?: string;
|
||||
description?: string;
|
||||
Icon?: FunctionComponent;
|
||||
}
|
||||
|
||||
type ActileProviderIds = 'actile-commands' | 'actile-attach-reference';
|
||||
|
||||
export interface ActileProvider {
|
||||
id: ActileProviderIds;
|
||||
title: string;
|
||||
searchPrefix: string;
|
||||
|
||||
checkTriggerText: (trailingText: string) => boolean;
|
||||
|
||||
fetchItems: () => Promise<ActileItem[]>;
|
||||
export interface ActileProvider<TItem extends ActileItem = ActileItem> {
|
||||
fastCheckTriggerText: (trailingText: string) => boolean;
|
||||
fetchItems: () => Promise<{ title: string, searchPrefix: string, items: TItem[] }>;
|
||||
onItemSelect: (item: ActileItem) => void;
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
|
||||
|
||||
/*export const providerAttachReference: ActileProvider = {
|
||||
id: 'actile-attach-reference',
|
||||
title: 'Attach Reference',
|
||||
searchPrefix: '@',
|
||||
|
||||
checkTriggerText: (trailingText: string) =>
|
||||
trailingText.endsWith(' @'),
|
||||
|
||||
fetchItems: async () => {
|
||||
return [{
|
||||
id: 'test-1',
|
||||
label: 'Attach This',
|
||||
description: 'Attach this to the message',
|
||||
Icon: undefined,
|
||||
}];
|
||||
},
|
||||
|
||||
onItemSelect: (item: ActileItem) => {
|
||||
console.log('Selected item:', item);
|
||||
},
|
||||
};*/
|
||||
@@ -2,23 +2,25 @@ import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
import { findAllChatCommands } from '../../../commands/commands.registry';
|
||||
|
||||
|
||||
export const providerCommands = (onItemSelect: (item: ActileItem) => void): ActileProvider => ({
|
||||
id: 'actile-commands',
|
||||
title: 'Chat Commands',
|
||||
searchPrefix: '/',
|
||||
export function providerCommands(onCommandSelect: (item: ActileItem) => void): ActileProvider {
|
||||
return {
|
||||
|
||||
checkTriggerText: (trailingText: string) =>
|
||||
trailingText.trim() === '/',
|
||||
// only the literal '/' is a trigger
|
||||
fastCheckTriggerText: (trailingText: string) => trailingText === '/',
|
||||
|
||||
fetchItems: async () => {
|
||||
return findAllChatCommands().map((cmd) => ({
|
||||
id: cmd.primary,
|
||||
label: cmd.primary,
|
||||
argument: cmd.arguments?.join(' ') ?? undefined,
|
||||
description: cmd.description,
|
||||
Icon: cmd.Icon,
|
||||
}));
|
||||
},
|
||||
// no real need to be async
|
||||
fetchItems: async () => ({
|
||||
title: 'Chat Commands',
|
||||
searchPrefix: '/',
|
||||
items: findAllChatCommands().map((cmd) => ({
|
||||
key: cmd.primary,
|
||||
label: cmd.primary,
|
||||
argument: cmd.arguments?.join(' ') ?? undefined,
|
||||
description: cmd.description,
|
||||
Icon: cmd.Icon,
|
||||
} satisfies ActileItem)),
|
||||
}),
|
||||
|
||||
onItemSelect,
|
||||
});
|
||||
onItemSelect: onCommandSelect,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { conversationTitle, DConversationId, messageHasUserFlag, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
|
||||
|
||||
export interface StarredMessageItem extends ActileItem {
|
||||
conversationId: DConversationId,
|
||||
messageId: string,
|
||||
}
|
||||
|
||||
export function providerStarredMessage(onMessageSeelect: (item: StarredMessageItem) => void): ActileProvider<StarredMessageItem> {
|
||||
return {
|
||||
|
||||
// only the literal '@' at start of chat, or ' @' at end of chat
|
||||
fastCheckTriggerText: (trailingText: string) => trailingText === '@' || trailingText.endsWith(' @'),
|
||||
|
||||
// finds all the starred messages in all the conversations - this could be heavy
|
||||
fetchItems: async () => {
|
||||
const { conversations } = useChatStore.getState();
|
||||
|
||||
const starredMessages: StarredMessageItem[] = [];
|
||||
conversations.forEach((conversation) => {
|
||||
conversation.messages.forEach((message) => {
|
||||
messageHasUserFlag(message, 'starred') && starredMessages.push({
|
||||
// data
|
||||
conversationId: conversation.id,
|
||||
messageId: message.id,
|
||||
// looks
|
||||
key: message.id,
|
||||
label: conversationTitle(conversation) + ' - ' + message.text.slice(0, 32) + '...',
|
||||
// description: message.text.slice(32, 100),
|
||||
Icon: undefined,
|
||||
} satisfies StarredMessageItem);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
title: 'Starred Messages',
|
||||
searchPrefix: '',
|
||||
items: starredMessages,
|
||||
};
|
||||
},
|
||||
|
||||
onItemSelect: item => onMessageSeelect(item as StarredMessageItem),
|
||||
};
|
||||
}
|
||||
@@ -9,6 +9,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
const [popupOpen, setPopupOpen] = React.useState(false);
|
||||
const [provider, setProvider] = React.useState<ActileProvider | null>(null);
|
||||
|
||||
const [title, setTitle] = React.useState<string>('');
|
||||
const [items, setItems] = React.useState<ActileItem[]>([]);
|
||||
const [activeSearchString, setActiveSearchString] = React.useState<string>('');
|
||||
const [activeItemIndex, setActiveItemIndex] = React.useState<number>(0);
|
||||
@@ -17,7 +18,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
// derived state
|
||||
const activeItems = React.useMemo(() => {
|
||||
const search = activeSearchString.trim().toLowerCase();
|
||||
return items.filter(item => item.label.toLowerCase().startsWith(search));
|
||||
return items.filter(item => item.label?.toLowerCase().startsWith(search));
|
||||
}, [items, activeSearchString]);
|
||||
const activeItem = activeItemIndex >= 0 && activeItemIndex < activeItems.length ? activeItems[activeItemIndex] : null;
|
||||
|
||||
@@ -25,6 +26,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
const handleClose = React.useCallback(() => {
|
||||
setPopupOpen(false);
|
||||
setProvider(null);
|
||||
setTitle('');
|
||||
setItems([]);
|
||||
setActiveSearchString('');
|
||||
setActiveItemIndex(0);
|
||||
@@ -42,13 +44,19 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
|
||||
const actileInterceptTextChange = React.useCallback((trailingText: string) => {
|
||||
for (const provider of providers) {
|
||||
if (provider.checkTriggerText(trailingText)) {
|
||||
setProvider(provider);
|
||||
setPopupOpen(true);
|
||||
setActiveSearchString(provider.searchPrefix);
|
||||
if (provider.fastCheckTriggerText(trailingText)) {
|
||||
provider
|
||||
.fetchItems()
|
||||
.then(items => setItems(items))
|
||||
.then(({ title, searchPrefix, items }) => {
|
||||
// if there are no items, ignore
|
||||
if (items.length) {
|
||||
setPopupOpen(true);
|
||||
setProvider(provider);
|
||||
setTitle(title);
|
||||
setItems(items);
|
||||
setActiveSearchString(searchPrefix);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
handleClose();
|
||||
console.error('Failed to fetch popup items:', error);
|
||||
@@ -100,14 +108,14 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
|
||||
<ActilePopup
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={handleClose}
|
||||
title={provider?.title}
|
||||
title={title}
|
||||
items={activeItems}
|
||||
activeItemIndex={activeItemIndex}
|
||||
activePrefixLength={activeSearchString.length}
|
||||
onItemClick={handlePopupItemClicked}
|
||||
/>
|
||||
);
|
||||
}, [activeItemIndex, activeItems, activeSearchString.length, anchorRef, handleClose, handlePopupItemClicked, popupOpen, provider?.title]);
|
||||
}, [activeItemIndex, activeItems, activeSearchString.length, anchorRef, handleClose, handlePopupItemClicked, popupOpen, title]);
|
||||
|
||||
return {
|
||||
actileComponent,
|
||||
|
||||
@@ -6,6 +6,7 @@ import CodeIcon from '@mui/icons-material/Code';
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
|
||||
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
|
||||
import PivotTableChartIcon from '@mui/icons-material/PivotTableChart';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
import TextureIcon from '@mui/icons-material/Texture';
|
||||
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
|
||||
@@ -73,6 +74,7 @@ const converterTypeToIconMap: { [key in AttachmentConverterType]: React.Componen
|
||||
'pdf-images': PictureAsPdfIcon,
|
||||
'image': ImageOutlinedIcon,
|
||||
'image-ocr': AbcIcon,
|
||||
'ego-message-md': TelegramIcon,
|
||||
'unhandled': TextureIcon,
|
||||
};
|
||||
|
||||
@@ -126,7 +128,7 @@ export function AttachmentItem(props: {
|
||||
|
||||
|
||||
const handleToggleMenu = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
onItemMenuToggle(attachment.id, event.currentTarget);
|
||||
}, [attachment, onItemMenuToggle]);
|
||||
|
||||
@@ -179,6 +181,7 @@ export function AttachmentItem(props: {
|
||||
size='sm'
|
||||
variant={variant} color={color}
|
||||
onClick={handleToggleMenu}
|
||||
onContextMenu={handleToggleMenu}
|
||||
sx={{
|
||||
backgroundColor: props.menuShown ? `${color}.softActiveBg` : variant === 'outlined' ? 'background.popup' : undefined,
|
||||
border: variant === 'soft' ? '1px solid' : undefined,
|
||||
|
||||
@@ -68,8 +68,10 @@ export function Attachments(props: {
|
||||
|
||||
const handleOverallMenuHide = () => setOverallMenuAnchor(null);
|
||||
|
||||
const handleOverallMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) =>
|
||||
const handleOverallMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
setOverallMenuAnchor(anchor => anchor ? null : event.currentTarget);
|
||||
};
|
||||
|
||||
|
||||
// overall operations
|
||||
@@ -112,6 +114,7 @@ export function Attachments(props: {
|
||||
{/* Overall Menu button */}
|
||||
<IconButton
|
||||
onClick={handleOverallMenuToggle}
|
||||
onContextMenu={handleOverallMenuToggle}
|
||||
sx={{
|
||||
// borderRadius: 'sm',
|
||||
borderRadius: 0,
|
||||
|
||||
@@ -132,6 +132,18 @@ export async function attachmentLoadInputAsync(source: Readonly<AttachmentSource
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ego':
|
||||
edit({
|
||||
label: source.label,
|
||||
ref: source.blockTitle,
|
||||
input: {
|
||||
mimeType: 'ego/message',
|
||||
data: source.textPlain,
|
||||
dataSize: source.textPlain.length,
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
edit({ inputLoading: false });
|
||||
@@ -192,6 +204,11 @@ export function attachmentDefineConverters(sourceType: AttachmentSource['media']
|
||||
converters.push({ id: 'image-ocr', name: 'As Text (OCR)' });
|
||||
break;
|
||||
|
||||
// EGO
|
||||
case input.mimeType === 'ego/message':
|
||||
converters.push({ id: 'ego-message-md', name: 'Message' });
|
||||
break;
|
||||
|
||||
// catch-all
|
||||
default:
|
||||
converters.push({ id: 'unhandled', name: `${input.mimeType}`, unsupported: true });
|
||||
@@ -333,6 +350,15 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ego-message-md':
|
||||
outputs.push({
|
||||
type: 'text-block',
|
||||
text: inputDataToString(input.data),
|
||||
title: ref,
|
||||
collapsible: true,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'unhandled':
|
||||
// force the user to explicitly select 'as text' if they want to proceed
|
||||
break;
|
||||
|
||||
@@ -24,6 +24,12 @@ export type AttachmentSource = {
|
||||
method: 'clipboard-read' | AttachmentSourceOriginDTO;
|
||||
textPlain?: string;
|
||||
textHtml?: string;
|
||||
} | {
|
||||
media: 'ego';
|
||||
method: 'ego-message';
|
||||
label: string;
|
||||
blockTitle: string;
|
||||
textPlain: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -41,6 +47,7 @@ export type AttachmentConverterType =
|
||||
| 'text' | 'rich-text' | 'rich-text-table'
|
||||
| 'pdf-text' | 'pdf-images'
|
||||
| 'image' | 'image-ocr'
|
||||
| 'ego-message-md'
|
||||
| 'unhandled';
|
||||
|
||||
export type AttachmentConverter = {
|
||||
@@ -62,7 +69,7 @@ export type Attachment = {
|
||||
readonly id: AttachmentId;
|
||||
readonly source: AttachmentSource,
|
||||
label: string;
|
||||
ref: string;
|
||||
ref: string; // will be used in ```ref\n...``` for instance
|
||||
|
||||
inputLoading: boolean;
|
||||
inputError: string | null;
|
||||
|
||||
@@ -100,6 +100,16 @@ export const useAttachments = (enableLoadURLs: boolean) => {
|
||||
}, [attachAppendFile, createAttachment, enableLoadURLs]);
|
||||
|
||||
|
||||
const attachAppendEgoMessage = React.useCallback((blockTitle: string, textPlain: string, attachmentLabel: string) => {
|
||||
if (ATTACHMENTS_DEBUG_INTAKE)
|
||||
console.log('attachAppendEgo', { blockTitle, textPlain, attachmentLabel });
|
||||
|
||||
return createAttachment({
|
||||
media: 'ego', method: 'ego-message', label: attachmentLabel, blockTitle: blockTitle, textPlain: textPlain,
|
||||
});
|
||||
}, [createAttachment]);
|
||||
|
||||
|
||||
const attachAppendClipboardItems = React.useCallback(async () => {
|
||||
|
||||
// if there's an issue accessing the clipboard, show it passively
|
||||
@@ -178,6 +188,7 @@ export const useAttachments = (enableLoadURLs: boolean) => {
|
||||
// create attachments
|
||||
attachAppendClipboardItems,
|
||||
attachAppendDataTransfer,
|
||||
attachAppendEgoMessage,
|
||||
attachAppendFile,
|
||||
|
||||
// manage attachments
|
||||
|
||||
@@ -131,11 +131,13 @@ function attachmentCollapseOutputs(initialTextBlockText: string | null, outputs:
|
||||
// start a new part
|
||||
else {
|
||||
if (output.type === 'text-block') {
|
||||
// THIS IS NOT CORRECT - we seem to be doing it just for downstream token counting - FIX IT
|
||||
// Do not serialize here
|
||||
accumulatedOutputs.push({
|
||||
type: 'text-block',
|
||||
text: `\n\n\`\`\`${output.title}\n${output.text}\n\`\`\``,
|
||||
title: null,
|
||||
collapsible: false,
|
||||
collapsible: false, // Wrong
|
||||
});
|
||||
} else {
|
||||
accumulatedOutputs.push(output);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
const desktopLegend =
|
||||
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
|
||||
Combine the answers from multiple models<br />
|
||||
{/*{platformAwareKeystrokes('Ctrl + Enter')}*/}
|
||||
<KeyStroke combo='Ctrl + Enter' sx={{ mt: 0.5, mb: 0.25 }} />
|
||||
</Box>;
|
||||
|
||||
const mobileSx: SxProps = {
|
||||
mr: { xs: 1, md: 2 },
|
||||
};
|
||||
|
||||
const desktopSx: SxProps = {
|
||||
'--Button-gap': '1rem',
|
||||
backgroundColor: 'background.popup',
|
||||
// border: '1px solid',
|
||||
// borderColor: 'primary.outlinedBorder',
|
||||
boxShadow: '0 4px 16px -4px rgb(var(--joy-palette-primary-mainChannel) / 10%)',
|
||||
animation: `${animationEnterBelow} 0.1s ease-out`,
|
||||
};
|
||||
|
||||
|
||||
export const ButtonBeamMemo = React.memo(ButtonBeam);
|
||||
|
||||
function ButtonBeam(props: { isMobile?: boolean, disabled?: boolean, onClick: () => void }) {
|
||||
return props.isMobile ? (
|
||||
<IconButton variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
|
||||
<ChatBeamIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tooltip disableInteractive variant='solid' arrow placement='right' title={desktopLegend}>
|
||||
<Button variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} endDecorator={<ChatBeamIcon />} sx={desktopSx}>
|
||||
Beam
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,7 @@ const desktopSx: SxProps = {
|
||||
|
||||
export const ButtonCallMemo = React.memo(ButtonCall);
|
||||
|
||||
export function ButtonCall(props: { isMobile?: boolean, disabled?: boolean, onClick: () => void }) {
|
||||
function ButtonCall(props: { isMobile?: boolean, disabled?: boolean, onClick: () => void }) {
|
||||
return props.isMobile ? (
|
||||
<IconButton variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
|
||||
<CallIcon />
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ButtonMultiChat(props: { isMobile?: boolean, multiChat: boolean,
|
||||
<FormControl orientation='horizontal' sx={{ minHeight: '2.25rem', justifyContent: 'space-between' }}>
|
||||
<FormLabel sx={{ gap: 1, flexFlow: 'row nowrap' }}>
|
||||
<Box sx={{ display: { xs: 'none', lg: 'inline-block' } }}>
|
||||
{multiChat ? <ChatMulticastOnIcon sx={{ color: 'warning.solidBg' }} /> : <ChatMulticastOffIcon />}
|
||||
{multiChat ? <ChatMulticastOnIcon color='primary' /> : <ChatMulticastOffIcon />}
|
||||
</Box>
|
||||
{multiChat ? 'Multichat · On' : 'Multichat'}
|
||||
</FormLabel>
|
||||
|
||||
@@ -2,13 +2,13 @@ import * as React from 'react';
|
||||
|
||||
import { Button, IconButton } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
|
||||
|
||||
export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => void, sx?: SxProps }) {
|
||||
return props.isMobile ? (
|
||||
<IconButton variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
|
||||
<FormatPaintIcon />
|
||||
<FormatPaintTwoToneIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { List, ListItem, ListItemButton, ListItemDecorator, Sheet } from '@mui/joy';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
import { StrictModeDroppable } from '~/common/components/StrictModeDroppable';
|
||||
|
||||
import { AddFolderButton } from './AddFolderButton';
|
||||
import { FolderListItem } from './FolderListItem';
|
||||
import { StrictModeDroppable } from './StrictModeDroppable';
|
||||
|
||||
|
||||
export function ChatFolderList(props: {
|
||||
@@ -17,6 +18,7 @@ export function ChatFolderList(props: {
|
||||
contentScaling: ContentScaling;
|
||||
activeFolderId: string | null;
|
||||
onFolderSelect: (folderId: string | null) => void;
|
||||
sx?: SxProps;
|
||||
}) {
|
||||
|
||||
// derived props
|
||||
@@ -31,13 +33,18 @@ export function ChatFolderList(props: {
|
||||
|
||||
|
||||
return (
|
||||
<Sheet variant='soft' sx={{ p: 2 }}>
|
||||
<Sheet variant='soft' sx={props.sx}>
|
||||
<List
|
||||
variant='plain'
|
||||
sx={(theme) => ({
|
||||
// added to be responsive to parent's layout sizing
|
||||
height: '100%',
|
||||
overflowY: 'auto',
|
||||
|
||||
// original list properties
|
||||
'& ul': {
|
||||
'--List-gap': '0px',
|
||||
bgcolor: 'background.surface',
|
||||
bgcolor: 'background.popup',
|
||||
'& > li:first-of-type > [role="button"]': {
|
||||
borderTopRightRadius: 'var(--List-radius)',
|
||||
borderTopLeftRadius: 'var(--List-radius)',
|
||||
@@ -131,6 +138,6 @@ export function ChatFolderList(props: {
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
</Sheet>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { FormLabel, IconButton, ListItem, ListItemButton, ListItemContent, ListI
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import Done from '@mui/icons-material/Done';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
@@ -214,7 +214,7 @@ export function FolderListItem(props: {
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<EditIcon />
|
||||
<EditRoundedIcon />
|
||||
</ListItemDecorator>
|
||||
Edit
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Droppable, DroppableProps } from "react-beautiful-dnd";
|
||||
|
||||
export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const animation = requestAnimationFrame(() => setEnabled(true));
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animation);
|
||||
setEnabled(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Droppable {...props}>{children}</Droppable>;
|
||||
};
|
||||
|
||||
@@ -3,19 +3,21 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Avatar, Box, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import AccountTreeTwoToneIcon from '@mui/icons-material/AccountTreeTwoTone';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import DifferenceIcon from '@mui/icons-material/Difference';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import Face6Icon from '@mui/icons-material/Face6';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
|
||||
|
||||
@@ -26,12 +28,13 @@ import { useSanityTextDiffs } from '~/modules/blocks/RenderTextDiff';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { DMessage } from '~/common/state/store-chats';
|
||||
import { DMessage, DMessageUserFlag, messageHasUserFlag } from '~/common/state/store-chats';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { adjustContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { cssRainbowColorKeyframes, themeScalingMap } from '~/common/app.theme';
|
||||
import { prettyBaseModel } from '~/common/util/modelUtils';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
@@ -59,11 +62,31 @@ export function messageBackground(messageRole: DMessage['role'] | string, wasEdi
|
||||
}
|
||||
}
|
||||
|
||||
const avatarIconSx = { width: 36, height: 36 };
|
||||
const avatarIconSx = {
|
||||
width: 36,
|
||||
height: 36,
|
||||
};
|
||||
|
||||
const personaSx: SxProps = {
|
||||
// make this stick to the top of the screen
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
|
||||
// flexBasis: 0, // this won't let the item grow
|
||||
minWidth: { xs: 50, md: 64 },
|
||||
maxWidth: 80,
|
||||
textAlign: 'center',
|
||||
// layout
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
|
||||
export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['role'] | string, messageOriginLLM: string | undefined, messagePurposeId: SystemPurposeId | undefined, messageSender: string, messageTyping: boolean, size: 'sm' | undefined = undefined): React.JSX.Element {
|
||||
if (typeof messageAvatar === 'string' && messageAvatar)
|
||||
return <Avatar alt={messageSender} src={messageAvatar} />;
|
||||
|
||||
const mascotSx = size === 'sm' ? avatarIconSx : { width: 64, height: 64 };
|
||||
switch (messageRole) {
|
||||
case 'system':
|
||||
@@ -74,36 +97,40 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
|
||||
|
||||
case 'assistant':
|
||||
// typing gif (people seem to love this, so keeping it after april fools')
|
||||
const isDownload = messageOriginLLM === 'web';
|
||||
const isTextToImage = messageOriginLLM === 'DALL·E' || messageOriginLLM === 'Prodia';
|
||||
const isReact = messageOriginLLM?.startsWith('react-');
|
||||
if (messageTyping) {
|
||||
|
||||
// animation: message typing
|
||||
if (messageTyping)
|
||||
return <Avatar
|
||||
alt={messageSender} variant='plain'
|
||||
src={isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
|
||||
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
|
||||
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'}
|
||||
src={isDownload ? 'https://i.giphy.com/26u6dIwIphLj8h10A.webp' // hourglass: https://i.giphy.com/TFSxpAIYz5inJGuY8f.webp, small-lq: https://i.giphy.com/131tNuGktpXGhy.webp, floppy: https://i.giphy.com/RxR1KghIie2iI.webp
|
||||
: isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp' // brush
|
||||
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp' // mind
|
||||
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'} // typing
|
||||
sx={{ ...mascotSx, borderRadius: 'sm' }}
|
||||
/>;
|
||||
}
|
||||
|
||||
// text-to-image: icon
|
||||
// icon: text-to-image
|
||||
if (isTextToImage)
|
||||
return <FormatPaintIcon sx={{
|
||||
return <FormatPaintTwoToneIcon sx={{
|
||||
...avatarIconSx,
|
||||
animation: `${cssRainbowColorKeyframes} 1s linear 2.66`,
|
||||
animation: `${animationColorRainbow} 1s linear 2.66`,
|
||||
}} />;
|
||||
|
||||
// purpose symbol (if present)
|
||||
const symbol = SystemPurposes[messagePurposeId!]?.symbol;
|
||||
if (symbol) return <Box sx={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
minWidth: `${avatarIconSx.width}px`,
|
||||
lineHeight: `${avatarIconSx.height}px`,
|
||||
}}>
|
||||
{symbol}
|
||||
</Box>;
|
||||
if (symbol)
|
||||
return <Box sx={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
minWidth: `${avatarIconSx.width}px`,
|
||||
lineHeight: `${avatarIconSx.height}px`,
|
||||
}}>
|
||||
{symbol}
|
||||
</Box>;
|
||||
|
||||
// default assistant avatar
|
||||
return <SmartToyOutlinedIcon sx={avatarIconSx} />; // https://mui.com/static/images/avatar/2.jpg
|
||||
@@ -189,12 +216,18 @@ export function ChatMessage(props: {
|
||||
isBottom?: boolean,
|
||||
isImagining?: boolean,
|
||||
isSpeaking?: boolean,
|
||||
blocksShowDate?: boolean,
|
||||
onConversationBranch?: (messageId: string) => void,
|
||||
onConversationRestartFrom?: (messageId: string, offset: number, chatEffectBeam: boolean) => Promise<void>,
|
||||
onConversationTruncate?: (messageId: string) => void,
|
||||
showAvatar?: boolean, // auto if undefined
|
||||
showBlocksDate?: boolean,
|
||||
showUnsafeHtml?: boolean,
|
||||
adjustContentScaling?: number,
|
||||
topDecorator?: React.ReactNode,
|
||||
onMessageAssistantFrom?: (messageId: string, offset: number) => Promise<void>,
|
||||
onMessageBeam?: (messageId: string) => Promise<void>,
|
||||
onMessageBranch?: (messageId: string) => void,
|
||||
onMessageDelete?: (messageId: string) => void,
|
||||
onMessageEdit?: (messageId: string, text: string) => void,
|
||||
onMessageToggleUserFlag?: (messageId: string, flag: DMessageUserFlag) => void,
|
||||
onMessageTruncate?: (messageId: string) => void,
|
||||
onTextDiagram?: (messageId: string, text: string) => Promise<void>
|
||||
onTextImagine?: (text: string) => Promise<void>
|
||||
onTextSpeak?: (text: string) => Promise<void>
|
||||
@@ -209,10 +242,10 @@ export function ChatMessage(props: {
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const labsChatBeam = useUXLabsStore(state => state.labsChatBeam);
|
||||
const { cleanerLooks, contentScaling, doubleClickToEdit, renderMarkdown } = useUIPreferencesStore(state => ({
|
||||
cleanerLooks: state.zenMode === 'cleaner',
|
||||
contentScaling: state.contentScaling,
|
||||
const labsBeam = useUXLabsStore(state => state.labsBeam);
|
||||
const { showAvatar, contentScaling, doubleClickToEdit, renderMarkdown } = useUIPreferencesStore(state => ({
|
||||
showAvatar: props.showAvatar !== undefined ? props.showAvatar : state.zenMode !== 'cleaner',
|
||||
contentScaling: adjustContentScaling(state.contentScaling, props.adjustContentScaling),
|
||||
doubleClickToEdit: state.doubleClickToEdit,
|
||||
renderMarkdown: state.renderMarkdown,
|
||||
}), shallow);
|
||||
@@ -233,12 +266,12 @@ export function ChatMessage(props: {
|
||||
updated: messageUpdated,
|
||||
} = props.message;
|
||||
|
||||
const isUserStarred = messageHasUserFlag(props.message, 'starred');
|
||||
|
||||
const fromAssistant = messageRole === 'assistant';
|
||||
const fromSystem = messageRole === 'system';
|
||||
const wasEdited = !!messageUpdated;
|
||||
|
||||
const showAvatars = !cleanerLooks;
|
||||
|
||||
const textSel = selMenuText ? selMenuText : messageText;
|
||||
const isSpecialT2I = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/draw ') || textSel.startsWith('/imagine ') || textSel.startsWith('/img ');
|
||||
const couldDiagram = textSel?.length >= 100 && !isSpecialT2I;
|
||||
@@ -255,6 +288,8 @@ export function ChatMessage(props: {
|
||||
|
||||
// Operations Menu
|
||||
|
||||
const { onMessageToggleUserFlag } = props;
|
||||
|
||||
const closeOpsMenu = () => setOpsMenuAnchor(null);
|
||||
|
||||
const handleOpsCopy = (e: React.MouseEvent) => {
|
||||
@@ -271,23 +306,27 @@ export function ChatMessage(props: {
|
||||
closeOpsMenu();
|
||||
}, [isEditing, messageTyping]);
|
||||
|
||||
const handleOpsConversationBranch = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // to try to not steal the focus from the banched conversation
|
||||
props.onConversationBranch && props.onConversationBranch(messageId);
|
||||
closeOpsMenu();
|
||||
};
|
||||
const handleOpsToggleStarred = React.useCallback(() => {
|
||||
onMessageToggleUserFlag?.(messageId, 'starred');
|
||||
}, [messageId, onMessageToggleUserFlag]);
|
||||
|
||||
const handleOpsConversationRestartFrom = async (e: React.MouseEvent) => {
|
||||
const handleOpsAssistantFrom = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
closeOpsMenu();
|
||||
props.onConversationRestartFrom && await props.onConversationRestartFrom(messageId, fromAssistant ? -1 : 0, false);
|
||||
await props.onMessageAssistantFrom?.(messageId, fromAssistant ? -1 : 0);
|
||||
};
|
||||
|
||||
const handleOpsConversationRestartFromBeam = async (e: React.MouseEvent) => {
|
||||
const handleOpsBeamFrom = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
closeOpsMenu();
|
||||
props.onConversationRestartFrom && labsChatBeam && await props.onConversationRestartFrom(messageId, fromAssistant ? -1 : 0, true);
|
||||
labsBeam && await props.onMessageBeam?.(messageId);
|
||||
};
|
||||
|
||||
const handleOpsBranch = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // to try to not steal the focus from the banched conversation
|
||||
props.onMessageBranch?.(messageId);
|
||||
closeOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsToggleShowDiff = () => setShowDiff(!showDiff);
|
||||
@@ -320,12 +359,12 @@ export function ChatMessage(props: {
|
||||
};
|
||||
|
||||
const handleOpsTruncate = (_e: React.MouseEvent) => {
|
||||
props.onConversationTruncate && props.onConversationTruncate(messageId);
|
||||
props.onMessageTruncate?.(messageId);
|
||||
closeOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsDelete = (_e: React.MouseEvent) => {
|
||||
props.onMessageDelete && props.onMessageDelete(messageId);
|
||||
props.onMessageDelete?.(messageId);
|
||||
};
|
||||
|
||||
|
||||
@@ -399,55 +438,84 @@ export function ChatMessage(props: {
|
||||
|
||||
// avatar
|
||||
const avatarEl: React.JSX.Element | null = React.useMemo(
|
||||
() => showAvatars ? makeAvatar(messageAvatar, messageRole, messageOriginLLM, messagePurposeId, messageSender, messageTyping) : null,
|
||||
[messageAvatar, messageOriginLLM, messagePurposeId, messageRole, messageSender, messageTyping, showAvatars],
|
||||
() => showAvatar ? makeAvatar(messageAvatar, messageRole, messageOriginLLM, messagePurposeId, messageSender, messageTyping) : null,
|
||||
[messageAvatar, messageOriginLLM, messagePurposeId, messageRole, messageSender, messageTyping, showAvatar],
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
role='chat-message'
|
||||
sx={{
|
||||
display: 'flex', flexDirection: !fromAssistant ? 'row-reverse' : 'row', alignItems: 'flex-start',
|
||||
gap: { xs: 0, md: 1 },
|
||||
// style
|
||||
backgroundColor: backgroundColor,
|
||||
px: { xs: 1, md: themeScalingMap[contentScaling]?.chatMessagePadding ?? 2 },
|
||||
py: themeScalingMap[contentScaling]?.chatMessagePadding ?? 2,
|
||||
backgroundColor,
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
...(ENABLE_COPY_MESSAGE_OVERLAY && { position: 'relative' }),
|
||||
|
||||
// style: omit border if set externally
|
||||
...(!('borderBottom' in (props.sx || {})) && {
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
}),
|
||||
|
||||
// style: when starred
|
||||
...(isUserStarred && {
|
||||
outline: '3px solid',
|
||||
outlineColor: 'primary.solidBg',
|
||||
boxShadow: 'lg',
|
||||
borderRadius: 'lg',
|
||||
zIndex: 1,
|
||||
}),
|
||||
|
||||
// style: make room for a top decorator if set
|
||||
...(!!props.topDecorator && {
|
||||
pt: '2.5rem',
|
||||
}),
|
||||
'&:hover > button': { opacity: 1 },
|
||||
|
||||
// layout
|
||||
display: 'flex',
|
||||
flexDirection: !fromAssistant ? 'row-reverse' : 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: { xs: 0, md: 1 },
|
||||
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Avatar */}
|
||||
{showAvatars && (
|
||||
<Box
|
||||
onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}
|
||||
onClick={event => setOpsMenuAnchor(event.currentTarget)}
|
||||
sx={{
|
||||
// flexBasis: 0, // this won't let the item grow
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
minWidth: { xs: 50, md: 64 },
|
||||
maxWidth: 80,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
{/* (Optional) underlayed top decorator */}
|
||||
{props.topDecorator && (
|
||||
<Box sx={{ position: 'absolute', left: 0, right: 0, top: 0, textAlign: 'center' }}>
|
||||
{props.topDecorator}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{isHovering ? (
|
||||
<IconButton variant='soft' color={(fromAssistant || fromSystem) ? 'neutral' : 'primary'} sx={avatarIconSx}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
avatarEl
|
||||
)}
|
||||
{/* Avatar (Persona) */}
|
||||
{showAvatar && (
|
||||
<Box sx={personaSx}>
|
||||
|
||||
{/* Persona Avatar or Menu Button */}
|
||||
<Box
|
||||
onClick={event => setOpsMenuAnchor(event.currentTarget)}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
sx={{ display: 'flex' }}
|
||||
>
|
||||
{(isHovering || opsMenuAnchor) ? (
|
||||
<IconButton variant={opsMenuAnchor ? 'solid' : 'soft'} color={(fromAssistant || fromSystem) ? 'neutral' : 'primary'} sx={avatarIconSx}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
avatarEl
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Assistant model name */}
|
||||
{fromAssistant && (
|
||||
<Tooltip title={messageTyping ? null : (messageOriginLLM || 'unk-model')} variant='solid'>
|
||||
<Tooltip arrow title={messageTyping ? null : (messageOriginLLM || 'unk-model')} variant='solid'>
|
||||
<Typography level='body-xs' sx={{
|
||||
overflowWrap: 'anywhere',
|
||||
...(messageTyping ? { animation: `${cssRainbowColorKeyframes} 5s linear infinite` } : {}),
|
||||
...(messageTyping ? { animation: `${animationColorRainbow} 5s linear infinite` } : {}),
|
||||
}}>
|
||||
{prettyBaseModel(messageOriginLLM)}
|
||||
</Typography>
|
||||
@@ -477,7 +545,8 @@ export function ChatMessage(props: {
|
||||
isBottom={props.isBottom}
|
||||
renderTextAsMarkdown={renderMarkdown}
|
||||
renderTextDiff={textDiffs || undefined}
|
||||
showDate={props.blocksShowDate === true ? messageUpdated || messageCreated || undefined : undefined}
|
||||
showDate={props.showBlocksDate === true ? messageUpdated || messageCreated || undefined : undefined}
|
||||
showUnsafeHtml={props.showUnsafeHtml}
|
||||
wasUserEdited={wasEdited}
|
||||
onContextMenu={(props.onMessageEdit && ENABLE_SELECTION_RIGHT_CLICK_MENU) ? handleBlocksContextMenu : undefined}
|
||||
onDoubleClick={(props.onMessageEdit && doubleClickToEdit) ? handleBlocksDoubleClick : undefined}
|
||||
@@ -520,29 +589,33 @@ export function ChatMessage(props: {
|
||||
|
||||
{/* Edit / Copy */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{/* Edit */}
|
||||
{!!props.onMessageEdit && (
|
||||
<MenuItem variant='plain' disabled={messageTyping} onClick={handleOpsEdit} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator><EditIcon /></ListItemDecorator>
|
||||
<ListItemDecorator><EditRoundedIcon /></ListItemDecorator>
|
||||
{isEditing ? 'Discard' : 'Edit'}
|
||||
{/*{!isEditing && <span style={{ opacity: 0.5, marginLeft: '8px' }}>{doubleClickToEdit ? '(double-click)' : ''}</span>}*/}
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Copy */}
|
||||
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
|
||||
Copy
|
||||
</MenuItem>
|
||||
{/* Starred */}
|
||||
{!!onMessageToggleUserFlag && (
|
||||
<MenuItem onClick={handleOpsToggleStarred} sx={{ flexGrow: 0, px: 1 }}>
|
||||
{isUserStarred
|
||||
? <StarRoundedIcon color='primary' sx={{ fontSize: 'xl2' }} />
|
||||
: <StarOutlineRoundedIcon sx={{ fontSize: 'xl2' }} />
|
||||
}
|
||||
</MenuItem>
|
||||
)}
|
||||
</Box>
|
||||
{/* Delete / Branch / Truncate */}
|
||||
{!!props.onMessageDelete && <ListDivider />}
|
||||
{!!props.onMessageDelete && (
|
||||
<MenuItem onClick={handleOpsDelete} disabled={false /*fromSystem*/}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Delete
|
||||
<span style={{ opacity: 0.5 }}>message</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onConversationBranch && (
|
||||
<MenuItem onClick={handleOpsConversationBranch} disabled={fromSystem}>
|
||||
{!!props.onMessageBranch && <ListDivider />}
|
||||
{!!props.onMessageBranch && (
|
||||
<MenuItem onClick={handleOpsBranch} disabled={fromSystem}>
|
||||
<ListItemDecorator>
|
||||
<ForkRightIcon />
|
||||
</ListItemDecorator>
|
||||
@@ -550,7 +623,14 @@ export function ChatMessage(props: {
|
||||
{!props.isBottom && <span style={{ opacity: 0.5 }}>from here</span>}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onConversationTruncate && (
|
||||
{!!props.onMessageDelete && (
|
||||
<MenuItem onClick={handleOpsDelete} disabled={false /*fromSystem*/}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Delete
|
||||
<span style={{ opacity: 0.5 }}>message</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onMessageTruncate && (
|
||||
<MenuItem onClick={handleOpsTruncate} disabled={props.isBottom}>
|
||||
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
|
||||
Truncate
|
||||
@@ -570,47 +650,44 @@ export function ChatMessage(props: {
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && (
|
||||
<MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
|
||||
<ListItemDecorator><AccountTreeIcon color='success' /></ListItemDecorator>
|
||||
Diagram ...
|
||||
<ListItemDecorator><AccountTreeTwoToneIcon /></ListItemDecorator>
|
||||
Auto-Diagram ...
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextImagine && (
|
||||
<MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
|
||||
Draw ...
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintTwoToneIcon />}</ListItemDecorator>
|
||||
Auto-Draw
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextSpeak && (
|
||||
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverTwoToneIcon />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Restart/try */}
|
||||
{!!props.onConversationRestartFrom && <ListDivider />}
|
||||
{!!props.onConversationRestartFrom && (
|
||||
<MenuItem onClick={handleOpsConversationRestartFrom}>
|
||||
{/* Beam/Restart */}
|
||||
{(!!props.onMessageAssistantFrom || !!props.onMessageBeam) && <ListDivider />}
|
||||
{!!props.onMessageAssistantFrom && (
|
||||
<MenuItem disabled={fromSystem} onClick={handleOpsAssistantFrom}>
|
||||
<ListItemDecorator>{fromAssistant ? <ReplayIcon color='primary' /> : <TelegramIcon color='primary' />}</ListItemDecorator>
|
||||
{!fromAssistant
|
||||
? <>Restart <span style={{ opacity: 0.5 }}>from here</span></>
|
||||
: !props.isBottom
|
||||
? <>Retry <span style={{ opacity: 0.5 }}>from here</span></>
|
||||
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Retry
|
||||
<KeyStroke combo='Ctrl + Shift + R' />
|
||||
</Box>}
|
||||
{labsChatBeam && (
|
||||
<Tooltip title={messageTyping ? null : 'Best-Of'}>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='outlined' color='primary'
|
||||
onClick={handleOpsConversationRestartFromBeam}
|
||||
sx={{ ml: 'auto', my: '-0.25rem' /* absorb the menuItem padding */ }}
|
||||
>
|
||||
<ChatBeamIcon /> {/*<GavelIcon />*/}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>Retry<KeyStroke combo='Ctrl + Shift + R' /></Box>}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onMessageBeam && labsBeam && (
|
||||
<MenuItem disabled={fromSystem} onClick={handleOpsBeamFrom}>
|
||||
<ListItemDecorator>
|
||||
<ChatBeamIcon color={fromSystem ? undefined : 'primary'} />
|
||||
</ListItemDecorator>
|
||||
{!fromAssistant
|
||||
? <>Beam <span style={{ opacity: 0.5 }}>from here</span></>
|
||||
: !props.isBottom
|
||||
? <>Beam <span style={{ opacity: 0.5 }}>this message</span></>
|
||||
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>Beam<KeyStroke combo='Ctrl + Shift + B' /></Box>}
|
||||
</MenuItem>
|
||||
)}
|
||||
</CloseableMenu>
|
||||
@@ -623,20 +700,21 @@ export function ChatMessage(props: {
|
||||
open anchorEl={selMenuAnchor} onClose={closeSelectionMenu}
|
||||
sx={{ minWidth: 220 }}
|
||||
>
|
||||
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1 }}>
|
||||
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1, alignItems: 'center' }}>
|
||||
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
|
||||
Copy <span style={{ opacity: 0.5 }}>selection</span>
|
||||
Copy
|
||||
</MenuItem>
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && <MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram || props.isImagining}>
|
||||
<ListItemDecorator><AccountTreeIcon color='success' /></ListItemDecorator>
|
||||
Diagram ...
|
||||
<ListItemDecorator><AccountTreeTwoToneIcon /></ListItemDecorator>
|
||||
Auto-Diagram ...
|
||||
</MenuItem>}
|
||||
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
|
||||
Imagine
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintTwoToneIcon />}</ListItemDecorator>
|
||||
Auto-Draw
|
||||
</MenuItem>}
|
||||
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverTwoToneIcon />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>}
|
||||
</CloseableMenu>
|
||||
|
||||
@@ -16,7 +16,7 @@ import { makeAvatar, messageBackground } from './ChatMessage';
|
||||
export const MessagesSelectionHeader = (props: { hasSelected: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
|
||||
<Sheet color='warning' variant='solid' invertedColors sx={{
|
||||
display: 'flex', flexDirection: 'row', alignItems: 'center',
|
||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101,
|
||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101 /* Cleanup Selection Header on top of messages */,
|
||||
boxShadow: 'md',
|
||||
gap: { xs: 1, sm: 2 }, px: { xs: 1, md: 2 }, py: 1,
|
||||
}}>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
@@ -9,25 +10,36 @@ import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
// change this to increase/decrease the number history steps per pane
|
||||
const MAX_HISTORY_LENGTH = 10;
|
||||
|
||||
// change this to allow for more/less panes
|
||||
const MAX_CONCURRENT_PANES = 4;
|
||||
|
||||
// change to true to enable verbose console logging
|
||||
const DEBUG_PANES_MANAGER = false;
|
||||
|
||||
|
||||
interface ChatPane {
|
||||
|
||||
paneId: string;
|
||||
|
||||
conversationId: DConversationId | null;
|
||||
|
||||
// other per-pane storage? or would this be cluttering the panes(view)-only abstaction?
|
||||
// ... we are currently creating companion ConversationHandler obects for this
|
||||
|
||||
history: DConversationId[]; // History of the conversationIds for this pane
|
||||
historyIndex: number; // Current position in the history for this pane
|
||||
|
||||
}
|
||||
|
||||
interface AppChatPanesStore {
|
||||
interface AppChatPanesState {
|
||||
|
||||
// state
|
||||
chatPanes: ChatPane[];
|
||||
chatPaneFocusIndex: number | null;
|
||||
|
||||
}
|
||||
|
||||
interface AppChatPanesStore extends AppChatPanesState {
|
||||
|
||||
// actions
|
||||
openConversationInFocusedPane: (conversationId: DConversationId) => void;
|
||||
openConversationInSplitPane: (conversationId: DConversationId) => void;
|
||||
@@ -35,19 +47,29 @@ interface AppChatPanesStore {
|
||||
duplicateFocusedPane: (/*paneIndex: number*/) => void;
|
||||
removeOtherPanes: () => void;
|
||||
removePane: (paneIndex: number) => void;
|
||||
setFocusedPane: (paneIndex: number) => void;
|
||||
onConversationsChanged: (conversationIds: DConversationId[]) => void;
|
||||
setFocusedPaneIndex: (paneIndex: number) => void;
|
||||
_onConversationsChanged: (conversationIds: DConversationId[]) => void;
|
||||
|
||||
}
|
||||
|
||||
function createPane(conversationId: DConversationId | null = null): ChatPane {
|
||||
return {
|
||||
paneId: uuidv4(),
|
||||
conversationId,
|
||||
history: conversationId ? [conversationId] : [],
|
||||
historyIndex: conversationId ? 0 : -1,
|
||||
};
|
||||
}
|
||||
|
||||
function duplicatePane(pane: ChatPane): ChatPane {
|
||||
return {
|
||||
paneId: uuidv4(),
|
||||
conversationId: pane.conversationId,
|
||||
history: [...pane.history],
|
||||
historyIndex: pane.historyIndex,
|
||||
};
|
||||
}
|
||||
|
||||
const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
(_set, _get) => ({
|
||||
|
||||
@@ -68,8 +90,14 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
};
|
||||
}
|
||||
|
||||
// Check if the conversation is already open in the focused pane.
|
||||
// Sanity check: Get the focused pane
|
||||
const focusedPane = chatPanes[chatPaneFocusIndex];
|
||||
if (!focusedPane) {
|
||||
console.warn('openConversationInFocusedPane: focusedPane is null', chatPaneFocusIndex, chatPanes);
|
||||
return state;
|
||||
}
|
||||
|
||||
// Check if the conversation is already open in the focused pane.
|
||||
if (focusedPane.conversationId === conversationId) {
|
||||
if (DEBUG_PANES_MANAGER)
|
||||
console.log(`open-focuses: ${conversationId} is open in focused pane`, chatPaneFocusIndex, chatPanes);
|
||||
@@ -80,7 +108,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
const truncatedHistory = focusedPane.history.slice(0, focusedPane.historyIndex + 1);
|
||||
const newHistory = [...truncatedHistory, conversationId].slice(-MAX_HISTORY_LENGTH);
|
||||
|
||||
// Update the focused pane with the new conversation.
|
||||
// Update the focused pane with the new conversation and history.
|
||||
const newPanes = [...chatPanes];
|
||||
newPanes[chatPaneFocusIndex] = {
|
||||
...focusedPane,
|
||||
@@ -103,21 +131,30 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
// Open a conversation in a new pane, reusing an existing pane if possible.
|
||||
const { chatPanes, chatPaneFocusIndex, openConversationInFocusedPane } = _get();
|
||||
|
||||
// one pane open: split it
|
||||
if (chatPanes.length === 1) {
|
||||
_set({
|
||||
chatPanes: Array.from({ length: 2 }, () => ({ ...chatPanes[0] })),
|
||||
chatPaneFocusIndex: 1,
|
||||
});
|
||||
// Copy from the focused pane, if there's one
|
||||
const focusedPane = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex] ?? null : null;
|
||||
|
||||
// if fewer than the maximum panes, create a new pane and focus it
|
||||
if (chatPanes.length < MAX_CONCURRENT_PANES) {
|
||||
const insertIndex = chatPaneFocusIndex !== null ? chatPaneFocusIndex + 1 : chatPanes.length;
|
||||
_set((state) => ({
|
||||
chatPanes: [
|
||||
...state.chatPanes.slice(0, insertIndex),
|
||||
focusedPane ? duplicatePane(focusedPane) : createPane(null),
|
||||
...state.chatPanes.slice(insertIndex),
|
||||
],
|
||||
chatPaneFocusIndex: insertIndex,
|
||||
}));
|
||||
}
|
||||
// more than 2 panes, reuse the alt pane
|
||||
else if (chatPanes.length >= 2 && chatPaneFocusIndex !== null) {
|
||||
// max reached, replace the next pane (with wraparound) - note the outside logic won't get us here
|
||||
else {
|
||||
const replaceIndex = (chatPaneFocusIndex !== null ? chatPaneFocusIndex + 1 : 0) % MAX_CONCURRENT_PANES;
|
||||
_set({
|
||||
chatPaneFocusIndex: chatPaneFocusIndex === 0 ? 1 : 0,
|
||||
chatPaneFocusIndex: replaceIndex,
|
||||
});
|
||||
}
|
||||
|
||||
// will create a pane if none exists, or load the conversation in the focused pane
|
||||
// Open the conversation in the newly created or updated pane
|
||||
openConversationInFocusedPane(conversationId);
|
||||
|
||||
if (DEBUG_PANES_MANAGER)
|
||||
@@ -171,21 +208,18 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
|
||||
// Clone the pane at the specified index, including a deep copy of the history array
|
||||
const paneToDuplicate = chatPanes[_srcIndex];
|
||||
const duplicatedPane = {
|
||||
...paneToDuplicate,
|
||||
history: [...paneToDuplicate.history], // Deep copy of the history array
|
||||
};
|
||||
const dstIndex = _srcIndex + 1;
|
||||
|
||||
// Insert the duplicated pane into the array, right after the original pane
|
||||
const newPanes = [
|
||||
...chatPanes.slice(0, _srcIndex + 1),
|
||||
duplicatedPane,
|
||||
...chatPanes.slice(_srcIndex + 1),
|
||||
...chatPanes.slice(0, dstIndex),
|
||||
duplicatePane(paneToDuplicate),
|
||||
...chatPanes.slice(dstIndex),
|
||||
];
|
||||
|
||||
return {
|
||||
chatPanes: newPanes,
|
||||
chatPaneFocusIndex: _srcIndex + 1,
|
||||
chatPaneFocusIndex: dstIndex,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -217,7 +251,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
};
|
||||
}),
|
||||
|
||||
setFocusedPane: (paneIndex: number) =>
|
||||
setFocusedPaneIndex: (paneIndex: number) =>
|
||||
_set(state => {
|
||||
if (state.chatPaneFocusIndex === paneIndex)
|
||||
return state;
|
||||
@@ -232,7 +266,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
* It takes care of `creating the first pane` as well as `removing invalid history items, reassiging
|
||||
* conversationIds, and re-focusing the pane`.
|
||||
*/
|
||||
onConversationsChanged: (conversationIds: DConversationId[]) =>
|
||||
_onConversationsChanged: (conversationIds: DConversationId[]) =>
|
||||
_set(state => {
|
||||
const { chatPanes, chatPaneFocusIndex } = state;
|
||||
|
||||
@@ -284,7 +318,8 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
}),
|
||||
|
||||
}), {
|
||||
name: 'app-app-chat-panes',
|
||||
// note: added the '-2' suffix on 20240308 to invalidate the persisted state, as we are adding a paneId
|
||||
name: 'app-app-chat-panes-2',
|
||||
},
|
||||
));
|
||||
|
||||
@@ -294,40 +329,29 @@ export function getInstantAppChatPanesCount() {
|
||||
|
||||
export function usePanesManager() {
|
||||
// use Panes
|
||||
const { onConversationsChanged, ...panesFunctions } = useAppChatPanesStore(state => {
|
||||
const {
|
||||
chatPaneFocusIndex,
|
||||
chatPanes,
|
||||
navigateHistoryInFocusedPane,
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
} = state;
|
||||
const focusedConversationId = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex]?.conversationId ?? null : null;
|
||||
return {
|
||||
chatPanes: chatPanes as Readonly<ChatPane[]>,
|
||||
focusedConversationId,
|
||||
navigateHistoryInFocusedPane,
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
focusedPaneIndex: chatPaneFocusIndex,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
};
|
||||
}, shallow);
|
||||
const { _onConversationsChanged, ...panesFunctions } = useAppChatPanesStore(useShallow(state => ({
|
||||
// state
|
||||
chatPanes: state.chatPanes as Readonly<ChatPane[]>,
|
||||
focusedPaneIndex: state.chatPaneFocusIndex,
|
||||
focusedPaneConversationId: state.chatPaneFocusIndex !== null ? state.chatPanes[state.chatPaneFocusIndex]?.conversationId ?? null : null,
|
||||
// methods
|
||||
openConversationInFocusedPane: state.openConversationInFocusedPane,
|
||||
openConversationInSplitPane: state.openConversationInSplitPane,
|
||||
navigateHistoryInFocusedPane: state.navigateHistoryInFocusedPane,
|
||||
removePane: state.removePane,
|
||||
setFocusedPaneIndex: state.setFocusedPaneIndex,
|
||||
_onConversationsChanged: state._onConversationsChanged,
|
||||
})));
|
||||
|
||||
// use Conversation IDs[]
|
||||
const conversationIDs: DConversationId[] = useChatStore(state => {
|
||||
return state.conversations.map(_c => _c.id);
|
||||
}, shallow);
|
||||
const conversationIDs: DConversationId[] = useChatStore(useShallow(state =>
|
||||
state.conversations.map(_c => _c.id),
|
||||
));
|
||||
|
||||
// [Effect] Ensure all Panes have a valid Conversation ID
|
||||
React.useEffect(() => {
|
||||
onConversationsChanged(conversationIDs);
|
||||
}, [conversationIDs, onConversationsChanged]);
|
||||
_onConversationsChanged(conversationIDs);
|
||||
}, [conversationIDs, _onConversationsChanged]);
|
||||
|
||||
return {
|
||||
...panesFunctions,
|
||||
@@ -335,10 +359,12 @@ export function usePanesManager() {
|
||||
}
|
||||
|
||||
export function usePaneDuplicateOrClose() {
|
||||
return useAppChatPanesStore(state => ({
|
||||
canAddPane: state.chatPanes.length < 4,
|
||||
return useAppChatPanesStore(useShallow(state => ({
|
||||
// state
|
||||
canAddPane: state.chatPanes.length < MAX_CONCURRENT_PANES,
|
||||
isMultiPane: state.chatPanes.length > 1,
|
||||
// actions
|
||||
duplicateFocusedPane: state.duplicateFocusedPane,
|
||||
removeOtherPanes: state.removeOtherPanes,
|
||||
}), shallow);
|
||||
})));
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Alert, Avatar, Box, Button, Card, CardContent, Checkbox, IconButton, Input, List, ListItem, ListItemButton, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
@@ -26,9 +26,10 @@ import { usePurposeStore } from './store-purposes';
|
||||
|
||||
// 'special' purpose IDs, for tile hiding purposes
|
||||
const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__';
|
||||
const TILE_ACTIVE_COLOR = 'primary' as const;
|
||||
|
||||
// defined looks
|
||||
const tileSize = 7.5; // rem
|
||||
const tileSize = 7; // rem
|
||||
const tileGap = 0.5; // rem
|
||||
|
||||
|
||||
@@ -46,32 +47,36 @@ function Tile(props: {
|
||||
return (
|
||||
<Button
|
||||
variant={(!props.isEditMode && props.isActive) ? 'solid' : props.isHighlighted ? 'soft' : 'soft'}
|
||||
color={(!props.isEditMode && props.isActive) ? 'primary' : props.isHighlighted ? 'primary' : 'neutral'}
|
||||
color={(!props.isEditMode && props.isActive) ? 'primary' : props.isHighlighted ? 'primary' : TILE_ACTIVE_COLOR}
|
||||
onClick={props.onClick}
|
||||
sx={{
|
||||
aspectRatio: 1,
|
||||
height: `${tileSize}rem`,
|
||||
fontWeight: 'md',
|
||||
lineHeight: 'xs',
|
||||
...((props.isEditMode || !props.isActive) ? {
|
||||
boxShadow: props.isHighlighted ? '0 2px 8px -2px rgb(var(--joy-palette-primary-mainChannel) / 50%)' : 'sm',
|
||||
backgroundColor: props.isHighlighted ? undefined : 'background.surface',
|
||||
...(props.imageUrl && {
|
||||
backgroundImage: `linear-gradient(rgba(255 255 255 /0.85), rgba(255 255 255 /1)), url(${props.imageUrl})`,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover',
|
||||
'&:hover': {
|
||||
backgroundImage: 'none',
|
||||
},
|
||||
}),
|
||||
boxShadow: `0 2px 8px -3px rgb(var(--joy-palette-${TILE_ACTIVE_COLOR}-darkChannel) / 30%)`,
|
||||
// boxShadow: props.isHighlighted
|
||||
// ? '0 2px 8px -2px rgb(var(--joy-palette-primary-darkChannel) / 30%)'
|
||||
// : 'sm',
|
||||
backgroundColor: props.isHighlighted ? undefined : 'background.popup',
|
||||
// ...(props.imageUrl && {
|
||||
// backgroundImage: `linear-gradient(rgba(255 255 255 /0.85), rgba(255 255 255 /1)), url(${props.imageUrl})`,
|
||||
// backgroundPosition: 'center',
|
||||
// backgroundSize: 'cover',
|
||||
// '&:hover': {
|
||||
// backgroundImage: 'none',
|
||||
// },
|
||||
// }),
|
||||
} : {}),
|
||||
flexDirection: 'column', gap: 1,
|
||||
flexDirection: 'column', gap: props.symbol === '🎭' ? 0.5 : 1.25, pt: 1.25,
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{/* [Edit mode checkbox] */}
|
||||
{props.isEditMode && (
|
||||
<Checkbox
|
||||
variant='soft' color='neutral'
|
||||
variant='soft' color={TILE_ACTIVE_COLOR}
|
||||
checked={!props.isHidden}
|
||||
// label={<Typography level='body-xs'>show</Typography>}
|
||||
sx={{ position: 'absolute', left: `${tileGap}rem`, top: `${tileGap}rem` }}
|
||||
@@ -259,7 +264,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
</Typography>
|
||||
<Tooltip disableInteractive title={editMode ? 'Done Editing' : 'Edit Tiles'}>
|
||||
<IconButton size='sm' onClick={toggleEditMode} sx={{ my: '-0.25rem' /* absorb the button padding */ }}>
|
||||
{editMode ? <DoneIcon /> : <EditIcon />}
|
||||
{editMode ? <DoneIcon /> : <EditRoundedIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
@@ -293,6 +298,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
isHidden={hidePersonaCreator}
|
||||
onClick={() => editMode ? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR) : void navigateToPersonas()}
|
||||
sx={{
|
||||
fontSize: 'xs',
|
||||
boxShadow: 'xs',
|
||||
backgroundColor: 'neutral.softDisabledBg',
|
||||
}}
|
||||
@@ -326,24 +332,24 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
sx={{
|
||||
// example items 2-col layout
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${tileSize * 2 + 1}rem, 1fr))`,
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${tileSize * 3 + 1}rem, 1fr))`,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{fourExamples?.map((example, idx) => (
|
||||
<ListItem
|
||||
key={idx}
|
||||
variant='soft'
|
||||
variant='outlined'
|
||||
sx={{
|
||||
// padding: '0.25rem 0.5rem',
|
||||
backgroundColor: 'background.popup',
|
||||
borderRadius: 'md',
|
||||
// boxShadow: 'xs',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'xs',
|
||||
'& svg': { opacity: 0.1, transition: 'opacity 0.2s' },
|
||||
'&:hover svg': { opacity: 1 },
|
||||
}}
|
||||
>
|
||||
<ListItemButton onClick={() => props.runExample(example)} sx={{ justifyContent: 'space-between' }}>
|
||||
<ListItemButton onClick={() => props.runExample(example)} sx={{ justifyContent: 'space-between', borderRadius: 'md' }}>
|
||||
<Typography level='body-sm'>
|
||||
{example}
|
||||
</Typography>
|
||||
|
||||
+67
-39
@@ -1,7 +1,7 @@
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import type { DFolder } from '~/common/state/store-folders';
|
||||
import { conversationTitle, DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { conversationTitle, DConversationId, DMessageUserFlag, messageHasUserFlag, messageUserFlagToEmoji, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
import type { ChatNavigationItemData } from './ChatDrawerItem';
|
||||
|
||||
@@ -12,6 +12,8 @@ const SEARCH_MIN_CHARS = 3;
|
||||
|
||||
export type ChatNavGrouping = false | 'date' | 'persona';
|
||||
|
||||
export type ChatSearchSorting = 'frequency' | 'date';
|
||||
|
||||
interface ChatNavigationGroupData {
|
||||
type: 'nav-item-group',
|
||||
title: string,
|
||||
@@ -66,18 +68,28 @@ function getTimeBucketEn(currentTime: number, midnightTime: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
export function isDrawerSearching(filterByQuery: string): { isSearching: boolean, lcTextQuery: string } {
|
||||
const lcTextQuery = filterByQuery.trim().toLowerCase();
|
||||
return {
|
||||
isSearching: lcTextQuery.length >= SEARCH_MIN_CHARS,
|
||||
lcTextQuery,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Optimization: return a reduced version of the DConversation object for 'Drawer Items' purposes,
|
||||
* to avoid unnecessary re-renders on each new character typed by the assistant
|
||||
*/
|
||||
export function useChatNavRenderItems(
|
||||
export function useChatDrawerRenderItems(
|
||||
activeConversationId: DConversationId | null,
|
||||
chatPanesConversationIds: DConversationId[],
|
||||
filterByQuery: string,
|
||||
activeFolder: DFolder | null,
|
||||
allFolders: DFolder[],
|
||||
filterHasStars: boolean,
|
||||
grouping: ChatNavGrouping,
|
||||
searchSorting: ChatSearchSorting,
|
||||
showRelativeSize: boolean,
|
||||
): {
|
||||
renderNavItems: ChatRenderItemData[],
|
||||
@@ -93,50 +105,59 @@ export function useChatNavRenderItems(
|
||||
const selectedConversations = !activeFolder ? conversations : conversations.filter(_c => activeFolder.conversationIds.includes(_c.id));
|
||||
|
||||
// filter 2: preparation: lowercase the query
|
||||
const lcTextQuery = filterByQuery.trim().toLowerCase();
|
||||
const isSearching = lcTextQuery.length >= SEARCH_MIN_CHARS;
|
||||
const { isSearching, lcTextQuery } = isDrawerSearching(filterByQuery);
|
||||
|
||||
// transform (the conversations into ChatNavigationItemData) + filter2 (if searching)
|
||||
const chatNavItems = selectedConversations.map((_c): ChatNavigationItemData => {
|
||||
// rich properties
|
||||
const title = conversationTitle(_c);
|
||||
const isAlsoOpen = findOpenInViewNumbers(chatPanesConversationIds, _c.id);
|
||||
const chatNavItems = selectedConversations
|
||||
.filter(_c => !filterHasStars || _c.messages.some(m => messageHasUserFlag(m, 'starred')))
|
||||
.map((_c): ChatNavigationItemData => {
|
||||
// rich properties
|
||||
const title = conversationTitle(_c);
|
||||
const isAlsoOpen = findOpenInViewNumbers(chatPanesConversationIds, _c.id);
|
||||
|
||||
// set the frequency counters if filtering is enabled
|
||||
let searchFrequency: number = 0;
|
||||
if (isSearching) {
|
||||
const titleFrequency = title.toLowerCase().split(lcTextQuery).length - 1;
|
||||
const messageFrequency = _c.messages.reduce((count, message) => count + (message.text.toLowerCase().split(lcTextQuery).length - 1), 0);
|
||||
searchFrequency = titleFrequency + messageFrequency;
|
||||
}
|
||||
// set the frequency counters if filtering is enabled
|
||||
let searchFrequency: number = 0;
|
||||
if (isSearching) {
|
||||
const titleFrequency = title.toLowerCase().split(lcTextQuery).length - 1;
|
||||
const messageFrequency = _c.messages.reduce((count, message) => count + (message.text.toLowerCase().split(lcTextQuery).length - 1), 0);
|
||||
searchFrequency = titleFrequency + messageFrequency;
|
||||
}
|
||||
|
||||
// create the ChatNavigationData
|
||||
return {
|
||||
type: 'nav-item-chat-data',
|
||||
conversationId: _c.id,
|
||||
isActive: _c.id === activeConversationId,
|
||||
isAlsoOpen,
|
||||
isEmpty: !_c.messages.length && !_c.userTitle,
|
||||
title,
|
||||
folder: !allFolders.length
|
||||
? undefined // don't show folder select if folders are disabled
|
||||
: _c.id === activeConversationId // only show the folder for active conversation(s)
|
||||
? allFolders.find(folder => folder.conversationIds.includes(_c.id)) ?? null
|
||||
: null,
|
||||
updatedAt: _c.updated || _c.created || 0,
|
||||
messageCount: _c.messages.length,
|
||||
assistantTyping: !!_c.abortController,
|
||||
systemPurposeId: _c.systemPurposeId,
|
||||
searchFrequency,
|
||||
};
|
||||
}).filter(item => !isSearching || item.searchFrequency > 0);
|
||||
// union of message flags -> emoji string
|
||||
const allFlags = new Set<DMessageUserFlag>();
|
||||
_c.messages.forEach(_m => _m.userFlags?.forEach(flag => allFlags.add(flag)));
|
||||
const userFlagsSummary = !allFlags.size ? undefined : Array.from(allFlags).map(messageUserFlagToEmoji).join('');
|
||||
|
||||
// create the ChatNavigationData
|
||||
return {
|
||||
type: 'nav-item-chat-data',
|
||||
conversationId: _c.id,
|
||||
isActive: _c.id === activeConversationId,
|
||||
isAlsoOpen,
|
||||
isEmpty: !_c.messages.length && !_c.userTitle,
|
||||
title,
|
||||
userFlagsSummary,
|
||||
folder: !allFolders.length
|
||||
? undefined // don't show folder select if folders are disabled
|
||||
: _c.id === activeConversationId // only show the folder for active conversation(s)
|
||||
? allFolders.find(folder => folder.conversationIds.includes(_c.id)) ?? null
|
||||
: null,
|
||||
updatedAt: _c.updated || _c.created || 0,
|
||||
messageCount: _c.messages.length,
|
||||
assistantTyping: !!_c.abortController,
|
||||
systemPurposeId: _c.systemPurposeId,
|
||||
searchFrequency,
|
||||
};
|
||||
})
|
||||
.filter(item => !isSearching || item.searchFrequency > 0);
|
||||
|
||||
// check if the active conversation has an item in the list
|
||||
const filteredChatsIncludeActive = chatNavItems.some(_c => _c.conversationId === activeConversationId);
|
||||
|
||||
|
||||
// [sort by frequency, don't group] if there's a search query
|
||||
chatNavItems.sort((a, b) => b.searchFrequency - a.searchFrequency);
|
||||
if (isSearching && searchSorting === 'frequency')
|
||||
chatNavItems.sort((a, b) => b.searchFrequency - a.searchFrequency);
|
||||
|
||||
// Render List
|
||||
let renderNavItems: ChatRenderItemData[] = chatNavItems;
|
||||
@@ -179,7 +200,12 @@ export function useChatNavRenderItems(
|
||||
|
||||
// [empty message] if there are no items
|
||||
if (!renderNavItems.length)
|
||||
renderNavItems.push({ type: 'nav-item-info-message', message: isSearching ? 'No results found' : 'No conversations in folder' });
|
||||
renderNavItems.push({
|
||||
type: 'nav-item-info-message',
|
||||
message: filterHasStars ? 'No starred results'
|
||||
: isSearching ? 'No results found'
|
||||
: 'No conversations in folder',
|
||||
});
|
||||
|
||||
// other derived state
|
||||
const filteredChatIDs = chatNavItems.map(_c => _c.conversationId);
|
||||
@@ -203,8 +229,10 @@ export function useChatNavRenderItems(
|
||||
return a.renderNavItems.length === b.renderNavItems.length
|
||||
&& a.renderNavItems.every((_a, i) => shallow(_a, b.renderNavItems[i]))
|
||||
&& shallow(a.filteredChatIDs, b.filteredChatIDs)
|
||||
// we also compare this, as it changes with a parameter
|
||||
&& a.filteredChatsBarBasis === b.filteredChatsBarBasis;
|
||||
&& a.filteredChatsCount === b.filteredChatsCount
|
||||
&& a.filteredChatsAreEmpty === b.filteredChatsAreEmpty
|
||||
&& a.filteredChatsBarBasis === b.filteredChatsBarBasis
|
||||
&& a.filteredChatsIncludeActive === b.filteredChatsIncludeActive;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { callBrowseFetchPage } from '~/modules/browse/browse.client';
|
||||
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
|
||||
|
||||
export const runBrowseGetPageUpdatingState = async (conversationId: string, url: string) => {
|
||||
const cHandler = ConversationManager.getHandler(conversationId);
|
||||
export const runBrowseGetPageUpdatingState = async (cHandler: ConversationHandler, url?: string) => {
|
||||
if (!url) {
|
||||
cHandler.messageAppendAssistant('Issue: no URL provided.', undefined, 'issue', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// noinspection HttpUrlsUsage
|
||||
const shortUrl = url.replace('https://www.', '').replace('https://', '').replace('http://', '').replace('www.', '');
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(`Loading page at ${shortUrl}...`, 'web', undefined);
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(`Loading page at ${shortUrl}...`, undefined, 'web', true);
|
||||
|
||||
try {
|
||||
const page = await callBrowseFetchPage(url);
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
import type { StreamingClientUpdate } from '~/modules/llms/vendors/unifiedStreamingClient';
|
||||
import { SystemPurposeId } from '../../../data';
|
||||
import { autoSuggestions } from '~/modules/aifn/autosuggestions/autoSuggestions';
|
||||
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
import { llmStreamingChatGenerate } from '~/modules/llms/llm.client';
|
||||
import { llmStreamingChatGenerate, VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
|
||||
import { ChatAutoSpeakType, getChatAutoAI } from '../store-app-chat';
|
||||
|
||||
|
||||
export const STREAM_TEXT_INDICATOR = '...';
|
||||
|
||||
|
||||
/**
|
||||
* The main "chat" function. TODO: this is here so we can soon move it to the data model.
|
||||
*/
|
||||
export async function runAssistantUpdatingState(conversationId: string, history: DMessage[], assistantLlmId: DLLMId, systemPurpose: SystemPurposeId, parallelViewCount: number) {
|
||||
const cHandler = ConversationManager.getHandler(conversationId);
|
||||
export async function runAssistantUpdatingState(conversationId: string, history: DMessage[], assistantLlmId: DLLMId, parallelViewCount: number) {
|
||||
const cHandler = ConversationsManager.getHandler(conversationId);
|
||||
|
||||
// ai follow-up operations (fire/forget)
|
||||
const { autoSpeak, autoSuggestDiagrams, autoSuggestQuestions, autoTitleChat } = getChatAutoAI();
|
||||
|
||||
// update the system message from the active Purpose, if not manually edited
|
||||
history = cHandler.resyncPurposeInHistory(history, assistantLlmId, systemPurpose);
|
||||
|
||||
// create a blank and 'typing' message for the assistant
|
||||
const assistantMessageId = cHandler.messageAppendAssistant('...', assistantLlmId, history[0].purposeId);
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(STREAM_TEXT_INDICATOR, history[0].purposeId, assistantLlmId, true);
|
||||
|
||||
// when an abort controller is set, the UI switches to the "stop" mode
|
||||
const abortController = new AbortController();
|
||||
@@ -34,7 +33,7 @@ export async function runAssistantUpdatingState(conversationId: string, history:
|
||||
// stream the assistant's messages
|
||||
await streamAssistantMessage(
|
||||
assistantLlmId,
|
||||
history,
|
||||
history.map((m): VChatMessageIn => ({ role: m.role, content: m.text })),
|
||||
parallelViewCount,
|
||||
autoSpeak,
|
||||
(update) => cHandler.messageEdit(assistantMessageId, update, false),
|
||||
@@ -53,22 +52,26 @@ export async function runAssistantUpdatingState(conversationId: string, history:
|
||||
autoSuggestions(conversationId, assistantMessageId, autoSuggestDiagrams, autoSuggestQuestions);
|
||||
}
|
||||
|
||||
type StreamMessageOutcome = 'success' | 'aborted' | 'errored';
|
||||
type StreamMessageStatus = { outcome: StreamMessageOutcome, errorMessage?: string };
|
||||
|
||||
async function streamAssistantMessage(
|
||||
export async function streamAssistantMessage(
|
||||
llmId: DLLMId,
|
||||
history: DMessage[],
|
||||
messagesHistory: VChatMessageIn[],
|
||||
throttleUnits: number, // 0: disable, 1: default throttle (12Hz), 2+ reduce the message frequency with the square root
|
||||
autoSpeak: ChatAutoSpeakType,
|
||||
editMessage: (update: Partial<DMessage>) => void,
|
||||
abortSignal: AbortSignal,
|
||||
) {
|
||||
): Promise<StreamMessageStatus> {
|
||||
|
||||
const returnStatus: StreamMessageStatus = {
|
||||
outcome: 'success',
|
||||
errorMessage: undefined,
|
||||
};
|
||||
|
||||
// speak once
|
||||
let spokenLine = false;
|
||||
|
||||
const messages = history.map(({ role, text }) => ({ role, content: text }));
|
||||
|
||||
|
||||
// Throttling setup
|
||||
let lastCallTime = 0;
|
||||
let throttleDelay = 1000 / 12; // 12 messages per second works well for 60Hz displays (single chat, and 24 in 4 chats, see the square root below)
|
||||
@@ -86,7 +89,7 @@ async function streamAssistantMessage(
|
||||
const incrementalAnswer: Partial<DMessage> = { text: '' };
|
||||
|
||||
try {
|
||||
await llmStreamingChatGenerate(llmId, messages, null, null, abortSignal, (update: StreamingClientUpdate) => {
|
||||
await llmStreamingChatGenerate(llmId, messagesHistory, null, null, abortSignal, (update: StreamingClientUpdate) => {
|
||||
const textSoFar = update.textSoFar;
|
||||
|
||||
// grow the incremental message
|
||||
@@ -116,7 +119,10 @@ async function streamAssistantMessage(
|
||||
console.error('Fetch request error:', error);
|
||||
const errorText = ` [Issue: ${error.message || (typeof error === 'string' ? error : 'Chat stopped.')}]`;
|
||||
incrementalAnswer.text = (incrementalAnswer.text || '') + errorText;
|
||||
}
|
||||
returnStatus.outcome = 'errored';
|
||||
returnStatus.errorMessage = error.message;
|
||||
} else
|
||||
returnStatus.outcome = 'aborted';
|
||||
}
|
||||
|
||||
// Optimized:
|
||||
@@ -127,4 +133,6 @@ async function streamAssistantMessage(
|
||||
// 📢 TTS: all
|
||||
if ((autoSpeak === 'all' || autoSpeak === 'firstLine') && incrementalAnswer.text && !spokenLine && !abortSignal.aborted)
|
||||
void speakText(incrementalAnswer.text);
|
||||
|
||||
return returnStatus;
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
import { getActiveTextToImageProviderOrThrow, t2iGenerateImageOrThrow } from '~/modules/t2i/t2i.client';
|
||||
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
import type { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
|
||||
|
||||
/**
|
||||
* Text to image, appended as an 'assistant' message
|
||||
*/
|
||||
export async function runImageGenerationUpdatingState(conversationId: string, imageText: string) {
|
||||
const handler = ConversationManager.getHandler(conversationId);
|
||||
export async function runImageGenerationUpdatingState(cHandler: ConversationHandler, imageText?: string) {
|
||||
if (!imageText) {
|
||||
cHandler.messageAppendAssistant('Issue: no image description provided.', undefined, 'issue', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Acquire the active TextToImageProvider
|
||||
let t2iProvider: TextToImageProvider | undefined = undefined;
|
||||
try {
|
||||
t2iProvider = getActiveTextToImageProviderOrThrow();
|
||||
} catch (error: any) {
|
||||
const assistantErrorMessageId = handler.messageAppendAssistant(`[Issue] Sorry, I can't generate images right now. ${error?.message || error?.toString() || 'Unknown error'}.`, 'issue', undefined);
|
||||
handler.messageEdit(assistantErrorMessageId, { typing: false }, true);
|
||||
cHandler.messageAppendAssistant(`[Issue] Sorry, I can't generate images right now. ${error?.message || error?.toString() || 'Unknown error'}.`, undefined, 'issue', false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -26,17 +28,16 @@ export async function runImageGenerationUpdatingState(conversationId: string, im
|
||||
if (repeat > 1)
|
||||
imageText = imageText.replace(/x(\d+)$|\[(\d+)]$/, '').trim(); // Remove the "xN" or "[N]" part from the imageText
|
||||
|
||||
const assistantMessageId = handler.messageAppendAssistant(
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(
|
||||
`Give me ${t2iProvider.vendor === 'openai' ? 'a dozen' : 'a few'} seconds while I draw ${imageText?.length > 20 ? 'that' : '"' + imageText + '"'}...`,
|
||||
'', undefined,
|
||||
undefined, t2iProvider.painter, true,
|
||||
);
|
||||
handler.messageEdit(assistantMessageId, { originLLM: t2iProvider.painter }, false);
|
||||
|
||||
try {
|
||||
const imageUrls = await t2iGenerateImageOrThrow(t2iProvider, imageText, repeat);
|
||||
handler.messageEdit(assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
|
||||
cHandler.messageEdit(assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || error?.toString() || 'Unknown error';
|
||||
handler.messageEdit(assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
cHandler.messageEdit(assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,9 @@ import { Agent } from '~/modules/aifn/react/react';
|
||||
import { DLLMId } from '~/modules/llms/store-llms';
|
||||
import { useBrowseStore } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { ConversationManager } from '~/common/chats/ConversationHandler';
|
||||
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
|
||||
import { STREAM_TEXT_INDICATOR } from './chat-stream';
|
||||
|
||||
const EPHEMERAL_DELETION_DELAY = 5 * 1000;
|
||||
|
||||
@@ -10,12 +12,15 @@ const EPHEMERAL_DELETION_DELAY = 5 * 1000;
|
||||
/**
|
||||
* Synchronous ReAct chat function - TODO: event loop, auto-ui, cleanups, etc.
|
||||
*/
|
||||
export async function runReActUpdatingState(conversationId: string, question: string, assistantLlmId: DLLMId) {
|
||||
const cHandler = ConversationManager.getHandler(conversationId);
|
||||
export async function runReActUpdatingState(cHandler: ConversationHandler, question: string | undefined, assistantLlmId: DLLMId) {
|
||||
if (!question) {
|
||||
cHandler.messageAppendAssistant('Issue: no question provided.', undefined, 'issue', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// create a blank and 'typing' message for the assistant - to be filled when we're done
|
||||
const assistantModelLabel = 'react-' + assistantLlmId.slice(4, 7); // HACK: this is used to change the Avatar animation
|
||||
const assistantMessageId = cHandler.messageAppendAssistant('...', assistantModelLabel, undefined);
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(STREAM_TEXT_INDICATOR, undefined, assistantModelLabel, true);
|
||||
const { enableReactTool: enableBrowse } = useBrowseStore.getState();
|
||||
|
||||
// create an ephemeral space
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { create } from 'zustand';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
|
||||
export type ChatAutoSpeakType = 'off' | 'firstLine' | 'all';
|
||||
@@ -26,9 +27,15 @@ interface AppChatStore {
|
||||
|
||||
// chat UI
|
||||
|
||||
filterHasStars: boolean;
|
||||
setFilterHasStars: (filterHasStars: boolean) => void;
|
||||
|
||||
micTimeoutMs: number;
|
||||
setMicTimeoutMs: (micTimeoutMs: number) => void;
|
||||
|
||||
showPersonaIcons: boolean;
|
||||
setShowPersonaIcons: (showPersonaIcons: boolean) => void;
|
||||
|
||||
showRelativeSize: boolean;
|
||||
setShowRelativeSize: (showRelativeSize: boolean) => void;
|
||||
|
||||
@@ -56,9 +63,15 @@ const useAppChatStore = create<AppChatStore>()(persist(
|
||||
autoTitleChat: true,
|
||||
setAutoTitleChat: (autoTitleChat: boolean) => _set({ autoTitleChat }),
|
||||
|
||||
filterHasStars: false,
|
||||
setFilterHasStars: (filterHasStars: boolean) => _set({ filterHasStars }),
|
||||
|
||||
micTimeoutMs: 2000,
|
||||
setMicTimeoutMs: (micTimeoutMs: number) => _set({ micTimeoutMs }),
|
||||
|
||||
showPersonaIcons: true,
|
||||
setShowPersonaIcons: (showPersonaIcons: boolean) => _set({ showPersonaIcons }),
|
||||
|
||||
showRelativeSize: false,
|
||||
setShowRelativeSize: (showRelativeSize: boolean) => _set({ showRelativeSize }),
|
||||
|
||||
@@ -113,10 +126,18 @@ export const useChatMicTimeoutMsValue = (): number =>
|
||||
export const useChatMicTimeoutMs = (): [number, (micTimeoutMs: number) => void] =>
|
||||
useAppChatStore(state => [state.micTimeoutMs, state.setMicTimeoutMs], shallow);
|
||||
|
||||
export const useChatShowRelativeSize = (): { showRelativeSize: boolean, toggleRelativeSize: () => void } => {
|
||||
const showRelativeSize = useAppChatStore(state => state.showRelativeSize);
|
||||
const toggleRelativeSize = () => useAppChatStore.getState().setShowRelativeSize(!showRelativeSize);
|
||||
return { showRelativeSize, toggleRelativeSize };
|
||||
export const useChatDrawerFilters = () => {
|
||||
const values = useAppChatStore(useShallow(state => ({
|
||||
filterHasStars: state.filterHasStars,
|
||||
showPersonaIcons: state.showPersonaIcons,
|
||||
showRelativeSize: state.showRelativeSize,
|
||||
})));
|
||||
return {
|
||||
...values,
|
||||
toggleFilterHasStars: () => useAppChatStore.getState().setFilterHasStars(!values.filterHasStars),
|
||||
toggleShowPersonaIcons: () => useAppChatStore.getState().setShowPersonaIcons(!values.showPersonaIcons),
|
||||
toggleShowRelativeSize: () => useAppChatStore.getState().setShowRelativeSize(!values.showRelativeSize),
|
||||
};
|
||||
};
|
||||
|
||||
export const useChatShowTextDiff = (): [boolean, (showDiff: boolean) => void] =>
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
export function Gallery() {
|
||||
return (
|
||||
|
||||
<AppPlaceholder text='Drawing App is under development. v1.13 or v1.14.' />
|
||||
<AppPlaceholder text='Drawing App is under development. v1.16.' />
|
||||
|
||||
);
|
||||
}
|
||||
@@ -137,7 +137,9 @@ export function TextToImage(props: {
|
||||
|
||||
// layout
|
||||
display: 'grid',
|
||||
gridTemplateColumns: props.isMobile ? 'repeat(auto-fit, minmax(320px, 1fr))' : 'repeat(auto-fit, minmax(400px, 1fr))',
|
||||
gridTemplateColumns: props.isMobile
|
||||
? 'repeat(auto-fit, minmax(320px, 1fr))'
|
||||
: 'repeat(auto-fit, minmax(max(min(100%, 400px), 100%/5), 1fr))',
|
||||
gap: { xs: 2, md: 2 },
|
||||
}}>
|
||||
{prompts.map((prompt, index) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, ButtonGroup, IconButton, Tooltip } from '@mui/joy';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import LightbulbOutlinedIcon from '@mui/icons-material/LightbulbOutlined';
|
||||
|
||||
// const desktopButtonLegend =
|
||||
@@ -48,7 +48,7 @@ export function ButtonPromptFromIdea(props: {
|
||||
</Button>
|
||||
<Tooltip disableInteractive title='Use Idea'>
|
||||
<IconButton size='sm' onClick={onIdeaUse}>
|
||||
<ArrowForwardIcon />
|
||||
<ArrowForwardRoundedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -2,9 +2,9 @@ import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, ButtonGroup, Chip, Divider, IconButton, Typography } from '@mui/joy';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
|
||||
import { niceShadowKeyframes } from '../../call/Contacts';
|
||||
import { animationShadowRingLimey } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
export function DrawHeading(props: {
|
||||
@@ -30,9 +30,9 @@ export function DrawHeading(props: {
|
||||
borderRadius: '50%',
|
||||
pointerEvents: 'none',
|
||||
backgroundColor: 'background.popup',
|
||||
animation: `${niceShadowKeyframes} 5s infinite`,
|
||||
animation: `${animationShadowRingLimey} 5s infinite`,
|
||||
}}>
|
||||
<FormatPaintIcon />
|
||||
<FormatPaintTwoToneIcon />
|
||||
</IconButton>
|
||||
|
||||
{/* Messaging */}
|
||||
|
||||
@@ -4,17 +4,16 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, ButtonGroup, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import MoreTimeIcon from '@mui/icons-material/MoreTime';
|
||||
import RemoveIcon from '@mui/icons-material/Remove';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
|
||||
import { animationStopEnter } from '../../chat/components/composer/Composer';
|
||||
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
import { lineHeightTextareaMd } from '~/common/app.theme';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
@@ -219,7 +218,7 @@ export function PromptDesigner(props: {
|
||||
|
||||
<Dropdown>
|
||||
<MenuButton slots={{ root: IconButton }}>
|
||||
<ArrowForwardIcon />
|
||||
<ArrowForwardRoundedIcon />
|
||||
</MenuButton>
|
||||
<Menu placement='top'>
|
||||
{/* Add From History? */}
|
||||
@@ -288,10 +287,10 @@ export function PromptDesigner(props: {
|
||||
<Button
|
||||
key='draw-queue'
|
||||
variant='solid' color='primary'
|
||||
endDecorator={<FormatPaintIcon />}
|
||||
endDecorator={<FormatPaintTwoToneIcon />}
|
||||
onClick={handlePromptEnqueue}
|
||||
sx={{
|
||||
animation: `${animationStopEnter} 0.1s ease-out`,
|
||||
animation: `${animationEnterBelow} 0.1s ease-out`,
|
||||
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)` : 'none',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
@@ -306,7 +305,7 @@ export function PromptDesigner(props: {
|
||||
endDecorator={<StopOutlinedIcon sx={{ fontSize: 18 }} />}
|
||||
onClick={handleDrawStop}
|
||||
sx={{
|
||||
// animation: `${animationStopEnter} 0.1s ease-out`,
|
||||
// animation: `${animationEnterBelow} 0.1s ease-out`,
|
||||
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-warning-mainChannel) / 20%)` : 'none',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
@@ -321,7 +320,7 @@ export function PromptDesigner(props: {
|
||||
endDecorator={<MoreTimeIcon sx={{ fontSize: 18 }} />}
|
||||
onClick={handlePromptEnqueue}
|
||||
sx={{
|
||||
animation: `${animationStopEnter} 0.1s ease-out`,
|
||||
animation: `${animationEnterBelow} 0.1s ease-out`,
|
||||
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)` : 'none',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FormControl, FormLabel, ListItemDecorator, Option, Select } from '@mui/joy';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
|
||||
import type { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
import { OpenAIIcon } from '~/common/components/icons/vendors/OpenAIIcon';
|
||||
@@ -22,7 +22,7 @@ export function ProviderSelect(props: {
|
||||
label: provider.label + (provider.painter !== provider.label ? ` ${provider.painter}` : ''),
|
||||
value: provider.id,
|
||||
configured: provider.configured,
|
||||
Icon: provider.vendor === 'openai' ? OpenAIIcon : FormatPaintIcon,
|
||||
Icon: provider.vendor === 'openai' ? OpenAIIcon : FormatPaintTwoToneIcon,
|
||||
});
|
||||
});
|
||||
return options;
|
||||
@@ -41,7 +41,7 @@ export function ProviderSelect(props: {
|
||||
value={props.activeProviderId}
|
||||
placeholder='Select a service'
|
||||
onChange={(_event, value) => value && props.setActiveProviderId(value)}
|
||||
// startDecorator={<FormatPaintIcon sx={{ display: { xs: 'none', sm: 'inherit' } }} />}
|
||||
// startDecorator={<FormatPaintTwoToneIcon sx={{ display: { xs: 'none', sm: 'inherit' } }} />}
|
||||
sx={{
|
||||
minWidth: '12rem',
|
||||
}}
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Box, Button, Card, CardContent, List, ListItem, Tooltip, Typography } f
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { ChatMessageMemo } from '../chat/components/message/ChatMessage';
|
||||
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
|
||||
import { useChatShowSystemMessages } from '../chat/store-app-chat';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { launchAppChat } from '~/common/app.routes';
|
||||
@@ -63,7 +63,7 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
|
||||
const handleClone = async (canOverwrite: boolean) => {
|
||||
setCloning(true);
|
||||
const importedId = useChatStore.getState().importConversation({ ...props.conversation }, !canOverwrite);
|
||||
await launchAppChat(importedId);
|
||||
void launchAppChat(importedId);
|
||||
setCloning(false);
|
||||
};
|
||||
|
||||
@@ -108,17 +108,10 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
|
||||
p: 0,
|
||||
}}>
|
||||
|
||||
<ScrollToBottom
|
||||
bootToBottom bootSmoothly
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<ScrollToBottom bootToBottom bootSmoothly>
|
||||
|
||||
<List sx={{
|
||||
minHeight: '100%',
|
||||
p: 0,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
@@ -142,7 +135,7 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
|
||||
key={'msg-' + message.id}
|
||||
message={message}
|
||||
fitScreen={isMobile}
|
||||
blocksShowDate={idx === 0 || idx === filteredMessages.length - 1 /* first and last message */}
|
||||
showBlocksDate={idx === 0 || idx === filteredMessages.length - 1 /* first and last message */}
|
||||
onMessageEdit={(_messageId, text: string) => message.text = text}
|
||||
/>,
|
||||
)}
|
||||
|
||||
+59
-28
@@ -1,35 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import NextImage from 'next/image';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, IconButton, Typography } from '@mui/joy';
|
||||
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, Grid, IconButton, Typography } from '@mui/joy';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { ROUTE_INDEX } from '~/common/app.routes';
|
||||
import { animationColorBlues, animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { cssRainbowColorKeyframes } from '~/common/app.theme';
|
||||
|
||||
import { NewsItems, newsRoadmapCallout } from './news.data';
|
||||
import { NewsItems } from './news.data';
|
||||
import { beamNewsCallout } from './beam.data';
|
||||
|
||||
|
||||
// number of news items to show by default, before the expander
|
||||
const DEFAULT_NEWS_COUNT = 3;
|
||||
const DEFAULT_NEWS_COUNT = 4;
|
||||
|
||||
export const cssColorKeyframes = keyframes`
|
||||
0%, 100% {
|
||||
color: #636B74; /* Neutral main color (500) */
|
||||
}
|
||||
25% {
|
||||
color: #12467B; /* Primary darker shade (700) */
|
||||
}
|
||||
50% {
|
||||
color: #0B6BCB; /* Primary main color (500) */
|
||||
}
|
||||
75% {
|
||||
color: #083e75; /* Primary lighter shade (300) */
|
||||
}`;
|
||||
|
||||
export const newsRoadmapCallout =
|
||||
<Card variant='solid' invertedColors>
|
||||
<CardContent sx={{ gap: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
Open Roadmap
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Take a peek at our roadmap to see what's in the pipeline.
|
||||
Discover upcoming features and let us know what excites you the most!
|
||||
</Typography>
|
||||
<Grid container spacing={1}>
|
||||
<Grid xs={12} sm={7}>
|
||||
<Button
|
||||
fullWidth variant='soft' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={Brand.URIs.OpenProject} noLinkStyle target='_blank'
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth variant='plain' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={Brand.URIs.OpenRepo + '/issues/new?template=roadmap-request.md&title=%5BSuggestion%5D'} noLinkStyle target='_blank'
|
||||
>
|
||||
Suggest a Feature
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>;
|
||||
|
||||
|
||||
export function AppNews() {
|
||||
@@ -55,7 +75,7 @@ export function AppNews() {
|
||||
}}>
|
||||
|
||||
<Typography level='h1' sx={{ fontSize: '2.9rem', mb: 4 }}>
|
||||
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${cssColorKeyframes} 10s infinite`, zIndex: 1 }}>{firstNews?.versionCode}</Box>!
|
||||
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${animationColorBlues} 10s infinite`, zIndex: 1 /* perf-opt */ }}>{firstNews?.versionCode}</Box>!
|
||||
</Typography>
|
||||
|
||||
<Typography sx={{ mb: 2 }} level='title-sm'>
|
||||
@@ -88,6 +108,13 @@ export function AppNews() {
|
||||
const addPadding = false; //!firstCard; // || showExpander;
|
||||
return <React.Fragment key={idx}>
|
||||
|
||||
{/* Inject the Beam item here*/}
|
||||
{idx === 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{beamNewsCallout}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* News Item */}
|
||||
<Card key={'news-' + idx} sx={{ mb: 3, minHeight: 32, gap: 1 }}>
|
||||
<CardContent sx={{ position: 'relative', pr: addPadding ? 4 : 0 }}>
|
||||
@@ -97,9 +124,9 @@ export function AppNews() {
|
||||
<Box
|
||||
component='span'
|
||||
sx={idx ? {} : {
|
||||
animation: `${cssRainbowColorKeyframes} 5s infinite`,
|
||||
animation: `${animationColorRainbow} 5s infinite`,
|
||||
fontWeight: 'lg',
|
||||
zIndex: 1,
|
||||
zIndex: 1, /* perf-opt */
|
||||
}}
|
||||
>
|
||||
{ni.versionName}
|
||||
@@ -111,12 +138,15 @@ export function AppNews() {
|
||||
</Box>
|
||||
|
||||
{!!ni.items && (ni.items.length > 0) && (
|
||||
<ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: '1.5rem' }}>
|
||||
{ni.items.filter(item => item.dev !== true).map((item, idx) => <li key={idx}>
|
||||
< Typography component='div' level='body-sm'>
|
||||
{item.text}
|
||||
</Typography>
|
||||
</li>)}
|
||||
<ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: '1.5rem', listStyleType: '"- "' }}>
|
||||
{ni.items.filter(item => item.dev !== true).map((item, idx) => (
|
||||
<li key={idx} style={{ listStyle: (item.icon || item.noBullet) ? '" "' : '"- "', marginLeft: item.icon ? '-1.125rem' : undefined }}>
|
||||
<Typography component='div' sx={{ fontSize: 'sm' }}>
|
||||
{item.icon && <item.icon sx={{ fontSize: 'xs', mr: 0.75 }} />}
|
||||
{item.text}
|
||||
</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
@@ -146,6 +176,7 @@ export function AppNews() {
|
||||
// commented: we scale the images to 600px wide (>300 px tall)
|
||||
// sizes='(max-width: 1200px) 100vw, 50vw'
|
||||
priority={idx === 0}
|
||||
quality={90}
|
||||
/>
|
||||
</AspectRatio>
|
||||
</CardOverflow>
|
||||
@@ -153,7 +184,7 @@ export function AppNews() {
|
||||
</Card>
|
||||
|
||||
{/* Inject the roadmap item here*/}
|
||||
{idx === 0 && (
|
||||
{idx === 3 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{newsRoadmapCallout}
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, Card, CardContent, Grid, Typography } from '@mui/joy';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
import ThumbUpRoundedIcon from '@mui/icons-material/ThumbUpRounded';
|
||||
|
||||
import { Link } from '~/common/components/Link';
|
||||
|
||||
|
||||
export const beamReleaseDate = '2024-04-01T22:00:00Z';
|
||||
export const beamBlogUrl = 'https://big-agi.com/blog/beam-multi-model-ai-reasoning/';
|
||||
|
||||
export const beamNewsCallout =
|
||||
<Card variant='solid' invertedColors>
|
||||
<CardContent sx={{ gap: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
Beam - just launched in 1.15
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Beam is a world-first, multi-model AI chat modality that accelerates the discovery of superior solutions by leveraging the collective strengths of diverse LLMs.
|
||||
{/*Beam is a world-first, multi-model AI chat modality. By combining the strenghts of diverse LLMs, Beam allows you to find better answers, faster.*/}
|
||||
</Typography>
|
||||
<Grid container spacing={1}>
|
||||
<Grid xs={12} sm={7}>
|
||||
<Button
|
||||
fullWidth variant='soft' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={beamBlogUrl} noLinkStyle target='_blank'
|
||||
>
|
||||
Blog
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
|
||||
{/*<Button*/}
|
||||
{/* fullWidth variant='outlined' color='primary' startDecorator={<ThumbUpRoundedIcon />}*/}
|
||||
{/* // endDecorator={<LaunchIcon />}*/}
|
||||
{/* component={Link} href={beamHNUrl} noLinkStyle target='_blank'*/}
|
||||
{/*>*/}
|
||||
{/* on Hackernews 🙏*/}
|
||||
{/*</Button>*/}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>;
|
||||
+102
-75
@@ -2,88 +2,34 @@ import * as React from 'react';
|
||||
import { StaticImageData } from 'next/image';
|
||||
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, Card, CardContent, Chip, Grid, Typography } from '@mui/joy';
|
||||
import { Box, Chip, SvgIconProps, Typography } from '@mui/joy';
|
||||
import AutoStoriesOutlinedIcon from '@mui/icons-material/AutoStoriesOutlined';
|
||||
import GoogleIcon from '@mui/icons-material/Google';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
|
||||
import { AnthropicIcon } from '~/common/components/icons/vendors/AnthropicIcon';
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { GroqIcon } from '~/common/components/icons/vendors/GroqIcon';
|
||||
import { LocalAIIcon } from '~/common/components/icons/vendors/LocalAIIcon';
|
||||
import { MistralIcon } from '~/common/components/icons/vendors/MistralIcon';
|
||||
import { PerplexityIcon } from '~/common/components/icons/vendors/PerplexityIcon';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { clientUtmSource } from '~/common/util/pwaUtils';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
|
||||
// Images
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is calling on a 3D origami old-school pink telephone and the camera is zooming on the telephone. Close up photography, bokeh, white background.
|
||||
|
||||
// Cover Images
|
||||
// (not exactly) Imagine a futuristic, holographically bounded space. Inside this space, four capybaras stand. Three of them are in various stages of materialization, their forms made up of thousands of tiny, vibrant particles of electric blues, purples, and greens. These particles represent the merging of different intelligent inputs, symbolizing the concept of 'Beaming'. Positioned slightly towards the center and ahead of the others, the fourth capybara is fully materialized and composed of shimmering golden cotton candy, representing the optimal solution the 'Beam' feature seeks to achieve. The golden capybara gazes forward confidently, embodying a target achieved. Illuminated grid lines softly glow on the floor and walls of the setting, amplifying the futuristic aspect. In front of the golden capybara, floating, holographic interfaces depict complex networks of points and lines symbolizing the solution space 'Beaming' explores. The capybara interacts with these interfaces, implying the user's ability to control and navigate towards the best outcomes.
|
||||
import coverV115 from '../../../public/images/covers/release-cover-v1.15.0.png';
|
||||
// An image of a capybara sculpted entirely from iridescent blue cotton candy, gazing into a holographic galaxy of floating AI model icons (representing various AI models like Perplexity, Groq, etc.). The capybara is wearing a lightweight, futuristic headset, and its paws are gesturing as if orchestrating the movement of the models in the galaxy. The backdrop is minimalist, with occasional bursts of neon light beams, creating a sense of depth and wonder. Close-up photography, bokeh effect, with a dark but vibrant background to make the colors pop.
|
||||
import coverV114 from '../../../public/images/covers/release-cover-v1.14.0.png';
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is using a computer with split screen made of origami, split keyboard and is wearing origami sunglasses with very different split reflections. Split halves are very contrasting. Close up photography, bokeh, white background.
|
||||
import coverV113 from '../../../public/images/covers/release-cover-v1.13.0.png';
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is calling on a 3D origami old-school pink telephone and the camera is zooming on the telephone. Close up photography, bokeh, white background.
|
||||
import coverV112 from '../../../public/images/covers/release-cover-v1.12.0.png';
|
||||
|
||||
|
||||
const wowStyle: SxProps = {
|
||||
textDecoration: 'underline',
|
||||
textDecorationThickness: '0.4em',
|
||||
textDecorationColor: 'rgba(var(--joy-palette-primary-lightChannel) / 1)',
|
||||
// textDecorationColor: 'rgba(0 255 0 / 0.5)',
|
||||
textDecorationSkipInk: 'none',
|
||||
// textUnderlineOffset: '-0.5em',
|
||||
};
|
||||
|
||||
function B(props: {
|
||||
// one-of
|
||||
href?: string,
|
||||
issue?: number,
|
||||
code?: string,
|
||||
|
||||
wow?: boolean,
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const href =
|
||||
props.issue ? `${Brand.URIs.OpenRepo}/issues/${props.issue}`
|
||||
: props.code ? `${Brand.URIs.OpenRepo}/blob/main/${props.code}`
|
||||
: props.href;
|
||||
const boldText = (
|
||||
<Typography component='span' color={!!href ? 'primary' : 'neutral'} sx={{ fontWeight: 'lg' }}>
|
||||
{props.children}
|
||||
</Typography>
|
||||
);
|
||||
if (!href)
|
||||
return boldText;
|
||||
return (
|
||||
<Link href={href + clientUtmSource()} target='_blank' sx={props.wow ? wowStyle : undefined}>
|
||||
{boldText} <LaunchIcon sx={{ mx: 0.5, fontSize: 16 }} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// callout, for special occasions
|
||||
export const newsRoadmapCallout =
|
||||
<Card variant='solid' invertedColors>
|
||||
<CardContent sx={{ gap: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
Open Roadmap
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Take a peek at our roadmap to see what's in the pipeline.
|
||||
Discover upcoming features and let us know what excites you the most!
|
||||
</Typography>
|
||||
<Grid container spacing={1}>
|
||||
<Grid xs={12} sm={7}>
|
||||
<Button
|
||||
fullWidth variant='soft' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={Brand.URIs.OpenProject} noLinkStyle target='_blank'
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth variant='plain' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={Brand.URIs.OpenRepo + '/issues/new?template=roadmap-request.md&title=%5BSuggestion%5D'} noLinkStyle target='_blank'
|
||||
>
|
||||
Suggest a Feature
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>;
|
||||
import { beamBlogUrl, beamReleaseDate } from './beam.data';
|
||||
|
||||
|
||||
interface NewsItem {
|
||||
@@ -94,15 +40,58 @@ interface NewsItem {
|
||||
versionCoverImage?: StaticImageData;
|
||||
text?: string | React.JSX.Element;
|
||||
items?: {
|
||||
text: string | React.JSX.Element;
|
||||
text: React.ReactNode;
|
||||
dev?: boolean;
|
||||
issue?: number;
|
||||
icon?: React.FC<SvgIconProps>;
|
||||
noBullet?: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
// news and feature surfaces
|
||||
export const NewsItems: NewsItem[] = [
|
||||
// still unannounced: screen capture (when removed from labs)
|
||||
/*{
|
||||
versionCode: '1.16.0',
|
||||
items: [
|
||||
Draw
|
||||
...
|
||||
Screen Capture (when removed from labs)
|
||||
]
|
||||
}*/
|
||||
{
|
||||
versionCode: '1.15.0',
|
||||
versionName: 'Beam',
|
||||
versionDate: new Date(beamReleaseDate),
|
||||
versionCoverImage: coverV115,
|
||||
items: [
|
||||
{ text: <><B href={beamBlogUrl} wow>Beam</B>: Find better answers with multi-model AI reasoning</>, issue: 443, icon: ChatBeamIcon },
|
||||
{ text: <><B>Explore diverse perspectives</B> and <B>synthesize optimal responses</B></>, noBullet: true },
|
||||
{ text: <><B issue={436}>Auto-configure</B> models for managed deployments</>, issue: 436 },
|
||||
{ text: <>Message <B issue={476}>starring ⭐</B>, filtering and attachment</>, issue: 476 },
|
||||
{ text: <>Default persona improvements</> },
|
||||
{ text: <>Fixes to Gemini models and SVGs, improvements to UI and icons, and more</> },
|
||||
{ text: <>Developers: imperative LLM models discovery</>, dev: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.14.1',
|
||||
versionName: 'Modelmorphic',
|
||||
versionCoverImage: coverV114,
|
||||
versionDate: new Date('2024-03-07T08:00:00Z'),
|
||||
items: [
|
||||
{ text: <>Anthropic <B href='https://www.anthropic.com/news/claude-3-family'>Claude-3</B> support for smarter chats</>, issue: 443, icon: AnthropicIcon },
|
||||
{ text: <><B issue={407}>Perplexity</B> support, including Online models</>, issue: 407, icon: PerplexityIcon },
|
||||
{ text: <><B issue={427}>Groq</B> support, with speeds up to 500 tok/s</>, issue: 427, icon: GroqIcon },
|
||||
{ text: <>Support for new Mistral-Large models</>, icon: MistralIcon },
|
||||
{ text: <>Support for Google Gemini 1.5 models and various improvements</>, icon: GoogleIcon as any },
|
||||
{ text: <>Deeper LocalAI integration including support for <B issue={411}>model galleries</B></>, icon: LocalAIIcon },
|
||||
{ text: <>Major <B href='https://twitter.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: <>Fixes: standalone LaTeX blocks, close views by dragging, knowledge cutoff dates, crashes on Google translate (thanks dad)</> },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.13.0',
|
||||
versionName: 'Multi + Mind',
|
||||
@@ -286,3 +275,41 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const wowStyle: SxProps = {
|
||||
textDecoration: 'underline',
|
||||
textDecorationThickness: '0.4em',
|
||||
textDecorationColor: 'rgba(var(--joy-palette-primary-lightChannel) / 1)',
|
||||
// textDecorationColor: 'rgba(0 255 0 / 0.5)',
|
||||
textDecorationSkipInk: 'none',
|
||||
// textUnderlineOffset: '-0.5em',
|
||||
};
|
||||
|
||||
function B(props: {
|
||||
// one-of
|
||||
href?: string,
|
||||
issue?: number,
|
||||
code?: string,
|
||||
|
||||
wow?: boolean,
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const href =
|
||||
props.issue ? `${Brand.URIs.OpenRepo}/issues/${props.issue}`
|
||||
: props.code ? `${Brand.URIs.OpenRepo}/blob/main/${props.code}`
|
||||
: props.href;
|
||||
const isExtIcon = !props.issue;
|
||||
const boldText = (
|
||||
<Typography component='span' color={!!href ? 'primary' : 'neutral'} sx={{ fontWeight: 'lg' }}>
|
||||
{props.children}
|
||||
</Typography>
|
||||
);
|
||||
if (!href)
|
||||
return boldText;
|
||||
return (
|
||||
<Link href={href + clientUtmSource()} target='_blank' sx={props.wow ? wowStyle : undefined}>
|
||||
{boldText} {isExtIcon ? <LaunchIcon sx={{ mx: 0.5, fontSize: 16 }} /> : <AutoStoriesOutlinedIcon sx={{ mx: 0.5, fontSize: 16 }} />}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
|
||||
|
||||
export const incrementalNewsVersion: number = 13;
|
||||
export const incrementalNewsVersion: number = 15;
|
||||
|
||||
|
||||
export function shallRedirectToNews() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Alert, Box, Button, Card, CardContent, CircularProgress, Divider, FormLabel, Grid, IconButton, LinearProgress, Tab, TabList, TabPanel, Tabs, Typography } from '@mui/joy';
|
||||
import { Alert, Box, Button, Card, CardContent, CircularProgress, Divider, FormLabel, Grid, IconButton, LinearProgress, Tab, tabClasses, TabList, TabPanel, Tabs, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
|
||||
@@ -11,7 +11,7 @@ import { RenderMarkdownMemo } from '~/modules/blocks/markdown/RenderMarkdown';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { useFormEditTextArray } from '~/common/components/forms/useFormEditTextArray';
|
||||
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
|
||||
import { useLLMSelect, useLLMSelectLocalState } from '~/common/components/forms/useLLMSelect';
|
||||
import { useToggleableBoolean } from '~/common/util/useToggleableBoolean';
|
||||
|
||||
import { FromText } from './FromText';
|
||||
@@ -93,7 +93,8 @@ export function Creator(props: { display: boolean }) {
|
||||
const [showIntermediates, setShowIntermediates] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const [personaLlm, llmComponent] = useLLMSelect(true, 'Persona Creation Model');
|
||||
const [personaLlmId, setPersonaLlmId] = useLLMSelectLocalState(true);
|
||||
const [personaLlm, llmComponent] = useLLMSelect(personaLlmId, setPersonaLlmId, 'Persona Creation Model');
|
||||
|
||||
|
||||
// editable prompts
|
||||
@@ -176,8 +177,20 @@ export function Creator(props: { display: boolean }) {
|
||||
display: isTransforming ? 'none' : undefined,
|
||||
}}
|
||||
>
|
||||
<TabList sx={{ minHeight: '3rem' }}>
|
||||
<Tab>From YouTube Video</Tab>
|
||||
<TabList
|
||||
sx={{
|
||||
minHeight: '3rem',
|
||||
[`& .${tabClasses.root}[aria-selected="true"]`]: {
|
||||
// color: 'primary.softColor',
|
||||
bgcolor: 'background.popup',
|
||||
boxShadow: 'sm',
|
||||
fontWeight: 'lg',
|
||||
},
|
||||
// first element
|
||||
'& > *:first-child': { borderTopLeftRadius: '0.5rem' },
|
||||
}}
|
||||
>
|
||||
<Tab>From YouTube</Tab>
|
||||
<Tab>From Text</Tab>
|
||||
</TabList>
|
||||
<TabPanel keepMounted value={0} sx={{ p: 3 }}>
|
||||
|
||||
@@ -132,7 +132,7 @@ export function FromYouTube(props: {
|
||||
placeholder='YouTube Video URL'
|
||||
value={videoURL}
|
||||
onChange={handleVideoURLChange}
|
||||
sx={{ mb: 1.5 }}
|
||||
sx={{ mb: 1.5, backgroundColor: 'background.popup' }}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Accordion, AccordionDetails, accordionDetailsClasses, AccordionGroup, AccordionSummary, accordionSummaryClasses, Avatar, Button, Divider, ListItemContent, Stack, styled, Tab, tabClasses, TabList, TabPanel, Tabs } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ScienceIcon from '@mui/icons-material/Science';
|
||||
@@ -97,6 +98,13 @@ function Topic(props: { title?: string, icon?: string | React.ReactNode, startCo
|
||||
}
|
||||
|
||||
|
||||
const settingTaxSx: SxProps = {
|
||||
fontFamily: 'body',
|
||||
flex: 1,
|
||||
p: 0,
|
||||
m: 0,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component that allows the User to modify the application settings,
|
||||
* persisted on the client via localStorage.
|
||||
@@ -111,8 +119,6 @@ export function SettingsModal(props: {
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const tabFixSx = { fontFamily: 'body', flex: 1, p: 0, m: 0 };
|
||||
|
||||
return (
|
||||
<GoodModal
|
||||
title='Preferences' strongerTitle
|
||||
@@ -147,10 +153,10 @@ export function SettingsModal(props: {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab disableIndicator value={PreferencesTab.Chat} sx={tabFixSx}>Chat</Tab>
|
||||
<Tab disableIndicator value={PreferencesTab.Voice} sx={tabFixSx}>Voice</Tab>
|
||||
<Tab disableIndicator value={PreferencesTab.Draw} sx={tabFixSx}>Draw</Tab>
|
||||
<Tab disableIndicator value={PreferencesTab.Tools} sx={tabFixSx}>Tools</Tab>
|
||||
<Tab disableIndicator value={PreferencesTab.Chat} sx={settingTaxSx}>Chat</Tab>
|
||||
<Tab disableIndicator value={PreferencesTab.Voice} sx={settingTaxSx}>Voice</Tab>
|
||||
<Tab disableIndicator value={PreferencesTab.Draw} sx={settingTaxSx}>Draw</Tab>
|
||||
<Tab disableIndicator value={PreferencesTab.Tools} sx={settingTaxSx}>Tools</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value={PreferencesTab.Chat} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
|
||||
@@ -14,7 +14,8 @@ const shortcutsMd = platformAwareKeystrokes(`
|
||||
| **Edit** | |
|
||||
| Shift + Enter | Newline |
|
||||
| Alt + Enter | Append (no response) |
|
||||
| Ctrl + Shift + R | Regenerate answer |
|
||||
| Ctrl + Shift + B | **Beam** last message |
|
||||
| Ctrl + Shift + R | **Regenerate** last message |
|
||||
| Ctrl + Shift + V | Attach clipboard (better than Ctrl + V) |
|
||||
| Ctrl + M | Microphone (voice typing) |
|
||||
| **Chats** | |
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
|
||||
// uncomment for more settings
|
||||
const DEV_MODE_SETTINGS = false;
|
||||
export const DEV_MODE_SETTINGS = false;
|
||||
|
||||
|
||||
export function UxLabsSettings() {
|
||||
@@ -24,22 +24,17 @@ export function UxLabsSettings() {
|
||||
const isMobile = useIsMobile();
|
||||
const {
|
||||
labsAttachScreenCapture, setLabsAttachScreenCapture,
|
||||
labsBeam, setLabsBeam,
|
||||
labsCameraDesktop, setLabsCameraDesktop,
|
||||
labsChatBarAlt, setLabsChatBarAlt,
|
||||
labsChatBeam, setLabsChatBeam,
|
||||
labsHighPerformance, setLabsHighPerformance,
|
||||
} = useUXLabsStore();
|
||||
|
||||
return <>
|
||||
|
||||
{DEV_MODE_SETTINGS && <FormSwitchControl
|
||||
title={<><ChatBeamIcon color={labsChatBeam ? 'primary' : undefined} sx={{ mr: 0.25 }} />Chat Beam</>} description={'v1.14 · ' + (labsChatBeam ? 'Active' : 'Off')}
|
||||
checked={labsChatBeam} onChange={setLabsChatBeam}
|
||||
/>}
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><TitleIcon color={labsChatBarAlt ? 'primary' : undefined} sx={{ mr: 0.25 }} />Chat Title</>} description={'v1.14 · ' + (labsChatBarAlt === 'title' ? 'Show Title' : 'Show Options')}
|
||||
checked={labsChatBarAlt === 'title'} onChange={(on) => setLabsChatBarAlt(on ? 'title' : false)}
|
||||
title={<><ChatBeamIcon color={labsBeam ? 'primary' : undefined} sx={{ mr: 0.25 }} />Chat Beam</>} description={'v1.15 · ' + (labsBeam ? 'Active' : 'Off')}
|
||||
checked={labsBeam} onChange={setLabsBeam}
|
||||
/>
|
||||
|
||||
<FormSwitchControl
|
||||
@@ -47,6 +42,11 @@ export function UxLabsSettings() {
|
||||
checked={labsHighPerformance} onChange={setLabsHighPerformance}
|
||||
/>
|
||||
|
||||
{DEV_MODE_SETTINGS && <FormSwitchControl
|
||||
title={<><TitleIcon color={labsChatBarAlt ? 'primary' : undefined} sx={{ mr: 0.25 }} />Chat Title</>} description={'v1.14 · ' + (labsChatBarAlt === 'title' ? 'Show Title' : 'Show Models')}
|
||||
checked={labsChatBarAlt === 'title'} onChange={(on) => setLabsChatBarAlt(on ? 'title' : false)}
|
||||
/>}
|
||||
|
||||
{!isMobile && <FormSwitchControl
|
||||
title={<><ScreenshotMonitorIcon color={labsAttachScreenCapture ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Screen Capture</>} description={'v1.13 · ' + (labsAttachScreenCapture ? 'Enabled' : 'Disabled')}
|
||||
checked={labsAttachScreenCapture} onChange={setLabsAttachScreenCapture}
|
||||
|
||||
+20
-4
@@ -1,8 +1,8 @@
|
||||
import type { FunctionComponent } from 'react';
|
||||
|
||||
// App icons
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
|
||||
import AccountTreeTwoToneIcon from '@mui/icons-material/AccountTreeTwoTone';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
@@ -10,8 +10,8 @@ import CallOutlinedIcon from '@mui/icons-material/CallOutlined';
|
||||
import Diversity2Icon from '@mui/icons-material/Diversity2';
|
||||
import EventNoteIcon from '@mui/icons-material/EventNote';
|
||||
import EventNoteOutlinedIcon from '@mui/icons-material/EventNoteOutlined';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import FormatPaintOutlinedIcon from '@mui/icons-material/FormatPaintOutlined';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
import ImageIcon from '@mui/icons-material/Image';
|
||||
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
@@ -29,6 +29,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { hasNoChatLinkItems } from '~/modules/trade/link/store-link';
|
||||
|
||||
|
||||
@@ -60,6 +61,7 @@ export interface NavItemApp extends ItemBase {
|
||||
hideNav?: boolean
|
||||
| (() => boolean), // set to hide the Nav bar (note: must have a way to navigate back)
|
||||
fullWidth?: boolean, // set to true to override the user preference
|
||||
isDev?: boolean, // show a 'dev mode' badge
|
||||
_delete?: boolean, // delete from the UI
|
||||
}
|
||||
|
||||
@@ -108,11 +110,12 @@ export const navItems: {
|
||||
name: 'Draw',
|
||||
barTitle: 'Generate Images',
|
||||
icon: FormatPaintOutlinedIcon,
|
||||
iconActive: FormatPaintIcon,
|
||||
iconActive: FormatPaintTwoToneIcon,
|
||||
type: 'app',
|
||||
route: '/draw',
|
||||
// hideOnMobile: true,
|
||||
hideDrawer: true,
|
||||
isDev: true,
|
||||
_delete: true,
|
||||
},
|
||||
{
|
||||
@@ -121,14 +124,16 @@ export const navItems: {
|
||||
iconActive: AutoAwesomeIcon,
|
||||
type: 'app',
|
||||
route: '/cortex',
|
||||
isDev: true,
|
||||
_delete: true,
|
||||
},
|
||||
{
|
||||
name: 'Patterns',
|
||||
icon: AccountTreeOutlinedIcon,
|
||||
iconActive: AccountTreeIcon,
|
||||
iconActive: AccountTreeTwoToneIcon,
|
||||
type: 'app',
|
||||
route: '/patterns',
|
||||
isDev: true,
|
||||
_delete: true,
|
||||
},
|
||||
{
|
||||
@@ -139,6 +144,7 @@ export const navItems: {
|
||||
route: '/workspace',
|
||||
hideDrawer: true,
|
||||
hideOnMobile: true,
|
||||
isDev: true,
|
||||
_delete: true,
|
||||
},
|
||||
// <-- divider here -->
|
||||
@@ -156,12 +162,22 @@ export const navItems: {
|
||||
route: '/personas',
|
||||
hideBar: true,
|
||||
},
|
||||
{
|
||||
name: 'Beam',
|
||||
icon: ChatBeamIcon,
|
||||
type: 'app',
|
||||
route: '/dev/beam',
|
||||
hideDrawer: true,
|
||||
hideIcon: true,
|
||||
isDev: true,
|
||||
},
|
||||
{
|
||||
name: 'Media Library',
|
||||
icon: ImageOutlinedIcon,
|
||||
iconActive: ImageIcon,
|
||||
type: 'app',
|
||||
route: '/media',
|
||||
isDev: true,
|
||||
_delete: true,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
import Router, { useRouter } from 'next/router';
|
||||
|
||||
import type { AppCallIntent } from '../apps/call/AppCall';
|
||||
import type { AppChatIntent } from '../apps/chat/AppChat';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { isBrowser } from './util/pwaUtils';
|
||||
|
||||
@@ -39,7 +41,7 @@ export const getChatLinkRelativePath = (chatLinkId: string) => ROUTE_APP_LINK_CH
|
||||
|
||||
export function useRouterQuery<TQuery>(): TQuery {
|
||||
const { query } = useRouter();
|
||||
return query as TQuery;
|
||||
return (query || {}) as TQuery;
|
||||
}
|
||||
|
||||
export function useRouterRoute(): string {
|
||||
@@ -69,24 +71,17 @@ function navigateFn(path: string) {
|
||||
|
||||
/// Launch Apps
|
||||
|
||||
/* Note: not used yet
|
||||
export interface AppChatQueryParams {
|
||||
conversationId?: string;
|
||||
}*/
|
||||
|
||||
export const launchAppChat = async (conversationId?: DConversationId) => {
|
||||
await Router.push(
|
||||
export async function launchAppChat(conversationId?: DConversationId) {
|
||||
return Router.push(
|
||||
{
|
||||
pathname: ROUTE_APP_CHAT,
|
||||
query: conversationId ? {
|
||||
conversationId,
|
||||
} /*satisfies AppChatQueryParams*/
|
||||
: undefined,
|
||||
query: !conversationId ? undefined : {
|
||||
initialConversationId: conversationId,
|
||||
} satisfies AppChatIntent,
|
||||
},
|
||||
ROUTE_APP_CHAT,
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export function launchAppCall(conversationId: string, personaId: string) {
|
||||
void Router.push(
|
||||
|
||||
+20
-42
@@ -2,7 +2,6 @@ import createCache from '@emotion/cache';
|
||||
|
||||
import { Inter, JetBrains_Mono } from 'next/font/google';
|
||||
import { extendTheme } from '@mui/joy';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
|
||||
// CSS utils
|
||||
@@ -15,7 +14,7 @@ export const formLabelStartWidth = 140;
|
||||
|
||||
// Theme & Fonts
|
||||
|
||||
const inter = Inter({
|
||||
const font = Inter({
|
||||
weight: [ /* '300', sm */ '400' /* (undefined, default) */, '500' /* md */, '600' /* lg */, '700' /* xl */],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
@@ -31,7 +30,8 @@ const jetBrainsMono = JetBrains_Mono({
|
||||
|
||||
export const appTheme = extendTheme({
|
||||
fontFamily: {
|
||||
body: inter.style.fontFamily,
|
||||
body: font.style.fontFamily,
|
||||
display: font.style.fontFamily,
|
||||
code: jetBrainsMono.style.fontFamily,
|
||||
},
|
||||
colorSchemes: {
|
||||
@@ -104,6 +104,14 @@ export const appTheme = extendTheme({
|
||||
},
|
||||
},
|
||||
|
||||
// JoyMenuItem: {
|
||||
// styleOverrides: {
|
||||
// root: {
|
||||
// '--Icon-fontSize': '1rem', // smaller menu(s) icon - default is 1.25rem ('xl', 20px)
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
|
||||
// JoyModal: {
|
||||
// styleOverrides: {
|
||||
// backdrop: {
|
||||
@@ -138,6 +146,7 @@ export const themeBgAppChatComposer = 'background.surface';
|
||||
export const lineHeightChatTextMd = 1.75;
|
||||
export const lineHeightTextareaMd = 1.75;
|
||||
|
||||
export const themeZIndexBeamView = 10;
|
||||
export const themeZIndexPageBar = 25;
|
||||
export const themeZIndexDesktopDrawer = 26;
|
||||
export const themeZIndexDesktopNav = 27;
|
||||
@@ -149,6 +158,14 @@ export const themeBreakpoints = appTheme.breakpoints.values;
|
||||
// Dyanmic UI Sizing
|
||||
export type ContentScaling = 'xs' | 'sm' | 'md';
|
||||
|
||||
export function adjustContentScaling(scaling: ContentScaling, offset?: number) {
|
||||
if (!offset) return scaling;
|
||||
const scalingArray = ['xs', 'sm', 'md'];
|
||||
const scalingIndex = scalingArray.indexOf(scaling);
|
||||
const newScalingIndex = Math.max(0, Math.min(scalingArray.length - 1, scalingIndex + offset));
|
||||
return scalingArray[newScalingIndex] as ContentScaling;
|
||||
}
|
||||
|
||||
interface ContentScalingOptions {
|
||||
// BlocksRenderer
|
||||
blockCodeFontSize: string;
|
||||
@@ -196,45 +213,6 @@ export const themeScalingMap: Record<ContentScaling, ContentScalingOptions> = {
|
||||
};
|
||||
|
||||
|
||||
export const cssRainbowColorKeyframes = keyframes`
|
||||
100%, 0% {
|
||||
color: rgb(255, 0, 0);
|
||||
}
|
||||
8% {
|
||||
color: rgb(204, 102, 0);
|
||||
}
|
||||
16% {
|
||||
color: rgb(128, 128, 0);
|
||||
}
|
||||
25% {
|
||||
color: rgb(77, 153, 0);
|
||||
}
|
||||
33% {
|
||||
color: rgb(0, 179, 0);
|
||||
}
|
||||
41% {
|
||||
color: rgb(0, 153, 82);
|
||||
}
|
||||
50% {
|
||||
color: rgb(0, 128, 128);
|
||||
}
|
||||
58% {
|
||||
color: rgb(0, 102, 204);
|
||||
}
|
||||
66% {
|
||||
color: rgb(0, 0, 255);
|
||||
}
|
||||
75% {
|
||||
color: rgb(127, 0, 255);
|
||||
}
|
||||
83% {
|
||||
color: rgb(153, 0, 153);
|
||||
}
|
||||
91% {
|
||||
color: rgb(204, 0, 102);
|
||||
}`;
|
||||
|
||||
|
||||
// Emotion Cache (with insertion point on the SSR pass)
|
||||
|
||||
const isBrowser = typeof document !== 'undefined';
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DMessage } from '~/common/state/store-chats';
|
||||
|
||||
import type { ConversationHandler } from './ConversationHandler';
|
||||
|
||||
|
||||
export type BeamConfig = {
|
||||
history: DMessage[];
|
||||
lastMessage: string;
|
||||
configError?: string;
|
||||
};
|
||||
|
||||
function createConfig(history: DMessage[]): BeamConfig {
|
||||
return { history, lastMessage: history.slice(-1)[0]?.text || '' };
|
||||
}
|
||||
|
||||
export interface BeamCandidate {
|
||||
id: string;
|
||||
text: string;
|
||||
placeholder: string;
|
||||
}
|
||||
|
||||
function createCandidate(): BeamCandidate {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
text: '',
|
||||
placeholder: '...',
|
||||
};
|
||||
}
|
||||
|
||||
export class BeamStore extends EventTarget {
|
||||
private config: BeamConfig | null = null;
|
||||
private readonly candidates: BeamCandidate[] = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
get(): { config: BeamConfig | null, candidates: BeamCandidate[] } {
|
||||
return { config: this.config, candidates: this.candidates };
|
||||
}
|
||||
|
||||
create(history: DMessage[]) {
|
||||
if (this.config) {
|
||||
this.config.configError = 'Warning: config already exists. Skipping...';
|
||||
} else {
|
||||
this.config = createConfig([...history]);
|
||||
}
|
||||
if (history.length < 1)
|
||||
this.config.configError = 'Warning: empty history. Skipping...';
|
||||
this.dispatchEvent(new CustomEvent('stateChanged', { detail: { config: this.config } }));
|
||||
}
|
||||
|
||||
appendCandidate(candidate: BeamCandidate): void {
|
||||
this.candidates.push(candidate);
|
||||
this.dispatchEvent(new CustomEvent('stateChanged', { detail: { candidates: this.candidates } }));
|
||||
}
|
||||
|
||||
deleteCandidate(candidateId: BeamCandidate['id']): void {
|
||||
const index = this.candidates.findIndex(e => e.id === candidateId);
|
||||
if (index >= 0) {
|
||||
this.candidates.splice(index, 1);
|
||||
this.dispatchEvent(new CustomEvent('stateChanged', { detail: { candidates: this.candidates } }));
|
||||
}
|
||||
}
|
||||
|
||||
updateCandidate(candidateId: BeamCandidate['id'], update: Partial<BeamCandidate>): void {
|
||||
const candidate = this.candidates.find(c => c.id === candidateId);
|
||||
if (candidate) {
|
||||
Object.assign(candidate, update);
|
||||
this.dispatchEvent(new CustomEvent('stateChanged', { detail: { candidates: this.candidates } }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function useBeam(conversationHandler: ConversationHandler | null): { config: BeamConfig | null, candidates: BeamCandidate[] } {
|
||||
|
||||
// state
|
||||
const [beamState, setBeamState] = React.useState<{ config: BeamConfig | null, candidates: BeamCandidate[] }>(() => {
|
||||
return conversationHandler ? conversationHandler.beamStore.get() : { config: null, candidates: [] };
|
||||
});
|
||||
|
||||
// [effect] subscribe to events
|
||||
React.useEffect(() => {
|
||||
if (!conversationHandler) return;
|
||||
const handleStateChanged = (event: Event) => {
|
||||
setBeamState(state => ({ ...state, ...(event as CustomEvent<{ config?: BeamConfig, candidates?: BeamCandidate[] }>).detail }));
|
||||
};
|
||||
conversationHandler.beamStore.addEventListener('stateChanged', handleStateChanged);
|
||||
return () => {
|
||||
conversationHandler.beamStore.removeEventListener('stateChanged', handleStateChanged);
|
||||
};
|
||||
}, [conversationHandler]);
|
||||
|
||||
return beamState;
|
||||
}
|
||||
@@ -1,19 +1,26 @@
|
||||
import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
import { DLLMId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../data';
|
||||
|
||||
import { ChatActions, createDMessage, DConversationId, DMessage, useChatStore } from '../state/store-chats';
|
||||
import { ChatActions, createDMessage, DConversationId, DMessage, getConversationSystemPurposeId, useChatStore } from '../state/store-chats';
|
||||
|
||||
import { createBeamVanillaStore } from '~/modules/beam/store-beam-vanilla';
|
||||
|
||||
import { BeamStore } from './BeamStore';
|
||||
import { EphemeralHandler, EphemeralsStore } from './EphemeralsStore';
|
||||
|
||||
|
||||
/**
|
||||
* ConversationHandler is a class to overlay state onto a conversation.
|
||||
* It is a singleton per conversationId.
|
||||
* - View classes will react to this class (or its members) to update the UI.
|
||||
* - Controller classes will call directly methods in this class.
|
||||
*/
|
||||
export class ConversationHandler {
|
||||
private readonly chatActions: ChatActions;
|
||||
private readonly conversationId: DConversationId;
|
||||
|
||||
readonly beamStore: BeamStore = new BeamStore();
|
||||
private readonly beamStore = createBeamVanillaStore();
|
||||
readonly ephemeralsStore: EphemeralsStore = new EphemeralsStore();
|
||||
|
||||
|
||||
@@ -25,9 +32,10 @@ export class ConversationHandler {
|
||||
|
||||
// Conversation Management
|
||||
|
||||
resyncPurposeInHistory(history: DMessage[], assistantLlmId: DLLMId, purposeId: SystemPurposeId): DMessage[] {
|
||||
inlineUpdatePurposeInHistory(history: DMessage[], assistantLlmId: DLLMId | undefined): DMessage[] {
|
||||
const purposeId = getConversationSystemPurposeId(this.conversationId);
|
||||
const systemMessageIndex = history.findIndex(m => m.role === 'system');
|
||||
const systemMessage: DMessage = systemMessageIndex >= 0 ? history.splice(systemMessageIndex, 1)[0] : createDMessage('system', '');
|
||||
let systemMessage: DMessage = systemMessageIndex >= 0 ? history.splice(systemMessageIndex, 1)[0] : createDMessage('system', '');
|
||||
if (!systemMessage.updated && purposeId && SystemPurposes[purposeId]?.systemMessage) {
|
||||
systemMessage.purposeId = purposeId;
|
||||
systemMessage.text = bareBonesPromptMixer(SystemPurposes[purposeId].systemMessage, assistantLlmId);
|
||||
@@ -35,9 +43,13 @@ export class ConversationHandler {
|
||||
// HACK: this is a special case for the 'Custom' persona, to set the message in stone (so it doesn't get updated when switching to another persona)
|
||||
if (purposeId === 'Custom')
|
||||
systemMessage.updated = Date.now();
|
||||
|
||||
// HACK: refresh the object to trigger a re-render of this message
|
||||
systemMessage = { ...systemMessage };
|
||||
}
|
||||
history.unshift(systemMessage);
|
||||
this.chatActions.setMessages(this.conversationId, history);
|
||||
// NOTE: disabled on 2024-03-13; we are only manipulating the history in-place, an we'll set it later in every code branch
|
||||
// this.chatActions.setMessages(this.conversationId, history);
|
||||
return history;
|
||||
}
|
||||
|
||||
@@ -48,10 +60,16 @@ export class ConversationHandler {
|
||||
|
||||
// Message Management
|
||||
|
||||
messageAppendAssistant(text: string, llmLabel: DLLMId | string /* 'DALL·E' | 'Prodia' | 'react-...' | 'web'*/, purposeId?: SystemPurposeId): string {
|
||||
/**
|
||||
* @param text assistant text
|
||||
* @param llmLabel LlmId or string, such as 'DALL·E' | 'Prodia' | 'react-...' | 'web'
|
||||
* @param purposeId purpose that supposedly triggered this message
|
||||
* @param typing whether the assistant is typing at the onset
|
||||
*/
|
||||
messageAppendAssistant(text: string, purposeId: SystemPurposeId | undefined, llmLabel: DLLMId | string, typing: boolean): string {
|
||||
const assistantMessage: DMessage = createDMessage('assistant', text);
|
||||
assistantMessage.typing = true;
|
||||
assistantMessage.purposeId = purposeId;
|
||||
assistantMessage.typing = typing;
|
||||
assistantMessage.purposeId = purposeId ?? undefined;
|
||||
assistantMessage.originLLM = llmLabel;
|
||||
this.chatActions.appendMessage(this.conversationId, assistantMessage);
|
||||
return assistantMessage.id;
|
||||
@@ -61,6 +79,50 @@ export class ConversationHandler {
|
||||
this.chatActions.editMessage(this.conversationId, messageId, update, touch);
|
||||
}
|
||||
|
||||
messagesReplace(messages: DMessage[]): void {
|
||||
this.chatActions.setMessages(this.conversationId, messages);
|
||||
|
||||
// if zeroing the messages, also terminate an active beam
|
||||
if (!messages.length)
|
||||
this.beamStore.getState().terminate();
|
||||
}
|
||||
|
||||
|
||||
// Beam
|
||||
|
||||
getBeamStore = () => this.beamStore;
|
||||
|
||||
/**
|
||||
* Opens a beam over the given history
|
||||
*
|
||||
* @param viewHistory The history up to the point where the beam is invoked
|
||||
* @param importMessages If set, any message to import into the beam as pre-set rays
|
||||
* @param destReplaceMessageId If set, the output will replace the message with this id, otherwise it will append to the history
|
||||
*/
|
||||
beamInvoke(viewHistory: Readonly<DMessage[]>, importMessages: DMessage[], destReplaceMessageId: DMessage['id'] | null): void {
|
||||
const { open: beamOpen, importRays: beamImportRays, terminate: beamTerminate } = this.beamStore.getState();
|
||||
|
||||
const onBeamSuccess = (messageText: string, llmId: DLLMId) => {
|
||||
// set output when going back to the chat
|
||||
if (destReplaceMessageId) {
|
||||
// replace a single message in the conversation history
|
||||
this.messageEdit(destReplaceMessageId, { text: messageText, originLLM: llmId }, true);
|
||||
} else {
|
||||
// replace (may truncate) the conversation history and append a message
|
||||
const newMessage = createDMessage('assistant', messageText);
|
||||
newMessage.originLLM = llmId;
|
||||
newMessage.purposeId = getConversationSystemPurposeId(this.conversationId) ?? undefined;
|
||||
this.messagesReplace([...viewHistory, newMessage]);
|
||||
}
|
||||
|
||||
// close beam
|
||||
this.beamStore.getState().terminate();
|
||||
};
|
||||
|
||||
beamOpen(viewHistory, useModelsStore.getState().chatLLMId, onBeamSuccess);
|
||||
importMessages.length && beamImportRays(importMessages);
|
||||
}
|
||||
|
||||
|
||||
// Ephemerals
|
||||
|
||||
@@ -69,33 +131,3 @@ export class ConversationHandler {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Singleton to get a global instance relate to a conversationId. Note we don't have reference counting, and mainly because we cannot
|
||||
// do comprehensive lifecycle tracking.
|
||||
export class ConversationManager {
|
||||
private static _instance: ConversationManager;
|
||||
private readonly handlers: Map<DConversationId, ConversationHandler> = new Map();
|
||||
|
||||
static getHandler(conversationId: DConversationId): ConversationHandler {
|
||||
const instance = ConversationManager._instance || (ConversationManager._instance = new ConversationManager());
|
||||
let handler = instance.handlers.get(conversationId);
|
||||
if (!handler) {
|
||||
handler = new ConversationHandler(conversationId);
|
||||
instance.handlers.set(conversationId, handler);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
// Acquires a ConversationHandler, ensuring automatic release when done, with debug location.
|
||||
// enable in 2025, after support from https://github.com/tc39/proposal-explicit-resource-management
|
||||
/*usingHandler(conversationId: DConversationId, debugLocation: string) {
|
||||
const handler = this.getHandler(conversationId, debugLocation);
|
||||
return {
|
||||
handler,
|
||||
[Symbol.dispose]: () => {
|
||||
this.releaseHandler(handler, debugLocation);
|
||||
},
|
||||
};
|
||||
}*/
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
|
||||
import { ConversationHandler } from './ConversationHandler';
|
||||
|
||||
/**
|
||||
* Singleton to get a global instance related to a conversationId. Note we don't have reference counting, and mainly because we cannot
|
||||
* do comprehensive lifecycle tracking.
|
||||
*
|
||||
* The handlers returned are used for overlaying transitory state on top of DB objects, and to provide utility methods that will survive
|
||||
* the former react-state implementation.
|
||||
*/
|
||||
export class ConversationsManager {
|
||||
private static _instance: ConversationsManager;
|
||||
private readonly handlers: Map<DConversationId, ConversationHandler> = new Map();
|
||||
|
||||
static getHandler(conversationId: DConversationId): ConversationHandler {
|
||||
const instance = ConversationsManager._instance || (ConversationsManager._instance = new ConversationsManager());
|
||||
let handler = instance.handlers.get(conversationId);
|
||||
if (!handler) {
|
||||
handler = new ConversationHandler(conversationId);
|
||||
instance.handlers.set(conversationId, handler);
|
||||
}
|
||||
return handler;
|
||||
}
|
||||
|
||||
// Acquires a ConversationHandler, ensuring automatic release when done, with debug location.
|
||||
// enable in 2025, after support from https://github.com/tc39/proposal-explicit-resource-management
|
||||
/*usingHandler(conversationId: DConversationId, debugLocation: string) {
|
||||
const handler = this.getHandler(conversationId, debugLocation);
|
||||
return {
|
||||
handler,
|
||||
[Symbol.dispose]: () => {
|
||||
this.releaseHandler(handler, debugLocation);
|
||||
},
|
||||
};
|
||||
}*/
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { customEventHelpers } from '~/common/util/eventUtils';
|
||||
|
||||
import type { ConversationHandler } from './ConversationHandler';
|
||||
|
||||
|
||||
@@ -23,6 +25,9 @@ function createDEphemeral(title: string, initialText: string): DEphemeral {
|
||||
};
|
||||
}
|
||||
|
||||
const [dispatchEphemeralsChanged, installEphemeralsChangedListener] = customEventHelpers<DEphemeral[]>('ephemeralsChanged');
|
||||
|
||||
|
||||
/**
|
||||
* [store]: diy reactive store for a list of ephemerals
|
||||
*/
|
||||
@@ -39,7 +44,7 @@ export class EphemeralsStore extends EventTarget {
|
||||
|
||||
append(ephemeral: DEphemeral): void {
|
||||
this.ephemerals.push(ephemeral);
|
||||
this.dispatchEvent(new CustomEvent('ephemeralsChanged', { detail: { ephemerals: this.ephemerals } }));
|
||||
dispatchEphemeralsChanged(this, this.ephemerals);
|
||||
}
|
||||
|
||||
delete(ephemeralId: string): void {
|
||||
@@ -47,7 +52,7 @@ export class EphemeralsStore extends EventTarget {
|
||||
console.log('EphemeralsStore: delete', index);
|
||||
if (index >= 0) {
|
||||
this.ephemerals.splice(index, 1);
|
||||
this.dispatchEvent(new CustomEvent('ephemeralsChanged', { detail: { ephemerals: this.ephemerals } }));
|
||||
dispatchEphemeralsChanged(this, this.ephemerals);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +60,7 @@ export class EphemeralsStore extends EventTarget {
|
||||
const ephemeral = this.ephemerals.find(e => e.id === ephemeralId);
|
||||
if (ephemeral) {
|
||||
Object.assign(ephemeral, update);
|
||||
this.dispatchEvent(new CustomEvent('ephemeralsChanged', { detail: { ephemerals: this.ephemerals } }));
|
||||
dispatchEphemeralsChanged(this, this.ephemerals);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -91,12 +96,7 @@ export function useEphemerals(conversationHandler: ConversationHandler | null):
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!conversationHandler) return;
|
||||
const handleEphemeralsChanged = (event: Event) => {
|
||||
const customEvent = event as CustomEvent<{ ephemerals: DEphemeral[] }>;
|
||||
setEphemerals([...customEvent.detail.ephemerals]);
|
||||
};
|
||||
conversationHandler.ephemeralsStore.addEventListener('ephemeralsChanged', handleEphemeralsChanged);
|
||||
return () => conversationHandler.ephemeralsStore.removeEventListener('ephemeralsChanged', handleEphemeralsChanged);
|
||||
return installEphemeralsChangedListener(conversationHandler.ephemeralsStore.find(), conversationHandler.ephemeralsStore, (detail) => setEphemerals([...detail]));
|
||||
}, [conversationHandler]);
|
||||
|
||||
return ephemerals;
|
||||
|
||||
@@ -13,26 +13,38 @@ import { GoodModal } from '~/common/components/GoodModal';
|
||||
export function ConfirmationModal(props: {
|
||||
open?: boolean, onClose: () => void, onPositive: () => void,
|
||||
title?: string | React.JSX.Element,
|
||||
noTitleBar?: boolean,
|
||||
lowStakes?: boolean,
|
||||
confirmationText: string | React.JSX.Element,
|
||||
positiveActionText: string
|
||||
positiveActionText: React.ReactNode,
|
||||
negativeActionText?: React.ReactNode,
|
||||
negativeActionStartDecorator?: React.ReactNode,
|
||||
}) {
|
||||
return (
|
||||
<GoodModal
|
||||
open={props.open === undefined ? true : props.open}
|
||||
title={props.title || 'Confirmation'}
|
||||
titleStartDecorator={<WarningRoundedIcon sx={{ color: 'danger.solidBg' }} />}
|
||||
title={props.noTitleBar ? undefined : (props.title || 'Confirmation')}
|
||||
titleStartDecorator={props.noTitleBar ? undefined : <WarningRoundedIcon sx={{ color: 'danger.solidBg' }} />}
|
||||
noTitleBar={props.noTitleBar}
|
||||
onClose={props.onClose}
|
||||
hideBottomClose
|
||||
>
|
||||
<Divider />
|
||||
{!props.noTitleBar && <Divider />}
|
||||
|
||||
<Typography level='body-md'>
|
||||
{props.confirmationText}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'flex-end', mt: 2 }}>
|
||||
<Button autoFocus variant='plain' color='neutral' onClick={props.onClose}>
|
||||
Cancel
|
||||
<Button autoFocus variant='plain' color='neutral' onClick={props.onClose} startDecorator={props.negativeActionStartDecorator}>
|
||||
{props.negativeActionText || 'Cancel'}
|
||||
</Button>
|
||||
<Button variant='solid' color='danger' onClick={props.onPositive} sx={{ lineHeight: '1.5em' }}>
|
||||
<Button
|
||||
variant={props.lowStakes ? 'soft' : 'solid'}
|
||||
color={props.lowStakes ? undefined : 'danger'}
|
||||
onClick={props.onPositive}
|
||||
sx={{ lineHeight: '1.5em' }}
|
||||
>
|
||||
{props.positiveActionText}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { InputProps } from '@mui/joy/Input';
|
||||
import { IconButton, Input } from '@mui/joy';
|
||||
import { Box, IconButton, Input } from '@mui/joy';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
@@ -12,11 +12,11 @@ type DebounceInputProps = Omit<InputProps, 'onChange'> & {
|
||||
};
|
||||
|
||||
const DebounceInput: React.FC<DebounceInputProps> = ({
|
||||
minChars,
|
||||
onDebounce,
|
||||
debounceTimeout,
|
||||
...rest
|
||||
}) => {
|
||||
minChars,
|
||||
onDebounce,
|
||||
debounceTimeout,
|
||||
...rest
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const timerRef = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
@@ -57,11 +57,14 @@ const DebounceInput: React.FC<DebounceInputProps> = ({
|
||||
aria-label={rest['aria-label'] || 'Search'}
|
||||
startDecorator={<SearchIcon />}
|
||||
endDecorator={
|
||||
!inputValue ? rest.endDecorator : (
|
||||
<IconButton aria-label='Clear search' onClick={handleClear}>
|
||||
<ClearIcon sx={{ fontSize: 'xl' }} />
|
||||
</IconButton>
|
||||
)
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{!!inputValue && (
|
||||
<IconButton size='sm' aria-label='Clear search' onClick={handleClear}>
|
||||
<ClearIcon sx={{ fontSize: 'xl' }} />
|
||||
</IconButton>
|
||||
)}
|
||||
{rest.endDecorator}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
import React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, Step, stepClasses, StepIndicator, stepIndicatorClasses, Stepper, Typography } from '@mui/joy';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded';
|
||||
|
||||
import { ChatMessageMemo } from '../../apps/chat/components/message/ChatMessage';
|
||||
|
||||
import { AgiSquircleIcon } from '~/common/components/icons/AgiSquircleIcon';
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { createDMessage } from '~/common/state/store-chats';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
|
||||
|
||||
// Steps - the top stepper
|
||||
|
||||
interface ExplainerStep {
|
||||
stepDigits: string,
|
||||
stepName: string,
|
||||
}
|
||||
|
||||
const stepSequenceSx: SxProps = {
|
||||
// width: '100%',
|
||||
[`& .${stepClasses.completed}::after`]: {
|
||||
bgcolor: 'primary.500',
|
||||
},
|
||||
[`& .${stepClasses.active} .${stepIndicatorClasses.root}`]: {
|
||||
borderColor: 'primary.500',
|
||||
},
|
||||
[`& .${stepClasses.root}:has(+ .${stepClasses.active})::after`]: {
|
||||
color: 'primary.500',
|
||||
backgroundColor: 'transparent',
|
||||
backgroundImage: 'radial-gradient(currentColor 2px, transparent 2px)',
|
||||
backgroundSize: '7px 7px',
|
||||
backgroundPosition: 'center left',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
function AllStepsStepper(props: {
|
||||
steps: ExplainerStep[],
|
||||
activeIndex: number,
|
||||
isMobile: boolean,
|
||||
onStepClicked: (stepIndex: number) => void,
|
||||
}) {
|
||||
return (
|
||||
<Stepper sx={stepSequenceSx}>
|
||||
{props.steps.map(((step, stepIndex) => {
|
||||
const completed = props.activeIndex > stepIndex;
|
||||
const active = props.activeIndex === stepIndex;
|
||||
return (
|
||||
<Step
|
||||
key={'step-' + stepIndex}
|
||||
orientation='vertical'
|
||||
completed={completed}
|
||||
active={active}
|
||||
onClick={() => props.onStepClicked(stepIndex)}
|
||||
indicator={
|
||||
<StepIndicator variant={(completed || active) ? 'solid' : 'outlined'} color='primary'>
|
||||
{completed ? <CheckRoundedIcon /> : active ? <KeyboardArrowDownRoundedIcon /> : undefined}
|
||||
</StepIndicator>
|
||||
}
|
||||
>
|
||||
<Typography
|
||||
fontSize={props.isMobile ? 'sm' : undefined}
|
||||
fontWeight='xl'
|
||||
endDecorator={
|
||||
step.stepName && <Typography fontSize='sm' fontWeight='normal' sx={{ mr: 0.5 }}>{step.stepName}</Typography>
|
||||
}
|
||||
>{step.stepDigits ?? null}</Typography>
|
||||
</Step>
|
||||
);
|
||||
}))}
|
||||
</Stepper>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
// The Explainer - Carousel of pages
|
||||
|
||||
export interface ExplainerPage extends ExplainerStep {
|
||||
titlePrefix?: string,
|
||||
titleSquircle?: boolean,
|
||||
titleSpark?: string,
|
||||
titleSuffix?: string,
|
||||
mdContent: string
|
||||
}
|
||||
|
||||
export function ExplainerCarousel(props: {
|
||||
steps: ExplainerPage[],
|
||||
footer?: React.ReactNode,
|
||||
showPrevious?: boolean,
|
||||
onFinished: () => any,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [stepIndex, setStepIndex] = React.useState(0);
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
// derived state
|
||||
const { onFinished } = props;
|
||||
const isLastPage = stepIndex === props.steps.length - 1;
|
||||
const activeStep = props.steps[stepIndex] ?? null;
|
||||
|
||||
// handlers
|
||||
|
||||
const mdText = activeStep?.mdContent ?? null;
|
||||
const mdMessage = React.useMemo(() => {
|
||||
return mdText ? createDMessage('assistant', mdText) : null;
|
||||
}, [mdText]);
|
||||
|
||||
const handlePrevPage = React.useCallback(() => {
|
||||
setStepIndex(step => step > 0 ? step - 1 : step);
|
||||
}, []);
|
||||
|
||||
const handleNextPage = React.useCallback(() => {
|
||||
if (isLastPage)
|
||||
onFinished();
|
||||
else
|
||||
setStepIndex(step => step < props.steps.length - 1 ? step + 1 : step);
|
||||
}, [isLastPage, onFinished, props.steps.length]);
|
||||
|
||||
|
||||
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
|
||||
[ShortcutKeyName.Left, false, false, false, handlePrevPage],
|
||||
[ShortcutKeyName.Right, false, false, false, handleNextPage],
|
||||
], [handleNextPage, handlePrevPage]);
|
||||
useGlobalShortcuts(shortcuts);
|
||||
|
||||
|
||||
// [effect] restart from 0 if steps change
|
||||
// React.useEffect(() => {
|
||||
// setStepIndex(0);
|
||||
// }, [props.steps]);
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
flex: 1,
|
||||
mx: 'auto',
|
||||
width: { sm: '92%', md: '86%' }, /* Default to 80% width */
|
||||
maxWidth: '820px', /* But don't go over 900px */
|
||||
|
||||
// content
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-around',
|
||||
gap: 2,
|
||||
}}>
|
||||
|
||||
|
||||
{/* Page Title */}
|
||||
<Typography
|
||||
level='h1'
|
||||
component='h1'
|
||||
sx={{
|
||||
fontSize: isMobile ? '2rem' : '2.75rem',
|
||||
fontWeight: 'md',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
{activeStep?.titlePrefix}{' '}
|
||||
{!!activeStep?.titleSquircle && <AgiSquircleIcon inverted sx={{ color: 'white', fontSize: isMobile ? '1.55rem' : '2.04rem', borderRadius: 'md' }} />}
|
||||
{!!activeStep?.titleSquircle && '-'}
|
||||
{!!activeStep?.titleSpark && <Box component='span' sx={{ fontWeight: 'lg', /*animation: `${animationTextShadowLimey} 15s linear infinite`*/ color: 'primary.softColor' }}>
|
||||
{activeStep.titleSpark}
|
||||
</Box>}{activeStep?.titleSuffix}
|
||||
</Typography>
|
||||
|
||||
|
||||
{/* All Steps */}
|
||||
<Box>
|
||||
<AllStepsStepper
|
||||
steps={props.steps}
|
||||
activeIndex={stepIndex}
|
||||
isMobile={isMobile}
|
||||
onStepClicked={setStepIndex}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Page Message */}
|
||||
{!!mdMessage && (
|
||||
<ChatMessageMemo
|
||||
message={mdMessage}
|
||||
fitScreen={isMobile}
|
||||
showAvatar={false}
|
||||
adjustContentScaling={isMobile ? 0 : undefined}
|
||||
sx={{
|
||||
minHeight: '19rem', // 256px
|
||||
py: 2,
|
||||
border: 'none',
|
||||
bordreRadius: 0,
|
||||
borderRadius: 'xl',
|
||||
// boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-darkChannel) / 0.12)',
|
||||
boxShadow: '0 60px 32px -60px rgb(var(--joy-palette-primary-darkChannel) / 0.14)',
|
||||
|
||||
// customize the embedded GitHub Markdown for transparent images
|
||||
['.markdown-body img']: {
|
||||
'--color-canvas-default': 'transparent!important',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Buttons */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
|
||||
{/* Advance Button */}
|
||||
<Button
|
||||
variant='solid'
|
||||
size='lg'
|
||||
endDecorator={isLastPage ? <ChatBeamIcon /> : <ArrowForwardRoundedIcon />}
|
||||
onClick={handleNextPage}
|
||||
sx={{
|
||||
boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
|
||||
minWidth: 180,
|
||||
}}
|
||||
>
|
||||
{isLastPage ? 'Start' : 'Next'}
|
||||
</Button>
|
||||
|
||||
{/* Back Button */}
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
onClick={handlePrevPage}
|
||||
sx={{
|
||||
minWidth: 140,
|
||||
}}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Final words of wisdom (also perfect for centering the other components) */}
|
||||
{props.footer}
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,14 @@
|
||||
import * as React from 'react';
|
||||
// import { keyframes } from '@emotion/react';
|
||||
|
||||
import { Box, Button, Divider, Modal, ModalClose, ModalDialog, ModalOverflow, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
// // noinspection CssUnresolvedCustomProperty
|
||||
// const cssBackgroundFadeIn = keyframes`
|
||||
// 0% {
|
||||
// background-color: transparent
|
||||
// }
|
||||
// 100% {
|
||||
// background-color: var(--joy-palette-background-backdrop)
|
||||
// }`;
|
||||
|
||||
|
||||
/**
|
||||
* Base for our Modal components (Preferences, Models Setup, etc.)
|
||||
*/
|
||||
export function GoodModal(props: {
|
||||
title?: string | React.JSX.Element,
|
||||
title?: React.ReactNode,
|
||||
titleStartDecorator?: React.JSX.Element,
|
||||
strongerTitle?: boolean,
|
||||
noTitleBar?: boolean,
|
||||
|
||||
@@ -8,9 +8,10 @@ import { SxProps } from '@mui/joy/styles/types';
|
||||
* Tooltip with text that wraps to multiple lines (doesn't go too long)
|
||||
*/
|
||||
export const GoodTooltip = (props: {
|
||||
title: string | React.JSX.Element | null,
|
||||
title: React.ReactNode,
|
||||
placement?: 'top' | 'bottom' | 'top-start',
|
||||
isError?: boolean, isWarning?: boolean,
|
||||
usePlain?: boolean,
|
||||
children: React.JSX.Element,
|
||||
sx?: SxProps
|
||||
}) =>
|
||||
@@ -18,7 +19,7 @@ export const GoodTooltip = (props: {
|
||||
title={props.title}
|
||||
placement={props.placement}
|
||||
disableInteractive
|
||||
variant={(props.isError || props.isWarning) ? 'soft' : undefined}
|
||||
variant={(props.isError || props.isWarning) ? 'soft' : props.usePlain ? 'plain' : undefined}
|
||||
color={props.isError ? 'danger' : props.isWarning ? 'warning' : undefined}
|
||||
sx={{
|
||||
maxWidth: { sm: '50vw', md: '25vw' },
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Textarea } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
@@ -9,7 +9,9 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
export function InlineTextarea(props: {
|
||||
initialText: string,
|
||||
placeholder?: string,
|
||||
decolor?: boolean
|
||||
invertedColors?: boolean,
|
||||
minRows?: number,
|
||||
onEdit: (text: string) => void,
|
||||
onCancel?: () => void,
|
||||
sx?: SxProps,
|
||||
@@ -38,9 +40,9 @@ export function InlineTextarea(props: {
|
||||
return (
|
||||
<Textarea
|
||||
variant={props.invertedColors ? 'plain' : 'soft'}
|
||||
color={props.invertedColors ? 'primary' : 'warning'}
|
||||
autoFocus
|
||||
minRows={1}
|
||||
color={props.decolor ? undefined : props.invertedColors ? 'primary' : 'warning'}
|
||||
autoFocus={!props.decolor}
|
||||
minRows={props.minRows !== undefined ? props.minRows : 1}
|
||||
placeholder={props.placeholder}
|
||||
value={text} onChange={handleEditTextChanged}
|
||||
onKeyDown={handleEditKeyDown} onBlur={handleEditBlur}
|
||||
|
||||
@@ -10,9 +10,11 @@ import { isMacUser } from '~/common/util/pwaUtils';
|
||||
export function platformAwareKeystrokes(text: string) {
|
||||
return isMacUser
|
||||
? text
|
||||
.replaceAll('Ctrl', '⌘')
|
||||
.replaceAll('Alt', '⌥')
|
||||
.replaceAll('Ctrl', '⌘' /* Command */)
|
||||
.replaceAll('Alt', '⌥' /* Option */)
|
||||
.replaceAll('Shift', '⇧')
|
||||
// Optional: Replace "Enter" with "Return" if you want to align with Mac keyboard labeling
|
||||
// .replaceAll('Enter', 'Return')
|
||||
: text;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { Droppable, type DroppableProps } from 'react-beautiful-dnd';
|
||||
|
||||
|
||||
/**
|
||||
* A wrapper around React-Beautiful-Dnd `Droppable` that skips the first render,
|
||||
*/
|
||||
export function StrictModeDroppable({ children, ...props }: DroppableProps) {
|
||||
const [enabled, setEnabled] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const animation = requestAnimationFrame(() => setEnabled(true));
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animation);
|
||||
setEnabled(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return enabled ? <Droppable {...props}>{children}</Droppable> : null;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, FormHelperText, FormLabel } from '@mui/joy';
|
||||
import { FormHelperText, FormLabel } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import InfoIcon from '@mui/icons-material/Info';
|
||||
|
||||
@@ -11,40 +11,41 @@ import { formLabelStartWidth } from '~/common/app.theme';
|
||||
/**
|
||||
* Shared label part (left side)
|
||||
*/
|
||||
export const FormLabelStart = (props: {
|
||||
const FormLabelStartBase = (props: {
|
||||
title: string | React.JSX.Element,
|
||||
description?: string | React.JSX.Element
|
||||
tooltip?: string | React.JSX.Element,
|
||||
onClick?: (event: React.MouseEvent) => void,
|
||||
sx?: SxProps,
|
||||
}) => React.useMemo(() =>
|
||||
<Box>
|
||||
{/* Title */}
|
||||
<FormLabel
|
||||
onClick={props.onClick}
|
||||
}) =>
|
||||
<div>
|
||||
{/* Title */}
|
||||
<FormLabel
|
||||
onClick={props.onClick}
|
||||
sx={{
|
||||
minWidth: formLabelStartWidth,
|
||||
...(!!props.onClick && { cursor: 'pointer', textDecoration: 'underline' }),
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{props.title} {props.tooltip && (
|
||||
<GoodTooltip title={props.tooltip}>
|
||||
<InfoIcon sx={{ ml: 0.5, cursor: 'pointer', fontSize: 'md', color: 'primary.solidBg' }} />
|
||||
</GoodTooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
|
||||
{/* [SubTitle] */}
|
||||
{!!props.description && (
|
||||
<FormHelperText
|
||||
sx={{
|
||||
minWidth: formLabelStartWidth,
|
||||
...(!!props.onClick && { cursor: 'pointer', textDecoration: 'underline' }),
|
||||
...props.sx,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{props.title} {props.tooltip && (
|
||||
<GoodTooltip title={props.tooltip}>
|
||||
<InfoIcon sx={{ mx: 0.5, cursor: 'pointer', fontSize: 'md', color: 'primary.solidBg' }} />
|
||||
</GoodTooltip>
|
||||
)}
|
||||
</FormLabel>
|
||||
{props.description}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</div>;
|
||||
FormLabelStartBase.displayName = 'FormLabelStart';
|
||||
|
||||
{/* [SubTitle] */}
|
||||
{!!props.description && (
|
||||
<FormHelperText
|
||||
sx={{
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
{props.description}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</Box>
|
||||
, [props.onClick, props.sx, props.title, props.tooltip, props.description],
|
||||
);
|
||||
export const FormLabelStart = React.memo(FormLabelStartBase);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user