Compare commits

...

453 Commits

Author SHA1 Message Date
Enrico Ros 78e3a57857 parsing of HTML code blocks 2024-04-02 21:07:35 -07:00
Enrico Ros 79d0c96b20 Gemini: call out RECITATIONS 2024-04-02 20:53:26 -07:00
Enrico Ros 21ed38a20e DuoTonal for AI functions 2024-04-02 19:20:35 -07:00
Enrico Ros d8b1f99114 Divider 2024-04-02 18:42:33 -07:00
Enrico Ros b0fb1b9890 Fix build 2024-04-02 01:48:11 -07:00
Enrico Ros a63932cff2 Show HTML code when beaming, by default 2024-04-02 01:42:06 -07:00
Enrico Ros 0b22165d2a Beam: remove link 2024-04-02 01:29:23 -07:00
Enrico Ros 41b1951abe Merge pull request #481 from aj47/patch-1
Update README.md typo
2024-04-01 23:41:37 -07:00
AJ (@techfren) 353431e54c Update README.md 2024-04-02 17:41:08 +11:00
Enrico Ros 7b232dd7d8 Renamed vercel.json to vercel_PRODUCTION.json to get it out of the way and fix #468
Fix #468 once and for all. Documentation on the env
2024-04-01 18:05:27 -07:00
Enrico Ros d32adf9dbf 1.15.0: Add hackernews callout 2024-04-01 17:17:33 -07:00
Enrico Ros 940d490217 1.15.0: Beam News improved copy 2024-04-01 15:29:08 -07:00
Enrico Ros 46e41e38cf 1.15.0: Beam News callout 2024-04-01 15:15:52 -07:00
Enrico Ros 276ff8f995 Merge branch 'release-1.15.0' 2024-04-01 15:05:10 -07:00
Enrico Ros 030837fccf 1.15.0: Readme and Changelog 2024-04-01 15:04:15 -07:00
Enrico Ros a7d38aefb1 1.15.0: Update News 2024-04-01 14:42:07 -07:00
Enrico Ros 230a0d7caf Beam: update intro. 2024-04-01 14:21:12 -07:00
Enrico Ros 6e14e43c78 Beam: update in-app explainer. 2024-04-01 14:07:33 -07:00
Enrico Ros e6389f08be Branch before delete 2024-03-30 23:19:48 -07:00
Enrico Ros a4edeb098e 1.15.0: news placeholder 2024-03-30 20:14:30 -07:00
Enrico Ros 093c536415 1.15.0: version number 2024-03-30 19:04:37 -07:00
Enrico Ros 7479b50fea 1.15.0: Disable Title Bar Setting (2 people got confused) 2024-03-30 19:03:45 -07:00
Enrico Ros ebce36d043 1.15.0: Package Version 2024-03-30 18:57:46 -07:00
Enrico Ros 77bab1aa74 Beam: earlyaccess: edit the Custom merges 2024-03-30 01:52:01 -07:00
Enrico Ros ebcac3405c Beam: custom: improve icon 2024-03-30 00:56:15 -07:00
Enrico Ros d2781a6f87 Beam: custom: do not auto-start Custom 2024-03-30 00:40:23 -07:00
Enrico Ros f5954f5bb3 Beam: Gather: change some prop names 2024-03-30 00:40:08 -07:00
Enrico Ros 6baf694d6f Beam: earlyaccess: remove Show Dev Methods 2024-03-30 00:20:08 -07:00
Enrico Ros cb3b586d4d Beam: custom: improve hardcoding 2024-03-30 00:16:41 -07:00
Enrico Ros f68789ab20 Beam: earlyaccess: add Menu Option for response identification 2024-03-30 00:13:33 -07:00
Enrico Ros 0c6a3f1917 Beam: earlyaccess: Fix the "checklist issue" with the mentioned "unicode bullet" 2024-03-29 23:45:53 -07:00
Enrico Ros 05fccaf982 Beam: earlyaccess: Improve popup menu to hint at saving/loading model combos 2024-03-29 23:42:47 -07:00
Enrico Ros 7340b9ecc2 Beam: earlyaccess: Address the UI problem where the menu option does not open when the screen is maximized 2024-03-29 23:15:57 -07:00
Enrico Ros 78eb4ebe0b Beam: earlyaccess: Correct the "Synthesizing" typo. 2024-03-29 23:07:03 -07:00
Enrico Ros b1453a34ec Beam: fix issue with older installs 2024-03-29 21:48:28 -07:00
Enrico Ros c357e9e2f5 Beam: ensure non-empty gather messages 2024-03-29 21:48:28 -07:00
Enrico Ros 98717bf8a9 Beam: scroller: smaller 2024-03-29 21:48:28 -07:00
Enrico Ros d7077ada0e Beam: scroll the instruciton gen too 2024-03-29 21:48:28 -07:00
Enrico Ros 64f63ed1d3 Beam: score to 100 2024-03-29 21:48:28 -07:00
Enrico Ros 2a27f6c30d Beam: fusion zone 2024-03-29 21:48:28 -07:00
Enrico Ros 9fdddeaba8 Beam: Checklist -> Guided 2024-03-29 21:48:28 -07:00
Enrico Ros 2cfa5e93e4 Beam: improve prompts 2024-03-29 21:48:28 -07:00
Enrico Ros 778ac14344 Beam: enhance checklist quality 2024-03-29 21:48:28 -07:00
Enrico Ros 85fcf8be61 Beam: re-merge will not change the model 2024-03-29 21:48:28 -07:00
Enrico Ros b31eb09015 Beam: no green shade 2024-03-29 21:48:28 -07:00
Enrico Ros 5154dd1740 Beam: improve Fusion layout 2024-03-29 21:48:27 -07:00
Enrico Ros 274f11ef1d Beam: change the Fusion model 2024-03-29 21:48:27 -07:00
Enrico Ros aeb1acf458 Beam: bits 2024-03-29 21:48:27 -07:00
Enrico Ros a204f4a58e Beam: Ray grid: bits 2024-03-29 21:48:27 -07:00
Enrico Ros 8e4a57aa01 Beam: auto-fit 2024-03-29 21:48:27 -07:00
Enrico Ros 797ed0a553 Beam: shorter scroll on mobile beams 2024-03-29 21:48:27 -07:00
Enrico Ros 663bc0d471 Beam: shadow on mobile scatter 2024-03-29 21:48:27 -07:00
Enrico Ros 8d7e2d2c46 Beam: remove lastScatterLlmId 2024-03-29 21:48:27 -07:00
Enrico Ros 19d96bb30b Beam: remove llm Linkage 2024-03-29 21:48:27 -07:00
Enrico Ros 47f2f20d9c Beam: relax checklist parsing 2024-03-29 21:48:27 -07:00
Enrico Ros 12c7c634c0 Beam: improve LLM usage 2024-03-29 21:48:27 -07:00
Enrico Ros 9a322c150a Beam: reduce space 2024-03-29 21:48:27 -07:00
Enrico Ros 1a3bc4f666 Beam: move instructions 2024-03-29 21:48:27 -07:00
Enrico Ros d4881b1ce5 Beam: move to modules 2024-03-29 21:48:27 -07:00
Enrico Ros a2ad2df473 Beam: prompt update 2024-03-29 21:48:27 -07:00
Enrico Ros 541c5bd1c3 Beam: prompt update 2024-03-29 21:48:27 -07:00
Enrico Ros b744e9673b Beam: Checklist done 2024-03-29 21:48:27 -07:00
Enrico Ros bb94b7c5c6 Beam: prompt updates 2024-03-29 21:48:27 -07:00
Enrico Ros e9ff57d5e1 Beam: Gather: User Input 2024-03-29 21:48:27 -07:00
Enrico Ros 179245457c Beam: Gather: extract instructions 2024-03-29 21:48:27 -07:00
Enrico Ros 1493f74691 Beam: Gather: render 2024-03-29 21:48:27 -07:00
Enrico Ros 4857503ed3 Beam: Gather: intermediate components 2024-03-29 21:48:27 -07:00
Enrico Ros a0e38b4f0c Beam: Gather: begin ui production 2024-03-29 21:48:27 -07:00
Enrico Ros 1d62cad9e9 Beam: Gather: large state redux 2024-03-29 21:48:27 -07:00
Enrico Ros 855761020c Beam: Instructions: interrupt the fake user op 2024-03-29 21:48:27 -07:00
Enrico Ros 0950d06dfb Beam: Instructions: improve state machinery much 2024-03-29 21:48:27 -07:00
Enrico Ros 1496402325 Beam: layout seems ok 2024-03-29 21:48:27 -07:00
Enrico Ros 77e2c4babb Beam: stop this madness 2024-03-29 21:48:27 -07:00
Enrico Ros a465082984 Beam: Dev methods by default 2024-03-29 21:48:27 -07:00
Enrico Ros 025fdac686 Beam: Iconoclastic 2024-03-29 21:48:27 -07:00
Enrico Ros 6bde5ec64c Beam: Gather: Fin of Fin 2024-03-29 21:48:27 -07:00
Enrico Ros f099a9ec39 Beam: Gather Cleanups galore 2024-03-29 21:48:27 -07:00
Enrico Ros 5bfcef92ee Beam: Persist (and get off the way) more state 2024-03-29 21:48:27 -07:00
Enrico Ros 79a8fbd881 Beam: Extract the Module Beam Store 2024-03-29 21:48:27 -07:00
Enrico Ros 7f96a14cf6 Beam: Debug: swap properties 2024-03-29 21:48:27 -07:00
Enrico Ros 5fe6d70713 Beam: App: more clearer debug 2024-03-29 21:48:27 -07:00
Enrico Ros dcba4dd4bc Beam: App: clearer debug 2024-03-29 21:48:27 -07:00
Enrico Ros ccbe77913b Beam: Gather: beginning of output 2024-03-29 21:48:27 -07:00
Enrico Ros 2844cb81c2 Beam: Gather: wait indicator 2024-03-29 21:48:27 -07:00
Enrico Ros d86e8e5920 Beam: bits 2024-03-29 21:48:27 -07:00
Enrico Ros 9665fa1eb4 Beam: good button on mobile 2024-03-29 21:48:27 -07:00
Enrico Ros 2788ef679b Beam: scroll-fix 2024-03-29 21:48:27 -07:00
Enrico Ros e1a88e1fd8 Beam: move gapper 2024-03-29 21:48:27 -07:00
Enrico Ros 32163c5302 Beam: bottom gapper 2024-03-29 21:48:26 -07:00
Enrico Ros 2d3d5efe87 Beam: fix merge dim when inactive 2024-03-29 21:48:26 -07:00
Enrico Ros e1bbba392c Beam: Scatter: save file rename 2024-03-29 21:48:26 -07:00
Enrico Ros ed642c856b Beam: Scatter: complete the dialog 2024-03-29 21:48:26 -07:00
Enrico Ros 927e462f7a Beam: Scatter: preset save (full) 2024-03-29 21:48:26 -07:00
Enrico Ros e250499a3b Beam: Scatter: preset save (part) 2024-03-29 21:48:26 -07:00
Enrico Ros 91d96a6639 Beam: bits 2024-03-29 21:48:26 -07:00
Enrico Ros 104ec4c87c Beam: improve Composer button 2024-03-29 21:48:26 -07:00
Enrico Ros 0a7e8436c3 Beam: Gather: starts to work like a charm 2024-03-29 21:48:26 -07:00
Enrico Ros 9e597e0a28 Beam: Gather: first response! 2024-03-29 21:48:26 -07:00
Enrico Ros 01fbb5d47c Beam: Gather: rename executor to instructions 2024-03-29 21:48:26 -07:00
Enrico Ros 6517d16337 Beam: Gather: Mega Pint of state cleanup 2024-03-29 21:48:26 -07:00
Enrico Ros 0e636adf28 Beam: Gather: more state cleanuppery 2024-03-29 21:48:26 -07:00
Enrico Ros 0bb281237b Beam: Gather: some customization 2024-03-29 21:48:26 -07:00
Enrico Ros 2b224376c2 Beam: Gather: further improvements 2024-03-29 21:48:26 -07:00
Enrico Ros e510b369d7 Beam: Gather: ui fix 2024-03-29 21:48:26 -07:00
Enrico Ros a0de1f7230 Beam: Gather: wire things up 2024-03-29 21:48:26 -07:00
Enrico Ros 4591132269 Beam: the ghost in the machine 2024-03-29 21:48:26 -07:00
Enrico Ros a03de8d490 Beam: Gather: And Here We Go (Again -final.r002.copy.goodone) 2024-03-29 21:48:26 -07:00
Enrico Ros 27bcfec17e Beam: Gather: And Here We Go (Again) 2024-03-29 21:48:26 -07:00
Enrico Ros f6dbec3e1d Beam: Gather: And Here We Go 2024-03-29 21:48:26 -07:00
Enrico Ros aebc45f705 Beam: Gather: messaging & lime 2024-03-29 21:48:26 -07:00
Enrico Ros 310c60b9d9 Beam: Gather: pre-fusion 2024-03-29 21:48:26 -07:00
Enrico Ros bcba67c209 Beam: 4->6px 2024-03-29 21:48:26 -07:00
Enrico Ros fc013aed52 Beam: Gather: icons 2024-03-29 21:48:26 -07:00
Enrico Ros 8ad41c059b Beam: Scatter: cleaner 2024-03-29 21:48:26 -07:00
Enrico Ros 8eaf8db850 Beam: Gather: cleaner 2024-03-29 21:48:26 -07:00
Enrico Ros 896883766c Beam: Gather: perfect styles 2024-03-29 21:48:26 -07:00
Enrico Ros 258dacf3ed Beam: Gather: higher contrast 2024-03-29 21:48:26 -07:00
Enrico Ros 242243f485 Beam: Gather: even better ux 2024-03-29 21:48:26 -07:00
Enrico Ros a18436dce1 Beam: Gather: real good ux 2024-03-29 21:48:26 -07:00
Enrico Ros 5323cbc00e Beam: Gather: simplify state 2024-03-29 21:48:26 -07:00
Enrico Ros ddd3b137ac Beam: Gather: convert to Fusion IDs 2024-03-29 21:48:26 -07:00
Enrico Ros 94550088e5 Beam: Gather: show/hide dev methods 2024-03-29 21:48:26 -07:00
Enrico Ros 1375ca6f5c Beam: Gather: style multiline 2024-03-29 21:48:26 -07:00
Enrico Ros e4c4fe0495 Beam: Gather: start from 0 2024-03-29 21:48:26 -07:00
Enrico Ros 2fa5277e56 Beam: Gather: add Eval 2024-03-29 21:48:26 -07:00
Enrico Ros b73ad8fdc1 Beam: Gather: icons 2024-03-29 21:48:26 -07:00
Enrico Ros 9cc281e65e Beam: redo optionality 2024-03-29 21:48:26 -07:00
Enrico Ros d62107d39b 1.15.0: Cover image 2024-03-29 21:47:33 -07:00
Enrico Ros 4a8d20ad72 News: raise the quality 75 -> 90 2024-03-29 21:47:33 -07:00
Enrico Ros 5acb72c39b T2I: max 4 columns 2024-03-29 21:47:33 -07:00
Enrico Ros 67e8236a60 Fix deprecation 2024-03-29 21:47:32 -07:00
Enrico Ros 18b8853f82 Merge branch 'main-stable' 2024-03-29 21:39:05 -07:00
Enrico Ros 65c7df7938 Backend: auto-configuration. Fixes #436 2024-03-29 05:07:37 -07:00
Enrico Ros 15678cdfa2 Backend: removed onSuccess callbacks! 2024-03-29 05:07:36 -07:00
Enrico Ros 6cd6c62046 Backend: migration to async fetch from Query. plus consistency of behaviors 2024-03-29 05:07:36 -07:00
Enrico Ros dbf92805a2 Backend: reprio 2024-03-29 05:07:35 -07:00
Enrico Ros 11fc9a7b85 Backend: capability variables 2024-03-29 05:07:35 -07:00
Enrico Ros 8bc970ff57 Backend: autoconf only on chat 2024-03-29 05:07:34 -07:00
Enrico Ros a16eefd97b react-query: disable refetch on focus by default 2024-03-29 05:07:34 -07:00
Enrico Ros ca5e5b820c Backend: autoconf base logic 2024-03-29 05:07:33 -07:00
Enrico Ros f73ad52441 Backend: ->getBackendCapabilities() 2024-03-29 05:07:33 -07:00
Enrico Ros 729ec1d1bf Backend: config hash, to detect backend config updates 2024-03-29 05:07:32 -07:00
Enrico Ros 4adb30b861 AppChat: use intent to navigate to it from the link importer 2024-03-29 05:07:32 -07:00
Enrico Ros 999f6de45f Serverless Functions timeout: set it in the Vercel functions as the conditional was not working. Fix (again) #468 2024-03-28 23:20:40 -07:00
Enrico Ros 70686502b4 Revert "Set the Vercel serverless max duration as env variable. Fixes #468"
This reverts commit d17a980151.
2024-03-28 23:16:11 -07:00
Enrico Ros d17a980151 Set the Vercel serverless max duration as env variable. Fixes #468 2024-03-28 23:12:25 -07:00
Enrico Ros 7fa5947030 Chat Nav Grouping: when unset, the search won't sort by frequency
TODO: needs a better UX pattern here.
2024-03-28 22:48:12 -07:00
Enrico Ros de8f120fd4 Update README.md 2024-03-28 17:01:11 -07:00
Enrico Ros 9b54603264 Update README.md 2024-03-28 17:00:10 -07:00
Enrico Ros 698c77d7ba Tease the upcoming Beam 2024-03-28 16:53:34 -07:00
Enrico Ros 18d83a4d18 PersonaSelector: better tiles 2024-03-27 22:11:52 -07:00
Enrico Ros 8e849d93b2 Style fixes 2024-03-27 21:59:49 -07:00
Enrico Ros 4ca42f028b SVG: parse alternatives 2024-03-27 21:23:19 -07:00
Enrico Ros 3118337879 Timeout on Vercel/Serverless raised to 25 (for Browsing/Browserless requests) 2024-03-27 21:22:47 -07:00
Enrico Ros db4490affb SVG: improve compat with Opus 2024-03-27 18:33:28 -07:00
Enrico Ros 51ab79384e SVG: more compatible 2024-03-27 18:33:28 -07:00
Enrico Ros 3ee30a252d Creator: fixes 2024-03-27 18:33:28 -07:00
Enrico Ros b883566ebb Shrink the Folders list when running out of space (at twice the Chat Titles rate) 2024-03-27 18:32:46 -07:00
Enrico Ros ac78fb85b8 Shadow 2024-03-27 18:32:46 -07:00
Enrico Ros 0d2b11d0c4 Fonts 2024-03-27 18:32:45 -07:00
Enrico Ros 5b610c88c1 Gemini: fix RECITATION 2024-03-27 18:32:45 -07:00
Enrico Ros bf444ce043 Attachments: support RMB 2024-03-27 18:32:45 -07:00
Enrico Ros c91c027dab Compress icons 2024-03-27 18:32:44 -07:00
Enrico Ros 81fd87c510 Reduced badges 2024-03-27 18:32:44 -07:00
Enrico Ros 9da174a962 Roll packages 2024-03-27 18:32:44 -07:00
Enrico Ros 84f54a7e65 PersonaSelector: improve examples 2024-03-27 18:25:41 -07:00
Enrico Ros baeecf1464 PersonaSelector: reshade 2024-03-27 18:25:34 -07:00
Enrico Ros f2fdd39c96 Persona Selector: smaller tiles 2024-03-27 18:25:18 -07:00
Enrico Ros 53b074d78e Personas: show enablement, not disablement 2024-03-27 18:22:38 -07:00
Enrico Ros f4fc1e6775 Persona: update example 2024-03-27 18:22:28 -07:00
Enrico Ros dba791b8db Personas: update Dev examples 2024-03-27 18:22:24 -07:00
Enrico Ros 750fa02621 Personas: update custom task 2024-03-27 18:22:20 -07:00
Enrico Ros 7a67816111 Update default prompt. 2024-03-27 18:22:14 -07:00
Enrico Ros 613625644e LocalAI T2I: integration skel 2024-03-23 04:16:59 -07:00
Enrico Ros 0e25071ef0 Prevent pull-to-refresh on mobile - would be triggered while scrolling up 2024-03-22 22:40:56 -07:00
Enrico Ros ed1932cd26 Link env vars 2024-03-20 23:08:47 -07:00
Enrico Ros 67b89213d0 Your input 2024-03-20 22:39:34 -07:00
Enrico Ros 814f142c5f Fix zIndex of the ScrollToBottomButton 2024-03-20 22:39:33 -07:00
Enrico Ros 16cd3e7d5a Desktop Nav: fix key 2024-03-20 04:55:30 -07:00
Enrico Ros c5dcb8faef Beam: Gather: disable for now 2024-03-20 04:54:53 -07:00
Enrico Ros 6b46c022f9 Beam: Gather: improve prompt definitions 2024-03-20 03:56:15 -07:00
Enrico Ros 88ef05fc72 Beam: Gather: baseline prompts 2024-03-20 03:13:48 -07:00
Enrico Ros 445ea367fc Beam: copy Ray to clipboard 2024-03-20 02:10:20 -07:00
Enrico Ros c819554f43 Prompt-mixin: custom filters 2024-03-20 02:08:01 -07:00
Enrico Ros bbc8a79ded Beam: inline edit the Custom 2024-03-20 01:25:25 -07:00
Enrico Ros 3d181bc10d Beam: optimize App 2024-03-20 00:40:49 -07:00
Enrico Ros ba5478f382 Beam: Fusion: improved Input 2024-03-20 00:25:52 -07:00
Enrico Ros 136c993c8d Beam: Fusion: show prompts option 2024-03-19 23:00:23 -07:00
Enrico Ros 6cf18ea4e8 fix tooltip missing on nav 2024-03-19 22:46:54 -07:00
Enrico Ros fe7f56c82e fix check icon 2024-03-19 22:46:45 -07:00
Enrico Ros 6c580f1e43 Beam: Gather: edit custom instructions 2024-03-19 19:51:30 -07:00
Enrico Ros f171cd4f03 Beam: Gather: enable customization 2024-03-19 18:12:38 -07:00
Enrico Ros ea109e6c30 EditRounded 2024-03-19 13:51:36 -07:00
Enrico Ros f514eed226 Beam: Gather: instruction definition 2024-03-19 13:47:49 -07:00
Enrico Ros 274ba80149 Beam: Gather: bits 2024-03-19 11:57:45 -07:00
Enrico Ros 46b4dfc458 Beam: Gather: reinit state 2024-03-19 11:52:24 -07:00
Enrico Ros 4af8f4ff6a [desktop] Improve overflow 2024-03-19 11:40:20 -07:00
Enrico Ros df5810d695 [desktop] Application Overflow menu 2024-03-19 11:32:25 -07:00
Enrico Ros d9ad96c374 Beam: 'from chat' 2024-03-19 02:21:43 -07:00
Enrico Ros 06cc93fd82 Beam: begin Fusion state 2024-03-19 02:16:50 -07:00
Enrico Ros 41da63765f Beam: state cleanup 2024-03-19 01:33:27 -07:00
Enrico Ros 3975411c78 Beam: slices pattern 2024-03-19 01:09:38 -07:00
Enrico Ros fc2e75ef61 Beam: separated gather and scatter, physically 2024-03-19 00:00:40 -07:00
Enrico Ros ef0f2dd3d0 Beam: bits 2024-03-18 23:44:40 -07:00
Enrico Ros 548c3c5d72 Beam: clean styles 2024-03-18 20:32:15 -07:00
Enrico Ros d2e3a0cb8e Beam: add gather config and fusion 2024-03-18 20:16:38 -07:00
Enrico Ros 9cdace6f81 Beam: rename Panes 2024-03-18 19:09:11 -07:00
Enrico Ros 12f020570e Beam: extract Scatter input 2024-03-18 19:07:55 -07:00
Enrico Ros bef2551eec Beam: Gather commands shall be ok 2024-03-18 18:49:05 -07:00
Enrico Ros 7e20f8c189 Beam: wire Gather 2024-03-18 18:30:31 -07:00
Enrico Ros 56e8390e55 Beam: Fusion rename 2024-03-18 17:52:32 -07:00
Enrico Ros 89fff16385 Beam: Gather style 2024-03-18 04:00:00 -07:00
Enrico Ros 2cf15a24eb Beam: Gather layout 2024-03-18 03:48:30 -07:00
Enrico Ros 512e867034 Beam: final style fixes on Beam 2024-03-18 02:44:37 -07:00
Enrico Ros ce8c55c3c7 Beam: the beam panel seems done 2024-03-18 02:24:56 -07:00
Enrico Ros 8e0d904d9a Beam: Style updates 2024-03-18 00:57:26 -07:00
Enrico Ros 6c846a8ae7 Beam: very large state update 2024-03-18 00:03:10 -07:00
Enrico Ros 5004469fe9 Beam: DRay -> BRay 2024-03-17 21:54:42 -07:00
Enrico Ros 14d0af74ed Beam: extract rays 2024-03-17 21:51:38 -07:00
Enrico Ros 5a76cf9486 Beam: move the pre-beam where it shall go 2024-03-17 21:34:06 -07:00
Enrico Ros 82901ccd02 Beam: desktop sticky controls for Scatter and Gather 2024-03-17 21:26:56 -07:00
Enrico Ros 1dc9d66673 Beam: unused callout 2024-03-17 21:20:51 -07:00
Enrico Ros a0cbfaf390 Beam: fix explainer layout 2024-03-17 21:14:35 -07:00
Enrico Ros 9a01ae61ef ChatDrawer (item groups): sticky 2024-03-17 17:01:22 -07:00
Enrico Ros 91837d5acd Optimize 2024-03-17 16:47:00 -07:00
Enrico Ros 1b9ebdda22 Beam: Maximized Mode(al) 2024-03-17 16:43:33 -07:00
Enrico Ros b6f6177af3 Beam: improve looks 2024-03-17 16:08:11 -07:00
Enrico Ros d35486196b Scroll/Beam: embeddable ScrollToBottomButton 2024-03-17 16:05:31 -07:00
Enrico Ros 1603637e3b Scroll/Beam: improve usage 2024-03-17 15:53:44 -07:00
Enrico Ros 8f20840169 Beam: optimize when in Chat 2024-03-17 15:28:49 -07:00
Enrico Ros 4fff2394de ScrollToBottom: centralize styles 2024-03-17 15:28:00 -07:00
Enrico Ros afb74e68ee ScrollToBottom: moved to shared components 2024-03-17 14:54:15 -07:00
Enrico Ros d5fa7844c5 ScrollToBottom: allow to disable auto-stick (button only) 2024-03-17 14:47:02 -07:00
Enrico Ros b8470cd640 ScrollToBottom: allow the button to be inline 2024-03-17 14:44:27 -07:00
Enrico Ros 9a23f573a6 Beam: remove badge (hat on a hat) 2024-03-16 21:17:38 -07:00
Enrico Ros efe8fa0fda Beam: remove Phase 2024-03-16 21:16:27 -07:00
Enrico Ros 2d16e8bb4f UserFlags: show on messages 2024-03-16 20:44:54 -07:00
Enrico Ros bbd95eebff Update Models Attraction icon 2024-03-15 22:52:26 -07:00
Enrico Ros ceb00b4e93 Roll packages 2024-03-15 20:21:51 -07:00
Enrico Ros cc60d26d1c Turn multicast blue 2024-03-15 19:57:17 -07:00
Enrico Ros ba3ff739f6 Improve icons 2024-03-15 19:56:59 -07:00
Enrico Ros 6062647705 App: remove graying out - gets in the way a lot 2024-03-15 18:22:39 -07:00
Enrico Ros 070c1c2de9 Composer: tutorial happiness preserver 2024-03-15 18:16:21 -07:00
Enrico Ros d3aaa69409 Composer: tutorialize 2024-03-15 18:09:46 -07:00
Enrico Ros 0ac7753e35 Beam: terminate on Conversation clear 2024-03-15 17:49:56 -07:00
Enrico Ros eba9d53d2e Reduce the usage of backendCapabilities() 2024-03-15 17:35:38 -07:00
Enrico Ros d04d4ec8e7 Reorder providers 2024-03-15 16:34:42 -07:00
Enrico Ros c7c3efcbe7 Progress with bootstrap logic 2024-03-15 15:51:41 -07:00
Enrico Ros 2b8d53a44c Update wrappers 2024-03-15 15:41:08 -07:00
Enrico Ros ef6b573e08 Update TRPC Query Settings 2024-03-15 15:39:38 -07:00
Enrico Ros 61eedd41df Bootstrapper cleanup 2024-03-15 15:27:34 -07:00
Enrico Ros b265bcda20 Start cleaning up Bootstrapper 2024-03-15 14:32:15 -07:00
Enrico Ros d703d32a1f Cleanup knowledge of backend capabilities 2024-03-15 14:15:59 -07:00
Enrico Ros aab9334404 Build fix 2024-03-15 04:47:57 -07:00
Enrico Ros c2570f6955 New: attach starred messages with @
Note: the marshalling shall be moved inside the pipeline, probably
with a converter of type `ego-message-frontmatter` or similar
2024-03-15 04:42:16 -07:00
Enrico Ros 8e936a6334 Prevent this 2024-03-15 04:02:16 -07:00
Enrico Ros 46bfc22869 Show error on misused /beam 2024-03-15 02:48:45 -07:00
Enrico Ros db1620dd56 Actile: improve logic 2024-03-15 02:40:47 -07:00
Enrico Ros e59f8a42a3 Improve TRPC errors 2024-03-15 02:38:36 -07:00
Enrico Ros 17d18bd85d Fix /commands parsing 2024-03-15 02:37:30 -07:00
Enrico Ros fb256cf578 Bits 2024-03-15 01:39:51 -07:00
Enrico Ros 1b6b5db76d Actiles: improve provider search 2024-03-15 01:39:42 -07:00
Enrico Ros 41647ca83a Proactively get the user out of trouble. 2024-03-15 01:16:59 -07:00
Enrico Ros 07d2a17a87 Filter by starred chats. #109 2024-03-15 01:12:19 -07:00
Enrico Ros 6d744dfb7e ScrolltoBottomButton: improve 2024-03-15 00:40:01 -07:00
Enrico Ros b9b946c35f Messages: add 'starring' #109 2024-03-15 00:32:56 -07:00
Enrico Ros 17adfe2117 DMessage: add flag list support 2024-03-15 00:04:17 -07:00
Enrico Ros 1e5e21102d DMessage: improve edit support 2024-03-15 00:03:49 -07:00
Enrico Ros 4af992222f Shortcuts work 2024-03-14 21:47:59 -07:00
Enrico Ros a9447c6a11 Beam: misc 2024-03-14 21:33:42 -07:00
Enrico Ros db71323313 Misc 2024-03-14 21:19:02 -07:00
Enrico Ros b9b2748e05 Improved Avatar menu looks 2024-03-14 21:00:27 -07:00
Enrico Ros 387231f743 Fix Avatar menus 2024-03-14 20:52:34 -07:00
Enrico Ros 2216a89aa3 Beam: messaging 2024-03-14 19:10:15 -07:00
Enrico Ros 4faa6326fa Explainer: shortcuts 2024-03-14 19:08:11 -07:00
Enrico Ros cb22b3d9a1 Beam: update images 2024-03-14 19:07:34 -07:00
Enrico Ros 152a3873bd Beam: update scatter image 2024-03-14 18:50:25 -07:00
Enrico Ros adc2760a89 Beam: gather image 2024-03-14 18:24:36 -07:00
Enrico Ros dde64acb06 Improve Streaming issue reporting. Fixes #457 2024-03-14 17:47:55 -07:00
Enrico Ros 008adbd8bc Fix #459 2024-03-14 16:50:45 -07:00
Enrico Ros 0e4866a5a2 Beam: tutorial complete x2 2024-03-14 15:11:51 -07:00
Enrico Ros 5cb96cae3a Beam: tutorial complete 2024-03-14 15:05:36 -07:00
Enrico Ros 8cbb82a67f Beam: BEAM image, transparent 2024-03-14 14:57:56 -07:00
Enrico Ros 848ddbe477 Beam: BEAM image 2024-03-14 14:53:47 -07:00
Enrico Ros 083c1cde8b Explainer: adj auto resize 2024-03-14 14:53:47 -07:00
Enrico Ros b792971062 Add Gemini icon 2024-03-14 14:16:49 -07:00
Enrico Ros 07dde8f4b1 Chat messages: sticky headers 2024-03-14 04:19:57 -07:00
Enrico Ros 01f94127dd Beam: vendor icons 2024-03-14 04:07:50 -07:00
Enrico Ros 4d457b4e9e Beam: re-show explainer, with double-click 2024-03-14 03:28:27 -07:00
Enrico Ros 8ac93ff2da Beam: update explainer, with an end 2024-03-14 03:28:08 -07:00
Enrico Ros ef33a4b08e Beam: link 2024-03-14 02:47:27 -07:00
Enrico Ros fdd3b25a27 Beam: add Explainers 2024-03-14 02:37:01 -07:00
Enrico Ros 4dc979da08 SquircleIcon: support an alt color 2024-03-14 01:28:53 -07:00
Enrico Ros 8f426e03c4 Uniformize Roundicons 2024-03-14 01:28:40 -07:00
Enrico Ros 40cd085bf8 ExploreCarousel: the new Wizard experience 2024-03-14 01:28:20 -07:00
Enrico Ros 6aa75fc5d1 Animutils: amazing animations (not) 2024-03-14 01:27:55 -07:00
Enrico Ros eae5920f9d Beam: initial Explainer support 2024-03-13 21:59:55 -07:00
Enrico Ros 2f6bfa37cc Beam: balance title 2024-03-13 21:55:08 -07:00
Enrico Ros 9d6fd9b9b8 Styles fix 2024-03-13 20:14:58 -07:00
Enrico Ros 260cd67c96 Beam: user message editing 2024-03-13 17:34:07 -07:00
Enrico Ros aff76e2d18 Beam: improve grid 2024-03-13 17:21:50 -07:00
Enrico Ros 52e4343045 Improve drawer sizing 2024-03-13 17:15:50 -07:00
Enrico Ros 1ffbb135c6 Anthropic: add Haiku
(cherry picked from commit c3ec522261)
2024-03-13 14:46:39 -07:00
Enrico Ros c3ec522261 Anthropic: add Haiku 2024-03-13 14:45:50 -07:00
Enrico Ros 4538839376 Beam: unify invocation logic, from 7 places 2024-03-13 14:41:39 -07:00
Enrico Ros 834edd3a71 Beam: improve chat message popup 2024-03-13 14:22:06 -07:00
Enrico Ros 581c3d9593 Beam: document shortcut 2024-03-13 14:21:50 -07:00
Enrico Ros 0c672fbaa5 Beam: add disabled support for letters 2024-03-13 14:15:41 -07:00
Enrico Ros 6d96b9a312 Beam: add badges on menu and chat mode menu 2024-03-13 14:02:20 -07:00
Enrico Ros 691791ccd0 Beam: improve user message 2024-03-13 14:01:54 -07:00
Enrico Ros f4299121d5 Beam: highlight in modes menu 2024-03-13 13:43:48 -07:00
Enrico Ros 1adfb7eedd Chat drawer: setting to show persona icons 2024-03-13 13:36:55 -07:00
Enrico Ros 33ad583d15 New chat: better button spacings 2024-03-13 13:30:58 -07:00
Enrico Ros a7e2fe2277 New chat: better button 2024-03-13 13:16:35 -07:00
Enrico Ros 5a479d5863 DesktopDrawer: perfect shadows 2024-03-13 13:05:57 -07:00
Enrico Ros 873ff034d2 DesktopDrawer: fix shadow 2024-03-13 04:18:10 -07:00
Enrico Ros 61d3537617 Composer: fix zIndex 2024-03-13 04:11:09 -07:00
Enrico Ros ae068a3f64 Beam: shortcuts 2024-03-13 03:47:56 -07:00
Enrico Ros f7402cd6f5 Beam: close dialog after using selected 2024-03-13 03:47:48 -07:00
Enrico Ros c53f9c8020 Beam: use selected 2024-03-13 03:28:42 -07:00
Enrico Ros 798b4d57f4 Beam: disable on system message 2024-03-13 03:12:05 -07:00
Enrico Ros 98d428fb34 Beam: enable high performance mode 2024-03-13 02:32:42 -07:00
Enrico Ros 3ac5ace216 Share stream text indicator 2024-03-13 02:32:29 -07:00
Enrico Ros 444a1a7ab9 Temp download gif 2024-03-13 02:32:19 -07:00
Enrico Ros 43ea4bd4b5 Large cleanups in execution logic 2024-03-13 02:32:09 -07:00
Enrico Ros 6a9272e40a Beam: fix 2024-03-13 02:23:32 -07:00
Enrico Ros 10589a11aa Beam: business logic to continue/replace messages, including import 2024-03-13 00:15:41 -07:00
Enrico Ros a88f898bc0 Chat/Message/List: improve Beam and related restart logic 2024-03-12 22:55:40 -07:00
Enrico Ros 7a84038b04 Beam: initialize/terminate instead of open/close 2024-03-12 19:56:11 -07:00
Enrico Ros 111c40732d Beam: slight text changes 2024-03-12 18:58:35 -07:00
Enrico Ros 69bb78c8be Beam: reduce direct open calls 2024-03-12 18:54:45 -07:00
Enrico Ros ad3b327d69 Beam: close confirmation: add callbacks 2024-03-12 18:36:23 -07:00
Enrico Ros dc27f38534 Beam: close confirmation 2024-03-12 18:33:24 -07:00
Enrico Ros 5b0816cb92 Beam: esc to close 2024-03-12 18:18:46 -07:00
Enrico Ros 57f6955303 Beam: alt bar improvement 2024-03-12 18:03:31 -07:00
Enrico Ros 78915f878d Beam: clean gather pane 2024-03-12 18:01:29 -07:00
Enrico Ros 6ced6d626b Beam: improve integration 2024-03-12 18:01:17 -07:00
Enrico Ros ee3cb819b4 Beam: back to dev 2024-03-12 18:01:09 -07:00
Enrico Ros cc17b1d19d Beam: Chat Title bar to close the pane 2024-03-12 17:50:16 -07:00
Enrico Ros 2c83240d47 Snacks: review state 2024-03-12 16:48:03 -07:00
Enrico Ros 54f18ff120 Chat: focused state review 2024-03-12 16:47:50 -07:00
Enrico Ros 5e1fe363c3 PanesManager: cleanups (shall be safe) 2024-03-12 15:42:25 -07:00
Enrico Ros 3d2ec507e1 Chat: clarify state 2024-03-12 13:54:40 -07:00
Enrico Ros 1dd7af3c8b Beam: gather test icons 2024-03-12 13:41:50 -07:00
Enrico Ros 06ec1fcebf Beam: improve messaging 2024-03-12 13:08:08 -07:00
Enrico Ros 86cb863fd4 Beam: explored the modal 2024-03-12 13:07:57 -07:00
Enrico Ros d5ef1288d8 Beam: unify layout again 2024-03-12 12:45:04 -07:00
Enrico Ros f3354c498d Beam: unify layout again 2024-03-12 12:44:59 -07:00
Enrico Ros 9557141b38 Beam: bits (drag-drop didn't work out, it's a grid layout) 2024-03-12 12:36:32 -07:00
Enrico Ros 3144b66e73 StrictModeDroppable: share 2024-03-12 11:55:20 -07:00
Enrico Ros 6dbefa3d2f Beam: bits 2024-03-12 11:55:08 -07:00
Enrico Ros c8f3b139e8 Beam: bits 2024-03-12 11:06:52 -07:00
Enrico Ros 288663325d Beam: rename Panes 2024-03-12 11:05:16 -07:00
Enrico Ros 49947ee01d Beam: extract the Grid 2024-03-12 11:03:55 -07:00
Enrico Ros fa7a45ebc7 bits 2024-03-12 10:54:03 -07:00
Enrico Ros 9a074c222f Beam: adjustments 2024-03-12 10:51:18 -07:00
Enrico Ros 4e0d7b6ed9 Beam: down to non-removable 1 2024-03-12 10:41:16 -07:00
Enrico Ros 1f3defb04c Beam: optimize ControlsRow 2024-03-12 02:43:59 -07:00
Enrico Ros 6c52c43460 Beam: auto-hide composer 2024-03-12 02:38:18 -07:00
Enrico Ros deae2879f1 Beam: improve hooks 2024-03-12 01:59:42 -07:00
Enrico Ros 5b255a7d8b LLMSelect: try stabilize 2024-03-12 01:58:54 -07:00
Enrico Ros 6e06c24b7a Beam: extract hooks 2024-03-12 01:58:28 -07:00
Enrico Ros 2fde1efdd3 Beam: begin wiring the Gatherer 2024-03-11 23:59:04 -07:00
Enrico Ros aeb29d983a FormLabelStart: optimize 2024-03-11 23:27:58 -07:00
Enrico Ros c8a7123da9 Beam: fix styles 2024-03-11 23:13:20 -07:00
Enrico Ros 5c22061415 Beam: state cleanup and sync 2024-03-11 16:32:25 -07:00
Enrico Ros 9a0fda8c02 Beam: scrollable main layout 2024-03-11 16:00:22 -07:00
Enrico Ros 2f9a17c44a Beam: fixes 2024-03-11 15:46:29 -07:00
Enrico Ros 50559015d8 Beam: fix scattering (empty) issue 2024-03-11 15:40:20 -07:00
Enrico Ros a8d4e143c2 Beam: selection (disable, does not look great) 2024-03-11 15:32:54 -07:00
Enrico Ros 2a6c69538d Beam: increase ray state consistency 2024-03-11 15:09:43 -07:00
Enrico Ros 0ba5d61353 Beam: Ray lifecycle tracking 2024-03-11 14:44:36 -07:00
Enrico Ros d436ec5790 chat-stream: streamAssistantMessage: add an outcome type 2024-03-11 14:01:30 -07:00
Enrico Ros 759b822b92 Beam: relayout with Gather Controls skel 2024-03-11 13:49:19 -07:00
Enrico Ros 9df45af698 Beam: rename Scatter Controls 2024-03-11 13:34:57 -07:00
Enrico Ros 3474e81446 Beam: show preceding messages count 2024-03-11 13:26:06 -07:00
Enrico Ros e1f07eb957 ChatMessage: support top decorator (4rem default size) 2024-03-11 13:16:52 -07:00
Enrico Ros 71ff1b98be Beam: extract Ray controls row 2024-03-11 12:41:01 -07:00
Enrico Ros 9b370dfa88 Remove warnings 2024-03-11 12:40:40 -07:00
Enrico Ros 0be0661750 ButtonGroup background 2024-03-11 12:40:33 -07:00
Enrico Ros eaa7230af7 Improve Expand/Collapse (position, length) 2024-03-11 12:40:07 -07:00
Enrico Ros 11cb000481 Beam: fix stops and deletes 2024-03-11 01:54:39 -07:00
Enrico Ros 8ae3554a58 Beam: start/stop Rays 2024-03-11 00:25:50 -07:00
Enrico Ros dfd4736386 LLMSelect: support disablement 2024-03-11 00:25:39 -07:00
Enrico Ros feb793c9fa Beam: improve controller 2024-03-10 22:50:46 -07:00
Enrico Ros ee962fde08 GoodTooltip: fix 2024-03-10 22:12:12 -07:00
Enrico Ros c08dd96de3 Beam: add Stop buttons 2024-03-10 21:39:57 -07:00
Enrico Ros b52f771133 BlocksRenderer: improve expand buttons 2024-03-10 21:39:28 -07:00
Enrico Ros 4631232551 Animations: centralize 2024-03-10 21:39:02 -07:00
Enrico Ros df7f5047aa Beam: first Wiring 2024-03-10 20:58:37 -07:00
Enrico Ros 467d14324d zIndices: cleanup 2024-03-10 20:53:09 -07:00
Enrico Ros cbdce08e96 Beam: improve rays 2024-03-10 17:50:46 -07:00
Enrico Ros d6bf8f8854 Beam: rename View again 2024-03-10 17:25:51 -07:00
Enrico Ros 4599da3ded Revert "Beam: remove optionality"
This reverts commit 6d50952b2e.
2024-03-10 17:24:42 -07:00
Enrico Ros 6d50952b2e Beam: remove optionality 2024-03-10 17:23:00 -07:00
Enrico Ros 7066947809 Beam: move files 2024-03-10 17:15:01 -07:00
Enrico Ros e2924aacab Beam: cleanups 2024-03-10 16:49:36 -07:00
Enrico Ros 1e86d2503f Beam: merged -> gather 2024-03-10 16:33:24 -07:00
Enrico Ros eb67eee53a Beam: improve Debug methods 2024-03-10 16:33:15 -07:00
Enrico Ros dfdad45963 Beam: improve Debug info 2024-03-10 16:21:04 -07:00
Enrico Ros 4735508d87 Beam: cleanups 2024-03-10 16:17:43 -07:00
Enrico Ros c43c47eab8 Beam: standalone debug app 2024-03-10 16:12:48 -07:00
Enrico Ros fafb2dc6b9 Dev Apps 2024-03-10 16:12:20 -07:00
Enrico Ros 140e99c465 Beam: start from neg scale 2024-03-10 15:59:43 -07:00
Enrico Ros 7ba1974390 Beam: Encapsulate and move logic to BeamStore 2024-03-10 15:34:34 -07:00
Enrico Ros 51b8510f17 Misc 2024-03-10 15:05:01 -07:00
Enrico Ros 5d6949d471 Force the hard work 2024-03-10 14:54:11 -07:00
Enrico Ros 8e9d0c1fd1 The Beauty and the Beam 2024-03-10 14:01:39 -07:00
Enrico Ros 3852a3b779 User Text: Collapse as well as Expand 2024-03-10 13:47:21 -07:00
Enrico Ros 8b4ba96936 Beam: rays increase button 2024-03-09 18:06:06 -08:00
Enrico Ros 0c17e18491 Beam: Rays close to gen 2024-03-09 17:57:30 -08:00
Enrico Ros 2bdbab3afc Messages: controllable Avatar sightings and content scaling offset 2024-03-09 17:54:27 -08:00
Enrico Ros b97499a95e Beam: renames 2024-03-09 17:39:36 -08:00
Enrico Ros a70ac57872 Beam: stored Rays 2024-03-09 17:01:16 -08:00
Enrico Ros a9cf457024 Beam: dynamic Rays 2024-03-09 13:07:22 -08:00
Enrico Ros e5c938ac37 Beam: optimize Ray 2024-03-09 12:44:50 -08:00
Enrico Ros edad54efa2 Beam: optimize View 2024-03-09 12:44:35 -08:00
Enrico Ros f88426758f Beam: ensure component recreation 2024-03-09 12:44:09 -08:00
Enrico Ros 77a28eb810 Optimize LLMSelect 2024-03-09 12:43:56 -08:00
Enrico Ros f834b27562 Optimize FormLabelStart 2024-03-09 12:43:49 -08:00
Enrico Ros 984e257cc5 Move to a better (more reactive?) BeamStore 2024-03-09 11:24:28 -08:00
Enrico Ros 729e7612bc Improve LLMSelect (fix dependency) 2024-03-09 11:23:59 -08:00
Enrico Ros 59fadeae57 Improve LLMSelect 2024-03-09 11:20:18 -08:00
Enrico Ros bfbf7a298a Beam: actor -> ray 2024-03-09 00:32:32 -08:00
Enrico Ros aad5d3bd65 Beam: improve style 2024-03-09 00:07:28 -08:00
Enrico Ros 504f19c445 Beam: cleanups 2024-03-08 23:34:05 -08:00
Enrico Ros 19c47eb442 Beam: improve state 2024-03-08 23:11:13 -08:00
Enrico Ros ab6043df60 Beam: rename 2024-03-08 21:58:32 -08:00
Enrico Ros 3305549a0f Fix customEvent helpers 2024-03-08 21:57:59 -08:00
Enrico Ros c24c3cb571 Beam: misc highlights 2024-03-08 18:55:38 -08:00
Enrico Ros 952999258b Beam: header: improve looks 2024-03-08 18:50:01 -08:00
Enrico Ros 0713eaa52c Beam: extract header 2024-03-08 18:43:35 -08:00
Enrico Ros 8fee689f60 Beam: update layout 2024-03-08 18:05:13 -08:00
Enrico Ros 75ddb17fed Beam: begin UI 2024-03-08 18:04:59 -08:00
Enrico Ros 0c6a74626c Beam: update store 2024-03-08 18:04:39 -08:00
Enrico Ros 41e3d0eaf9 Use customEventHelpers for creating and subscribing to custom events 2024-03-08 15:40:41 -08:00
Enrico Ros 8b9cfebd42 Beam: misc 2024-03-08 14:21:48 -08:00
Enrico Ros 16badee259 Beam: renames 2024-03-08 14:21:36 -08:00
Enrico Ros 9d5171dd36 Panes: bits 2024-03-08 13:43:56 -08:00
Enrico Ros e0c0e81b7d Panes: improve branching behavior 2024-03-08 13:42:13 -08:00
Enrico Ros fd4e8985fc Beam: 1.15 2024-03-08 12:16:58 -08:00
Enrico Ros 1d9b8503c0 Roll packages 2024-03-08 12:16:12 -08:00
Enrico Ros b3ef7b914d Beam: enable dev setting 2024-03-08 11:59:12 -08:00
Enrico Ros 2f59e12e20 Remove log 2024-03-06 22:20:40 -08:00
Enrico Ros 30e8652c2a 1.14.1: Release for Claude-3 2024-03-06 22:10:41 -08:00
Enrico Ros 5ee6aceb60 cleanups 2024-03-06 21:51:15 -08:00
Enrico Ros 6940b6a6d1 Anthropic: Full support for Claude-3 models. Closes #443, #450
Thanks to @slapglif in #450 for a reference implementation.
2024-03-06 21:50:24 -08:00
Enrico Ros 4e33ce9415 misc 2024-03-06 20:56:32 -08:00
Enrico Ros 944e22bde6 Anthropic: if there's a single system message, treat it as-if it was a user message 2024-03-06 20:49:59 -08:00
Enrico Ros 6054fa0a26 Anthropic: use the new Messages format (thanks @slapglif #450) 2024-03-06 20:42:33 -08:00
Enrico Ros 4db13cfed4 Anthropic: wire types (fully switch to the new Messages API) 2024-03-06 20:33:59 -08:00
Enrico Ros 6a6adda2e0 misc 2024-03-06 20:33:12 -08:00
Enrico Ros 4afa55c0db Anthropic: update models 2024-03-06 18:36:07 -08:00
234 changed files with 8272 additions and 2642 deletions
+19 -8
View File
@@ -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. 🤖
[![Official Website](https://img.shields.io/badge/BIG--AGI.com-%23096bde?style=for-the-badge&logo=vercel&label=launch)](https://big-agi.com)
@@ -17,15 +19,24 @@ 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.14.0 · March 6, 2024 · Modelmorphic
### 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
- [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
@@ -83,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
| ![Advanced AI](https://img.shields.io/badge/Advanced%20AI-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![100+ AI Models](https://img.shields.io/badge/100%2B%20AI%20Models-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![Flow-state UX](https://img.shields.io/badge/Flow--state%20UX-32383e?style=for-the-badge&logo=flow&logoColor=white) | ![Privacy First](https://img.shields.io/badge/Privacy%20First-32383e?style=for-the-badge&logo=privacy&logoColor=white) | ![Advanced Tools](https://img.shields.io/badge/Fun%20To%20Use-f22a85?style=for-the-badge&logo=tools&logoColor=white) |
|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| **Chat**<br/>**Call** 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 |
![big-AGI screenshot](docs/pixels/big-AGI-compo-20240201_small.png)
+4
View File
@@ -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
View File
@@ -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
+13 -5
View File
@@ -5,15 +5,23 @@ by release.
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
### 1.15.0 - Mar 2024
### 1.16.0 - Mar 2024
Prediction: OpenAI will release GPT-5 on March 14, 2024. We will support it on day 1.
- milestone: [1.15.0](https://github.com/enricoros/big-agi/milestone/15)
- 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.14.0 · March 6, 2024 · Modelmorphic
### 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
+22 -3
View File
@@ -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.
@@ -53,10 +72,10 @@ We introduced the `/info/debug` page that provides a detailed overview of the ap
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/>![CoolAGI Logo](https://github.com/nextgen-user/freegpt4plus/assets/150797204/9b0e1232-4791-4d61-b949-16f9eb284c22) | 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.
+1 -1
View File
@@ -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.
+487 -460
View File
File diff suppressed because it is too large Load Diff
+15 -15
View File
@@ -1,6 +1,6 @@
{
"name": "big-agi",
"version": "1.14.0",
"version": "1.15.0",
"private": true,
"author": "Enrico Ros <enrico.ros@gmail.com>",
"repository": "https://github.com/enricoros/big-agi",
@@ -22,11 +22,11 @@
"@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.2",
"@next/third-parties": "^14.1.2",
"@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.2",
"next": "^14.1.4",
"nprogress": "^0.2.0",
"pdfjs-dist": "4.0.379",
"plantuml-encoder": "^1.4.0",
@@ -51,7 +51,7 @@
"react-katex": "^3.0.1",
"react-markdown": "^9.0.1",
"react-player": "^2.15.1",
"react-resizable-panels": "^2.0.12",
"react-resizable-panels": "^2.0.13",
"react-timeago": "^7.2.0",
"remark-gfm": "^4.0.0",
"sharp": "^0.33.2",
@@ -63,23 +63,23 @@
"zustand": "^4.5.2"
},
"devDependencies": {
"@cloudflare/puppeteer": "^0.0.5",
"@types/node": "^20.11.24",
"@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.62",
"@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.2",
"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
View File
@@ -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
View File
@@ -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 */}
+10
View File
@@ -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 />);
}
+3 -3
View File
@@ -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: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

+17 -15
View File
@@ -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}
+100
View File
@@ -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>
);
}
+8 -46
View File
@@ -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>
+2 -24
View File
@@ -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>
+6 -19
View File
@@ -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) =>
+2 -13
View File
@@ -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
View File
@@ -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)}
/>
)}
+4 -4
View File
@@ -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,
}] : [],
+2 -2
View File
@@ -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,
}],
};
+27 -13
View File
@@ -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,
}];
}
+113
View File
@@ -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>
);
}
@@ -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,
}) {
@@ -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>
);
}
+134 -82
View File
@@ -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 />
+19 -14
View File
@@ -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>
+51 -19
View File
@@ -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}
+2 -2
View File
@@ -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
-137
View File
@@ -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>)}
+93 -41
View File
@@ -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>;
};
+199 -121
View File
@@ -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>
@@ -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;
},
);
}
+7 -4
View File
@@ -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);
+26 -18
View File
@@ -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;
}
+12 -11
View File
@@ -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);
}
}
+9 -4
View File
@@ -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
+25 -4
View File
@@ -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] =>
+1 -1
View File
@@ -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.' />
);
}
+3 -1
View File
@@ -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>
+4 -4
View File
@@ -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 */}
+8 -9
View File
@@ -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',
}}
+3 -3
View File
@@ -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 -12
View File
@@ -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}
/>,
)}
+16 -23
View File
@@ -1,5 +1,4 @@
import * as React from 'react';
import { keyframes } from '@emotion/react';
import NextImage from 'next/image';
import TimeAgo from 'react-timeago';
@@ -10,31 +9,17 @@ 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 } from './news.data';
import { beamNewsCallout } from './beam.data';
// number of news items to show by default, before the expander
const DEFAULT_NEWS_COUNT = 3;
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) */
}`;
const DEFAULT_NEWS_COUNT = 4;
// callout, for special occasions
export const newsRoadmapCallout =
<Card variant='solid' invertedColors>
<CardContent sx={{ gap: 2 }}>
@@ -90,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'>
@@ -123,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 }}>
@@ -132,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}
@@ -148,7 +140,7 @@ export function AppNews() {
{!!ni.items && (ni.items.length > 0) && (
<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 ? '" "' : '"- "', marginLeft: item.icon ? '-1.125rem' : undefined }}>
<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}
@@ -184,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>
@@ -191,7 +184,7 @@ export function AppNews() {
</Card>
{/* Inject the roadmap item here*/}
{idx === 0 && (
{idx === 3 && (
<Box sx={{ mb: 3 }}>
{newsRoadmapCallout}
</Box>
+43
View File
@@ -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>;
+26 -5
View File
@@ -7,6 +7,8 @@ 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';
@@ -18,13 +20,16 @@ import { clientUtmSource } from '~/common/util/pwaUtils';
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
// Images
// 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';
import { beamBlogUrl, beamReleaseDate } from './beam.data';
interface NewsItem {
@@ -39,26 +44,42 @@ interface NewsItem {
dev?: boolean;
issue?: number;
icon?: React.FC<SvgIconProps>;
noBullet?: boolean;
}[];
}
// news and feature surfaces
export const NewsItems: NewsItem[] = [
/*{
versionCode: '1.15.0',
versionCode: '1.16.0',
items: [
Best-Of
Draw
...
Screen Capture (when removed from labs)
]
}*/
{
versionCode: '1.14.0',
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-06T08:00:00Z'),
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 },
+1 -1
View File
@@ -4,7 +4,7 @@
import { useAppStateStore } from '~/common/state/store-appstate';
export const incrementalNewsVersion: number = 14;
export const incrementalNewsVersion: number = 15;
export function shallRedirectToNews() {
+18 -5
View File
@@ -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 }}>
+1 -1
View File
@@ -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' }}>
+12 -6
View File
@@ -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' }}>
+2 -1
View File
@@ -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** | |
+8 -8
View File
@@ -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,28 +24,28 @@ 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={<><ChatBeamIcon color={labsBeam ? 'primary' : undefined} sx={{ mr: 0.25 }} />Chat Beam</>} description={'v1.15 · ' + (labsBeam ? 'Active' : 'Off')}
checked={labsBeam} onChange={setLabsBeam}
/>
<FormSwitchControl
title={<><SpeedIcon color={labsHighPerformance ? 'primary' : undefined} sx={{ mr: 0.25 }} />Performance</>} description={'v1.14 · ' + (labsHighPerformance ? 'Unlocked' : 'Default')}
checked={labsHighPerformance} onChange={setLabsHighPerformance}
/>
<FormSwitchControl
{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')}
+20 -4
View File
@@ -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,
},
{
+9 -14
View File
@@ -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
View File
@@ -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';
-99
View File
@@ -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;
}
+72 -40
View File
@@ -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);
},
};
}*/
}
+37
View File
@@ -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);
},
};
}*/
}
+9 -9
View File
@@ -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;
+19 -7
View File
@@ -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>
+14 -11
View File
@@ -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>
}
/>
);
+246
View File
@@ -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 -11
View File
@@ -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,
+3 -2
View File
@@ -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' },
+6 -4
View File
@@ -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}
+4 -2
View File
@@ -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;
}
+30 -29
View File
@@ -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);
+112 -83
View File
@@ -1,7 +1,8 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { Box, FormControl, ListDivider, ListItemDecorator, Option, Select } from '@mui/joy';
import type { SxProps } from '@mui/joy/styles/types';
import { FormControl, ListDivider, ListItemDecorator, Option, Select, SvgIconProps } from '@mui/joy';
import { DLLM, DLLMId, useModelsStore } from '~/modules/llms/store-llms';
import { findVendorById } from '~/modules/llms/vendors/vendors.registry';
@@ -10,110 +11,138 @@ import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
import { IModelVendor } from '~/modules/llms/vendors/IModelVendor';
/*export function useLLMSelectGlobalState(): [DLLMId | null, (llmId: DLLMId | null) => void] {
return useModelsStore(state => [state.chatLLMId, state.setChatLLMId], shallow);
}*/
export function useLLMSelectLocalState(initFromGlobal: boolean): [DLLMId | null, (llmId: DLLMId | null) => void] {
return React.useState<DLLMId | null>(initFromGlobal ? () => {
return useModelsStore.getState().chatLLMId;
} : null);
}
const llmSelectSx: SxProps = {
flex: 1,
backgroundColor: 'background.popup',
// minWidth: '200',
};
/**
* Select the Model, synced with either Global (Chat) LLM state, or local
*
* @param localState if true, the state is local to the hook, otherwise the global chat model is changed
* @param chatLLMId (required) the LLM id
* @param setChatLLMId (required) the function to set the LLM id
* @param label label of the select, use '' to hide it
* @param smaller if true, the select is smaller
* @param disabled
* @param placeholder placeholder of the select
* @param isHorizontal if true, the select is horizontal (label - select)
*/
export function useLLMSelect(localState: boolean = true, label: string = 'Model', placeholder: string = 'Models …'): [DLLM | null, React.JSX.Element | null] {
// state
const localSwitch = React.useRef(localState);
export function useLLMSelect(
chatLLMId: DLLMId | null,
setChatLLMId: (llmId: DLLMId | null) => void,
label: string = 'Model',
smaller: boolean = false,
disabled: boolean = false,
placeholder: string = 'Models …',
isHorizontal: boolean = false,
): [DLLM | null, React.JSX.Element | null, React.FunctionComponent<SvgIconProps> | undefined] {
// external state
const { llms, globalChatLLMId, globalSetChatLLMId } = useModelsStore(state => ({
llms: state.llms,
globalChatLLMId: state.chatLLMId,
globalSetChatLLMId: state.setChatLLMId,
}), shallow);
// local state initially synced to the global state (may be used or not)
const [localLLMId, setLocalLLMId] = React.useState<DLLMId | null>(globalChatLLMId);
// global/local (stable) switch - do not change at runtime
const chatLLMId = localSwitch.current ? localLLMId : globalChatLLMId;
const setChatLLMId = localSwitch.current ? setLocalLLMId : globalSetChatLLMId;
const _filteredLLMs = useModelsStore(state => {
return state.llms.filter(llm => !llm.hidden || (chatLLMId && llm.id === chatLLMId));
}, shallow);
// derived state
const chatLLM = chatLLMId ? llms.find(llm => llm.id === chatLLMId) ?? null : null;
const noIcons = false; //smaller;
const chatLLM = chatLLMId
? _filteredLLMs.find(llm => llm.id === chatLLMId) ?? null
: null;
const component = React.useMemo(() => {
// hide invisible models, except the current model
const filteredLLMs = llms.filter(llm => !llm.hidden || llm.id === chatLLMId);
// Memo the LLM Options for the Select
const componentOptions = React.useMemo(() => {
// create the option items
let formerVendor: IModelVendor | null = null;
const options = filteredLLMs.map((llm) => {
return _filteredLLMs.reduce((acc, llm, _index) => {
const vendor = findVendorById(llm._source?.vId);
const vendorChanged = vendor !== formerVendor;
const addSeparator = vendorChanged && formerVendor !== null;
if (vendorChanged)
formerVendor = vendor;
return (
<React.Fragment key={'llm-' + llm.id}>
{addSeparator && <ListDivider />}
<Option
value={llm.id}
sx={llm.id === chatLLMId ? { fontWeight: 'md' } : undefined}
>
{!!vendor?.Icon && (
<ListItemDecorator>
<vendor.Icon />
</ListItemDecorator>
)}
{/*<Tooltip title={llm.description}>*/}
{llm.label}
{/*</Tooltip>*/}
{/*{llm.gen === 'sdxl' && <Chip size='sm' variant='outlined'>XL</Chip>} {llm.label}*/}
</Option>
</React.Fragment>
// add separators if the vendor changed (and more than one vendor)
const addSeparator = vendorChanged && formerVendor !== null;
if (addSeparator)
acc.push(<ListDivider key={'llm-sep-' + llm.id}>{vendor?.name}</ListDivider>);
// the option component
acc.push(
<Option
key={'llm-' + llm.id}
value={llm.id}
// Disabled to avoid regenerating the memo too frequently
// sx={llm.id === chatLLMId ? { fontWeight: 'md' } : undefined}
>
{(!noIcons && !!vendor?.Icon) && (
<ListItemDecorator>
<vendor.Icon />
</ListItemDecorator>
)}
{/*<Tooltip title={llm.description}>*/}
{llm.label}
{/*</Tooltip>*/}
{/*{llm.gen === 'sdxl' && <Chip size='sm' variant='outlined'>XL</Chip>} {llm.label}*/}
</Option>,
);
});
// create the component
return (
<FormControl>
{!!label && <FormLabelStart title={label} />}
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Select
variant='outlined'
value={chatLLMId}
onChange={(_event, value) => value && setChatLLMId(value)}
placeholder={placeholder}
slotProps={{
listbox: {
sx: {
// larger list
'--ListItem-paddingLeft': '1rem',
'--ListItem-minHeight': '2.5rem',
// minWidth: '100%',
},
},
button: {
sx: {
// show the full name on the button
whiteSpace: 'inherit',
},
},
}}
sx={{
flex: 1,
// minWidth: '200',
}}
>
{options}
</Select>
</Box>
</FormControl>
);
}, [chatLLMId, label, llms, placeholder, setChatLLMId]);
return acc;
}, [] as React.JSX.Element[]);
}, [_filteredLLMs, noIcons]);
return [chatLLM, component];
const onSelectChange = React.useCallback((_event: unknown, value: DLLMId | null) => value && setChatLLMId(value), [setChatLLMId]);
// Memo the Select component
const llmSelectComponent = React.useMemo(() => (
<FormControl orientation={isHorizontal ? 'horizontal' : undefined}>
{!!label && <FormLabelStart title={label} sx={/*{ mb: '0.25rem' }*/ undefined} />}
{/*<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>*/}
<Select
variant='outlined'
value={chatLLMId}
size={smaller ? 'sm' : undefined}
disabled={disabled}
onChange={onSelectChange}
placeholder={placeholder}
slotProps={{
listbox: {
sx: {
// larger list
'--ListItem-paddingLeft': '1rem',
'--ListItem-minHeight': '2.5rem',
// minWidth: '100%',
},
},
button: {
sx: {
// show the full name on the button
whiteSpace: 'inherit',
},
},
}}
sx={llmSelectSx}
>
{componentOptions}
</Select>
{/*</Box>*/}
</FormControl>
), [chatLLMId, componentOptions, disabled, isHorizontal, label, onSelectChange, placeholder, smaller]);
// Memo the vendor icon for the chat LLM
const chatLLMVendorIconFC = React.useMemo(() => {
return findVendorById(chatLLM?._source?.vId)?.Icon;
}, [chatLLM]);
return [chatLLM, llmSelectComponent, chatLLMVendorIconFC];
}
+2 -4
View File
@@ -1,10 +1,8 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { SvgIcon } from '@mui/joy';
import { SvgIcon, SvgIconProps } from '@mui/joy';
export function CodePenIcon(props: { sx?: SxProps }) {
export function CodePenIcon(props: SvgIconProps) {
return <SvgIcon viewBox='0 0 100 100' width='100' height='100' stroke='none' fill='currentColor' {...props}>
<path d='M100 34.2c-.4-2.6-3.3-4-5.3-5.3-3.6-2.4-7.1-4.7-10.7-7.1-8.5-5.7-17.1-11.4-25.6-17.1-2-1.3-4-2.7-6-4-1.4-1-3.3-1-4.8 0-5.7 3.8-11.5 7.7-17.2 11.5L5.2 29C3 30.4.1 31.8 0 34.8c-.1 3.3 0 6.7 0 10v16c0 2.9-.6 6.3 2.1 8.1 6.4 4.4 12.9 8.6 19.4 12.9 8 5.3 16 10.7 24 16 2.2 1.5 4.4 3.1 7.1 1.3 2.3-1.5 4.5-3 6.8-4.5 8.9-5.9 17.8-11.9 26.7-17.8l9.9-6.6c.6-.4 1.3-.8 1.9-1.3 1.4-1 2-2.4 2-4.1V37.3c.1-1.1.2-2.1.1-3.1 0-.1 0 .2 0 0zM54.3 12.3 88 34.8 73 44.9 54.3 32.4V12.3zm-8.6 0v20L27.1 44.8 12 34.8l33.7-22.5zM8.6 42.8 19.3 50 8.6 57.2V42.8zm37.1 44.9L12 65.2l15-10.1 18.6 12.5v20.1zM50 60.2 34.8 50 50 39.8 65.2 50 50 60.2zm4.3 27.5v-20l18.6-12.5 15 10.1-33.6 22.4zm37.1-30.5L80.7 50l10.8-7.2-.1 14.4z' />
<path d='M75 0L100 0L100 25Z' />

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