Compare commits

...

191 Commits

Author SHA1 Message Date
Enrico Ros 677facb867 Merge branch 'release-1.9.0' 2023-12-28 14:49:58 -08:00
Enrico Ros 494086765b 1.9.0: README and Changelog 2023-12-28 14:47:04 -08:00
Enrico Ros 59ca03e17d Release: update template 2023-12-28 14:32:19 -08:00
Enrico Ros e0e56d70c9 1.9.0: News 2023-12-28 14:29:11 -08:00
Enrico Ros b408267e6e DALL·E: reorder options 2023-12-28 14:09:17 -08:00
Enrico Ros 6385d7aa84 DALL·E: raw prompting for DALL·E 3 as well 2023-12-28 14:04:14 -08:00
Enrico Ros fa811c951c 1.9.0: Version 2023-12-28 13:26:40 -08:00
Enrico Ros 7085c3a7aa DALL·E: temporary image notice 2023-12-28 13:22:53 -08:00
Enrico Ros 2333318cb4 Release: update template 2023-12-28 13:00:54 -08:00
Enrico Ros 3aebcb360c Release: update template 2023-12-28 12:56:59 -08:00
Enrico Ros bf60d699e3 Release: update template 2023-12-28 12:51:19 -08:00
Enrico Ros d775d47623 New UI - Part 1 - Details inside:
- Optima Layout: new Context based pluggable layout system
   - Now children have context functions, for better behaviors
   - Removed `store-applayout`
   - using withLayout on top-level Pages
 - ScrollToBottom: grounds-up subsystem for smooth scrolling with snap-to-bottom
 - Panes subsystem: use react-resizeable-panels together with our Panes subsystem
   - New: Split window chats, Drag to close windows, Button to split
   - using: https://github.com/bvaughn/react-resizable-panels
 - Cosmetic: Colors: update Light and Dark themes
 - Bootstrap Logic provider: will enable Mobile use cases
 - Removed NoSSR (the backend provided natually acts as the same)
 - Next load progress: loading indicator for slower pages (>300ms)
 - withLayout() system

Additional benefits include: no-pluggable-flashing, pane-ready,
fixed X-scrolling on Firefox, and more.

Closes #308, #304, #255, #59.
Progress on #305, #201, #296, #233, #208, #203.
2023-12-28 02:16:55 -08:00
Enrico Ros 2eb3397394 Scroll-To-Bottom: complete Framework. Fixes #304, Fixes #60, Fixes #59 2023-12-28 02:13:14 -08:00
Enrico Ros e27c35373d Update year, almost there 2023-12-28 02:02:11 -08:00
Enrico Ros 5e1966af5f Scroller: begins to work well 2023-12-28 00:36:41 -08:00
Enrico Ros 7cbcf01ca9 Scroller: vastly improve the framework 2023-12-27 23:41:50 -08:00
Enrico Ros 6898fa6cc1 Scroller: framework (incomplete), Fixes #59 2023-12-27 20:08:28 -08:00
Enrico Ros 1e796299a2 Scroller: straighten messages (remove bottoms) 2023-12-27 19:50:07 -08:00
Enrico Ros 7026024da5 Scroller: straighten messages 2023-12-27 18:16:59 -08:00
Enrico Ros 3ed52fa92f Panes: move 2023-12-27 16:55:22 -08:00
Enrico Ros a3e04f5973 Panes: duplicate current 2023-12-27 16:16:27 -08:00
Enrico Ros 8bf90e3622 Use react-resizable-panels instead of the flexbox
Also Fix #255 due to the large layout restructuring.
2023-12-27 05:21:07 -08:00
Enrico Ros cdc2de5018 Composer: fix attachments layout 2023-12-27 02:29:53 -08:00
Enrico Ros b26370a85a OptimaLayout: begin 2023-12-27 00:52:49 -08:00
Enrico Ros adf0197a9e Disable debug 2023-12-26 22:29:21 -08:00
Enrico Ros c00c41a160 AppBar: begin cleanup 2023-12-26 22:28:43 -08:00
Enrico Ros 09c74e6cf4 OptimaLayout: migrate to Context for better React usage 2023-12-26 22:23:50 -08:00
Enrico Ros 304e66b098 Routing shall be homogeneous now 2023-12-26 20:04:12 -08:00
Enrico Ros 64b6b08652 Routing bits 2023-12-26 19:59:30 -08:00
Enrico Ros cbea304a97 Improve routing 2023-12-26 19:58:01 -08:00
Enrico Ros c3e73fa9c8 BootstrapProvider: check for mobile 2023-12-26 19:46:12 -08:00
Enrico Ros 4c978020d9 Add the Bootstrap Logic provider 2023-12-26 19:39:49 -08:00
Enrico Ros 481b85bdad Providers into a dedicated folder 2023-12-26 19:32:20 -08:00
Enrico Ros b80fd0494a Move this (unused) utility 2023-12-26 19:31:22 -08:00
Enrico Ros c7dea43d1a Move providers 2023-12-26 19:30:51 -08:00
Enrico Ros 726053ffcd GoodDropdown shared 2023-12-26 13:01:17 -08:00
Enrico Ros ee4e2c265b Next Router Loading Progress 2023-12-26 12:55:43 -08:00
Enrico Ros a5332d2c82 Deflate bundle by reverting to per-page Layouts (keep the typings at least) 2023-12-26 01:11:21 -08:00
Enrico Ros 2f45ce48fa Fix 2023-12-26 00:43:15 -08:00
Enrico Ros 104922dc20 Dynamic layouting 2023-12-26 00:36:18 -08:00
Enrico Ros d68ccd9dfb Optimize 2023-12-25 23:57:59 -08:00
Enrico Ros 676bcadd17 Remove NoSSR: the Backend provider does the same and doesn't seem to flash the screen that much 2023-12-25 22:36:59 -08:00
Enrico Ros c08e83c618 More uniform App backgrounds 2023-12-25 22:16:06 -08:00
Enrico Ros 7a69b32506 Theme: update background shades for Light and Dark 2023-12-25 22:12:50 -08:00
Enrico Ros a9e1a968e8 DallE3: support multiple parallel image request for count>1 2023-12-23 03:36:06 -08:00
Enrico Ros dc30a7a55a Pixels 2023-12-23 03:08:43 -08:00
Enrico Ros f570627b09 Settings: light outline 2023-12-23 02:44:46 -08:00
Enrico Ros e601302db8 Dall-E: show pricing when changing settings 2023-12-23 02:35:46 -08:00
Enrico Ros f9e207ff7c RenderImage: larger tooltip 2023-12-23 02:07:33 -08:00
Enrico Ros 8100c5cfd1 Merge branch 'feature-dalle'
Fixes #212
2023-12-23 02:00:02 -08:00
Enrico Ros 0b0c3891bb Bits 2023-12-23 01:58:58 -08:00
Enrico Ros b4cdd5546d T2I: Final Naming and Cleanups. Closes #212 2023-12-23 01:55:32 -08:00
Enrico Ros 8444b32db2 T2I: tti -> t2i 2023-12-23 01:45:52 -08:00
Enrico Ros 69098273bf TTI: return Markdown Image References.
Will be rendered neatly with or without markdown on.
2023-12-23 01:20:04 -08:00
Enrico Ros 5cd5702b83 Dalle: improve configuration 2023-12-23 01:11:42 -08:00
Enrico Ros 605d288da6 Dalle: improve typedefs 2023-12-23 01:10:24 -08:00
Enrico Ros 499840cae3 Parse the new markdown image blocks 2023-12-23 01:09:54 -08:00
Enrico Ros 4529fc325b RenderImage: vastly improve the Image Block, incl. the ALT Text 2023-12-23 01:09:35 -08:00
Enrico Ros 4769e9b900 T2I: move store 2023-12-22 23:22:09 -08:00
Enrico Ros 64d13a0d52 T2I: remove auto-set from OpenAI setup 2023-12-22 23:15:57 -08:00
Enrico Ros 7df1517b23 T2I: Settings (choose active) 2023-12-22 23:15:21 -08:00
Enrico Ros 56c372455d T2I: fix OpenAI DallE path 2023-12-22 23:13:13 -08:00
Enrico Ros 2e649ea12b Image Block: add Dalle 2023-12-22 23:12:03 -08:00
Enrico Ros 2a67315504 FormRadioControl: improve mobile, support undefined 2023-12-22 22:20:02 -08:00
Enrico Ros b53ceb70c4 T2I: improvements 2023-12-22 19:10:20 -08:00
Enrico Ros 3c9d06aac7 T2I: misc 2023-12-22 19:00:49 -08:00
Enrico Ros 77e7c1d467 T2I: move methods around 2023-12-22 19:00:35 -08:00
Enrico Ros eb38e119b8 misc: rename file 2023-12-22 18:48:00 -08:00
Enrico Ros 06402cc5c1 T2I: capability checks 2023-12-22 18:40:26 -08:00
Enrico Ros ddf631cdfc T2I: integrate with OpenAI Access credentials 2023-12-22 18:31:46 -08:00
Enrico Ros f7e89ae65c bits 2023-12-22 18:31:19 -08:00
Enrico Ros 07e1e1c580 T2I: client (capabilities, immediate generation) 2023-12-22 18:30:51 -08:00
Enrico Ros f6eb2aecee T2I: capabilities update 2023-12-22 18:29:38 -08:00
Enrico Ros f416b1df97 T2I: openAI generation 2023-12-22 18:29:17 -08:00
Enrico Ros 29d17795b8 T2I: cmd change 2023-12-22 18:28:30 -08:00
Enrico Ros 3b30f649c6 T2I: move Prodia in the Text2Image module 2023-12-22 18:27:33 -08:00
Enrico Ros ba9a9714a7 OpenAI: router: generate images 2023-12-22 16:34:06 -08:00
Enrico Ros c304ab5f3b Llms: Cleanup some type definitions 2023-12-22 15:43:49 -08:00
Enrico Ros cd4d5042e9 Roll packages 2023-12-22 15:22:01 -08:00
Enrico Ros 6c4d177bfc Metadata: update 2023-12-22 06:06:55 -08:00
Enrico Ros 5d1620b5c1 OpenRouter: limit free model calls to 1/5s. Closes #291 2023-12-22 03:42:42 -08:00
Enrico Ros bd78808950 Implement Rate limiting framework 2023-12-22 03:40:48 -08:00
Enrico Ros 6aee6aeac1 DLLM: add a 'Free' attribute (only on OpenRouter for now)
Shall have this on Local models as well?
2023-12-22 03:23:05 -08:00
Enrico Ros 5ae970a526 LLM Options: display Free models 2023-12-22 03:22:26 -08:00
Enrico Ros 87718d73d2 Models Loading progress 2023-12-22 03:04:42 -08:00
Enrico Ros 7c8498573e LLMOptions: improve display (add tooltips and advanced) 2023-12-22 02:58:01 -08:00
Enrico Ros f6e82d0c0c OpenRouter: drop the hardcoded list 2023-12-22 02:57:46 -08:00
Enrico Ros f7f827660d Merge pull request #290 from joriskalz/fix-reset-values-when-switching-mode
[BUG] Reset values when switching between text and youtube mode
2023-12-21 14:31:14 -08:00
Enrico Ros 664b221e67 Imagine: unified pipeline. Adds to #289 2023-12-21 14:29:11 -08:00
Enrico Ros f184a4bf97 Imagine: remove former 'mode' 2023-12-21 14:21:41 -08:00
Joris Kalz e442816c15 fix to reset state when switching between modes. 2023-12-21 18:37:31 +01:00
Enrico Ros aaa3b65cd8 Merge branch 'joriskalz-Persona-From-Text'. Fixes #282 2023-12-21 04:31:58 -08:00
Enrico Ros c6441662b0 Persona Creator: on by default, can be hidden like other tiles 2023-12-21 04:30:29 -08:00
Enrico Ros b902a7bce8 Persona Creator: consistent naming 2023-12-21 04:30:01 -08:00
Enrico Ros 87a916ba09 Persona Creator: remove the 'Labs' flag 2023-12-21 04:29:31 -08:00
Enrico Ros 35a85ed2fa Persona Creator: final fix I swear 2023-12-21 03:32:01 -08:00
Enrico Ros 75d56bfb56 Persona Creator: change the 'copy' location and improve paddings 2023-12-21 03:15:03 -08:00
Enrico Ros d0a125fad5 Persona Creator: rename model selector label 2023-12-21 02:51:53 -08:00
Enrico Ros 2af8437f6d Persona Creator: reorder blocks, and show the LLM name 2023-12-21 02:49:30 -08:00
Enrico Ros 0c3e65575c Persona Creator: remove YT -> renamed to PersonaCreator.tsx 2023-12-21 02:12:25 -08:00
Enrico Ros 1c15057fca Persona Creator: style: update TextArea and margins 2023-12-21 02:11:33 -08:00
Joris Kalz 44da928489 Optimzed text to cover both use cases 2023-12-20 21:54:46 +01:00
Joris Kalz 85027d3e3a Added Persona from Text 2023-12-20 21:46:19 +01:00
Enrico Ros 0fc83cf6f5 Merge branch 'release-1.8.0' 2023-12-20 02:38:51 -08:00
Enrico Ros 2949feccd5 Maintainers Release 2023-12-20 02:32:47 -08:00
Enrico Ros d6f1c2da81 1.8.0: Readme and Changelog 2023-12-20 02:11:13 -08:00
Enrico Ros fabb433fde 1.8.0: news.data.tsx 2023-12-20 01:54:23 -08:00
Enrico Ros b57445eb14 1.8.0: Version 2023-12-20 01:11:08 -08:00
Enrico Ros 5f8f4aba78 Ollama: update models 2023-12-20 00:59:14 -08:00
Enrico Ros d693cdaeba Ollama: update admin panel 2023-12-20 00:59:03 -08:00
Enrico Ros 39fbcfd97b OpenRouter: update models 2023-12-20 00:55:27 -08:00
Enrico Ros 7694bc3d52 OpenRouter: update models 2023-12-20 00:53:16 -08:00
Enrico Ros 7f21b2ac3d Merge branch 'feature-gemini'
Fixes #275
2023-12-20 00:16:44 -08:00
Enrico Ros fdb66da1a7 Gemini: choose a content filtering threshold 2023-12-20 00:14:53 -08:00
Enrico Ros 6b62a6733b Gemini: show block reason 2023-12-20 00:14:53 -08:00
Enrico Ros 5d62056807 Streaming: muxing format 2023-12-20 00:14:53 -08:00
Enrico Ros efff7126af Gemini: final touches 2023-12-20 00:14:53 -08:00
Enrico Ros 45046c70ed Gemini: stream on 2023-12-20 00:14:53 -08:00
Enrico Ros 7b5b852793 Gemini: trim key 2023-12-20 00:14:53 -08:00
Enrico Ros 9952b757b8 Gemini: client version 2023-12-20 00:14:53 -08:00
Enrico Ros b08ecc9012 Models Modal: improve caps 2023-12-20 00:14:53 -08:00
Enrico Ros bc5a38fa89 Models List: show a helpful message 2023-12-20 00:14:53 -08:00
Enrico Ros bee49a4b1c Llms: streaming as a vendor function (then all directed to the unified) 2023-12-20 00:14:53 -08:00
Enrico Ros 0ece1ce58c Llms: vendor-specific RPC to ChatGenerate 2023-12-20 00:14:53 -08:00
Enrico Ros fd897b55b2 Llms: improve list generics 2023-12-20 00:14:53 -08:00
Enrico Ros dd41a402d0 Llms: move models modal 2023-12-20 00:14:53 -08:00
Enrico Ros 3f9defd18c Llms: restructure 2023-12-20 00:14:53 -08:00
Enrico Ros 49c77f5a10 Llms: cleanup model lists (bits) 2023-12-20 00:14:52 -08:00
Enrico Ros 6b2bfa6060 Llms: cleanup model lists 2023-12-20 00:14:52 -08:00
Enrico Ros 8e3f247bfb Gemini: cleaner 2023-12-20 00:14:52 -08:00
Enrico Ros 201e3a7252 Streaming: cleanup 2023-12-20 00:14:52 -08:00
Enrico Ros 044ed4df79 Bits for the future 2023-12-20 00:14:52 -08:00
Enrico Ros 0df7297cca Gemini: configuration, list models, and immediate generation 2023-12-20 00:14:52 -08:00
Enrico Ros 453a3e5751 LLM Vendors: auto IDs 2023-12-20 00:14:52 -08:00
Enrico Ros 34c1c425b9 Gemini: backend env var 2023-12-20 00:14:52 -08:00
Enrico Ros e0a010189f LLMOptions Modal: fix display 2023-12-20 00:14:52 -08:00
Enrico Ros 7a07f10ed1 Move ModelVendor enum 2023-12-20 00:14:52 -08:00
Enrico Ros 33cb2b84b2 Anthropic: allow for 39 chars sks 2023-12-20 00:13:58 -08:00
Enrico Ros 3adec85e1f Fix shortcuts on Mac. 2023-12-18 19:59:03 -08:00
Enrico Ros 18cfe5e296 DB: drop URL validation for POSTGRES_PRISMA_URL. #277 2023-12-18 15:16:02 -08:00
Enrico Ros 566ba366b4 Merge pull request #280
[Visualize] Add custom instruction #218
2023-12-18 12:19:03 -08:00
Enrico Ros 7ed653b315 Fix. 2023-12-18 04:54:04 -08:00
Enrico Ros cb333c33d7 Better 1-click deployment, fixes #279 2023-12-18 03:22:18 -08:00
Joris Kalz 22ba37074b [Visualize] Add custom instruction #218 2023-12-16 23:22:47 +01:00
Enrico Ros 84d7b7644a Ollama: update models 2023-12-15 15:48:41 -08:00
Enrico Ros 71445dafc8 Ollama: improved diagram 2023-12-15 15:29:56 -08:00
Enrico Ros 66a5ad7f00 Ollama: update md 2023-12-15 15:27:11 -08:00
Enrico Ros 09f80adfaa Ollama: update md 2023-12-15 15:26:38 -08:00
Enrico Ros 9febd97065 Ollama: update md 2023-12-15 15:24:48 -08:00
Enrico Ros 5219f9928d Ollama: update md 2023-12-15 15:24:13 -08:00
Enrico Ros aec9f4665f Update config-ollama.md 2023-12-15 15:23:48 -08:00
Enrico Ros db48465204 Ollama: document network issue resolution. #276 2023-12-15 15:20:33 -08:00
Enrico Ros c2c858730a Bite the bullet with Zustand 2023-12-13 14:57:06 -08:00
Enrico Ros 402bde9a81 Newpad 2023-12-13 02:06:19 -08:00
Enrico Ros ba1c0ba0d9 Enforce a Single instance (Tab) of the app. Closes #268 2023-12-13 00:09:56 -08:00
Enrico Ros 084d77cd78 Linting 2023-12-12 18:24:59 -08:00
Enrico Ros 30c17a9b73 Roll Joy 2023-12-12 18:10:46 -08:00
Enrico Ros 2442463da3 deploy-docker.md: update Official guide 2023-12-12 17:52:28 -08:00
Enrico Ros 84a3e8cfdb Fix docker-compose to point to the 'latest' (stable) version, instead of the no more existing 'main' 2023-12-12 17:17:30 -08:00
Enrico Ros 6ae440d252 1.7.3: Patch release for Mistral support 2023-12-12 17:01:40 -08:00
Enrico Ros c0c724afc1 Mistral Platform: full support
Closes #273.
2023-12-12 16:39:06 -08:00
Enrico Ros a265112ce1 Mistral Platform: backend-configurable support (#273) 2023-12-12 16:39:06 -08:00
Enrico Ros 75605ed408 Dropdown: support model vendor icons 2023-12-12 16:39:06 -08:00
Enrico Ros ad38ff4157 LLMs: safer and smarter access 2023-12-12 16:39:06 -08:00
Enrico Ros 08c60e53b1 LLMs: reorder template params 2023-12-12 16:39:06 -08:00
Enrico Ros d0dcb2ac02 LLMs: getTransportAccess 2023-12-12 16:39:06 -08:00
Enrico Ros fbeb604b26 Update README.md 2023-12-12 03:42:05 -08:00
Enrico Ros c4f3b1df77 Update README.md 2023-12-12 03:40:44 -08:00
Enrico Ros 5a1f9caaac Roll rest 2023-12-12 03:16:35 -08:00
Enrico Ros 2fc70d5e95 Roll other dev deps 2023-12-12 03:12:43 -08:00
Enrico Ros 43adadef78 Roll Material/Joy/Next 2023-12-12 03:11:14 -08:00
Enrico Ros 96f6e7628b Roll Prisma 2023-12-12 03:08:10 -08:00
Enrico Ros 32ad82bcee Drag/Drop: do not remove the text from the source 2023-12-12 03:07:31 -08:00
Enrico Ros 3d72aec369 Roll pdfjs-dist 2023-12-12 02:58:06 -08:00
Enrico Ros d244ee2cca Update Docker image workflow.
Assume the vX.Y.Z is the latest (and will have the latest tag). Removing this to remove the 'stable' tag, as
latest is better.

The 'main' branch keeps the development tag.
2023-12-12 01:38:57 -08:00
Enrico Ros cc8a235ae3 Bits 2023-12-12 01:21:43 -08:00
Enrico Ros ae348812de OpenRouter: improve showing of discounted models 2023-12-12 01:14:33 -08:00
Enrico Ros 6053636f66 OpenRouter: OAuth login support 2023-12-11 22:35:40 -08:00
Enrico Ros f2e2aee672 1.7.2: Stable Patch Version 2023-12-11 21:22:31 -08:00
Enrico Ros 11cbb2bbf0 OpenRouter: update models 2023-12-11 21:21:22 -08:00
Enrico Ros 30bd19d6ce HTML Table to Markdown Table: improve reliability and ignore hidden data 2023-12-11 20:46:34 -08:00
Enrico Ros d0b5c02062 Improve how Stream errors are shown 2023-12-11 18:22:15 -08:00
Enrico Ros 771192e406 Ollama: support ollama errors via API 2023-12-11 18:19:38 -08:00
Enrico Ros 13f502bd76 1.7.1: Release (Ollama chat). #270 2023-12-10 22:17:35 -08:00
Enrico Ros 11055b12ca Ollama: use the new Chat endpoint. Closes #270 2023-12-10 22:12:51 -08:00
Enrico Ros d0ea96eec0 Ollama: Admin: optional sort by Pulls, and UI link to the Model page 2023-12-10 22:03:55 -08:00
Enrico Ros 02eafc03f1 Ollama: update models, and sort by Featured 2023-12-10 22:01:50 -08:00
Enrico Ros 33d07a0313 Ollama: update documentation 2023-12-10 21:30:30 -08:00
Enrico Ros 763b852148 Ollama: administration: external link 2023-12-10 20:24:20 -08:00
Enrico Ros d5b0617fd7 Comment for now 2023-12-10 06:14:49 -08:00
Enrico Ros e3ce83674c Update Ollama 2023-12-10 06:09:54 -08:00
175 changed files with 5682 additions and 2191 deletions
+39 -14
View File
@@ -9,6 +9,8 @@ assignees: enricoros
## Release checklist:
- [x] Create a new [Release Issue](https://github.com/enricoros/big-AGI/issues/new?assignees=enricoros&projects=enricoros/4&template=maintainers-release.md&title=Release+1.2.3)
- [ ] Replace 1.1.0 with the _former_ release, and _1.2.3_ with THIS
- [ ] Update the [Roadmap](https://github.com/users/enricoros/projects/4/views/2) calling out shipped features
- [ ] Create and update a [Milestone](https://github.com/enricoros/big-agi/milestones) for the release
- [ ] Assign this task
@@ -34,23 +36,40 @@ assignees: enricoros
- [ ] Discord announcement
- [ ] Twitter announcement
### Links
## Links
Milestone:
Former release task:
GitHub release:
- Milestone: https://github.com/enricoros/big-AGI/milestone/X
- GitHub release: https://github.com/enricoros/big-AGI/releases/tag/vX.Y.Z
- Former release task: https://github.com/enricoros/big-AGI/issues/XXX
## Artifacts Generation
1) The following is my opensource application
- paste README.md
2) I am announcing a new version, 1.7.0. The following were the announcements for 1.6.0. Discord announcement, GitHub Release, in-app news.data.tsx, changelog.md.
- paste the former: `discord announcement`, `GitHub release`, `news.data.tsx`, `changelog.md`
3) The following is the new data I have for 1.7.0
- paste the link to the milestone (closed) and each individual issue (content will be downloaded)
- paste the git changelog `git log v1.6.0..v1.7.0 | clip`
```markdown
You help me generate the following collateral for the new release of my opensource application
called big-AGI. The new release is 1.2.3.
To familiarize yourself with the application, the following are the Website and the GitHub README.md.
```
- paste the URL: https://big-agi.com
- drag & drop: [README.md](https://raw.githubusercontent.com/enricoros/big-AGI/main/README.md)
```markdown
I am announcing a new version, 1.2.3.
For reference, the following was the collateral for 1.1.0 (Discord announcement,
GitHub Release, in-app-news file news.data.tsx, changelog.md).
```
- paste the former: `discord announcement`,
- `GitHub release`,
- `news.data.tsx`,
- `changelog.md`
```markdown
The following are the new developments for 1.2.3:
```
- paste the link to the milestone (closed) and each individual issue (content will be downloaded)
- paste the git changelog `git log v1.1.0..v1.2.3 | clip`
### news.data.TSX
@@ -65,7 +84,13 @@ I need the following from you:
### GitHub release
Now paste the former release (or 1.5.0 which was accurate and great), including the new contributors and
```markdown
Please create the 1.2.3 Release Notes for GitHub.
Use a truthful and honest tone, understanding that people's time and attention span is short.
Today is 2024-1-1.
```
Now paste-attachment the former release notes (or 1.5.0 which was accurate and great), including the new contributors and
some stats (# of commits, etc.), and roll it for the new release.
### Discord announcement
+1 -1
View File
@@ -13,7 +13,7 @@ on:
push:
branches:
- main
- main-stable # Trigger on pushes to the main-stable branch
#- main-stable # Disabled as the v* tag is used for stable releases
tags:
- 'v*' # Trigger on version tags (e.g., v1.7.0)
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 Enrico Ros
Copyright (c) 2023-2024 Enrico Ros
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+26 -35
View File
@@ -1,8 +1,8 @@
# BIG-AGI 🧠✨
Welcome to big-AGI 👋, the GPT application for professionals that need form, function,
simplicity, and speed. Powered by the latest models from 7 vendors, including
open-source, `big-AGI` offers best-in-class Voice and Chat with AI Personas,
Welcome to big-AGI 👋, the GPT application for professionals that need function, form,
simplicity, and speed. Powered by the latest models from 8 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.
Pros use big-AGI. 🚀 Developers love big-AGI. 🤖
@@ -11,7 +11,7 @@ Pros use big-AGI. 🚀 Developers love big-AGI. 🤖
Or fork & run on Vercel
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-agi&env=OPENAI_API_KEY,OPENAI_API_HOST&envDescription=OpenAI%20KEY%20for%20your%20deployment.%20Set%20HOST%20only%20if%20non-default.)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-agi&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-agi)
## 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2)
@@ -21,41 +21,32 @@ shows the current developments and future ideas.
- Got a suggestion? [_Add your roadmap ideas_](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
- Want to contribute? [_Pick up a task!_](https://github.com/users/enricoros/projects/4/views/4) - _easy_ to _pro_
### What's New in 1.7.0 · Dec 10, 2023 · Attachment Theory 🌟
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
- **Attachments System Overhaul**: Drag, paste, link, snap, text, images, PDFs and more. [#251](https://github.com/enricoros/big-agi/issues/251)
- **Desktop Webcam Capture**: Image capture now available as Labs feature. [#253](https://github.com/enricoros/big-agi/issues/253)
- **Independent Browsing**: Full browsing support with Browserless. [Learn More](https://github.com/enricoros/big-agi/blob/main/docs/config-browse.md)
- **Overheat LLMs**: Push the creativity with higher LLM temperatures. [#256](https://github.com/enricoros/big-agi/issues/256)
- **Model Options Shortcut**: Quick adjust with `Ctrl+Shift+O`
- Optimized Voice Input and Performance
- Latest Ollama and Oobabooga models
- For developers: **Password Protection**: HTTP Basic Auth. [Learn How](https://github.com/enricoros/big-agi/blob/main/docs/deploy-authentication.md)
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
- **Perfect scrolling mechanics** across devices. [#304](https://github.com/enricoros/big-AGI/issues/304)
- Persona creation now supports **text input**. [#287](https://github.com/enricoros/big-AGI/pull/287)
- Openrouter updates for better model management and rate limit handling
- Image drawing UX improvements
- Layout fix for Firefox users
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
### What's New in 1.6.0 - Nov 28, 2023
### What's New in 1.8.0 · Dec 20, 2023
- **Web Browsing**: Download web pages within chats - [browsing guide](https://github.com/enricoros/big-agi/blob/main/docs/config-browse.md)
- **Branching Discussions**: Create new conversations from any message
- **Keyboard Navigation**: Swift chat navigation with new shortcuts (e.g. ctrl+alt+left/right)
- **Performance Boost**: Faster rendering for a smoother experience
- **UI Enhancements**: Refined interface based on user feedback
- **New Features**: Anthropic Claude 2.1, `/help` command, and Flattener tool
- **For Developers**: Code quality upgrades and snackbar notifications
- **Google Gemini Support**: Use the newest Google models. [#275](https://github.com/enricoros/big-agi/issues/275)
- **Mistral Platform**: Mixtral and future models support. [#273](https://github.com/enricoros/big-agi/issues/273)
- **Diagram Instructions**. Thanks to @joriskalz! [#280](https://github.com/enricoros/big-agi/pull/280)
- Ollama Chats: Enhanced chatting experience. [#270](https://github.com/enricoros/big-agi/issues/270)
- Mac Shortcuts Fix: Improved UX on Mac
- **Single-Tab Mode**: Data integrity with single window. [#268](https://github.com/enricoros/big-agi/issues/268)
- **Updated Models**: Latest Ollama (v0.1.17) and OpenRouter models
- Official Downloads: Easy access to the latest big-AGI on [big-AGI.com](https://big-agi.com)
- For developers: [troubleshot networking](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483), fixed Vercel deployment, cleaned up the LLMs/Streaming framework
### What's New in 1.5.0 - Nov 19, 2023
### What's New in... ?
- **Continued Voice**: Engage with hands-free interaction for a seamless experience
- **Visualization Tool**: Create data representations with our new visualization capabilities
- **Ollama Local Models**: Leverage local models support with our comprehensive guide
- **Text Tools**: Enjoy tools including highlight differences to refine your content
- **Mermaid Diagramming**: Render complex diagrams with our Mermaid language support
- **OpenAI 1106 Chat Models**: Experience the cutting-edge capabilities of the latest OpenAI models
- **SDXL Support**: Enhance your image generation with SDXL support for Prodia
- **Cloudflare OpenAI API Gateway**: Integrate with Cloudflare for a robust API gateway
- **Helicone for Anthropic**: Utilize Helicone's tools for Anthropic models
Check out the [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), or
the [past releases changelog](docs/changelog.md).
> [To The Moon And Back, Attachment Theory, Surf's Up, Loaded, and more releases...](docs/changelog.md).
> Check out the [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2)
## ✨ Key Features 👊
@@ -145,7 +136,7 @@ Please refer to the [Cloudflare deployment documentation](docs/deploy-cloudflare
Create your GitHub fork, create a Vercel project over that fork, and deploy it. Or press the button below for convenience.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-agi&env=OPENAI_API_KEY,OPENAI_API_HOST&envDescription=OpenAI%20KEY%20for%20your%20deployment.%20Set%20HOST%20only%20if%20non-default.)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-agi&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-agi)
## Integrations:
+1 -1
View File
@@ -1,2 +1,2 @@
export const runtime = 'edge';
export { openaiStreamingRelayHandler as POST } from '~/modules/llms/transports/server/openai/openai.streaming';
export { llmStreamingRelayHandler as POST } from '~/modules/llms/server/llm.server.streaming';
+1 -1
View File
@@ -6,7 +6,7 @@ version: '3.9'
services:
big-agi:
image: ghcr.io/enricoros/big-agi:main
image: ghcr.io/enricoros/big-agi:latest
ports:
- "3000:3000"
env_file:
+25 -3
View File
@@ -5,12 +5,34 @@ by release.
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
### 1.8.0 - Dec 2023
### 1.10.0 - Jan 2024
- milestone: [1.10.0](https://github.com/enricoros/big-agi/milestone/10)
- 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)
- milestone: [1.8.0](https://github.com/enricoros/big-agi/milestone/8)
### What's New in 1.7.0 · Dec 10, 2023 · Attachment Theory 🌟
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
- **Perfect scrolling mechanics** across devices. [#304](https://github.com/enricoros/big-AGI/issues/304)
- Persona creation now supports **text input**. [#287](https://github.com/enricoros/big-AGI/pull/287)
- Openrouter updates for better model management and rate limit handling
- Image drawing UX improvements
- Layout fix for Firefox users
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
### What's New in 1.8.0 · Dec 20, 2023 · To The Moon And Back
- **Google Gemini Support**: Use the newest Google models. [#275](https://github.com/enricoros/big-agi/issues/275)
- **Mistral Platform**: Mixtral and future models support. [#273](https://github.com/enricoros/big-agi/issues/273)
- **Diagram Instructions**. Thanks to @joriskalz! [#280](https://github.com/enricoros/big-agi/pull/280)
- Ollama Chats: Enhanced chatting experience. [#270](https://github.com/enricoros/big-agi/issues/270)
- Mac Shortcuts Fix: Improved UX on Mac
- **Single-Tab Mode**: Data integrity with single window. [#268](https://github.com/enricoros/big-agi/issues/268)
- **Updated Models**: Latest Ollama (v0.1.17) and OpenRouter models
- Official Downloads: Easy access to the latest big-AGI on [big-AGI.com](https://big-agi.com)
- For developers: [troubleshot networking](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483), fixed Vercel deployment, cleaned up the LLMs/Streaming framework
### What's New in 1.7.0 · Dec 11, 2023 · Attachment Theory
- **Attachments System Overhaul**: Drag, paste, link, snap, text, images, PDFs and more. [#251](https://github.com/enricoros/big-agi/issues/251)
- **Desktop Webcam Capture**: Image capture now available as Labs feature. [#253](https://github.com/enricoros/big-agi/issues/253)
+1 -1
View File
@@ -30,5 +30,5 @@ For instance with [Use luna-ai-llama2 with docker compose](https://localai.io/ba
> NOTE: LocalAI does not list details about the mdoels. Every model is assumed to be
> capable of chatting, and with a context window of 4096 tokens.
> Please update the [src/modules/llms/transports/server/openai/models.data.ts](../src/modules/llms/transports/server/openai/models.data.ts)
> Please update the [src/modules/llms/transports/server/openai/models.data.ts](../src/modules/llms/server/openai/models.data.ts)
> file with the mapping information between LocalAI model IDs and names/descriptions/tokens, etc.
+33 -16
View File
@@ -5,31 +5,46 @@ This guide helps you connect [Ollama](https://ollama.ai) [models](https://ollama
experience. The integration brings the popular big-AGI features to Ollama, including: voice chats,
editing tools, models switching, personas, and more.
_Last updated Dec 16, 2023_
![config-local-ollama-0-example.png](pixels/config-ollama-0-example.png)
## Quick Integration Guide
1. **Ensure Ollama API Server is Running**: Before starting, make sure your Ollama API server is up and running.
2. **Add Ollama as a Model Source**: In `big-AGI`, navigate to the **Models** section, select **Add a model source**, and choose **Ollama**.
3. **Enter Ollama Host URL**: Provide the Ollama Host URL where the API server is accessible (e.g., `http://localhost:11434`).
4. **Refresh Model List**: Once connected, refresh the list of available models to include the Ollama models.
5. **Start Using AI Personas**: Select an Ollama model and begin interacting with AI personas tailored to your needs.
1. **Ensure Ollama API Server is Running**: Follow the official instructions to get Ollama up and running on your machine
- For detailed instructions on setting up the Ollama API server, please refer to the
[Ollama download page](https://ollama.ai/download) and [instructions for linux](https://github.com/jmorganca/ollama/blob/main/docs/linux.md).
2. **Add Ollama as a Model Source**: In `big-AGI`, navigate to the **Models** section, select **Add a model source**, and choose **Ollama**
3. **Enter Ollama Host URL**: Provide the Ollama Host URL where the API server is accessible (e.g., `http://localhost:11434`)
4. **Refresh Model List**: Once connected, refresh the list of available models to include the Ollama models
> Optional: use the Ollama Admin interface to see which models are available and 'Pull' them in your local machine. Note
that this operation will likely timeout due to Edge Functions timeout on the big-AGI server while pulling, and
you'll have to press the 'Pull' button again, until a green message appears.
5. **Chat with Ollama models**: select an Ollama model and begin chatting with AI personas
### Ollama: installation and Setup
**Visual Configuration Guide**:
For detailed instructions on setting up the Ollama API server, please refer to the
[Ollama download page](https://ollama.ai/download) and [instructions for linux](https://github.com/jmorganca/ollama/blob/main/docs/linux.md).
* After adding the `Ollama` model vendor, entering the IP address of an Ollama server, and refreshing models:<br/>
<img src="pixels/config-ollama-1-models.png" alt="config-local-ollama-1-models.png" width="320">
### Visual Guide
* The `Ollama` admin panel, with the `Pull` button highlighted, after pulling the "Yi" model:<br/>
<img src="pixels/config-ollama-2-admin-pull.png" alt="config-local-ollama-2-admin-pull.png" width="320">
* After adding the `Ollama` model vendor, entering the IP address of an Ollama server, and refreshing models:
<img src="pixels/config-ollama-1-models.png" alt="config-local-ollama-1-models.png" style="max-width: 320px;">
* You can now switch model/persona dynamically and text/voice chat with the models:<br/>
<img src="pixels/config-ollama-3-chat.png" alt="config-local-ollama-3-chat.png" width="320">
* The `Ollama` admin panel, with the `Pull` button highlighted, after pulling the "Yi" model:
<img src="pixels/config-ollama-2-admin-pull.png" alt="config-local-ollama-2-admin-pull.png" style="max-width: 320px;">
<br/>
* You can now switch model/persona dynamically and text/voice chat with the models:
<img src="pixels/config-ollama-3-chat.png" alt="config-local-ollama-3-chat.png" style="max-width: 320px;">
### ⚠️ Network Troubleshooting
If you get errors about the server having trouble connecting with Ollama, please see
[this message](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483) on Issue #276.
And in brief, make sure the Ollama endpoint is accessible from the servers where you run big-AGI (which could
be localhost or cloud servers).
![Ollama Networking Chart](pixels/config-ollama-network.png)
<br/>
### Advanced: Model parameters
@@ -68,6 +83,8 @@ Then, edit the nginx configuration file `/etc/nginx/sites-enabled/default` and a
Reach out to our community if you need help with this.
<br/>
### Community and Support
Join our community to share your experiences, get help, and discuss best practices:
@@ -78,4 +95,4 @@ Join our community to share your experiences, get help, and discuss best practic
---
`big-AGI` is committed to providing a powerful, intuitive, and privacy-respecting AI experience.
We are excited for you to explore the possibilities with Ollama models. Happy creating!
We are excited for you to explore the possibilities with Ollama models. Happy creating!
+37 -20
View File
@@ -21,33 +21,23 @@ Docker ensures faster development cycles, easier collaboration, and seamless env
```
4. Browse to [http://localhost:3000](http://localhost:3000)
## Documentation
<br/>
The big-AGI repository includes a Dockerfile and a GitHub Actions workflow for building and publishing a
Docker image of the application.
## Run Official Containers 📦
### Dockerfile
`big-AGI` is pre-built from source code and published as a Docker image on the GitHub Container Registry (ghcr).
The build process is transparent, and happens via GitHub Actions, as described in the
file.
The [`Dockerfile`](../Dockerfile) describes how to create a Docker image. It establishes a Node.js environment,
installs dependencies, and creates a production-ready version of the application as a local container.
### Official Images: [ghcr.io/enricoros/big-agi](https://github.com/enricoros/big-agi/pkgs/container/big-agi)
### Official container images
#### Run using *docker* 🚀
The [`.github/workflows/docker-image.yml`](../.github/workflows/docker-image.yml) file automates the
building and publishing of the Docker images to the GitHub Container Registry (ghcr) when changes are
pushed to the `main` branch.
Official pre-built containers: [ghcr.io/enricoros/big-agi](https://github.com/enricoros/big-agi/pkgs/container/big-agi)
Run official pre-built containers:
```bash
docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi
docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi:latest
```
### Run official containers
In addition, the repository also includes a `docker-compose.yaml` file, configured to run the pre-built
'ghcr image'. This file is used to define the `big-agi` service, the ports to expose, and the command to run.
#### Run using *docker-compose* 🚀
If you have Docker Compose installed, you can run the Docker container with `docker-compose up`
to pull the Docker image (if it hasn't been pulled already) and start a Docker container. If you want to
@@ -57,4 +47,31 @@ update the image to the latest version, you can run `docker-compose pull` before
docker-compose up -d
```
Leverage Docker's capabilities for a reliable and efficient big-AGI deployment.
### Make Local Services Visible to Docker 🌐
To make local services running on your host machine accessible to a Docker container, such as a
[Browseless](./config-browse.md) service or a local API, you can follow this simplified guide:
| Operating System | Steps to Make Local Services Visible to Docker |
|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Windows and macOS | Use the special DNS name `host.docker.internal` to refer to the host machine from within the Docker container. No additional network configuration is required. Access local services using `host.docker.internal:<PORT>`. |
| Linux | Two options: *A*. Use <ins>--network="host"</ins> (`docker run --network="host" -d big-agi`) when running the Docker container to merge the container within the host network stack; however, this reduces container isolation. Alternatively: *B*. Connect to local services <ins>using the host's IP address</ins> directly, as host.docker.internal is not available by default on Linux. |
<br/>
### More Information
The [`Dockerfile`](../Dockerfile) describes how to create a Docker image. It establishes a Node.js environment,
installs dependencies, and creates a production-ready version of the application as a local container.
The [`docker-compose.yaml`](../docker-compose.yaml) file is configured to run the
official image (big-agi:latest). This file is used to define the `big-agi` service, to expose
port 3000 on the host, and launch big-AGI within the container (startup command).
The [`.github/workflows/docker-image.yml`](../.github/workflows/docker-image.yml) file is used
to build the Official Docker images and publish them to the GitHub Container Registry (ghcr).
The build process is transparent and happens via GitHub Actions.
<br/>
Leverage Docker's capabilities for a reliable and efficient big-AGI deployment!
+1 -1
View File
@@ -12,7 +12,7 @@ version: '3.9'
services:
big-agi:
image: ghcr.io/enricoros/big-agi:main
image: ghcr.io/enricoros/big-agi:latest
ports:
- "3000:3000"
env_file:
+6 -5
View File
@@ -24,6 +24,8 @@ AZURE_OPENAI_API_ENDPOINT=
AZURE_OPENAI_API_KEY=
ANTHROPIC_API_KEY=
ANTHROPIC_API_HOST=
GEMINI_API_KEY=
MISTRAL_API_KEY=
OLLAMA_API_HOST=
OPENROUTER_API_KEY=
@@ -45,7 +47,7 @@ PUPPETEER_WSS_ENDPOINT=
# Backend Analytics
BACKEND_ANALYTICS=
# Backend HTTP Basic Authentication
# Backend HTTP Basic Authentication (see `deploy-authentication.md` for turning on authentication)
HTTP_BASIC_AUTH_USERNAME=
HTTP_BASIC_AUTH_PASSWORD=
```
@@ -79,6 +81,8 @@ requiring the user to enter an API key
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key, see [config-azure-openai.md](config-azure-openai.md) | Optional, but if set `AZURE_OPENAI_API_ENDPOINT` must also be set |
| `ANTHROPIC_API_KEY` | The API key for Anthropic | Optional |
| `ANTHROPIC_API_HOST` | Changes the backend host for the Anthropic vendor, to enable platforms such as [config-aws-bedrock.md](config-aws-bedrock.md) | Optional |
| `GEMINI_API_KEY` | The API key for Google AI's Gemini | Optional |
| `MISTRAL_API_KEY` | The API key for Mistral | Optional |
| `OLLAMA_API_HOST` | Changes the backend host for the Ollama vendor. See [config-ollama.md](config-ollama.md) | |
| `OPENROUTER_API_KEY` | The API key for OpenRouter | Optional |
@@ -113,10 +117,7 @@ Enable the app to Talk, Draw, and Google things up.
| `PUPPETEER_WSS_ENDPOINT` | Puppeteer WebSocket endpoint - used for browsing, etc. |
| **Backend** | |
| `BACKEND_ANALYTICS` | Semicolon-separated list of analytics flags (see backend.analytics.ts). Flags: `domain` logs the responding domain. |
| `HTTP_BASIC_AUTH_USERNAME` | Username for HTTP Basic Authentication. See the [Authentication](deploy-authentication.md) guide. |
| `HTTP_BASIC_AUTH_USERNAME` | See the [Authentication](deploy-authentication.md) guide. Username for HTTP Basic Authentication. |
| `HTTP_BASIC_AUTH_PASSWORD` | Password for HTTP Basic Authentication. |
---
Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

+541 -280
View File
File diff suppressed because it is too large Load Diff
+22 -19
View File
@@ -1,6 +1,6 @@
{
"name": "big-agi",
"version": "1.7.0",
"version": "1.9.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -18,13 +18,13 @@
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.14.18",
"@mui/joy": "^5.0.0-beta.15",
"@next/bundle-analyzer": "^14.0.3",
"@prisma/client": "^5.6.0",
"@mui/icons-material": "^5.15.1",
"@mui/joy": "^5.0.0-beta.19",
"@next/bundle-analyzer": "^14.0.4",
"@prisma/client": "^5.7.1",
"@sanity/diff-match-patch": "^3.1.1",
"@t3-oss/env-nextjs": "^0.7.1",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-query": "~4.36.1",
"@trpc/client": "^10.44.1",
"@trpc/next": "^10.44.1",
"@trpc/react-query": "^10.44.1",
@@ -33,37 +33,40 @@
"browser-fs-access": "^0.35.0",
"eventsource-parser": "^1.1.1",
"idb-keyval": "^6.2.1",
"next": "^14.0.3",
"pdfjs-dist": "4.0.189",
"next": "^14.0.4",
"nprogress": "^0.2.0",
"pdfjs-dist": "4.0.269",
"plantuml-encoder": "^1.4.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-katex": "^3.0.1",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^1.0.5",
"react-timeago": "^7.2.0",
"remark-gfm": "^4.0.0",
"superjson": "^2.2.1",
"tesseract.js": "^5.0.3",
"uuid": "^9.0.1",
"zod": "^3.22.4",
"zustand": "~4.3.9"
"zustand": "^4.4.7"
},
"devDependencies": {
"@cloudflare/puppeteer": "^0.0.5",
"@types/node": "^20.10.0",
"@types/node": "^20.10.5",
"@types/nprogress": "^0.2.3",
"@types/plantuml-encoder": "^1.4.2",
"@types/prismjs": "^1.26.3",
"@types/react": "^18.2.38",
"@types/react-dom": "^18.2.17",
"@types/react-katex": "^3.0.3",
"@types/react-timeago": "^4.1.6",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.18",
"@types/react-katex": "^3.0.4",
"@types/react-timeago": "^4.1.7",
"@types/uuid": "^9.0.7",
"eslint": "^8.54.0",
"eslint-config-next": "^14.0.3",
"prettier": "^3.1.0",
"prisma": "^5.6.0",
"typescript": "^5.3.2"
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"prettier": "^3.1.1",
"prisma": "^5.7.1",
"typescript": "^5.3.3"
},
"engines": {
"node": "^20.0.0 || ^18.0.0"
+17 -11
View File
@@ -10,10 +10,12 @@ import 'katex/dist/katex.min.css';
import '~/common/styles/CodePrism.css';
import '~/common/styles/GithubMarkdown.css';
import { ProviderBackend } from '~/common/state/ProviderBackend';
import { ProviderSnacks } from '~/common/state/ProviderSnacks';
import { ProviderTRPCQueryClient } from '~/common/state/ProviderTRPCQueryClient';
import { ProviderTheming } from '~/common/state/ProviderTheming';
import { ProviderBackendAndNoSSR } from '~/common/providers/ProviderBackendAndNoSSR';
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 { ProviderTheming } from '~/common/providers/ProviderTheming';
const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
@@ -25,13 +27,17 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
</Head>
<ProviderTheming emotionCache={emotionCache}>
<ProviderTRPCQueryClient>
<ProviderSnacks>
<ProviderBackend>
<Component {...pageProps} />
</ProviderBackend>
</ProviderSnacks>
</ProviderTRPCQueryClient>
<ProviderSingleTab>
<ProviderBootstrapLogic>
<ProviderTRPCQueryClient>
<ProviderSnacks>
<ProviderBackendAndNoSSR>
<Component {...pageProps} />
</ProviderBackendAndNoSSR>
</ProviderSnacks>
</ProviderTRPCQueryClient>
</ProviderBootstrapLogic>
</ProviderSingleTab>
</ProviderTheming>
<VercelAnalytics debug={false} />
+2 -6
View File
@@ -2,13 +2,9 @@ import * as React from 'react';
import { AppCall } from '../src/apps/call/AppCall';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function CallPage() {
return (
<AppLayout>
<AppCall />
</AppLayout>
);
return withLayout({ type: 'optima' }, <AppCall />);
}
+5 -9
View File
@@ -1,18 +1,14 @@
import * as React from 'react';
import { AppChat } from '../src/apps/chat/AppChat';
import { useShowNewsOnUpdate } from '../src/apps/news/news.hooks';
import { useRedirectToNewsOnUpdates } from '../src/apps/news/news.hooks';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function ChatPage() {
// show the News page on updates
useShowNewsOnUpdate();
// show the News page if there are unseen updates
useRedirectToNewsOnUpdates();
return (
<AppLayout>
<AppChat />
</AppLayout>
);
return withLayout({ type: 'optima' }, <AppChat />);
}
+94
View File
@@ -0,0 +1,94 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { Box, Typography } from '@mui/joy';
import { useModelsStore } from '~/modules/llms/store-llms';
import { InlineError } from '~/common/components/InlineError';
import { apiQuery } from '~/common/util/trpc.client';
import { navigateToIndex } from '~/common/app.routes';
import { themeBgApp } from '~/common/app.theme';
import { withLayout } from '~/common/layout/withLayout';
function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
// external state
const { data, isError, error, isLoading } = apiQuery.backend.exchangeOpenRouterKey.useQuery({ code: props.openRouterCode || '' }, {
enabled: !!props.openRouterCode,
refetchOnWindowFocus: false,
staleTime: Infinity,
});
// derived state
const isErrorInput = !props.openRouterCode;
const openRouterKey = data?.key ?? undefined;
const isSuccess = !!openRouterKey;
// Success: save the key and redirect to the chat app
React.useEffect(() => {
if (!isSuccess)
return;
// 1. Save the key as the client key
useModelsStore.getState().setOpenRoutersKey(openRouterKey);
// 2. Navigate to the chat app
void navigateToIndex(true); //.then(openModelsSetup);
}, [isSuccess, openRouterKey]);
return (
<Box sx={{
flexGrow: 1,
backgroundColor: themeBgApp,
overflowY: 'auto',
display: 'flex', justifyContent: 'center',
p: { xs: 3, md: 6 },
}}>
<Box sx={{
// my: 'auto',
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: 4,
}}>
<Typography level='title-lg'>
Welcome Back
</Typography>
{isLoading && <Typography level='body-sm'>Loading...</Typography>}
{isErrorInput && <InlineError error='There was an issue retrieving the code from OpenRouter.' />}
{isError && <InlineError error={error} />}
{data && (
<Typography level='body-md'>
Success! You can now close this window.
</Typography>
)}
</Box>
</Box>
);
}
/**
* This page will be invoked by OpenRouter as a Callback
*
* Docs: https://openrouter.ai/docs#oauth
* Example URL: https://localhost:3000/link/callback_openrouter?code=SomeCode
*/
export default function CallbackPage() {
// get the 'code=...' from the URL
const { query } = useRouter();
const { code: openRouterCode } = query;
return withLayout({ type: 'plain' }, <CallbackOpenRouterPage openRouterCode={openRouterCode as (string | undefined)} />);
}
+2 -6
View File
@@ -3,16 +3,12 @@ import { useRouter } from 'next/router';
import { AppChatLink } from '../../../src/apps/link/AppChatLink';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function ChatLinkPage() {
const { query } = useRouter();
const chatLinkId = query?.chatLinkId as string ?? '';
return (
<AppLayout suspendAutoModelsSetup>
<AppChatLink linkId={chatLinkId} />
</AppLayout>
);
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppChatLink linkId={chatLinkId} />);
}
+5 -8
View File
@@ -8,10 +8,11 @@ import { setComposerStartupText } from '../../src/apps/chat/components/composer/
import { callBrowseFetchPage } from '~/modules/browse/browse.client';
import { AppLayout } from '~/common/layout/AppLayout';
import { LogoProgress } from '~/common/components/LogoProgress';
import { asValidURL } from '~/common/util/urlUtils';
import { navigateToIndex } from '~/common/app.routes';
import { themeBgApp } from '~/common/app.theme';
import { withLayout } from '~/common/layout/withLayout';
/**
@@ -90,7 +91,7 @@ function AppShareTarget() {
return (
<Box sx={{
backgroundColor: 'background.level2',
backgroundColor: themeBgApp,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
flexGrow: 1,
}}>
@@ -132,10 +133,6 @@ function AppShareTarget() {
* This page will be invoked on mobile when sharing Text/URLs/Files from other APPs
* Example URL: https://localhost:3000/link/share_target?title=This+Title&text=https%3A%2F%2Fexample.com%2Fapp%2Fpath
*/
export default function LaunchPage() {
return (
<AppLayout>
<AppShareTarget />
</AppLayout>
);
export default function ShareTargetPage() {
return withLayout({ type: 'plain' }, <AppShareTarget />);
}
+3 -7
View File
@@ -3,16 +3,12 @@ import * as React from 'react';
import { AppNews } from '../src/apps/news/AppNews';
import { useMarkNewsAsSeen } from '../src/apps/news/news.hooks';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function NewsPage() {
// update the last seen news version
// 'touch' the last seen news version
useMarkNewsAsSeen();
return (
<AppLayout suspendAutoModelsSetup>
<AppNews />
</AppLayout>
);
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppNews />);
}
+2 -6
View File
@@ -2,13 +2,9 @@ import * as React from 'react';
import { AppPersonas } from '../src/apps/personas/AppPersonas';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function PersonasPage() {
return (
<AppLayout>
<AppPersonas />
</AppLayout>
);
return withLayout({ type: 'optima' }, <AppPersonas />);
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+7 -7
View File
@@ -15,20 +15,20 @@ import { useChatLLMDropdown } from '../chat/components/applayout/useLLMDropdown'
import { EXPERIMENTAL_speakTextStream } from '~/modules/elevenlabs/elevenlabs.client';
import { SystemPurposeId, SystemPurposes } from '../../data';
import { VChatMessageIn } from '~/modules/llms/transports/chatGenerate';
import { streamChat } from '~/modules/llms/transports/streamChat';
import { llmStreamingChatGenerate, VChatMessageIn } from '~/modules/llms/llm.client';
import { useElevenLabsVoiceDropdown } from '~/modules/elevenlabs/useElevenLabsVoiceDropdown';
import { Link } from '~/common/components/Link';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { conversationTitle, createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
import { playSoundUrl, usePlaySoundUrl } from '~/common/util/audioUtils';
import { useLayoutPluggable } from '~/common/layout/store-applayout';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { CallAvatar } from './components/CallAvatar';
import { CallButton } from './components/CallButton';
import { CallMessage } from './components/CallMessage';
import { CallStatus } from './components/CallStatus';
import { ROUTE_APP_CHAT } from '~/common/app.routes';
function CallMenuItems(props: {
@@ -179,7 +179,7 @@ export function CallUI(props: {
case 'Goodbye.':
setStage('ended');
setTimeout(() => {
void routerPush('/');
void routerPush(ROUTE_APP_CHAT);
}, 2000);
return;
// command: regenerate answer
@@ -216,7 +216,7 @@ export function CallUI(props: {
responseAbortController.current = new AbortController();
let finalText = '';
let error: any | null = null;
streamChat(chatLLMId, callPrompt, responseAbortController.current.signal, (updatedMessage: Partial<DMessage>) => {
llmStreamingChatGenerate(chatLLMId, callPrompt, null, null, responseAbortController.current.signal, (updatedMessage: Partial<DMessage>) => {
const text = updatedMessage.text?.trim();
if (text) {
finalText = text;
@@ -273,7 +273,7 @@ export function CallUI(props: {
, [overridePersonaVoice, pushToTalk],
);
useLayoutPluggable(chatLLMDropdown, null, menuItems);
usePluggableOptimaLayout(null, chatLLMDropdown, menuItems, 'CallUI');
return <>
@@ -367,7 +367,7 @@ export function CallUI(props: {
)}
{/* [ended] Back / Call Again */}
{(isEnded || isDeclined) && <Link noLinkStyle href='/'><CallButton Icon={ArrowBackIcon} text='Back' variant='soft' /></Link>}
{(isEnded || isDeclined) && <Link noLinkStyle href={ROUTE_APP_CHAT}><CallButton Icon={ArrowBackIcon} text='Back' variant='soft' /></Link>}
{(isEnded || isDeclined) && <CallButton Icon={CallIcon} text='Call Again' color='success' variant='soft' onClick={() => setStage('connected')} />}
</Box>
+3 -2
View File
@@ -11,9 +11,9 @@ import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
import WarningIcon from '@mui/icons-material/Warning';
import { navigateBack } from '~/common/app.routes';
import { openLayoutPreferences } from '~/common/layout/store-applayout';
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
import { useChatStore } from '~/common/state/store-chats';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUICounter } from '~/common/state/store-ui';
@@ -81,6 +81,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
const [recognitionOverride, setRecognitionOverride] = React.useState(false);
// external state
const { openPreferences } = useOptimaLayout();
const recognition = useCapabilityBrowserSpeechRecognition();
const synthesis = useCapabilityElevenLabs();
const chatIsEmpty = useChatStore(state => {
@@ -103,7 +104,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
const handleOverrideRecognition = () => setRecognitionOverride(true);
const handleConfigureElevenLabs = () => {
openLayoutPreferences(3);
openPreferences(3);
};
const handleFinishButton = () => {
+1 -1
View File
@@ -3,7 +3,7 @@ import * as React from 'react';
import { Chip, ColorPaletteProp, VariantProp } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import { VChatMessageIn } from '~/modules/llms/transports/chatGenerate';
import type { VChatMessageIn } from '~/modules/llms/llm.client';
export function CallMessage(props: {
+147 -86
View File
@@ -1,11 +1,12 @@
import * as React from 'react';
import { Box } from '@mui/joy';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { Box, useTheme } from '@mui/joy';
import { CmdRunBrowse } from '~/modules/browse/browse.client';
import { CmdRunProdia } from '~/modules/prodia/prodia.client';
import { CmdRunReact } from '~/modules/aifn/react/react';
import { CmdRunT2I, useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
@@ -18,7 +19,8 @@ import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
import { createDMessage, DConversationId, DMessage, getConversation, useConversation } from '~/common/state/store-chats';
import { openLayoutLLMOptions, useLayoutPluggable } from '~/common/layout/store-applayout';
import { themeBgApp, themeBgAppChatComposer } from '~/common/app.theme';
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
@@ -29,7 +31,9 @@ import { ChatMessageList } from './components/ChatMessageList';
import { CmdAddRoleMessage, CmdHelp, createCommandsHelpMessage, extractCommands } from './editors/commands';
import { Composer } from './components/composer/Composer';
import { Ephemerals } from './components/Ephemerals';
import { usePanesManager } from './components/usePanesManager';
import { ScrollToBottom } from './components/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from './components/scroll-to-bottom/ScrollToBottomButton';
import { usePanesManager } from './components/panes/usePanesManager';
import { runAssistantUpdatingState } from './editors/chat-stream';
import { runBrowseUpdatingState } from './editors/browse-load';
@@ -40,7 +44,11 @@ import { runReActUpdatingState } from './editors/react-tangent';
/**
* Mode: how to treat the input from the Composer
*/
export type ChatModeId = 'immediate' | 'write-user' | 'react' | 'draw-imagine' | 'draw-imagine-plus';
export type ChatModeId =
| 'generate-text'
| 'append-user'
| 'generate-image'
| 'generate-react';
const SPECIAL_ID_WIPE_ALL: DConversationId = 'wipe-chats';
@@ -58,6 +66,10 @@ export function AppChat() {
const composerTextAreaRef = React.useRef<HTMLTextAreaElement>(null);
// external state
const theme = useTheme();
const { openLlmOptions } = useOptimaLayout();
const { chatLLM } = useChatLLM();
const {
@@ -66,7 +78,10 @@ export function AppChat() {
navigateHistoryInFocusedPane,
openConversationInFocusedPane,
openConversationInSplitPane,
setFocusedPaneIndex,
paneIndex,
duplicatePane,
removePane,
setFocusedPane,
} = usePanesManager();
const {
@@ -83,14 +98,13 @@ export function AppChat() {
setMessages,
} = useConversation(focusedConversationId);
const { mayWork: capabilityHasT2I } = useCapabilityTextToImage();
// Window actions
const chatPaneIDs = chatPanes.length > 0 ? chatPanes.map(pane => pane.conversationId) : [null];
const setActivePaneIndex = React.useCallback((idx: number) => {
setFocusedPaneIndex(idx);
}, [setFocusedPaneIndex]);
const panesConversationIDs = chatPanes.length > 0 ? chatPanes.map(pane => pane.conversationId) : [null];
const isSplitPane = chatPanes.length > 1;
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
conversationId && openConversationInFocusedPane(conversationId);
@@ -100,6 +114,13 @@ export function AppChat() {
conversationId && openConversationInSplitPane(conversationId);
}, [openConversationInSplitPane]);
const toggleSplitPane = React.useCallback(() => {
if (isSplitPane)
removePane(paneIndex ?? chatPanes.length - 1);
else
duplicatePane(paneIndex ?? chatPanes.length - 1);
}, [chatPanes.length, duplicatePane, isSplitPane, paneIndex, removePane]);
const handleNavigateHistory = React.useCallback((direction: 'back' | 'forward') => {
if (navigateHistoryInFocusedPane(direction))
showNextTitle.current = true;
@@ -127,7 +148,7 @@ export function AppChat() {
const pieces = extractCommands(lastMessage.text);
if (pieces.length == 2 && pieces[0].type === 'cmd' && pieces[1].type === 'text') {
const [command, prompt] = [pieces[0].value, pieces[1].value];
if (CmdRunProdia.includes(command)) {
if (CmdRunT2I.includes(command)) {
setMessages(conversationId, history);
return await runImageGenerationUpdatingState(conversationId, prompt);
}
@@ -154,27 +175,26 @@ export function AppChat() {
// synchronous long-duration tasks, which update the state as they go
if (chatLLMId && focusedSystemPurposeId) {
switch (chatModeId) {
case 'immediate':
case 'generate-text':
return await runAssistantUpdatingState(conversationId, history, chatLLMId, focusedSystemPurposeId);
case 'write-user':
case 'append-user':
return setMessages(conversationId, history);
case 'react':
case 'generate-image':
if (!lastMessage?.text)
break;
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
...message,
text: `${CmdRunT2I[0]} ${lastMessage.text}`,
}));
return await runImageGenerationUpdatingState(conversationId, lastMessage.text);
case 'generate-react':
if (!lastMessage?.text)
break;
setMessages(conversationId, history);
return await runReActUpdatingState(conversationId, lastMessage.text, chatLLMId);
case 'draw-imagine':
case 'draw-imagine-plus':
if (!lastMessage?.text)
break;
const imagePrompt = chatModeId == 'draw-imagine-plus'
? await imaginePromptFromText(lastMessage.text) || 'An error sign.'
: lastMessage.text;
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
...message,
text: `${CmdRunProdia[0]} ${imagePrompt}`,
}));
return await runImageGenerationUpdatingState(conversationId, imagePrompt);
}
}
@@ -213,13 +233,13 @@ export function AppChat() {
};
const handleConversationExecuteHistory = async (conversationId: DConversationId, history: DMessage[]) =>
await _handleExecute('immediate', conversationId, history);
await _handleExecute('generate-text', conversationId, history);
const handleMessageRegenerateLast = React.useCallback(async () => {
const focusedConversation = getConversation(focusedConversationId);
if (focusedConversation?.messages?.length) {
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
return await _handleExecute('immediate', focusedConversation.id, lastMessage.role === 'assistant'
return await _handleExecute('generate-text', focusedConversation.id, lastMessage.role === 'assistant'
? focusedConversation.messages.slice(0, -1)
: [...focusedConversation.messages],
);
@@ -228,13 +248,15 @@ export function AppChat() {
const handleTextDiagram = async (diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig);
const handleTextImaginePlus = async (conversationId: DConversationId, messageText: string) => {
const handleTextImagine = async (conversationId: DConversationId, messageText: string) => {
const conversation = getConversation(conversationId);
if (conversation)
return await _handleExecute('draw-imagine-plus', conversationId, [
...conversation.messages,
createDMessage('user', messageText),
]);
if (!conversation)
return;
const imaginedPrompt = await imaginePromptFromText(messageText) || 'An error sign.';
return await _handleExecute('generate-image', conversationId, [
...conversation.messages,
createDMessage('user', imaginedPrompt),
]);
};
const handleTextSpeak = async (text: string) => {
@@ -317,8 +339,8 @@ export function AppChat() {
const handleOpenChatLlmOptions = React.useCallback(() => {
const { chatLLMId } = useModelsStore.getState();
if (!chatLLMId) return;
openLayoutLLMOptions(chatLLMId);
}, []);
openLlmOptions(chatLLMId);
}, [openLlmOptions]);
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
['o', true, true, false, handleOpenChatLlmOptions],
@@ -336,8 +358,12 @@ export function AppChat() {
// Pluggable ApplicationBar components
const centerItems = React.useMemo(() =>
<ChatDropdowns conversationId={focusedConversationId} />,
[focusedConversationId],
<ChatDropdowns
conversationId={focusedConversationId}
isSplitPanes={isSplitPane}
onToggleSplitPanes={toggleSplitPane}
/>,
[focusedConversationId, isSplitPane, toggleSplitPane],
);
const drawerItems = React.useMemo(() =>
@@ -368,72 +394,107 @@ export function AppChat() {
[areChatsEmpty, focusedConversationId, handleConversationBranch, isFocusedChatEmpty, isMessageSelectionMode],
);
useLayoutPluggable(centerItems, drawerItems, menuItems);
usePluggableOptimaLayout(drawerItems, centerItems, menuItems, 'AppChat');
return <>
<Box sx={{
flexGrow: 1,
display: 'flex', flexDirection: { xs: 'column', md: 'row' },
overflow: 'clip',
}}>
<PanelGroup direction='horizontal'>
{chatPaneIDs.map((_conversationId, idx) => (
<Box key={'chat-pane-' + idx} onClick={() => setActivePaneIndex(idx)} sx={{
flexGrow: 1, flexBasis: 1,
display: 'flex', flexDirection: 'column',
overflow: 'clip',
}}>
{panesConversationIDs.map((_conversationId, idx, panels) => <React.Fragment key={`chat-pane-${idx}-${panels.length}-${_conversationId}`}>
<ChatMessageList
conversationId={_conversationId}
chatLLMContextTokens={chatLLM?.contextTokens}
isMessageSelectionMode={isMessageSelectionMode}
setIsMessageSelectionMode={setIsMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationExecuteHistory={handleConversationExecuteHistory}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImaginePlus}
onTextSpeak={handleTextSpeak}
<Panel
id={'chat-pane-' + _conversationId}
order={idx}
collapsible
defaultSize={panels.length > 0 ? Math.round(100 / panels.length) : undefined}
minSize={20}
onClick={() => setFocusedPane(idx)}
onCollapse={() => setTimeout(() => removePane(idx), 50)}
style={{
// for anchoring the scroll button in place
position: 'relative',
// border only for active pane (if two or more panes)
...(panesConversationIDs.length < 2 ? {}
: (_conversationId === focusedConversationId)
? { border: `2px solid ${theme.palette.primary.solidBg}` }
: { border: `2px solid ${theme.palette.background.level1}` }),
}}
>
<ScrollToBottom
bootToBottom
stickToBottom
sx={{
flexGrow: 1,
backgroundColor: 'background.level1',
// allows the content to be scrolled (all browsers)
overflowY: 'auto',
minHeight: 96,
// outline the current focused pane
...(chatPaneIDs.length < 2 ? {}
: (_conversationId === focusedConversationId)
? {
border: '2px solid',
borderColor: 'primary.solidBg',
} : {
padding: '2px',
}),
// actually make sure this scrolls & fills
height: '100%',
}}
/>
>
<Ephemerals
conversationId={_conversationId}
sx={{
// flexGrow: 0.1,
flexShrink: 0.5,
overflowY: 'auto',
minHeight: 64,
<ChatMessageList
conversationId={_conversationId}
capabilityHasT2I={capabilityHasT2I}
chatLLMContextTokens={chatLLM?.contextTokens}
isMessageSelectionMode={isMessageSelectionMode}
setIsMessageSelectionMode={setIsMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationExecuteHistory={handleConversationExecuteHistory}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImagine}
onTextSpeak={handleTextSpeak}
sx={{
backgroundColor: themeBgApp,
minHeight: '100%', // ensures filling of the blank space on newer chats
}}
/>
<Ephemerals
conversationId={_conversationId}
sx={{
// TODO: Fixme post panels?
// flexGrow: 0.1,
flexShrink: 0.5,
overflowY: 'auto',
minHeight: 64,
}} />
{/* Visibility and actions are handled via Context */}
<ScrollToBottomButton />
</ScrollToBottom>
</Panel>
{/* Panel Separators & Resizers */}
{idx < panels.length - 1 && (
<PanelResizeHandle>
<Box sx={{
backgroundColor: themeBgApp,
height: '100%',
width: '4px',
'&:hover': {
backgroundColor: 'primary.softActiveBg',
},
}} />
</PanelResizeHandle>
)}
</Box>
))}
</Box>
</React.Fragment>)}
</PanelGroup>
<Composer
chatLLM={chatLLM}
composerTextAreaRef={composerTextAreaRef}
conversationId={focusedConversationId}
capabilityHasT2I={capabilityHasT2I}
isDeveloperMode={focusedSystemPurposeId === 'Developer'}
onAction={handleComposerAction}
onTextImagine={handleTextImagine}
sx={{
zIndex: 21, // position: 'sticky', bottom: 0,
backgroundColor: 'background.surface',
backgroundColor: themeBgAppChatComposer,
borderTop: `1px solid`,
borderTopColor: 'divider',
p: { xs: 1, md: 2 },
+37 -27
View File
@@ -9,13 +9,14 @@ import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
import { InlineError } from '~/common/components/InlineError';
import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats';
import { openLayoutPreferences } from '~/common/layout/store-applayout';
import { useCapabilityElevenLabs, useCapabilityProdia } from '~/common/components/useCapabilities';
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { ChatMessageMemo } from './message/ChatMessage';
import { CleanerMessage, MessagesSelectionHeader } from './message/CleanerMessage';
import { PersonaSelector } from './persona-selector/PersonaSelector';
import { useChatShowSystemMessages } from '../store-app-chat';
import { useScrollToBottom } from './scroll-to-bottom/useScrollToBottom';
/**
@@ -23,6 +24,7 @@ import { useChatShowSystemMessages } from '../store-app-chat';
*/
export function ChatMessageList(props: {
conversationId: DConversationId | null,
capabilityHasT2I: boolean,
chatLLMContextTokens?: number,
isMessageSelectionMode: boolean, setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
@@ -39,6 +41,8 @@ export function ChatMessageList(props: {
const [selectedMessages, setSelectedMessages] = React.useState<Set<string>>(new Set());
// external state
const { notifyBooting } = useScrollToBottom();
const { openPreferences } = useOptimaLayout();
const [showSystemMessages] = useChatShowSystemMessages();
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
@@ -50,11 +54,10 @@ export function ChatMessageList(props: {
setMessages: state.setMessages,
};
}, shallow);
const { mayWork: isImaginable } = useCapabilityProdia();
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
// derived state
const { conversationId, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
const { conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
// text actions
@@ -98,22 +101,22 @@ export function ChatMessageList(props: {
}, [conversationId, onTextDiagram]);
const handleTextImagine = React.useCallback(async (text: string) => {
if (!isImaginable)
return openLayoutPreferences(2);
if (!capabilityHasT2I)
return openPreferences(2);
if (conversationId) {
setIsImagining(true);
await onTextImagine(conversationId, text);
setIsImagining(false);
}
}, [conversationId, isImaginable, onTextImagine]);
}, [capabilityHasT2I, conversationId, onTextImagine, openPreferences]);
const handleTextSpeak = React.useCallback(async (text: string) => {
if (!isSpeakable)
return openLayoutPreferences(3);
return openPreferences(3);
setIsSpeaking(true);
await onTextSpeak(text);
setIsSpeaking(false);
}, [isSpeakable, onTextSpeak]);
}, [isSpeakable, onTextSpeak, openPreferences]);
// operate on the local selection set
@@ -157,11 +160,19 @@ export function ChatMessageList(props: {
return { diffMessage: undefined, diffText: undefined };
}, [conversationMessages]);
// scroll to the very bottom of a new chat
React.useEffect(() => {
if (conversationId)
notifyBooting();
}, [conversationId, notifyBooting]);
// no content: show the persona selector
const filteredMessages = conversationMessages
.filter(m => m.role !== 'system' || showSystemMessages) // hide the System message if the user choses to
.reverse(); // 'reverse' is because flexDirection: 'column-reverse' to auto-snap-to-bottom
.filter(m => m.role !== 'system' || showSystemMessages); // hide the System message if the user choses to
if (!filteredMessages.length)
return (
@@ -176,18 +187,29 @@ export function ChatMessageList(props: {
<List sx={{
p: 0, ...(props.sx || {}),
// this makes sure that the the window is scrolled to the bottom (column-reverse)
display: 'flex', flexDirection: 'column-reverse',
display: 'flex',
flexDirection: 'column',
// fix for the double-border on the last message (one by the composer, one to the bottom of the message)
// marginBottom: '-1px',
}}>
{filteredMessages.map((message, idx) =>
{props.isMessageSelectionMode && (
<MessagesSelectionHeader
hasSelected={selectedMessages.size > 0}
sumTokens={historyTokenCount}
onClose={() => props.setIsMessageSelectionMode(false)}
onSelectAll={handleSelectAll}
onDeleteMessages={handleSelectionDelete}
/>
)}
{filteredMessages.map((message, idx, { length: count }) =>
props.isMessageSelectionMode ? (
<CleanerMessage
key={'sel-' + message.id}
message={message}
isBottom={idx === 0} remainingTokens={(props.chatLLMContextTokens || 0) - historyTokenCount}
remainingTokens={(props.chatLLMContextTokens || 0) - historyTokenCount}
selected={selectedMessages.has(message.id)} onToggleSelected={handleSelectMessage}
/>
@@ -197,7 +219,7 @@ export function ChatMessageList(props: {
key={'msg-' + message.id}
message={message}
diffPreviousText={message === diffMessage ? diffText : undefined}
isBottom={idx === 0}
isBottom={idx === count - 1}
isImagining={isImagining} isSpeaking={isSpeaking}
onConversationBranch={handleConversationBranch}
onConversationRestartFrom={handleConversationRestartFrom}
@@ -212,18 +234,6 @@ export function ChatMessageList(props: {
),
)}
{/* Header at the bottom because of 'row-reverse' */}
{props.isMessageSelectionMode && (
<MessagesSelectionHeader
hasSelected={selectedMessages.size > 0}
isBottom={filteredMessages.length === 0}
sumTokens={historyTokenCount}
onClose={() => props.setIsMessageSelectionMode(false)}
onSelectAll={handleSelectAll}
onDeleteMessages={handleSelectionDelete}
/>
)}
</List>
);
}
@@ -8,7 +8,7 @@ import FileUploadIcon from '@mui/icons-material/FileUpload';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
import { closeLayoutDrawer } from '~/common/layout/store-applayout';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
@@ -34,6 +34,7 @@ function ChatDrawerItems(props: {
// const [grouping] = React.useState<ListGrouping>('off');
// external state
const { closeAppDrawer } = useOptimaLayout();
const conversations = useChatStore(state => state.conversations, shallow);
const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner');
const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI);
@@ -48,14 +49,14 @@ function ChatDrawerItems(props: {
const handleButtonNew = React.useCallback(() => {
onConversationNew();
closeLayoutDrawer();
}, [onConversationNew]);
closeAppDrawer();
}, [closeAppDrawer, onConversationNew]);
const handleConversationActivate = React.useCallback((conversationId: DConversationId, closeMenu: boolean) => {
onConversationActivate(conversationId);
if (closeMenu)
closeLayoutDrawer();
}, [onConversationActivate]);
closeAppDrawer();
}, [closeAppDrawer, onConversationActivate]);
const handleConversationDelete = React.useCallback((conversationId: DConversationId) => {
!singleChat && conversationId && onConversationDelete(conversationId, true);
@@ -1,6 +1,10 @@
import * as React from 'react';
import { IconButton } from '@mui/joy';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import type { DConversationId } from '~/common/state/store-chats';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { useChatLLMDropdown } from './useLLMDropdown';
import { usePersonaIdDropdown } from './usePersonaDropdown';
@@ -8,12 +12,17 @@ import { usePersonaIdDropdown } from './usePersonaDropdown';
export function ChatDropdowns(props: {
conversationId: DConversationId | null
isSplitPanes: boolean
onToggleSplitPanes: () => void
}) {
// state
const { chatLLMDropdown } = useChatLLMDropdown();
const { personaDropdown } = usePersonaIdDropdown(props.conversationId);
// external state
const labsSplitBranching = useUXLabsStore(state => state.labsSplitBranching);
return <>
{/* Model selector */}
@@ -22,5 +31,17 @@ export function ChatDropdowns(props: {
{/* Persona selector */}
{personaDropdown}
{/* Split Panes button */}
{labsSplitBranching && <IconButton
variant={props.isSplitPanes ? 'solid' : 'plain'}
color='neutral'
onClick={props.onToggleSplitPanes}
sx={{
ml: 'auto',
}}
>
<VerticalSplitIcon />
</IconButton>}
</>;
}
@@ -11,7 +11,7 @@ import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
import type { DConversationId } from '~/common/state/store-chats';
import { KeyStroke } from '~/common/components/KeyStroke';
import { closeLayoutMenu } from '~/common/layout/store-applayout';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUICounter } from '~/common/state/store-ui';
import { useChatShowSystemMessages } from '../../store-app-chat';
@@ -30,6 +30,7 @@ export function ChatMenuItems(props: {
}) {
// external state
const { closeAppMenu } = useOptimaLayout();
const { touch: shareTouch } = useUICounter('export-share');
const [showSystemMessages, setShowSystemMessages] = useChatShowSystemMessages();
@@ -39,7 +40,7 @@ export function ChatMenuItems(props: {
const closeMenu = (event: React.MouseEvent) => {
event.stopPropagation();
closeLayoutMenu();
closeAppMenu();
};
const handleConversationClear = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -7,9 +7,9 @@ import SettingsIcon from '@mui/icons-material/Settings';
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
import { AppBarDropdown, DropdownItems } from '~/common/layout/AppBarDropdown';
import { GoodDropdown, DropdownItems } from '~/common/components/GoodDropdown';
import { KeyStroke } from '~/common/components/KeyStroke';
import { openLayoutLLMOptions, openLayoutModelsSetup } from '~/common/layout/store-applayout';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
function AppBarLLMDropdown(props: {
@@ -19,27 +19,42 @@ function AppBarLLMDropdown(props: {
placeholder?: string,
}) {
// external state
const { openLlmOptions, openModelsSetup } = useOptimaLayout();
// build model menu items, filtering-out hidden models, and add Source separators
const llmItems: DropdownItems = {};
let prevSourceId: DModelSourceId | null = null;
for (const llm of props.llms) {
if (!llm.hidden || llm.id === props.chatLlmId) {
if (!prevSourceId || llm.sId !== prevSourceId) {
if (prevSourceId)
llmItems[`sep-${llm.id}`] = { type: 'separator', title: llm.sId };
prevSourceId = llm.sId;
}
llmItems[llm.id] = { title: llm.label };
// filter-out hidden models
if (!(!llm.hidden || llm.id === props.chatLlmId))
continue;
// add separators when changing sources
if (!prevSourceId || llm.sId !== prevSourceId) {
if (prevSourceId)
llmItems[`sep-${llm.id}`] = {
type: 'separator',
title: llm.sId,
};
prevSourceId = llm.sId;
}
// add the model item
llmItems[llm.id] = {
title: llm.label,
// icon: llm.id.startsWith('some vendor') ? <VendorIcon /> : undefined,
};
}
const handleChatLLMChange = (_event: any, value: DLLMId | null) => value && props.setChatLlmId(value);
const handleOpenLLMOptions = () => props.chatLlmId && openLayoutLLMOptions(props.chatLlmId);
const handleOpenLLMOptions = () => props.chatLlmId && openLlmOptions(props.chatLlmId);
return (
<AppBarDropdown
<GoodDropdown
items={llmItems}
value={props.chatLlmId} onChange={handleChatLLMChange}
placeholder={props.placeholder || 'Models …'}
@@ -55,7 +70,7 @@ function AppBarLLMDropdown(props: {
</ListItemButton>
)}
<ListItemButton key='menu-llms' onClick={openLayoutModelsSetup}>
<ListItemButton key='menu-llms' onClick={openModelsSetup}>
<ListItemDecorator><BuildCircleIcon color='success' /></ListItemDecorator>
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
Models
@@ -6,8 +6,8 @@ import CallIcon from '@mui/icons-material/Call';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { AppBarDropdown } from '~/common/layout/AppBarDropdown';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { GoodDropdown } from '~/common/components/GoodDropdown';
import { launchAppCall } from '~/common/app.routes';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
@@ -42,7 +42,7 @@ function AppBarPersonaDropdown(props: {
}
return (
<AppBarDropdown
<GoodDropdown
items={SystemPurposes} showSymbols={zenMode !== 'cleaner'}
value={props.systemPurposeId} onChange={handleSystemPurposeChange}
appendOption={appendOption}
@@ -11,7 +11,7 @@ export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => vo
<FormatPaintIcon />
</IconButton>
) : (
<Button variant='soft' color='warning' onClick={props.onClick} endDecorator={<FormatPaintIcon />} sx={props.sx}>
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
Options
</Button>
);
@@ -5,7 +5,6 @@ import { Box, MenuItem, Radio, Typography } from '@mui/joy';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { KeyStroke } from '~/common/components/KeyStroke';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { ChatModeId } from '../../AppChat';
@@ -14,29 +13,25 @@ interface ChatModeDescription {
label: string;
description: string | React.JSX.Element;
shortcut?: string;
experimental?: boolean;
requiresTTI?: boolean;
}
const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
'immediate': {
'generate-text': {
label: 'Chat',
description: 'Persona replies',
},
'write-user': {
'append-user': {
label: 'Write',
description: 'Appends a message',
shortcut: 'Alt + Enter',
},
'draw-imagine': {
'generate-image': {
label: 'Draw',
description: 'AI Image Generation',
requiresTTI: true,
},
'draw-imagine-plus': {
label: 'Assisted Draw',
description: 'Assisted Image Generation',
experimental: true,
},
'react': {
'generate-react': {
label: 'Reason + Act · α',
description: 'Answers questions in multiple steps',
},
@@ -49,11 +44,14 @@ function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
return shortcut;
}
export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClose: () => void, chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void }) {
export function ChatModeMenu(props: {
anchorEl: HTMLAnchorElement | null, onClose: () => void,
chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void
capabilityHasTTI: boolean,
}) {
// external state
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
const labsMagicDraw = useUXLabsStore(state => state.labsMagicDraw);
return <CloseableMenu
placement='top-end' sx={{ minWidth: 320 }}
@@ -68,14 +66,13 @@ export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClos
{/* ChatMode items */}
{Object.entries(ChatModeItems)
.filter(([, { experimental }]) => labsMagicDraw || !experimental)
.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} />
<Box sx={{ flexGrow: 1 }}>
<Typography>{data.label}</Typography>
<Typography level='body-xs'>{data.description}</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)} />
+58 -31
View File
@@ -3,11 +3,13 @@ import { shallow } from 'zustand/shallow';
import { fileOpen, FileWithHandle } from 'browser-fs-access';
import { keyframes } from '@emotion/react';
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Typography } from '@mui/joy';
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Tooltip, Typography } from '@mui/joy';
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
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 PsychologyIcon from '@mui/icons-material/Psychology';
import SendIcon from '@mui/icons-material/Send';
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
@@ -24,12 +26,12 @@ import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { countModelTokens } from '~/common/util/token-counter';
import { launchAppCall } from '~/common/app.routes';
import { openLayoutPreferences } from '~/common/layout/store-applayout';
import { playSoundUrl } from '~/common/util/audioUtils';
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
import { useDebouncer } from '~/common/components/useDebouncer';
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
@@ -71,8 +73,10 @@ export function Composer(props: {
chatLLM: DLLM | null;
composerTextAreaRef: React.RefObject<HTMLTextAreaElement>;
conversationId: DConversationId | null;
capabilityHasT2I: boolean;
isDeveloperMode: boolean;
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
onTextImagine: (conversationId: DConversationId, text: string) => void;
sx?: SxProps;
}) {
@@ -85,11 +89,12 @@ export function Composer(props: {
// external state
const isMobile = useIsMobile();
const { openPreferences } = useOptimaLayout();
const { labsCalling, labsCameraDesktop } = useUXLabsStore(state => ({
labsCalling: state.labsCalling,
labsCameraDesktop: state.labsCameraDesktop,
}), shallow);
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('immediate');
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
const [startupText, setStartupText] = useComposerStartupText();
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
@@ -167,7 +172,7 @@ export function Composer(props: {
// Alt: append the message instead
if (e.altKey) {
handleSendAction('write-user', composeText);
handleSendAction('append-user', composeText);
return e.preventDefault();
}
@@ -189,7 +194,14 @@ export function Composer(props: {
const handleCallClicked = () => props.conversationId && systemPurposeId && launchAppCall(props.conversationId, systemPurposeId);
const handleDrawOptionsClicked = () => openLayoutPreferences(2);
const handleDrawOptionsClicked = () => openPreferences(2);
const handleTextImagineClicked = () => {
if (!composeText || !props.conversationId)
return;
props.onTextImagine(props.conversationId, composeText);
setComposeText('');
};
// Mode menu
@@ -331,7 +343,8 @@ export function Composer(props: {
const handleOverlayDragOver = React.useCallback((e: React.DragEvent) => {
eatDragEvent(e);
// e.dataTransfer.dropEffect = 'copy';
// this makes sure we don't "transfer" (or move) the attachment, but we tell the sender we'll copy it
e.dataTransfer.dropEffect = 'copy';
}, [eatDragEvent]);
const handleOverlayDrop = React.useCallback(async (event: React.DragEvent) => {
@@ -349,24 +362,23 @@ export function Composer(props: {
}, [attachAppendDataTransfer, eatDragEvent, setComposeText]);
const isImmediate = chatModeId === 'immediate';
const isWriteUser = chatModeId === 'write-user';
const isChat = isImmediate || isWriteUser;
const isReAct = chatModeId === 'react';
const isDraw = chatModeId === 'draw-imagine';
const isDrawPlus = chatModeId === 'draw-imagine-plus';
const buttonColor: ColorPaletteProp = isReAct ? 'success' : (isDraw || isDrawPlus) ? 'warning' : 'primary';
const isText = chatModeId === 'generate-text';
const isAppend = chatModeId === 'append-user';
const isChat = isText || isAppend;
const isReAct = chatModeId === 'generate-react';
const isDraw = chatModeId === 'generate-image';
const buttonColor: ColorPaletteProp = isReAct ? 'success' : isDraw ? 'warning' : 'primary';
const textPlaceholder: string =
isDrawPlus
? 'Write a subject, and we\'ll add detail...'
: isDraw
? 'Describe an idea or a drawing...'
: isReAct
? 'Multi-step reasoning question...'
: props.isDeveloperMode
? 'Chat with me · drop source files · attach code...'
: /*isProdiaConfigured ?*/ 'Chat · /react · /imagine · drop text files...' /*: 'Chat · /react · drop text files...'*/;
isDraw
? 'Describe an idea or a drawing...'
: isReAct
? 'Multi-step reasoning question...'
: props.isDeveloperMode
? 'Chat with me · drop source files · attach code...'
: props.capabilityHasT2I
? 'Chat · /react · /draw · drop text files...'
: 'Chat · /react · drop text files...';
return (
@@ -416,7 +428,7 @@ export function Composer(props: {
<Box sx={{
flexGrow: 1,
display: 'flex', flexDirection: 'column', gap: 1,
overflowX: 'clip',
minWidth: 250, // enable X-scrolling (resetting any possible minWidth due to the attachments)
}}>
{/* Edit box + Overlays + Mic buttons */}
@@ -426,7 +438,7 @@ export function Composer(props: {
<Box sx={{ position: 'relative' }}>
<Textarea
variant='outlined' color={(isDraw || isDrawPlus) ? 'warning' : isReAct ? 'success' : 'neutral'}
variant='outlined' color={isDraw ? 'warning' : isReAct ? 'success' : 'neutral'}
autoFocus
minRows={5} maxRows={10}
placeholder={textPlaceholder}
@@ -451,7 +463,6 @@ export function Composer(props: {
'&:focus-within': {
backgroundColor: 'background.popup',
},
// fontSize: '16px',
lineHeight: 1.75,
}} />
@@ -554,14 +565,14 @@ export function Composer(props: {
{/* [mobile] bottom-corner secondary button */}
{isMobile && (isChat
? <ButtonCall isMobile disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} sx={{ mr: { xs: 1, md: 2 } }} />
: (isDraw || isDrawPlus)
: isDraw
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
: <IconButton disabled variant='plain' color='neutral' sx={{ mr: { xs: 1, md: 2 } }} />
)}
{/* Responsive Send/Stop buttons */}
<ButtonGroup
variant={isWriteUser ? 'outlined' : 'solid'}
variant={isAppend ? 'outlined' : 'solid'}
color={buttonColor}
sx={{
flexGrow: 1,
@@ -573,10 +584,16 @@ export function Composer(props: {
key='composer-act'
fullWidth disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
onClick={handleSendClicked}
endDecorator={micContinuation ? <AutoModeIcon /> : isWriteUser ? <SendIcon sx={{ fontSize: 18 }} /> : isReAct ? <PsychologyIcon /> : <TelegramIcon />}
endDecorator={
micContinuation ? <AutoModeIcon /> :
isAppend ? <SendIcon sx={{ fontSize: 18 }} /> :
isReAct ? <PsychologyIcon /> :
isDraw ? <FormatPaintIcon />
: <TelegramIcon />
}
>
{micContinuation && 'Voice '}
{isWriteUser ? 'Write' : isReAct ? 'ReAct' : isDraw ? 'Draw' : isDrawPlus ? 'Draw+' : 'Chat'}
{isAppend ? 'Write' : isReAct ? 'ReAct' : isDraw ? 'Draw' : 'Chat'}
</Button>
) : (
<Button
@@ -589,7 +606,16 @@ export function Composer(props: {
Stop
</Button>
)}
<IconButton disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor} onClick={handleModeSelectorShow}>
{/* [Draw] Imagine */}
{isDraw && !!composeText && <Tooltip title='Imagine a drawing prompt'>
<IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleTextImagineClicked}>
<AutoAwesomeIcon />
</IconButton>
</Tooltip>}
{/* Mode expander */}
<IconButton variant={isDraw ? undefined : undefined} disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor} onClick={handleModeSelectorShow}>
<ExpandLessIcon />
</IconButton>
</ButtonGroup>
@@ -604,7 +630,7 @@ export function Composer(props: {
{isChat && <ButtonCall disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
{/* [desktop] Draw Options secondary button */}
{(isDraw || isDrawPlus) && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
</Box>}
@@ -617,6 +643,7 @@ export function Composer(props: {
<ChatModeMenu
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
capabilityHasTTI={props.capabilityHasT2I}
/>
)}
@@ -254,7 +254,7 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
case 'rich-text-table':
let mdTable: string;
try {
mdTable = htmlTableToMarkdown(input.altData!);
mdTable = htmlTableToMarkdown(input.altData!, false);
} catch (error) {
// fallback to text/plain
mdTable = inputDataToString(input.data);
@@ -14,7 +14,6 @@ import Face6Icon from '@mui/icons-material/Face6';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import PaletteOutlinedIcon from '@mui/icons-material/PaletteOutlined';
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
import ReplayIcon from '@mui/icons-material/Replay';
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
@@ -78,38 +77,42 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
case 'system':
return <SettingsSuggestIcon sx={iconSx} />; // https://em-content.zobj.net/thumbs/120/apple/325/robot_1f916.png
case 'user':
return <Face6Icon sx={iconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
case 'assistant':
// display a gif avatar when the assistant is typing (people seem to love this, so keeping it after april fools')
// typing gif (people seem to love this, so keeping it after april fools')
const isTextToImage = messageOriginLLM === 'DALL·E' || messageOriginLLM === 'Prodia';
const isReact = messageOriginLLM?.startsWith('react-');
if (messageTyping) {
return <Avatar
alt={messageSender} variant='plain'
src={messageOriginLLM === 'prodia'
? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
: messageOriginLLM?.startsWith('react-')
? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
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'}
sx={{ ...mascotSx, borderRadius: 'var(--joy-radius-sm)' }}
sx={{ ...mascotSx, borderRadius: 'sm' }}
/>;
}
// display the purpose symbol
if (messageOriginLLM === 'prodia')
return <PaletteOutlinedIcon sx={iconSx} />;
// text-to-image: icon
if (isTextToImage)
return <FormatPaintIcon sx={{
...iconSx,
animation: `${cssRainbowColorKeyframes} 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: `${iconSx.width}px`, lineHeight: `${iconSx.height}px`,
}}
>
{symbol}
</Box>;
if (symbol) return <Box sx={{
fontSize: '24px',
textAlign: 'center',
width: '100%', minWidth: `${iconSx.width}px`, lineHeight: `${iconSx.height}px`,
}}>
{symbol}
</Box>;
// default assistant avatar
return <SmartToyOutlinedIcon sx={iconSx} />; // https://mui.com/static/images/avatar/2.jpg
case 'user':
return <Face6Icon sx={iconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
}
return <Avatar alt={messageSender} />;
}
@@ -167,6 +170,8 @@ function explainErrorInMessage(text: string, isAssistant: boolean, modelId?: str
make sure the usage is under <Link noLinkStyle href='https://platform.openai.com/account/billing/limits' target='_blank'>the limits</Link>.
</>;
}
// else
// errorMessage = <>{text || 'Unknown error'}</>;
return { errorMessage, isAssistantError };
}
@@ -254,9 +259,9 @@ export function ChatMessage(props: {
const showAvatars = props.hideAvatars !== true && !cleanerLooks;
const textSel = selMenuText ? selMenuText : messageText;
const isSpecialProdia = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/imagine') || textSel.startsWith('/img');
const couldDiagram = textSel?.length >= 100 && !isSpecialProdia;
const couldImagine = textSel?.length >= 2 && !isSpecialProdia;
const isSpecialT2I = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/draw ') || textSel.startsWith('/imagine ') || textSel.startsWith('/img ');
const couldDiagram = textSel?.length >= 100 && !isSpecialT2I;
const couldImagine = textSel?.length >= 2 && !isSpecialT2I;
const couldSpeak = couldImagine;
@@ -439,7 +444,6 @@ export function ChatMessage(props: {
borderBottomColor: 'divider',
}),
...(ENABLE_COPY_MESSAGE_OVERLAY && { position: 'relative' }),
...(props.isBottom === true && { mb: 'auto' }),
'&:hover > button': { opacity: 1 },
...props.sx,
}}
@@ -520,7 +524,7 @@ export function ChatMessage(props: {
: block.type === 'code'
? <RenderCode key={'code-' + index} codeBlock={block} sx={codeSx} noCopyButton={props.diagramMode} />
: block.type === 'image'
? <RenderImage key={'image-' + index} imageBlock={block} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
? <RenderImage key={'image-' + index} imageBlock={block} isFirst={!index} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
: block.type === 'latex'
? <RenderLatex key={'latex-' + index} latexBlock={block} />
: block.type === 'diff'
@@ -615,7 +619,7 @@ export function ChatMessage(props: {
</MenuItem>}
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
Imagine
Draw ...
</MenuItem>}
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
@@ -13,7 +13,7 @@ import { makeAvatar, messageBackground } from './ChatMessage';
/**
* Header bar for controlling the operations during the Selection mode
*/
export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
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,
@@ -39,7 +39,7 @@ export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom:
*
* Shall look similarly to the main ChatMessage, for consistency, but just allow a simple checkbox selection
*/
export function CleanerMessage(props: { message: DMessage, isBottom: boolean, selected: boolean, remainingTokens?: number, onToggleSelected?: (messageId: string, selected: boolean) => void }) {
export function CleanerMessage(props: { message: DMessage, selected: boolean, remainingTokens?: number, onToggleSelected?: (messageId: string, selected: boolean) => void }) {
// derived state
const {
@@ -77,7 +77,6 @@ export function CleanerMessage(props: { message: DMessage, isBottom: boolean, se
borderBottom: '1px solid',
borderBottomColor: 'divider',
// position: 'relative',
...(props.isBottom && { mb: 'auto' }),
'&:hover > button': { opacity: 1 },
}}>
+108 -15
View File
@@ -1,8 +1,8 @@
import * as React from 'react';
import { Box, IconButton, Tooltip } from '@mui/joy';
import { Alert, Box, IconButton, Tooltip, Typography } from '@mui/joy';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import ReplayIcon from '@mui/icons-material/Replay';
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
import { Link } from '~/common/components/Link';
@@ -10,25 +10,116 @@ import { ImageBlock } from './blocks';
import { overlayButtonsSx } from './RenderCode';
export const RenderImage = (props: { imageBlock: ImageBlock, allowRunAgain: boolean, onRunAgain?: (e: React.MouseEvent) => void }) => {
const imageUrls = props.imageBlock.url.split('\n');
const mdImageReferenceRegex = /^!\[([^\]]*)]\(([^)]+)\)$/;
const imageExtensions = /\.(jpg|jpeg|png|gif|bmp|svg)/i;
return imageUrls.map((url, index) => (
<Box
/**
* Checks if the entire content consists solely of Markdown image references.
* If so, returns an array of ImageBlock objects for each image reference.
* If any non-image content is present or if there are no image references, returns null.
*/
export function heuristicMarkdownImageReferenceBlocks(fullText: string) {
// Check if all lines are valid Markdown image references with image URLs
const imageBlocks: ImageBlock[] = [];
for (const line of fullText.split('\n')) {
if (line.trim() === '') continue; // skip empty lines
const match = mdImageReferenceRegex.exec(line);
if (match && imageExtensions.test(match[2])) {
const alt = match[1];
const url = match[2];
imageBlocks.push({ type: 'image', url, alt });
} else {
// if there is any outlier line, return null
return null;
}
}
// Return the image blocks if all lines are image references with valid image URLs
return imageBlocks.length > 0 ? imageBlocks : null;
}
const prodiaUrlRegex = /^(https?:\/\/images\.prodia\.\S+)$/i;
/**
* Legacy heuristic for detecting images from "images.prodia." URLs.
*/
export function heuristicLegacyImageBlocks(fullText: string): ImageBlock[] | null {
// Check if all lines are URLs starting with "http://images.prodia." or "https://images.prodia."
const imageBlocks: ImageBlock[] = [];
for (const line of fullText.split('\n')) {
const match = prodiaUrlRegex.exec(line);
if (match) {
const url = match[1];
imageBlocks.push({ type: 'image', url });
} else {
// if there is any outlier line, return null
return null;
}
}
// Return the image blocks if all lines are URLs from "images.prodia."
return imageBlocks.length > 0 ? imageBlocks : null;
}
export const RenderImage = (props: { imageBlock: ImageBlock, isFirst: boolean, allowRunAgain: boolean, onRunAgain?: (e: React.MouseEvent) => void }) => {
const { url, alt } = props.imageBlock;
const imageUrls = url.split('\n');
return imageUrls.map((url, index) => {
// display a notice for temporary images DallE
const isTempDalleUrl = url.startsWith('https://oaidalle');
return <Box
key={'gen-img-' + index}
sx={{
display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', position: 'relative',
mx: 1.5, mt: index > 0 ? 1.5 : 0,
mx: 1.5, mb: 1.5, // mt: (index > 0 || !props.isFirst) ? 1.5 : 0,
// p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1,
minWidth: 64, minHeight: 64, boxShadow: 'lg',
minWidth: 128, minHeight: 128,
boxShadow: 'md',
backgroundColor: 'neutral.solidBg',
'& picture': { display: 'flex' },
'& img': { maxWidth: '100%', maxHeight: '100%' },
'&:hover > .overlay-buttons': { opacity: 1 },
}}>
}}
>
{/* External Image */}
<picture><img src={url} alt='Generated Image' /></picture>
{alt ? (
<Tooltip
variant='outlined' color='neutral'
title={
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{isTempDalleUrl && <Alert variant='soft' color='warning' sx={{ flexDirection: 'column', alignItems: 'start' }}>
<Typography level='title-sm'> Temporary Image</Typography>
<Typography level='body-sm'>
This image will be deleted from the OpenAI servers in one hour. <b>Please save it to your device</b>.
</Typography>
{/*<Typography level='body-xs'>*/}
{/* The following is the re-written DALL·E prompt that generated this image.*/}
{/*</Typography>*/}
</Alert>}
<Typography level='title-sm' sx={{ p: 2 }}>
{alt}
</Typography>
</Box>
}
placement='top-start'
sx={{
maxWidth: { sm: '90vw', md: '70vw' },
boxShadow: 'md',
}}
>
<picture><img src={url} alt={`Generated Image: ${alt}`} /></picture>
</Tooltip>
) : (
<picture><img src={url} alt='Generated Image' /></picture>
)}
{/* Image Buttons */}
<Box className='overlay-buttons' sx={{ ...overlayButtonsSx, pt: 0.5, px: 0.5, gap: 0.5 }}>
@@ -39,10 +130,12 @@ export const RenderImage = (props: { imageBlock: ImageBlock, allowRunAgain: bool
</IconButton>
</Tooltip>
)}
<IconButton component={Link} href={url} target='_blank' variant='solid' color='neutral'>
<ZoomOutMapIcon />
</IconButton>
<Tooltip title='Open in new tab'>
<IconButton component={Link} href={url} target='_blank' variant='solid' color='neutral'>
<OpenInNewIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
));
</Box>;
});
};
+12 -7
View File
@@ -1,12 +1,13 @@
import type { Diff as TextDiff } from '@sanity/diff-match-patch';
import { heuristicIsHtml } from './RenderHtml';
import { heuristicMarkdownImageReferenceBlocks, heuristicLegacyImageBlocks } from './RenderImage';
type Block = CodeBlock | DiffBlock | HtmlBlock | ImageBlock | LatexBlock | TextBlock;
export type CodeBlock = { type: 'code'; blockTitle: string; blockCode: string; complete: boolean; };
export type DiffBlock = { type: 'diff'; textDiffs: TextDiff[] };
export type HtmlBlock = { type: 'html'; html: string; };
export type ImageBlock = { type: 'image'; url: string; };
export type ImageBlock = { type: 'image'; url: string; alt?: string }; // Added optional alt property
export type LatexBlock = { type: 'latex'; latex: string; };
export type TextBlock = { type: 'text'; content: string; }; // for Text or Markdown
@@ -21,9 +22,18 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
if (heuristicIsHtml(text))
return [{ type: 'html', html: text }];
// special case: markdown image references (e.g. ![alt text](https://example.com/image.png))
const mdImageBlocks = heuristicMarkdownImageReferenceBlocks(text);
if (mdImageBlocks)
return mdImageBlocks;
// special case: legacy prodia images
const legacyImageBlocks = heuristicLegacyImageBlocks(text);
if (legacyImageBlocks)
return legacyImageBlocks;
const regexPatterns = {
codeBlock: /`{3,}([\w\\.+-_]+)?\n([\s\S]*?)(`{3,}\n?|$)/g,
imageBlock: /(https:\/\/images\.prodia\.xyz\/.*?\.png)/g, // NOTE: only Prodia for now - but this shall be expanded to markdown images ![alt](url) or any png/jpeg
latexBlock: /\$\$([\s\S]*?)\$\$/g,
// latexBlockOrInline: /\$\$([\s\S]*?)\$\$|\$([^$]*?)\$/g,
};
@@ -61,11 +71,6 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
blocks.push({ type: 'code', blockTitle, blockCode, complete: blockEnd.startsWith('```') });
break;
case 'imageBlock':
const url: string = match[1];
blocks.push({ type: 'image', url });
break;
case 'latexBlock':
const latex: string = match[1];
blocks.push({ type: 'latex', latex });
@@ -33,9 +33,9 @@ interface AppChatPanesStore {
openConversationInFocusedPane: (conversationId: DConversationId) => void;
openConversationInSplitPane: (conversationId: DConversationId) => void;
navigateHistoryInFocusedPane: (direction: 'back' | 'forward') => boolean;
setFocusedPaneIndex: (paneIndex: number) => void;
splitChatPane: (numberOfPanes: number) => void;
unsplitChatPane: (paneIndexToKeep: number) => void;
duplicatePane: (paneIndex: number) => void;
removePane: (paneIndex: number) => void;
setFocusedPane: (paneIndex: number) => void;
onConversationsChanged: (conversationIds: DConversationId[]) => void;
}
@@ -160,7 +160,52 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
return true;
},
setFocusedPaneIndex: (paneIndex: number) =>
duplicatePane: (paneIndex: number) =>
_set(state => {
const { chatPanes } = state;
// Validate index
if (paneIndex < 0 || paneIndex >= chatPanes.length) {
console.warn('Attempted to duplicate a pane with an out-of-range index:', paneIndex);
return state; // Return the existing state without changes
}
// Clone the pane at the specified index, including a deep copy of the history array
const paneToDuplicate = chatPanes[paneIndex];
const duplicatedPane = {
...paneToDuplicate,
history: [...paneToDuplicate.history], // Deep copy of the history array
};
// Insert the duplicated pane into the array, right after the original pane
const newPanes = [
...chatPanes.slice(0, paneIndex + 1),
duplicatedPane,
...chatPanes.slice(paneIndex + 1),
];
return {
chatPanes: newPanes,
chatPaneFocusIndex: paneIndex + 1,
};
}),
removePane: (paneIndex: number) =>
_set(state => {
const { chatPanes } = state;
if (paneIndex < 0 || paneIndex >= chatPanes.length)
return state;
const newPanes = chatPanes.toSpliced(paneIndex, 1);
// when a pane is removed, focus the pane 0, or null if no panes remain
return {
chatPanes: newPanes,
chatPaneFocusIndex: newPanes.length ? 0 : null,
};
}),
setFocusedPane: (paneIndex: number) =>
_set(state => {
if (state.chatPaneFocusIndex === paneIndex)
return state;
@@ -169,22 +214,6 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
};
}),
splitChatPane: (numberOfPanes: number) => {
const { chatPanes, chatPaneFocusIndex } = _get();
const focusedPane = (chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex] : null) ?? createPane();
_set({
chatPanes: Array.from({ length: numberOfPanes }, () => ({ ...focusedPane })),
chatPaneFocusIndex: 0,
});
},
unsplitChatPane: (paneIndexToKeep: number) =>
_set(state => ({
chatPanes: [state.chatPanes[paneIndexToKeep] || createPane()],
chatPaneFocusIndex: 0,
})),
/**
* This function is vital, as is invoked when the conversationId[] changes in the global chats store.
@@ -258,7 +287,9 @@ export function usePanesManager() {
onConversationsChanged,
openConversationInFocusedPane,
openConversationInSplitPane,
setFocusedPaneIndex,
duplicatePane,
removePane,
setFocusedPane,
} = state;
const focusedConversationId = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex]?.conversationId ?? null : null;
return {
@@ -268,7 +299,10 @@ export function usePanesManager() {
onConversationsChanged,
openConversationInFocusedPane,
openConversationInSplitPane,
setFocusedPaneIndex,
paneIndex: chatPaneFocusIndex,
duplicatePane,
removePane,
setFocusedPane,
};
}, shallow);
@@ -3,19 +3,20 @@ import { shallow } from 'zustand/shallow';
import { Box, Button, Checkbox, Grid, IconButton, Input, Stack, Textarea, Typography } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import ScienceIcon from '@mui/icons-material/Science';
import SearchIcon from '@mui/icons-material/Search';
import TelegramIcon from '@mui/icons-material/Telegram';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { Link } from '~/common/components/Link';
import { navigateToPersonas } from '~/common/app.routes';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { usePurposeStore } from './store-purposes';
// 'special' purpose IDs, for tile hiding purposes
const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__';
// Constants for tile sizes / grid width - breakpoints need to be computed here to work around
// the "flex box cannot shrink over wrapped content" issue
//
@@ -47,7 +48,6 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
// external state
const showFinder = useUIPreferencesStore(state => state.showPurposeFinder);
const labsPersonaYTCreator = useUXLabsStore(state => state.labsPersonaYTCreator);
const { systemPurposeId, setSystemPurposeId } = useChatStore(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
return {
@@ -113,6 +113,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
const unfilteredPurposeIDs = (filteredIDs && showFinder) ? filteredIDs : Object.keys(SystemPurposes);
const purposeIDs = editMode ? unfilteredPurposeIDs : unfilteredPurposeIDs.filter(id => !hiddenPurposeIDs.includes(id));
const hidePersonaCreator = hiddenPurposeIDs.includes(PURPOSE_ID_PERSONA_CREATOR);
const selectedPurpose = purposeIDs.length ? (SystemPurposes[systemPurposeId] ?? null) : null;
const selectedExample = selectedPurpose?.examples && getRandomElement(selectedPurpose.examples) || null;
@@ -156,10 +158,14 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
<Button
variant={(!editMode && systemPurposeId === spId) ? 'solid' : 'soft'}
color={(!editMode && systemPurposeId === spId) ? 'primary' : SystemPurposes[spId as SystemPurposeId]?.highlighted ? 'warning' : 'neutral'}
onClick={() => !editMode && handlePurposeChanged(spId as SystemPurposeId)}
onClick={() => editMode
? toggleHiddenPurposeId(spId)
: handlePurposeChanged(spId as SystemPurposeId)
}
sx={{
flexDirection: 'column',
fontWeight: 500,
// paddingInline: 1,
gap: bpTileGap,
height: bpTileSize,
width: bpTileSize,
@@ -171,9 +177,10 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
>
{editMode && (
<Checkbox
label={<Typography level='body-sm'>show</Typography>}
checked={!hiddenPurposeIDs.includes(spId)} onChange={() => toggleHiddenPurposeId(spId)}
sx={{ alignSelf: 'flex-start' }}
color='neutral'
checked={!hiddenPurposeIDs.includes(spId)}
// label={<Typography level='body-xs'>show</Typography>}
sx={{ position: 'absolute', left: 8, top: 8 }}
/>
)}
<div style={{ fontSize: '2rem' }}>
@@ -185,28 +192,43 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
</Button>
</Grid>
))}
{/* Button to start the YouTube persona creator */}
{labsPersonaYTCreator && <Grid>
{/* Button to start the Persona Creator */}
{(editMode || !hidePersonaCreator) && <Grid>
<Button
variant='soft' color='neutral'
component={Link} noLinkStyle href='/personas'
onClick={() => editMode
? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR)
: void navigateToPersonas()
}
sx={{
'--Icon-fontSize': '2rem',
flexDirection: 'column',
fontWeight: 500,
// gap: bpTileGap,
// paddingInline: 1,
gap: bpTileGap,
height: bpTileSize,
width: bpTileSize,
border: `1px dashed`,
boxShadow: 'md',
backgroundColor: 'background.surface',
// border: `1px dashed`,
// borderColor: 'neutral.softActiveBg',
boxShadow: 'xs',
backgroundColor: 'neutral.softDisabledBg',
}}
>
{editMode && (
<Checkbox
color='neutral'
checked={!hidePersonaCreator}
// label={<Typography level='body-xs'>show</Typography>}
sx={{ position: 'absolute', left: 8, top: 8 }}
/>
)}
<div>
<ScienceIcon />
<div style={{ fontSize: '2rem' }}>
🎭
</div>
{/*<SettingsAccessibilityIcon style={{ opacity: 0.5 }} />*/}
</div>
<div>
YouTube persona creator
<div style={{ textAlign: 'center' }}>
Persona Creator
</div>
</Button>
</Grid>}
@@ -0,0 +1,228 @@
/**
* Copyright (c) 2023-2024 Enrico Ros
*
* This subsystem is responsible for 'snap-to-bottom' and 'scroll-to-bottom' features,
* with an animated, gradual scroll.
*
* See the `ScrollToBottomButton` component for the button that triggers the scroll.
*
* Example usage:
* <ScrollToBottom bootToBottom stickToBottom sx={{ overflowY: 'auto', height: '100%' }}>
* <LongMessagesList />
* <ScrollToBottomButton />
* </ScrollToBottom>
*
* Within the Context (children components), functions are made available by using:
* const { notifyBooting, setStickToBottom } = useScrollToBottom();
*
*/
import * as React from 'react';
import { Box } from '@mui/joy';
import type { SxProps } from '@mui/joy/styles/types';
import { isBrowser } from '~/common/util/pwaUtils';
import { ScrollToBottomState, UseScrollToBottomProvider } from './useScrollToBottom';
// set this to true to debug this component
const DEBUG_SCROLL_TO_BOTTOM = false;
// NOTE: in Chrome a wheel scroll event is 100px
const USER_STICKY_MARGIN = 60;
// during the 'booting' timeout, scrolls happen instantly instead of smoothly
const BOOTING_TIMEOUT = 400;
function DebugBorderBox(props: { heightPx: number, color: string }) {
return (
<Box sx={{
position: 'absolute', bottom: 0, right: 0, left: 0,
height: `${props.heightPx}px`,
border: `1px solid ${props.color}`,
pointerEvents: 'none',
}} />
);
}
export function ScrollToBottom(props: {
bootToBottom?: boolean
stickToBottom?: boolean
sx?: SxProps
children: React.ReactNode,
}) {
// state
const [state, setState] = React.useState<ScrollToBottomState>({
stickToBottom: props.stickToBottom || false,
booting: props.bootToBottom || false,
atBottom: undefined,
});
// track scrollable (for events and to scroll it)
const scrollableElementRef = React.useRef<HTMLDivElement>(null);
// track programmatic scrolls
const isProgrammaticScroll = React.useRef(false);
// derived state
const bootToBottom = props.bootToBottom || false;
const scrollBehavior: ScrollBehavior = state.booting ? 'auto' : 'smooth';
// [Debugging]
if (DEBUG_SCROLL_TO_BOTTOM)
console.log('ScrollToBottom', { ...state });
// main programmatic scroll to bottom function
const doScrollToBottom = React.useCallback(() => {
const scrollable = scrollableElementRef.current;
if (scrollable) {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log(' -> doScrollToBottom()', { scrollHeight: scrollable.scrollHeight, offsetHeight: scrollable.offsetHeight });
// eat the next scroll event
isProgrammaticScroll.current = true;
// smooth scrolling only after booting
scrollable.scrollTo({ top: scrollable.scrollHeight, behavior: scrollBehavior });
}
}, [scrollBehavior]);
/**
* Booting state reset (after BOOTING_TIMEOUT ms)
* - the "Booting" window will scroll instantly instead of smoothly
*/
React.useEffect(() => {
if (!state.booting || !isBrowser) return;
const _clearBootingHandler = () => {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log(' -> booting done');
setState(state => ({ ...state, booting: false }));
if (bootToBottom)
doScrollToBottom();
};
// cancelable listener
const timeout = window.setTimeout(_clearBootingHandler, BOOTING_TIMEOUT);
return () => clearTimeout(timeout);
}, [bootToBottom, doScrollToBottom, state.booting]);
/**
* Children elements resize event listener
* - note that the 'scrollable' will likely have a fixed size, while its children are the ones who become scrollable
*/
React.useEffect(() => {
const scrollable = scrollableElementRef.current;
if (!scrollable) return;
const _containerResizeObserver = new ResizeObserver(entries => {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log(' -> scrollable children resized', entries.length);
if (entries.length > 0 && state.stickToBottom)
doScrollToBottom();
});
// cancelable observer of resize of scrollable's children elements
Array.from(scrollable.children).forEach(child => _containerResizeObserver.observe(child));
return () => _containerResizeObserver.disconnect();
}, [state.stickToBottom, doScrollToBottom]);
/**
* (User) Scroll events listener
* - will cancel any state.stickToBottom, if the user dragged the scroll bar
*/
React.useEffect(() => {
if (state.booting) return;
const scrollable = scrollableElementRef.current;
if (!scrollable) return;
const _scrollEventsListener = () => {
// ignore scroll events during programmatic scrolls
// NOTE: some will go through, but somewhat the framework is stable
if (isProgrammaticScroll.current) {
isProgrammaticScroll.current = false;
return;
}
// compute intersections
const atBottom = scrollable.scrollHeight - scrollable.scrollTop <= scrollable.offsetHeight + USER_STICKY_MARGIN;
// assume this is = to the user intention
const stickToBottom = atBottom;
// update state only if anything changed
setState(state => (state.stickToBottom !== stickToBottom || state.atBottom !== atBottom)
? ({ ...state, stickToBottom, atBottom })
: state,
);
};
// _scrollEventsListener(true);
// cancelable listener (user and programatic scroll events)
scrollable.addEventListener('scroll', _scrollEventsListener);
return () => scrollable.removeEventListener('scroll', _scrollEventsListener);
}, [state.booting]);
// actions for this context
const notifyBooting = React.useCallback(() => {
if (bootToBottom)
setState(state => state.booting ? state : ({ ...state, booting: true }));
}, [bootToBottom]);
/*const notifyContentUpdated = React.useCallback(() => {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log('-= notifyContentUpdated');
if (state.stickToBottom)
doScrollToBottom();
}, [doScrollToBottom, state.stickToBottom]);*/
const setStickToBottom = React.useCallback((stickToBottom: boolean) => {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log('-= setStickToBottom', stickToBottom);
setState(state => state.stickToBottom !== stickToBottom
? ({ ...state, stickToBottom })
: state,
);
if (stickToBottom)
doScrollToBottom();
}, [doScrollToBottom]);
return (
<UseScrollToBottomProvider value={{
...state,
notifyBooting,
setStickToBottom,
}}>
<Box ref={scrollableElementRef} sx={props.sx}>
{props.children}
{DEBUG_SCROLL_TO_BOTTOM && <DebugBorderBox heightPx={USER_STICKY_MARGIN} color='red' />}
{DEBUG_SCROLL_TO_BOTTOM && <DebugBorderBox heightPx={100} color='blue' />}
</Box>
</UseScrollToBottomProvider>
);
}
@@ -0,0 +1,56 @@
import * as React from 'react';
import { IconButton } from '@mui/joy';
import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown';
import { useScrollToBottom } from './useScrollToBottom';
export function ScrollToBottomButton() {
// state
const { atBottom, stickToBottom, setStickToBottom } = useScrollToBottom();
const handleStickToBottom = React.useCallback(() => {
setStickToBottom(true);
}, [setStickToBottom]);
// do not render the button at all if we're already snapping
if (atBottom || stickToBottom)
return null;
return (
// <Tooltip title={
// <Typography variant='solid' level='title-sm' sx={{ px: 1 }}>
// Scroll to bottom
// </Typography>
// }>
<IconButton
variant='outlined' color='neutral' size='md'
onClick={handleStickToBottom}
sx={{
// place this on the bottom-right corner (FAB-like)
position: 'absolute',
bottom: '2rem',
right: {
xs: '1rem',
md: '2rem',
},
// style it
backgroundColor: 'background.surface',
borderRadius: '50%',
boxShadow: 'md',
// fade it in when hovering
// transition: 'all 0.15s',
// '&:hover': {
// transform: 'scale(1.1)',
// },
}}
>
<KeyboardDoubleArrowDownIcon />
</IconButton>
// </Tooltip>
);
}
@@ -0,0 +1,34 @@
import * as React from 'react';
/**
* State is minimal - to keep state machinery stable and simple
*/
export interface ScrollToBottomState {
// config
stickToBottom: boolean;
// state
booting: boolean;
atBottom: boolean | undefined;
}
/**
* Actions are very simplified, for providing a minimal control surface from the outside
*/
export interface ScrollToBottomActions {
notifyBooting: () => void;
setStickToBottom: (stick: boolean) => void;
}
type ScrollToBottomContext = ScrollToBottomState & ScrollToBottomActions;
const UseScrollToBottom = React.createContext<ScrollToBottomContext | undefined>(undefined);
export const UseScrollToBottomProvider = UseScrollToBottom.Provider;
export const useScrollToBottom = (): ScrollToBottomContext => {
const context = React.useContext(UseScrollToBottom);
if (!context)
throw new Error('useScrollToBottom must be used within a ScrollToBottomProvider');
return context;
};
+2 -2
View File
@@ -2,8 +2,8 @@ import { DLLMId } from '~/modules/llms/store-llms';
import { SystemPurposeId } from '../../../data';
import { autoSuggestions } from '~/modules/aifn/autosuggestions/autoSuggestions';
import { autoTitle } from '~/modules/aifn/autotitle/autoTitle';
import { llmStreamingChatGenerate } from '~/modules/llms/llm.client';
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
import { streamChat } from '~/modules/llms/transports/streamChat';
import { DMessage, useChatStore } from '~/common/state/store-chats';
@@ -63,7 +63,7 @@ async function streamAssistantMessage(
const messages = history.map(({ role, text }) => ({ role, content: text }));
try {
await streamChat(llmId, messages, abortSignal,
await llmStreamingChatGenerate(llmId, messages, null, null, abortSignal,
(updatedMessage: Partial<DMessage>) => {
// update the message in the store (and thus schedule a re-render)
editMessage(updatedMessage);
+2 -2
View File
@@ -1,7 +1,7 @@
import { CmdRunBrowse } from '~/modules/browse/browse.client';
import { CmdRunProdia } from '~/modules/prodia/prodia.client';
import { CmdRunReact } from '~/modules/aifn/react/react';
import { CmdRunSearch } from '~/modules/google/search.client';
import { CmdRunT2I } from '~/modules/t2i/t2i.client';
import { Brand } from '~/common/app.config';
import { createDMessage, DMessage } from '~/common/state/store-chats';
@@ -10,7 +10,7 @@ export const CmdAddRoleMessage: string[] = ['/assistant', '/a', '/system', '/s']
export const CmdHelp: string[] = ['/help', '/h', '/?'];
export const commands = [...CmdRunBrowse, ...CmdRunProdia, ...CmdRunReact, ...CmdRunSearch, ...CmdAddRoleMessage, ...CmdHelp];
export const commands = [...CmdRunBrowse, ...CmdRunT2I, ...CmdRunReact, ...CmdRunSearch, ...CmdAddRoleMessage, ...CmdHelp];
export interface SentencePiece {
type: 'text' | 'cmd';
+1 -1
View File
@@ -4,7 +4,7 @@ import { SystemPurposeId, SystemPurposes } from '../../../data';
import { createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
export function createAssistantTypingMessage(conversationId: string, assistantLlmLabel: DLLMId | 'prodia' | 'react-...' | 'web', assistantPurposeId: SystemPurposeId | undefined, text: string): string {
export function createAssistantTypingMessage(conversationId: string, assistantLlmLabel: DLLMId | string /* 'DALL·E' | 'Prodia' | 'react-...' | 'web' */, assistantPurposeId: SystemPurposeId | undefined, text: string): string {
const assistantMessage: DMessage = createDMessage('assistant', text);
assistantMessage.typing = true;
assistantMessage.purposeId = assistantPurposeId;
+10 -8
View File
@@ -1,4 +1,4 @@
import { prodiaGenerateImage } from '~/modules/prodia/prodia.client';
import { getActiveTextToImageProviderOrThrow, t2iGenerateImageOrThrow } from '~/modules/t2i/t2i.client';
import { useChatStore } from '~/common/state/store-chats';
@@ -6,7 +6,7 @@ import { createAssistantTypingMessage } from './editors';
/**
* The main 'image generation' function - for now specialized to the 'imagine' command.
* Text to image, appended as an 'assistant' message
*/
export async function runImageGenerationUpdatingState(conversationId: string, imageText: string) {
@@ -17,21 +17,23 @@ export async function runImageGenerationUpdatingState(conversationId: string, im
imageText = imageText.replace(/x(\d+)$|\[(\d+)]$/, '').trim(); // Remove the "xN" or "[N]" part from the imageText
// create a blank and 'typing' message for the assistant
const assistantMessageId = createAssistantTypingMessage(conversationId, 'prodia', undefined,
const assistantMessageId = createAssistantTypingMessage(conversationId, '', undefined,
`Give me a few seconds while I draw ${imageText?.length > 20 ? 'that' : '"' + imageText + '"'}...`);
// reference the state editing functions
const { editMessage } = useChatStore.getState();
try {
const imageUrls = await prodiaGenerateImage(count, imageText);
// Concatenate all the resulting URLs and update the assistant message with these URLs
const allImageUrls = imageUrls.join('\n');
editMessage(conversationId, assistantMessageId, { text: allImageUrls, typing: false }, false);
const t2iProvider = getActiveTextToImageProviderOrThrow();
editMessage(conversationId, assistantMessageId, { originLLM: t2iProvider.painter }, false);
const imageUrls = await t2iGenerateImageOrThrow(t2iProvider, imageText, count);
editMessage(conversationId, assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
} catch (error: any) {
const errorMessage = error?.message || error?.toString() || 'Unknown error';
editMessage(conversationId, assistantMessageId, { text: `Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
if (assistantMessageId)
editMessage(conversationId, assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
}
}
+6 -5
View File
@@ -13,7 +13,8 @@ import { LogoProgress } from '~/common/components/LogoProgress';
import { apiAsyncNode } from '~/common/util/trpc.client';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { conversationTitle } from '~/common/state/store-chats';
import { useLayoutPluggable } from '~/common/layout/store-applayout';
import { themeBgAppDarker } from '~/common/app.theme';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { AppChatLinkDrawerItems } from './AppChatLinkDrawerItems';
import { AppChatLinkMenuItems } from './AppChatLinkMenuItems';
@@ -30,7 +31,7 @@ const Centerer = (props: { backgroundColor: string, children?: React.ReactNode }
</Box>;
const ShowLoading = () =>
<Centerer backgroundColor='background.level3'>
<Centerer backgroundColor={themeBgAppDarker}>
<LogoProgress showProgress={true} />
<Typography level='title-sm' sx={{ mt: 2 }}>
Loading Chat...
@@ -38,7 +39,7 @@ const ShowLoading = () =>
</Centerer>;
const ShowError = (props: { error: any }) =>
<Centerer backgroundColor='background.level2'>
<Centerer backgroundColor={themeBgAppDarker}>
<InlineError error={props.error} severity='warning' />
</Centerer>;
@@ -85,7 +86,7 @@ export function AppChatLink(props: { linkId: string }) {
const drawerItems = React.useMemo(() => <AppChatLinkDrawerItems />, []);
const menuItems = React.useMemo(() => <AppChatLinkMenuItems />, []);
useLayoutPluggable(null, hasLinkItems ? drawerItems : null, menuItems);
usePluggableOptimaLayout(hasLinkItems ? drawerItems : null, null, menuItems, 'AppChatLink');
const pageTitle = (data?.conversation && conversationTitle(data.conversation)) || 'Chat Link';
@@ -102,7 +103,7 @@ export function AppChatLink(props: { linkId: string }) {
? <ShowError error={error} />
: !!data?.conversation
? <ViewChatLink conversation={data.conversation} storedAt={data.storedAt} expiresAt={data.expiresAt} />
: <Centerer backgroundColor='background.level3' />}
: <Centerer backgroundColor={themeBgAppDarker} />}
</>;
}
+3 -2
View File
@@ -8,8 +8,8 @@ import { useChatLinkItems } from '~/modules/trade/store-module-trade';
import { Brand } from '~/common/app.config';
import { Link } from '~/common/components/Link';
import { closeLayoutDrawer } from '~/common/layout/store-applayout';
import { getChatLinkRelativePath, ROUTE_INDEX } from '~/common/app.routes';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
/**
@@ -19,6 +19,7 @@ import { getChatLinkRelativePath, ROUTE_INDEX } from '~/common/app.routes';
export function AppChatLinkDrawerItems() {
// external state
const { closeAppDrawer } = useOptimaLayout();
const chatLinkItems = useChatLinkItems()
.slice()
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
@@ -27,7 +28,7 @@ export function AppChatLinkDrawerItems() {
return <>
<MenuItem
onClick={closeLayoutDrawer}
onClick={closeAppDrawer}
component={Link} href={ROUTE_INDEX} noLinkStyle
>
<ListItemDecorator><ArrowBackIcon /></ListItemDecorator>
+4 -3
View File
@@ -9,7 +9,8 @@ import { useChatShowSystemMessages } from '../chat/store-app-chat';
import { Brand } from '~/common/app.config';
import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats';
import { navigateToChat } from '~/common/app.routes';
import { launchAppChat } from '~/common/app.routes';
import { themeBgAppDarker } from '~/common/app.theme';
import { useUIPreferencesStore } from '~/common/state/store-ui';
@@ -58,7 +59,7 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
const handleClone = async (canOverwrite: boolean) => {
setCloning(true);
const importedId = useChatStore.getState().importConversation({ ...props.conversation }, !canOverwrite);
await navigateToChat(importedId);
await launchAppChat(importedId);
setCloning(false);
};
@@ -67,7 +68,7 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
<Box sx={{
flexGrow: 1,
backgroundColor: 'background.level3',
backgroundColor: themeBgAppDarker,
display: 'flex', flexFlow: 'column nowrap', minHeight: 96, alignItems: 'center',
gap: { xs: 4, md: 5, xl: 6 },
px: { xs: 2 },
+5 -4
View File
@@ -10,6 +10,7 @@ import { GoodTooltip } from '~/common/components/GoodTooltip';
import { Link } from '~/common/components/Link';
import { ROUTE_INDEX } from '~/common/app.routes';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { themeBgApp } from '~/common/app.theme';
import { newsCallout, NewsItems } from './news.data';
@@ -43,7 +44,7 @@ export function AppNews() {
<Box sx={{
flexGrow: 1,
backgroundColor: 'background.level1',
backgroundColor: themeBgApp,
overflowY: 'auto',
display: 'flex', justifyContent: 'center',
p: { xs: 3, md: 6 },
@@ -78,14 +79,14 @@ export function AppNews() {
{!!news && <Container disableGutters maxWidth='sm'>
{news?.map((ni, idx) => {
const firstCard = idx === 0;
// const firstCard = idx === 0;
const hasCardAfter = news.length < NewsItems.length;
const showExpander = hasCardAfter && (idx === news.length - 1);
const addPadding = false; //!firstCard; // || showExpander;
return <Card key={'news-' + idx} sx={{ mb: 2, minHeight: 32 }}>
<CardContent sx={{ position: 'relative', pr: addPadding ? 4 : 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
<GoodTooltip title={ni.versionName || null} placement='top-start'>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 0 }}>
<GoodTooltip title={ni.versionName ? `${ni.versionName} ${ni.versionMoji || ''}` : null} placement='top-start'>
<Typography level='title-sm' component='div' sx={{ flexGrow: 1 }}>
{ni.text ? ni.text : ni.versionName ? `${ni.versionCode} · ${ni.versionName}` : `Version ${ni.versionCode}:`}
</Typography>
+42 -14
View File
@@ -10,10 +10,10 @@ import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
// update this variable every time you want to broadcast a new version to clients
export const incrementalVersion: number = 8;
export const incrementalVersion: number = 10;
const B = (props: { href?: string, children: React.ReactNode }) => {
const boldText = <Typography color={!!props.href ? 'primary' : 'warning'} sx={{ fontWeight: 600 }}>{props.children}</Typography>;
const boldText = <Typography color={!!props.href ? 'primary' : 'neutral'} sx={{ fontWeight: 600 }}>{props.children}</Typography>;
return props.href ?
<Link href={props.href + clientUtmSource()} target='_blank' sx={{ /*textDecoration: 'underline'*/ }}>{boldText} <LaunchIcon sx={{ ml: 1 }} /></Link> :
boldText;
@@ -27,11 +27,12 @@ const RIssues = `${OpenRepo}/issues`;
export const newsCallout =
<Card>
<CardContent sx={{ gap: 2 }}>
<Typography level='h4'>
<Typography level='title-lg'>
Open Roadmap
</Typography>
<Typography>
The roadmap is officially out. For the first time you get a look at what&apos;s brewing, up and coming, and get a chance to pick up cool features!
<Typography level='body-md'>
Take a peek at our roadmap to see what&apos;s in the pipeline.
Discover upcoming features and let us know what excites you the most!
</Typography>
<Grid container spacing={1}>
<Grid xs={12} sm={7}>
@@ -39,7 +40,7 @@ export const newsCallout =
fullWidth variant='soft' color='primary' endDecorator={<LaunchIcon />}
component={Link} href={OpenProject} noLinkStyle target='_blank'
>
Explore the Roadmap
Explore
</Button>
</Grid>
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
@@ -57,19 +58,44 @@ export const newsCallout =
// news and feature surfaces
export const NewsItems: NewsItem[] = [
/*{
// https://github.com/enricoros/big-agi/milestone/7
// https://github.com/users/enricoros/projects/4/views/2
versionName: '1.7.0',
// still unannounced: phone calls, split windows, ...
{
versionCode: '1.9.0',
versionName: 'Creative Horizons',
versionMoji: '🎨🌌',
versionDate: new Date('2023-12-28T22:30:00Z'),
items: [
// multi-window support
// phone calls
{ text: <><B href={RIssues + '/212'}>DALL·E 3</B> support (/draw), with advanced control</>, issue: 212 },
{ text: <><B href={RIssues + '/304'}>Perfect scrolling</B> UX, on all devices</>, issue: 304 },
{ text: <>Create personas <B href={RIssues + '/287'}>from text</B></>, issue: 287 },
{ text: <>Openrouter: auto-detect models, support free-tiers and rates</>, issue: 291 },
{ text: <>Image drawing: unified UX, including auto-prompting</> },
{ text: <>Fix layout on Firefox</>, issue: 255 },
{ text: <>Developers: new Text2Image subsystem, Optima layout subsystem, ScrollToBottom library, using new Panes library, improved Llms subsystem</>, dev: true },
],
},*/
},
{
versionCode: '1.8.0',
versionName: 'To The Moon And Back',
// versionMoji: '🚀🌕🔙❤️',
versionDate: new Date('2023-12-20T09:30:00Z'),
items: [
{ text: <><B href={RIssues + '/275'}>Google Gemini</B> models support</> },
{ text: <><B href={RIssues + '/273'}>Mistral Platform</B> support</> },
{ text: <><B href={RIssues + '/270'}>Ollama chats</B> perfection</> },
{ text: <>Custom <B href={RIssues + '/280'}>diagrams instructions</B> (@joriskalz)</> },
{ text: <><B>Single-Tab</B> mode, enhances data integrity and prevents DB corruption</> },
{ text: <>Updated Ollama (v0.1.17) and OpenRouter models</> },
{ text: <>More: fixed shortcuts on Mac</> },
{ text: <><Link href='https://big-agi.com'>Website</Link>: official downloads</> },
{ text: <>Easier Vercel deployment, documented <Link href='https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483'>network troubleshooting</Link></>, dev: true },
],
},
{
versionCode: '1.7.0',
versionName: 'Attachment Theory',
versionDate: new Date('2023-12-10T12:00:00Z'), // new Date().toISOString()
// versionDate: new Date('2023-12-11T06:00:00Z'), // 1.7.3
versionDate: new Date('2023-12-10T12:00:00Z'), // 1.7.0
items: [
{ text: <>Redesigned <B href={RIssues + '/251'}>attachments system</B>: drag, paste, link, snap, images, text, pdfs</> },
{ text: <>Desktop <B href={RIssues + '/253'}>webcam access</B> for direct image capture (Labs option)</> },
@@ -158,10 +184,12 @@ export const NewsItems: NewsItem[] = [
interface NewsItem {
versionCode: string;
versionName?: string;
versionMoji?: string;
versionDate?: Date;
text?: string | React.JSX.Element;
items?: {
text: string | React.JSX.Element;
dev?: boolean;
issue?: number;
}[];
}
+5 -10
View File
@@ -1,25 +1,20 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useRouter } from 'next/router';
import { navigateToNews } from '~/common/app.routes';
import { useAppStateStore } from '~/common/state/store-appstate';
import { incrementalVersion } from './news.data';
export function useShowNewsOnUpdate() {
const { push: routerPush } = useRouter();
const { usageCount, lastSeenNewsVersion } = useAppStateStore(state => ({
usageCount: state.usageCount,
lastSeenNewsVersion: state.lastSeenNewsVersion,
}), shallow);
export function useRedirectToNewsOnUpdates() {
React.useEffect(() => {
const { usageCount, lastSeenNewsVersion } = useAppStateStore.getState();
const isNewsOutdated = (lastSeenNewsVersion || 0) < incrementalVersion;
if (isNewsOutdated && usageCount > 2) {
// Disable for now
void routerPush('/news');
void navigateToNews();
}
}, [lastSeenNewsVersion, routerPush, usageCount]);
}, []);
}
export function useMarkNewsAsSeen() {
+7 -13
View File
@@ -1,9 +1,10 @@
import * as React from 'react';
import { Box, Container, ListDivider, Sheet, Typography } from '@mui/joy';
import { Container, ListDivider, Sheet, Typography } from '@mui/joy';
import { YTPersonaCreator } from './YTPersonaCreator';
import ScienceIcon from '@mui/icons-material/Science';
import { themeBgApp } from '~/common/app.theme';
import { PersonaCreator } from './PersonaCreator';
export function AppPersonas() {
@@ -11,26 +12,19 @@ export function AppPersonas() {
<Sheet sx={{
flexGrow: 1,
overflowY: 'auto',
backgroundColor: 'background.level1',
backgroundColor: themeBgApp,
p: { xs: 3, md: 6 },
}}>
<Container disableGutters maxWidth='md' sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography level='title-lg' sx={{ textAlign: 'center' }}>
Advanced AI Personas
AI Personas Creator
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
<Typography>
Experimental
</Typography>
<ScienceIcon color='primary' />
</Box>
<ListDivider sx={{ my: 2 }} />
<YTPersonaCreator />
<PersonaCreator />
</Container>
@@ -1,11 +1,13 @@
import * as React from 'react';
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, IconButton, Input, LinearProgress, Tooltip, Typography } from '@mui/joy';
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, Input, LinearProgress, Tab, TabList, TabPanel, Tabs, Textarea, Typography } from '@mui/joy';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import WhatshotIcon from '@mui/icons-material/Whatshot';
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
import TextFieldsIcon from '@mui/icons-material/TextFields';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { GoodModal } from '~/common/components/GoodModal';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { apiQuery } from '~/common/util/trpc.client';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
@@ -37,9 +39,9 @@ function useTranscriptFromVideo(videoID: string | null) {
}
const YouTubePersonaSteps: LLMChainStep[] = [
const PersonaCreationSteps: LLMChainStep[] = [
{
name: 'Analyzing the transcript',
name: 'Analyzing the transcript / text',
setSystem: 'You are skilled in analyzing and embodying diverse characters. You meticulously study transcripts to capture key attributes, draft comprehensive character sheets, and refine them for authenticity. Feel free to make assumptions without hedging, be concise and be creative.',
addUserInput: true,
addUser: 'Conduct comprehensive research on the provided transcript. Identify key characteristics of the speaker, including age, professional field, distinct personality traits, style of communication, narrative context, and self-awareness. Additionally, consider any unique aspects such as their use of humor, their cultural background, core values, passions, fears, personal history, and social interactions. Your output for this stage is an in-depth written analysis that exhibits an understanding of both the superficial and more profound aspects of the speaker\'s persona.',
@@ -62,23 +64,34 @@ const YouTubePersonaSteps: LLMChainStep[] = [
];
export function YTPersonaCreator() {
export function PersonaCreator() {
// state
const [videoURL, setVideoURL] = React.useState('');
const [videoID, setVideoID] = React.useState('');
const [personaTranscript, setPersonaTranscript] = React.useState<string | null>(null);
const [personaText, setPersonaText] = React.useState('');
const [selectedTab, setSelectedTab] = React.useState(0);
// external state
const [diagramLlm, llmComponent] = useFormRadioLlmType();
const [personaLlm, llmComponent] = useFormRadioLlmType('Persona Creation Model');
// fetch transcript when the Video ID is ready, then store it
const { transcript, thumbnailUrl, title, isFetching, isError, error: transcriptError } =
useTranscriptFromVideo(videoID);
React.useEffect(() => setPersonaTranscript(transcript), [transcript]);
// Reset the relevant state when the selected tab changes
React.useEffect(() => {
// reset state
setVideoURL('');
setVideoID('');
setPersonaTranscript(null);
setPersonaText('');
}, [selectedTab]);
// use the transformation sequence to create a persona
const { isFinished, isTransforming, chainProgress, chainIntermediates, chainStepName, chainOutput, chainError, abortChain } =
useLLMChain(YouTubePersonaSteps, diagramLlm?.id, personaTranscript ?? undefined);
useLLMChain(PersonaCreationSteps, personaLlm?.id, personaTranscript ?? undefined);
const handleVideoIdChange = (e: React.ChangeEvent<HTMLInputElement>) => setVideoURL(e.target.value);
@@ -93,61 +106,89 @@ export function YTPersonaCreator() {
}
};
// New handler for persona text change
const handlePersonaTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPersonaText(e.target.value);
};
return <>
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 1 }}>
<YouTubeIcon sx={{ color: '#f00' }} />
<Typography level='title-lg'>
YouTube -&gt; AI persona
</Typography>
</Box>
<Typography level='title-sm' mb={3}>
Create the <em>System Prompt</em> of an AI Persona from YouTube or Text.
</Typography>
<form onSubmit={handleFetchTranscript}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
<Input
required
type='url'
fullWidth
<Tabs defaultValue={0} variant='outlined'
value={selectedTab}
onChange={(event, newValue) => setSelectedTab(newValue as number)}>
<TabList sx={{ minHeight: 48 }}>
<Tab>From YouTube Video</Tab>
<Tab>From Text</Tab>
</TabList>
{/* YouTube URL inputs */}
<TabPanel value={0} sx={{ p: 3 }}>
<Typography level='title-md' startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />} sx={{ mb: 3 }}>
YouTube -&gt; Persona
</Typography>
<form onSubmit={handleFetchTranscript}>
<Input
required
type='url'
fullWidth
variant='outlined'
placeholder='YouTube Video URL'
value={videoURL}
onChange={handleVideoIdChange}
sx={{ mb: 1.5 }}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button type='submit' variant='solid' disabled={isFetching || isTransforming || !videoURL} loading={isFetching} sx={{ minWidth: 140 }}>
Create
</Button>
<GoodTooltip title='This example comes from the popular Fireship YouTube channel, which presents technical topics with irreverent humor.'>
<Button variant='outlined' color='neutral' onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}>
Example
</Button>
</GoodTooltip>
</Box>
</form>
</TabPanel>
{/* Text area for users to paste copied text */}
<TabPanel value={1} sx={{ p: 3 }}>
<Typography level='title-md' startDecorator={<TextFieldsIcon />} sx={{ mb: 3 }}>
<b>Text</b> -&gt; Persona
</Typography>
<Textarea
variant='outlined'
placeholder='YouTube Video URL'
value={videoURL} onChange={handleVideoIdChange}
endDecorator={
<IconButton
variant='outlined' color='neutral'
onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}
>
<WhatshotIcon />
</IconButton>
}
minRows={4} maxRows={8}
placeholder='Paste your text here...'
value={personaText}
onChange={handlePersonaTextChange}
sx={{
backgroundColor: 'background.level1',
'&:focus-within': {
backgroundColor: 'background.popup',
},
lineHeight: 1.75,
mb: 1.5,
}}
/>
<Button
type='submit'
variant='solid' disabled={isFetching || isTransforming} loading={isFetching}
sx={{ minWidth: 120 }}>
Create
</Button>
</Box>
</form>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button variant='solid' disabled={isFetching || isTransforming || !personaText} onClick={() => setPersonaTranscript(personaText)} sx={{ minWidth: 140 }}>
Create
</Button>
{!!personaText?.length && <Typography level='body-sm'>{personaText.length.toLocaleString()}</Typography>}
</Box>
</TabPanel>
</Tabs>
{/* LLM selector (chat vs fast) */}
{!isTransforming && !isFinished && llmComponent}
{/* 1. Transcript*/}
{personaTranscript && (
<Card sx={{ mt: 2, boxShadow: 'md' }}>
<CardContent>
<Typography level='title-md' sx={{ mb: 1 }}>
{title || 'Transcript'}
</Typography>
<Box>
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Image' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
<Typography level='body-sm'>
{personaTranscript.slice(0, 280)}...
</Typography>
</Box>
</CardContent>
</Card>
)}
{!isTransforming && !isFinished && <Box sx={{ mt: 3 }}>{llmComponent}</Box>}
{/* Errors */}
{isError && (
@@ -161,49 +202,64 @@ export function YTPersonaCreator() {
</Alert>
)}
{/* Persona! */}
{chainOutput && <Box sx={{ mt: 2 }}>
<Typography level='title-lg'>
YouTuber Persona System Prompt
</Typography>
<Card sx={{ boxShadow: 'md' }}>
<CardContent sx={{
position: 'relative',
'&:hover > button': { opacity: 1 },
}}>
{chainOutput && <>
<Card sx={{ boxShadow: 'md', mt: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography level='title-lg' color='success' startDecorator={<SettingsAccessibilityIcon color='success' />}>
Persona Prompt
</Typography>
<GoodTooltip title='Copy system prompt'>
<Button color='success' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')} endDecorator={<ContentCopyIcon />} sx={{ minWidth: 120 }}>
Copy
</Button>
</GoodTooltip>
</Box>
<CardContent>
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
You can now copy the following text and use it as Custom prompt!
You may now copy the text below and use it as Custom prompt!
</Alert>
<Tooltip title='Copy system prompt' variant='solid'>
<IconButton
variant='outlined' color='neutral' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')}
sx={{
position: 'absolute', right: 0, zIndex: 10,
// opacity: 0, transition: 'opacity 0.3s',
}}>
<ContentCopyIcon />
</IconButton>
</Tooltip>
<Typography level='body-sm'>
<Typography level='title-sm' sx={{ lineHeight: 1.75 }}>
{chainOutput}
</Typography>
</CardContent>
</Card>
</Box>}
</>}
{/* Input: Transcript*/}
{personaTranscript && <>
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
Input Data
</Typography>
<Card>
<CardContent>
<Typography level='title-md' sx={{ mb: 1 }}>
{title || 'Transcript / Text'}
</Typography>
<Box>
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Thumbnail' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
<Typography level='body-sm'>
{personaTranscript.slice(0, 280)}...
</Typography>
</Box>
</CardContent>
</Card>
</>}
{/* Intermediate outputs rendered as cards in a grid */}
{chainIntermediates && chainIntermediates.length > 0 && <Box sx={{ mt: 2 }}>
<Typography level='title-lg'>
{chainIntermediates && chainIntermediates.length > 0 && <>
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
{isTransforming ? 'Working...' : 'Intermediate Work'}
</Typography>
<Grid container spacing={2}>
{chainIntermediates.map((intermediate, i) =>
<Grid xs={12} sm={6} md={4} key={i}>
<Card>
<Card sx={{ height: '100%' }}>
<CardContent>
<Typography level='title-sm' sx={{ mb: 1 }}>
{i + 1}. {YouTubePersonaSteps[i].name}
{i + 1}. {PersonaCreationSteps[i].name}
</Typography>
<Typography level='body-sm'>
{intermediate?.slice(0, 140)}...
@@ -213,27 +269,35 @@ export function YTPersonaCreator() {
</Grid>,
)}
</Grid>
</Box>}
</>}
{/* Embodiment Progress */}
{/* Dialog: Embodiment Progress */}
{isTransforming && <GoodModal open>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 2 }}>
<CircularProgress color='primary' value={Math.max(10, 100 * chainProgress)} />
</Box>
<Typography color='success' level='title-lg' sx={{ mt: 1 }}>
Embodying Persona ...
</Typography>
<Typography color='success' level='title-sm' sx={{ mt: 1, fontWeight: 600 }}>
{chainStepName}
</Typography>
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1, mb: 2 }} />
<Box>
<Typography color='success' level='title-lg'>
Embodying Persona ...
</Typography>
<Typography level='title-sm' sx={{ mt: 1 }}>
Using: {personaLlm?.label}
</Typography>
</Box>
<Box>
<Typography color='success' level='title-sm' sx={{ fontWeight: 600 }}>
{chainStepName}
</Typography>
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1.5 }} />
</Box>
<Typography level='title-sm'>
This may take 1-2 minutes. Do not close this window or the progress will be lost.
If you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
While larger models will produce higher quality prompts,
if you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
please try again with faster/smaller models.
</Typography>
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 5 }}>
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 3 }}>
Cancel
</Button>
</GoodModal>}
+2 -2
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { DLLMId, useModelsStore } from '~/modules/llms/store-llms';
import { callChatGenerate, VChatMessageIn } from '~/modules/llms/transports/chatGenerate';
import { llmChatGenerateOrThrow, VChatMessageIn } from '~/modules/llms/llm.client';
export interface LLMChainStep {
@@ -80,7 +80,7 @@ export function useLLMChain(steps: LLMChainStep[], llmId: DLLMId | undefined, ch
_chainAbortController.signal.addEventListener('abort', globalToStepListener);
// LLM call
callChatGenerate(llmId, llmChatInput, chain.overrideResponseTokens)
llmChatGenerateOrThrow(llmId, llmChatInput, null, null, chain.overrideResponseTokens)
.then(({ content }) => {
stepDone = true;
if (!stepAbortController.signal.aborted)
+10 -5
View File
@@ -9,8 +9,8 @@ import WidthWideIcon from '@mui/icons-material/WidthWide';
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
import { FormRadioControl } from '~/common/components/forms/FormRadioControl';
import { isPwa } from '~/common/util/pwaUtils';
import { openLayoutModelsSetup } from '~/common/layout/store-applayout';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUIPreferencesStore } from '~/common/state/store-ui';
@@ -18,10 +18,14 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
const SHOW_PURPOSE_FINDER = false;
const ModelOptionsButton = () =>
<Button
const ModelsSetupButton = () => {
// external state
const { openModelsSetup } = useOptimaLayout();
return <Button
// variant='soft' color='success'
onClick={openLayoutModelsSetup}
onClick={openModelsSetup}
startDecorator={<BuildCircleIcon />}
sx={{
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
@@ -29,6 +33,7 @@ const ModelOptionsButton = () =>
>
Models
</Button>;
};
export function AppChatSettingsUI() {
@@ -64,7 +69,7 @@ export function AppChatSettingsUI() {
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<FormLabelStart title='AI Models'
description='Setup' />
<ModelOptionsButton />
<ModelsSetupButton />
</FormControl>
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between' }}>
+23 -12
View File
@@ -6,12 +6,13 @@ import ScienceIcon from '@mui/icons-material/Science';
import SearchIcon from '@mui/icons-material/Search';
import { BrowseSettings } from '~/modules/browse/BrowseSettings';
import { DallESettings } from '~/modules/t2i/dalle/DallESettings';
import { ElevenlabsSettings } from '~/modules/elevenlabs/ElevenlabsSettings';
import { GoogleSearchSettings } from '~/modules/google/GoogleSearchSettings';
import { ProdiaSettings } from '~/modules/prodia/ProdiaSettings';
import { ProdiaSettings } from '~/modules/t2i/prodia/ProdiaSettings';
import { T2ISettings } from '~/modules/t2i/T2ISettings';
import { GoodModal } from '~/common/components/GoodModal';
import { closeLayoutPreferences, openLayoutShortcuts, useLayoutPreferencesTab } from '~/common/layout/store-applayout';
import { settingsGap } from '~/common/app.theme';
import { useIsMobile } from '~/common/components/useMatchMedia';
@@ -100,20 +101,24 @@ function Topic(props: { title?: string, icon?: string | React.ReactNode, startCo
* Component that allows the User to modify the application settings,
* persisted on the client via localStorage.
*/
export function SettingsModal() {
export function SettingsModal(props: {
open: boolean,
tabIndex: number,
onClose: () => void,
onOpenShortcuts: () => void,
}) {
// external state
const isMobile = useIsMobile();
const settingsTabIndex = useLayoutPreferencesTab();
const tabFixSx = { fontFamily: 'body', flex: 1, p: 0, m: 0 };
return (
<GoodModal
title='Preferences' strongerTitle
open={!!settingsTabIndex} onClose={closeLayoutPreferences}
open={props.open} onClose={props.onClose}
startButton={isMobile ? undefined : (
<Button variant='soft' onClick={openLayoutShortcuts}>
<Button variant='soft' onClick={props.onOpenShortcuts}>
👉 See Shortcuts
</Button>
)}
@@ -124,7 +129,7 @@ export function SettingsModal() {
<Divider />
<Tabs aria-label='Settings tabbed menu' defaultValue={settingsTabIndex}>
<Tabs aria-label='Settings tabbed menu' defaultValue={props.tabIndex}>
<TabList
variant='soft'
disableUnderline
@@ -151,7 +156,7 @@ export function SettingsModal() {
<Tab disableIndicator value={4} sx={tabFixSx}>Tools</Tab>
</TabList>
<TabPanel value={1} sx={{ p: 'var(--Tabs-gap)' }}>
<TabPanel value={1} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
<Topics>
<Topic>
<AppChatSettingsUI />
@@ -165,7 +170,7 @@ export function SettingsModal() {
</Topics>
</TabPanel>
<TabPanel value={3} sx={{ p: 'var(--Tabs-gap)' }}>
<TabPanel value={3} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
<Topics>
<Topic icon='🎙️' title='Voice settings'>
<VoiceSettings />
@@ -176,15 +181,21 @@ export function SettingsModal() {
</Topics>
</TabPanel>
<TabPanel value={2} sx={{ p: 'var(--Tabs-gap)' }}>
<TabPanel value={2} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
<Topics>
<Topic icon='🖍️' title='Prodia API'>
<Topic>
<T2ISettings />
</Topic>
<Topic icon='🖍️' title='OpenAI DALL·E' startCollapsed>
<DallESettings />
</Topic>
<Topic icon='🖍️' title='Prodia API' startCollapsed>
<ProdiaSettings />
</Topic>
</Topics>
</TabPanel>
<TabPanel value={4} sx={{ p: 'var(--Tabs-gap)' }}>
<TabPanel value={4} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
<Topics>
<Topic icon={<SearchIcon />} title='Browsing' startCollapsed>
<BrowseSettings />
+2 -11
View File
@@ -3,7 +3,6 @@ import * as React from 'react';
import { ChatMessage } from '../chat/components/message/ChatMessage';
import { GoodModal } from '~/common/components/GoodModal';
import { closeLayoutShortcuts, useLayoutShortcuts } from '~/common/layout/store-applayout';
import { createDMessage } from '~/common/state/store-chats';
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
@@ -36,17 +35,9 @@ const shortcutsMd = `
const shortcutsMessage = createDMessage('assistant', platformAwareKeystrokes(shortcutsMd));
export function ShortcutsModal() {
// external state
const showShortcuts = useLayoutShortcuts();
export function ShortcutsModal(props: { onClose: () => void }) {
return (
<GoodModal
open={showShortcuts}
title='Desktop Shortcuts'
onClose={closeLayoutShortcuts}
>
<GoodModal open title='Desktop Shortcuts' onClose={props.onClose}>
<ChatMessage message={shortcutsMessage} hideAvatars noBottomBorder sx={{ p: 0, m: 0 }} />
</GoodModal>
);
+3 -15
View File
@@ -3,9 +3,7 @@ import * as React from 'react';
import { FormControl, Typography } from '@mui/joy';
import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
import CallIcon from '@mui/icons-material/Call';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
@@ -19,22 +17,12 @@ export function UxLabsSettings() {
// external state
const isMobile = useIsMobile();
const {
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsMagicDraw, labsPersonaYTCreator, labsSplitBranching,
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsMagicDraw, setLabsPersonaYTCreator, setLabsSplitBranching,
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsSplitBranching,
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsSplitBranching,
} = useUXLabsStore();
return <>
<FormSwitchControl
title={<><YouTubeIcon color={labsPersonaYTCreator ? 'primary' : undefined} sx={{ mr: 0.25 }} /> YouTube Personas</>} description={labsPersonaYTCreator ? 'Creator Enabled' : 'Disabled'}
checked={labsPersonaYTCreator} onChange={setLabsPersonaYTCreator}
/>
<FormSwitchControl
title={<><FormatPaintIcon color={labsMagicDraw ? 'primary' : undefined} sx={{ mr: 0.25 }} />Assisted Draw</>} description={labsMagicDraw ? 'Enabled' : 'Disabled'}
checked={labsMagicDraw} onChange={setLabsMagicDraw}
/>
<FormSwitchControl
title={<><CallIcon color={labsCalling ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Voice Calls</>} description={labsCalling ? 'Call AGI' : 'Disabled'}
checked={labsCalling} onChange={setLabsCalling}
@@ -58,7 +46,7 @@ export function UxLabsSettings() {
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<FormLabelStart title='Graduated' />
<Typography level='body-xs'>
<Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Relative chat size · Text Tools · LLM Overheat
<Link href='https://github.com/enricoros/big-AGI/issues/282' target='_blank'>Persona Creator</Link> · <Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Imagine · Relative chat size · Text Tools · LLM Overheat
</Typography>
</FormControl>
+2 -2
View File
@@ -12,8 +12,8 @@ export const Brand = {
Common: (process.env.NODE_ENV === 'development' ? '[DEV] ' : '') + 'big-AGI',
},
Meta: {
Description: 'Leading open-source AI web interface to help you learn, think, and do. AI personas, superior privacy, advanced features, and fun UX.',
SiteName: 'big-AGI | Harnessing AI for You',
Description: 'Launch big-AGI to unlock the full potential of AI, with precise control over your data and models. Voice interface, AI personas, advanced features, and fun UX.',
SiteName: 'big-AGI | Precision AI for You',
ThemeColor: '#32383E',
TwitterSite: '@enricoros',
},
+52 -20
View File
@@ -7,40 +7,72 @@
import Router from 'next/router';
import type { DConversationId } from '~/common/state/store-chats';
import { isBrowser } from './util/pwaUtils';
export const ROUTE_INDEX = '/';
export const ROUTE_APP_CHAT = '/';
export const ROUTE_APP_CALL = '/call';
export const ROUTE_APP_LINK_CHAT = '/link/chat/:linkId';
export const ROUTE_APP_NEWS = '/news';
export const ROUTE_APP_PERSONAS = '/personas';
const ROUTE_CALLBACK_OPENROUTER = '/link/callback_openrouter';
export const getIndexLink = () => ROUTE_INDEX;
// Get Paths
export const getCallbackUrl = (source: 'openrouter') => {
const callbackUrl = new URL(window.location.href);
switch (source) {
case 'openrouter':
callbackUrl.pathname = ROUTE_CALLBACK_OPENROUTER;
break;
default:
throw new Error(`Unknown source: ${source}`);
}
return callbackUrl.toString();
};
export const getChatLinkRelativePath = (chatLinkId: string) => ROUTE_APP_LINK_CHAT.replace(':linkId', chatLinkId);
const navigateFn = (path: string) => (replace?: boolean): Promise<boolean> =>
Router[replace ? 'replace' : 'push'](path);
/// Simple Navigation
export const navigateToIndex = navigateFn(ROUTE_INDEX);
export const navigateToChat = async (conversationId?: DConversationId) => {
if (conversationId) {
await Router.push(
{
pathname: ROUTE_APP_CHAT,
query: {
conversationId,
},
},
ROUTE_APP_CHAT,
);
} else {
await Router.push(ROUTE_APP_CHAT, ROUTE_APP_CHAT);
}
};
export const navigateToNews = navigateFn(ROUTE_APP_NEWS);
export const navigateToPersonas = navigateFn(ROUTE_APP_PERSONAS);
export const navigateBack = Router.back;
export const reloadPage = () => isBrowser && window.location.reload();
function navigateFn(path: string) {
return (replace?: boolean): Promise<boolean> => Router[replace ? 'replace' : 'push'](path);
}
/// Launch Apps
/* Note: not used yet
export interface AppChatQueryParams {
conversationId?: string;
}*/
export const launchAppChat = async (conversationId?: DConversationId) => {
await Router.push(
{
pathname: ROUTE_APP_CHAT,
query: conversationId ? {
conversationId,
} /*satisfies AppChatQueryParams*/
: undefined,
},
ROUTE_APP_CHAT,
);
};
export interface AppCallQueryParams {
conversationId: string;
personaId: string;
@@ -49,12 +81,12 @@ export interface AppCallQueryParams {
export function launchAppCall(conversationId: string, personaId: string) {
void Router.push(
{
pathname: `/call`,
pathname: ROUTE_APP_CALL,
query: {
conversationId,
personaId,
} satisfies AppCallQueryParams,
},
// '/call',
// ROUTE_APP_CALL,
).then();
}
+19 -6
View File
@@ -46,10 +46,17 @@ export const appTheme = extendTheme({
text: {
icon: 'var(--joy-palette-neutral-700)', // <IconButton color='neutral' /> icon color
secondary: 'var(--joy-palette-neutral-800)', // increase contrast a bit
// tertiary: 'var(--joy-palette-neutral-700)', // increase contrast a bit
},
// popup [white] > surface [50] > level1 [100] > level2 [200] > level3 [300] > body [white -> 400]
// popup [white] > surface [50] > level1 [100] > level2 [200] > level3 [300 -> unused] > body [white -> 300]
background: {
body: 'var(--joy-palette-neutral-400, #9FA6AD)', // background to stand back after all levels
// New
surface: 'var(--joy-palette-neutral-50, #FBFCFE)',
level1: 'var(--joy-palette-neutral-100, #F0F4F8)',
level2: 'var(--joy-palette-neutral-200, #DDE7EE)',
body: 'var(--joy-palette-neutral-300, #CDD7E1)',
// Former
// body: 'var(--joy-palette-neutral-400, #9FA6AD)',
},
},
},
@@ -61,10 +68,12 @@ export const appTheme = extendTheme({
// tertiary: 'var(--joy-palette-neutral-400, #9FA6AD)',
},
background: {
surface: 'var(--joy-palette-neutral-900, #131318)',
level1: 'var(--joy-palette-common-black, #09090D)',
level2: 'var(--joy-palette-neutral-800, #25252D)',
// popup: 'var(--joy-palette-common-black, #09090D)',
// New
surface: 'var(--joy-palette-neutral-800, #171A1C)',
level1: 'var(--joy-palette-neutral-900, #0B0D0E)',
level2: 'var(--joy-palette-neutral-800, #171A1C)',
body: 'var(--joy-palette-common-black, #000)',
// Former: surface [900] > level 1 [black], level 2 [800] > body [black]
},
},
},
@@ -133,6 +142,10 @@ export const appTheme = extendTheme({
},
});
export const themeBgApp = 'background.level1';
export const themeBgAppDarker = 'background.level2';
export const themeBgAppChatComposer = 'background.surface';
export const bodyFontClassName = inter.className;
export const themeBreakpoints = appTheme.breakpoints.values;
+2 -3
View File
@@ -1,5 +1,4 @@
import * as React from 'react';
import { KeyboardEvent } from 'react';
import { ClickAwayListener, Popper, PopperPlacementType } from '@mui/base';
import { MenuList, styled } from '@mui/joy';
@@ -37,12 +36,12 @@ export function CloseableMenu(props: {
children?: React.ReactNode,
}) {
const handleClose = (event: MouseEvent | TouchEvent | KeyboardEvent) => {
const handleClose = (event: MouseEvent | TouchEvent | React.KeyboardEvent) => {
event.stopPropagation();
props.onClose();
};
const handleListKeyDown = (event: KeyboardEvent) => {
const handleListKeyDown = (event: React.KeyboardEvent) => {
if (event.key === 'Tab') {
handleClose(event);
} else if (event.key === 'Escape') {
@@ -9,13 +9,14 @@ export type DropdownItems = Record<string, {
title: string,
symbol?: string,
type?: 'separator'
icon?: React.ReactNode,
}>;
/**
* A Select component that blends-in nicely (cleaner, easier to the eyes)
*/
export function AppBarDropdown<TValue extends string>(props: {
export function GoodDropdown<TValue extends string>(props: {
items: DropdownItems,
prependOption?: React.JSX.Element,
appendOption?: React.JSX.Element,
@@ -71,20 +72,25 @@ export function AppBarDropdown<TValue extends string>(props: {
{!!props.prependOption && Object.keys(props.items).length >= 1 && <Divider />}
<Box sx={{ overflowY: 'auto' }}>
{Object.keys(props.items).map((key: string, idx: number) => <React.Fragment key={'key-' + idx}>
{props.items[key].type === 'separator'
? <ListDivider />
: <Option value={key} sx={{ whiteSpace: 'nowrap' }}>
{props.showSymbols && <ListItemDecorator sx={{ fontSize: 'xl' }}>{props.items[key]?.symbol + ' '}</ListItemDecorator>}
{props.items[key].title}
{Object.keys(props.items).map((key: string, idx: number) => {
const item = props.items[key];
if (item.type === 'separator')
return <ListDivider key={'key-' + idx} />;
return (
<Option key={'key-' + idx} value={key} sx={{ whiteSpace: 'nowrap' }}>
{props.showSymbols && <ListItemDecorator sx={{ fontSize: 'xl' }}>{item?.symbol + ' '}</ListItemDecorator>}
{props.showSymbols && !!item.icon && <ListItemDecorator>{item?.icon}</ListItemDecorator>}
{item.title}
{/*{key === props.value && (*/}
{/* <IconButton variant='soft' onClick={() => alert('aa')} sx={{ ml: 'auto' }}>*/}
{/* <SettingsIcon color='success' />*/}
{/* </IconButton>*/}
{/*)}*/}
</Option>
}
</React.Fragment>)}
);
})}
</Box>
{!!props.appendOption && Object.keys(props.items).length >= 1 && <ListDivider />}
+1 -1
View File
@@ -23,7 +23,7 @@ export function GoodModal(props: {
const showBottomClose = !!props.onClose && props.hideBottomClose !== true;
return (
<Modal open={props.open} onClose={props.onClose}>
<ModalOverflow>
<ModalOverflow sx={{p:1}}>
<ModalDialog
sx={{
minWidth: { xs: 360, sm: 500, md: 600, lg: 700 },
-20
View File
@@ -1,20 +0,0 @@
import * as React from 'react';
/**
* Prevents the children from being rendered on the server.
*
* This is vital for using localStorage, which is not available on the server, and which
* state is loaded synchronously on the client.
*
* The discrepancy between server and client state can cause hydration errors for React,
* and we avoid those by using this wrapper.
*
* Suggestion: use sparingly, to show you are aware of the root causes of hydration errors.
*/
export const NoSSR = ({ children }: { children: any }): React.JSX.Element | null => {
const [isMounted, setIsMounted] = React.useState(false);
React.useEffect(() => setIsMounted(true), []);
return isMounted ? children : null;
};
@@ -19,7 +19,7 @@ export const FormRadioControl = <TValue extends string>(props: {
tooltip?: string | React.JSX.Element,
disabled?: boolean;
options: FormRadioOption<TValue>[];
value: TValue;
value?: TValue;
onChange: (value: TValue) => void;
}) =>
<FormControl orientation='horizontal' disabled={props.disabled} sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
@@ -28,6 +28,7 @@ export const FormRadioControl = <TValue extends string>(props: {
orientation='horizontal'
value={props.value}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => event.target.value && props.onChange(event.target.value as TValue)}
sx={{ flexWrap: 'wrap' }}
>
{props.options.map((option) =>
<Radio key={'opt-' + option.value} value={option.value} label={option.label} disabled={option.disabled || props.disabled} />,
@@ -11,7 +11,9 @@ import type { ToggleableBoolean } from '~/common/util/useToggleableBoolean';
*/
export function SetupFormRefetchButton(props: {
refetch: () => void,
disabled: boolean, error: boolean,
disabled: boolean,
loading: boolean,
error: boolean,
leftButton?: React.ReactNode,
advanced?: ToggleableBoolean
}) {
@@ -29,6 +31,7 @@ export function SetupFormRefetchButton(props: {
<Button
color={props.error ? 'warning' : 'primary'}
disabled={props.disabled}
loading={props.loading}
endDecorator={<SyncIcon />}
onClick={props.refetch}
sx={{ minWidth: 120, ml: 'auto' }}
@@ -0,0 +1,10 @@
import * as React from 'react';
import { SvgIcon } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
export function MistralIcon(props: { sx?: SxProps }) {
return <SvgIcon viewBox='0 0 24 24' width='24' height='24' strokeWidth={0} stroke='none' fill='currentColor' strokeLinecap='butt' strokeLinejoin='miter' {...props}>
<path d='m 2,2 v 4 4 V 14 v 4 4 h 4 v -4 -4 h 4 v 4 h 4 v -4 h 4 v 4 4 h 4 v -4 -4 -4 -4 V 2 h -4 v 4 h -4 v 4 h -4 v -4 H 6 V 2 Z' />
</SvgIcon>;
}
+16 -4
View File
@@ -33,13 +33,25 @@ export interface CapabilityElevenLabsSpeechSynthesis {
export { useCapability as useCapabilityElevenLabs } from '~/modules/elevenlabs/elevenlabs.client';
/// Image Generation: Prodia
/// Image Generation
export interface CapabilityProdiaImageGeneration {
mayWork: boolean;
export interface TextToImageProvider {
id: string;
label: string;
painter: string;
description: string;
configured: boolean;
vendor: 'openai' | 'prodia';
}
export { useCapability as useCapabilityProdia } from '~/modules/prodia/prodia.client';
export interface CapabilityTextToImage {
mayWork: boolean;
providers: TextToImageProvider[],
activeProviderId: string | null;
setActiveProviderId: (providerId: string | null) => void;
}
export { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
/// Browsing
+9 -3
View File
@@ -21,8 +21,13 @@ export const useGlobalShortcut = (shortcutKey: string | false, useCtrl: boolean,
if (!shortcutKey) return;
const lcShortcut = shortcutKey.toLowerCase();
const handleKeyDown = (event: KeyboardEvent) => {
if ((useCtrl === event.ctrlKey) && (useShift === event.shiftKey) && (useAlt === event.altKey)
&& event.key.toLowerCase() === lcShortcut) {
const isCtrlOrCmd = (event.ctrlKey && !event.metaKey) || (event.metaKey && !event.ctrlKey);
if (
(useCtrl === isCtrlOrCmd) &&
(useShift === event.shiftKey) &&
(useAlt === event.altKey) &&
event.key.toLowerCase() === lcShortcut
) {
event.preventDefault();
event.stopPropagation();
callback();
@@ -46,9 +51,10 @@ export const useGlobalShortcuts = (shortcuts: GlobalShortcutItem[]) => {
React.useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
for (const [key, useCtrl, useShift, useAlt, action] of shortcuts) {
const isCtrlOrCmd = (event.ctrlKey && !event.metaKey) || (event.metaKey && !event.ctrlKey);
if (
key &&
(useCtrl === event.ctrlKey) &&
(useCtrl === isCtrlOrCmd) &&
(useShift === event.shiftKey) &&
(useAlt === event.altKey) &&
event.key.toLowerCase() === key.toLowerCase()
+2 -1
View File
@@ -4,8 +4,9 @@ import { themeBreakpoints } from '../app.theme';
import { isBrowser } from '~/common/util/pwaUtils';
export const isMobileQuery = () => `(max-width: ${themeBreakpoints.md - 1}px)`;
export const useIsMobile = (): boolean => useMatchMedia(`(max-width: ${themeBreakpoints.md - 1}px)`, false);
export const useIsMobile = (): boolean => useMatchMedia(isMobileQuery(), false);
export function useMatchMedia(query: string, ssrValue: boolean): boolean {
const [matches, setMatches] = React.useState(isBrowser ? window.matchMedia(query).matches : ssrValue);
@@ -0,0 +1,95 @@
import * as React from 'react';
/**
* The AloneDetector class checks if the current client is the only one present for a given app. It uses
* BroadcastChannel to talk to other clients. If no other clients reply within a short time, it assumes it's
* the only one and tells the caller.
*/
class AloneDetector {
private readonly clientId: string;
private readonly broadcastChannel: BroadcastChannel;
private aloneCallback: ((isAlone: boolean) => void) | null;
private aloneTimerId: number | undefined;
constructor(channelName: string, onAlone: (isAlone: boolean) => void) {
this.clientId = Math.random().toString(36).substring(2, 10);
this.aloneCallback = onAlone;
this.broadcastChannel = new BroadcastChannel(channelName);
this.broadcastChannel.onmessage = this.handleIncomingMessage;
}
public onUnmount(): void {
// close channel
this.broadcastChannel.onmessage = null;
this.broadcastChannel.close();
// clear timeout
if (this.aloneTimerId)
clearTimeout(this.aloneTimerId);
this.aloneTimerId = undefined;
this.aloneCallback = null;
}
public checkIfAlone(): void {
// triggers other clients
this.broadcastChannel.postMessage({ type: 'CHECK', sender: this.clientId });
// if no response within 500ms, assume this client is alone
this.aloneTimerId = window.setTimeout(() => {
this.aloneTimerId = undefined;
this.aloneCallback?.(true);
}, 500);
}
private handleIncomingMessage = (event: MessageEvent): void => {
// ignore self messages
if (event.data.sender === this.clientId) return;
switch (event.data.type) {
case 'CHECK':
this.broadcastChannel.postMessage({ type: 'ALIVE', sender: this.clientId });
break;
case 'ALIVE':
// received an ALIVE message, tell the client they're not alone
if (this.aloneTimerId) {
clearTimeout(this.aloneTimerId);
this.aloneTimerId = undefined;
}
this.aloneCallback?.(false);
this.aloneCallback = null;
break;
}
};
}
/**
* React hook that checks whether the current tab is the only one open for a specific channel.
*
* @param {string} channelName - The name of the BroadcastChannel to communicate on.
* @returns {boolean | null} - True if the current tab is alone, false if not, or null before the check completes.
*/
export function useSingleTabEnforcer(channelName: string): boolean | null {
const [isAlone, setIsAlone] = React.useState<boolean | null>(null);
React.useEffect(() => {
const tabManager = new AloneDetector(channelName, setIsAlone);
tabManager.checkIfAlone();
return () => {
tabManager.onUnmount();
};
}, [channelName]);
return isAlone;
}
-79
View File
@@ -1,79 +0,0 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { Box, Container } from '@mui/joy';
import { ModelsModal } from '../../apps/models-modal/ModelsModal';
import { SettingsModal } from '../../apps/settings-modal/SettingsModal';
import { ShortcutsModal } from '../../apps/settings-modal/ShortcutsModal';
import { isPwa } from '~/common/util/pwaUtils';
import { useAppStateStore } from '~/common/state/store-appstate';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { AppBar } from './AppBar';
import { GlobalShortcutItem, useGlobalShortcuts } from '../components/useGlobalShortcut';
import { NoSSR } from '../components/NoSSR';
import { openLayoutModelsSetup, openLayoutPreferences, openLayoutShortcuts } from './store-applayout';
export function AppLayout(props: {
noAppBar?: boolean, suspendAutoModelsSetup?: boolean,
children: React.ReactNode,
}) {
// external state
const { centerMode } = useUIPreferencesStore(state => ({ centerMode: isPwa() ? 'full' : state.centerMode }), shallow);
// usage counter, for progressive disclosure of features
useAppStateStore(state => state.usageCount);
// global shortcuts for modals
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
['m', true, true, false, openLayoutModelsSetup],
['p', true, true, false, openLayoutPreferences],
['?', true, true, false, openLayoutShortcuts],
], []);
useGlobalShortcuts(shortcuts);
return (
// Global NoSSR wrapper: the overall Container could have hydration issues when using localStorage and non-default maxWidth
<NoSSR>
<Container
disableGutters
maxWidth={centerMode === 'full' ? false : centerMode === 'narrow' ? 'md' : 'xl'}
sx={{
boxShadow: {
xs: 'none',
md: centerMode === 'narrow' ? 'md' : 'none',
xl: centerMode !== 'full' ? 'lg' : 'none',
},
}}>
<Box sx={{
display: 'flex', flexDirection: 'column',
height: '100dvh',
}}>
{!props.noAppBar && <AppBar sx={{
zIndex: 20, // position: 'sticky', top: 0,
}} />}
{props.children}
</Box>
</Container>
{/* Overlay Settings */}
<SettingsModal />
{/* Overlay Models (& Model Options )*/}
<ModelsModal suspendAutoModelsSetup={props.suspendAutoModelsSetup} />
{/* Overlay Shortcuts */}
<ShortcutsModal />
</NoSSR>
);
}
@@ -9,19 +9,19 @@ import MenuIcon from '@mui/icons-material/Menu';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import { Brand } from '../app.config';
import { CloseableMenu } from '../components/CloseableMenu';
import { Link } from '../components/Link';
import { LogoSquircle } from '../components/LogoSquircle';
import { Brand } from '~/common/app.config';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { Link } from '~/common/components/Link';
import { LogoSquircle } from '~/common/components/LogoSquircle';
import { ROUTE_INDEX } from '~/common/app.routes';
// import { AppBarSupportItem } from './AppBarSupportItem';
import { AppBarSwitcherItem } from './AppBarSwitcherItem';
import { closeLayoutDrawer, closeLayoutMenu, openLayoutPreferences, setLayoutDrawerAnchor, setLayoutMenuAnchor, useLayoutComponents } from './store-applayout';
import { useOptimaLayout } from './useOptimaLayout';
function AppBarTitle() {
return (
<Link href='/'>
<Link href={ROUTE_INDEX}>
<LogoSquircle sx={{
width: 32,
height: 32,
@@ -40,12 +40,14 @@ function AppBarTitle() {
function CommonMenuItems(props: { onClose: () => void }) {
// external state
const { openPreferences } = useOptimaLayout();
const { mode: colorMode, setMode: setColorMode } = useColorScheme();
const handleShowSettings = (event: React.MouseEvent) => {
event.stopPropagation();
openLayoutPreferences();
openPreferences();
props.onClose();
};
@@ -93,10 +95,14 @@ export function AppBar(props: { sx?: SxProps }) {
// const [value, setValue] = React.useState<ContainedAppType>('chat');
// external state
const { centerItems, drawerAnchor, drawerItems, menuAnchor, menuItems } = useLayoutComponents();
const {
appBarItems, appDrawerAnchor, appPaneContent, appMenuAnchor, appMenuItems,
closeAppMenu, closeAppDrawer,
setAppDrawerAnchor, setAppMenuAnchor,
} = useOptimaLayout();
const commonMenuItems = React.useMemo(() =>
<CommonMenuItems onClose={closeLayoutMenu} />, []);
<CommonMenuItems onClose={closeAppMenu} />, [closeAppMenu]);
return <>
@@ -110,47 +116,47 @@ export function AppBar(props: { sx?: SxProps }) {
}}>
{/* Drawer Anchor */}
{!drawerItems ? (
<IconButton component={Link} href='/' noLinkStyle variant='plain'>
{!appPaneContent ? (
<IconButton component={Link} href={ROUTE_INDEX} noLinkStyle variant='plain'>
<ArrowBackIcon />
</IconButton>
) : (
<IconButton disabled={!!drawerAnchor || !drawerItems} variant='plain' onClick={event => setLayoutDrawerAnchor(event.currentTarget)}>
<IconButton disabled={!!appDrawerAnchor || !appPaneContent} variant='plain' onClick={event => setAppDrawerAnchor(event.currentTarget)}>
<MenuIcon />
</IconButton>
)}
{/* Center Items */}
<Box sx={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', my: 'auto' }}>
{!!centerItems ? centerItems : <AppBarTitle />}
{!!appBarItems ? appBarItems : <AppBarTitle />}
</Box>
{/* Menu Anchor */}
<IconButton disabled={!!menuAnchor /*|| !menuItems*/} variant='plain' onClick={event => setLayoutMenuAnchor(event.currentTarget)}>
<IconButton disabled={!!appMenuAnchor /*|| !appMenuItems*/} variant='plain' onClick={event => setAppMenuAnchor(event.currentTarget)}>
<MoreVertIcon />
</IconButton>
</Sheet>
{/* Drawer Menu */}
{!!drawerItems && <CloseableMenu
{!!appPaneContent && <CloseableMenu
maxHeightGapPx={56 + 24} sx={{ minWidth: 320 }}
open={!!drawerAnchor} anchorEl={drawerAnchor} onClose={closeLayoutDrawer}
open={!!appDrawerAnchor} anchorEl={appDrawerAnchor} onClose={closeAppDrawer}
placement='bottom-start'
>
{drawerItems}
{appPaneContent}
</CloseableMenu>}
{/* Menu Menu */}
<CloseableMenu
maxHeightGapPx={56 + 24} noBottomPadding noTopPadding sx={{ minWidth: 320 }}
open={!!menuAnchor} anchorEl={menuAnchor} onClose={closeLayoutMenu}
open={!!appMenuAnchor} anchorEl={appMenuAnchor} onClose={closeAppMenu}
placement='bottom-end'
>
{commonMenuItems}
{!!menuItems && <ListDivider sx={{ mt: 0 }} />}
{!!menuItems && <Box sx={{ overflowY: 'auto' }}>{menuItems}</Box>}
{!!menuItems && <ListDivider sx={{ mb: 0 }} />}
{!!appMenuItems && <ListDivider sx={{ mt: 0 }} />}
{!!appMenuItems && <Box sx={{ overflowY: 'auto' }}>{appMenuItems}</Box>}
{!!appMenuItems && <ListDivider sx={{ mb: 0 }} />}
<AppBarSwitcherItem />
{/*<AppBarSupportItem />*/}
</CloseableMenu>
@@ -5,8 +5,8 @@ import { SxProps } from '@mui/joy/styles/types';
// import GitHubIcon from '@mui/icons-material/GitHub';
// import { Brand } from '..//common/app.brand';
import { Link } from '../components/Link';
import { cssRainbowColorKeyframes } from '../app.theme';
import { Link } from '~/common/components/Link';
import { cssRainbowColorKeyframes } from '~/common/app.theme';
// missing from MUI, using Tabler for Discord
@@ -4,10 +4,11 @@ import { useRouter } from 'next/router';
import { Box, Button, ButtonGroup, ListItem } from '@mui/joy';
import GitHubIcon from '@mui/icons-material/GitHub';
import { Brand } from '../app.config';
import { Brand } from '~/common/app.config';
import { ROUTE_APP_CHAT, ROUTE_APP_NEWS } from '~/common/app.routes';
import { BringTheLove, DiscordIcon } from './AppBarSupportItem';
import { closeLayoutMenu } from './store-applayout';
import { useOptimaLayout } from './useOptimaLayout';
// routes for the quick switcher menu item
@@ -19,7 +20,7 @@ const AppItems: ContainedAppType[] = ['chat', 'news'];
const AppRouteMap: { [key in ContainedAppType]: { name: string, route: string } } = {
'chat': {
name: 'Chat',
route: '/',
route: ROUTE_APP_CHAT,
},
// 'data': {
// name: 'Data',
@@ -27,13 +28,15 @@ const AppRouteMap: { [key in ContainedAppType]: { name: string, route: string }
// },
'news': {
name: 'News',
route: '/news',
route: ROUTE_APP_NEWS,
},
};
export function AppBarSwitcherItem() {
// external state
const { closeAppMenu } = useOptimaLayout();
const { route, push: routerPush } = useRouter();
// find the current ContainedAppType or null
@@ -42,7 +45,7 @@ export function AppBarSwitcherItem() {
// switcher
const switchApp = (app: ContainedAppType) => {
if (currentApp !== app) {
closeLayoutMenu();
closeAppMenu();
void routerPush(AppRouteMap[app].route);
}
};
@@ -0,0 +1,65 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { default as NProgress } from 'nprogress';
import 'nprogress/nprogress.css';
/**
* Not show the bar for very fast loads (with a delay), and for the same route
*/
export function NextRouterProgress(props: { color: string, delay?: number }) {
// external state
const router = useRouter();
// this fires both when the page is refreshed, and when the route changes
React.useEffect(() => {
NProgress.configure({
showSpinner: false,
});
// timeout to not show the progress bar for very fast loads
let timeout: number;
const handleStop = () => {
clearTimeout(timeout);
NProgress.done();
};
const handleStart = (newRoute: string) => {
handleStop();
if (newRoute == router.route)
return;
timeout = window.setTimeout(
() => NProgress.start(),
props.delay === undefined ? 250 : props.delay,
);
};
router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleStop);
router.events.on('routeChangeError', handleStop);
return () => {
handleStop();
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleStop);
router.events.off('routeChangeError', handleStop);
};
}, [props.delay, router]);
return (
<style>
{`
#nprogress .bar {
height: 4px;
background: ${props.color};
}
#nprogress .peg {
box-shadow: 0 0 10px ${props.color}, 0 0 5px ${props.color};
}
`}
</style>
);
}
+127
View File
@@ -0,0 +1,127 @@
import * as React from 'react';
import { Box, Container } from '@mui/joy';
import { ModelsModal } from '~/modules/llms/models-modal/ModelsModal';
import { SettingsModal } from '../../../apps/settings-modal/SettingsModal';
import { ShortcutsModal } from '../../../apps/settings-modal/ShortcutsModal';
import { isPwa } from '~/common/util/pwaUtils';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { AppBar } from './AppBar';
import { NextRouterProgress } from './NextLoadProgress';
import { useOptimaLayout } from './useOptimaLayout';
/*function ResponsiveNavigation() {
return <>
<Drawer
open={false}
variant='solid'
anchor='left'
onClose={() => {
}}
sx={{
'& .MuiDrawer-paper': {
width: 256,
boxSizing: 'border-box',
},
}}
>
<Box sx={{ width: 256, height: '100%' }}>
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ flexGrow: 1 }} />
</Box>
</Box>
</Drawer>
</>;
}*/
/**
* Core layout of big-AGI, used by all the Primary applications therein.
*
* Main functions:
* - modern responsive layout
* - core layout of the application, with the Nav, Panes, Appbar, etc.
* - the child(ren) of this layout are placed in the main content area
* - allows for pluggable components of children applications, via usePluggableOptimaLayout
* - overlays and displays various modals
* - flicker free
*/
export function OptimaLayout(props: { suspendAutoModelsSetup?: boolean, children: React.ReactNode, }) {
// external state
const isMobile = useIsMobile();
let centerMode = useUIPreferencesStore(state => (isPwa() || isMobile) ? 'full' : state.centerMode);
const {
closePreferences, closeShortcuts,
openShortcuts,
showPreferencesTab, showShortcuts,
} = useOptimaLayout();
return <>
{/*<Box sx={{*/}
{/* display: 'flex', flexDirection: 'row',*/}
{/* maxWidth: '100%', flexWrap: 'nowrap',*/}
{/* // overflowX: 'hidden',*/}
{/* background: 'lime',*/}
{/*}}>*/}
{/*<Box sx={{ background: 'rgba(100 0 0 / 0.5)' }}>a</Box>*/}
{/*<ResponsiveNavigation />*/}
<Container
disableGutters
maxWidth={centerMode === 'full' ? false : centerMode === 'narrow' ? 'md' : 'xl'}
sx={{
// minWidth: 0,
boxShadow: {
xs: 'none',
md: centerMode === 'narrow' ? 'md' : 'none',
xl: centerMode !== 'full' ? 'lg' : 'none',
},
}}>
<Box sx={{
display: 'flex', flexDirection: 'column',
height: '100dvh',
}}>
<AppBar sx={{
zIndex: 20,
}} />
{/* Children must make the assumption they're in a flex-col layout */}
{props.children}
</Box>
</Container>
{/*<Box sx={{ background: 'rgba(100 0 0 / 0.5)' }}>bb</Box>*/}
{/*</Box>*/}
{/* Overlay Settings */}
<SettingsModal open={!!showPreferencesTab} tabIndex={showPreferencesTab} onClose={closePreferences} onOpenShortcuts={openShortcuts} />
{/* Overlay Models + LLM Options */}
<ModelsModal suspendAutoModelsSetup={props.suspendAutoModelsSetup} />
{/* Overlay Shortcuts */}
{showShortcuts && <ShortcutsModal onClose={closeShortcuts} />}
{/* Route loading progress overlay */}
<NextRouterProgress color='var(--joy-palette-neutral-700, #32383E)' />
</>;
}
@@ -0,0 +1,158 @@
import * as React from 'react';
import type { DLLMId } from '~/modules/llms/store-llms';
import { GlobalShortcutItem, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
const DEBUG_OPTIMA_LAYOUT_PLUGGING = false;
type PC = React.JSX.Element | null;
interface OptimaLayoutState {
// pluggable UI
appPaneContent: PC;
appBarItems: PC;
appMenuItems: PC;
// anchors - for externally closeable menus
appDrawerAnchor: HTMLElement | null;
appMenuAnchor: HTMLElement | null;
// modals that can overlay anything
showPreferencesTab: number;
showModelsSetup: boolean;
showLlmOptions: DLLMId | null;
showShortcuts: boolean;
}
const initialState: OptimaLayoutState = {
appPaneContent: null,
appBarItems: null,
appMenuItems: null,
appDrawerAnchor: null,
appMenuAnchor: null,
showPreferencesTab: 0, // 0 = closed, 1+ open tab n-1
showModelsSetup: false,
showLlmOptions: null,
showShortcuts: false,
};
interface OptimaLayoutActions {
setPluggableComponents: (
appPaneContent: PC,
appBarItems: PC,
appMenuItems: PC,
) => void;
setAppDrawerAnchor: (anchor: HTMLElement | null) => void;
closeAppDrawer: () => void;
setAppMenuAnchor: (anchor: HTMLElement | null) => void;
closeAppMenu: () => void;
openPreferences: (tab?: number) => void;
closePreferences: () => void;
openModelsSetup: () => void;
closeModelsSetup: () => void;
openLlmOptions: (id: DLLMId) => void;
closeLlmOptions: () => void;
openShortcuts: () => void;
closeShortcuts: () => void;
}
// React Context with ...state and ...actions
const UseOptimaLayout = React.createContext<
(OptimaLayoutState & OptimaLayoutActions) | undefined
>(undefined);
export function OptimaLayoutProvider(props: { children: React.ReactNode }) {
// optima state, only modified by the static actions
const [state, setState] = React.useState<OptimaLayoutState>(initialState);
// actions
const actions: OptimaLayoutActions = React.useMemo(() => ({
setPluggableComponents: (appPaneContent: PC, appBarItems: PC, appMenuItems: PC) =>
setState(state => ({ ...state, appPaneContent, appBarItems, appMenuItems })),
setAppDrawerAnchor: (anchor: HTMLElement | null) => setState(state => ({ ...state, appDrawerAnchor: anchor })),
closeAppDrawer: () => setState(state => ({ ...state, appDrawerAnchor: null })),
setAppMenuAnchor: (anchor: HTMLElement | null) => setState(state => ({ ...state, appMenuAnchor: anchor })),
closeAppMenu: () => setState(state => ({ ...state, appMenuAnchor: null })),
openPreferences: (tab?: number) => setState(state => ({ ...state, showPreferencesTab: tab || 1 })),
closePreferences: () => setState(state => ({ ...state, showPreferencesTab: 0 })),
openModelsSetup: () => setState(state => ({ ...state, showModelsSetup: true })),
closeModelsSetup: () => setState(state => ({ ...state, showModelsSetup: false })),
openLlmOptions: (id: DLLMId) => setState(state => ({ ...state, showLlmOptions: id })),
closeLlmOptions: () => setState(state => ({ ...state, showLlmOptions: null })),
openShortcuts: () => setState(state => ({ ...state, showShortcuts: true })),
closeShortcuts: () => setState(state => ({ ...state, showShortcuts: false })),
}), []);
// global shortcuts for Optima
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
['?', true, true, false, actions.openShortcuts],
['m', true, true, false, actions.openModelsSetup],
['p', true, true, false, actions.openPreferences],
], [actions]);
useGlobalShortcuts(shortcuts);
return (
<UseOptimaLayout.Provider value={{ ...state, ...actions }}>
{props.children}
</UseOptimaLayout.Provider>
);
}
/**
* Optima Layout accessor for getting state and actions
*/
export const useOptimaLayout = (): OptimaLayoutState & OptimaLayoutActions => {
const context = React.useContext(UseOptimaLayout);
if (!context)
throw new Error('useOptimaLayout must be used within an OptimaLayoutProvider');
return context;
};
/**
* used by the active UI client to register its components (and unregister on cleanup)
*/
export const usePluggableOptimaLayout = (appPaneContent: PC, appBarItems: PC, appMenuItems: PC, debugCallerName: string) => {
const { setPluggableComponents } = useOptimaLayout();
React.useEffect(() => {
if (DEBUG_OPTIMA_LAYOUT_PLUGGING)
console.log(' +PLUG layout', debugCallerName);
setPluggableComponents(appPaneContent, appBarItems, appMenuItems);
return () => {
if (DEBUG_OPTIMA_LAYOUT_PLUGGING)
console.log(' -UNplug layout', debugCallerName);
setPluggableComponents(null, null, null);
};
}, [appBarItems, appMenuItems, appPaneContent, debugCallerName, setPluggableComponents]);
};
+25
View File
@@ -0,0 +1,25 @@
import * as React from 'react';
import { Box, Container } from '@mui/joy';
export function PlainLayout(props: { children?: React.ReactNode }) {
return <>
{/* Headers as needed */}
<Container disableGutters>
<Box sx={{
display: 'flex', flexDirection: 'column',
minHeight: '100dvh',
}}>
{props.children}
</Box>
</Container>
{/* Footers as needed */}
</>;
}
-82
View File
@@ -1,82 +0,0 @@
import * as React from 'react';
import { create } from 'zustand';
import { shallow } from 'zustand/shallow';
import type { DLLMId } from '~/modules/llms/store-llms';
interface AppLayoutStore {
// pluggable UI
drawerItems: React.JSX.Element | null;
centerItems: React.JSX.Element | null;
menuItems: React.JSX.Element | null;
// anchors - for externally closeable menus
drawerAnchor: HTMLElement | null;
menuAnchor: HTMLElement | null;
// modals, which are on the AppLayout
preferencesTab: number; // 0: closed, 1..N: tab index
modelsSetupOpen: boolean;
llmOptionsId: DLLMId | null;
shortcutsOpen: boolean;
}
const useAppLayoutStore = create<AppLayoutStore>()(
() => ({
drawerItems: null,
centerItems: null,
menuItems: null,
drawerAnchor: null,
menuAnchor: null,
preferencesTab: 0,
modelsSetupOpen: false,
llmOptionsId: null,
shortcutsOpen: false,
}),
);
/**
* used by the active UI client to register its components (and unregister on cleanup)
*/
export function useLayoutPluggable(centerItems: React.JSX.Element | null, drawerItems: React.JSX.Element | null, menuItems: React.JSX.Element | null) {
React.useEffect(() => {
useAppLayoutStore.setState({ centerItems, drawerItems, menuItems });
return () => useAppLayoutStore.setState({ centerItems: null, drawerItems: null, menuItems: null });
}, [centerItems, drawerItems, menuItems]);
}
export function useLayoutComponents() {
return useAppLayoutStore(state => ({
drawerItems: state.drawerItems,
centerItems: state.centerItems,
menuItems: state.menuItems,
drawerAnchor: state.drawerAnchor,
menuAnchor: state.menuAnchor,
}), shallow);
}
export const setLayoutDrawerAnchor = (anchor: HTMLElement | null) => useAppLayoutStore.setState({ drawerAnchor: anchor });
export const closeLayoutDrawer = () => useAppLayoutStore.setState({ drawerAnchor: null });
export const setLayoutMenuAnchor = (anchor: HTMLElement) => useAppLayoutStore.setState({ menuAnchor: anchor });
export const closeLayoutMenu = () => useAppLayoutStore.setState({ menuAnchor: null });
export const useLayoutPreferencesTab = () => useAppLayoutStore(state => state.preferencesTab);
export const openLayoutPreferences = (tab?: number) => useAppLayoutStore.setState({ preferencesTab: tab || 1 });
export const closeLayoutPreferences = () => useAppLayoutStore.setState({ preferencesTab: 0 });
export const useLayoutModelsSetup = (): [open: boolean, llmId: DLLMId | null] => useAppLayoutStore(state => [state.modelsSetupOpen, state.llmOptionsId], shallow);
export const openLayoutModelsSetup = () => useAppLayoutStore.setState({ modelsSetupOpen: true });
export const closeLayoutModelsSetup = () => useAppLayoutStore.setState({ modelsSetupOpen: false });
export const openLayoutLLMOptions = (llmId: DLLMId) => useAppLayoutStore.setState({ llmOptionsId: llmId });
export const closeLayoutLLMOptions = () => useAppLayoutStore.setState({ llmOptionsId: null });
export const useLayoutShortcuts = () => useAppLayoutStore(state => state.shortcutsOpen);
export const openLayoutShortcuts = () => useAppLayoutStore.setState({ shortcutsOpen: true });
export const closeLayoutShortcuts = () => useAppLayoutStore.setState({ shortcutsOpen: false });
+31
View File
@@ -0,0 +1,31 @@
import * as React from 'react';
import { OptimaLayout } from './optima/OptimaLayout';
import { OptimaLayoutProvider } from './optima/useOptimaLayout';
import { PlainLayout } from './plain/PlainLayout';
type WithLayout = {
type: 'optima';
suspendAutoModelsSetup?: boolean;
} | {
type: 'plain';
};
/**
* Dynamic page-level layouting: a wrapper that adds the layout around the children.
*/
export function withLayout(layoutOptions: WithLayout, children: React.ReactNode): React.ReactElement {
// Optima layout: also wrap it in the OptimaLayoutProvider
if (layoutOptions.type === 'optima')
return <OptimaLayoutProvider><OptimaLayout {...layoutOptions}>{children}</OptimaLayout></OptimaLayoutProvider>;
else if (layoutOptions.type === 'plain')
return <PlainLayout {...layoutOptions}>{children}</PlainLayout>;
// if no layout is specified, return the children as-is
console.error('No layout specified for this top-level page');
return <>{children}</>;
}
@@ -4,8 +4,11 @@ import { useBackendCapsLoader } from '~/modules/backend/state-backend';
import { apiQuery } from '~/common/util/trpc.client';
export function ProviderBackend(props: { children: React.ReactNode }) {
/**
* Note: we used to have a NoSSR wrapper inside the AppLayout component (which was delaying rendering 1 cycle),
* however this wrapper is now providing the same function, given the network roundtrip.
*/
export function ProviderBackendAndNoSSR(props: { children: React.ReactNode }) {
// external state
const [loaded, setCapabilties] = useBackendCapsLoader();
@@ -22,7 +25,6 @@ export function ProviderBackend(props: { children: React.ReactNode }) {
setCapabilties(capabilities);
}, [capabilities, setCapabilties]);
// block rendering until the capabilities are loaded
return !loaded ? null : props.children;
}
@@ -0,0 +1,23 @@
import * as React from 'react';
import { isBrowser } from '~/common/util/pwaUtils';
import { isMobileQuery } from '~/common/components/useMatchMedia';
export function ProviderBootstrapLogic(props: { children: React.ReactNode }) {
// NOTE: just a pass-through for now. Will be used for the following:
// - loading the latest news (see ChatPage -> useRedirectToNewsOnUpdates)
// - loading the commander
// - ...
// boot-up logic. this is not updated at route changes, but only at app startup
React.useEffect(() => {
const isMobile = isBrowser ? window.matchMedia(isMobileQuery()).matches : false;
if (isMobile) {
// TODO: the app booted in mobile mode
}
}, []);
return props.children;
}
@@ -0,0 +1,42 @@
import * as React from 'react';
import { Button, Sheet, Typography } from '@mui/joy';
import { Brand } from '../app.config';
import { reloadPage } from '../app.routes';
import { useSingleTabEnforcer } from '../components/useSingleTabEnforcer';
export const ProviderSingleTab = (props: { children: React.ReactNode }) => {
// state
const isSingleTab = useSingleTabEnforcer('big-agi-tabs');
// pass-through until we know for sure that other tabs are open
if (isSingleTab === null || isSingleTab)
return props.children;
return (
<Sheet
variant='solid'
invertedColors
sx={{
flexGrow: 1,
display: 'flex', flexDirection: { xs: 'column', md: 'row' }, justifyContent: 'center', alignItems: 'center', gap: 2,
p: 3,
}}
>
<Typography>
It looks like {Brand.Title.Base} is already running in another tab or window.
To continue here, please close the other instance first.
</Typography>
<Button onClick={reloadPage}>
Reload
</Button>
</Sheet>
);
};
-12
View File
@@ -21,12 +21,6 @@ interface UXLabsStore {
labsEnhancedUI: boolean;
setLabsEnhancedUI: (labsEnhancedUI: boolean) => void;
labsMagicDraw: boolean;
setLabsMagicDraw: (labsMagicDraw: boolean) => void;
labsPersonaYTCreator: boolean;
setLabsPersonaYTCreator: (labsPersonaYTCreator: boolean) => void;
labsSplitBranching: boolean;
setLabsSplitBranching: (labsSplitBranching: boolean) => void;
@@ -45,12 +39,6 @@ export const useUXLabsStore = create<UXLabsStore>()(
labsEnhancedUI: false,
setLabsEnhancedUI: (labsEnhancedUI: boolean) => set({ labsEnhancedUI }),
labsMagicDraw: false,
setLabsMagicDraw: (labsMagicDraw: boolean) => set({ labsMagicDraw }),
labsPersonaYTCreator: true, // NOTE: default to true, as it is a graduated experiment
setLabsPersonaYTCreator: (labsPersonaYTCreator: boolean) => set({ labsPersonaYTCreator }),
labsSplitBranching: false,
setLabsSplitBranching: (labsSplitBranching: boolean) => set({ labsSplitBranching }),
+3 -5
View File
@@ -1,11 +1,9 @@
import type { EmotionCache } from '@emotion/react';
// export type GetLayout = (page: ReactElement) => ReactNode;
// Extend the NextPage type with an optional getLayout function
// type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
// getLayout?: GetLayout;
// export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
// // require .layoutOptions on the page component
// layoutOptions: LayoutOptions;
// };
// Extend the AppProps type with the custom page component type
+40 -5
View File
@@ -2,11 +2,13 @@
* @fileoverview Utility functions for Markdown.
*/
import { isBrowser } from '~/common/util/pwaUtils';
/**
* Quick and dirty conversion of HTML tables to Markdown tables.
* Big plus: doesn't require any dependencies.
*/
export function htmlTableToMarkdown(html: string): string {
export function htmlTableToMarkdown(html: string, includeInvisible: boolean): string {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const table = doc.querySelector('table');
@@ -16,20 +18,53 @@ export function htmlTableToMarkdown(html: string): string {
const headerCells = table.querySelectorAll('thead th');
if (headerCells.length > 0) {
const headerRow = '| ' + Array.from(headerCells)
.map(cell => cell.textContent?.trim() || '')
.join(' | ') + '| ';
.map(cell => getTextWithSpaces(cell, includeInvisible).trim())
.join(' | ') + ' |';
markdownRows.push(headerRow);
markdownRows.push('|:' + Array(headerCells.length).fill('-').join('|:') + '|');
markdownRows.push('|:' + Array(headerCells.length).fill('---').join('|:') + '|');
}
const bodyRows = table.querySelectorAll('tbody tr');
for (const row of Array.from(bodyRows)) {
const rowCells = row.querySelectorAll('td');
const markdownRow = '| ' + Array.from(rowCells)
.map(cell => cell.textContent?.trim() || '')
.map(cell => getTextWithSpaces(cell, includeInvisible).trim())
.join(' | ') + ' |';
markdownRows.push(markdownRow);
}
return markdownRows.join('\n');
}
// Helper function to get text with spaces, ignoring hidden elements
function getTextWithSpaces(node: Node, includeInvisible: boolean): string {
let text = '';
node.childNodes.forEach(child => {
if (child.nodeType === Node.TEXT_NODE)
text += child.textContent;
else if (child.nodeType === Node.ELEMENT_NODE)
if (includeInvisible || isVisible(child as Element))
text += ' ' + getTextWithSpaces(child, includeInvisible) + ' ';
});
return text;
}
// Helper function to determine if an element is visible
function isVisible(element: Element): boolean {
if (!isBrowser) return true;
// if the cell is hidden, don't include it
const style = window.getComputedStyle(element);
if (style.display === 'none' || style.visibility === 'hidden')
return false;
// Check for common classes used to hide content or indicate tooltip/popover content.
// You may need to add more classes here based on your actual HTML/CSS.
const ignoredClasses = ['hidden', 'group-hover', 'tooltip', 'pointer-events-none', 'opacity-0'];
for (const ignoredClass of ignoredClasses)
if (element.classList.contains(ignoredClass))
return false;
// Otherwise, the element is considered visible
return true;
}
+1 -1
View File
@@ -14,7 +14,7 @@ export async function pdfToText(pdfBuffer: ArrayBuffer): Promise<string> {
const { getDocument, GlobalWorkerOptions } = await import('pdfjs-dist');
// Set the worker script path
GlobalWorkerOptions.workerSrc = '/workers/pdf.worker.min.js';
GlobalWorkerOptions.workerSrc = '/workers/pdf.worker.min.mjs';
const pdf = await getDocument(pdfBuffer).promise;
const textPages: string[] = []; // Initialize an array to hold text from all pages

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