Compare commits

...

685 Commits

Author SHA1 Message Date
Enrico Ros 1744b5b9d0 Throw if function calling on a model that doesn't support it 2024-06-07 12:15:25 -07:00
Enrico Ros 0c15476dd2 1.16.2: release 2024-06-06 22:10:27 -07:00
Enrico Ros 94ef76c67e Gemini: update
(cherry picked from commit 3050b546ac)
2024-06-06 21:42:47 -07:00
Enrico Ros bd5bf6f94f Gemini: update
(cherry picked from commit 1429726ba6)
2024-06-06 21:42:47 -07:00
Enrico Ros 1fbf454c3c Add Codestral - Fixes #558
(cherry picked from commit 4075581acd)
2024-06-06 21:42:47 -07:00
Enrico Ros 07b62fe5c1 Streaming uplink: index sources for unification. 2024-06-06 21:42:47 -07:00
Enrico Ros 7fbf6ee2e8 Fix Domino issue (crash) by upgrading Turndown to 7.2.0
See:
https://github.com/mixmark-io/turndown/issues/439
https://github.com/fgnass/domino/issues/146
(cherry picked from commit baad3ae1c3)
2024-06-06 21:41:04 -07:00
Enrico Ros ba66fc30c5 Fix TimeoutError issue
(cherry picked from commit 7c099cab94)
2024-06-06 21:41:04 -07:00
Enrico Ros 45b7ed3220 Mistral: update pricing
(cherry picked from commit 05aa4b547f)
2024-06-06 21:41:04 -07:00
Enrico Ros 20f1c4c0ae Mistral: update
#518

(cherry picked from commit 6afb61d25d)
2024-06-06 21:41:04 -07:00
Enrico Ros 97b6fc5e2b Already Set
(cherry picked from commit a7ce5c1ca6)
2024-06-06 21:41:04 -07:00
Enrico Ros 44d8c30187 Start opened
(cherry picked from commit 952bd2bd93)
2024-06-06 21:41:04 -07:00
Enrico Ros e3957bf08b Page download: improve
(cherry picked from commit f9d33d4888)
2024-06-06 21:41:03 -07:00
Enrico Ros acfe0aba21 Beam: bits
(cherry picked from commit 81d99f19d4)
2024-06-06 21:41:03 -07:00
Enrico Ros 6247b5411b Beam: recall importing rays
(cherry picked from commit 454a4257da)
2024-06-06 21:41:03 -07:00
Enrico Ros 5cc0b0a011 Beam: fix reactive bug
(cherry picked from commit e513b42786)
2024-06-06 21:41:03 -07:00
Enrico Ros 1fed2fb18c Beam: if auto-start, give the chance to change merge model
(cherry picked from commit b607e3c034)
2024-06-06 21:41:03 -07:00
Enrico Ros 8a0e7a4e3d Tiktoken: in the future, show tokens
(cherry picked from commit d5c3f5012b)
2024-06-06 21:41:03 -07:00
Enrico Ros 29a784c6c6 Update TikToken for perfect token computation on 'o' models.
(cherry picked from commit 21d045be59)
2024-06-06 21:41:03 -07:00
Enrico Ros 409a3ee194 DChat: remove IDB migration
(cherry picked from commit 44ab0483b6)
2024-06-06 21:41:03 -07:00
Enrico Ros 54caa3e01a Gemini: improve support (incl. interfaces, cost, visibility)
(cherry picked from commit 9eb0cc0b62)
2024-06-06 21:41:03 -07:00
Enrico Ros e1a723a39f (bits)
(cherry picked from commit 2db74867f5)
2024-06-06 21:41:03 -07:00
Enrico Ros 463ea35d7c Default to the full context window
(cherry picked from commit fd30baafb8)
2024-06-06 21:41:03 -07:00
Enrico Ros f751c91c68 Browse: improve markdown transform
(cherry picked from commit 3623eef47f)
2024-06-06 21:41:03 -07:00
Enrico Ros ad24c8771a Browse: full support for markdown transform
(cherry picked from commit 7b07bb7884)
2024-06-06 21:41:03 -07:00
Enrico Ros 6f82e2c3ed Browse: markdown transform as default
(cherry picked from commit 7946cd6614)
2024-06-06 21:41:03 -07:00
Enrico Ros f4b39071f0 Browse: support transform (skel)
(cherry picked from commit 51b6e30986)
2024-06-06 21:41:03 -07:00
Enrico Ros 621c968f3f Hold Shift to delete without confirmation: fixes #537
(cherry picked from commit 002df7b0f9)
2024-06-06 21:41:03 -07:00
Enrico Ros 564cf0fed0 1.16.1: default:hidden on the first Turbo 2024-05-13 12:04:31 -07:00
Enrico Ros dee9492d4c 1.16.0: update 2024-05-13 12:02:02 -07:00
Enrico Ros 6ae026f7c5 OpenAI: un-star Turbo 2024-05-13 11:49:10 -07:00
Enrico Ros 6bcbe286f3 OpenAI: add support for 'o' models 2024-05-13 11:47:55 -07:00
Enrico Ros 6f35f72607 Beam: auto-merge 2024-05-12 23:39:06 -07:00
Enrico Ros 3a7aa75538 Soft-wrap as a global preference. Fixes #517 2024-05-10 04:05:50 -07:00
Enrico Ros e4e7ac260a pdfjs: image generation (just in case) 2024-05-10 02:46:28 -07:00
Enrico Ros b8aaa4bb42 pdfjs: better parsing (for humans) 2024-05-10 02:19:45 -07:00
Enrico Ros 7793e2694b pdfjs: roll 2024-05-10 02:19:32 -07:00
Enrico Ros 83f2c72f29 Roll packages 2024-05-10 01:38:56 -07:00
Enrico Ros 1caeaee7f0 1.16.0: update News 2024-05-09 01:00:53 -07:00
Enrico Ros f354134234 Update README 2024-05-09 00:56:50 -07:00
Enrico Ros 66219d30e0 ReplyTo: fix bubble 2024-05-09 00:48:24 -07:00
Enrico Ros b9e3942ed8 ChatMessage: fix broken overflow 2024-05-09 00:18:29 -07:00
Enrico Ros 2354cdc1d1 ReplyTo: render in ChatMessage 2024-05-09 00:18:21 -07:00
Enrico Ros d929438df9 ReplyTo: extract 2024-05-09 00:09:17 -07:00
Enrico Ros 1acaed1de7 ReplyTo: Move Bubble 2024-05-09 00:03:22 -07:00
Enrico Ros 16195f8a55 ReplyTo: works 100 for OpenAI, ok for Anthropic, exposes Chat sequencing issues for a couple 2024-05-09 00:00:37 -07:00
Enrico Ros d7fc8c178f 1.16.0: enable cost by default 2024-05-08 15:39:03 -07:00
Enrico Ros 2894e16706 Merge branch 'release-1.16.0' 2024-05-08 15:11:10 -07:00
Enrico Ros c2340f3432 1.16.0: README 2024-05-08 15:03:32 -07:00
Enrico Ros 3b7b3106db Misc 2024-05-08 14:37:31 -07:00
Enrico Ros cff92819f9 1.16.0: News 2024-05-08 14:13:01 -07:00
Enrico Ros 2f981d852b Show message costs (option) 2024-05-08 13:11:21 -07:00
Enrico Ros 8eef74d776 1.16.0: version 2024-05-08 11:53:56 -07:00
Enrico Ros 60e46204dc Update default contextWindow to 8192
And override as per https://github.com/enricoros/big-AGI/pull/518#issuecomment-2090736347
2024-05-07 04:44:38 -07:00
Enrico Ros 6a5d783435 Show Costs on Hover. #480, #341 2024-05-07 04:33:39 -07:00
Enrico Ros 0223e076c4 LLM Options: improve 2024-05-07 03:54:28 -07:00
Enrico Ros ce80c78319 1.16.0: disable Reply-To (note: full in a different branch) 2024-05-07 02:55:14 -07:00
Enrico Ros cc0085ae61 Group vendors (disabled) 2024-05-07 02:46:41 -07:00
Enrico Ros f28e243b9d Chat: perfect execution error reporting, Fixes #523 2024-05-07 02:19:54 -07:00
Enrico Ros 2e4532593f Toggle JSON mode, Fixes #515 2024-05-07 00:58:02 -07:00
Enrico Ros 1f10905a03 Fix model temperaturs 2024-05-07 00:47:11 -07:00
Enrico Ros 88762db484 Anthropic: more precise usage link to show the token usage. Fixes #524 2024-05-06 23:48:41 -07:00
Enrico Ros 3b5ab0ac70 Beam: fix relaxed parsing. Fixes #528 2024-05-06 23:45:34 -07:00
Enrico Ros 8903c9296b OpenRouter: update parser 2024-05-06 22:56:09 -07:00
Enrico Ros 97858a3c94 docs/installation: mention optionality 2024-05-06 22:26:40 -07:00
Enrico Ros 0ec3e83518 Merge pull request #521 from dandv/patch-1
Docs: fix command to run local build
2024-05-06 22:25:51 -07:00
Enrico Ros 8c007b5bf7 Merge pull request #522 from dandv/patch-2
E: grammar in OpenAISourceSetup.tsx
2024-05-06 22:21:46 -07:00
Enrico Ros 768236b0e2 Merge pull request #525 from PrivTEC/patch-1
Correct typo in config-feature-browse.md
2024-05-06 22:20:18 -07:00
Enrico Ros 495d78b885 Perplexity: update models, with the ne online models 2024-05-06 21:20:02 -07:00
Enrico Ros 34b1e515fe Figure out unused model vendors 2024-05-06 21:04:02 -07:00
PrivTEC 79edbd3fa5 Correct typo in config-feature-browse.md
Corrected the typo from "proyy" to "proxy" in the file `config-feature-browse.md`. This change addresses a small, but significant error in the configuration documentation.
2024-05-06 03:51:04 +02:00
Dan Dascalescu f50d9994e2 E: grammar in OpenAISourceSetup.tsx 2024-05-04 22:22:34 +03:00
Dan Dascalescu 1603d3085f Docs: fix command to run local build 2024-05-04 22:16:12 +03:00
Enrico Ros ccf7036f33 Longer timeouts 2024-05-02 00:43:10 -07:00
Enrico Ros a0a1a5e3c1 Update the proxy desc 2024-05-02 00:09:17 -07:00
Enrico Ros fbf9120859 Default to llama3 2024-05-01 23:59:09 -07:00
Enrico Ros 8a770beec3 Update Ollama models 2024-05-01 23:05:30 -07:00
Enrico Ros 6b31669765 Fix diagrams in Dark mode. Fixes #520 2024-05-01 22:54:53 -07:00
Enrico Ros 26d72fc2d8 DMesage: add metadata 2024-04-25 22:17:36 -07:00
Enrico Ros 5eb56d0994 Move Diff'er. 2024-04-25 22:16:14 -07:00
Enrico Ros dbc4a922d5 Message Toolbar: good looking too. 2024-04-25 22:15:20 -07:00
Enrico Ros 141f423842 Diagrams: auto-switch 2024-04-25 22:15:00 -07:00
Enrico Ros 667f2433ab Diagrams: enter 2024-04-25 22:14:59 -07:00
Enrico Ros fd930ef548 Message Toolbar: fix disappearance 2024-04-25 22:14:49 -07:00
Enrico Ros 7eadfb1a63 E: PageDrawerHeader style 2024-04-25 22:11:28 -07:00
Enrico Ros 67cb07ac92 E: Style 2024-04-25 21:53:10 -07:00
Enrico Ros 96d28c43fc Manifest: update 2024-04-25 18:38:56 -07:00
Enrico Ros e57e3f5f0a Code: soft wrap. Closes #517 2024-04-25 11:41:34 -07:00
Enrico Ros 7b99bd71da Update overlay buttons 2024-04-25 11:36:58 -07:00
Enrico Ros 861a037321 Tweaks 2024-04-24 18:51:40 -07:00
Enrico Ros 84cbe6c434 RenderCode: title looks 2024-04-24 18:33:45 -07:00
Enrico Ros 2cbb811523 RenderCode: fix titles 2024-04-24 12:32:17 -07:00
Enrico Ros 8ef4faa10f Llms: update 'latest' 2024-04-24 12:25:34 -07:00
Enrico Ros f6a1c9bf52 Diagrams: fix centering 2024-04-24 03:42:50 -07:00
Enrico Ros 5d9f6fb4f5 Code blocks: undo the removal of ? 2024-04-24 03:31:00 -07:00
Enrico Ros 66840a8ecd Diagrams: center Mermaid and PlantUML diagrams 2024-04-24 03:30:28 -07:00
Enrico Ros a8ee6b255a Diagrams: improve hotfixes for Haiku and 3.5 2024-04-24 03:30:16 -07:00
Enrico Ros bd73d1c533 Diagrams: improve prompts 2024-04-24 03:30:05 -07:00
Enrico Ros e33c0ebc42 Fix code block separation in case of nested blocks. 2024-04-24 02:42:43 -07:00
Enrico Ros 57e4a35fee AppChat: extract chat executor (1st step) 2024-04-24 01:59:49 -07:00
Enrico Ros d490b57410 Diagrams: improve instructions 2024-04-24 01:59:08 -07:00
Enrico Ros 0416602e5f Diagrams: improve dialog 2024-04-24 01:59:01 -07:00
Enrico Ros ddc27b2eb9 BlockCode: improve looks 2024-04-24 01:36:32 -07:00
Enrico Ros 374deb147b Composer: improve ReplyTo integration 2024-04-24 00:03:30 -07:00
Enrico Ros d2eabd1ad0 Composer: correctness of activation 2024-04-24 00:02:42 -07:00
Enrico Ros efbc625cc3 Composer: onAction callback 2024-04-23 23:52:09 -07:00
Enrico Ros 91ae0b8cb0 Codeblocks: broader inclusion of filenames 2024-04-23 23:46:20 -07:00
Enrico Ros ddc5741b00 Attachments: getCollapsedAttachments 2024-04-23 23:18:39 -07:00
Enrico Ros 4729aca6b0 ReplyTo: improve bubble 2024-04-23 22:56:05 -07:00
Enrico Ros bb4fc3a70c Anthropic: relax key validation on custom deployments. Closes #511 2024-04-23 20:32:08 -07:00
Enrico Ros 5d8084b650 Llms: streaming: cleanups 2024-04-23 05:07:55 -07:00
Enrico Ros f316b892f5 Revert "Llms: fix Streaming timeouts (2)"
This reverts commit cbda1d7cd0.
2024-04-23 03:15:07 -07:00
Enrico Ros cbda1d7cd0 Llms: fix Streaming timeouts (2) 2024-04-23 02:07:20 -07:00
Enrico Ros 2f8e879976 Llms: fix Streaming timeouts 2024-04-23 01:45:27 -07:00
Enrico Ros cc0ac5ae3c React: fix llm naming 2024-04-22 23:59:30 -07:00
Enrico Ros 0185d24fb3 Beam: improve Merge disablement 2024-04-22 23:59:08 -07:00
Enrico Ros 97dbdc9c31 Beam: improve inlining (not ready yet) 2024-04-22 23:58:26 -07:00
Enrico Ros a07c66c9a3 Beam: lay down some inlining code 2024-04-22 21:49:14 -07:00
Enrico Ros 308bd25bc0 Beam: improve Tutorial 2024-04-22 21:48:00 -07:00
Enrico Ros 70066a03b6 Explainer Carousel: improvements 2024-04-22 21:44:17 -07:00
Enrico Ros a7f3872af3 Beam: update bar icons 2024-04-22 16:38:26 -07:00
Enrico Ros 22e10e675a RMB on Chat Avatar brings up the menu 2024-04-22 16:31:30 -07:00
Enrico Ros 89679e946d Beam: remove optionality (/beam, chat mode, composer button & shortcut, message beam from) 2024-04-22 16:12:09 -07:00
Enrico Ros 1d1bb9d3df Beam: explain a possible missing user message 2024-04-22 15:58:39 -07:00
Enrico Ros 8faf2b2595 Beam: move scroll button to the Gather pane 2024-04-22 15:58:18 -07:00
Enrico Ros e47ad9700e Anthropic: workaround for history[0] being assistant 2024-04-22 15:40:48 -07:00
Enrico Ros 372b19a057 Formulas: fix rendering for OpenAI-style inline '\(' and block '\[' latex. Fixes #508 2024-04-22 04:39:12 -07:00
Enrico Ros cbe156a868 Merge branch 'refs/heads/main-stable' 2024-04-22 02:57:08 -07:00
Enrico Ros 181a3881e2 Groq: update models
(cherry picked from commit 3eef03b303)
2024-04-22 02:56:47 -07:00
Enrico Ros 3eef03b303 Groq: update models 2024-04-22 02:52:19 -07:00
Enrico Ros ad56e3165c Beam: fix pixel-bound loading of presets 2024-04-22 02:27:07 -07:00
Enrico Ros b1a96b6e75 Beam: clear heuristics for llm selection 2024-04-22 02:26:48 -07:00
Enrico Ros 56419b1b4e Beam: persist the last configuration 2024-04-22 02:19:17 -07:00
Enrico Ros 372f14a9c5 Beam: auto-configure from Elo 2024-04-22 01:01:43 -07:00
Enrico Ros e1ec56a120 Beam: remove fallbackLlmId 2024-04-22 01:01:33 -07:00
Enrico Ros 5bb11249d6 Beam: remove reactive (view-based) ray conf 2024-04-22 01:01:17 -07:00
Enrico Ros 9fbcca1ff2 Llms: avoid name clash 2024-04-22 00:54:41 -07:00
Enrico Ros 323f2b2c3e Llms: cleaner 2024-04-22 00:52:56 -07:00
Enrico Ros b971d38dd5 Llms: heuristic to auto-pick the best diverse LLMs 2024-04-22 00:49:06 -07:00
Enrico Ros 278f479a3a Beam: rename terminate 2024-04-22 00:48:36 -07:00
Enrico Ros 03aea5678d Llms: misc 2024-04-22 00:17:49 -07:00
Enrico Ros b62b8ee7e6 Beam: App: fix state 2024-04-22 00:12:49 -07:00
Enrico Ros 63f55551e5 Beam: gather show all prompts 2024-04-21 23:30:41 -07:00
Enrico Ros b185fbc57d Beam: fallback llm Id 2024-04-21 23:24:52 -07:00
Enrico Ros ceb9d58e72 Beam: fix import rays 2024-04-21 23:10:47 -07:00
Enrico Ros a0bb515a4f Beam: minor bits 2024-04-21 22:28:36 -07:00
Enrico Ros 2cfac2f18b Beam: combine two menus into one 2024-04-21 22:05:08 -07:00
Enrico Ros d412f538b2 Make it more explicit we're only not rolling this one. 2024-04-21 21:30:26 -07:00
Enrico Ros 94f90ad861 Roll packages, but hold Next back. 2024-04-21 21:22:47 -07:00
Enrico Ros 4a402e7937 Roll pdfjs 2024-04-21 21:19:30 -07:00
Enrico Ros c226d6c391 Lock Next to 14.1, as 14.2 introduces the async/await messages when running/building, and we don't know what it means yet.
"The generated code contains 'async/await' because this module is using "topLevelAwait"."

See: https://github.com/vercel/next.js/issues/64792
2024-04-21 21:17:24 -07:00
Enrico Ros 67410e6c59 Revert "Roll packages." - Next v14.2.2 shows some async/await messages.
See https://github.com/vercel/next.js/issues/64792

This reverts commit 419c361147.
2024-04-21 21:12:32 -07:00
Enrico Ros 419c361147 Roll packages. 2024-04-21 20:39:56 -07:00
Enrico Ros 3769a53ffa Merge pull request #507 from mludvig/arm-build-1
Build multi-arch docker image for x64-64 and ARM64
2024-04-15 22:04:07 -07:00
Michael Ludvig ec4aaa3bfb Cleanup 2024-04-16 16:51:57 +12:00
Michael Ludvig be52680fcd Put back hashes and comments 2024-04-16 16:20:48 +12:00
Michael Ludvig 9d41ab9339 Merge branch 'enricoros:main' into arm-build-1 2024-04-16 12:36:23 +12:00
Michael Ludvig f126fc3087 Cleanup 2024-04-16 11:52:58 +12:00
Michael Ludvig 764377037c Disabled arm 32 again (not supported by Prisma) 2024-04-16 11:22:15 +12:00
Michael Ludvig 8e09eaab45 Add sha tag 2024-04-16 11:10:32 +12:00
Michael Ludvig 6523da186c Update versions, add arm32 2024-04-16 10:29:18 +12:00
Michael Ludvig 6471fd8b6f Enable action 2024-04-16 10:01:41 +12:00
Michael Ludvig 247a74881a Added buildx support 2024-04-15 11:34:42 +12:00
Enrico Ros 3ef09f0a5f Models: upgrade data structure to v2 - auto-pick 2024-04-12 05:50:46 -07:00
Enrico Ros b924d331f9 Models: upgrade data structure to v2 2024-04-12 05:36:18 -07:00
Enrico Ros 14041b6012 Beam: simplify a bit 2024-04-12 03:44:54 -07:00
Enrico Ros 2c6cc5ecec Cleanup models update logic 2024-04-12 02:44:14 -07:00
Enrico Ros ac022b1df0 Models: adding prices and benchmarks for a few models 2024-04-12 02:09:14 -07:00
Enrico Ros 0a2081de08 Better Beam Hint 2024-04-12 01:06:25 -07:00
Enrico Ros 64a8e554c7 Designer update 2024-04-12 00:46:58 -07:00
Enrico Ros 082d29fd2f Improve style 2024-04-12 00:45:00 -07:00
Enrico Ros ba5cf9d002 Composer: show the bubble 2024-04-12 00:22:55 -07:00
Enrico Ros 57a55318df Stabilize 2024-04-12 00:07:40 -07:00
Enrico Ros e70f4f7a59 ChatMessageList: this side is probably done 2024-04-11 21:10:56 -07:00
Enrico Ros 1d217fad67 Warning 2024-04-11 21:10:39 -07:00
Enrico Ros e95d46f085 ConversationHandler: prepare chat overlays 2024-04-11 21:08:04 -07:00
Enrico Ros f4577878e1 ChatMessage: Reply on 2024-04-11 20:36:32 -07:00
Enrico Ros 1bd1e5c8e3 ChatMessage: Toolbar complete 2024-04-11 20:19:30 -07:00
Enrico Ros c975dee965 ChatMessageList: remove menu items if t2i off 2024-04-11 19:22:03 -07:00
Enrico Ros 9d690f4219 ChatMessage: fix double-closure 2024-04-11 18:22:12 -07:00
Enrico Ros 29ddb3f58d ChatMessage: improve menu 2024-04-11 18:12:44 -07:00
Enrico Ros 8626bc0b1c BlocksRenderer: selection color 2024-04-11 18:12:37 -07:00
Enrico Ros c362cf6596 Propagate information on whether this can be spoken 2024-04-11 17:52:50 -07:00
Enrico Ros 97264fc5ff ChatMessage: toolbar framework 2024-04-11 17:04:44 -07:00
Enrico Ros 494c4409c1 BlocksRenderer: more v-padding for an improved mouse-up behavior 2024-04-11 16:40:47 -07:00
Enrico Ros d46e366c81 Blocks Renderer: use refs 2024-04-11 13:16:13 -07:00
Enrico Ros 6afe33ee9c decolor 2024-04-11 10:13:54 -07:00
Enrico Ros 903c9e1cc3 Improve options 2024-04-11 10:12:03 -07:00
Enrico Ros 3ef43fc3f5 Merge branch 'joriskalz-chat-with-youtube' 2024-04-11 09:58:56 -07:00
Enrico Ros b1c3be05dd Integrate YouTube transcriber (hidden by default) 2024-04-11 09:58:45 -07:00
Enrico Ros efee23b4a7 Update shadows 2024-04-11 09:49:13 -07:00
Enrico Ros 06b67a7586 Merge branch 'chat-with-youtube' of https://github.com/joriskalz/big-AGI-dev into joriskalz-chat-with-youtube 2024-04-11 09:33:56 -07:00
Joris Kalz 889a2dbf9d Remvoved unwanted new line. 2024-04-11 11:45:03 +01:00
Joris Kalz 2f80fcc888 Removed comments 2024-04-11 11:43:54 +01:00
Joris Kalz f7ee479c1d Removed comments 2024-04-11 11:36:27 +01:00
Joris Kalz 94fa0981fe Update YouTube Transcriber voiceId in data.ts 2024-04-11 11:33:55 +01:00
Joris Kalz 4c74afe438 Update YouTube Transcriber system message in data.ts 2024-04-11 11:33:42 +01:00
Joris Kalz f76cea22de Fix YouTube Transcriber activation bug in PersonaSelector component 2024-04-10 22:18:35 +01:00
Joris Kalz 3d49110808 Implement handleAddMessage function in PersonaSelector component 2024-04-10 22:14:15 +01:00
Joris Kalz 88a4579f7a Refactor PersonaSelector component to handle YouTube Transcriber tile click 2024-04-10 22:00:29 +01:00
Joris Kalz 241bde0333 Update YouTubeURLInput component to handle YouTube video transcripts 2024-04-10 21:48:20 +01:00
Joris Kalz 73c7867cd6 Add YouTube Transcriber persona and handle YouTube Transcriber tile click 2024-04-10 11:53:48 +01:00
Enrico Ros b35254f7ad Qol 2024-04-10 03:14:15 -07:00
Enrico Ros 213e78c956 Beam: save the merge model, and shrink rays when loading a smaller preset 2024-04-10 03:01:18 -07:00
Enrico Ros 7bf552c491 1.15.1 2024-04-10 01:09:25 -07:00
Enrico Ros 3bf9923f86 Update README 2024-04-10 01:03:06 -07:00
Enrico Ros a6a8a28f59 Update models pricing 2024-04-10 00:33:16 -07:00
Enrico Ros 56a8e452bf OpenAI: 2024-04-09 models 2024-04-10 00:17:08 -07:00
Enrico Ros 6bec0bf70d Models: precise id matching 2024-04-09 23:15:17 -07:00
Enrico Ros 5dc9c8f90e Gemini: support Pro 1.5 2024-04-09 22:41:05 -07:00
Enrico Ros e3290e12b1 Fix mic layout 2024-04-09 22:37:47 -07:00
Enrico Ros 9f37ce9e42 Warn if clipboard access is prevented 2024-04-09 19:49:52 -07:00
Enrico Ros 8904c0c811 E: Consistent file names, shortcuts. 2024-04-09 19:40:56 -07:00
Enrico Ros b0d021b7f2 E: Ctrl+O opens chat file 2024-04-09 17:41:53 -07:00
Enrico Ros 0175f3b8a1 E: Manifest File Handlers 2024-04-09 17:04:48 -07:00
Enrico Ros 0fa9d5bf62 E: Save conversations. Closes #466 2024-04-09 16:44:40 -07:00
Enrico Ros 4919e38e3e OpenAI-derivatives: Remove UI validation - never helped. Fixes #446 2024-04-09 16:28:09 -07:00
Enrico Ros 2e99533f96 Fit on mobile 2024-04-09 16:21:05 -07:00
Enrico Ros f095645d89 Merge pull request #498 from dogmatic69/trailing-whitespace
chore: remove trailing whitespace
2024-04-09 01:48:19 -07:00
Enrico Ros 757c83142e Merge pull request #497 from dogmatic69/env-docs
chore: link to env docs
2024-04-09 01:47:50 -07:00
Carl Sutton 36d274ca9f chore: remove trailing whitespace 2024-04-09 10:13:35 +02:00
Carl Sutton ec11b61f67 chore: link to env docs 2024-04-09 10:09:37 +02:00
Enrico Ros 7765271d63 PPLX: fix alternation 2024-04-09 00:55:04 -07:00
Enrico Ros 7c2464bba7 PPLX: fix models 2024-04-09 00:32:27 -07:00
Enrico Ros 17e010f93c Anthropic: fix empty messages 2024-04-09 00:19:48 -07:00
Enrico Ros 452d630a2a Tryfix for the Autocomplete 2024-04-08 23:39:06 -07:00
Enrico Ros f317a3e38f Test client-side fetch (no cors) 2024-04-08 22:15:15 -07:00
Enrico Ros f56195058e Fix ssr issue 2024-04-08 22:13:46 -07:00
Enrico Ros 2e93dbb10c Improve Error reporting 2024-04-08 21:01:57 -07:00
Enrico Ros f862456d73 Decrease errors 2024-04-08 18:52:11 -07:00
Enrico Ros d99b0b2137 Reduce errors 2024-04-08 18:43:01 -07:00
Enrico Ros 1d390f9aa7 3,000 2024-04-07 16:18:45 -07:00
Enrico Ros 514beb7940 Merge pull request #492 from enricoros/main
Update BeamFusionGrid.tsx
2024-04-06 16:48:46 -07:00
Enrico Ros c7bdfce734 Update BeamFusionGrid.tsx 2024-04-06 16:47:36 -07:00
Enrico Ros e5fe4b06ad Show warning for non-US merges 2024-04-06 15:58:38 -07:00
Enrico Ros 89b7c265d3 show URL attachments as well 2024-04-06 15:41:35 -07:00
Enrico Ros 698c31943e Centralize Lang 2024-04-06 14:27:21 -07:00
Enrico Ros b70060d46e Beam: understand tutorial usage 2024-04-06 14:27:10 -07:00
Enrico Ros 6ddc5ef53e Roll packages 2024-04-06 13:57:57 -07:00
Enrico Ros 212023c7e4 Merge pull request #484
Update README.md (Added Midori AI subsystem to the readme)
2024-04-05 18:30:32 -07:00
Enrico Ros b687f23c95 Anthropic: server status. #485 2024-04-05 14:07:13 -07:00
Luna Midori 7a05d01554 Update README.md 2024-04-03 15:17:43 -07:00
Enrico Ros 78e3a57857 parsing of HTML code blocks 2024-04-02 21:07:35 -07:00
Enrico Ros 79d0c96b20 Gemini: call out RECITATIONS 2024-04-02 20:53:26 -07:00
Enrico Ros 21ed38a20e DuoTonal for AI functions 2024-04-02 19:20:35 -07:00
Enrico Ros d8b1f99114 Divider 2024-04-02 18:42:33 -07:00
Enrico Ros b0fb1b9890 Fix build 2024-04-02 01:48:11 -07:00
Enrico Ros a63932cff2 Show HTML code when beaming, by default 2024-04-02 01:42:06 -07:00
Enrico Ros 0b22165d2a Beam: remove link 2024-04-02 01:29:23 -07:00
Enrico Ros 41b1951abe Merge pull request #481 from aj47/patch-1
Update README.md typo
2024-04-01 23:41:37 -07:00
AJ (@techfren) 353431e54c Update README.md 2024-04-02 17:41:08 +11:00
Enrico Ros 7b232dd7d8 Renamed vercel.json to vercel_PRODUCTION.json to get it out of the way and fix #468
Fix #468 once and for all. Documentation on the env
2024-04-01 18:05:27 -07:00
Enrico Ros d32adf9dbf 1.15.0: Add hackernews callout 2024-04-01 17:17:33 -07:00
Enrico Ros 940d490217 1.15.0: Beam News improved copy 2024-04-01 15:29:08 -07:00
Enrico Ros 46e41e38cf 1.15.0: Beam News callout 2024-04-01 15:15:52 -07:00
Enrico Ros 276ff8f995 Merge branch 'release-1.15.0' 2024-04-01 15:05:10 -07:00
Enrico Ros 030837fccf 1.15.0: Readme and Changelog 2024-04-01 15:04:15 -07:00
Enrico Ros a7d38aefb1 1.15.0: Update News 2024-04-01 14:42:07 -07:00
Enrico Ros 230a0d7caf Beam: update intro. 2024-04-01 14:21:12 -07:00
Enrico Ros 6e14e43c78 Beam: update in-app explainer. 2024-04-01 14:07:33 -07:00
Enrico Ros e6389f08be Branch before delete 2024-03-30 23:19:48 -07:00
Enrico Ros a4edeb098e 1.15.0: news placeholder 2024-03-30 20:14:30 -07:00
Enrico Ros 093c536415 1.15.0: version number 2024-03-30 19:04:37 -07:00
Enrico Ros 7479b50fea 1.15.0: Disable Title Bar Setting (2 people got confused) 2024-03-30 19:03:45 -07:00
Enrico Ros ebce36d043 1.15.0: Package Version 2024-03-30 18:57:46 -07:00
Enrico Ros 77bab1aa74 Beam: earlyaccess: edit the Custom merges 2024-03-30 01:52:01 -07:00
Enrico Ros ebcac3405c Beam: custom: improve icon 2024-03-30 00:56:15 -07:00
Enrico Ros d2781a6f87 Beam: custom: do not auto-start Custom 2024-03-30 00:40:23 -07:00
Enrico Ros f5954f5bb3 Beam: Gather: change some prop names 2024-03-30 00:40:08 -07:00
Enrico Ros 6baf694d6f Beam: earlyaccess: remove Show Dev Methods 2024-03-30 00:20:08 -07:00
Enrico Ros cb3b586d4d Beam: custom: improve hardcoding 2024-03-30 00:16:41 -07:00
Enrico Ros f68789ab20 Beam: earlyaccess: add Menu Option for response identification 2024-03-30 00:13:33 -07:00
Enrico Ros 0c6a3f1917 Beam: earlyaccess: Fix the "checklist issue" with the mentioned "unicode bullet" 2024-03-29 23:45:53 -07:00
Enrico Ros 05fccaf982 Beam: earlyaccess: Improve popup menu to hint at saving/loading model combos 2024-03-29 23:42:47 -07:00
Enrico Ros 7340b9ecc2 Beam: earlyaccess: Address the UI problem where the menu option does not open when the screen is maximized 2024-03-29 23:15:57 -07:00
Enrico Ros 78eb4ebe0b Beam: earlyaccess: Correct the "Synthesizing" typo. 2024-03-29 23:07:03 -07:00
Enrico Ros b1453a34ec Beam: fix issue with older installs 2024-03-29 21:48:28 -07:00
Enrico Ros c357e9e2f5 Beam: ensure non-empty gather messages 2024-03-29 21:48:28 -07:00
Enrico Ros 98717bf8a9 Beam: scroller: smaller 2024-03-29 21:48:28 -07:00
Enrico Ros d7077ada0e Beam: scroll the instruciton gen too 2024-03-29 21:48:28 -07:00
Enrico Ros 64f63ed1d3 Beam: score to 100 2024-03-29 21:48:28 -07:00
Enrico Ros 2a27f6c30d Beam: fusion zone 2024-03-29 21:48:28 -07:00
Enrico Ros 9fdddeaba8 Beam: Checklist -> Guided 2024-03-29 21:48:28 -07:00
Enrico Ros 2cfa5e93e4 Beam: improve prompts 2024-03-29 21:48:28 -07:00
Enrico Ros 778ac14344 Beam: enhance checklist quality 2024-03-29 21:48:28 -07:00
Enrico Ros 85fcf8be61 Beam: re-merge will not change the model 2024-03-29 21:48:28 -07:00
Enrico Ros b31eb09015 Beam: no green shade 2024-03-29 21:48:28 -07:00
Enrico Ros 5154dd1740 Beam: improve Fusion layout 2024-03-29 21:48:27 -07:00
Enrico Ros 274f11ef1d Beam: change the Fusion model 2024-03-29 21:48:27 -07:00
Enrico Ros aeb1acf458 Beam: bits 2024-03-29 21:48:27 -07:00
Enrico Ros a204f4a58e Beam: Ray grid: bits 2024-03-29 21:48:27 -07:00
Enrico Ros 8e4a57aa01 Beam: auto-fit 2024-03-29 21:48:27 -07:00
Enrico Ros 797ed0a553 Beam: shorter scroll on mobile beams 2024-03-29 21:48:27 -07:00
Enrico Ros 663bc0d471 Beam: shadow on mobile scatter 2024-03-29 21:48:27 -07:00
Enrico Ros 8d7e2d2c46 Beam: remove lastScatterLlmId 2024-03-29 21:48:27 -07:00
Enrico Ros 19d96bb30b Beam: remove llm Linkage 2024-03-29 21:48:27 -07:00
Enrico Ros 47f2f20d9c Beam: relax checklist parsing 2024-03-29 21:48:27 -07:00
Enrico Ros 12c7c634c0 Beam: improve LLM usage 2024-03-29 21:48:27 -07:00
Enrico Ros 9a322c150a Beam: reduce space 2024-03-29 21:48:27 -07:00
Enrico Ros 1a3bc4f666 Beam: move instructions 2024-03-29 21:48:27 -07:00
Enrico Ros d4881b1ce5 Beam: move to modules 2024-03-29 21:48:27 -07:00
Enrico Ros a2ad2df473 Beam: prompt update 2024-03-29 21:48:27 -07:00
Enrico Ros 541c5bd1c3 Beam: prompt update 2024-03-29 21:48:27 -07:00
Enrico Ros b744e9673b Beam: Checklist done 2024-03-29 21:48:27 -07:00
Enrico Ros bb94b7c5c6 Beam: prompt updates 2024-03-29 21:48:27 -07:00
Enrico Ros e9ff57d5e1 Beam: Gather: User Input 2024-03-29 21:48:27 -07:00
Enrico Ros 179245457c Beam: Gather: extract instructions 2024-03-29 21:48:27 -07:00
Enrico Ros 1493f74691 Beam: Gather: render 2024-03-29 21:48:27 -07:00
Enrico Ros 4857503ed3 Beam: Gather: intermediate components 2024-03-29 21:48:27 -07:00
Enrico Ros a0e38b4f0c Beam: Gather: begin ui production 2024-03-29 21:48:27 -07:00
Enrico Ros 1d62cad9e9 Beam: Gather: large state redux 2024-03-29 21:48:27 -07:00
Enrico Ros 855761020c Beam: Instructions: interrupt the fake user op 2024-03-29 21:48:27 -07:00
Enrico Ros 0950d06dfb Beam: Instructions: improve state machinery much 2024-03-29 21:48:27 -07:00
Enrico Ros 1496402325 Beam: layout seems ok 2024-03-29 21:48:27 -07:00
Enrico Ros 77e2c4babb Beam: stop this madness 2024-03-29 21:48:27 -07:00
Enrico Ros a465082984 Beam: Dev methods by default 2024-03-29 21:48:27 -07:00
Enrico Ros 025fdac686 Beam: Iconoclastic 2024-03-29 21:48:27 -07:00
Enrico Ros 6bde5ec64c Beam: Gather: Fin of Fin 2024-03-29 21:48:27 -07:00
Enrico Ros f099a9ec39 Beam: Gather Cleanups galore 2024-03-29 21:48:27 -07:00
Enrico Ros 5bfcef92ee Beam: Persist (and get off the way) more state 2024-03-29 21:48:27 -07:00
Enrico Ros 79a8fbd881 Beam: Extract the Module Beam Store 2024-03-29 21:48:27 -07:00
Enrico Ros 7f96a14cf6 Beam: Debug: swap properties 2024-03-29 21:48:27 -07:00
Enrico Ros 5fe6d70713 Beam: App: more clearer debug 2024-03-29 21:48:27 -07:00
Enrico Ros dcba4dd4bc Beam: App: clearer debug 2024-03-29 21:48:27 -07:00
Enrico Ros ccbe77913b Beam: Gather: beginning of output 2024-03-29 21:48:27 -07:00
Enrico Ros 2844cb81c2 Beam: Gather: wait indicator 2024-03-29 21:48:27 -07:00
Enrico Ros d86e8e5920 Beam: bits 2024-03-29 21:48:27 -07:00
Enrico Ros 9665fa1eb4 Beam: good button on mobile 2024-03-29 21:48:27 -07:00
Enrico Ros 2788ef679b Beam: scroll-fix 2024-03-29 21:48:27 -07:00
Enrico Ros e1a88e1fd8 Beam: move gapper 2024-03-29 21:48:27 -07:00
Enrico Ros 32163c5302 Beam: bottom gapper 2024-03-29 21:48:26 -07:00
Enrico Ros 2d3d5efe87 Beam: fix merge dim when inactive 2024-03-29 21:48:26 -07:00
Enrico Ros e1bbba392c Beam: Scatter: save file rename 2024-03-29 21:48:26 -07:00
Enrico Ros ed642c856b Beam: Scatter: complete the dialog 2024-03-29 21:48:26 -07:00
Enrico Ros 927e462f7a Beam: Scatter: preset save (full) 2024-03-29 21:48:26 -07:00
Enrico Ros e250499a3b Beam: Scatter: preset save (part) 2024-03-29 21:48:26 -07:00
Enrico Ros 91d96a6639 Beam: bits 2024-03-29 21:48:26 -07:00
Enrico Ros 104ec4c87c Beam: improve Composer button 2024-03-29 21:48:26 -07:00
Enrico Ros 0a7e8436c3 Beam: Gather: starts to work like a charm 2024-03-29 21:48:26 -07:00
Enrico Ros 9e597e0a28 Beam: Gather: first response! 2024-03-29 21:48:26 -07:00
Enrico Ros 01fbb5d47c Beam: Gather: rename executor to instructions 2024-03-29 21:48:26 -07:00
Enrico Ros 6517d16337 Beam: Gather: Mega Pint of state cleanup 2024-03-29 21:48:26 -07:00
Enrico Ros 0e636adf28 Beam: Gather: more state cleanuppery 2024-03-29 21:48:26 -07:00
Enrico Ros 0bb281237b Beam: Gather: some customization 2024-03-29 21:48:26 -07:00
Enrico Ros 2b224376c2 Beam: Gather: further improvements 2024-03-29 21:48:26 -07:00
Enrico Ros e510b369d7 Beam: Gather: ui fix 2024-03-29 21:48:26 -07:00
Enrico Ros a0de1f7230 Beam: Gather: wire things up 2024-03-29 21:48:26 -07:00
Enrico Ros 4591132269 Beam: the ghost in the machine 2024-03-29 21:48:26 -07:00
Enrico Ros a03de8d490 Beam: Gather: And Here We Go (Again -final.r002.copy.goodone) 2024-03-29 21:48:26 -07:00
Enrico Ros 27bcfec17e Beam: Gather: And Here We Go (Again) 2024-03-29 21:48:26 -07:00
Enrico Ros f6dbec3e1d Beam: Gather: And Here We Go 2024-03-29 21:48:26 -07:00
Enrico Ros aebc45f705 Beam: Gather: messaging & lime 2024-03-29 21:48:26 -07:00
Enrico Ros 310c60b9d9 Beam: Gather: pre-fusion 2024-03-29 21:48:26 -07:00
Enrico Ros bcba67c209 Beam: 4->6px 2024-03-29 21:48:26 -07:00
Enrico Ros fc013aed52 Beam: Gather: icons 2024-03-29 21:48:26 -07:00
Enrico Ros 8ad41c059b Beam: Scatter: cleaner 2024-03-29 21:48:26 -07:00
Enrico Ros 8eaf8db850 Beam: Gather: cleaner 2024-03-29 21:48:26 -07:00
Enrico Ros 896883766c Beam: Gather: perfect styles 2024-03-29 21:48:26 -07:00
Enrico Ros 258dacf3ed Beam: Gather: higher contrast 2024-03-29 21:48:26 -07:00
Enrico Ros 242243f485 Beam: Gather: even better ux 2024-03-29 21:48:26 -07:00
Enrico Ros a18436dce1 Beam: Gather: real good ux 2024-03-29 21:48:26 -07:00
Enrico Ros 5323cbc00e Beam: Gather: simplify state 2024-03-29 21:48:26 -07:00
Enrico Ros ddd3b137ac Beam: Gather: convert to Fusion IDs 2024-03-29 21:48:26 -07:00
Enrico Ros 94550088e5 Beam: Gather: show/hide dev methods 2024-03-29 21:48:26 -07:00
Enrico Ros 1375ca6f5c Beam: Gather: style multiline 2024-03-29 21:48:26 -07:00
Enrico Ros e4c4fe0495 Beam: Gather: start from 0 2024-03-29 21:48:26 -07:00
Enrico Ros 2fa5277e56 Beam: Gather: add Eval 2024-03-29 21:48:26 -07:00
Enrico Ros b73ad8fdc1 Beam: Gather: icons 2024-03-29 21:48:26 -07:00
Enrico Ros 9cc281e65e Beam: redo optionality 2024-03-29 21:48:26 -07:00
Enrico Ros d62107d39b 1.15.0: Cover image 2024-03-29 21:47:33 -07:00
Enrico Ros 4a8d20ad72 News: raise the quality 75 -> 90 2024-03-29 21:47:33 -07:00
Enrico Ros 5acb72c39b T2I: max 4 columns 2024-03-29 21:47:33 -07:00
Enrico Ros 67e8236a60 Fix deprecation 2024-03-29 21:47:32 -07:00
Enrico Ros 18b8853f82 Merge branch 'main-stable' 2024-03-29 21:39:05 -07:00
Enrico Ros 65c7df7938 Backend: auto-configuration. Fixes #436 2024-03-29 05:07:37 -07:00
Enrico Ros 15678cdfa2 Backend: removed onSuccess callbacks! 2024-03-29 05:07:36 -07:00
Enrico Ros 6cd6c62046 Backend: migration to async fetch from Query. plus consistency of behaviors 2024-03-29 05:07:36 -07:00
Enrico Ros dbf92805a2 Backend: reprio 2024-03-29 05:07:35 -07:00
Enrico Ros 11fc9a7b85 Backend: capability variables 2024-03-29 05:07:35 -07:00
Enrico Ros 8bc970ff57 Backend: autoconf only on chat 2024-03-29 05:07:34 -07:00
Enrico Ros a16eefd97b react-query: disable refetch on focus by default 2024-03-29 05:07:34 -07:00
Enrico Ros ca5e5b820c Backend: autoconf base logic 2024-03-29 05:07:33 -07:00
Enrico Ros f73ad52441 Backend: ->getBackendCapabilities() 2024-03-29 05:07:33 -07:00
Enrico Ros 729ec1d1bf Backend: config hash, to detect backend config updates 2024-03-29 05:07:32 -07:00
Enrico Ros 4adb30b861 AppChat: use intent to navigate to it from the link importer 2024-03-29 05:07:32 -07:00
Enrico Ros 999f6de45f Serverless Functions timeout: set it in the Vercel functions as the conditional was not working. Fix (again) #468 2024-03-28 23:20:40 -07:00
Enrico Ros 70686502b4 Revert "Set the Vercel serverless max duration as env variable. Fixes #468"
This reverts commit d17a980151.
2024-03-28 23:16:11 -07:00
Enrico Ros d17a980151 Set the Vercel serverless max duration as env variable. Fixes #468 2024-03-28 23:12:25 -07:00
Enrico Ros 7fa5947030 Chat Nav Grouping: when unset, the search won't sort by frequency
TODO: needs a better UX pattern here.
2024-03-28 22:48:12 -07:00
Enrico Ros de8f120fd4 Update README.md 2024-03-28 17:01:11 -07:00
Enrico Ros 9b54603264 Update README.md 2024-03-28 17:00:10 -07:00
Enrico Ros 698c77d7ba Tease the upcoming Beam 2024-03-28 16:53:34 -07:00
Enrico Ros 18d83a4d18 PersonaSelector: better tiles 2024-03-27 22:11:52 -07:00
Enrico Ros 8e849d93b2 Style fixes 2024-03-27 21:59:49 -07:00
Enrico Ros 4ca42f028b SVG: parse alternatives 2024-03-27 21:23:19 -07:00
Enrico Ros 3118337879 Timeout on Vercel/Serverless raised to 25 (for Browsing/Browserless requests) 2024-03-27 21:22:47 -07:00
Enrico Ros db4490affb SVG: improve compat with Opus 2024-03-27 18:33:28 -07:00
Enrico Ros 51ab79384e SVG: more compatible 2024-03-27 18:33:28 -07:00
Enrico Ros 3ee30a252d Creator: fixes 2024-03-27 18:33:28 -07:00
Enrico Ros b883566ebb Shrink the Folders list when running out of space (at twice the Chat Titles rate) 2024-03-27 18:32:46 -07:00
Enrico Ros ac78fb85b8 Shadow 2024-03-27 18:32:46 -07:00
Enrico Ros 0d2b11d0c4 Fonts 2024-03-27 18:32:45 -07:00
Enrico Ros 5b610c88c1 Gemini: fix RECITATION 2024-03-27 18:32:45 -07:00
Enrico Ros bf444ce043 Attachments: support RMB 2024-03-27 18:32:45 -07:00
Enrico Ros c91c027dab Compress icons 2024-03-27 18:32:44 -07:00
Enrico Ros 81fd87c510 Reduced badges 2024-03-27 18:32:44 -07:00
Enrico Ros 9da174a962 Roll packages 2024-03-27 18:32:44 -07:00
Enrico Ros 84f54a7e65 PersonaSelector: improve examples 2024-03-27 18:25:41 -07:00
Enrico Ros baeecf1464 PersonaSelector: reshade 2024-03-27 18:25:34 -07:00
Enrico Ros f2fdd39c96 Persona Selector: smaller tiles 2024-03-27 18:25:18 -07:00
Enrico Ros 53b074d78e Personas: show enablement, not disablement 2024-03-27 18:22:38 -07:00
Enrico Ros f4fc1e6775 Persona: update example 2024-03-27 18:22:28 -07:00
Enrico Ros dba791b8db Personas: update Dev examples 2024-03-27 18:22:24 -07:00
Enrico Ros 750fa02621 Personas: update custom task 2024-03-27 18:22:20 -07:00
Enrico Ros 7a67816111 Update default prompt. 2024-03-27 18:22:14 -07:00
Enrico Ros 613625644e LocalAI T2I: integration skel 2024-03-23 04:16:59 -07:00
Enrico Ros 0e25071ef0 Prevent pull-to-refresh on mobile - would be triggered while scrolling up 2024-03-22 22:40:56 -07:00
Enrico Ros ed1932cd26 Link env vars 2024-03-20 23:08:47 -07:00
Enrico Ros 67b89213d0 Your input 2024-03-20 22:39:34 -07:00
Enrico Ros 814f142c5f Fix zIndex of the ScrollToBottomButton 2024-03-20 22:39:33 -07:00
Enrico Ros 16cd3e7d5a Desktop Nav: fix key 2024-03-20 04:55:30 -07:00
Enrico Ros c5dcb8faef Beam: Gather: disable for now 2024-03-20 04:54:53 -07:00
Enrico Ros 6b46c022f9 Beam: Gather: improve prompt definitions 2024-03-20 03:56:15 -07:00
Enrico Ros 88ef05fc72 Beam: Gather: baseline prompts 2024-03-20 03:13:48 -07:00
Enrico Ros 445ea367fc Beam: copy Ray to clipboard 2024-03-20 02:10:20 -07:00
Enrico Ros c819554f43 Prompt-mixin: custom filters 2024-03-20 02:08:01 -07:00
Enrico Ros bbc8a79ded Beam: inline edit the Custom 2024-03-20 01:25:25 -07:00
Enrico Ros 3d181bc10d Beam: optimize App 2024-03-20 00:40:49 -07:00
Enrico Ros ba5478f382 Beam: Fusion: improved Input 2024-03-20 00:25:52 -07:00
Enrico Ros 136c993c8d Beam: Fusion: show prompts option 2024-03-19 23:00:23 -07:00
Enrico Ros 6cf18ea4e8 fix tooltip missing on nav 2024-03-19 22:46:54 -07:00
Enrico Ros fe7f56c82e fix check icon 2024-03-19 22:46:45 -07:00
Enrico Ros 6c580f1e43 Beam: Gather: edit custom instructions 2024-03-19 19:51:30 -07:00
Enrico Ros f171cd4f03 Beam: Gather: enable customization 2024-03-19 18:12:38 -07:00
Enrico Ros ea109e6c30 EditRounded 2024-03-19 13:51:36 -07:00
Enrico Ros f514eed226 Beam: Gather: instruction definition 2024-03-19 13:47:49 -07:00
Enrico Ros 274ba80149 Beam: Gather: bits 2024-03-19 11:57:45 -07:00
Enrico Ros 46b4dfc458 Beam: Gather: reinit state 2024-03-19 11:52:24 -07:00
Enrico Ros 4af8f4ff6a [desktop] Improve overflow 2024-03-19 11:40:20 -07:00
Enrico Ros df5810d695 [desktop] Application Overflow menu 2024-03-19 11:32:25 -07:00
Enrico Ros d9ad96c374 Beam: 'from chat' 2024-03-19 02:21:43 -07:00
Enrico Ros 06cc93fd82 Beam: begin Fusion state 2024-03-19 02:16:50 -07:00
Enrico Ros 41da63765f Beam: state cleanup 2024-03-19 01:33:27 -07:00
Enrico Ros 3975411c78 Beam: slices pattern 2024-03-19 01:09:38 -07:00
Enrico Ros fc2e75ef61 Beam: separated gather and scatter, physically 2024-03-19 00:00:40 -07:00
Enrico Ros ef0f2dd3d0 Beam: bits 2024-03-18 23:44:40 -07:00
Enrico Ros 548c3c5d72 Beam: clean styles 2024-03-18 20:32:15 -07:00
Enrico Ros d2e3a0cb8e Beam: add gather config and fusion 2024-03-18 20:16:38 -07:00
Enrico Ros 9cdace6f81 Beam: rename Panes 2024-03-18 19:09:11 -07:00
Enrico Ros 12f020570e Beam: extract Scatter input 2024-03-18 19:07:55 -07:00
Enrico Ros bef2551eec Beam: Gather commands shall be ok 2024-03-18 18:49:05 -07:00
Enrico Ros 7e20f8c189 Beam: wire Gather 2024-03-18 18:30:31 -07:00
Enrico Ros 56e8390e55 Beam: Fusion rename 2024-03-18 17:52:32 -07:00
Enrico Ros 89fff16385 Beam: Gather style 2024-03-18 04:00:00 -07:00
Enrico Ros 2cf15a24eb Beam: Gather layout 2024-03-18 03:48:30 -07:00
Enrico Ros 512e867034 Beam: final style fixes on Beam 2024-03-18 02:44:37 -07:00
Enrico Ros ce8c55c3c7 Beam: the beam panel seems done 2024-03-18 02:24:56 -07:00
Enrico Ros 8e0d904d9a Beam: Style updates 2024-03-18 00:57:26 -07:00
Enrico Ros 6c846a8ae7 Beam: very large state update 2024-03-18 00:03:10 -07:00
Enrico Ros 5004469fe9 Beam: DRay -> BRay 2024-03-17 21:54:42 -07:00
Enrico Ros 14d0af74ed Beam: extract rays 2024-03-17 21:51:38 -07:00
Enrico Ros 5a76cf9486 Beam: move the pre-beam where it shall go 2024-03-17 21:34:06 -07:00
Enrico Ros 82901ccd02 Beam: desktop sticky controls for Scatter and Gather 2024-03-17 21:26:56 -07:00
Enrico Ros 1dc9d66673 Beam: unused callout 2024-03-17 21:20:51 -07:00
Enrico Ros a0cbfaf390 Beam: fix explainer layout 2024-03-17 21:14:35 -07:00
Enrico Ros 9a01ae61ef ChatDrawer (item groups): sticky 2024-03-17 17:01:22 -07:00
Enrico Ros 91837d5acd Optimize 2024-03-17 16:47:00 -07:00
Enrico Ros 1b9ebdda22 Beam: Maximized Mode(al) 2024-03-17 16:43:33 -07:00
Enrico Ros b6f6177af3 Beam: improve looks 2024-03-17 16:08:11 -07:00
Enrico Ros d35486196b Scroll/Beam: embeddable ScrollToBottomButton 2024-03-17 16:05:31 -07:00
Enrico Ros 1603637e3b Scroll/Beam: improve usage 2024-03-17 15:53:44 -07:00
Enrico Ros 8f20840169 Beam: optimize when in Chat 2024-03-17 15:28:49 -07:00
Enrico Ros 4fff2394de ScrollToBottom: centralize styles 2024-03-17 15:28:00 -07:00
Enrico Ros afb74e68ee ScrollToBottom: moved to shared components 2024-03-17 14:54:15 -07:00
Enrico Ros d5fa7844c5 ScrollToBottom: allow to disable auto-stick (button only) 2024-03-17 14:47:02 -07:00
Enrico Ros b8470cd640 ScrollToBottom: allow the button to be inline 2024-03-17 14:44:27 -07:00
Enrico Ros 9a23f573a6 Beam: remove badge (hat on a hat) 2024-03-16 21:17:38 -07:00
Enrico Ros efe8fa0fda Beam: remove Phase 2024-03-16 21:16:27 -07:00
Enrico Ros 2d16e8bb4f UserFlags: show on messages 2024-03-16 20:44:54 -07:00
Enrico Ros bbd95eebff Update Models Attraction icon 2024-03-15 22:52:26 -07:00
Enrico Ros ceb00b4e93 Roll packages 2024-03-15 20:21:51 -07:00
Enrico Ros cc60d26d1c Turn multicast blue 2024-03-15 19:57:17 -07:00
Enrico Ros ba3ff739f6 Improve icons 2024-03-15 19:56:59 -07:00
Enrico Ros 6062647705 App: remove graying out - gets in the way a lot 2024-03-15 18:22:39 -07:00
Enrico Ros 070c1c2de9 Composer: tutorial happiness preserver 2024-03-15 18:16:21 -07:00
Enrico Ros d3aaa69409 Composer: tutorialize 2024-03-15 18:09:46 -07:00
Enrico Ros 0ac7753e35 Beam: terminate on Conversation clear 2024-03-15 17:49:56 -07:00
Enrico Ros eba9d53d2e Reduce the usage of backendCapabilities() 2024-03-15 17:35:38 -07:00
Enrico Ros d04d4ec8e7 Reorder providers 2024-03-15 16:34:42 -07:00
Enrico Ros c7c3efcbe7 Progress with bootstrap logic 2024-03-15 15:51:41 -07:00
Enrico Ros 2b8d53a44c Update wrappers 2024-03-15 15:41:08 -07:00
Enrico Ros ef6b573e08 Update TRPC Query Settings 2024-03-15 15:39:38 -07:00
Enrico Ros 61eedd41df Bootstrapper cleanup 2024-03-15 15:27:34 -07:00
Enrico Ros b265bcda20 Start cleaning up Bootstrapper 2024-03-15 14:32:15 -07:00
Enrico Ros d703d32a1f Cleanup knowledge of backend capabilities 2024-03-15 14:15:59 -07:00
Enrico Ros aab9334404 Build fix 2024-03-15 04:47:57 -07:00
Enrico Ros c2570f6955 New: attach starred messages with @
Note: the marshalling shall be moved inside the pipeline, probably
with a converter of type `ego-message-frontmatter` or similar
2024-03-15 04:42:16 -07:00
Enrico Ros 8e936a6334 Prevent this 2024-03-15 04:02:16 -07:00
Enrico Ros 46bfc22869 Show error on misused /beam 2024-03-15 02:48:45 -07:00
Enrico Ros db1620dd56 Actile: improve logic 2024-03-15 02:40:47 -07:00
Enrico Ros e59f8a42a3 Improve TRPC errors 2024-03-15 02:38:36 -07:00
Enrico Ros 17d18bd85d Fix /commands parsing 2024-03-15 02:37:30 -07:00
Enrico Ros fb256cf578 Bits 2024-03-15 01:39:51 -07:00
Enrico Ros 1b6b5db76d Actiles: improve provider search 2024-03-15 01:39:42 -07:00
Enrico Ros 41647ca83a Proactively get the user out of trouble. 2024-03-15 01:16:59 -07:00
Enrico Ros 07d2a17a87 Filter by starred chats. #109 2024-03-15 01:12:19 -07:00
Enrico Ros 6d744dfb7e ScrolltoBottomButton: improve 2024-03-15 00:40:01 -07:00
Enrico Ros b9b946c35f Messages: add 'starring' #109 2024-03-15 00:32:56 -07:00
Enrico Ros 17adfe2117 DMessage: add flag list support 2024-03-15 00:04:17 -07:00
Enrico Ros 1e5e21102d DMessage: improve edit support 2024-03-15 00:03:49 -07:00
Enrico Ros 4af992222f Shortcuts work 2024-03-14 21:47:59 -07:00
Enrico Ros a9447c6a11 Beam: misc 2024-03-14 21:33:42 -07:00
Enrico Ros db71323313 Misc 2024-03-14 21:19:02 -07:00
Enrico Ros b9b2748e05 Improved Avatar menu looks 2024-03-14 21:00:27 -07:00
Enrico Ros 387231f743 Fix Avatar menus 2024-03-14 20:52:34 -07:00
Enrico Ros 2216a89aa3 Beam: messaging 2024-03-14 19:10:15 -07:00
Enrico Ros 4faa6326fa Explainer: shortcuts 2024-03-14 19:08:11 -07:00
Enrico Ros cb22b3d9a1 Beam: update images 2024-03-14 19:07:34 -07:00
Enrico Ros 152a3873bd Beam: update scatter image 2024-03-14 18:50:25 -07:00
Enrico Ros adc2760a89 Beam: gather image 2024-03-14 18:24:36 -07:00
Enrico Ros dde64acb06 Improve Streaming issue reporting. Fixes #457 2024-03-14 17:47:55 -07:00
Enrico Ros 008adbd8bc Fix #459 2024-03-14 16:50:45 -07:00
Enrico Ros 0e4866a5a2 Beam: tutorial complete x2 2024-03-14 15:11:51 -07:00
Enrico Ros 5cb96cae3a Beam: tutorial complete 2024-03-14 15:05:36 -07:00
Enrico Ros 8cbb82a67f Beam: BEAM image, transparent 2024-03-14 14:57:56 -07:00
Enrico Ros 848ddbe477 Beam: BEAM image 2024-03-14 14:53:47 -07:00
Enrico Ros 083c1cde8b Explainer: adj auto resize 2024-03-14 14:53:47 -07:00
Enrico Ros b792971062 Add Gemini icon 2024-03-14 14:16:49 -07:00
Enrico Ros 07dde8f4b1 Chat messages: sticky headers 2024-03-14 04:19:57 -07:00
Enrico Ros 01f94127dd Beam: vendor icons 2024-03-14 04:07:50 -07:00
Enrico Ros 4d457b4e9e Beam: re-show explainer, with double-click 2024-03-14 03:28:27 -07:00
Enrico Ros 8ac93ff2da Beam: update explainer, with an end 2024-03-14 03:28:08 -07:00
Enrico Ros ef33a4b08e Beam: link 2024-03-14 02:47:27 -07:00
Enrico Ros fdd3b25a27 Beam: add Explainers 2024-03-14 02:37:01 -07:00
Enrico Ros 4dc979da08 SquircleIcon: support an alt color 2024-03-14 01:28:53 -07:00
Enrico Ros 8f426e03c4 Uniformize Roundicons 2024-03-14 01:28:40 -07:00
Enrico Ros 40cd085bf8 ExploreCarousel: the new Wizard experience 2024-03-14 01:28:20 -07:00
Enrico Ros 6aa75fc5d1 Animutils: amazing animations (not) 2024-03-14 01:27:55 -07:00
Enrico Ros eae5920f9d Beam: initial Explainer support 2024-03-13 21:59:55 -07:00
Enrico Ros 2f6bfa37cc Beam: balance title 2024-03-13 21:55:08 -07:00
Enrico Ros 9d6fd9b9b8 Styles fix 2024-03-13 20:14:58 -07:00
Enrico Ros 260cd67c96 Beam: user message editing 2024-03-13 17:34:07 -07:00
Enrico Ros aff76e2d18 Beam: improve grid 2024-03-13 17:21:50 -07:00
Enrico Ros 52e4343045 Improve drawer sizing 2024-03-13 17:15:50 -07:00
Enrico Ros 1ffbb135c6 Anthropic: add Haiku
(cherry picked from commit c3ec522261)
2024-03-13 14:46:39 -07:00
Enrico Ros c3ec522261 Anthropic: add Haiku 2024-03-13 14:45:50 -07:00
Enrico Ros 4538839376 Beam: unify invocation logic, from 7 places 2024-03-13 14:41:39 -07:00
Enrico Ros 834edd3a71 Beam: improve chat message popup 2024-03-13 14:22:06 -07:00
Enrico Ros 581c3d9593 Beam: document shortcut 2024-03-13 14:21:50 -07:00
Enrico Ros 0c672fbaa5 Beam: add disabled support for letters 2024-03-13 14:15:41 -07:00
Enrico Ros 6d96b9a312 Beam: add badges on menu and chat mode menu 2024-03-13 14:02:20 -07:00
Enrico Ros 691791ccd0 Beam: improve user message 2024-03-13 14:01:54 -07:00
Enrico Ros f4299121d5 Beam: highlight in modes menu 2024-03-13 13:43:48 -07:00
Enrico Ros 1adfb7eedd Chat drawer: setting to show persona icons 2024-03-13 13:36:55 -07:00
Enrico Ros 33ad583d15 New chat: better button spacings 2024-03-13 13:30:58 -07:00
Enrico Ros a7e2fe2277 New chat: better button 2024-03-13 13:16:35 -07:00
Enrico Ros 5a479d5863 DesktopDrawer: perfect shadows 2024-03-13 13:05:57 -07:00
Enrico Ros 873ff034d2 DesktopDrawer: fix shadow 2024-03-13 04:18:10 -07:00
Enrico Ros 61d3537617 Composer: fix zIndex 2024-03-13 04:11:09 -07:00
Enrico Ros ae068a3f64 Beam: shortcuts 2024-03-13 03:47:56 -07:00
Enrico Ros f7402cd6f5 Beam: close dialog after using selected 2024-03-13 03:47:48 -07:00
Enrico Ros c53f9c8020 Beam: use selected 2024-03-13 03:28:42 -07:00
Enrico Ros 798b4d57f4 Beam: disable on system message 2024-03-13 03:12:05 -07:00
Enrico Ros 98d428fb34 Beam: enable high performance mode 2024-03-13 02:32:42 -07:00
Enrico Ros 3ac5ace216 Share stream text indicator 2024-03-13 02:32:29 -07:00
Enrico Ros 444a1a7ab9 Temp download gif 2024-03-13 02:32:19 -07:00
Enrico Ros 43ea4bd4b5 Large cleanups in execution logic 2024-03-13 02:32:09 -07:00
Enrico Ros 6a9272e40a Beam: fix 2024-03-13 02:23:32 -07:00
Enrico Ros 10589a11aa Beam: business logic to continue/replace messages, including import 2024-03-13 00:15:41 -07:00
Enrico Ros a88f898bc0 Chat/Message/List: improve Beam and related restart logic 2024-03-12 22:55:40 -07:00
Enrico Ros 7a84038b04 Beam: initialize/terminate instead of open/close 2024-03-12 19:56:11 -07:00
Enrico Ros 111c40732d Beam: slight text changes 2024-03-12 18:58:35 -07:00
Enrico Ros 69bb78c8be Beam: reduce direct open calls 2024-03-12 18:54:45 -07:00
Enrico Ros ad3b327d69 Beam: close confirmation: add callbacks 2024-03-12 18:36:23 -07:00
Enrico Ros dc27f38534 Beam: close confirmation 2024-03-12 18:33:24 -07:00
Enrico Ros 5b0816cb92 Beam: esc to close 2024-03-12 18:18:46 -07:00
Enrico Ros 57f6955303 Beam: alt bar improvement 2024-03-12 18:03:31 -07:00
Enrico Ros 78915f878d Beam: clean gather pane 2024-03-12 18:01:29 -07:00
Enrico Ros 6ced6d626b Beam: improve integration 2024-03-12 18:01:17 -07:00
Enrico Ros ee3cb819b4 Beam: back to dev 2024-03-12 18:01:09 -07:00
Enrico Ros cc17b1d19d Beam: Chat Title bar to close the pane 2024-03-12 17:50:16 -07:00
Enrico Ros 2c83240d47 Snacks: review state 2024-03-12 16:48:03 -07:00
Enrico Ros 54f18ff120 Chat: focused state review 2024-03-12 16:47:50 -07:00
Enrico Ros 5e1fe363c3 PanesManager: cleanups (shall be safe) 2024-03-12 15:42:25 -07:00
Enrico Ros 3d2ec507e1 Chat: clarify state 2024-03-12 13:54:40 -07:00
Enrico Ros 1dd7af3c8b Beam: gather test icons 2024-03-12 13:41:50 -07:00
Enrico Ros 06ec1fcebf Beam: improve messaging 2024-03-12 13:08:08 -07:00
Enrico Ros 86cb863fd4 Beam: explored the modal 2024-03-12 13:07:57 -07:00
Enrico Ros d5ef1288d8 Beam: unify layout again 2024-03-12 12:45:04 -07:00
Enrico Ros f3354c498d Beam: unify layout again 2024-03-12 12:44:59 -07:00
Enrico Ros 9557141b38 Beam: bits (drag-drop didn't work out, it's a grid layout) 2024-03-12 12:36:32 -07:00
Enrico Ros 3144b66e73 StrictModeDroppable: share 2024-03-12 11:55:20 -07:00
Enrico Ros 6dbefa3d2f Beam: bits 2024-03-12 11:55:08 -07:00
Enrico Ros c8f3b139e8 Beam: bits 2024-03-12 11:06:52 -07:00
Enrico Ros 288663325d Beam: rename Panes 2024-03-12 11:05:16 -07:00
Enrico Ros 49947ee01d Beam: extract the Grid 2024-03-12 11:03:55 -07:00
Enrico Ros fa7a45ebc7 bits 2024-03-12 10:54:03 -07:00
Enrico Ros 9a074c222f Beam: adjustments 2024-03-12 10:51:18 -07:00
Enrico Ros 4e0d7b6ed9 Beam: down to non-removable 1 2024-03-12 10:41:16 -07:00
Enrico Ros 1f3defb04c Beam: optimize ControlsRow 2024-03-12 02:43:59 -07:00
Enrico Ros 6c52c43460 Beam: auto-hide composer 2024-03-12 02:38:18 -07:00
Enrico Ros deae2879f1 Beam: improve hooks 2024-03-12 01:59:42 -07:00
Enrico Ros 5b255a7d8b LLMSelect: try stabilize 2024-03-12 01:58:54 -07:00
Enrico Ros 6e06c24b7a Beam: extract hooks 2024-03-12 01:58:28 -07:00
Enrico Ros 2fde1efdd3 Beam: begin wiring the Gatherer 2024-03-11 23:59:04 -07:00
Enrico Ros aeb29d983a FormLabelStart: optimize 2024-03-11 23:27:58 -07:00
Enrico Ros c8a7123da9 Beam: fix styles 2024-03-11 23:13:20 -07:00
Enrico Ros 5c22061415 Beam: state cleanup and sync 2024-03-11 16:32:25 -07:00
Enrico Ros 9a0fda8c02 Beam: scrollable main layout 2024-03-11 16:00:22 -07:00
Enrico Ros 2f9a17c44a Beam: fixes 2024-03-11 15:46:29 -07:00
Enrico Ros 50559015d8 Beam: fix scattering (empty) issue 2024-03-11 15:40:20 -07:00
Enrico Ros a8d4e143c2 Beam: selection (disable, does not look great) 2024-03-11 15:32:54 -07:00
Enrico Ros 2a6c69538d Beam: increase ray state consistency 2024-03-11 15:09:43 -07:00
Enrico Ros 0ba5d61353 Beam: Ray lifecycle tracking 2024-03-11 14:44:36 -07:00
Enrico Ros d436ec5790 chat-stream: streamAssistantMessage: add an outcome type 2024-03-11 14:01:30 -07:00
Enrico Ros 759b822b92 Beam: relayout with Gather Controls skel 2024-03-11 13:49:19 -07:00
Enrico Ros 9df45af698 Beam: rename Scatter Controls 2024-03-11 13:34:57 -07:00
Enrico Ros 3474e81446 Beam: show preceding messages count 2024-03-11 13:26:06 -07:00
Enrico Ros e1f07eb957 ChatMessage: support top decorator (4rem default size) 2024-03-11 13:16:52 -07:00
Enrico Ros 71ff1b98be Beam: extract Ray controls row 2024-03-11 12:41:01 -07:00
Enrico Ros 9b370dfa88 Remove warnings 2024-03-11 12:40:40 -07:00
Enrico Ros 0be0661750 ButtonGroup background 2024-03-11 12:40:33 -07:00
Enrico Ros eaa7230af7 Improve Expand/Collapse (position, length) 2024-03-11 12:40:07 -07:00
Enrico Ros 11cb000481 Beam: fix stops and deletes 2024-03-11 01:54:39 -07:00
Enrico Ros 8ae3554a58 Beam: start/stop Rays 2024-03-11 00:25:50 -07:00
Enrico Ros dfd4736386 LLMSelect: support disablement 2024-03-11 00:25:39 -07:00
Enrico Ros feb793c9fa Beam: improve controller 2024-03-10 22:50:46 -07:00
Enrico Ros ee962fde08 GoodTooltip: fix 2024-03-10 22:12:12 -07:00
Enrico Ros c08dd96de3 Beam: add Stop buttons 2024-03-10 21:39:57 -07:00
Enrico Ros b52f771133 BlocksRenderer: improve expand buttons 2024-03-10 21:39:28 -07:00
Enrico Ros 4631232551 Animations: centralize 2024-03-10 21:39:02 -07:00
Enrico Ros df7f5047aa Beam: first Wiring 2024-03-10 20:58:37 -07:00
Enrico Ros 467d14324d zIndices: cleanup 2024-03-10 20:53:09 -07:00
Enrico Ros cbdce08e96 Beam: improve rays 2024-03-10 17:50:46 -07:00
Enrico Ros d6bf8f8854 Beam: rename View again 2024-03-10 17:25:51 -07:00
Enrico Ros 4599da3ded Revert "Beam: remove optionality"
This reverts commit 6d50952b2e.
2024-03-10 17:24:42 -07:00
Enrico Ros 6d50952b2e Beam: remove optionality 2024-03-10 17:23:00 -07:00
Enrico Ros 7066947809 Beam: move files 2024-03-10 17:15:01 -07:00
Enrico Ros e2924aacab Beam: cleanups 2024-03-10 16:49:36 -07:00
Enrico Ros 1e86d2503f Beam: merged -> gather 2024-03-10 16:33:24 -07:00
Enrico Ros eb67eee53a Beam: improve Debug methods 2024-03-10 16:33:15 -07:00
Enrico Ros dfdad45963 Beam: improve Debug info 2024-03-10 16:21:04 -07:00
Enrico Ros 4735508d87 Beam: cleanups 2024-03-10 16:17:43 -07:00
Enrico Ros c43c47eab8 Beam: standalone debug app 2024-03-10 16:12:48 -07:00
Enrico Ros fafb2dc6b9 Dev Apps 2024-03-10 16:12:20 -07:00
Enrico Ros 140e99c465 Beam: start from neg scale 2024-03-10 15:59:43 -07:00
Enrico Ros 7ba1974390 Beam: Encapsulate and move logic to BeamStore 2024-03-10 15:34:34 -07:00
Enrico Ros 51b8510f17 Misc 2024-03-10 15:05:01 -07:00
Enrico Ros 5d6949d471 Force the hard work 2024-03-10 14:54:11 -07:00
Enrico Ros 8e9d0c1fd1 The Beauty and the Beam 2024-03-10 14:01:39 -07:00
Enrico Ros 3852a3b779 User Text: Collapse as well as Expand 2024-03-10 13:47:21 -07:00
Enrico Ros 8b4ba96936 Beam: rays increase button 2024-03-09 18:06:06 -08:00
Enrico Ros 0c17e18491 Beam: Rays close to gen 2024-03-09 17:57:30 -08:00
Enrico Ros 2bdbab3afc Messages: controllable Avatar sightings and content scaling offset 2024-03-09 17:54:27 -08:00
Enrico Ros b97499a95e Beam: renames 2024-03-09 17:39:36 -08:00
Enrico Ros a70ac57872 Beam: stored Rays 2024-03-09 17:01:16 -08:00
Enrico Ros a9cf457024 Beam: dynamic Rays 2024-03-09 13:07:22 -08:00
Enrico Ros e5c938ac37 Beam: optimize Ray 2024-03-09 12:44:50 -08:00
Enrico Ros edad54efa2 Beam: optimize View 2024-03-09 12:44:35 -08:00
Enrico Ros f88426758f Beam: ensure component recreation 2024-03-09 12:44:09 -08:00
Enrico Ros 77a28eb810 Optimize LLMSelect 2024-03-09 12:43:56 -08:00
Enrico Ros f834b27562 Optimize FormLabelStart 2024-03-09 12:43:49 -08:00
Enrico Ros 984e257cc5 Move to a better (more reactive?) BeamStore 2024-03-09 11:24:28 -08:00
Enrico Ros 729e7612bc Improve LLMSelect (fix dependency) 2024-03-09 11:23:59 -08:00
Enrico Ros 59fadeae57 Improve LLMSelect 2024-03-09 11:20:18 -08:00
Enrico Ros bfbf7a298a Beam: actor -> ray 2024-03-09 00:32:32 -08:00
Enrico Ros aad5d3bd65 Beam: improve style 2024-03-09 00:07:28 -08:00
Enrico Ros 504f19c445 Beam: cleanups 2024-03-08 23:34:05 -08:00
Enrico Ros 19c47eb442 Beam: improve state 2024-03-08 23:11:13 -08:00
Enrico Ros ab6043df60 Beam: rename 2024-03-08 21:58:32 -08:00
Enrico Ros 3305549a0f Fix customEvent helpers 2024-03-08 21:57:59 -08:00
Enrico Ros c24c3cb571 Beam: misc highlights 2024-03-08 18:55:38 -08:00
Enrico Ros 952999258b Beam: header: improve looks 2024-03-08 18:50:01 -08:00
Enrico Ros 0713eaa52c Beam: extract header 2024-03-08 18:43:35 -08:00
Enrico Ros 8fee689f60 Beam: update layout 2024-03-08 18:05:13 -08:00
Enrico Ros 75ddb17fed Beam: begin UI 2024-03-08 18:04:59 -08:00
Enrico Ros 0c6a74626c Beam: update store 2024-03-08 18:04:39 -08:00
Enrico Ros 41e3d0eaf9 Use customEventHelpers for creating and subscribing to custom events 2024-03-08 15:40:41 -08:00
Enrico Ros 8b9cfebd42 Beam: misc 2024-03-08 14:21:48 -08:00
Enrico Ros 16badee259 Beam: renames 2024-03-08 14:21:36 -08:00
Enrico Ros 9d5171dd36 Panes: bits 2024-03-08 13:43:56 -08:00
Enrico Ros e0c0e81b7d Panes: improve branching behavior 2024-03-08 13:42:13 -08:00
Enrico Ros fd4e8985fc Beam: 1.15 2024-03-08 12:16:58 -08:00
Enrico Ros 1d9b8503c0 Roll packages 2024-03-08 12:16:12 -08:00
Enrico Ros b3ef7b914d Beam: enable dev setting 2024-03-08 11:59:12 -08:00
309 changed files with 12965 additions and 4678 deletions
+9 -1
View File
@@ -32,6 +32,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
@@ -49,13 +55,15 @@ jobs:
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main-stable' }}
type=ref,event=tag # Use the tag name as a tag for tag builds
type=semver,pattern={{version}} # Generate semantic versioning tags for tag builds
type=sha # Just in case none of the above applies
- name: Build and push Docker image
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
build-args: NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
+77 -88
View File
@@ -1,11 +1,13 @@
# BIG-AGI 🧠✨
Welcome to big-AGI 👋, the GPT application for professionals that need function, form,
Welcome to big-AGI, the AI suite for professionals that need function, form,
simplicity, and speed. Powered by the latest models from 12 vendors and
open-source model servers, `big-AGI` offers best-in-class Voice and Chat with AI Personas,
visualizations, coding, drawing, calling, and quite more -- all in a polished UX.
open-source servers, `big-AGI` offers best-in-class Chats,
[Beams](https://github.com/enricoros/big-AGI/issues/470),
and [Calls](https://github.com/enricoros/big-AGI/issues/354) with AI personas,
visualizations, coding, drawing, side-by-side chatting, and more -- all wrapped in a polished UX.
Pros use big-AGI. 🚀 Developers love big-AGI. 🤖
Stay ahead of the curve with big-AGI. 🚀 Pros & Devs love big-AGI. 🤖
[![Official Website](https://img.shields.io/badge/BIG--AGI.com-%23096bde?style=for-the-badge&logo=vercel&label=launch)](https://big-agi.com)
@@ -13,11 +15,54 @@ 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&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) 👉 [documentation](docs/README.md)
## 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [installation](docs/installation.md) 👉 [documentation](docs/README.md)
big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap
> Note: bigger better features (incl. Beam-2) are being cooked outside of `main`.
### What's New in 1.14.1 · March 7, 2024 · Modelmorphic
[//]: # (big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap)
### What's New in 1.16.2 · Jun 7, 2024 (minor release)
- Improve web downloads, as text, markdwon, or HTML
- Proper support for Gemini models
- Added the latest Mistral model
- Tokenizer support for gpt-4o
- Updates to Beam
### What's New in 1.16.1 · May 13, 2024 (minor release)
- Support for the new OpenAI GPT-4o 2024-05-13 model
### What's New in 1.16.0 · May 9, 2024 · Crystal Clear
- [Beam](https://big-agi.com/blog/beam-multi-model-ai-reasoning) core and UX improvements based on user feedback
- Chat cost estimation 💰 (enable it in Labs / hover the token counter)
- Save/load chat files with Ctrl+S / Ctrl+O on desktop
- Major enhancements to the Auto-Diagrams tool
- YouTube Transcriber Persona for chatting with video content, [#500](https://github.com/enricoros/big-AGI/pull/500)
- Improved formula rendering (LaTeX), and dark-mode diagrams, [#508](https://github.com/enricoros/big-AGI/issues/508), [#520](https://github.com/enricoros/big-AGI/issues/520)
- Models update: **Anthropic**, **Groq**, **Ollama**, **OpenAI**, **OpenRouter**, **Perplexity**
- Code soft-wrap, chat text selection toolbar, 3x faster on Apple silicon, and more [#517](https://github.com/enricoros/big-AGI/issues/517), [507](https://github.com/enricoros/big-AGI/pull/507)
#### 3,000 Commits Milestone · April 7, 2024
![big-AGI Milestone](https://github.com/enricoros/big-AGI/assets/32999/47fddbb1-9bd6-4b58-ace4-781dfcb80923)
- 🥇 Today we <b>celebrate commit 3000</b> in just over one year, and going stronger 🚀
- 📢️ Thanks everyone for your support and words of love for Big-AGI, we are committed to creating the best AI experiences for everyone.
### What's New in 1.15.0 · April 1, 2024 · Beam
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
- Managed Deployments **Auto-Configuration**: simplify the UI models setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
- Message **Starring ⭐**: star important messages within chats, to attach them later. [#476](https://github.com/enricoros/big-AGI/issues/476)
- Enhanced the default Persona
- Fixes to Gemini models and SVGs, improvements to UI and icons
- 1.15.1: Support for Gemini Pro 1.5 and OpenAI Turbo models
- Beast release, over 430 commits, 10,000+ lines changed: [release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.15.0), and changes [v1.14.1...v1.15.0](https://github.com/enricoros/big-AGI/compare/v1.14.1...v1.15.0)
<details>
<summary>What's New in 1.14.1 · March 7, 2024 · Modelmorphic</summary>
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
- New **[Perplexity](https://www.perplexity.ai/)** and **[Groq](https://groq.com/)** integration (thanks @Penagwin). [#407](https://github.com/enricoros/big-AGI/issues/407), [#427](https://github.com/enricoros/big-AGI/issues/427)
@@ -26,9 +71,11 @@ big-AGI is an open book; see the **[ready-to-ship and future ideas](https://gith
- Performance optimizations: runs [much faster](https://twitter.com/enricoros/status/1756553038293303434?utm_source=localhost:3000&utm_medium=big-agi), saves lots of power, reduces memory usage
- Enhanced UX with auto-sizing charts, refined search and folder functionalities, perfected scaling
- And with more UI improvements, documentation, bug fixes (20 tickets), and developer enhancements
- [Release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.14.0), and changes [v1.13.1...v1.14.1](https://github.com/enricoros/big-AGI/compare/v1.13.1...v1.14.1) (233 commits, 8,000+ lines changed)
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
</details>
<details>
<summary>What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind</summary>
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
@@ -40,6 +87,8 @@ https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385
- Better looking chats with improved spacing, fonts, and menus
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-lmstudio.md) (thanks @aj47), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/deploy-database.md) (thanks @ranfysvalle02), and speedups
</details>
<details>
<summary>What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline</summary>
@@ -84,11 +133,11 @@ https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cf
For full details and former releases, check out the [changelog](docs/changelog.md).
## Key Features 👊
## 👉 Key Features
| ![Advanced AI](https://img.shields.io/badge/Advanced%20AI-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![100+ AI Models](https://img.shields.io/badge/100%2B%20AI%20Models-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![Flow-state UX](https://img.shields.io/badge/Flow--state%20UX-32383e?style=for-the-badge&logo=flow&logoColor=white) | ![Privacy First](https://img.shields.io/badge/Privacy%20First-32383e?style=for-the-badge&logo=privacy&logoColor=white) | ![Advanced Tools](https://img.shields.io/badge/Fun%20To%20Use-f22a85?style=for-the-badge&logo=tools&logoColor=white) |
|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| **Chat**<br/>**Call** AGI<br/>**Draw** images<br/>**Agents**, ... | Local & Cloud<br/>Open & Closed<br/>Cheap & Heavy<br/>Google, Mistral, ... | Attachments<br/>Diagrams<br/>Multi-Chat<br/>Mobile-first UI | Stored Locally<br/>Easy self-Host<br/>Local actions<br/>Data = Gold | AI Personas<br/>Voice Modes<br/>Screen Capture<br/>Camera + OCR |
| **Chat**<br/>**Call**<br/>**Beam**<br/>**Draw**, ... | Local & Cloud<br/>Open & Closed<br/>Cheap & Heavy<br/>Google, Mistral, ... | Attachments<br/>Diagrams<br/>Multi-Chat<br/>Mobile-first UI | Stored Locally<br/>Easy self-Host<br/>Local actions<br/>Data = Gold | AI Personas<br/>Voice Modes<br/>Screen Capture<br/>Camera + OCR |
![big-AGI screenshot](docs/pixels/big-AGI-compo-20240201_small.png)
@@ -131,6 +180,22 @@ Add extra functionality with these integrations:
<br/>
## 🚀 Installation
To get started with big-AGI, follow our comprehensive [Installation Guide](docs/installation.md).
The guide covers various installation options, whether you're spinning it up on
your local computer, deploying on Vercel, on Cloudflare, or rolling it out
through Docker.
Whether you're a developer, system integrator, or enterprise user, you'll find step-by-step instructions
to set up big-AGI quickly and easily.
[![Installation Guide](https://img.shields.io/badge/Installation%20Guide-blue?style=for-the-badge&logo=read-the-docs&logoColor=white)](docs/installation.md)
Or bring your API keys and jump straight into our free instance on [big-AGI.com](https://big-agi.com).
<br/>
# 🌟 Get Involved!
[//]: # ([![Official Discord]&#40;https://img.shields.io/discord/1098796266906980422?label=discord&logo=discord&logoColor=%23fff&style=for-the-badge&#41;]&#40;https://discord.gg/MkH4qj2Jp9&#41;)
@@ -140,86 +205,10 @@ Add extra functionality with these integrations:
- [ ]**Give us a star** on GitHub 👆
- [ ] 🚀 **Do you like code**? You'll love this gem of a project! [_Pick up a task!_](https://github.com/users/enricoros/projects/4/views/4) - _easy_ to _pro_
- [ ] 💡 Got a feature suggestion? [_Add your roadmap ideas_](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
- [ ]Deploy your [fork](docs/customizations.md) for your friends and family, or [customize it for work](docs/customizations.md)
- [ ] Check out some of the big-AGI [**community projects**](docs/customizations.md)
| Project | Features | GitHub |
|---------|----------------------------------------------------|-------------------------------------------------------------------------------------|
| CoolAGI | Code Interpreter, Vision, Mind maps, and much more | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
- [ ][Deploy](docs/installation.md) your [fork](docs/customizations.md) for your friends and family, or [customize it for work](docs/customizations.md)
<br/>
# 🧩 Develop
[//]: # (![TypeScript]&#40;https://img.shields.io/badge/TypeScript-007ACC?style=&logo=typescript&logoColor=white&#41;)
[//]: # (![React]&#40;https://img.shields.io/badge/React-61DAFB?style=&logo=react&logoColor=black&#41;)
[//]: # (![Next.js]&#40;https://img.shields.io/badge/Next.js-000000?style=&logo=vercel&logoColor=white&#41;)
To download and run this Typescript/React/Next.js project locally, the only prerequisite is Node.js with the `npm` package manager.
Clone this repo, install the dependencies (all local), and run the development server (which auto-watches the
files for changes):
```bash
git clone https://github.com/enricoros/big-agi.git
cd big-agi
npm install
npm run dev
# You will see something like:
#
# ▲ Next.js 14.1.0
# - Local: http://localhost:3000
# ✓ Ready in 2.6s
```
The development app will be running on `http://localhost:3000`. Development builds have the advantage of not requiring
a build step, but can be slower than production builds. Also, development builds won't have timeout on edge functions.
## 🛠️ Deploy from source
The _production_ build of the application is optimized for performance and is performed by the `npm run build` command,
after installing the required dependencies.
```bash
# .. repeat the steps above up to `npm install`, then:
npm run build
next start --port 3000
```
The app will be running on the specified port, e.g. `http://localhost:3000`.
Want to deploy with username/password? See the [Authentication](docs/deploy-authentication.md) guide.
## 🐳 Deploy with Docker
For more detailed information on deploying with Docker, please refer to the [docker deployment documentation](docs/deploy-docker.md).
Build and run:
```bash
docker build -t big-agi .
docker run -d -p 3000:3000 big-agi
```
Or run the official container:
- manually: `docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi`
- or, with docker-compose: `docker-compose up` or see [the documentation](docs/deploy-docker.md) for a composer file with integrated browsing
## ☁️ Deploy on Cloudflare Pages
Please refer to the [Cloudflare deployment documentation](docs/deploy-cloudflare.md).
## 🚀 Deploy on Vercel
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&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)
[//]: # ([![GitHub stars]&#40;https://img.shields.io/github/stars/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/stargazers&#41;)
[//]: # ([![GitHub forks]&#40;https://img.shields.io/github/forks/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/network&#41;)
+4
View File
@@ -16,4 +16,8 @@ const handlerNodeRoutes = (req: Request) =>
});
export const runtime = 'nodejs';
// NOTE: the following statement breaks the build on non-pro deployments, and conditionals don't work either
// so we resorted to raising the timeout from 10s to 25s in the vercel.json file instead
// export const maxDuration = 25;
export const dynamic = 'force-dynamic';
export { handlerNodeRoutes as GET, handlerNodeRoutes as POST };
+9 -15
View File
@@ -28,7 +28,7 @@ Detailed guides to configure your big-AGI interface and models.
- **Advanced Feature Configuration**:
- **[Browse](config-feature-browse.md)**: Enable web page download through third-party services or your own cloud (advanced)
- **ElevenLabs API**: Voice and cutom voice generation, only requires their API key
- **Google Search API**: guide not yet available, see the Google options in 'Environment Variables'
- **Google Search API**: guide not yet available, see the Google options in '[Environment Variables](environment-variables.md)'
- **Prodia API**: Stable Diffusion XL image generation, only requires their API key, alternative to DALL·E
## Deployment
@@ -37,22 +37,16 @@ System integrators, administrators, whitelabelers: instead of using the public b
Step-by-step deployment and system configuration instructions.
- **Deploy Your Own**
- straightforward: **Local development**, **Vercel 1-Click**
- **[Cloudflare Deployment](deploy-cloudflare.md)**
- **[Docker Deployment](deploy-docker.md)**: Containers for Local or Cloud deployments
- **[Installation](installation.md)**: Set up your own instance of big-AGI and related products
- build from source or use pre-built
- locally, in the public cloud, or on your own servers
- **Deployment Server Features**
- **[Database Setup](deploy-database.md)**: Optional, only required to enable "Chat Link Sharing"
- **[Environment Variables](environment-variables.md)**: 📌 Set server-side API keys and special features in your deployments
- **[HTTP Basic Authentication](deploy-authentication.md)**: Optional, Secure your big-AGI instance with a username and password
## Customization & Derivative UIs
👏 Customize big-AGI to fit your needs.
- **[Customizing big-AGI](customizations.md)**: how to alter source code and server-side configuration
- **Advanced Customizations**:
- **[Source code alterations guide](customizations.md)**: source code primer and alterations guidelines
- **[Basic Authentication](deploy-authentication.md)**: Optional, adds a username and password wall
- **[Database Setup](deploy-database.md)**: Optional, enables "Chat Link Sharing"
- **[Environment Variables](environment-variables.md)**: 📌 Pre-configures models and services
## Support and Community
+41 -4
View File
@@ -5,13 +5,50 @@ by release.
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
### 1.15.0 - Mar 2024
### 1.17.0 - Jun 2024
Prediction: OpenAI will release GPT-5 on March 14, 2024. We will support it on day 1.
- milestone: [1.15.0](https://github.com/enricoros/big-agi/milestone/15)
- milestone: [1.17.0](https://github.com/enricoros/big-agi/milestone/17)
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
### What's New in 1.16.2 · Jun 7, 2024 (minor release)
- Improve web downloads, as text, markdwon, or HTML
- Proper support for Gemini models
- Added the latest Mistral model
- Tokenizer support for gpt-4o
- Updates to Beam
### What's New in 1.16.1 · May 13, 2024 (minor release)
- Support for the new OpenAI GPT-4o 2024-05-13 model
### What's New in 1.16.0 · May 9, 2024 · Crystal Clear
- [Beam](https://big-agi.com/blog/beam-multi-model-ai-reasoning) core and UX improvements based on user feedback
- Chat cost estimation 💰 (enable it in Labs / hover the token counter)
- Save/load chat files with Ctrl+S / Ctrl+O on desktop
- Major enhancements to the Auto-Diagrams tool
- YouTube Transcriber Persona for chatting with video content, [#500](https://github.com/enricoros/big-AGI/pull/500)
- Improved formula rendering (LaTeX), and dark-mode diagrams, [#508](https://github.com/enricoros/big-AGI/issues/508), [#520](https://github.com/enricoros/big-AGI/issues/520)
- Models update: **Anthropic**, **Groq**, **Ollama**, **OpenAI**, **OpenRouter**, **Perplexity**
- Code soft-wrap, chat text selection toolbar, 3x faster on Apple silicon, and more [#517](https://github.com/enricoros/big-AGI/issues/517), [507](https://github.com/enricoros/big-AGI/pull/507)
- Developers: update the LLMs data structures
### What's New in 1.15.1 · April 10, 2024 (minor release, models support)
- Support for the newly released Gemini Pro 1.5 models
- Support for the new OpenAI 2024-04-09 Turbo models
- Resilience fixes after the large success of 1.15.0
### What's New in 1.15.0 · April 1, 2024 · Beam
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
- Managed Deployments **Auto-Configuration**: simplify the UI mdoels setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
- Message **Starring ⭐**: star important messages within chats, to attach them later. [#476](https://github.com/enricoros/big-AGI/issues/476)
- Enhanced the default Persona
- Fixes to Gemini models and SVGs, improvements to UI and icons
- Beast release, over 430 commits, 10,000+ lines changed: [release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.15.0), and changes [v1.14.1...v1.15.0](https://github.com/enricoros/big-AGI/compare/v1.14.1...v1.15.0)
### What's New in 1.14.1 · March 7, 2024 · Modelmorphic
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
+3
View File
@@ -20,6 +20,9 @@ If you have an `API Endpoint` and `API Key`, you can configure big-AGI as follow
The deployed models are now available in the application. If you don't have a configured
Azure OpenAI service instance, continue with the next section.
In addition to using the UI, configuration can also be done using
[environment variables](environment-variables.md).
## Setting Up Azure
### Step 1: Azure Account & Subscription
+2 -2
View File
@@ -68,7 +68,7 @@ The chat agent won't be able to access the web sites if the browserless containe
- MAX_CONCURRENT_SESSIONS=10
```
You can then add the proyy lines to your `.env` file.
You can then add the proxy lines to your `.env` file.
```
https_proxy=http://PROXY-IP:PROXY-PORT
@@ -115,4 +115,4 @@ If you encounter any issues or have questions about configuring the browse funct
Enjoy the enhanced browsing experience within `big-AGI` and explore the web without ever leaving your chat!
Last updated on Feb 27, 2024 ([edit on GitHub](https://github.com/enricoros/big-AGI/edit/main/docs/config-feature-browse.md))
Last updated on Feb 27, 2024 ([edit on GitHub](https://github.com/enricoros/big-AGI/edit/main/docs/config-feature-browse.md))
+3
View File
@@ -37,6 +37,9 @@ Check the URL and modify if different.
2. Enter the API URL: `http://localhost:1234` (modify if different)
3. Refresh by clicking on the `Models` button to load models from LM Studio
In addition to using the UI, configuration can also be done using
[environment variables](environment-variables.md).
## Troubleshooting
- **Missing @mui/material**: Execute `npm install @mui/material` or `yarn add @mui/material`
+3
View File
@@ -36,6 +36,9 @@ Follow the guide at: https://localai.io/basics/getting_started/
- Load the models (click on `Models 🔄`)
- Select the model and chat
In addition to using the UI, configuration can also be done using
[environment variables](environment-variables.md).
### Integration: Models Gallery
If the running LocalAI instance is configured with a [Model Gallery](https://localai.io/models/):
+16 -9
View File
@@ -13,7 +13,7 @@ _Last updated Dec 16, 2023_
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).
[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
@@ -22,6 +22,9 @@ _Last updated Dec 16, 2023_
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
In addition to using the UI, configuration can also be done using
[environment variables](environment-variables.md).
**Visual Configuration Guide**:
* After adding the `Ollama` model vendor, entering the IP address of an Ollama server, and refreshing models:<br/>
@@ -37,7 +40,7 @@ _Last updated Dec 16, 2023_
### ⚠️ Network Troubleshooting
If you get errors about the server having trouble connecting with Ollama, please see
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
@@ -69,15 +72,19 @@ Then, edit the nginx configuration file `/etc/nginx/sites-enabled/default` and a
```nginx
location /ollama/ {
proxy_pass http://localhost:11434;
proxy_pass http://127.0.0.1:11434/;
# Disable buffering for the streaming responses (SSE)
proxy_set_header Connection '';
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Disable buffering for the streaming responses
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
# Longer timeouts
proxy_read_timeout 3600;
proxy_connect_timeout 3600;
proxy_send_timeout 3600;
}
```
+4 -4
View File
@@ -25,15 +25,15 @@ This guide assumes that **big-AGI** is already installed on your system. Note th
- Stop the Web UI as we need to modify the startup flags to enable the OpenAI API
2. Enable the **openai extension**
- Edit `CMD_FLAGS.txt`
- Make sure that `--listen --api` is present and uncommented
- Make sure that `--listen --api` is present and uncommented
3. Restart text-generation-webui
- Double-click on "start"
- You should see something like:
- You should see something like:
```
2023-12-07 21:51:21 INFO:Loading the extension "openai"...
2023-12-07 21:51:21 INFO:OpenAI-compatible API URL:
http://0.0.0.0:5000
http://0.0.0.0:5000
...
INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
Running on local URL: http://0.0.0.0:7860
+3
View File
@@ -22,6 +22,9 @@ This document details the process of integrating OpenRouter with big-AGI.
![feature-openrouter-configure.png](pixels/feature-openrouter-configure.png)
4. OpenAI GPT4-32k and other models will now be accessible and selectable in the application.
In addition to using the UI, configuration can also be done using
[environment variables](environment-variables.md).
### Pricing
OpenRouter independently manages its service and pricing and is not affiliated with big-AGI.
+25 -6
View File
@@ -22,6 +22,25 @@ Understand the Architecture: big-AGI uses Next.js, React for the front end, and
This necessitates a code change (file renaming) before build initiation, detailed in [deploy-authentication.md](deploy-authentication.md).
### Increase Vercel Functions Timeout
For long-running operations, Vercel allows paid deployments to increase the timeout on Functions.
Note that this applies to old-style Vercel Functions (based on Node.js) and not the new Edge Functions.
At time of writing, big-AGI has only 2 operations that run on Node.js Functions:
browsing (fetching web pages) and sharing. They both can exceed 10 seconds, especially
when fetching large pages or waiting for websites to be completed.
We provide `vercel_PRODUCTION.json` to raise the duration to 25 seconds (from a default of 10), to use it,
make sure to rename it to `vercel.json` before build.
From the Vercel Project > Settings > General > Build & Development Settings,
you can for instance set the build command to:
```bash
mv vercel_PRODUCTION.json vercel.json; next build
```
### Change the Personas
Edit the `src/data.ts` file to customize personas. This file houses the default personas. You can add, remove, or modify these to meet your project's needs.
@@ -47,20 +66,20 @@ Test your application thoroughly using local development (refer to README.md for
We introduced the `/info/debug` page that provides a detailed overview of the application's environment, including the API keys, environment variables, and other configuration settings.
<br/>
<br/>
## Community Projects - Share Your Project
After deployment, share your project with the community. We will link to your project to help others discover and learn from your work.
| Project | Features | GitHub |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| 🚀 CoolAGI: Where AI meets Imagination<br/>![CoolAGI Logo](https://github.com/nextgen-user/freegpt4plus/assets/150797204/9b0e1232-4791-4d61-b949-16f9eb284c22) | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
| Project | Features | GitHub |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| 🚀 CoolAGI: Where AI meets Imagination<br/>![CoolAGI Logo](https://github.com/nextgen-user/freegpt4plus/assets/150797204/9b0e1232-4791-4d61-b949-16f9eb284c22) | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
For public projects, update your README.md with your modifications and submit a pull request to add your project to our list, aiding in its discovery.
<br/>
<br/>
## Best Practices
+1 -1
View File
@@ -53,7 +53,7 @@ As of Feb 27, 2024, this feature is in development.
## Configurations
| Scope | Default | Description / Instructions |
| Scope | Default | Description / Instructions |
|-----------------------------------------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------|
| Your source builds of big-AGI | None | **Vercel**: enable Vercel Analytics from the dashboard. · **Google Analytics**: set environment variable at build time. |
| Your docker builds of big-AGI | None | **Vercel**: n/a. · **Google Analytics**: set environment variable at `docker build` time. |
+1 -1
View File
@@ -9,7 +9,7 @@ Docker ensures faster development cycles, easier collaboration, and seamless env
```bash
git clone https://github.com/enricoros/big-agi.git
cd big-agi
```
```
2. **Build the Docker Image**: Build a local docker image from the provided Dockerfile:
```bash
docker build -t big-agi .
+3 -3
View File
@@ -91,7 +91,7 @@ requiring the user to enter an API key
| `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 |
| `GROQ_API_KEY` | The API key for Groq Cloud | Optional |
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
| `LOCALAI_API_KEY` | The (Optional) API key for LocalAI | Optional |
| `MISTRAL_API_KEY` | The API key for Mistral | Optional |
| `OLLAMA_API_HOST` | Changes the backend host for the Ollama vendor. See [config-local-ollama.md](config-local-ollama) | |
@@ -128,7 +128,7 @@ Enable the app to Talk, Draw, and Google things up.
| `GOOGLE_CSE_ID` | Google Custom/Programmable Search Engine ID - [Link to PSE](https://programmablesearchengine.google.com/) |
| **Browse** | |
| `PUPPETEER_WSS_ENDPOINT` | Puppeteer WebSocket endpoint - used for browsing (pade downloadeing), etc. |
| **Backend** | |
| **Backend** | |
| `BACKEND_ANALYTICS` | Semicolon-separated list of analytics flags (see backend.analytics.ts). Flags: `domain` logs the responding domain. |
| `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. |
@@ -147,5 +147,5 @@ The value of these variables are passed to the frontend (Web UI) - make sure the
---
For a higher level overview of backend code and environemnt customization,
For a higher level overview of backend code and environment customization,
see the [big-AGI Customization](customizations.md) guide.
+119
View File
@@ -0,0 +1,119 @@
# Installation Guide
Welcome to the big-AGI Installation Guide - Whether you're a developer
eager to explore, a system integrator, or an enterprise looking for a
white-label solution, this comprehensive guide ensures a smooth setup
process for your own instance of big-AGI and related products.
**Try big-AGI** - You don't need to install anything if you want to play with big-AGI
and have your API keys to various model services. You can access our free instance on [big-AGI.com](https://big-agi.com).
The free instance runs the latest `main-stable` branch from this repository.
## 🧩 Build-your-own
If you want to change the code, have a deeper configuration,
add your own models, or run your own instance, follow the steps below.
### Local Development
**Prerequisites:**
- Node.js and npm installed on your machine.
**Steps:**
1. Clone the big-AGI repository:
```bash
git clone https://github.com/enricoros/big-AGI.git
cd big-AGI
```
2. Install dependencies:
```bash
npm install
```
3. Run the development server:
```bash
npm run dev
```
Your big-AGI instance is now running at `http://localhost:3000`.
### Local Production build
The production build is optimized for performance and follows
the same steps 1 and 2 as for [local development](#local-development).
3. Build the production version:
```bash
# .. repeat the steps above up to `npm install`, then:
npm run build
```
4. Start the production server (`npx` may be optional):
```bash
npx next start --port 3000
```
Your big-AGI production instance is on `http://localhost:3000`.
### Advanced Customization
Want to pre-enable models, customize the interface, or deploy with username/password or alter code to your needs?
Check out the [Customizations Guide](README.md) for detailed instructions.
## ☁️ Cloud Deployment Options
To deploy big-AGI on a public server, you have several options. Choose the one that best fits your needs.
### Deploy on Vercel
Install big-AGI on Vercel with just a few clicks.
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&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)
### Deploy on Cloudflare
Deploy on Cloudflare's global network by installing big-AGI on
Cloudflare Pages. Check out the [Cloudflare Installation Guide](deploy-cloudflare.md)
for step-by-step instructions.
### Docker Deployments
Containerize your big-AGI installation using Docker for portability and scalability.
Our [Docker Deployment Guide](deploy-docker.md) will walk you through the process,
or follow the steps below for a quick start.
1. (optional) Build the Docker image - if you do not want to use the [pre-built Docker images](https://github.com/enricoros/big-AGI/pkgs/container/big-agi):
```bash
docker build -t big-agi .
```
2. Run the Docker container with either:
```bash
# 2A. if you built the image yourself:
docker run -d -p 3000:3000 big-agi
# 2B. or use the pre-built image:
docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi
# 2C. or use docker-compose:
docker-compose up
```
Access your big-AGI instance at `http://localhost:3000`.
### Midori AI Subsystem for Docker Deployment
Follow the instructions found on [Midori AI Subsystem Site](https://io.midori-ai.xyz/subsystem/manager/)
for your host OS. After completing the setup process, install the Big-AGI docker backend to the Midori AI Subsystem.
## Enterprise-Grade Installation
For businesses seeking a fully-managed, scalable solution, consider our managed installations.
Enjoy all the features of big-AGI without the hassle of infrastructure management. [hello@big-agi.com](mailto:hello@big-agi.com) to learn more.
## Support
Join our vibrant community of developers, researchers, and AI enthusiasts. Share your projects, get help, and collaborate with others.
- [Discord Community](https://discord.gg/MkH4qj2Jp9)
- [Twitter](https://twitter.com/yourusername)
For any questions or inquiries, please don't hesitate to [reach out to our team](mailto:hello@big-agi.com).
+1378 -775
View File
File diff suppressed because it is too large Load Diff
+32 -26
View File
@@ -1,6 +1,6 @@
{
"name": "big-agi",
"version": "1.14.1",
"version": "1.16.0",
"private": true,
"author": "Enrico Ros <enrico.ros@gmail.com>",
"repository": "https://github.com/enricoros/big-agi",
@@ -21,14 +21,15 @@
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.4",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.11",
"@mui/joy": "^5.0.0-beta.29",
"@next/bundle-analyzer": "^14.1.2",
"@next/third-parties": "^14.1.2",
"@prisma/client": "^5.10.2",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.15.17",
"@mui/joy": "^5.0.0-beta.36",
"@mui/material": "^5.15.17",
"@next/bundle-analyzer": "^14.2.3",
"@next/third-parties": "^14.2.3",
"@prisma/client": "^5.13.0",
"@sanity/diff-match-patch": "^3.1.1",
"@t3-oss/env-nextjs": "^0.9.2",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "~4.36.1",
"@trpc/client": "10.44.1",
"@trpc/next": "10.44.1",
@@ -37,49 +38,54 @@
"@vercel/analytics": "^1.2.2",
"@vercel/speed-insights": "^1.0.10",
"browser-fs-access": "^0.35.0",
"cheerio": "^1.0.0-rc.12",
"eventsource-parser": "^1.1.2",
"idb-keyval": "^6.2.1",
"next": "^14.1.2",
"next": "~14.1.4",
"nprogress": "^0.2.0",
"pdfjs-dist": "4.0.379",
"pdfjs-dist": "4.2.67",
"plantuml-encoder": "^1.4.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react": "^18.3.1",
"react-beautiful-dnd": "^13.1.1",
"react-csv": "^2.2.2",
"react-dom": "^18.2.0",
"react-dom": "^18.3.1",
"react-katex": "^3.0.1",
"react-markdown": "^9.0.1",
"react-player": "^2.15.1",
"react-resizable-panels": "^2.0.12",
"react-player": "^2.16.0",
"react-resizable-panels": "^2.0.19",
"react-timeago": "^7.2.0",
"rehype-katex": "^7.0.0",
"remark-gfm": "^4.0.0",
"sharp": "^0.33.2",
"remark-math": "^6.0.0",
"sharp": "^0.33.3",
"superjson": "^2.2.1",
"tesseract.js": "^5.0.5",
"tiktoken": "^1.0.13",
"tesseract.js": "^5.1.0",
"tiktoken": "^1.0.15",
"turndown": "^7.2.0",
"uuid": "^9.0.1",
"zod": "^3.22.4",
"zod": "^3.23.8",
"zustand": "^4.5.2"
},
"devDependencies": {
"@cloudflare/puppeteer": "^0.0.5",
"@types/node": "^20.11.24",
"@cloudflare/puppeteer": "0.0.5",
"@types/node": "^20.12.11",
"@types/nprogress": "^0.2.3",
"@types/plantuml-encoder": "^1.4.2",
"@types/prismjs": "^1.26.3",
"@types/react": "^18.2.62",
"@types/prismjs": "^1.26.4",
"@types/react": "^18.3.1",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-csv": "^1.1.10",
"@types/react-dom": "^18.2.19",
"@types/react-dom": "^18.3.0",
"@types/react-katex": "^3.0.4",
"@types/react-timeago": "^4.1.7",
"@types/turndown": "^5.0.4",
"@types/uuid": "^9.0.8",
"eslint": "^8.57.0",
"eslint-config-next": "^14.1.2",
"eslint-config-next": "^14.2.3",
"prettier": "^3.2.5",
"prisma": "^5.10.2",
"typescript": "^5.3.3"
"prisma": "^5.13.0",
"typescript": "^5.4.5"
},
"engines": {
"node": "^20.0.0 || ^18.0.0"
+11 -10
View File
@@ -13,11 +13,11 @@ import '~/common/styles/GithubMarkdown.css';
import '~/common/styles/NProgress.css';
import '~/common/styles/app.styles.css';
import { ProviderBackendAndNoSSR } from '~/common/providers/ProviderBackendAndNoSSR';
import { ProviderBackendCapabilities } from '~/common/providers/ProviderBackendCapabilities';
import { ProviderBootstrapLogic } from '~/common/providers/ProviderBootstrapLogic';
import { ProviderSingleTab } from '~/common/providers/ProviderSingleTab';
import { ProviderSnacks } from '~/common/providers/ProviderSnacks';
import { ProviderTRPCQueryClient } from '~/common/providers/ProviderTRPCQueryClient';
import { ProviderTRPCQuerySettings } from '~/common/providers/ProviderTRPCQuerySettings';
import { ProviderTheming } from '~/common/providers/ProviderTheming';
import { hasGoogleAnalytics, OptionalGoogleAnalytics } from '~/common/components/GoogleAnalytics';
import { isVercelFromFrontend } from '~/common/util/pwaUtils';
@@ -33,15 +33,16 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
<ProviderTheming emotionCache={emotionCache}>
<ProviderSingleTab>
<ProviderBootstrapLogic>
<ProviderTRPCQueryClient>
<ProviderSnacks>
<ProviderBackendAndNoSSR>
<ProviderTRPCQuerySettings>
<ProviderBackendCapabilities>
{/* ^ SSR boundary */}
<ProviderBootstrapLogic>
<ProviderSnacks>
<Component {...pageProps} />
</ProviderBackendAndNoSSR>
</ProviderSnacks>
</ProviderTRPCQueryClient>
</ProviderBootstrapLogic>
</ProviderSnacks>
</ProviderBootstrapLogic>
</ProviderBackendCapabilities>
</ProviderTRPCQuerySettings>
</ProviderSingleTab>
</ProviderTheming>
+1 -1
View File
@@ -26,7 +26,7 @@ export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
<link rel='icon' type='image/png' sizes='16x16' href='/icons/favicon-16x16.png' />
<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
<link rel='manifest' href='/manifest.json' />
<meta name='apple-mobile-web-app-capable' content='yes' />
<meta name='mobile-web-app-capable' content='yes' />
<meta name='apple-mobile-web-app-status-bar-style' content='black' />
{/* Opengraph */}
+10
View File
@@ -0,0 +1,10 @@
import * as React from 'react';
import { AppBeam } from '../../src/apps/beam/AppBeam';
import { withLayout } from '~/common/layout/withLayout';
export default function BeamPage() {
return withLayout({ type: 'optima' }, <AppBeam />);
}
+8 -6
View File
@@ -6,7 +6,7 @@ import DownloadIcon from '@mui/icons-material/Download';
import { AppPlaceholder } from '../../src/apps/AppPlaceholder';
import { backendCaps } from '~/modules/backend/state-backend';
import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities';
import { getPlantUmlServerUrl } from '~/modules/blocks/code/RenderCode';
import { withLayout } from '~/common/layout/withLayout';
@@ -17,7 +17,7 @@ import { Brand } from '~/common/app.config';
import { ROUTE_APP_CHAT, ROUTE_INDEX } from '~/common/app.routes';
// apps access
import { incrementalNewsVersion } from '../../src/apps/news/news.version';
import { incrementalNewsVersion, useAppNewsStateStore } from '../../src/apps/news/news.version';
// capabilities access
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs, useCapabilityTextToImage } from '~/common/components/useCapabilities';
@@ -32,6 +32,7 @@ import { useUXLabsStore } from '~/common/state/store-ux-labs';
// utils access
import { clientHostName, isChromeDesktop, isFirefox, isIPhoneUser, isMacUser, isPwa, isVercelFromFrontend } from '~/common/util/pwaUtils';
import { getGA4MeasurementId } from '~/common/components/GoogleAnalytics';
import { prettyTimestampForFilenames } from '~/common/util/timeUtils';
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
@@ -76,11 +77,12 @@ function AppDebug() {
const [saved, setSaved] = React.useState(false);
// external state
const backendCapabilities = backendCaps();
const backendCaps = getBackendCapabilities();
const chatsCount = useChatStore.getState().conversations?.length;
const uxLabsExperiments = Object.entries(useUXLabsStore.getState()).filter(([_k, v]) => v === true).map(([k, _]) => k).join(', ');
const { folders, enableFolders } = useFolderStore.getState();
const { lastSeenNewsVersion, usageCount } = useAppStateStore.getState();
const { lastSeenNewsVersion } = useAppNewsStateStore.getState();
const { usageCount } = useAppStateStore.getState();
// derived state
@@ -112,7 +114,7 @@ function AppDebug() {
},
};
const cBackend = {
configuration: backendCapabilities,
configuration: backendCaps,
deployment: {
home: Brand.URIs.Home,
hostName: clientHostName(),
@@ -127,7 +129,7 @@ function AppDebug() {
const handleDownload = async () => {
fileSave(
new Blob([JSON.stringify({ client: cClient, agi: cProduct, backend: cBackend }, null, 2)], { type: 'application/json' }),
{ fileName: `big-agi-debug-${new Date().toISOString().replace(/:/g, '-')}.json`, extensions: ['.json'] },
{ fileName: `big-agi_debug_${prettyTimestampForFilenames()}.json`, extensions: ['.json'] },
)
.then(() => setSaved(true))
.catch(e => console.error('Error saving debug.json', e));
+6 -3
View File
@@ -77,9 +77,12 @@ function AppShareTarget() {
setIsDownloading(true);
callBrowseFetchPage(intentURL)
.then(page => {
if (page.stopReason !== 'error')
queueComposerTextAndLaunchApp('\n\n```' + intentURL + '\n' + page.content + '\n```\n');
else
if (page.stopReason !== 'error') {
let pageContent = page.content.markdown || page.content.text || page.content.html || '';
if (pageContent)
pageContent = '\n\n```' + intentURL + '\n' + pageContent + '\n```\n';
queueComposerTextAndLaunchApp(pageContent);
} else
setErrorMessage('Could not read any data' + page.error ? ': ' + page.error : '');
})
.catch(error => setErrorMessage(error?.message || error || 'Unknown error'))
Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

+28 -3
View File
@@ -3,9 +3,16 @@
"short_name": "big-AGI",
"theme_color": "#32383E",
"background_color": "#9FA6AD",
"description": "Personal AGI App",
"description": "Your Generative AI Suite",
"categories": [
"productivity",
"AI",
"tool",
"utilities"
],
"display": "standalone",
"start_url": "/",
"start_url": "/?source=pwa",
"scope": "/",
"icons": [
{
"src": "/icons/icon-192x192.png",
@@ -24,6 +31,17 @@
"type": "image/png"
}
],
"file_handlers": [
{
"action": "/link/share_target",
"accept": {
"application/big-agi": [
".agi",
".agi.json"
]
}
}
],
"share_target": {
"action": "/link/share_target",
"method": "GET",
@@ -33,5 +51,12 @@
"text": "text",
"url": "url"
}
}
},
"shortcuts": [
{
"name": "Call",
"url": "/call",
"description": "Call a Persona"
}
]
}
File diff suppressed because one or more lines are too long
+17 -15
View File
@@ -10,7 +10,7 @@ import { useRouterRoute } from '~/common/app.routes';
* https://github.com/enricoros/big-AGI/issues/299
*/
export function AppPlaceholder(props: {
title?: string,
title?: string | null,
text?: React.ReactNode,
children?: React.ReactNode,
}) {
@@ -29,23 +29,25 @@ export function AppPlaceholder(props: {
border: '1px solid blue',
}}>
<Box sx={{
my: 'auto',
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: 4,
border: '1px solid red',
}}>
{(props.title !== null || !!props.text) && (
<Box sx={{
my: 'auto',
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: 4,
border: '1px solid red',
}}>
<Typography level='h1'>
{placeholderAppName}
</Typography>
{!!props.text && (
<Typography>
{props.text}
<Typography level='h1'>
{placeholderAppName}
</Typography>
)}
{!!props.text && (
<Typography>
{props.text}
</Typography>
)}
</Box>
</Box>
)}
{props.children}
+105
View File
@@ -0,0 +1,105 @@
import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { Box, Button, Typography } from '@mui/joy';
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
import { BeamView } from '~/modules/beam/BeamView';
import { createBeamVanillaStore } from '~/modules/beam/store-beam-vanilla';
import { useModelsStore } from '~/modules/llms/store-llms';
import { createDConversation, createDMessage, DConversation, DMessage } from '~/common/state/store-chats';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
function initTestConversation(): DConversation {
const conversation = createDConversation();
conversation.messages.push(createDMessage('system', 'You are a helpful assistant.'));
conversation.messages.push(createDMessage('user', 'Hello, who are you? (please expand...)'));
return conversation;
}
function initTestBeamStore(messages: DMessage[], beamStore: BeamStoreApi = createBeamVanillaStore()): BeamStoreApi {
beamStore.getState().open(messages, useModelsStore.getState().chatLLMId, (text) => alert(text));
return beamStore;
}
export function AppBeam() {
// state
const [showDebug, setShowDebug] = React.useState(false);
const [conversation, setConversation] = React.useState<DConversation>(() => initTestConversation());
const [beamStoreApi] = React.useState(() => createBeamVanillaStore());
// reinit the beam store if the conversation changes
React.useEffect(() => {
initTestBeamStore(conversation.messages, beamStoreApi);
}, [beamStoreApi, conversation]);
// external state
const isMobile = useIsMobile();
const { isOpen, beamState } = useBeamStore(beamStoreApi, useShallow(state => {
return {
isOpen: state.isOpen,
beamState: showDebug ? state : null,
};
}));
const handleClose = React.useCallback(() => {
beamStoreApi.getState().terminateKeepingSettings();
}, [beamStoreApi]);
// layout
usePluggableOptimaLayout(null, React.useMemo(() => <>
{/* button to toggle debug info */}
<Button size='sm' variant='plain' color='neutral' onClick={() => setShowDebug(on => !on)}>
{showDebug ? 'Hide' : 'Show'} debug
</Button>
{/* 'open' */}
<Button size='sm' variant='plain' color='neutral' onClick={() => setConversation(initTestConversation())}>
.open
</Button>
{/* 'close' */}
<Button size='sm' variant='plain' color='neutral' onClick={handleClose}>
.close
</Button>
</>, [handleClose, showDebug]), null, 'AppBeam');
return (
<Box sx={{ flexGrow: 1, overflowY: 'auto', position: 'relative' }}>
{isOpen && (
<BeamView
beamStore={beamStoreApi}
isMobile={isMobile}
/>
)}
{showDebug && (
<Typography level='body-xs' sx={{
whiteSpace: 'pre',
position: 'absolute',
inset: 0,
zIndex: 1 /* debug on top of BeamView */,
backdropFilter: 'blur(4px)',
padding: '1rem',
}}>
{JSON.stringify(beamState, null, 2)
// add an extra newline between first level properties (space, space, double quote) to make it more readable
.split('\n').map(line => line.replace(/^\s\s"/g, '\n ')).join('\n')}
</Typography>
)}
</Box>
);
}
+8 -46
View File
@@ -1,60 +1,22 @@
import * as React from 'react';
import { Box, Button, Card, CardContent, IconButton, ListItemDecorator, Typography } from '@mui/joy';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
import ChatIcon from '@mui/icons-material/Chat';
import CheckIcon from '@mui/icons-material/Check';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import MicIcon from '@mui/icons-material/Mic';
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { cssRainbowColorKeyframes } from '~/common/app.theme';
import { animationColorRainbow } from '~/common/util/animUtils';
import { navigateBack } from '~/common/app.routes';
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
import { useChatStore } from '~/common/state/store-chats';
import { useUICounter } from '~/common/state/store-ui';
/*export const cssRainbowBackgroundKeyframes = keyframes`
100%, 0% {
background-color: rgb(128, 0, 0);
}
8% {
background-color: rgb(102, 51, 0);
}
16% {
background-color: rgb(64, 64, 0);
}
25% {
background-color: rgb(38, 76, 0);
}
33% {
background-color: rgb(0, 89, 0);
}
41% {
background-color: rgb(0, 76, 41);
}
50% {
background-color: rgb(0, 64, 64);
}
58% {
background-color: rgb(0, 51, 102);
}
66% {
background-color: rgb(0, 0, 128);
}
75% {
background-color: rgb(63, 0, 128);
}
83% {
background-color: rgb(76, 0, 76);
}
91% {
background-color: rgb(102, 0, 51);
}`;*/
function StatusCard(props: { icon: React.JSX.Element, hasIssue: boolean, text: string, button?: React.JSX.Element }) {
return (
<Card sx={{ width: '100%' }}>
@@ -67,7 +29,7 @@ function StatusCard(props: { icon: React.JSX.Element, hasIssue: boolean, text: s
{props.button}
</Typography>
<ListItemDecorator>
{props.hasIssue ? <WarningRoundedIcon color='warning' /> : <CheckIcon color='success' />}
{props.hasIssue ? <WarningRoundedIcon color='warning' /> : <CheckRoundedIcon color='success' />}
</ListItemDecorator>
</CardContent>
</Card>
@@ -124,7 +86,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
<Typography level='title-lg' sx={{ fontSize: '3rem', fontWeight: 'sm', textAlign: 'center' }}>
Welcome to<br />
<Box component='span' sx={{ animation: `${cssRainbowColorKeyframes} 15s linear infinite` }}>
<Box component='span' sx={{ animation: `${animationColorRainbow} 15s linear infinite` }}>
your first call
</Box>
</Typography>
@@ -167,7 +129,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
{/* Text to Speech status */}
<StatusCard
icon={<RecordVoiceOverIcon />}
icon={<RecordVoiceOverTwoToneIcon />}
text={
(synthesis.mayWork ? 'Voice synthesis should be ready.' : 'There might be an issue with ElevenLabs voice synthesis.')
+ (synthesis.isConfiguredServerSide ? '' : (synthesis.isConfiguredClientSide ? '' : ' Please add your API key in the settings.'))
@@ -208,7 +170,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string | n
// boxShadow: allGood ? 'md' : 'none',
}}
>
{allGood ? <ArrowForwardIcon sx={{ fontSize: '1.5em' }} /> : <CloseRoundedIcon sx={{ fontSize: '1.5em' }} />}
{allGood ? <ArrowForwardRoundedIcon sx={{ fontSize: '1.5em' }} /> : <CloseRoundedIcon sx={{ fontSize: '1.5em' }} />}
</IconButton>
</Box>
+2 -24
View File
@@ -1,12 +1,12 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { keyframes } from '@emotion/react';
import type { SxProps } from '@mui/joy/styles/types';
import { Avatar, Box, Card, CardContent, Chip, IconButton, Link as MuiLink, ListDivider, MenuItem, Sheet, Switch, Typography } from '@mui/joy';
import CallIcon from '@mui/icons-material/Call';
import { GitHubProjectIssueCard } from '~/common/components/GitHubProjectIssueCard';
import { animationShadowRingLimey } from '~/common/util/animUtils';
import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
@@ -19,27 +19,6 @@ import { useAppCallStore } from './state/store-app-call';
const COLLAPSED_COUNT = 2;
export const niceShadowKeyframes = keyframes`
100%, 0% {
//background-color: rgb(102, 0, 51);
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(183, 255, 0);
}
25% {
//background-color: rgb(76, 0, 76);
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(255, 251, 0);
//scale: 1.2;
}
50% {
//background-color: rgb(63, 0, 128);
box-shadow: 1px 1px 0 white, 2px 2px 12px rgba(0, 255, 81);
//scale: 0.8;
}
75% {
//background-color: rgb(0, 0, 128);
box-shadow: 1px 1px 0 white, 2px 2px 12px rgb(255, 153, 0);
}`;
const ContactCardAvatar = (props: { size: string, symbol?: string, imageUrl?: string, onClick?: () => void, sx?: SxProps }) =>
<Avatar
// variant='outlined'
@@ -125,7 +104,6 @@ function CallContactCard(props: {
sx={{
mx: 'auto',
mt: '-2.5rem',
zIndex: 1,
}}
/>
@@ -282,7 +260,7 @@ export function Contacts(props: { setCallIntent: (intent: AppCallIntent) => void
borderRadius: '50%',
pointerEvents: 'none',
backgroundColor: 'background.popup',
animation: `${niceShadowKeyframes} 5s infinite`,
animation: `${animationShadowRingLimey} 5s infinite`,
}}>
<CallIcon />
</IconButton>
+10 -23
View File
@@ -1,5 +1,5 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';
import { Box, Card, ListDivider, ListItemDecorator, MenuItem, Switch, Typography } from '@mui/joy';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -7,10 +7,10 @@ import CallEndIcon from '@mui/icons-material/CallEnd';
import CallIcon from '@mui/icons-material/Call';
import MicIcon from '@mui/icons-material/Mic';
import MicNoneIcon from '@mui/icons-material/MicNone';
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from '../chat/components/scroll-to-bottom/ScrollToBottomButton';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
import { useChatLLMDropdown } from '../chat/components/useLLMDropdown';
import { EXPERIMENTAL_speakTextStream } from '~/modules/elevenlabs/elevenlabs.client';
@@ -57,7 +57,7 @@ function CallMenuItems(props: {
</MenuItem>
<MenuItem onClick={handleChangeVoiceToggle}>
<ListItemDecorator><RecordVoiceOverIcon /></ListItemDecorator>
<ListItemDecorator><RecordVoiceOverTwoToneIcon /></ListItemDecorator>
Change Voice
<Switch checked={props.override} onChange={handleChangeVoiceToggle} sx={{ ml: 'auto' }} />
</MenuItem>
@@ -99,7 +99,7 @@ export function Telephone(props: {
// external state
const { chatLLMId, chatLLMDropdown } = useChatLLMDropdown();
const { chatTitle, reMessages } = useChatStore(state => {
const { chatTitle, reMessages } = useChatStore(useShallow(state => {
const conversation = props.callIntent.conversationId
? state.conversations.find(conversation => conversation.id === props.callIntent.conversationId) ?? null
: null;
@@ -107,7 +107,7 @@ export function Telephone(props: {
chatTitle: conversation ? conversationTitle(conversation) : null,
reMessages: conversation ? conversation.messages : null,
};
}, shallow);
}));
const persona = SystemPurposes[props.callIntent.personaId as SystemPurposeId] ?? undefined;
const personaCallStarters = persona?.call?.starters ?? undefined;
const personaVoiceId = overridePersonaVoice ? undefined : (persona?.voices?.elevenLabs?.voiceId ?? undefined);
@@ -225,7 +225,7 @@ export function Telephone(props: {
let finalText = '';
let error: any | null = null;
setPersonaTextInterim('💭...');
llmStreamingChatGenerate(chatLLMId, callPrompt, null, null, responseAbortController.current.signal, ({ textSoFar }) => {
llmStreamingChatGenerate(chatLLMId, callPrompt, 'call', callMessages[0].id, null, null, responseAbortController.current.signal, ({ textSoFar }) => {
const text = textSoFar?.trim();
if (text) {
finalText = text;
@@ -331,22 +331,9 @@ export function Telephone(props: {
padding: 0, // move this to the ScrollToBottom component
}}>
<ScrollToBottom
// bootToBottom
stickToBottom
sx={{
// allows the content to be scrolled (all browsers)
overflowY: 'auto',
// actually make sure this scrolls & fills
height: '100%',
<ScrollToBottom stickToBottomInitial>
// content
display: 'grid',
padding: 1,
}}
>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Box sx={{ minHeight: '100%', p: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>
{/* Call Messages [] */}
{callMessages.map((message) =>
+2 -13
View File
@@ -1,19 +1,8 @@
import * as React from 'react';
import { keyframes } from '@emotion/react';
import { Avatar, Box } from '@mui/joy';
const cssScaleKeyframes = keyframes`
0% {
transform: scale(1);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}`;
import { animationScalePulse } from '~/common/util/animUtils';
export function CallAvatar(props: { symbol: string, imageUrl?: string, isRinging?: boolean, onClick: () => void }) {
@@ -34,7 +23,7 @@ export function CallAvatar(props: { symbol: string, imageUrl?: string, isRinging
<Box
sx={{
...(props.isRinging
? { animation: `${cssScaleKeyframes} 1.4s ease-in-out infinite` }
? { animation: `${animationScalePulse} 1.4s ease-in-out infinite` }
: {}),
}}
>
+247 -256
View File
@@ -1,47 +1,48 @@
import * as React from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import type { SxProps } from '@mui/joy/styles/types';
import { useTheme } from '@mui/joy';
import { DEV_MODE_SETTINGS } from '../settings-modal/UxLabsSettings';
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
import { downloadConversation, openAndLoadConversations } from '~/modules/trade/trade.client';
import { getChatLLMId, useChatLLM } from '~/modules/llms/store-llms';
import { imaginePromptFromText } from '~/modules/aifn/imagine/imaginePromptFromText';
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
import { useAreBeamsOpen } from '~/modules/beam/store-beam.hooks';
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
import { Brand } from '~/common/app.config';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { ConversationManager } from '~/common/chats/ConversationHandler';
import { ConversationsManager } from '~/common/chats/ConversationsManager';
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
import { PreferencesTab, useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
import { createDMessage, DConversationId, DMessage, getConversation, getConversationSystemPurposeId, useConversation } from '~/common/state/store-chats';
import { getUXLabsHighPerformance, useUXLabsStore } from '~/common/state/store-ux-labs';
import { createDMessage, DConversationId, DMessage, DMessageMetadata, getConversation, getConversationSystemPurposeId, useConversation } from '~/common/state/store-chats';
import { themeBgAppChatComposer } from '~/common/app.theme';
import { useFolderStore } from '~/common/state/store-folders';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useRouterQuery } from '~/common/app.routes';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
import { Beam } from './components/beam/Beam';
import { ChatBarAltBeam } from './components/ChatBarAltBeam';
import { ChatBarAltTitle } from './components/ChatBarAltTitle';
import { ChatBarDropdowns } from './components/ChatBarDropdowns';
import { ChatBeamWrapper } from './components/ChatBeamWrapper';
import { ChatDrawerMemo } from './components/ChatDrawer';
import { ChatDropdowns } from './components/ChatDropdowns';
import { ChatMessageList } from './components/ChatMessageList';
import { ChatPageMenuItems } from './components/ChatPageMenuItems';
import { ChatTitle } from './components/ChatTitle';
import { Composer } from './components/composer/Composer';
import { ScrollToBottom } from './components/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from './components/scroll-to-bottom/ScrollToBottomButton';
import { getInstantAppChatPanesCount, usePanesManager } from './components/panes/usePanesManager';
import { usePanesManager } from './components/panes/usePanesManager';
import { extractChatCommand, findAllChatCommands } from './commands/commands.registry';
import { runAssistantUpdatingState } from './editors/chat-stream';
import { runBrowseGetPageUpdatingState } from './editors/browse-load';
import { runImageGenerationUpdatingState } from './editors/image-generate';
import { runReActUpdatingState } from './editors/react-tangent';
import { _handleExecute } from './editors/_handleExecute';
// what to say when a chat is new and has no title
@@ -59,6 +60,24 @@ export type ChatModeId =
| 'generate-react';
export interface AppChatIntent {
initialConversationId: string | null;
}
const composerOpenSx: SxProps = {
zIndex: 21, // just to allocate a surface, and potentially have a shadow
backgroundColor: themeBgAppChatComposer,
borderTop: `1px solid`,
borderTopColor: 'divider',
p: { xs: 1, md: 2 },
};
const composerClosedSx: SxProps = {
display: 'none',
};
export function AppChat() {
// state
@@ -78,35 +97,55 @@ export function AppChat() {
const isMobile = useIsMobile();
const showAltTitleBar = useUXLabsStore(state => state.labsChatBarAlt === 'title');
const intent = useRouterQuery<Partial<AppChatIntent>>();
const { openLlmOptions } = useOptimaLayout();
const showAltTitleBar = useUXLabsStore(state => DEV_MODE_SETTINGS && state.labsChatBarAlt === 'title');
const { openLlmOptions, openModelsSetup, openPreferencesTab } = useOptimaLayout();
const { chatLLM } = useChatLLM();
const {
// state
chatPanes,
focusedConversationId,
focusedPaneIndex,
focusedPaneConversationId,
// actions
navigateHistoryInFocusedPane,
openConversationInFocusedPane,
openConversationInSplitPane,
focusedPaneIndex,
removePane,
setFocusedPane,
setFocusedPaneIndex,
} = usePanesManager();
const chatHandlers = React.useMemo(() => chatPanes.map(pane => {
return pane.conversationId ? ConversationsManager.getHandler(pane.conversationId) : null;
}), [chatPanes]);
const beamsStores = React.useMemo(() => chatHandlers.map(handler => {
return handler?.getBeamStore() ?? null;
}), [chatHandlers]);
const beamsOpens = useAreBeamsOpen(beamsStores);
const beamOpenStoreInFocusedPane = React.useMemo(() => {
const open = focusedPaneIndex !== null ? (beamsOpens?.[focusedPaneIndex] ?? false) : false;
return open ? beamsStores?.[focusedPaneIndex!] ?? null : null;
}, [beamsOpens, beamsStores, focusedPaneIndex]);
const {
// focused
title: focusedChatTitle,
isChatEmpty: isFocusedChatEmpty,
isEmpty: isFocusedChatEmpty,
isDeveloper: isFocusedChatDeveloper,
areChatsEmpty,
conversationIdx: focusedChatNumber,
newConversationId,
// all
hasConversations,
recycleNewConversationId,
// actions
prependNewConversation,
branchConversation,
deleteConversations,
setMessages,
} = useConversation(focusedConversationId);
} = useConversation(focusedPaneConversationId);
const { mayWork: capabilityHasT2I } = useCapabilityTextToImage();
@@ -127,23 +166,25 @@ export function AppChat() {
const willMulticast = isComposerMulticast && isMultiConversationId;
const disableNewButton = isFocusedChatEmpty && !isMultiPane;
const chatHandlers = React.useMemo(() => chatPanes.map(pane => {
return pane.conversationId ? ConversationManager.getHandler(pane.conversationId) : null;
}), [chatPanes]);
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
const handleOpenConversationInFocusedPane = React.useCallback((conversationId: DConversationId | null) => {
conversationId && openConversationInFocusedPane(conversationId);
}, [openConversationInFocusedPane]);
const openSplitConversationId = React.useCallback((conversationId: DConversationId | null) => {
const handleOpenConversationInSplitPane = React.useCallback((conversationId: DConversationId | null) => {
conversationId && openConversationInSplitPane(conversationId);
}, [openConversationInSplitPane]);
const handleNavigateHistory = React.useCallback((direction: 'back' | 'forward') => {
const handleNavigateHistoryInFocusedPane = React.useCallback((direction: 'back' | 'forward') => {
if (navigateHistoryInFocusedPane(direction))
showNextTitleChange.current = true;
}, [navigateHistoryInFocusedPane]);
// [effect] Handle the initial conversation intent
React.useEffect(() => {
intent.initialConversationId && handleOpenConversationInFocusedPane(intent.initialConversationId);
}, [handleOpenConversationInFocusedPane, intent.initialConversationId]);
// [effect] Show snackbar with the focused chat title after a history navigation in focused pane
React.useEffect(() => {
if (showNextTitleChange.current) {
showNextTitleChange.current = false;
@@ -156,101 +197,20 @@ export function AppChat() {
// Execution
const _handleExecute = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]): Promise<void> => {
const chatLLMId = getChatLLMId();
if (!chatModeId || !conversationId || !chatLLMId) return;
const handleExecuteAndOutcome = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) => {
const outcome = await _handleExecute(chatModeId, conversationId, history);
if (outcome === 'err-no-chatllm')
openModelsSetup();
else if (outcome === 'err-t2i-unconfigured')
openPreferencesTab(PreferencesTab.Draw);
else if (outcome === 'err-no-persona')
addSnackbar({ key: 'chat-no-persona', message: 'No persona selected.', type: 'issue' });
else if (outcome === 'err-no-conversation')
addSnackbar({ key: 'chat-no-conversation', message: 'No active conversation.', type: 'issue' });
return outcome === true;
}, [openModelsSetup, openPreferencesTab]);
// "/command ...": overrides the chat mode
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
if (lastMessage?.role === 'user') {
const chatCommand = extractChatCommand(lastMessage.text)[0];
if (chatCommand && chatCommand.type === 'cmd') {
switch (chatCommand.providerId) {
case 'ass-beam':
return ConversationManager.getHandler(conversationId).beamStore.create(history);
case 'ass-browse':
setMessages(conversationId, history);
return await runBrowseGetPageUpdatingState(conversationId, chatCommand.params!);
case 'ass-t2i':
setMessages(conversationId, history);
return await runImageGenerationUpdatingState(conversationId, chatCommand.params!);
case 'ass-react':
setMessages(conversationId, history);
return await runReActUpdatingState(conversationId, chatCommand.params!, chatLLMId);
case 'chat-alter':
if (chatCommand.command === '/clear') {
if (chatCommand.params === 'all')
return setMessages(conversationId, []);
const helpMessage = createDMessage('assistant', 'This command requires the \'all\' parameter to confirm the operation.');
helpMessage.originLLM = Brand.Title.Base;
return setMessages(conversationId, [...history, helpMessage]);
}
Object.assign(lastMessage, {
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
sender: 'Bot',
text: chatCommand.params || '',
} satisfies Partial<DMessage>);
return setMessages(conversationId, history);
case 'cmd-help':
const chatCommandsText = findAllChatCommands()
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
.join('\n');
const helpMessage = createDMessage('assistant', 'Available Chat Commands:\n' + chatCommandsText);
helpMessage.originLLM = Brand.Title.Base;
return setMessages(conversationId, [...history, helpMessage]);
default:
return setMessages(conversationId, [...history, createDMessage('assistant', 'This command is not supported.')]);
}
}
}
// get the focused system purpose (note: we don't react to it, or it would invalidate half UI components..)
const conversationSystemPurposeId = getConversationSystemPurposeId(conversationId);
if (!conversationSystemPurposeId)
return setMessages(conversationId, [...history, createDMessage('assistant', 'No persona selected.')]);
// synchronous long-duration tasks, which update the state as they go
if (chatLLMId) {
switch (chatModeId) {
case 'generate-text':
return await runAssistantUpdatingState(conversationId, history, chatLLMId, conversationSystemPurposeId, getUXLabsHighPerformance() ? 0 : getInstantAppChatPanesCount());
case 'generate-text-beam':
return ConversationManager.getHandler(conversationId).beamStore.create(history);
case 'append-user':
return setMessages(conversationId, history);
case 'generate-image':
if (!lastMessage?.text)
break;
// also add a 'fake' user message with the '/draw' command
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
...message,
text: `/draw ${lastMessage.text}`,
}));
return await runImageGenerationUpdatingState(conversationId, lastMessage.text);
case 'generate-react':
if (!lastMessage?.text)
break;
setMessages(conversationId, history);
return await runReActUpdatingState(conversationId, lastMessage.text, chatLLMId);
}
}
// ISSUE: if we're here, it means we couldn't do the job, at least sync the history
console.log('handleExecuteConversation: issue running', chatModeId, conversationId, lastMessage);
setMessages(conversationId, history);
}, [setMessages]);
const handleComposerAction = React.useCallback((chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart): boolean => {
const handleComposerAction = React.useCallback((conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: ComposerOutputMultiPart, metadata?: DMessageMetadata): boolean => {
// validate inputs
if (multiPartMessage.length !== 1 || multiPartMessage[0].type !== 'text-block') {
addSnackbar({
@@ -266,50 +226,63 @@ export function AppChat() {
const userText = multiPartMessage[0].text;
// multicast: send the message to all the panes
const uniqueIds = new Set([conversationId]);
const uniqueConversationIds = new Set([conversationId]);
if (willMulticast)
chatPanes.forEach(pane => pane.conversationId && uniqueIds.add(pane.conversationId));
chatPanes.forEach(pane => pane.conversationId && uniqueConversationIds.add(pane.conversationId));
// we loop to handle both the normal and multicast modes
let enqueued = false;
for (const _cId of uniqueIds) {
const _conversation = getConversation(_cId);
if (_conversation) {
// start execution fire/forget
void _handleExecute(chatModeId, _cId, [..._conversation.messages, createDMessage('user', userText)]);
enqueued = true;
}
let enqueuedAny = false;
for (const _cId of uniqueConversationIds) {
const history = getConversation(_cId)?.messages;
if (!history) continue;
const newUserMessage = createDMessage('user', userText);
if (metadata) newUserMessage.metadata = metadata;
// fire/forget
void handleExecuteAndOutcome(chatModeId, _cId, [...history, newUserMessage]);
enqueuedAny = true;
}
return enqueued;
}, [chatPanes, willMulticast, _handleExecute]);
return enqueuedAny;
}, [chatPanes, handleExecuteAndOutcome, willMulticast]);
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[], chatEffectBeam: boolean): Promise<void> => {
await _handleExecute(!chatEffectBeam ? 'generate-text' : 'generate-text-beam', conversationId, history);
}, [_handleExecute]);
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[]) => {
await handleExecuteAndOutcome('generate-text', conversationId, history);
}, [handleExecuteAndOutcome]);
const handleMessageRegenerateLast = React.useCallback(async () => {
const focusedConversation = getConversation(focusedConversationId);
const handleMessageRegenerateLastInFocusedPane = React.useCallback(async () => {
const focusedConversation = getConversation(focusedPaneConversationId);
if (focusedConversation?.messages?.length) {
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
return await _handleExecute('generate-text', focusedConversation.id, lastMessage.role === 'assistant'
? focusedConversation.messages.slice(0, -1)
: [...focusedConversation.messages],
);
const history = lastMessage.role === 'assistant' ? focusedConversation.messages.slice(0, -1) : [...focusedConversation.messages];
await handleExecuteAndOutcome('generate-text', focusedConversation.id, history);
}
}, [focusedConversationId, _handleExecute]);
}, [focusedPaneConversationId, handleExecuteAndOutcome]);
const handleMessageBeamLastInFocusedPane = React.useCallback(async () => {
// Ctrl + Shift + B
const focusedConversation = getConversation(focusedPaneConversationId);
if (focusedConversation?.messages?.length) {
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
if (lastMessage.role === 'assistant')
ConversationsManager.getHandler(focusedConversation.id).beamInvoke(focusedConversation.messages.slice(0, -1), [lastMessage], lastMessage.id);
else if (lastMessage.role === 'user')
ConversationsManager.getHandler(focusedConversation.id).beamInvoke(focusedConversation.messages, [], null);
}
}, [focusedPaneConversationId]);
const handleTextDiagram = React.useCallback((diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig), []);
const handleTextImagine = React.useCallback(async (conversationId: DConversationId, messageText: string): Promise<void> => {
const handleTextImagine = React.useCallback(async (conversationId: DConversationId, messageText: string) => {
const conversation = getConversation(conversationId);
if (!conversation)
return;
const imaginedPrompt = await imaginePromptFromText(messageText) || 'An error sign.';
return await _handleExecute('generate-image', conversationId, [
await handleExecuteAndOutcome('generate-image', conversationId, [
...conversation.messages,
createDMessage('user', imaginedPrompt),
]);
}, [_handleExecute]);
}, [handleExecuteAndOutcome]);
const handleTextSpeak = React.useCallback(async (text: string): Promise<void> => {
await speakText(text);
@@ -318,13 +291,15 @@ export function AppChat() {
// Chat actions
const handleConversationNew = React.useCallback((forceNoRecycle?: boolean) => {
const handleConversationNewInFocusedPane = React.useCallback((forceNoRecycle?: boolean) => {
// activate an existing new conversation if present, or create another
const conversationId = (newConversationId && !forceNoRecycle)
? newConversationId
: prependNewConversation(getConversationSystemPurposeId(focusedConversationId) ?? undefined);
setFocusedConversationId(conversationId);
// create conversation (or recycle the existing top-of-stack empty conversation)
const conversationId = (recycleNewConversationId && !forceNoRecycle)
? recycleNewConversationId
: prependNewConversation(getConversationSystemPurposeId(focusedPaneConversationId) ?? undefined);
// switch the focused pane to the new conversation
handleOpenConversationInFocusedPane(conversationId);
// if a folder is active, add the new conversation to the folder
if (activeFolderId && conversationId)
@@ -333,7 +308,7 @@ export function AppChat() {
// focus the composer
composerTextAreaRef.current?.focus();
}, [activeFolderId, focusedConversationId, newConversationId, prependNewConversation, setFocusedConversationId]);
}, [activeFolderId, focusedPaneConversationId, handleOpenConversationInFocusedPane, prependNewConversation, recycleNewConversationId]);
const handleConversationImportDialog = React.useCallback(() => setTradeConfig({ dir: 'import' }), []);
@@ -341,6 +316,32 @@ export function AppChat() {
setTradeConfig({ dir: 'export', conversationId, exportAll });
}, []);
const handleFileOpenConversation = React.useCallback(() => {
openAndLoadConversations(true)
.then((outcome) => {
// activate the last (most recent) imported conversation
if (outcome?.activateConversationId) {
showNextTitleChange.current = true;
handleOpenConversationInFocusedPane(outcome.activateConversationId);
}
})
.catch(() => {
addSnackbar({ key: 'chat-import-fail', message: 'Could not open the file.', type: 'issue' });
});
}, [handleOpenConversationInFocusedPane]);
const handleFileSaveConversation = React.useCallback((conversationId: DConversationId | null) => {
const conversation = getConversation(conversationId);
conversation && downloadConversation(conversation, 'json')
.then(() => {
addSnackbar({ key: 'chat-save-as-ok', message: 'File saved.', type: 'success' });
})
.catch((err: any) => {
if (err?.name !== 'AbortError')
addSnackbar({ key: 'chat-save-as-fail', message: `Could not save the file. ${err?.message || ''}`, type: 'issue' });
});
}, []);
const handleConversationBranch = React.useCallback((srcConversationId: DConversationId, messageId: string | null): DConversationId | null => {
// clone data
const branchedConversationId = branchConversation(srcConversationId, messageId);
@@ -351,22 +352,22 @@ export function AppChat() {
// replace/open a new pane with this
showNextTitleChange.current = true;
if (isMultiAddable)
openSplitConversationId(branchedConversationId);
if (!isMultiAddable)
handleOpenConversationInFocusedPane(branchedConversationId);
else
setFocusedConversationId(branchedConversationId);
handleOpenConversationInSplitPane(branchedConversationId);
return branchedConversationId;
}, [activeFolderId, branchConversation, isMultiAddable, openSplitConversationId, setFocusedConversationId]);
}, [activeFolderId, branchConversation, handleOpenConversationInFocusedPane, handleOpenConversationInSplitPane, isMultiAddable]);
const handleConversationFlatten = React.useCallback((conversationId: DConversationId) => setFlattenConversationId(conversationId), []);
const handleConfirmedClearConversation = React.useCallback(() => {
if (clearConversationId) {
setMessages(clearConversationId, []);
ConversationsManager.getHandler(clearConversationId).messagesReplace([]);
setClearConversationId(null);
}
}, [clearConversationId, setMessages]);
}, [clearConversationId]);
const handleConversationClear = React.useCallback((conversationId: DConversationId) => setClearConversationId(conversationId), []);
@@ -374,13 +375,14 @@ export function AppChat() {
if (!bypassConfirmation)
return setDeleteConversationIds(conversationIds);
// perform deletion
// perform deletion, and return the next (or a new) conversation
const nextConversationId = deleteConversations(conversationIds, /*focusedSystemPurposeId ??*/ undefined);
setFocusedConversationId(nextConversationId);
// switch the focused pane to the new conversation - NOTE: this makes the assumption that deletion had impact on the focused pane
handleOpenConversationInFocusedPane(nextConversationId);
setDeleteConversationIds(null);
}, [deleteConversations, setFocusedConversationId]);
}, [deleteConversations, handleOpenConversationInFocusedPane]);
const handleConfirmedDeleteConversations = React.useCallback(() => {
!!deleteConversationIds?.length && handleDeleteConversations(deleteConversationIds, true);
@@ -396,17 +398,22 @@ export function AppChat() {
}, [openLlmOptions]);
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
// focused conversation
['b', true, true, false, handleMessageBeamLastInFocusedPane],
['r', true, true, false, handleMessageRegenerateLastInFocusedPane],
['n', true, false, true, handleConversationNewInFocusedPane],
['o', true, false, false, handleFileOpenConversation],
['s', true, false, false, () => handleFileSaveConversation(focusedPaneConversationId)],
['b', true, false, true, () => isFocusedChatEmpty || (focusedPaneConversationId && handleConversationBranch(focusedPaneConversationId, null))],
['x', true, false, true, () => isFocusedChatEmpty || (focusedPaneConversationId && handleConversationClear(focusedPaneConversationId))],
['d', true, false, true, () => focusedPaneConversationId && handleDeleteConversations([focusedPaneConversationId], false)],
[ShortcutKeyName.Left, true, false, true, () => handleNavigateHistoryInFocusedPane('back')],
[ShortcutKeyName.Right, true, false, true, () => handleNavigateHistoryInFocusedPane('forward')],
// global
['o', true, true, false, handleOpenChatLlmOptions],
['r', true, true, false, handleMessageRegenerateLast],
['n', true, false, true, handleConversationNew],
['b', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationBranch(focusedConversationId, null))],
['x', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationClear(focusedConversationId))],
['d', true, false, true, () => focusedConversationId && handleDeleteConversations([focusedConversationId], false)],
['+', true, true, false, useUIPreferencesStore.getState().increaseContentScaling],
['-', true, true, false, useUIPreferencesStore.getState().decreaseContentScaling],
[ShortcutKeyName.Left, true, false, true, () => handleNavigateHistory('back')],
[ShortcutKeyName.Right, true, false, true, () => handleNavigateHistory('forward')],
], [focusedConversationId, handleConversationBranch, handleConversationClear, handleConversationNew, handleDeleteConversations, handleMessageRegenerateLast, handleNavigateHistory, handleOpenChatLlmOptions, isFocusedChatEmpty]);
], [focusedPaneConversationId, handleConversationBranch, handleConversationClear, handleConversationNewInFocusedPane, handleFileOpenConversation, handleFileSaveConversation, handleDeleteConversations, handleMessageBeamLastInFocusedPane, handleMessageRegenerateLastInFocusedPane, handleNavigateHistoryInFocusedPane, handleOpenChatLlmOptions, isFocusedChatEmpty]);
useGlobalShortcuts(shortcuts);
@@ -414,48 +421,50 @@ export function AppChat() {
const barAltTitle = showAltTitleBar ? focusedChatTitle ?? 'No Chat' : null;
const barContent = React.useMemo(() =>
(barAltTitle === null)
? <ChatDropdowns conversationId={focusedConversationId} />
: <ChatTitle conversationId={focusedConversationId} conversationTitle={barAltTitle} />
, [focusedConversationId, barAltTitle],
const focusedBarContent = React.useMemo(() => beamOpenStoreInFocusedPane
? <ChatBarAltBeam beamStore={beamOpenStoreInFocusedPane} isMobile={isMobile} />
: (barAltTitle === null)
? <ChatBarDropdowns conversationId={focusedPaneConversationId} />
: <ChatBarAltTitle conversationId={focusedPaneConversationId} conversationTitle={barAltTitle} />
, [barAltTitle, beamOpenStoreInFocusedPane, focusedPaneConversationId, isMobile],
);
const drawerContent = React.useMemo(() =>
<ChatDrawerMemo
isMobile={isMobile}
activeConversationId={focusedConversationId}
activeConversationId={focusedPaneConversationId}
activeFolderId={activeFolderId}
chatPanesConversationIds={chatPanes.map(pane => pane.conversationId).filter(Boolean) as DConversationId[]}
disableNewButton={disableNewButton}
onConversationActivate={setFocusedConversationId}
onConversationActivate={handleOpenConversationInFocusedPane}
onConversationBranch={handleConversationBranch}
onConversationNew={handleConversationNew}
onConversationNew={handleConversationNewInFocusedPane}
onConversationsDelete={handleDeleteConversations}
onConversationsExportDialog={handleConversationExport}
onConversationsImportDialog={handleConversationImportDialog}
setActiveFolderId={setActiveFolderId}
/>,
[activeFolderId, chatPanes, disableNewButton, focusedConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNew, handleDeleteConversations, isMobile, setFocusedConversationId],
[activeFolderId, chatPanes, disableNewButton, focusedPaneConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNewInFocusedPane, handleDeleteConversations, handleOpenConversationInFocusedPane, isMobile],
);
const menuItems = React.useMemo(() =>
const focusedMenuItems = React.useMemo(() =>
<ChatPageMenuItems
isMobile={isMobile}
conversationId={focusedConversationId}
disableItems={!focusedConversationId || isFocusedChatEmpty}
hasConversations={!areChatsEmpty}
conversationId={focusedPaneConversationId}
disableItems={!focusedPaneConversationId || isFocusedChatEmpty}
hasConversations={hasConversations}
isMessageSelectionMode={isMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationClear={handleConversationClear}
onConversationFlatten={handleConversationFlatten}
// onConversationNew={handleConversationNew}
// onConversationNew={handleConversationNewInFocusedPane}
setIsMessageSelectionMode={setIsMessageSelectionMode}
/>,
[areChatsEmpty, focusedConversationId, handleConversationBranch, handleConversationClear, handleConversationFlatten, /*handleConversationNew,*/ isFocusedChatEmpty, isMessageSelectionMode, isMobile],
[focusedPaneConversationId, handleConversationBranch, handleConversationClear, handleConversationFlatten, hasConversations, isFocusedChatEmpty, isMessageSelectionMode, isMobile],
);
usePluggableOptimaLayout(drawerContent, barContent, menuItems, 'AppChat');
usePluggableOptimaLayout(drawerContent, focusedBarContent, focusedMenuItems, 'AppChat');
return <>
@@ -465,11 +474,14 @@ export function AppChat() {
>
{chatPanes.map((pane, idx) => {
const _paneIsFocused = idx === focusedPaneIndex;
const _paneConversationId = pane.conversationId;
const _paneChatHandler = chatHandlers[idx] ?? null;
const _paneBeamStore = beamsStores[idx] ?? null;
const _paneBeamIsOpen = !!beamsOpens?.[idx] && !!_paneBeamStore;
const _panesCount = chatPanes.length;
const _keyAndId = `chat-pane-${idx}-${_paneConversationId}`;
const _sepId = `sep-pane-${idx}-${_paneConversationId}`;
const _keyAndId = `chat-pane-${pane.paneId}`;
const _sepId = `sep-pane-${idx}`;
return <React.Fragment key={_keyAndId}>
<Panel
@@ -480,7 +492,7 @@ export function AppChat() {
minSize={20}
onClick={(event) => {
const setFocus = chatPanes.length < 2 || !event.altKey;
setFocusedPane(setFocus ? idx : -1);
setFocusedPaneIndex(setFocus ? idx : -1);
}}
onCollapse={() => {
// NOTE: despite the delay to try to let the draggin settle, there seems to be an issue with the Pane locking the screen
@@ -493,12 +505,13 @@ export function AppChat() {
position: 'relative',
...(isMultiPane ? {
borderRadius: '0.375rem',
border: `2px solid ${idx === focusedPaneIndex
border: `2px solid ${_paneIsFocused
? ((willMulticast || !isMultiConversationId) ? theme.palette.primary.solidBg : theme.palette.primary.solidBg)
: ((willMulticast || !isMultiConversationId) ? theme.palette.warning.softActiveBg : theme.palette.background.level1)}`,
filter: (!willMulticast && idx !== focusedPaneIndex)
? (!isMultiConversationId ? 'grayscale(66.67%)' /* clone of the same */ : 'grayscale(66.67%)')
: undefined,
: ((willMulticast || !isMultiConversationId) ? theme.palette.primary.softActiveBg : theme.palette.background.level1)}`,
// DISABLED on 2024-03-13, it gets in the way quite a lot
// filter: (!willMulticast && !_paneIsFocused)
// ? (!isMultiConversationId ? 'grayscale(66.67%)' /* clone of the same */ : 'grayscale(66.67%)')
// : undefined,
} : {
// NOTE: this is a workaround for the 'stuck-after-collapse-close' issue. We will collapse the 'other' pane, which
// will get it removed (onCollapse), and somehow this pane will be stuck with a pointerEvents: 'none' style, which de-facto
@@ -512,62 +525,46 @@ export function AppChat() {
<ScrollToBottom
bootToBottom
stickToBottom
sx={{
// allows the content to be scrolled (all browsers)
overflowY: 'auto',
// actually make sure this scrolls & fills
height: '100%',
}}
stickToBottomInitial
sx={{ display: 'flex', flexDirection: 'column' }}
>
<ChatMessageList
conversationId={_paneConversationId}
conversationHandler={_paneChatHandler}
capabilityHasT2I={capabilityHasT2I}
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
fitScreen={isMobile || isMultiPane}
isMessageSelectionMode={isMessageSelectionMode}
setIsMessageSelectionMode={setIsMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationExecuteHistory={handleConversationExecuteHistory}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImagine}
onTextSpeak={handleTextSpeak}
sx={{
minHeight: '100%', // ensures filling of the blank space on newer chats
}}
/>
{!_paneBeamIsOpen && (
<ChatMessageList
conversationId={_paneConversationId}
conversationHandler={_paneChatHandler}
capabilityHasT2I={capabilityHasT2I}
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
fitScreen={isMobile || isMultiPane}
isMessageSelectionMode={isMessageSelectionMode}
setIsMessageSelectionMode={setIsMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationExecuteHistory={handleConversationExecuteHistory}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImagine}
onTextSpeak={handleTextSpeak}
sx={{
flexGrow: 1,
}}
/>
)}
{/*<Ephemerals*/}
{/* conversationId={_paneConversationId}*/}
{/* sx={{*/}
{/* // TODO: Fixme post panels?*/}
{/* // flexGrow: 0.1,*/}
{/* flexShrink: 0.5,*/}
{/* overflowY: 'auto',*/}
{/* minHeight: 64,*/}
{/* }}*/}
{/*/>*/}
{_paneBeamIsOpen && (
<ChatBeamWrapper
beamStore={_paneBeamStore}
isMobile={isMobile}
inlineSx={{
flexGrow: 1,
// minHeight: 'calc(100vh - 69px - var(--AGI-Nav-width))',
}}
/>
)}
{/* Visibility and actions are handled via Context */}
<ScrollToBottomButton />
</ScrollToBottom>
{/* Best-Of Mode */}
<Beam
conversationHandler={_paneChatHandler}
isMobile={isMobile}
sx={{
overflowY: 'auto',
backgroundColor: 'background.level2',
position: 'absolute',
inset: 0,
zIndex: 1, // stay on top of Chips :shrug:
}}
/>
</Panel>
{/* Panel Separators & Resizers */}
@@ -586,20 +583,14 @@ export function AppChat() {
isMobile={isMobile}
chatLLM={chatLLM}
composerTextAreaRef={composerTextAreaRef}
conversationId={focusedConversationId}
conversationId={focusedPaneConversationId}
capabilityHasT2I={capabilityHasT2I}
isMulticast={!isMultiConversationId ? null : isComposerMulticast}
isDeveloperMode={isFocusedChatDeveloper}
onAction={handleComposerAction}
onTextImagine={handleTextImagine}
setIsMulticast={setIsComposerMulticast}
sx={{
zIndex: 21, // position: 'sticky', bottom: 0,
backgroundColor: themeBgAppChatComposer,
borderTop: `1px solid`,
borderTopColor: 'divider',
p: { xs: 1, md: 2 },
}}
sx={beamOpenStoreInFocusedPane ? composerClosedSx : composerOpenSx}
/>
{/* Diagrams */}
@@ -618,7 +609,7 @@ export function AppChat() {
{!!tradeConfig && (
<TradeModal
config={tradeConfig}
onConversationActivate={setFocusedConversationId}
onConversationActivate={handleOpenConversationInFocusedPane}
onClose={() => setTradeConfig(null)}
/>
)}
+4 -5
View File
@@ -1,17 +1,16 @@
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
import { getUXLabsChatBeam } from '~/common/state/store-ux-labs';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsBeam: ICommandsProvider = {
id: 'ass-beam',
id: 'mode-beam',
rank: 9,
getCommands: () => getUXLabsChatBeam() ? [{
getCommands: () => [{
primary: '/beam',
arguments: ['prompt'],
description: 'Best of multiple replies',
description: 'Combine the smarts of models',
Icon: ChatBeamIcon,
}] : [],
}],
};
+2 -2
View File
@@ -1,4 +1,4 @@
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
import type { ICommandsProvider } from './ICommandsProvider';
@@ -11,7 +11,7 @@ export const CommandsDraw: ICommandsProvider = {
alternatives: ['/imagine', '/img'],
arguments: ['prompt'],
description: 'Assistant will draw the text',
Icon: FormatPaintIcon,
Icon: FormatPaintTwoToneIcon,
}],
};
+27 -13
View File
@@ -8,7 +8,7 @@ import { CommandsHelp } from './CommandsHelp';
import { CommandsReact } from './CommandsReact';
export type CommandsProviderId = 'ass-beam' | 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help';
export type CommandsProviderId = 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help' | 'mode-beam';
type TextCommandPiece =
| { type: 'text'; value: string; }
@@ -16,12 +16,12 @@ type TextCommandPiece =
const ChatCommandsProviders: Record<CommandsProviderId, ICommandsProvider> = {
'ass-beam': CommandsBeam,
'ass-browse': CommandsBrowse,
'ass-react': CommandsReact,
'ass-t2i': CommandsDraw,
'chat-alter': CommandsAlter,
'cmd-help': CommandsHelp,
'mode-beam': CommandsBeam,
};
export function findAllChatCommands(): ChatCommand[] {
@@ -40,7 +40,10 @@ export function extractChatCommand(input: string): TextCommandPiece[] {
// Find the first space to separate the command from its parameters (if any)
const firstSpaceIndex = inputTrimmed.indexOf(' ');
const potentialCommand = inputTrimmed.substring(0, firstSpaceIndex >= 0 ? firstSpaceIndex : inputTrimmed.length);
const commandMatch = inputTrimmed.match(/^\/\S+/);
const potentialCommand = commandMatch ? commandMatch[0] : inputTrimmed;
const textAfterCommand = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
// Check if the potential command is an actual command
for (const provider of Object.values(ChatCommandsProviders)) {
@@ -48,22 +51,33 @@ export function extractChatCommand(input: string): TextCommandPiece[] {
if (cmd.primary === potentialCommand || cmd.alternatives?.includes(potentialCommand)) {
// command needs arguments: take the rest of the input as parameters
if (cmd.arguments?.length) {
const params = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
return [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: params || undefined, isError: !params || undefined }];
}
if (cmd.arguments?.length) return [{
type: 'cmd',
providerId: provider.id,
command: potentialCommand,
params: textAfterCommand || undefined,
isError: !textAfterCommand || undefined,
}];
// command without arguments, treat any text after as a separate text piece
const pieces: TextCommandPiece[] = [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: undefined }];
const textAfterCommand = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
if (textAfterCommand)
pieces.push({ type: 'text', value: textAfterCommand });
const pieces: TextCommandPiece[] = [{
type: 'cmd',
providerId: provider.id,
command: potentialCommand,
params: undefined,
}];
textAfterCommand && pieces.push({
type: 'text',
value: textAfterCommand,
});
return pieces;
}
}
}
// No command found, return the entire input as text
return [{ type: 'text', value: input }];
return [{
type: 'text',
value: input,
}];
}
+117
View File
@@ -0,0 +1,117 @@
import * as React from 'react';
import { useShallow } from 'zustand/react/shallow';
import { Box, IconButton, Typography } from '@mui/joy';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import FullscreenRoundedIcon from '@mui/icons-material/FullscreenRounded';
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { KeyStroke } from '~/common/components/KeyStroke';
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
import { animationBackgroundBeamGather, animationColorBeamScatterINV, animationEnterBelow } from '~/common/util/animUtils';
export function ChatBarAltBeam(props: {
beamStore: BeamStoreApi,
isMobile?: boolean
}) {
// state
const [showCloseConfirmation, setShowCloseConfirmation] = React.useState(false);
// external beam state
const { isScattering, isGatheringAny, requiresConfirmation, setIsMaximized, terminateBeam } = useBeamStore(props.beamStore, useShallow((store) => ({
// state
isScattering: store.isScattering,
isGatheringAny: store.isGatheringAny,
requiresConfirmation: store.isScattering || store.isGatheringAny || store.raysReady > 0,
// actions
setIsMaximized: store.setIsMaximized,
terminateBeam: store.terminateKeepingSettings,
})));
// closure handlers
const handleCloseBeam = React.useCallback(() => {
if (requiresConfirmation)
setShowCloseConfirmation(true);
else
terminateBeam();
}, [requiresConfirmation, terminateBeam]);
const handleCloseConfirmation = React.useCallback(() => {
terminateBeam();
setShowCloseConfirmation(false);
}, [terminateBeam]);
const handleCloseDenial = React.useCallback(() => {
setShowCloseConfirmation(false);
}, []);
const handleMaximizeBeam = React.useCallback(() => {
setIsMaximized(true);
}, [setIsMaximized]);
// intercept esc this beam is focused
useGlobalShortcut(ShortcutKeyName.Esc, false, false, false, handleCloseBeam);
return (
<Box sx={{ display: 'flex', gap: { xs: 1, md: 2 }, alignItems: 'center' }}>
{/* Title & Status */}
<Typography level='title-md'>
<Box
component='span'
sx={
isGatheringAny ? { animation: `${animationBackgroundBeamGather} 3s infinite, ${animationEnterBelow} 0.6s`, px: 1.5, py: 0.5 }
: isScattering ? { animation: `${animationColorBeamScatterINV} 5s infinite, ${animationEnterBelow} 0.6s` }
: { fontWeight: 'lg' }
}>
{isGatheringAny ? 'Merging...' : isScattering ? 'Beaming...' : 'Beam'}
</Box>
{(!isGatheringAny && !isScattering) && ' Mode'}
</Typography>
{/* Right Close Icon */}
<Box sx={{ display: 'flex' }}>
{/* [desktop] maximize button, or a disabled spacer */}
{!props.isMobile && (
<GoodTooltip usePlain title={<Box sx={{ p: 1 }}>Maximize</Box>}>
<IconButton size='sm' onClick={handleMaximizeBeam}>
<FullscreenRoundedIcon />
</IconButton>
</GoodTooltip>
)}
<GoodTooltip usePlain title={<Box sx={{ p: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>Back to Chat <KeyStroke combo='Esc' /></Box>}>
<IconButton aria-label='Close' size='sm' onClick={handleCloseBeam}>
<CloseRoundedIcon />
</IconButton>
</GoodTooltip>
</Box>
{/* Confirmation Modal */}
{showCloseConfirmation && (
<ConfirmationModal
open
onClose={handleCloseDenial}
onPositive={handleCloseConfirmation}
lowStakes
noTitleBar
confirmationText='Are you sure you want to close Beam Mode? Unsaved text will be lost.'
positiveActionText='Yes, close'
/>
)}
</Box>
);
}
@@ -13,7 +13,7 @@ import { CHAT_NOVEL_TITLE } from '../AppChat';
import { FadeInButton } from './ChatDrawerItem';
export function ChatTitle(props: {
export function ChatBarAltTitle(props: {
conversationId: DConversationId | null,
conversationTitle: string,
}) {
@@ -7,7 +7,7 @@ import { usePersonaIdDropdown } from './usePersonaDropdown';
import { useFolderDropdown } from './folders/useFolderDropdown';
export function ChatDropdowns(props: {
export function ChatBarDropdowns(props: {
conversationId: DConversationId | null
}) {
@@ -0,0 +1,59 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Modal, ModalClose } from '@mui/joy';
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
import { BeamView } from '~/modules/beam/BeamView';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
/*const overlaySx: SxProps = {
position: 'absolute',
inset: 0,
zIndex: themeZIndexBeamView, // stay on top of Message > Chips (:1), and Overlays (:2) - note: Desktop Drawer (:26)
}*/
export function ChatBeamWrapper(props: {
beamStore: BeamStoreApi,
isMobile: boolean,
inlineSx?: SxProps,
}) {
// state
const isMaximized = useBeamStore(props.beamStore, state => state.isMaximized);
const handleUnMaximize = React.useCallback(() => {
props.beamStore.getState().setIsMaximized(false);
}, [props.beamStore]);
// memo the beamview
const beamView = React.useMemo(() => (
<BeamView
beamStore={props.beamStore}
isMobile={props.isMobile}
showExplainer
/>
), [props.beamStore, props.isMobile]);
return isMaximized ? (
<Modal open onClose={handleUnMaximize}>
<Box sx={{
backgroundColor: 'background.level1',
position: 'absolute',
inset: 0,
}}>
<ScrollToBottom disableAutoStick>
{beamView}
</ScrollToBottom>
<ModalClose sx={{ color: 'white', backgroundColor: 'background.surface', boxShadow: 'xs', mr: 2 }} />
</Box>
</Modal>
) : (
<Box sx={props.inlineSx}>
{beamView}
</Box>
);
}
+134 -87
View File
@@ -1,15 +1,16 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';
import { Box, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
import { Box, Button, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
import AddIcon from '@mui/icons-material/Add';
import CheckIcon from '@mui/icons-material/Check';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import ClearIcon from '@mui/icons-material/Clear';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
import FolderIcon from '@mui/icons-material/Folder';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
import type { DConversationId } from '~/common/state/store-chats';
import { CloseableMenu } from '~/common/components/CloseableMenu';
@@ -26,9 +27,9 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
import { ChatDrawerItemMemo, FolderChangeRequest } from './ChatDrawerItem';
import { ChatFolderList } from './folders/ChatFolderList';
import { ChatNavGrouping, useChatNavRenderItems } from './useChatNavRenderItems';
import { ChatNavGrouping, ChatSearchSorting, isDrawerSearching, useChatDrawerRenderItems } from './useChatDrawerRenderItems';
import { ClearFolderText } from './folders/useFolderDropdown';
import { useChatShowRelativeSize } from '../store-app-chat';
import { useChatDrawerFilters } from '../store-app-chat';
// this is here to make shallow comparisons work on the next hook
@@ -37,7 +38,7 @@ const noFolders: DFolder[] = [];
/*
* Lists folders and returns the active folder
*/
export const useFolders = (activeFolderId: string | null) => useFolderStore(({ enableFolders, folders, toggleEnableFolders }) => {
export const useFolders = (activeFolderId: string | null) => useFolderStore(useShallow(({ enableFolders, folders, toggleEnableFolders }) => {
// finds the active folder if any
const activeFolder = (enableFolders && activeFolderId)
@@ -50,7 +51,7 @@ export const useFolders = (activeFolderId: string | null) => useFolderStore(({ e
enableFolders,
toggleEnableFolders,
};
}, shallow);
}));
export const ChatDrawerMemo = React.memo(ChatDrawer);
@@ -74,20 +75,25 @@ function ChatDrawer(props: {
// local state
const [navGrouping, setNavGrouping] = React.useState<ChatNavGrouping>('date');
const [searchSorting, setSearchSorting] = React.useState<ChatSearchSorting>('frequency');
const [debouncedSearchQuery, setDebouncedSearchQuery] = React.useState('');
const [folderChangeRequest, setFolderChangeRequest] = React.useState<FolderChangeRequest | null>(null);
// external state
const { closeDrawer, closeDrawerOnMobile } = useOptimaDrawers();
const { showRelativeSize, toggleRelativeSize } = useChatShowRelativeSize();
const {
filterHasStars, toggleFilterHasStars,
showPersonaIcons, toggleShowPersonaIcons,
showRelativeSize, toggleShowRelativeSize,
} = useChatDrawerFilters();
const { activeFolder, allFolders, enableFolders, toggleEnableFolders } = useFolders(props.activeFolderId);
const { filteredChatsCount, filteredChatIDs, filteredChatsAreEmpty, filteredChatsBarBasis, filteredChatsIncludeActive, renderNavItems } = useChatNavRenderItems(
props.activeConversationId, props.chatPanesConversationIds, debouncedSearchQuery, activeFolder, allFolders, navGrouping, showRelativeSize,
const { filteredChatsCount, filteredChatIDs, filteredChatsAreEmpty, filteredChatsBarBasis, filteredChatsIncludeActive, renderNavItems } = useChatDrawerRenderItems(
props.activeConversationId, props.chatPanesConversationIds, debouncedSearchQuery, activeFolder, allFolders, filterHasStars, navGrouping, searchSorting, showRelativeSize,
);
const { contentScaling, showSymbols } = useUIPreferencesStore(state => ({
const { contentScaling, showSymbols } = useUIPreferencesStore(useShallow(state => ({
contentScaling: state.contentScaling,
showSymbols: state.zenMode !== 'cleaner',
}), shallow);
})));
// New/Activate/Delete Conversation
@@ -140,6 +146,7 @@ function ChatDrawer(props: {
// memoize the group dropdown
const { isSearching } = isDrawerSearching(debouncedSearchQuery);
const groupingComponent = React.useMemo(() => (
<Dropdown>
<MenuButton
@@ -147,34 +154,67 @@ function ChatDrawer(props: {
slots={{ root: IconButton }}
slotProps={{ root: { size: 'sm' } }}
>
<MoreVertIcon sx={{ fontSize: 'xl' }} />
<MoreVertIcon />
</MenuButton>
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
<ListItem>
<Typography level='body-sm'>Group By</Typography>
</ListItem>
{(['date', 'persona'] as const).map(_gName => (
<MenuItem
key={'group-' + _gName}
aria-label={`Group by ${_gName}`}
selected={navGrouping === _gName}
onClick={() => setNavGrouping(grouping => grouping === _gName ? false : _gName)}
>
<ListItemDecorator>{navGrouping === _gName && <CheckIcon />}</ListItemDecorator>
{capitalizeFirstLetter(_gName)}
{!isSearching ? (
// Search/Filter default menu: Grouping, Filtering, ...
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
<ListItem>
<Typography level='body-sm'>Group By</Typography>
</ListItem>
{(['date', 'persona'] as const).map(_gName => (
<MenuItem
key={'group-' + _gName}
aria-label={`Group by ${_gName}`}
selected={navGrouping === _gName}
onClick={() => setNavGrouping(grouping => grouping === _gName ? false : _gName)}
>
<ListItemDecorator>{navGrouping === _gName && <CheckRoundedIcon />}</ListItemDecorator>
{capitalizeFirstLetter(_gName)}
</MenuItem>
))}
<ListDivider />
<ListItem>
<Typography level='body-sm'>Filter</Typography>
</ListItem>
<MenuItem onClick={toggleFilterHasStars}>
<ListItemDecorator>{filterHasStars && <CheckRoundedIcon />}</ListItemDecorator>
Starred <StarOutlineRoundedIcon />
</MenuItem>
))}
<ListDivider />
<ListItem>
<Typography level='body-sm'>Show</Typography>
</ListItem>
<MenuItem onClick={toggleRelativeSize}>
<ListItemDecorator>{showRelativeSize && <CheckIcon />}</ListItemDecorator>
Relative Size
</MenuItem>
</Menu>
<ListDivider />
<ListItem>
<Typography level='body-sm'>Show</Typography>
</ListItem>
<MenuItem onClick={toggleShowPersonaIcons}>
<ListItemDecorator>{showPersonaIcons && <CheckRoundedIcon />}</ListItemDecorator>
Icons
</MenuItem>
<MenuItem onClick={toggleShowRelativeSize}>
<ListItemDecorator>{showRelativeSize && <CheckRoundedIcon />}</ListItemDecorator>
Relative Size
</MenuItem>
</Menu>
) : (
// While searching, show the sorting options
<Menu placement='bottom-start' sx={{ minWidth: 180, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
<ListItem>
<Typography level='body-sm'>Sort By</Typography>
</ListItem>
<MenuItem selected={searchSorting === 'frequency'} onClick={() => setSearchSorting('frequency')}>
<ListItemDecorator>{searchSorting === 'frequency' && <CheckRoundedIcon />}</ListItemDecorator>
Matches
</MenuItem>
<MenuItem selected={searchSorting === 'date'} onClick={() => setSearchSorting('date')}>
<ListItemDecorator>{searchSorting === 'date' && <CheckRoundedIcon />}</ListItemDecorator>
Date
</MenuItem>
</Menu>
)}
</Dropdown>
), [navGrouping, showRelativeSize, toggleRelativeSize]);
), [filterHasStars, isSearching, navGrouping, searchSorting, showPersonaIcons, showRelativeSize, toggleFilterHasStars, toggleShowPersonaIcons, toggleShowRelativeSize]);
return <>
@@ -182,13 +222,13 @@ function ChatDrawer(props: {
{/* Drawer Header */}
<PageDrawerHeader title='Chats' onClose={closeDrawer}>
<Tooltip title={enableFolders ? 'Hide Folders' : 'Use Folders'}>
<IconButton onClick={toggleEnableFolders}>
<IconButton size='sm' onClick={toggleEnableFolders}>
{enableFolders ? <FoldersToggleOn /> : <FoldersToggleOff />}
</IconButton>
</Tooltip>
</PageDrawerHeader>
{/* Folders List */}
{/* Folders List (shrink at twice the rate as the Titles) */}
{/*<Box sx={{*/}
{/* display: 'grid',*/}
{/* gridTemplateRows: !enableFolders ? '0fr' : '1fr',*/}
@@ -205,6 +245,12 @@ function ChatDrawer(props: {
contentScaling={contentScaling}
activeFolderId={props.activeFolderId}
onFolderSelect={props.setActiveFolderId}
sx={{
// shrink this at twice the rate as the Titles list
flexGrow: 0, flexShrink: 2, overflow: 'hidden',
minHeight: '7.5rem',
p: 2,
}}
/>
)}
{/*</Box>*/}
@@ -214,81 +260,81 @@ function ChatDrawer(props: {
{enableFolders && <ListDivider sx={{ mb: 0 }} />}
{/* Search Input Field */}
<DebounceInputMemo
minChars={2}
onDebounce={setDebouncedSearchQuery}
debounceTimeout={300}
placeholder='Search...'
aria-label='Search'
endDecorator={groupingComponent}
sx={{ m: 2 }}
/>
{/* Search / New Chat */}
<Box sx={{ display: 'flex', flexDirection: 'column', m: 2, gap: 2 }}>
{/* New Chat Button */}
<ListItem sx={{ mx: '0.25rem', mb: 0.5 }}>
<ListItemButton
{/* Search Input Field */}
<DebounceInputMemo
minChars={2}
onDebounce={setDebouncedSearchQuery}
debounceTimeout={300}
placeholder='Search...'
aria-label='Search'
endDecorator={groupingComponent}
/>
{/* New Chat Button */}
<Button
// variant='outlined'
variant={disableNewButton ? undefined : 'outlined'}
variant={disableNewButton ? undefined : 'soft'}
disabled={disableNewButton}
onClick={handleButtonNew}
sx={{
// ...PageDrawerTallItemSx,
px: 'calc(var(--ListItem-paddingX) - 0.25rem)',
// text size
fontSize: 'sm',
fontWeight: 'lg',
justifyContent: 'flex-start',
padding: '0px 0.75rem',
// style
borderRadius: 'md',
boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'sm',
backgroundColor: 'background.popup',
transition: 'box-shadow 0.2s',
border: '1px solid',
borderColor: 'neutral.outlinedBorder',
borderRadius: 'sm',
'--ListItemDecorator-size': 'calc(2.5rem - 1px)', // compensate for the border
// backgroundColor: 'background.popup',
// boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'xs',
// transition: 'box-shadow 0.2s',
}}
>
<ListItemDecorator><AddIcon sx={{ '--Icon-fontSize': 'var(--joy-fontSize-xl)', pl: '0.125rem' }} /></ListItemDecorator>
<ListItemDecorator><AddIcon sx={{ fontSize: '' }} /></ListItemDecorator>
New chat
</ListItemButton>
</ListItem>
</Button>
{/*<ListDivider sx={{ mt: 0 }} />*/}
{/* List of Chat Titles (and actions) */}
<Box sx={{ flex: 1, overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
{/*<ListItem sticky sx={{ justifyContent: 'space-between', boxShadow: 'sm' }}>*/}
{/* <Typography level='body-sm'>*/}
{/* Conversations*/}
{/* </Typography>*/}
{/* <ToggleButtonGroup variant='soft' size='sm' value={grouping} onChange={(_event, newValue) => newValue && setGrouping(newValue)}>*/}
{/* <IconButton value='off'>*/}
{/* <AccessTimeIcon />*/}
{/* </IconButton>*/}
{/* <IconButton value='persona'>*/}
{/* <PersonIcon />*/}
{/* </IconButton>*/}
{/* </ToggleButtonGroup>*/}
{/*</ListItem>*/}
</Box>
{/* Chat Titles List (shrink as half the rate as the Folders List) */}
<Box sx={{ flexGrow: 1, flexShrink: 1, flexBasis: '20rem', overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
{renderNavItems.map((item, idx) => item.type === 'nav-item-chat-data' ? (
<ChatDrawerItemMemo
key={'nav-chat-' + item.conversationId}
item={item}
showSymbols={showSymbols}
showSymbols={showPersonaIcons && showSymbols}
bottomBarBasis={filteredChatsBarBasis}
onConversationActivate={handleConversationActivate}
onConversationBranch={onConversationBranch}
onConversationDelete={handleConversationDeleteNoConfirmation}
onConversationDeleteNoConfirmation={handleConversationDeleteNoConfirmation}
onConversationExport={onConversationsExportDialog}
onConversationFolderChange={handleConversationFolderChange}
/>
) : item.type === 'nav-item-group' ? (
<Typography key={'nav-divider-' + idx} level='body-xs' sx={{ textAlign: 'center', my: 'calc(var(--ListItem-minHeight) / 4)' }}>
<Typography key={'nav-divider-' + idx} level='body-xs' sx={{
textAlign: 'center',
my: 'calc(var(--ListItem-minHeight) / 4)',
// keeps the group header sticky to the top
position: 'sticky',
top: 0,
backgroundColor: 'background.popup',
zIndex: 1,
}}>
{item.title}
</Typography>
) : item.type === 'nav-item-info-message' ? (
<Typography key={'nav-info-' + idx} level='body-xs' sx={{ textAlign: 'center', my: 'calc(var(--ListItem-minHeight) / 2)' }}>
<Typography key={'nav-info-' + idx} level='body-xs' sx={{ textAlign: 'center', color: 'primary.softColor', my: 'calc(var(--ListItem-minHeight) / 4)' }}>
{filterHasStars && <StarOutlineRoundedIcon sx={{ color: 'primary.softColor', fontSize: 'xl', mb: -0.5, mr: 1 }} />}
{item.message}
{filterHasStars && <>
<Button variant='soft' size='sm' onClick={toggleFilterHasStars} sx={{ display: 'block', mt: 2, mx: 'auto' }}>
remove filters
</Button>
</>}
</Typography>
) : null,
)}
@@ -296,7 +342,8 @@ function ChatDrawer(props: {
<ListDivider sx={{ my: 0 }} />
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/* Bottom commands */}
<Box sx={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
<ListItemButton onClick={props.onConversationsImportDialog} sx={{ flex: 1 }}>
<ListItemDecorator>
<FileUploadOutlinedIcon />
+33 -19
View File
@@ -5,7 +5,7 @@ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import EditIcon from '@mui/icons-material/Edit';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import FolderIcon from '@mui/icons-material/Folder';
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
@@ -21,6 +21,7 @@ import { InlineTextarea } from '~/common/components/InlineTextarea';
import { isDeepEqual } from '~/common/util/jsUtils';
import { CHAT_NOVEL_TITLE } from '../AppChat';
import { STREAM_TEXT_INDICATOR } from '../editors/chat-stream';
// set to true to display the conversation IDs
@@ -41,7 +42,7 @@ export const ChatDrawerItemMemo = React.memo(ChatDrawerItem, (prev, next) =>
prev.bottomBarBasis === next.bottomBarBasis &&
prev.onConversationActivate === next.onConversationActivate &&
prev.onConversationBranch === next.onConversationBranch &&
prev.onConversationDelete === next.onConversationDelete &&
prev.onConversationDeleteNoConfirmation === next.onConversationDeleteNoConfirmation &&
prev.onConversationExport === next.onConversationExport &&
prev.onConversationFolderChange === next.onConversationFolderChange,
);
@@ -53,6 +54,7 @@ export interface ChatNavigationItemData {
isAlsoOpen: string | false;
isEmpty: boolean;
title: string;
userFlagsSummary: string | undefined;
folder: DFolder | null | undefined; // null: 'All', undefined: do not show folder select
updatedAt: number;
messageCount: number;
@@ -74,7 +76,7 @@ function ChatDrawerItem(props: {
bottomBarBasis: number,
onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void,
onConversationBranch: (conversationId: DConversationId, messageId: string | null) => void,
onConversationDelete: (conversationId: DConversationId) => void,
onConversationDeleteNoConfirmation: (conversationId: DConversationId) => void,
onConversationExport: (conversationId: DConversationId, exportAll: boolean) => void,
onConversationFolderChange: (folderChangeRequest: FolderChangeRequest) => void,
}) {
@@ -86,7 +88,7 @@ function ChatDrawerItem(props: {
// derived state
const { onConversationBranch, onConversationExport, onConversationFolderChange } = props;
const { conversationId, isActive, isAlsoOpen, title, folder, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
const { conversationId, isActive, isAlsoOpen, title, userFlagsSummary, folder, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
const isNew = messageCount === 0;
@@ -153,7 +155,16 @@ function ChatDrawerItem(props: {
// Delete
const handleDeleteButtonShow = React.useCallback(() => setDeleteArmed(true), []);
const { onConversationDeleteNoConfirmation } = props;
const handleDeleteButtonShow = React.useCallback((event: React.MouseEvent) => {
// special case: if 'Shift' is pressed, delete immediately
if (event.shiftKey) {
event.stopPropagation();
onConversationDeleteNoConfirmation(conversationId);
return;
}
setDeleteArmed(true);
}, [conversationId, onConversationDeleteNoConfirmation]);
const handleDeleteButtonHide = React.useCallback(() => setDeleteArmed(false), []);
@@ -161,9 +172,9 @@ function ChatDrawerItem(props: {
if (deleteArmed) {
setDeleteArmed(false);
event.stopPropagation();
props.onConversationDelete(conversationId);
onConversationDeleteNoConfirmation(conversationId);
}
}, [conversationId, deleteArmed, props]);
}, [conversationId, deleteArmed, onConversationDeleteNoConfirmation]);
const textSymbol = SystemPurposes[systemPurposeId]?.symbol || '❓';
@@ -204,7 +215,7 @@ function ChatDrawerItem(props: {
}}
>
{/*{DEBUG_CONVERSATION_IDS && `${conversationId} - `}*/}
{title.trim() ? title : CHAT_NOVEL_TITLE}{assistantTyping && '...'}
{title.trim() ? title : CHAT_NOVEL_TITLE}{assistantTyping && STREAM_TEXT_INDICATOR}
</Box>
) : (
<InlineTextarea
@@ -219,16 +230,19 @@ function ChatDrawerItem(props: {
/>
)}
{/* Display search frequency if it exists and is greater than 0 */}
{searchFrequency > 0 && (
<Box sx={{ ml: 1 }}>
<Typography level='body-sm'>
{searchFrequency}
</Typography>
</Box>
)}
{/* Right text */}
{searchFrequency > 0 ? (
// Display search frequency if it exists and is greater than 0
<Typography level='body-sm'>
{searchFrequency}
</Typography>
) : (userFlagsSummary && props.showSymbols) ? (
<Typography sx={{ mr: '5px' }}>
{userFlagsSummary}
</Typography>
) : null}
</>, [assistantTyping, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title]);
</>, [assistantTyping, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title, userFlagsSummary]);
const progressBarFixedComponent = React.useMemo(() =>
progress > 0 && (
@@ -278,7 +292,7 @@ function ChatDrawerItem(props: {
{/* buttons row */}
{isActive && (
<Box sx={{ display: 'flex', gap: 0.5, minHeight: '2.25rem', alignItems: 'center' }}>
<ListItemDecorator />
{props.showSymbols && <ListItemDecorator />}
{/* Current Folder color, and change initiator */}
{!deleteArmed && <>
@@ -300,7 +314,7 @@ function ChatDrawerItem(props: {
<Tooltip disableInteractive title='Rename'>
<FadeInButton size='sm' disabled={isEditingTitle || isAutoEditingTitle} onClick={handleTitleEditBegin}>
<EditIcon />
<EditRoundedIcon />
</FadeInButton>
</Tooltip>
+65 -25
View File
@@ -1,8 +1,8 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, List } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
@@ -10,17 +10,17 @@ import type { ConversationHandler } from '~/common/chats/ConversationHandler';
import { InlineError } from '~/common/components/InlineError';
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats';
import { createDMessage, DConversationId, DMessage, DMessageUserFlag, getConversation, messageToggleUserFlag, useChatStore } from '~/common/state/store-chats';
import { useBrowserTranslationWarning } from '~/common/components/useIsBrowserTranslating';
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
import { useEphemerals } from '~/common/chats/EphemeralsStore';
import { useScrollToBottom } from '~/common/scroll-to-bottom/useScrollToBottom';
import { ChatMessage, ChatMessageMemo } from './message/ChatMessage';
import { CleanerMessage, MessagesSelectionHeader } from './message/CleanerMessage';
import { Ephemerals } from './Ephemerals';
import { PersonaSelector } from './persona-selector/PersonaSelector';
import { useChatShowSystemMessages } from '../store-app-chat';
import { useScrollToBottom } from './scroll-to-bottom/useScrollToBottom';
/**
@@ -34,7 +34,7 @@ export function ChatMessageList(props: {
fitScreen: boolean,
isMessageSelectionMode: boolean,
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[], chatEffectBeam: boolean) => Promise<void>,
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[]) => Promise<void>,
onTextDiagram: (diagramConfig: DiagramConfig | null) => void,
onTextImagine: (conversationId: DConversationId, selectedText: string) => Promise<void>,
onTextSpeak: (selectedText: string) => Promise<void>,
@@ -52,7 +52,7 @@ export function ChatMessageList(props: {
const { openPreferencesTab } = useOptimaLayout();
const [showSystemMessages] = useChatShowSystemMessages();
const optionalTranslationWarning = useBrowserTranslationWarning();
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(useShallow(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
return {
conversationMessages: conversation ? conversation.messages : [],
@@ -61,7 +61,7 @@ export function ChatMessageList(props: {
editMessage: state.editMessage,
setMessages: state.setMessages,
};
}, shallow);
}));
const ephemerals = useEphemerals(props.conversationHandler);
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
@@ -71,26 +71,50 @@ export function ChatMessageList(props: {
// text actions
const handleRunExample = React.useCallback(async (text: string) => {
conversationId && await onConversationExecuteHistory(conversationId, [...conversationMessages, createDMessage('user', text)], false);
const handleRunExample = React.useCallback(async (examplePrompt: string) => {
conversationId && await onConversationExecuteHistory(conversationId, [...conversationMessages, createDMessage('user', examplePrompt)]);
}, [conversationId, conversationMessages, onConversationExecuteHistory]);
// message menu methods proxy
const handleConversationBranch = React.useCallback((messageId: string) => {
conversationId && onConversationBranch(conversationId, messageId);
}, [conversationId, onConversationBranch]);
const handleConversationRestartFrom = React.useCallback(async (messageId: string, offset: number, chatEffectBeam: boolean) => {
const handleMessageAssistantFrom = React.useCallback(async (messageId: string, offset: number) => {
const messages = getConversation(conversationId)?.messages;
if (messages) {
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + offset + 1);
conversationId && await onConversationExecuteHistory(conversationId, truncatedHistory, chatEffectBeam);
conversationId && await onConversationExecuteHistory(conversationId, truncatedHistory);
}
}, [conversationId, onConversationExecuteHistory]);
const handleConversationTruncate = React.useCallback((messageId: string) => {
const handleMessageBeam = React.useCallback(async (messageId: string) => {
// Right-click menu Beam
if (!conversationId || !props.conversationHandler) return;
const messages = getConversation(conversationId)?.messages;
if (messages?.length) {
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + 1);
const lastMessage = truncatedHistory[truncatedHistory.length - 1];
if (lastMessage) {
// assistant: do an in-place beam
if (lastMessage.role === 'assistant') {
if (truncatedHistory.length >= 2)
props.conversationHandler.beamInvoke(truncatedHistory.slice(0, -1), [lastMessage], lastMessage.id);
} else {
// user: truncate and append (but if the next message is an assistant message, import it)
const nextMessage = messages[truncatedHistory.length];
if (nextMessage?.role === 'assistant')
props.conversationHandler.beamInvoke(truncatedHistory, [nextMessage], null);
else
props.conversationHandler.beamInvoke(truncatedHistory, [], null);
}
}
}
}, [conversationId, props.conversationHandler]);
const handleMessageBranch = React.useCallback((messageId: string) => {
conversationId && onConversationBranch(conversationId, messageId);
}, [conversationId, onConversationBranch]);
const handleMessageTruncate = React.useCallback((messageId: string) => {
const messages = getConversation(conversationId)?.messages;
if (conversationId && messages) {
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + 1);
@@ -106,6 +130,16 @@ export function ChatMessageList(props: {
conversationId && editMessage(conversationId, messageId, { text: newText }, true);
}, [conversationId, editMessage]);
const handleMessageToggleUserFlag = React.useCallback((messageId: string, userFlag: DMessageUserFlag) => {
conversationId && editMessage(conversationId, messageId, (message) => ({
userFlags: messageToggleUserFlag(message, userFlag),
}), false);
}, [conversationId, editMessage]);
const handleReplyTo = React.useCallback((_messageId: string, text: string) => {
props.conversationHandler?.getOverlayStore().getState().setReplyToText(text);
}, [props.conversationHandler]);
const handleTextDiagram = React.useCallback(async (messageId: string, text: string) => {
conversationId && onTextDiagram({ conversationId: conversationId, messageId, text });
}, [conversationId, onTextDiagram]);
@@ -195,12 +229,15 @@ export function ChatMessageList(props: {
return (
<List sx={{
p: 0, ...(props.sx || {}),
// this makes sure that the the window is scrolled to the bottom (column-reverse)
display: 'flex',
flexDirection: 'column',
p: 0,
...(props.sx || {}),
// fix for the double-border on the last message (one by the composer, one to the bottom of the message)
// marginBottom: '-1px',
// layout
display: 'flex',
flexDirection: 'column',
}}>
{optionalTranslationWarning}
@@ -239,14 +276,17 @@ export function ChatMessageList(props: {
isBottom={idx === count - 1}
isImagining={isImagining}
isSpeaking={isSpeaking}
onConversationBranch={handleConversationBranch}
onConversationRestartFrom={handleConversationRestartFrom}
onConversationTruncate={handleConversationTruncate}
onMessageAssistantFrom={handleMessageAssistantFrom}
onMessageBeam={handleMessageBeam}
onMessageBranch={handleMessageBranch}
onMessageDelete={handleMessageDelete}
onMessageEdit={handleMessageEdit}
onMessageToggleUserFlag={handleMessageToggleUserFlag}
onMessageTruncate={handleMessageTruncate}
// onReplyTo={handleReplyTo}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImagine}
onTextSpeak={handleTextSpeak}
onTextImagine={capabilityHasT2I ? handleTextImagine : undefined}
onTextSpeak={isSpeakable ? handleTextSpeak : undefined}
/>
);
+2 -2
View File
@@ -4,7 +4,7 @@ import { Box, Grid, IconButton, Sheet, styled, Typography } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import { ConversationManager } from '~/common/chats/ConversationHandler';
import { ConversationsManager } from '~/common/chats/ConversationsManager';
import { DConversationId } from '~/common/state/store-chats';
import { DEphemeral } from '~/common/chats/EphemeralsStore';
import { lineHeightChatTextMd } from '~/common/app.theme';
@@ -78,7 +78,7 @@ function StateRenderer(props: { state: object }) {
function EphemeralItem({ conversationId, ephemeral }: { conversationId: string, ephemeral: DEphemeral }) {
const handleDelete = React.useCallback(() => {
ConversationManager.getHandler(conversationId).ephemeralsStore.delete(ephemeral.id);
ConversationsManager.getHandler(conversationId).ephemeralsStore.delete(ephemeral.id);
}, [conversationId, ephemeral.id]);
return <Box
-137
View File
@@ -1,137 +0,0 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Alert, Box, Sheet, Typography } from '@mui/joy';
import { ConversationHandler } from '~/common/chats/ConversationHandler';
import { useBeam } from '~/common/chats/BeamStore';
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
export function Beam(props: {
conversationHandler: ConversationHandler | null,
isMobile: boolean,
sx?: SxProps
}) {
// state
const { config, candidates } = useBeam(props.conversationHandler);
// external state
const [allChatLlm, allChatLlmComponent] = useLLMSelect(true, 'Beam LLM');
if (!config)
return null;
const lastMessage = config.history.slice(-1)[0] ?? null;
return (
<Box sx={{ ...props.sx, px: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Issues */}
{!!config.configError && (
<Alert>
{config.configError}
</Alert>
)}
{/* Models, [x] all same, */}
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'start', gap: 2 }}>
<Box sx={{ minWidth: 200 }}>
{allChatLlmComponent}
</Box>
{!!lastMessage && (
<Box sx={{
backgroundColor: 'background.surface',
boxShadow: 'xs',
borderRadius: 'lg',
borderTopRightRadius: 0,
borderTopLeftRadius: 0,
py: 1,
px: 1,
mb: 'auto',
flex: 1,
}}>
{lastMessage.text}
</Box>
// <ChatMessageMemo
// message={lastMessage}
// fitScreen={props.isMobile}
// sx={{
// borderRadius: 'lg',
// borderBottomRightRadius: lastMessage.role === 'assistant' ? undefined : 0,
// borderBottomLeftRadius: lastMessage.role === 'user' ? undefined : 0,
// boxShadow: 'xs',
// my: 2,
// px: 0,
// py: 1,
// alignSelf: 'self-end',
// flex: 1,
// maxHeight: '5rem',
// overflow: 'hidden',
// }}
// />
)}
</Box>
{/* Grid */}
<Box sx={{
// my: 'auto',
// display: 'flex', flexDirection: 'column', alignItems: 'center',
border: '1px solid purple',
minHeight: '300px',
// layout
display: 'grid',
gridTemplateColumns: props.isMobile ? 'repeat(auto-fit, minmax(320px, 1fr))' : 'repeat(auto-fit, minmax(400px, 1fr))',
gap: { xs: 2, md: 2 },
}}>
<Sheet sx={{ minHeight: '50%' }}>
b
</Sheet>
<Sheet>
a
</Sheet>
<Sheet>
a
</Sheet>
<Sheet>
a
</Sheet>
</Box>
{/* Auto-Gatherer: All-in-one, Best-Of */}
<Box>
Gatherer
</Box>
<Box sx={{ flex: 1 }}>
<Typography level='body-sm' sx={{ whiteSpace: 'break-spaces' }}>
{/*{JSON.stringify(config, null, 2)}*/}
</Typography>
</Box>
<Box sx={{
height: '100%',
borderRadius: 'lg',
borderBottomLeftRadius: 0,
backgroundColor: 'background.surface',
boxShadow: 'lg',
m: 2,
p: '0.25rem 1rem',
}}>
</Box>
<Box>
a
</Box>
</Box>
);
}
@@ -85,7 +85,7 @@ export function CameraCaptureModal(props: {
}}>
{/* Top bar */}
<Sheet variant='solid' invertedColors sx={{ zIndex: 10, display: 'flex', justifyContent: 'space-between', p: 1 }}>
<Sheet variant='solid' invertedColors sx={{ display: 'flex', justifyContent: 'space-between', p: 1 }}>
<Select
variant='solid' color='neutral'
value={cameraIdx} onChange={(_event: any, value: number | null) => setCameraIdx(value === null ? -1 : value)}
@@ -116,7 +116,7 @@ export function CameraCaptureModal(props: {
{showInfo && !!info && <Typography
sx={{
position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 1,
position: 'absolute', inset: 0, zIndex: 1, /* camera info on top of video */
background: 'rgba(0,0,0,0.5)', color: 'white',
whiteSpace: 'pre', overflowY: 'scroll',
}}>
@@ -127,7 +127,7 @@ export function CameraCaptureModal(props: {
</Box>
{/* Bottom controls (zoom, ocr, download) & progress */}
<Sheet variant='soft' sx={{ display: 'flex', flexDirection: 'column', zIndex: 20, gap: 1, p: 1 }}>
<Sheet variant='soft' sx={{ display: 'flex', flexDirection: 'column', gap: 1, p: 1 }}>
{!!error && <InlineError error={error} />}
@@ -137,7 +137,7 @@ export function CameraCaptureModal(props: {
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'space-between' }}>
{/* Info */}
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)}>
<InfoIcon />
</IconButton>
{/*<Button disabled={ocrProgress !== null} fullWidth variant='solid' size='lg' onClick={handleVideoOCRClicked} sx={{ flex: 1, maxWidth: 260 }}>*/}
@@ -3,17 +3,18 @@ import * as React from 'react';
import { Box, MenuItem, Radio, Typography } from '@mui/joy';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { KeyStroke } from '~/common/components/KeyStroke';
import { KeyStroke, platformAwareKeystrokes } from '~/common/components/KeyStroke';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { ChatModeId } from '../../AppChat';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
interface ChatModeDescription {
label: string;
description: string | React.JSX.Element;
highlight?: boolean;
shortcut?: string;
hideOnDesktop?: boolean;
requiresTTI?: boolean;
}
@@ -22,9 +23,15 @@ const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
label: 'Chat',
description: 'Persona replies',
},
'generate-text-beam': {
label: 'Beam', // Best of, Auto-Prime, Top Pick, Select Best
description: 'Combine multiple models', // Smarter: combine...
shortcut: 'Ctrl + Enter',
hideOnDesktop: true,
},
'append-user': {
label: 'Write',
description: 'Appends a message',
description: 'Append a message',
shortcut: 'Alt + Enter',
},
'generate-image': {
@@ -32,13 +39,9 @@ const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
description: 'AI Image Generation',
requiresTTI: true,
},
'generate-text-beam': {
label: 'Best-Of', // Best of, Auto-Prime, Top Pick, Select Best
description: 'Smarter: best of multiple replies',
},
'generate-react': {
label: 'Reason + Act', // · α
description: 'Answers questions in multiple steps',
description: 'Answer questions in multiple steps',
},
};
@@ -50,13 +53,15 @@ function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
}
export function ChatModeMenu(props: {
anchorEl: HTMLAnchorElement | null, onClose: () => void,
chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void
isMobile: boolean,
anchorEl: HTMLAnchorElement | null,
onClose: () => void,
chatModeId: ChatModeId,
onSetChatModeId: (chatMode: ChatModeId) => void,
capabilityHasTTI: boolean,
}) {
// external state
const labsChatBeam = useUXLabsStore(state => state.labsChatBeam);
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
return (
@@ -74,17 +79,17 @@ export function ChatModeMenu(props: {
{/* ChatMode items */}
{Object.entries(ChatModeItems)
.filter(([key, data]) => key !== 'generate-text-beam' || labsChatBeam)
.filter(([_key, data]) => !data.hideOnDesktop || props.isMobile)
.map(([key, data]) =>
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
<Radio checked={key === props.chatModeId} />
<Radio color={data.highlight ? 'success' : undefined} checked={key === props.chatModeId} />
<Box sx={{ flexGrow: 1 }}>
<Typography>{data.label}</Typography>
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
</Box>
{(key === props.chatModeId || !!data.shortcut) && (
<KeyStroke combo={fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline)} />
<KeyStroke combo={platformAwareKeystrokes(fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline))} />
)}
</Box>
</MenuItem>)}
+165 -69
View File
@@ -1,7 +1,6 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';
import { fileOpen, FileWithHandle } from 'browser-fs-access';
import { keyframes } from '@emotion/react';
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Tooltip, Typography } from '@mui/joy';
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
@@ -10,7 +9,7 @@ import AttachFileIcon from '@mui/icons-material/AttachFile';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import AutoModeIcon from '@mui/icons-material/AutoMode';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
import PsychologyIcon from '@mui/icons-material/Psychology';
import SendIcon from '@mui/icons-material/Send';
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
@@ -24,27 +23,34 @@ import type { LLMOptionsOpenAI } from '~/modules/llms/vendors/openai/openai.vend
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { ConversationsManager } from '~/common/chats/ConversationsManager';
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { animationEnterBelow } from '~/common/util/animUtils';
import { conversationTitle, DConversationId, DMessageMetadata, getConversation, useChatStore } from '~/common/state/store-chats';
import { countModelTokens } from '~/common/util/token-counter';
import { isMacUser } from '~/common/util/pwaUtils';
import { launchAppCall } from '~/common/app.routes';
import { lineHeightTextareaMd } from '~/common/app.theme';
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
import { playSoundUrl } from '~/common/util/audioUtils';
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
import { useAppStateStore } from '~/common/state/store-appstate';
import { useChatOverlayStore } from '~/common/chats/store-chat-overlay-vanilla';
import { useDebouncer } from '~/common/components/useDebouncer';
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import type { ActileItem, ActileProvider } from './actile/ActileProvider';
import type { ActileItem } from './actile/ActileProvider';
import { providerCommands } from './actile/providerCommands';
import { providerStarredMessage, StarredMessageItem } from './actile/providerStarredMessage';
import { useActileManager } from './actile/useActileManager';
import type { AttachmentId } from './attachments/store-attachments';
import { Attachments } from './attachments/Attachments';
import { getTextBlockText, useLLMAttachments } from './attachments/useLLMAttachments';
import { getSingleTextBlockText, useLLMAttachments } from './attachments/useLLMAttachments';
import { useAttachments } from './attachments/useAttachments';
import type { ComposerOutputMultiPart } from './composer.types';
@@ -52,27 +58,21 @@ import { ButtonAttachCameraMemo, useCameraCaptureModal } from './buttons/ButtonA
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
import { ButtonAttachScreenCaptureMemo } from './buttons/ButtonAttachScreenCapture';
import { ButtonBeamMemo } from './buttons/ButtonBeam';
import { ButtonCallMemo } from './buttons/ButtonCall';
import { ButtonMicContinuationMemo } from './buttons/ButtonMicContinuation';
import { ButtonMicMemo } from './buttons/ButtonMic';
import { ButtonMultiChatMemo } from './buttons/ButtonMultiChat';
import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
import { ChatModeMenu } from './ChatModeMenu';
import { ReplyToBubble } from '../message/ReplyToBubble';
import { TokenBadgeMemo } from './TokenBadge';
import { TokenProgressbarMemo } from './TokenProgressbar';
import { useComposerStartupText } from './store-composer';
export const animationStopEnter = keyframes`
from {
opacity: 0;
transform: translateY(8px)
}
to {
opacity: 1;
transform: translateY(0)
}
`;
const zIndexComposerOverlayDrop = 10;
const zIndexComposerOverlayMic = 20;
const dropperCardSx: SxProps = {
display: 'none',
@@ -81,7 +81,7 @@ const dropperCardSx: SxProps = {
border: '2px dashed',
borderRadius: 'xs',
boxShadow: 'none',
zIndex: 10,
zIndex: zIndexComposerOverlayDrop,
} as const;
const dropppedCardDraggingSx: SxProps = {
@@ -101,13 +101,14 @@ export function Composer(props: {
capabilityHasT2I: boolean;
isMulticast: boolean | null;
isDeveloperMode: boolean;
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
onAction: (conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: ComposerOutputMultiPart, metadata?: DMessageMetadata) => boolean;
onTextImagine: (conversationId: DConversationId, text: string) => void;
setIsMulticast: (on: boolean) => void;
sx?: SxProps;
}) {
// state
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
const [composeText, debouncedText, setComposeText] = useDebouncer('', 300, 1200, true);
const [micContinuation, setMicContinuation] = React.useState(false);
const [speechInterimResult, setSpeechInterimResult] = React.useState<SpeechResult | null>(null);
@@ -116,16 +117,19 @@ export function Composer(props: {
// external state
const { openPreferencesTab /*, setIsFocusedMode*/ } = useOptimaLayout();
const { labsAttachScreenCapture, labsCameraDesktop } = useUXLabsStore(state => ({
const { labsAttachScreenCapture, labsCameraDesktop, labsShowCost } = useUXLabsStore(useShallow(state => ({
labsAttachScreenCapture: state.labsAttachScreenCapture,
labsCameraDesktop: state.labsCameraDesktop,
}), shallow);
labsShowCost: state.labsShowCost,
})));
const timeToShowTips = useAppStateStore(state => state.usageCount > 2);
const { novel: explainShiftEnter, touch: touchShiftEnter } = useUICounter('composer-shift-enter');
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
const { novel: explainAltEnter, touch: touchAltEnter } = useUICounter('composer-alt-enter');
const { novel: explainCtrlEnter, touch: touchCtrlEnter } = useUICounter('composer-ctrl-enter');
const [startupText, setStartupText] = useComposerStartupText();
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(state => {
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(useShallow(state => {
const conversation = state.conversations.find(_c => _c.id === props.conversationId);
return {
assistantAbortible: conversation ? !!conversation.abortController : false,
@@ -133,11 +137,18 @@ export function Composer(props: {
tokenCount: conversation ? conversation.tokenCount : 0,
stopTyping: state.stopTyping,
};
}, shallow);
}));
const { inComposer: browsingInComposer } = useBrowseCapability();
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoMessage, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
useAttachments(browsingInComposer && !composeText.startsWith('/'));
// external overlay state (extra conversationId-dependent state)
const conversationHandler = props.conversationId ? ConversationsManager.getHandler(props.conversationId) : null;
const conversationOverlayStore = conversationHandler?.getOverlayStore() ?? null;
const { replyToGenerateText } = useChatOverlayStore(conversationOverlayStore, useShallow(store => ({
replyToGenerateText: chatModeId === 'generate-text' ? store.replyToText?.trim() || null : null,
})));
// derived state
@@ -162,6 +173,8 @@ export function Composer(props: {
const tokensHistory = _historyTokenCount;
const tokensReponseMax = (props.chatLLM?.options as LLMOptionsOpenAI /* FIXME: BIG ASSUMPTION */)?.llmResponseTokens || 0;
const tokenLimit = props.chatLLM?.contextTokens || 0;
const tokenPriceIn = props.chatLLM?.pricing?.chatIn;
const tokenPriceOut = props.chatLLM?.pricing?.chatOut;
// Effect: load initial text if queued up (e.g. by /link/share_targe)
@@ -173,6 +186,18 @@ export function Composer(props: {
}, [setComposeText, setStartupText, startupText]);
// Overlay actions
const handleReplyToCleared = React.useCallback(() => {
conversationOverlayStore?.getState().setReplyToText(null);
}, [conversationOverlayStore]);
React.useEffect(() => {
if (replyToGenerateText)
setTimeout(() => props.composerTextAreaRef.current?.focus(), 1 /* prevent focus theft */);
}, [replyToGenerateText, props.composerTextAreaRef]);
// Primary button
const { conversationId, onAction } = props;
@@ -181,25 +206,33 @@ export function Composer(props: {
if (!conversationId)
return false;
// get attachments
const multiPartMessage = llmAttachments.getAttachmentsOutputs(composerText || null);
// get the multipart output including all attachments
const multiPartMessage = llmAttachments.collapseWithAttachments(composerText || null);
if (!multiPartMessage.length)
return false;
// metadata
const metadata = replyToGenerateText ? { inReplyToText: replyToGenerateText } : undefined;
// send the message
const enqueued = onAction(_chatModeId, conversationId, multiPartMessage);
const enqueued = onAction(conversationId, _chatModeId, multiPartMessage, metadata);
if (enqueued) {
clearAttachments();
handleReplyToCleared();
setComposeText('');
}
return enqueued;
}, [clearAttachments, conversationId, llmAttachments, onAction, setComposeText]);
}, [clearAttachments, conversationId, handleReplyToCleared, llmAttachments, onAction, replyToGenerateText, setComposeText]);
const handleSendClicked = React.useCallback(() => {
handleSendAction(chatModeId, composeText);
}, [chatModeId, composeText, handleSendAction]);
const handleSendTextBeamClicked = React.useCallback(() => {
handleSendAction('generate-text-beam', composeText);
}, [composeText, handleSendAction]);
const handleStopClicked = React.useCallback(() => {
!!props.conversationId && stopTyping(props.conversationId);
}, [props.conversationId, stopTyping]);
@@ -241,7 +274,7 @@ export function Composer(props: {
// Actiles
const onActileCommandSelect = React.useCallback((item: ActileItem) => {
const onActileCommandPaste = React.useCallback((item: ActileItem) => {
if (props.composerTextAreaRef.current) {
const textArea = props.composerTextAreaRef.current;
const currentText = textArea.value;
@@ -262,9 +295,22 @@ export function Composer(props: {
}
}, [props.composerTextAreaRef, setComposeText]);
const actileProviders: ActileProvider[] = React.useMemo(() => {
return [providerCommands(onActileCommandSelect)];
}, [onActileCommandSelect]);
const onActileMessageAttach = React.useCallback((item: StarredMessageItem) => {
// get the message
const conversation = getConversation(item.conversationId);
const messageToAttach = conversation?.messages.find(m => m.id === item.messageId);
if (conversation && messageToAttach && messageToAttach.text) {
// Testing with this serialization for LLM. Note it will still be within a multi-part message,
// this could be in a titled markdown block. Don't know yet how this fares with different LLMs.
const chatTitle = conversationTitle(conversation);
const textPlain = `---\nitem id: ${messageToAttach.id}\ncontext title: ${chatTitle}\n---\n${messageToAttach.text.trim()}\n`;
void attachAppendEgoMessage('context-item', textPlain, `${chatTitle} > ${messageToAttach.text.slice(0, 10)}...`);
}
}, [attachAppendEgoMessage]);
const actileProviders = React.useMemo(() => {
return [providerCommands(onActileCommandPaste), providerStarredMessage(onActileMessageAttach)];
}, [onActileCommandPaste, onActileMessageAttach]);
const { actileComponent, actileInterceptKeydown, actileInterceptTextChange } = useActileManager(actileProviders, props.composerTextAreaRef);
@@ -284,9 +330,17 @@ export function Composer(props: {
// Enter: primary action
if (e.key === 'Enter') {
// Alt: append the message instead
// Alt (Windows) or Option (Mac) + Enter: append the message instead of sending it
if (e.altKey) {
handleSendAction('append-user', composeText);
if (handleSendAction('append-user', composeText))
touchAltEnter();
return e.preventDefault();
}
// Ctrl (Windows) or Command (Mac) + Enter: send for beaming
if ((isMacUser && e.metaKey && !e.ctrlKey) || (!isMacUser && e.ctrlKey && !e.metaKey)) {
if (handleSendAction('generate-text-beam', composeText))
touchCtrlEnter();
return e.preventDefault();
}
@@ -300,7 +354,7 @@ export function Composer(props: {
}
}
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchShiftEnter]);
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
// Focus mode
@@ -401,8 +455,8 @@ export function Composer(props: {
const handleAttachmentInlineText = React.useCallback((attachmentId: AttachmentId) => {
setComposeText(currentText => {
const attachmentOutputs = llmAttachments.getAttachmentOutputs(currentText, attachmentId);
const inlinedText = getTextBlockText(attachmentOutputs) || '';
const inlinedMultiPart = llmAttachments.collapseWithAttachment(currentText, attachmentId);
const inlinedText = getSingleTextBlockText(inlinedMultiPart) || '';
removeAttachment(attachmentId);
return inlinedText;
});
@@ -410,8 +464,8 @@ export function Composer(props: {
const handleAttachmentsInlineText = React.useCallback(() => {
setComposeText(currentText => {
const attachmentsOutputs = llmAttachments.getAttachmentsOutputs(currentText);
const inlinedText = getTextBlockText(attachmentsOutputs) || '';
const inlinedMultiPart = llmAttachments.collapseWithAttachments(currentText);
const inlinedText = getSingleTextBlockText(inlinedMultiPart) || '';
clearAttachments();
return inlinedText;
});
@@ -469,19 +523,22 @@ export function Composer(props: {
const isReAct = chatModeId === 'generate-react';
const isDraw = chatModeId === 'generate-image';
const showCall = isText || isAppend;
const showChatReplyTo = !!replyToGenerateText;
const showChatExtras = isText && !showChatReplyTo;
const buttonVariant: VariantProp = (isAppend || (isMobile && isTextBeam)) ? 'outlined' : 'solid';
const buttonColor: ColorPaletteProp =
assistantAbortible ? 'warning'
: isReAct ? 'success'
: isTextBeam ? 'success'
: isTextBeam ? 'primary'
: isDraw ? 'warning'
: 'primary';
const buttonText =
isAppend ? 'Write'
: isReAct ? 'ReAct'
: isTextBeam ? 'Best-Of'
: isTextBeam ? 'Beam'
: isDraw ? 'Draw'
: 'Chat';
@@ -490,18 +547,25 @@ export function Composer(props: {
: isAppend ? <SendIcon sx={{ fontSize: 18 }} />
: isReAct ? <PsychologyIcon />
: isTextBeam ? <ChatBeamIcon /> /* <GavelIcon /> */
: isDraw ? <FormatPaintIcon />
: isDraw ? <FormatPaintTwoToneIcon />
: <TelegramIcon />;
let textPlaceholder: string =
isDraw ? 'Describe an idea or a drawing...'
: isReAct ? 'Multi-step reasoning question...'
: isTextBeam ? 'Multi-chat with this persona...'
: props.isDeveloperMode ? 'Chat with me' + (isDesktop ? ' · drop source' : '') + ' · attach code...'
: props.capabilityHasT2I ? 'Chat · /react · /draw · drop files...'
: 'Chat · /react · drop files...';
if (isDesktop && explainShiftEnter)
textPlaceholder += !enterIsNewline ? '\nShift+Enter to add a new line' : '\nShift+Enter to send';
: isTextBeam ? 'Beam: combine the smarts of models...'
: showChatReplyTo ? 'Chat about this'
: props.isDeveloperMode ? 'Chat with me' + (isDesktop ? ' · drop source' : '') + ' · attach code...'
: props.capabilityHasT2I ? 'Chat · /beam · /draw · drop files...'
: 'Chat · /react · drop files...';
if (isDesktop && timeToShowTips) {
if (explainShiftEnter)
textPlaceholder += !enterIsNewline ? '\n\n💡 Shift + Enter to add a new line' : '\n\n💡 Shift + Enter to send';
else if (explainAltEnter)
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Alt + Enter to just append the message');
else if (explainCtrlEnter)
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Ctrl + Enter to beam');
}
return (
<Box aria-label='User Message' component='section' sx={props.sx}>
@@ -584,7 +648,7 @@ export function Composer(props: {
variant='outlined'
color={isDraw ? 'warning' : isReAct ? 'success' : undefined}
autoFocus
minRows={isMobile ? 4 : 5}
minRows={isMobile ? 4 : showChatReplyTo ? 4 : 5}
maxRows={isMobile ? 8 : 10}
placeholder={textPlaceholder}
value={composeText}
@@ -595,6 +659,7 @@ export function Composer(props: {
onPasteCapture={handleAttachCtrlV}
// onFocusCapture={handleFocusModeOn}
// onBlurCapture={handleFocusModeOff}
endDecorator={showChatReplyTo && <ReplyToBubble replyToText={replyToGenerateText} onClear={handleReplyToCleared} className='reply-to-bubble' />}
slotProps={{
textarea: {
enterKeyHint: enterIsNewline ? 'enter' : 'send',
@@ -607,16 +672,16 @@ export function Composer(props: {
}}
sx={{
backgroundColor: 'background.level1',
'&:focus-within': { backgroundColor: 'background.popup' },
'&:focus-within': { backgroundColor: 'background.popup', '.reply-to-bubble': { backgroundColor: 'background.popup' } },
lineHeight: lineHeightTextareaMd,
}} />
{tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
<TokenProgressbarMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} />
{!showChatReplyTo && tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
<TokenProgressbarMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} tokenPriceIn={tokenPriceIn} tokenPriceOut={tokenPriceOut} />
)}
{!!tokenLimit && (
<TokenBadgeMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} showExcess absoluteBottomRight />
{!showChatReplyTo && tokenLimit > 0 && (
<TokenBadgeMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} tokenPriceIn={tokenPriceIn} tokenPriceOut={tokenPriceOut} showCost={labsShowCost} showExcess absoluteBottomRight />
)}
</Box>
@@ -625,7 +690,7 @@ export function Composer(props: {
{isSpeechEnabled && (
<Box sx={{
position: 'absolute', top: 0, right: 0,
zIndex: 21,
zIndex: zIndexComposerOverlayMic + 1,
mt: isDesktop ? 1 : 0.25,
mr: isDesktop ? 1 : 0.25,
display: 'flex', flexDirection: 'column', gap: isDesktop ? 1 : 0.25,
@@ -644,20 +709,32 @@ export function Composer(props: {
{/* overlay: Mic */}
{micIsRunning && (
<Card
color='primary' variant='soft' invertedColors
color='primary' variant='soft'
sx={{
display: 'flex',
position: 'absolute', bottom: 0, left: 0, right: 0, top: 0,
// alignItems: 'center', justifyContent: 'center',
border: '1px solid',
borderColor: 'primary.solidBg',
borderRadius: 'sm',
zIndex: 20,
px: 1.5, py: 1,
zIndex: zIndexComposerOverlayMic,
pl: 1.5,
pr: { xs: 1.5, md: 5 },
py: 0.625,
overflow: 'auto',
}}>
<Typography sx={{
color: 'primary.softColor',
lineHeight: lineHeightTextareaMd,
'& .interim': {
textDecoration: 'underline',
textDecorationThickness: '0.25em',
textDecorationColor: 'rgba(var(--joy-palette-primary-mainChannel) / 0.1)',
textDecorationSkipInk: 'none',
textUnderlineOffset: '0.25em',
},
}}>
<Typography>
{speechInterimResult.transcript}{' '}
<span style={{ opacity: 0.8 }}>{speechInterimResult.interimTranscript}</span>
<span className={speechInterimResult.interimTranscript !== 'Listening...' ? 'interim' : undefined}>{speechInterimResult.interimTranscript}</span>
</Typography>
</Card>
)}
@@ -696,11 +773,12 @@ export function Composer(props: {
<Grid xs={12} md={3}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, height: '100%' } as const}>
{/* This row is here only for the [mobile] bottom-start corner item */}
<Box sx={{ display: 'flex' }}>
{/* [mobile] This row is here only for the [mobile] bottom-start corner item */}
{/* [desktop] This column arrangement will have the [desktop] beam button right under call */}
<Box sx={isMobile ? { display: 'flex' } : { display: 'grid', gap: 1 }}>
{/* [mobile] bottom-corner secondary button */}
{isMobile && (showCall
{isMobile && (showChatExtras
? <ButtonCallMemo isMobile disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />
: isDraw
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
@@ -709,11 +787,12 @@ export function Composer(props: {
{/* Responsive Send/Stop buttons */}
<ButtonGroup
variant={isAppend ? 'outlined' : 'solid'}
variant={buttonVariant}
color={buttonColor}
sx={{
flexGrow: 1,
boxShadow: isMobile ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
backgroundColor: (isMobile && buttonVariant === 'outlined') ? 'background.popup' : undefined,
boxShadow: (isMobile && buttonVariant !== 'outlined') ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
}}
>
{!assistantAbortible ? (
@@ -732,12 +811,19 @@ export function Composer(props: {
fullWidth variant='soft' disabled={!props.conversationId}
onClick={handleStopClicked}
endDecorator={<StopOutlinedIcon sx={{ fontSize: 18 }} />}
sx={{ animation: `${animationStopEnter} 0.1s ease-out` }}
sx={{ animation: `${animationEnterBelow} 0.1s ease-out` }}
>
Stop
</Button>
)}
{/* [Beam] Open Beam */}
{/*{isText && <Tooltip title='Open Beam'>*/}
{/* <IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleSendTextBeamClicked}>*/}
{/* <ChatBeamIcon />*/}
{/* </IconButton>*/}
{/*</Tooltip>}*/}
{/* [Draw] Imagine */}
{isDraw && !!composeText && <Tooltip title='Imagine a drawing prompt'>
<IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleTextImagineClicked}>
@@ -755,6 +841,15 @@ export function Composer(props: {
</IconButton>
</ButtonGroup>
{/* [desktop] secondary-top buttons */}
{isDesktop && showChatExtras && !assistantAbortible && (
<ButtonBeamMemo
disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
hasContent={!!composeText}
onClick={handleSendTextBeamClicked}
/>
)}
</Box>
{/* [desktop] Multicast switch (under the Chat button) */}
@@ -764,7 +859,7 @@ export function Composer(props: {
{isDesktop && <Box sx={{ mt: 'auto', display: 'grid', gap: 1 }}>
{/* [desktop] Call secondary button */}
{showCall && <ButtonCallMemo disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
{showChatExtras && <ButtonCallMemo disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
{/* [desktop] Draw Options secondary button */}
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
@@ -779,6 +874,7 @@ export function Composer(props: {
{/* Mode selector */}
{!!chatModeMenuAnchor && (
<ChatModeMenu
isMobile={isMobile}
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
capabilityHasTTI={props.capabilityHasT2I}
+110 -45
View File
@@ -3,41 +3,81 @@ import * as React from 'react';
import { Badge, Box, ColorPaletteProp, Tooltip } from '@mui/joy';
function alignRight(value: number, columnSize: number = 7) {
function alignRight(value: number, columnSize: number = 8) {
const str = value.toLocaleString();
return str.padStart(columnSize);
}
function formatCost(cost: number) {
return cost < 1
? (cost * 100).toFixed(cost < 0.010 ? 2 : 1) + ' ¢'
: '$ ' + cost.toFixed(2);
}
export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, historyTokens?: number, responseMaxTokens?: number): {
color: ColorPaletteProp, message: string, remainingTokens: number
export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, historyTokens?: number, responseMaxTokens?: number, tokenPriceIn?: number, tokenPriceOut?: number): {
color: ColorPaletteProp,
message: string,
remainingTokens: number,
costMax?: number,
costMin?: number,
} {
const usedTokens = directTokens + (historyTokens || 0) + (responseMaxTokens || 0);
const remainingTokens = tokenLimit - usedTokens;
const usedInputTokens = directTokens + (historyTokens || 0);
const usedMaxTokens = usedInputTokens + (responseMaxTokens || 0);
const remainingTokens = tokenLimit - usedMaxTokens;
const gteLimit = (remainingTokens <= 0 && tokenLimit > 0);
// message
let message: string = gteLimit ? '⚠️ ' : '';
// costs
let costMax: number | undefined = undefined;
let costMin: number | undefined = undefined;
// no limit: show used tokens only
if (!tokenLimit) {
message += `Requested: ${usedTokens.toLocaleString()} tokens`;
message += `Requested: ${usedMaxTokens.toLocaleString()} tokens`;
}
// has full information (d + i < l)
else if (historyTokens || responseMaxTokens) {
message +=
`${Math.abs(remainingTokens).toLocaleString()} ${remainingTokens >= 0 ? 'available' : 'excess'} message tokens\n\n` +
`${Math.abs(remainingTokens).toLocaleString()} ${remainingTokens >= 0 ? 'available' : 'excess'} message tokens\n\n` +
` = Model max tokens: ${alignRight(tokenLimit)}\n` +
` - This message: ${alignRight(directTokens)}\n` +
` - History: ${alignRight(historyTokens || 0)}\n` +
` - Max response: ${alignRight(responseMaxTokens || 0)}`;
// add the price, if available
if (tokenPriceIn || tokenPriceOut) {
costMin = tokenPriceIn ? usedInputTokens * tokenPriceIn / 1E6 : undefined;
const costOutMax = (tokenPriceOut && responseMaxTokens) ? responseMaxTokens * tokenPriceOut / 1E6 : undefined;
if (costMin || costOutMax) {
message += `\n\n\n▶ Chat Turn Cost (max, approximate)\n`;
if (costMin) message += '\n' +
` Input tokens: ${alignRight(usedInputTokens)}\n` +
` Input Price $/M: ${tokenPriceIn!.toFixed(2).padStart(8)}\n` +
` Input cost: ${('$' + costMin!.toFixed(4)).padStart(8)}\n`;
if (costOutMax) message += '\n' +
` Max output tokens: ${alignRight(responseMaxTokens!)}\n` +
` Output Price $/M: ${tokenPriceOut!.toFixed(2).padStart(8)}\n` +
` Max output cost: ${('$' + costOutMax!.toFixed(4)).padStart(8)}\n`;
if (costMin) message += '\n' +
` > Min turn cost: ${formatCost(costMin).padStart(8)}`;
costMax = (costMin && costOutMax) ? costMin + costOutMax : undefined;
if (costMax) message += '\n' +
` < Max turn cost: ${formatCost(costMax).padStart(8)}`;
}
}
}
// Cleaner mode: d + ? < R (total is the remaining in this case)
else {
message +=
`${(tokenLimit + usedTokens).toLocaleString()} available tokens after deleting this\n\n` +
`${(tokenLimit + usedMaxTokens).toLocaleString()} available tokens after deleting this\n\n` +
` = Currently free: ${alignRight(tokenLimit)}\n` +
` + This message: ${alignRight(usedTokens)}`;
` + This message: ${alignRight(usedMaxTokens)}`;
}
const color: ColorPaletteProp =
@@ -47,23 +87,21 @@ export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, h
? 'warning'
: 'primary';
return { color, message, remainingTokens };
return { color, message, remainingTokens, costMax, costMin };
}
export const TokenTooltip = (props: { message: string | null, color: ColorPaletteProp, placement?: 'top' | 'top-end', children: React.JSX.Element }) =>
export const TokenTooltip = (props: { message: string | null, color: ColorPaletteProp, placement?: 'top' | 'top-end', children: React.ReactElement }) =>
<Tooltip
placement={props.placement}
variant={props.color !== 'primary' ? 'solid' : 'soft'} color={props.color}
title={props.message
? <Box sx={{ p: 2, whiteSpace: 'pre' }}>
{props.message}
</Box>
: null
}
title={props.message ? <Box sx={{ p: 2, whiteSpace: 'pre' }}>{props.message}</Box> : null}
sx={{
fontFamily: 'code',
boxShadow: 'xl',
// fontSize: '0.8125rem',
border: '1px solid',
borderColor: `${props.color}.outlinedColor`,
boxShadow: 'md',
}}
>
{props.children}
@@ -76,38 +114,65 @@ export const TokenTooltip = (props: { message: string | null, color: ColorPalett
export const TokenBadgeMemo = React.memo(TokenBadge);
function TokenBadge(props: {
direct: number, history?: number, responseMax?: number, limit: number,
showExcess?: boolean, absoluteBottomRight?: boolean, inline?: boolean,
direct: number,
history?: number,
responseMax?: number,
limit: number,
tokenPriceIn?: number,
tokenPriceOut?: number,
showCost?: boolean
showExcess?: boolean,
absoluteBottomRight?: boolean,
inline?: boolean,
}) {
const { message, color, remainingTokens } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax);
const { message, color, remainingTokens, costMax, costMin } =
tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut);
// show the direct tokens, unless we exceed the limit and 'showExcess' is enabled
const value = (props.showExcess && (props.limit && remainingTokens <= 0))
? Math.abs(remainingTokens)
: props.direct;
let badgeValue: string;
const showAltCosts = !!props.showCost && !!costMax && costMin !== undefined;
if (showAltCosts) {
badgeValue = '< ' + formatCost(costMax);
} else {
// show the direct tokens, unless we exceed the limit and 'showExcess' is enabled
const value = (props.showExcess && (props.limit && remainingTokens <= 0))
? Math.abs(remainingTokens)
: props.direct;
badgeValue = value.toLocaleString();
}
const shallHide = !props.direct && remainingTokens >= 0 && !showAltCosts;
if (shallHide) return null;
return (
<Badge
variant='solid' color={color} max={100000}
invisible={!props.direct && remainingTokens >= 0}
badgeContent={
<TokenTooltip color={color} message={message}>
<span>{value.toLocaleString()}</span>
</TokenTooltip>
}
sx={{
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: 8 }),
cursor: 'help',
}}
slotProps={{
badge: {
sx: {
fontFamily: 'code',
...((props.absoluteBottomRight || props.inline) && { position: 'static', transform: 'none' }),
<TokenTooltip color={color} message={message} placement='top-end'>
<Badge
variant='soft' color={color} max={1000000}
// invisible={shallHide}
badgeContent={badgeValue}
slotProps={{
root: {
sx: {
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: 8 }),
cursor: 'help',
},
},
},
}}
/>
badge: {
sx: {
// the badge (not the tooltip)
// boxShadow: 'sm',
fontFamily: 'code',
fontSize: 'xs',
...((props.absoluteBottomRight || props.inline) && { position: 'static', transform: 'none' }),
},
},
}}
/>
</TokenTooltip>
);
}
@@ -12,7 +12,15 @@ import { tokensPrettyMath, TokenTooltip } from './TokenBadge';
*/
export const TokenProgressbarMemo = React.memo(TokenProgressbar);
function TokenProgressbar(props: { direct: number, history: number, responseMax: number, limit: number }) {
function TokenProgressbar(props: {
direct: number,
history: number,
responseMax: number,
limit: number,
tokenPriceIn?: number,
tokenPriceOut?: number,
}) {
// external state
const theme = useTheme();
@@ -40,7 +48,7 @@ function TokenProgressbar(props: { direct: number, history: number, responseMax:
const overflowColor = theme.palette.danger.softColor;
// tooltip message/color
const { message, color } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax);
const { message, color } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut);
// sizes
const containerHeight = 8;
@@ -49,7 +49,7 @@ export function ActilePopup(props: {
const labelNormal = item.label.slice(props.activePrefixLength);
return (
<ListItem
key={item.id}
key={item.key}
variant={isActive ? 'soft' : undefined}
color={isActive ? 'primary' : undefined}
onClick={() => props.onItemClick(item)}
@@ -1,22 +1,15 @@
import type { FunctionComponent } from 'react';
export interface ActileItem {
id: string;
key: string;
label: string;
argument?: string;
description?: string;
Icon?: FunctionComponent;
}
type ActileProviderIds = 'actile-commands' | 'actile-attach-reference';
export interface ActileProvider {
id: ActileProviderIds;
title: string;
searchPrefix: string;
checkTriggerText: (trailingText: string) => boolean;
fetchItems: () => Promise<ActileItem[]>;
export interface ActileProvider<TItem extends ActileItem = ActileItem> {
fastCheckTriggerText: (trailingText: string) => boolean;
fetchItems: () => Promise<{ title: string, searchPrefix: string, items: TItem[] }>;
onItemSelect: (item: ActileItem) => void;
}
@@ -1,24 +0,0 @@
//import { ActileItem, ActileProvider } from './ActileProvider';
/*export const providerAttachReference: ActileProvider = {
id: 'actile-attach-reference',
title: 'Attach Reference',
searchPrefix: '@',
checkTriggerText: (trailingText: string) =>
trailingText.endsWith(' @'),
fetchItems: async () => {
return [{
id: 'test-1',
label: 'Attach This',
description: 'Attach this to the message',
Icon: undefined,
}];
},
onItemSelect: (item: ActileItem) => {
console.log('Selected item:', item);
},
};*/
@@ -2,23 +2,25 @@ import { ActileItem, ActileProvider } from './ActileProvider';
import { findAllChatCommands } from '../../../commands/commands.registry';
export const providerCommands = (onItemSelect: (item: ActileItem) => void): ActileProvider => ({
id: 'actile-commands',
title: 'Chat Commands',
searchPrefix: '/',
export function providerCommands(onCommandSelect: (item: ActileItem) => void): ActileProvider {
return {
checkTriggerText: (trailingText: string) =>
trailingText.trim() === '/',
// only the literal '/' is a trigger
fastCheckTriggerText: (trailingText: string) => trailingText === '/',
fetchItems: async () => {
return findAllChatCommands().map((cmd) => ({
id: cmd.primary,
label: cmd.primary,
argument: cmd.arguments?.join(' ') ?? undefined,
description: cmd.description,
Icon: cmd.Icon,
}));
},
// no real need to be async
fetchItems: async () => ({
title: 'Chat Commands',
searchPrefix: '/',
items: findAllChatCommands().map((cmd) => ({
key: cmd.primary,
label: cmd.primary,
argument: cmd.arguments?.join(' ') ?? undefined,
description: cmd.description,
Icon: cmd.Icon,
} satisfies ActileItem)),
}),
onItemSelect,
});
onItemSelect: onCommandSelect,
};
}
@@ -0,0 +1,46 @@
import { conversationTitle, DConversationId, messageHasUserFlag, useChatStore } from '~/common/state/store-chats';
import { ActileItem, ActileProvider } from './ActileProvider';
export interface StarredMessageItem extends ActileItem {
conversationId: DConversationId,
messageId: string,
}
export function providerStarredMessage(onMessageSeelect: (item: StarredMessageItem) => void): ActileProvider<StarredMessageItem> {
return {
// only the literal '@' at start of chat, or ' @' at end of chat
fastCheckTriggerText: (trailingText: string) => trailingText === '@' || trailingText.endsWith(' @'),
// finds all the starred messages in all the conversations - this could be heavy
fetchItems: async () => {
const { conversations } = useChatStore.getState();
const starredMessages: StarredMessageItem[] = [];
conversations.forEach((conversation) => {
conversation.messages.forEach((message) => {
messageHasUserFlag(message, 'starred') && starredMessages.push({
// data
conversationId: conversation.id,
messageId: message.id,
// looks
key: message.id,
label: conversationTitle(conversation) + ' - ' + message.text.slice(0, 32) + '...',
// description: message.text.slice(32, 100),
Icon: undefined,
} satisfies StarredMessageItem);
});
});
return {
title: 'Starred Messages',
searchPrefix: '',
items: starredMessages,
};
},
onItemSelect: item => onMessageSeelect(item as StarredMessageItem),
};
}
@@ -9,6 +9,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
const [popupOpen, setPopupOpen] = React.useState(false);
const [provider, setProvider] = React.useState<ActileProvider | null>(null);
const [title, setTitle] = React.useState<string>('');
const [items, setItems] = React.useState<ActileItem[]>([]);
const [activeSearchString, setActiveSearchString] = React.useState<string>('');
const [activeItemIndex, setActiveItemIndex] = React.useState<number>(0);
@@ -17,7 +18,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
// derived state
const activeItems = React.useMemo(() => {
const search = activeSearchString.trim().toLowerCase();
return items.filter(item => item.label.toLowerCase().startsWith(search));
return items.filter(item => item.label?.toLowerCase().startsWith(search));
}, [items, activeSearchString]);
const activeItem = activeItemIndex >= 0 && activeItemIndex < activeItems.length ? activeItems[activeItemIndex] : null;
@@ -25,6 +26,7 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
const handleClose = React.useCallback(() => {
setPopupOpen(false);
setProvider(null);
setTitle('');
setItems([]);
setActiveSearchString('');
setActiveItemIndex(0);
@@ -42,13 +44,19 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
const actileInterceptTextChange = React.useCallback((trailingText: string) => {
for (const provider of providers) {
if (provider.checkTriggerText(trailingText)) {
setProvider(provider);
setPopupOpen(true);
setActiveSearchString(provider.searchPrefix);
if (provider.fastCheckTriggerText(trailingText)) {
provider
.fetchItems()
.then(items => setItems(items))
.then(({ title, searchPrefix, items }) => {
// if there are no items, ignore
if (items.length) {
setPopupOpen(true);
setProvider(provider);
setTitle(title);
setItems(items);
setActiveSearchString(searchPrefix);
}
})
.catch(error => {
handleClose();
console.error('Failed to fetch popup items:', error);
@@ -100,14 +108,14 @@ export const useActileManager = (providers: ActileProvider[], anchorRef: React.R
<ActilePopup
anchorEl={anchorRef.current}
onClose={handleClose}
title={provider?.title}
title={title}
items={activeItems}
activeItemIndex={activeItemIndex}
activePrefixLength={activeSearchString.length}
onItemClick={handlePopupItemClicked}
/>
);
}, [activeItemIndex, activeItems, activeSearchString.length, anchorRef, handleClose, handlePopupItemClicked, popupOpen, provider?.title]);
}, [activeItemIndex, activeItems, activeSearchString.length, anchorRef, handleClose, handlePopupItemClicked, popupOpen, title]);
return {
actileComponent,
@@ -6,6 +6,7 @@ import CodeIcon from '@mui/icons-material/Code';
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
import PictureAsPdfIcon from '@mui/icons-material/PictureAsPdf';
import PivotTableChartIcon from '@mui/icons-material/PivotTableChart';
import TelegramIcon from '@mui/icons-material/Telegram';
import TextFieldsIcon from '@mui/icons-material/TextFields';
import TextureIcon from '@mui/icons-material/Texture';
import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
@@ -73,6 +74,7 @@ const converterTypeToIconMap: { [key in AttachmentConverterType]: React.Componen
'pdf-images': PictureAsPdfIcon,
'image': ImageOutlinedIcon,
'image-ocr': AbcIcon,
'ego-message-md': TelegramIcon,
'unhandled': TextureIcon,
};
@@ -126,7 +128,7 @@ export function AttachmentItem(props: {
const handleToggleMenu = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
event.stopPropagation();
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
onItemMenuToggle(attachment.id, event.currentTarget);
}, [attachment, onItemMenuToggle]);
@@ -179,6 +181,7 @@ export function AttachmentItem(props: {
size='sm'
variant={variant} color={color}
onClick={handleToggleMenu}
onContextMenu={handleToggleMenu}
sx={{
backgroundColor: props.menuShown ? `${color}.softActiveBg` : variant === 'outlined' ? 'background.popup' : undefined,
border: variant === 'soft' ? '1px solid' : undefined,
@@ -153,7 +153,11 @@ export function AttachmentMenu(props: {
{/* Converters: {aConverters.map(((converter, idx) => ` ${converter.id}${(idx === aConverterIdx) ? '*' : ''}`)).join(', ')}*/}
{/*</Typography>*/}
<Typography level='body-xs'>
🡒 {isOutputMissing ? 'empty' : aOutputs.map(output => `${output.type}, ${output.type === 'text-block' ? output.text.length.toLocaleString() : '(base64 image)'} bytes`).join(' · ')}
🡒 {isOutputMissing ? 'empty' : aOutputs.map(output => `${output.type}, ${output.type === 'text-block'
? output.text.length.toLocaleString()
: output.type === 'image-part'
? output.base64Url.length.toLocaleString()
: '(other)'} bytes`).join(' · ')}
</Typography>
{!!tokenCountApprox && <Typography level='body-xs'>
🡒 {tokenCountApprox.toLocaleString()} tokens
@@ -68,8 +68,10 @@ export function Attachments(props: {
const handleOverallMenuHide = () => setOverallMenuAnchor(null);
const handleOverallMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) =>
const handleOverallMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
setOverallMenuAnchor(anchor => anchor ? null : event.currentTarget);
};
// overall operations
@@ -112,6 +114,7 @@ export function Attachments(props: {
{/* Overall Menu button */}
<IconButton
onClick={handleOverallMenuToggle}
onContextMenu={handleOverallMenuToggle}
sx={{
// borderRadius: 'sm',
borderRadius: 0,
@@ -150,7 +153,7 @@ export function Attachments(props: {
</MenuItem>
<MenuItem onClick={handleClearAttachments}>
<ListItemDecorator><ClearIcon /></ListItemDecorator>
Clear
Clear{attachments.length > 5 ? <span style={{ opacity: 0.5 }}> {attachments.length} attachments</span> : null}
</MenuItem>
</CloseableMenu>
)}
@@ -2,7 +2,7 @@ import { callBrowseFetchPage } from '~/modules/browse/browse.client';
import { createBase36Uid } from '~/common/util/textUtils';
import { htmlTableToMarkdown } from '~/common/util/htmlTableToMarkdown';
import { pdfToText } from '~/common/util/pdfUtils';
import { pdfToImageDataURLs, pdfToText } from '~/common/util/pdfUtils';
import type { Attachment, AttachmentConverter, AttachmentId, AttachmentInput, AttachmentSource } from './store-attachments';
import type { ComposerOutputMultiPart } from '../composer.types';
@@ -58,16 +58,12 @@ export async function attachmentLoadInputAsync(source: Readonly<AttachmentSource
edit({ label: source.refUrl, ref: source.refUrl });
try {
const page = await callBrowseFetchPage(source.url);
if (page.content) {
edit({
input: {
mimeType: 'text/plain',
data: page.content,
dataSize: page.content.length,
},
});
} else
edit({ inputError: 'No content found at this link' });
edit(
page.content.markdown ? { input: { mimeType: 'text/markdown', data: page.content.markdown, dataSize: page.content.markdown.length } }
: page.content.text ? { input: { mimeType: 'text/plain', data: page.content.text, dataSize: page.content.text.length } }
: page.content.html ? { input: { mimeType: 'text/html', data: page.content.html, dataSize: page.content.html.length } }
: { inputError: 'No content found at this link' },
);
} catch (error: any) {
edit({ inputError: `Issue downloading page: ${error?.message || (typeof error === 'string' ? error : JSON.stringify(error))}` });
}
@@ -132,6 +128,18 @@ export async function attachmentLoadInputAsync(source: Readonly<AttachmentSource
});
}
break;
case 'ego':
edit({
label: source.label,
ref: source.blockTitle,
input: {
mimeType: 'ego/message',
data: source.textPlain,
dataSize: source.textPlain.length,
},
});
break;
}
edit({ inputLoading: false });
@@ -192,6 +200,11 @@ export function attachmentDefineConverters(sourceType: AttachmentSource['media']
converters.push({ id: 'image-ocr', name: 'As Text (OCR)' });
break;
// EGO
case input.mimeType === 'ego/message':
converters.push({ id: 'ego-message-md', name: 'Message' });
break;
// catch-all
default:
converters.push({ id: 'unhandled', name: `${input.mimeType}`, unsupported: true });
@@ -280,7 +293,7 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
case 'pdf-text':
if (!(input.data instanceof ArrayBuffer)) {
console.log('Expected ArrayBuffer for PDF converter, got:', typeof input.data);
console.log('Expected ArrayBuffer for PDF text converter, got:', typeof input.data);
break;
}
// duplicate the ArrayBuffer to avoid mutation
@@ -295,7 +308,29 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
break;
case 'pdf-images':
// TODO: extract all pages as individual images
if (!(input.data instanceof ArrayBuffer)) {
console.log('Expected ArrayBuffer for PDF images converter, got:', typeof input.data);
break;
}
// duplicate the ArrayBuffer to avoid mutation
const pdfData2 = new Uint8Array(input.data.slice(0));
try {
const imageDataURLs = await pdfToImageDataURLs(pdfData2);
imageDataURLs.forEach((pdfImg, index) => {
outputs.push({
type: 'image-part',
base64Url: pdfImg.base64Url,
metadata: {
title: `Page ${index + 1}`,
width: pdfImg.width,
height: pdfImg.height,
},
collapsible: false,
});
});
} catch (error) {
console.error('Error converting PDF to images:', error);
}
break;
case 'image':
@@ -333,6 +368,15 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
}
break;
case 'ego-message-md':
outputs.push({
type: 'text-block',
text: inputDataToString(input.data),
title: ref,
collapsible: true,
});
break;
case 'unhandled':
// force the user to explicitly select 'as text' if they want to proceed
break;
@@ -24,6 +24,12 @@ export type AttachmentSource = {
method: 'clipboard-read' | AttachmentSourceOriginDTO;
textPlain?: string;
textHtml?: string;
} | {
media: 'ego';
method: 'ego-message';
label: string;
blockTitle: string;
textPlain: string;
};
@@ -41,6 +47,7 @@ export type AttachmentConverterType =
| 'text' | 'rich-text' | 'rich-text-table'
| 'pdf-text' | 'pdf-images'
| 'image' | 'image-ocr'
| 'ego-message-md'
| 'unhandled';
export type AttachmentConverter = {
@@ -62,7 +69,7 @@ export type Attachment = {
readonly id: AttachmentId;
readonly source: AttachmentSource,
label: string;
ref: string;
ref: string; // will be used in ```ref\n...``` for instance
inputLoading: boolean;
inputError: string | null;
@@ -100,6 +100,16 @@ export const useAttachments = (enableLoadURLs: boolean) => {
}, [attachAppendFile, createAttachment, enableLoadURLs]);
const attachAppendEgoMessage = React.useCallback((blockTitle: string, textPlain: string, attachmentLabel: string) => {
if (ATTACHMENTS_DEBUG_INTAKE)
console.log('attachAppendEgo', { blockTitle, textPlain, attachmentLabel });
return createAttachment({
media: 'ego', method: 'ego-message', label: attachmentLabel, blockTitle: blockTitle, textPlain: textPlain,
});
}, [createAttachment]);
const attachAppendClipboardItems = React.useCallback(async () => {
// if there's an issue accessing the clipboard, show it passively
@@ -178,6 +188,7 @@ export const useAttachments = (enableLoadURLs: boolean) => {
// create attachments
attachAppendClipboardItems,
attachAppendDataTransfer,
attachAppendEgoMessage,
attachAppendFile,
// manage attachments
@@ -10,8 +10,8 @@ import type { ComposerOutputMultiPart, ComposerOutputPartType } from '../compose
export interface LLMAttachments {
attachments: LLMAttachment[];
getAttachmentOutputs: (initialTextBlockText: string | null, attachmentId: AttachmentId) => ComposerOutputMultiPart;
getAttachmentsOutputs: (initialTextBlockText: string | null) => ComposerOutputMultiPart;
collapseWithAttachment: (initialTextBlockText: string | null, attachmentId: AttachmentId) => ComposerOutputMultiPart;
collapseWithAttachments: (initialTextBlockText: string | null) => ComposerOutputMultiPart;
isOutputAttacheable: boolean;
isOutputTextInlineable: boolean;
tokenCountApprox: number;
@@ -37,13 +37,13 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
const llmAttachments = attachments.map(attachment => toLLMAttachment(attachment, supportedOutputPartTypes, chatLLMId));
const getAttachmentOutputs = (initialTextBlockText: string | null, attachmentId: AttachmentId): ComposerOutputMultiPart => {
const collapseWithAttachment = (initialTextBlockText: string | null, attachmentId: AttachmentId): ComposerOutputMultiPart => {
// get outputs of a specific attachment
const outputs = attachments.find(a => a.id === attachmentId)?.outputs || [];
return attachmentCollapseOutputs(initialTextBlockText, outputs);
};
const getAttachmentsOutputs = (initialTextBlockText: string | null): ComposerOutputMultiPart => {
const collapseWithAttachments = (initialTextBlockText: string | null): ComposerOutputMultiPart => {
// accumulate all outputs of all attachments
const allOutputs = llmAttachments.reduce((acc, a) => acc.concat(a.attachment.outputs), [] as ComposerOutputMultiPart);
return attachmentCollapseOutputs(initialTextBlockText, allOutputs);
@@ -51,8 +51,8 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
return {
attachments: llmAttachments,
getAttachmentOutputs,
getAttachmentsOutputs,
collapseWithAttachment,
collapseWithAttachments,
isOutputAttacheable: llmAttachments.every(a => a.isOutputAttachable),
isOutputTextInlineable: llmAttachments.every(a => a.isOutputTextInlineable),
tokenCountApprox: llmAttachments.reduce((acc, a) => acc + (a.tokenCountApprox || 0), 0),
@@ -60,7 +60,7 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
}, [attachments, chatLLMId]);
}
export function getTextBlockText(outputs: ComposerOutputMultiPart): string | null {
export function getSingleTextBlockText(outputs: ComposerOutputMultiPart): string | null {
const textOutputs = outputs.filter(part => part.type === 'text-block');
return (textOutputs.length === 1 && textOutputs[0].type === 'text-block') ? textOutputs[0].text : null;
}
@@ -131,11 +131,13 @@ function attachmentCollapseOutputs(initialTextBlockText: string | null, outputs:
// start a new part
else {
if (output.type === 'text-block') {
// THIS IS NOT CORRECT - we seem to be doing it just for downstream token counting - FIX IT
// Do not serialize here
accumulatedOutputs.push({
type: 'text-block',
text: `\n\n\`\`\`${output.title}\n${output.text}\n\`\`\``,
title: null,
collapsible: false,
collapsible: false, // Wrong
});
} else {
accumulatedOutputs.push(output);
@@ -0,0 +1,50 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
import { KeyStroke } from '~/common/components/KeyStroke';
import { animationEnterBelow } from '~/common/util/animUtils';
const desktopLegend =
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
Combine the answers from multiple models<br />
<KeyStroke combo='Ctrl + Enter' sx={{ mt: 0.5, mb: 0.25 }} />
</Box>;
const desktopLegendNoContent =
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
Enter the text to Beam, then press this
</Box>;
const mobileSx: SxProps = {
mr: { xs: 1, md: 2 },
};
const desktopSx: SxProps = {
'--Button-gap': '1rem',
backgroundColor: 'background.popup',
// border: '1px solid',
// borderColor: 'primary.outlinedBorder',
boxShadow: '0 4px 16px -4px rgb(var(--joy-palette-primary-mainChannel) / 10%)',
animation: `${animationEnterBelow} 0.1s ease-out`,
};
export const ButtonBeamMemo = React.memo(ButtonBeam);
function ButtonBeam(props: { isMobile?: boolean, disabled?: boolean, hasContent?: boolean, onClick: () => void }) {
return props.isMobile ? (
<IconButton variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
<ChatBeamIcon />
</IconButton>
) : (
<Tooltip disableInteractive variant='solid' arrow placement='right' title={props.hasContent ? desktopLegend : desktopLegendNoContent}>
<Button variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} endDecorator={<ChatBeamIcon />} sx={desktopSx}>
Beam
</Button>
</Tooltip>
);
}
@@ -21,7 +21,7 @@ const desktopSx: SxProps = {
export const ButtonCallMemo = React.memo(ButtonCall);
export function ButtonCall(props: { isMobile?: boolean, disabled?: boolean, onClick: () => void }) {
function ButtonCall(props: { isMobile?: boolean, disabled?: boolean, onClick: () => void }) {
return props.isMobile ? (
<IconButton variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
<CallIcon />
@@ -22,7 +22,7 @@ export function ButtonMultiChat(props: { isMobile?: boolean, multiChat: boolean,
<FormControl orientation='horizontal' sx={{ minHeight: '2.25rem', justifyContent: 'space-between' }}>
<FormLabel sx={{ gap: 1, flexFlow: 'row nowrap' }}>
<Box sx={{ display: { xs: 'none', lg: 'inline-block' } }}>
{multiChat ? <ChatMulticastOnIcon sx={{ color: 'warning.solidBg' }} /> : <ChatMulticastOffIcon />}
{multiChat ? <ChatMulticastOnIcon color='primary' /> : <ChatMulticastOffIcon />}
</Box>
{multiChat ? 'Multichat · On' : 'Multichat'}
</FormLabel>
@@ -2,13 +2,13 @@ import * as React from 'react';
import { Button, IconButton } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => void, sx?: SxProps }) {
return props.isMobile ? (
<IconButton variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
<FormatPaintIcon />
<FormatPaintTwoToneIcon />
</IconButton>
) : (
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
@@ -9,6 +9,13 @@ export type ComposerOutputPart = {
// TODO: not implemented yet
type: 'image-part',
base64Url: string,
metadata: {
title?: string,
generatedBy?: string,
altText?: string,
width?: number,
height?: number,
},
collapsible: false,
};
@@ -1,15 +1,16 @@
import * as React from 'react';
import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd';
import type { SxProps } from '@mui/joy/styles/types';
import { List, ListItem, ListItemButton, ListItemDecorator, Sheet } from '@mui/joy';
import FolderIcon from '@mui/icons-material/Folder';
import { ContentScaling, themeScalingMap } from '~/common/app.theme';
import { DFolder, useFolderStore } from '~/common/state/store-folders';
import { StrictModeDroppable } from '~/common/components/StrictModeDroppable';
import { AddFolderButton } from './AddFolderButton';
import { FolderListItem } from './FolderListItem';
import { StrictModeDroppable } from './StrictModeDroppable';
export function ChatFolderList(props: {
@@ -17,6 +18,7 @@ export function ChatFolderList(props: {
contentScaling: ContentScaling;
activeFolderId: string | null;
onFolderSelect: (folderId: string | null) => void;
sx?: SxProps;
}) {
// derived props
@@ -31,13 +33,18 @@ export function ChatFolderList(props: {
return (
<Sheet variant='soft' sx={{ p: 2 }}>
<Sheet variant='soft' sx={props.sx}>
<List
variant='plain'
sx={(theme) => ({
// added to be responsive to parent's layout sizing
height: '100%',
overflowY: 'auto',
// original list properties
'& ul': {
'--List-gap': '0px',
bgcolor: 'background.surface',
bgcolor: 'background.popup',
'& > li:first-of-type > [role="button"]': {
borderTopRightRadius: 'var(--List-radius)',
borderTopLeftRadius: 'var(--List-radius)',
@@ -131,6 +138,6 @@ export function ChatFolderList(props: {
</ListItem>
</List>
</Sheet>
</Sheet>
);
}
@@ -5,7 +5,7 @@ import { FormLabel, IconButton, ListItem, ListItemButton, ListItemContent, ListI
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import Done from '@mui/icons-material/Done';
import EditIcon from '@mui/icons-material/Edit';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import FolderIcon from '@mui/icons-material/Folder';
import MoreVertIcon from '@mui/icons-material/MoreVert';
@@ -36,8 +36,9 @@ export function FolderListItem(props: {
// Menu
const handleMenuOpen = (event: React.MouseEvent<HTMLAnchorElement>) => {
setMenuAnchorEl(event.currentTarget);
const handleMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
setMenuAnchorEl(anchor => anchor ? null : event.currentTarget);
setDeleteArmed(false); // Reset delete armed state
};
@@ -188,9 +189,11 @@ export function FolderListItem(props: {
{/* Icon to show the Popup menu */}
<IconButton
size='sm'
variant='outlined'
className='menu-icon'
onClick={handleMenuOpen}
onClick={handleMenuToggle}
onContextMenu={handleMenuToggle}
sx={{
visibility: 'hidden',
my: '-0.25rem', /* absorb the button padding */
@@ -214,7 +217,7 @@ export function FolderListItem(props: {
}}
>
<ListItemDecorator>
<EditIcon />
<EditRoundedIcon />
</ListItemDecorator>
Edit
</MenuItem>
@@ -1,22 +0,0 @@
import { useEffect, useState } from "react";
import { Droppable, DroppableProps } from "react-beautiful-dnd";
export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
const [enabled, setEnabled] = useState(false);
useEffect(() => {
const animation = requestAnimationFrame(() => setEnabled(true));
return () => {
cancelAnimationFrame(animation);
setEnabled(false);
};
}, []);
if (!enabled) {
return null;
}
return <Droppable {...props}>{children}</Droppable>;
};
+417 -187
View File
@@ -1,21 +1,25 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';
import type { SxProps } from '@mui/joy/styles/types';
import { Avatar, Box, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import { Avatar, Box, ButtonGroup, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
import { ClickAwayListener, Popper } from '@mui/base';
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
import ClearIcon from '@mui/icons-material/Clear';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import DifferenceIcon from '@mui/icons-material/Difference';
import EditIcon from '@mui/icons-material/Edit';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import Face6Icon from '@mui/icons-material/Face6';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import FormatPaintOutlinedIcon from '@mui/icons-material/FormatPaintOutlined';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
import RecordVoiceOverOutlinedIcon from '@mui/icons-material/RecordVoiceOverOutlined';
import ReplayIcon from '@mui/icons-material/Replay';
import ReplyRoundedIcon from '@mui/icons-material/ReplyRounded';
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
import StarRoundedIcon from '@mui/icons-material/StarRounded';
import TelegramIcon from '@mui/icons-material/Telegram';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
@@ -26,32 +30,35 @@ import { useSanityTextDiffs } from '~/modules/blocks/RenderTextDiff';
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { DMessage } from '~/common/state/store-chats';
import { DMessage, DMessageUserFlag, messageHasUserFlag } from '~/common/state/store-chats';
import { InlineTextarea } from '~/common/components/InlineTextarea';
import { KeyStroke } from '~/common/components/KeyStroke';
import { Link } from '~/common/components/Link';
import { adjustContentScaling, themeScalingMap, themeZIndexPageBar } from '~/common/app.theme';
import { animationColorRainbow } from '~/common/util/animUtils';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { cssRainbowColorKeyframes, themeScalingMap } from '~/common/app.theme';
import { prettyBaseModel } from '~/common/util/modelUtils';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { ReplyToBubble } from './ReplyToBubble';
import { useChatShowTextDiff } from '../../store-app-chat';
// Enable the menu on text selection
const ENABLE_SELECTION_RIGHT_CLICK_MENU: boolean = true;
const ENABLE_SELECTION_RIGHT_CLICK_MENU = false;
const ENABLE_SELECTION_TOOLBAR = true;
const SELECTION_TOOLBAR_MIN_LENGTH = 3;
// Enable the hover button to copy the whole message. The Copy button is also available in Blocks, or in the Avatar Menu.
const ENABLE_COPY_MESSAGE_OVERLAY: boolean = false;
export function messageBackground(messageRole: DMessage['role'] | string, wasEdited: boolean, unknownAssistantIssue: boolean): string {
export function messageBackground(messageRole: DMessage['role'] | string, wasEdited: boolean, isAssistantIssue: boolean): string {
switch (messageRole) {
case 'user':
return 'primary.plainHoverBg'; // was .background.level1
case 'assistant':
return unknownAssistantIssue ? 'danger.softBg' : 'background.surface';
return isAssistantIssue ? 'danger.softBg' : 'background.surface';
case 'system':
return wasEdited ? 'warning.softHoverBg' : 'neutral.softBg';
default:
@@ -59,7 +66,26 @@ export function messageBackground(messageRole: DMessage['role'] | string, wasEdi
}
}
const avatarIconSx = { width: 36, height: 36 };
const avatarIconSx = {
width: 36,
height: 36,
};
const personaSx: SxProps = {
// make this stick to the top of the screen
position: 'sticky',
top: 0,
// flexBasis: 0, // this won't let the item grow
minWidth: { xs: 50, md: 64 },
maxWidth: 80,
textAlign: 'center',
// layout
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
};
export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['role'] | string, messageOriginLLM: string | undefined, messagePurposeId: SystemPurposeId | undefined, messageSender: string, messageTyping: boolean, size: 'sm' | undefined = undefined): React.JSX.Element {
if (typeof messageAvatar === 'string' && messageAvatar)
@@ -75,6 +101,7 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
case 'assistant':
// typing gif (people seem to love this, so keeping it after april fools')
const isDownload = messageOriginLLM === 'web';
const isTextToImage = messageOriginLLM === 'DALL·E' || messageOriginLLM === 'Prodia';
const isReact = messageOriginLLM?.startsWith('react-');
@@ -82,17 +109,18 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
if (messageTyping)
return <Avatar
alt={messageSender} variant='plain'
src={isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp' // brush
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp' // mind
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'} // typing
src={isDownload ? 'https://i.giphy.com/26u6dIwIphLj8h10A.webp' // hourglass: https://i.giphy.com/TFSxpAIYz5inJGuY8f.webp, small-lq: https://i.giphy.com/131tNuGktpXGhy.webp, floppy: https://i.giphy.com/RxR1KghIie2iI.webp
: isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp' // brush
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp' // mind
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'} // typing
sx={{ ...mascotSx, borderRadius: 'sm' }}
/>;
// icon: text-to-image
if (isTextToImage)
return <FormatPaintIcon sx={{
return <FormatPaintOutlinedIcon sx={{
...avatarIconSx,
animation: `${cssRainbowColorKeyframes} 1s linear 2.66`,
animation: `${animationColorRainbow} 1s linear 2.66`,
}} />;
// purpose symbol (if present)
@@ -192,12 +220,19 @@ export function ChatMessage(props: {
isBottom?: boolean,
isImagining?: boolean,
isSpeaking?: boolean,
blocksShowDate?: boolean,
onConversationBranch?: (messageId: string) => void,
onConversationRestartFrom?: (messageId: string, offset: number, chatEffectBeam: boolean) => Promise<void>,
onConversationTruncate?: (messageId: string) => void,
showAvatar?: boolean, // auto if undefined
showBlocksDate?: boolean,
showUnsafeHtml?: boolean,
adjustContentScaling?: number,
topDecorator?: React.ReactNode,
onMessageAssistantFrom?: (messageId: string, offset: number) => Promise<void>,
onMessageBeam?: (messageId: string) => Promise<void>,
onMessageBranch?: (messageId: string) => void,
onMessageDelete?: (messageId: string) => void,
onMessageEdit?: (messageId: string, text: string) => void,
onMessageToggleUserFlag?: (messageId: string, flag: DMessageUserFlag) => void,
onMessageTruncate?: (messageId: string) => void,
onReplyTo?: (messageId: string, selectedText: string) => void,
onTextDiagram?: (messageId: string, text: string) => Promise<void>
onTextImagine?: (text: string) => Promise<void>
onTextSpeak?: (text: string) => Promise<void>
@@ -205,20 +240,21 @@ export function ChatMessage(props: {
}) {
// state
const blocksRendererRef = React.useRef<HTMLDivElement>(null);
const [isHovering, setIsHovering] = React.useState(false);
const [opsMenuAnchor, setOpsMenuAnchor] = React.useState<HTMLElement | null>(null);
const [selMenuAnchor, setSelMenuAnchor] = React.useState<HTMLElement | null>(null);
const [selMenuText, setSelMenuText] = React.useState<string | null>(null);
const [selToolbarAnchor, setSelToolbarAnchor] = React.useState<HTMLElement | null>(null);
const [selText, setSelText] = React.useState<string | null>(null);
const [isEditing, setIsEditing] = React.useState(false);
// external state
const labsChatBeam = useUXLabsStore(state => state.labsChatBeam);
const { cleanerLooks, contentScaling, doubleClickToEdit, renderMarkdown } = useUIPreferencesStore(state => ({
cleanerLooks: state.zenMode === 'cleaner',
contentScaling: state.contentScaling,
const { showAvatar, contentScaling, doubleClickToEdit, renderMarkdown } = useUIPreferencesStore(useShallow(state => ({
showAvatar: props.showAvatar !== undefined ? props.showAvatar : state.zenMode !== 'cleaner',
contentScaling: adjustContentScaling(state.contentScaling, props.adjustContentScaling),
doubleClickToEdit: state.doubleClickToEdit,
renderMarkdown: state.renderMarkdown,
}), shallow);
})));
const [showDiff, setShowDiff] = useChatShowTextDiff();
const textDiffs = useSanityTextDiffs(props.message.text, props.diffPreviousText, showDiff);
@@ -232,20 +268,21 @@ export function ChatMessage(props: {
role: messageRole,
purposeId: messagePurposeId,
originLLM: messageOriginLLM,
metadata: messageMetadata,
created: messageCreated,
updated: messageUpdated,
} = props.message;
const isUserStarred = messageHasUserFlag(props.message, 'starred');
const fromAssistant = messageRole === 'assistant';
const fromSystem = messageRole === 'system';
const wasEdited = !!messageUpdated;
const showAvatars = !cleanerLooks;
const textSel = selMenuText ? selMenuText : messageText;
const textSel = selText ? selText : messageText;
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 couldDiagram = textSel.length >= 100 && !isSpecialT2I;
const couldImagine = textSel.length >= 3 && !isSpecialT2I;
const couldSpeak = couldImagine;
@@ -258,39 +295,51 @@ export function ChatMessage(props: {
// Operations Menu
const closeOpsMenu = () => setOpsMenuAnchor(null);
const { onMessageToggleUserFlag } = props;
const handleOpsMenuToggle = React.useCallback((event: React.MouseEvent<HTMLElement>) => {
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
setOpsMenuAnchor(anchor => anchor ? null : event.currentTarget);
}, []);
const handleCloseOpsMenu = React.useCallback(() => setOpsMenuAnchor(null), []);
const handleOpsCopy = (e: React.MouseEvent) => {
copyToClipboard(textSel, 'Text');
e.preventDefault();
closeOpsMenu();
handleCloseOpsMenu();
closeSelectionMenu();
closeToolbar();
};
const handleOpsEdit = React.useCallback((e: React.MouseEvent) => {
if (messageTyping && !isEditing) return; // don't allow editing while typing
setIsEditing(!isEditing);
e.preventDefault();
closeOpsMenu();
}, [isEditing, messageTyping]);
handleCloseOpsMenu();
}, [handleCloseOpsMenu, isEditing, messageTyping]);
const handleOpsConversationBranch = (e: React.MouseEvent) => {
const handleOpsToggleStarred = React.useCallback(() => {
onMessageToggleUserFlag?.(messageId, 'starred');
}, [messageId, onMessageToggleUserFlag]);
const handleOpsAssistantFrom = async (e: React.MouseEvent) => {
e.preventDefault();
handleCloseOpsMenu();
await props.onMessageAssistantFrom?.(messageId, fromAssistant ? -1 : 0);
};
const handleOpsBeamFrom = async (e: React.MouseEvent) => {
e.stopPropagation();
handleCloseOpsMenu();
await props.onMessageBeam?.(messageId);
};
const handleOpsBranch = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation(); // to try to not steal the focus from the banched conversation
props.onConversationBranch && props.onConversationBranch(messageId);
closeOpsMenu();
};
const handleOpsConversationRestartFrom = async (e: React.MouseEvent) => {
e.preventDefault();
closeOpsMenu();
props.onConversationRestartFrom && await props.onConversationRestartFrom(messageId, fromAssistant ? -1 : 0, false);
};
const handleOpsConversationRestartFromBeam = async (e: React.MouseEvent) => {
e.stopPropagation();
closeOpsMenu();
props.onConversationRestartFrom && labsChatBeam && await props.onConversationRestartFrom(messageId, fromAssistant ? -1 : 0, true);
props.onMessageBranch?.(messageId);
handleCloseOpsMenu();
};
const handleOpsToggleShowDiff = () => setShowDiff(!showDiff);
@@ -299,8 +348,9 @@ export function ChatMessage(props: {
e.preventDefault();
if (props.onTextDiagram) {
await props.onTextDiagram(messageId, textSel);
closeOpsMenu();
handleCloseOpsMenu();
closeSelectionMenu();
closeToolbar();
}
};
@@ -308,8 +358,19 @@ export function ChatMessage(props: {
e.preventDefault();
if (props.onTextImagine) {
await props.onTextImagine(textSel);
closeOpsMenu();
handleCloseOpsMenu();
closeSelectionMenu();
closeToolbar();
}
};
const handleOpsReplyTo = (e: React.MouseEvent) => {
e.preventDefault();
if (props.onReplyTo && textSel.trim().length >= SELECTION_TOOLBAR_MIN_LENGTH) {
props.onReplyTo(messageId, textSel.trim());
handleCloseOpsMenu();
closeSelectionMenu();
closeToolbar();
}
};
@@ -317,18 +378,19 @@ export function ChatMessage(props: {
e.preventDefault();
if (props.onTextSpeak) {
await props.onTextSpeak(textSel);
closeOpsMenu();
handleCloseOpsMenu();
closeSelectionMenu();
closeToolbar();
}
};
const handleOpsTruncate = (_e: React.MouseEvent) => {
props.onConversationTruncate && props.onConversationTruncate(messageId);
closeOpsMenu();
props.onMessageTruncate?.(messageId);
handleCloseOpsMenu();
};
const handleOpsDelete = (_e: React.MouseEvent) => {
props.onMessageDelete && props.onMessageDelete(messageId);
props.onMessageDelete?.(messageId);
};
@@ -359,17 +421,17 @@ export function ChatMessage(props: {
document.body.appendChild(anchorEl);
setSelMenuAnchor(anchorEl);
setSelMenuText(selectedText);
setSelText(selectedText);
}, [removeSelectionAnchor]);
const closeSelectionMenu = React.useCallback(() => {
// window.getSelection()?.removeAllRanges?.();
removeSelectionAnchor();
setSelMenuAnchor(null);
setSelMenuText(null);
setSelText(null);
}, [removeSelectionAnchor]);
const handleMouseUp = React.useCallback((event: MouseEvent) => {
const handleContextMenu = React.useCallback((event: MouseEvent) => {
const selection = window.getSelection();
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
@@ -380,16 +442,74 @@ export function ChatMessage(props: {
}, [openSelectionMenu]);
// Selection Toolbar
const closeToolbar = React.useCallback((anchorEl?: HTMLElement) => {
window.getSelection()?.removeAllRanges?.();
try {
const anchor = anchorEl || selToolbarAnchor;
anchor && document.body.removeChild(anchor);
} catch (e) {
// ignore...
}
setSelToolbarAnchor(null);
setSelText(null);
}, [selToolbarAnchor]);
const handleOpenToolbar = React.useCallback((_event: MouseEvent) => {
// check for selection
const selection = window.getSelection();
if (!selection || selection.rangeCount <= 0) return;
// check for enought selection
const selectionText = selection.toString().trim();
if (selectionText.length < SELECTION_TOOLBAR_MIN_LENGTH) return;
// check for the selection being inside the blocks renderer (core of the message)
const selectionRange = selection.getRangeAt(0);
const blocksElement = blocksRendererRef.current;
if (!blocksElement || !blocksElement.contains(selectionRange.commonAncestorContainer)) return;
const rangeRects = selectionRange.getClientRects();
if (rangeRects.length <= 0) return;
const firstRect = rangeRects[0];
const anchorEl = document.createElement('div');
anchorEl.style.position = 'fixed';
anchorEl.style.left = `${firstRect.left + window.scrollX}px`;
anchorEl.style.top = `${firstRect.top + window.scrollY}px`;
document.body.appendChild(anchorEl);
anchorEl.setAttribute('role', 'dialog');
// auto-close logic on unselect
const closeOnUnselect = () => {
const selection = window.getSelection();
if (!selection || selection.toString().trim() === '') {
closeToolbar(anchorEl);
document.removeEventListener('selectionchange', closeOnUnselect);
}
};
document.addEventListener('selectionchange', closeOnUnselect);
setSelToolbarAnchor(anchorEl);
setSelText(selectionText);
}, [closeToolbar]);
// Blocks renderer
const handleBlocksContextMenu = React.useCallback((event: React.MouseEvent) => {
handleMouseUp(event.nativeEvent);
}, [handleMouseUp]);
handleContextMenu(event.nativeEvent);
}, [handleContextMenu]);
const handleBlocksDoubleClick = React.useCallback((event: React.MouseEvent) => {
doubleClickToEdit && props.onMessageEdit && handleOpsEdit(event);
}, [doubleClickToEdit, handleOpsEdit, props.onMessageEdit]);
const handleBlocksMouseUp = React.useCallback((event: React.MouseEvent) => {
handleOpenToolbar(event.nativeEvent);
}, [handleOpenToolbar]);
// prettier upstream errors
const { isAssistantError, errorMessage } = React.useMemo(
@@ -402,92 +522,128 @@ export function ChatMessage(props: {
// avatar
const avatarEl: React.JSX.Element | null = React.useMemo(
() => showAvatars ? makeAvatar(messageAvatar, messageRole, messageOriginLLM, messagePurposeId, messageSender, messageTyping) : null,
[messageAvatar, messageOriginLLM, messagePurposeId, messageRole, messageSender, messageTyping, showAvatars],
() => showAvatar ? makeAvatar(messageAvatar, messageRole, messageOriginLLM, messagePurposeId, messageSender, messageTyping) : null,
[messageAvatar, messageOriginLLM, messagePurposeId, messageRole, messageSender, messageTyping, showAvatar],
);
return (
<ListItem
role='chat-message'
onMouseUp={(ENABLE_SELECTION_TOOLBAR && !fromSystem && !isAssistantError) ? handleBlocksMouseUp : undefined}
sx={{
display: 'flex', flexDirection: !fromAssistant ? 'row-reverse' : 'row', alignItems: 'flex-start',
gap: { xs: 0, md: 1 },
// style
backgroundColor: backgroundColor,
px: { xs: 1, md: themeScalingMap[contentScaling]?.chatMessagePadding ?? 2 },
py: themeScalingMap[contentScaling]?.chatMessagePadding ?? 2,
backgroundColor,
borderBottom: '1px solid',
borderBottomColor: 'divider',
...(ENABLE_COPY_MESSAGE_OVERLAY && { position: 'relative' }),
// style: omit border if set externally
...(!('borderBottom' in (props.sx || {})) && {
borderBottom: '1px solid',
borderBottomColor: 'divider',
}),
// style: when starred
...(isUserStarred && {
outline: '3px solid',
outlineColor: 'primary.solidBg',
boxShadow: 'lg',
borderRadius: 'lg',
zIndex: 1,
}),
// style: make room for a top decorator if set
'&:hover > button': { opacity: 1 },
// layout
display: 'block', // this is Needed, otherwise there will be a horizontal overflow
...props.sx,
}}
>
{/* Avatar */}
{showAvatars && (
<Box
onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}
onClick={event => setOpsMenuAnchor(event.currentTarget)}
sx={{
// flexBasis: 0, // this won't let the item grow
display: 'flex', flexDirection: 'column', alignItems: 'center',
minWidth: { xs: 50, md: 64 },
maxWidth: 80,
textAlign: 'center',
}}
>
{/* (Optional) underlayed top decorator */}
{props.topDecorator}
{isHovering ? (
<IconButton variant='soft' color={(fromAssistant || fromSystem) ? 'neutral' : 'primary'} sx={avatarIconSx}>
<MoreVertIcon />
</IconButton>
) : (
avatarEl
)}
{/* Message Row: Avatar, Blocks (1 text -> blocksRenderer) */}
<Box sx={{
display: 'flex',
flexDirection: !fromAssistant ? 'row-reverse' : 'row',
alignItems: 'flex-start',
gap: { xs: 0, md: 1 },
}}>
{/* Assistant model name */}
{fromAssistant && (
<Tooltip title={messageTyping ? null : (messageOriginLLM || 'unk-model')} variant='solid'>
<Typography level='body-xs' sx={{
overflowWrap: 'anywhere',
...(messageTyping ? { animation: `${cssRainbowColorKeyframes} 5s linear infinite` } : {}),
}}>
{prettyBaseModel(messageOriginLLM)}
</Typography>
</Tooltip>
)}
{/* Avatar (Persona) */}
{showAvatar && (
<Box sx={personaSx}>
</Box>
)}
{/* Persona Avatar or Menu Button */}
<Box
onClick={handleOpsMenuToggle}
onContextMenu={handleOpsMenuToggle}
onMouseEnter={() => setIsHovering(true)}
onMouseLeave={() => setIsHovering(false)}
sx={{ display: 'flex' }}
>
{(isHovering || opsMenuAnchor) ? (
<IconButton variant={opsMenuAnchor ? 'solid' : 'soft'} color={(fromAssistant || fromSystem) ? 'neutral' : 'primary'} sx={avatarIconSx}>
<MoreVertIcon />
</IconButton>
) : (
avatarEl
)}
</Box>
{/* Assistant model name */}
{fromAssistant && (
<Tooltip arrow title={messageTyping ? null : (messageOriginLLM || 'unk-model')} variant='solid'>
<Typography level='body-xs' sx={{
overflowWrap: 'anywhere',
...(messageTyping ? { animation: `${animationColorRainbow} 5s linear infinite` } : {}),
}}>
{prettyBaseModel(messageOriginLLM)}
</Typography>
</Tooltip>
)}
</Box>
)}
{/* Edit / Blocks */}
{isEditing ? (
{/* Edit / Blocks */}
{isEditing ? (
<InlineTextarea
initialText={messageText} onEdit={handleTextEdited}
sx={editBlocksSx}
/>
<InlineTextarea
initialText={messageText} onEdit={handleTextEdited}
sx={editBlocksSx}
/>
) : (
) : (
<BlocksRenderer
text={messageText}
fromRole={messageRole}
contentScaling={contentScaling}
errorMessage={errorMessage}
fitScreen={props.fitScreen}
isBottom={props.isBottom}
renderTextAsMarkdown={renderMarkdown}
renderTextDiff={textDiffs || undefined}
showDate={props.blocksShowDate === true ? messageUpdated || messageCreated || undefined : undefined}
wasUserEdited={wasEdited}
onContextMenu={(props.onMessageEdit && ENABLE_SELECTION_RIGHT_CLICK_MENU) ? handleBlocksContextMenu : undefined}
onDoubleClick={(props.onMessageEdit && doubleClickToEdit) ? handleBlocksDoubleClick : undefined}
optiAllowMemo={messageTyping}
/>
<BlocksRenderer
ref={blocksRendererRef}
text={messageText}
fromRole={messageRole}
contentScaling={contentScaling}
errorMessage={errorMessage}
fitScreen={props.fitScreen}
isBottom={props.isBottom}
renderTextAsMarkdown={renderMarkdown}
renderTextDiff={textDiffs || undefined}
showDate={props.showBlocksDate === true ? messageUpdated || messageCreated || undefined : undefined}
showUnsafeHtml={props.showUnsafeHtml}
wasUserEdited={wasEdited}
onContextMenu={(props.onMessageEdit && ENABLE_SELECTION_RIGHT_CLICK_MENU) ? handleBlocksContextMenu : undefined}
onDoubleClick={(props.onMessageEdit && doubleClickToEdit) ? handleBlocksDoubleClick : undefined}
optiAllowMemo={messageTyping}
/>
)}
)}
</Box>
{/* Reply-To Bubble */}
{!!messageMetadata?.inReplyToText && <ReplyToBubble inlineMessage replyToText={messageMetadata.inReplyToText} className='reply-to-bubble' />}
{/* Overlay copy icon */}
@@ -509,7 +665,7 @@ export function ChatMessage(props: {
{!!opsMenuAnchor && (
<CloseableMenu
dense placement='bottom-end'
open anchorEl={opsMenuAnchor} onClose={closeOpsMenu}
open anchorEl={opsMenuAnchor} onClose={handleCloseOpsMenu}
sx={{ minWidth: 280 }}
>
@@ -523,29 +679,33 @@ export function ChatMessage(props: {
{/* Edit / Copy */}
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/* Edit */}
{!!props.onMessageEdit && (
<MenuItem variant='plain' disabled={messageTyping} onClick={handleOpsEdit} sx={{ flex: 1 }}>
<ListItemDecorator><EditIcon /></ListItemDecorator>
<ListItemDecorator><EditRoundedIcon /></ListItemDecorator>
{isEditing ? 'Discard' : 'Edit'}
{/*{!isEditing && <span style={{ opacity: 0.5, marginLeft: '8px' }}>{doubleClickToEdit ? '(double-click)' : ''}</span>}*/}
</MenuItem>
)}
{/* Copy */}
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1 }}>
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
Copy
</MenuItem>
{/* Starred */}
{!!onMessageToggleUserFlag && (
<MenuItem onClick={handleOpsToggleStarred} sx={{ flexGrow: 0, px: 1 }}>
{isUserStarred
? <StarRoundedIcon color='primary' sx={{ fontSize: 'xl2' }} />
: <StarOutlineRoundedIcon sx={{ fontSize: 'xl2' }} />
}
</MenuItem>
)}
</Box>
{/* Delete / Branch / Truncate */}
{!!props.onMessageDelete && <ListDivider />}
{!!props.onMessageDelete && (
<MenuItem onClick={handleOpsDelete} disabled={false /*fromSystem*/}>
<ListItemDecorator><ClearIcon /></ListItemDecorator>
Delete
<span style={{ opacity: 0.5 }}>message</span>
</MenuItem>
)}
{!!props.onConversationBranch && (
<MenuItem onClick={handleOpsConversationBranch} disabled={fromSystem}>
{!!props.onMessageBranch && <ListDivider />}
{!!props.onMessageBranch && (
<MenuItem onClick={handleOpsBranch} disabled={fromSystem}>
<ListItemDecorator>
<ForkRightIcon />
</ListItemDecorator>
@@ -553,13 +713,40 @@ export function ChatMessage(props: {
{!props.isBottom && <span style={{ opacity: 0.5 }}>from here</span>}
</MenuItem>
)}
{!!props.onConversationTruncate && (
{!!props.onMessageDelete && (
<MenuItem onClick={handleOpsDelete} disabled={false /*fromSystem*/}>
<ListItemDecorator><ClearIcon /></ListItemDecorator>
Delete
<span style={{ opacity: 0.5 }}>message</span>
</MenuItem>
)}
{!!props.onMessageTruncate && (
<MenuItem onClick={handleOpsTruncate} disabled={props.isBottom}>
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
Truncate
<span style={{ opacity: 0.5 }}>after this</span>
</MenuItem>
)}
{/* Diagram / Draw / Speak */}
{!!props.onTextDiagram && <ListDivider />}
{!!props.onTextDiagram && (
<MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
<ListItemDecorator><AccountTreeOutlinedIcon /></ListItemDecorator>
Auto-Diagram ...
</MenuItem>
)}
{!!props.onTextImagine && (
<MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintOutlinedIcon />}</ListItemDecorator>
Auto-Draw
</MenuItem>
)}
{!!props.onTextSpeak && (
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverOutlinedIcon />}</ListItemDecorator>
Speak
</MenuItem>
)}
{/* Diff Viewer */}
{!!props.diffPreviousText && <ListDivider />}
{!!props.diffPreviousText && (
@@ -569,56 +756,98 @@ export function ChatMessage(props: {
<Switch checked={showDiff} onChange={handleOpsToggleShowDiff} sx={{ ml: 'auto' }} />
</MenuItem>
)}
{/* Diagram / Draw / Speak */}
{!!props.onTextDiagram && <ListDivider />}
{!!props.onTextDiagram && (
<MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
<ListItemDecorator><AccountTreeIcon color='success' /></ListItemDecorator>
Diagram ...
</MenuItem>
)}
{!!props.onTextImagine && (
<MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
Draw ...
</MenuItem>
)}
{!!props.onTextSpeak && (
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
Speak
</MenuItem>
)}
{/* Restart/try */}
{!!props.onConversationRestartFrom && <ListDivider />}
{!!props.onConversationRestartFrom && (
<MenuItem onClick={handleOpsConversationRestartFrom}>
{/* Beam/Restart */}
{(!!props.onMessageAssistantFrom || !!props.onMessageBeam) && <ListDivider />}
{!!props.onMessageAssistantFrom && (
<MenuItem disabled={fromSystem} onClick={handleOpsAssistantFrom}>
<ListItemDecorator>{fromAssistant ? <ReplayIcon color='primary' /> : <TelegramIcon color='primary' />}</ListItemDecorator>
{!fromAssistant
? <>Restart <span style={{ opacity: 0.5 }}>from here</span></>
: !props.isBottom
? <>Retry <span style={{ opacity: 0.5 }}>from here</span></>
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
Retry
<KeyStroke combo='Ctrl + Shift + R' />
</Box>}
{labsChatBeam && (
<Tooltip title={messageTyping ? null : 'Best-Of'}>
<IconButton
size='sm'
variant='outlined' color='primary'
onClick={handleOpsConversationRestartFromBeam}
sx={{ ml: 'auto', my: '-0.25rem' /* absorb the menuItem padding */ }}
>
<ChatBeamIcon /> {/*<GavelIcon />*/}
</IconButton>
</Tooltip>
)}
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>Retry<KeyStroke combo='Ctrl + Shift + R' /></Box>}
</MenuItem>
)}
{!!props.onMessageBeam && (
<MenuItem disabled={fromSystem} onClick={handleOpsBeamFrom}>
<ListItemDecorator>
<ChatBeamIcon color={fromSystem ? undefined : 'primary'} />
</ListItemDecorator>
{!fromAssistant
? <>Beam <span style={{ opacity: 0.5 }}>from here</span></>
: !props.isBottom
? <>Beam <span style={{ opacity: 0.5 }}>this message</span></>
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>Beam<KeyStroke combo='Ctrl + Shift + B' /></Box>}
</MenuItem>
)}
</CloseableMenu>
)}
{/* Selection Toolbar */}
{ENABLE_SELECTION_TOOLBAR && !!selToolbarAnchor && (
<Popper placement='top-start' open anchorEl={selToolbarAnchor} slotProps={{
root: { style: { zIndex: themeZIndexPageBar + 1 } },
}}>
<ClickAwayListener onClickAway={() => closeToolbar()}>
<ButtonGroup
variant='plain'
sx={{
'--ButtonGroup-separatorColor': 'none !important',
'--ButtonGroup-separatorSize': 0,
borderRadius: '0',
backgroundColor: 'background.popup',
border: '1px solid',
borderColor: 'primary.outlinedBorder',
boxShadow: '0px 4px 12px -4px rgb(var(--joy-palette-neutral-darkChannel) / 50%)',
mb: 1,
ml: -1,
alignItems: 'center',
'& > button': {
'--Icon-fontSize': '1rem',
minHeight: '2.5rem',
minWidth: '2.75rem',
},
}}
>
{!!props.onReplyTo && fromAssistant && <Tooltip disableInteractive arrow placement='top' title='Reply'>
<IconButton color='primary' onClick={handleOpsReplyTo}>
<ReplyRoundedIcon sx={{ fontSize: 'xl' }} />
</IconButton>
</Tooltip>}
{/*{!!props.onMessageBeam && fromAssistant && <Tooltip disableInteractive arrow placement='top' title='Beam'>*/}
{/* <IconButton color='primary'>*/}
{/* <ChatBeamIcon sx={{ fontSize: 'xl' }} />*/}
{/* </IconButton>*/}
{/*</Tooltip>}*/}
{!!props.onReplyTo && fromAssistant && <MoreVertIcon sx={{ color: 'neutral.outlinedBorder', fontSize: 'md' }} />}
<Tooltip disableInteractive arrow placement='top' title='Copy'>
<IconButton onClick={handleOpsCopy}>
<ContentCopyIcon />
</IconButton>
</Tooltip>
{(!!props.onTextDiagram || !!props.onTextSpeak) && <MoreVertIcon sx={{ color: 'neutral.outlinedBorder', fontSize: 'md' }} />}
{!!props.onTextDiagram && <Tooltip disableInteractive arrow placement='top' title={couldDiagram ? 'Auto-Diagram...' : 'Too short to Auto-Diagram'}>
<IconButton onClick={couldDiagram ? handleOpsDiagram : undefined}>
<AccountTreeOutlinedIcon sx={{ color: couldDiagram ? 'primary' : 'neutral.plainDisabledColor' }} />
</IconButton>
</Tooltip>}
{/*{!!props.onTextImagine && <Tooltip disableInteractive arrow placement='top' title='Auto-Draw'>*/}
{/* <IconButton onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>*/}
{/* {!props.isImagining ? <FormatPaintOutlinedIcon /> : <CircularProgress sx={{ '--CircularProgress-size': '16px' }} />}*/}
{/* </IconButton>*/}
{/*</Tooltip>}*/}
{!!props.onTextSpeak && <Tooltip disableInteractive arrow placement='top' title='Speak'>
<IconButton onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
{!props.isSpeaking ? <RecordVoiceOverOutlinedIcon /> : <CircularProgress sx={{ '--CircularProgress-size': '16px' }} />}
</IconButton>
</Tooltip>}
</ButtonGroup>
</ClickAwayListener>
</Popper>
)}
{/* Selection (Contextual) Menu */}
{!!selMenuAnchor && (
<CloseableMenu
@@ -626,20 +855,21 @@ export function ChatMessage(props: {
open anchorEl={selMenuAnchor} onClose={closeSelectionMenu}
sx={{ minWidth: 220 }}
>
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1 }}>
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1, alignItems: 'center' }}>
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
Copy <span style={{ opacity: 0.5 }}>selection</span>
Copy
</MenuItem>
{!!props.onTextDiagram && <ListDivider />}
{!!props.onTextDiagram && <MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram || props.isImagining}>
<ListItemDecorator><AccountTreeIcon color='success' /></ListItemDecorator>
Diagram ...
<ListItemDecorator><AccountTreeOutlinedIcon /></ListItemDecorator>
Auto-Diagram ...
</MenuItem>}
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
Imagine
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintOutlinedIcon />}</ListItemDecorator>
Auto-Draw
</MenuItem>}
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverOutlinedIcon />}</ListItemDecorator>
Speak
</MenuItem>}
</CloseableMenu>
@@ -16,7 +16,7 @@ import { makeAvatar, messageBackground } from './ChatMessage';
export const MessagesSelectionHeader = (props: { hasSelected: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
<Sheet color='warning' variant='solid' invertedColors sx={{
display: 'flex', flexDirection: 'row', alignItems: 'center',
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101,
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101 /* Cleanup Selection Header on top of messages */,
boxShadow: 'md',
gap: { xs: 1, sm: 2 }, px: { xs: 1, md: 2 }, py: 1,
}}>
@@ -0,0 +1,85 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, IconButton, Tooltip, Typography } from '@mui/joy';
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
import ReplyRoundedIcon from '@mui/icons-material/ReplyRounded';
// configuration
const INLINE_COLOR = 'primary';
const bubbleComposerSx: SxProps = {
// contained
width: '100%',
zIndex: 2, // stays on top of the 'tokens' bubble in the composer
// style
backgroundColor: 'background.surface',
border: '1px solid',
borderColor: 'neutral.outlinedBorder',
borderRadius: 'sm',
boxShadow: 'xs',
padding: '0.5rem 0.25rem 0.5rem 0.5rem',
// layout
display: 'flex',
alignItems: 'start',
};
const inlineMessageSx: SxProps = {
...bubbleComposerSx,
// redefine
// border: 'none',
mt: 1,
borderColor: `${INLINE_COLOR}.outlinedColor`,
borderRadius: 'sm',
boxShadow: 'xs',
width: undefined,
padding: '0.375rem 0.25rem 0.375rem 0.5rem',
// self-layout (parent: 'block', as 'grid' was not working and the user would scroll the app on the x-axis on mobile)
// ml: 'auto',
float: 'inline-end',
mr: { xs: 7.75, md: 10.5 }, // personaSx.minWidth + gap (md: 1) + 1.5 (text margin)
};
export function ReplyToBubble(props: {
replyToText: string | null,
inlineMessage?: boolean
onClear?: () => void,
className?: string,
}) {
return (
<Box className={props.className} sx={!props.inlineMessage ? bubbleComposerSx : inlineMessageSx}>
<Tooltip disableInteractive arrow title='Referring to this assistant text' placement='top'>
<ReplyRoundedIcon sx={{
color: props.inlineMessage ? `${INLINE_COLOR}.outlinedColor` : 'primary.solidBg',
fontSize: 'xl',
mt: 0.125,
}} />
</Tooltip>
<Typography level='body-sm' sx={{
flex: 1,
ml: 1,
mr: 0.5,
overflow: 'auto',
maxHeight: '5.75rem',
lineHeight: 'xl',
color: /*props.inlineMessage ? 'text.tertiary' :*/ 'text.secondary',
whiteSpace: 'break-spaces', // 'balance'
}}>
{props.replyToText}
</Typography>
{!!props.onClear && (
<IconButton size='sm' onClick={props.onClear} sx={{ my: -0.5, background: 'none' }}>
<CloseRoundedIcon />
</IconButton>
)}
</Box>
);
}
@@ -1,7 +1,8 @@
import * as React from 'react';
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';
import { v4 as uuidv4 } from 'uuid';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
@@ -9,25 +10,36 @@ import { DConversationId, useChatStore } from '~/common/state/store-chats';
// change this to increase/decrease the number history steps per pane
const MAX_HISTORY_LENGTH = 10;
// change this to allow for more/less panes
const MAX_CONCURRENT_PANES = 4;
// change to true to enable verbose console logging
const DEBUG_PANES_MANAGER = false;
interface ChatPane {
paneId: string;
conversationId: DConversationId | null;
// other per-pane storage? or would this be cluttering the panes(view)-only abstaction?
// ... we are currently creating companion ConversationHandler obects for this
history: DConversationId[]; // History of the conversationIds for this pane
historyIndex: number; // Current position in the history for this pane
}
interface AppChatPanesStore {
interface AppChatPanesState {
// state
chatPanes: ChatPane[];
chatPaneFocusIndex: number | null;
}
interface AppChatPanesStore extends AppChatPanesState {
// actions
openConversationInFocusedPane: (conversationId: DConversationId) => void;
openConversationInSplitPane: (conversationId: DConversationId) => void;
@@ -35,19 +47,29 @@ interface AppChatPanesStore {
duplicateFocusedPane: (/*paneIndex: number*/) => void;
removeOtherPanes: () => void;
removePane: (paneIndex: number) => void;
setFocusedPane: (paneIndex: number) => void;
onConversationsChanged: (conversationIds: DConversationId[]) => void;
setFocusedPaneIndex: (paneIndex: number) => void;
_onConversationsChanged: (conversationIds: DConversationId[]) => void;
}
function createPane(conversationId: DConversationId | null = null): ChatPane {
return {
paneId: uuidv4(),
conversationId,
history: conversationId ? [conversationId] : [],
historyIndex: conversationId ? 0 : -1,
};
}
function duplicatePane(pane: ChatPane): ChatPane {
return {
paneId: uuidv4(),
conversationId: pane.conversationId,
history: [...pane.history],
historyIndex: pane.historyIndex,
};
}
const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
(_set, _get) => ({
@@ -68,8 +90,14 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
};
}
// Check if the conversation is already open in the focused pane.
// Sanity check: Get the focused pane
const focusedPane = chatPanes[chatPaneFocusIndex];
if (!focusedPane) {
console.warn('openConversationInFocusedPane: focusedPane is null', chatPaneFocusIndex, chatPanes);
return state;
}
// Check if the conversation is already open in the focused pane.
if (focusedPane.conversationId === conversationId) {
if (DEBUG_PANES_MANAGER)
console.log(`open-focuses: ${conversationId} is open in focused pane`, chatPaneFocusIndex, chatPanes);
@@ -80,7 +108,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
const truncatedHistory = focusedPane.history.slice(0, focusedPane.historyIndex + 1);
const newHistory = [...truncatedHistory, conversationId].slice(-MAX_HISTORY_LENGTH);
// Update the focused pane with the new conversation.
// Update the focused pane with the new conversation and history.
const newPanes = [...chatPanes];
newPanes[chatPaneFocusIndex] = {
...focusedPane,
@@ -103,21 +131,30 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
// Open a conversation in a new pane, reusing an existing pane if possible.
const { chatPanes, chatPaneFocusIndex, openConversationInFocusedPane } = _get();
// one pane open: split it
if (chatPanes.length === 1) {
_set({
chatPanes: Array.from({ length: 2 }, () => ({ ...chatPanes[0] })),
chatPaneFocusIndex: 1,
});
// Copy from the focused pane, if there's one
const focusedPane = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex] ?? null : null;
// if fewer than the maximum panes, create a new pane and focus it
if (chatPanes.length < MAX_CONCURRENT_PANES) {
const insertIndex = chatPaneFocusIndex !== null ? chatPaneFocusIndex + 1 : chatPanes.length;
_set((state) => ({
chatPanes: [
...state.chatPanes.slice(0, insertIndex),
focusedPane ? duplicatePane(focusedPane) : createPane(null),
...state.chatPanes.slice(insertIndex),
],
chatPaneFocusIndex: insertIndex,
}));
}
// more than 2 panes, reuse the alt pane
else if (chatPanes.length >= 2 && chatPaneFocusIndex !== null) {
// max reached, replace the next pane (with wraparound) - note the outside logic won't get us here
else {
const replaceIndex = (chatPaneFocusIndex !== null ? chatPaneFocusIndex + 1 : 0) % MAX_CONCURRENT_PANES;
_set({
chatPaneFocusIndex: chatPaneFocusIndex === 0 ? 1 : 0,
chatPaneFocusIndex: replaceIndex,
});
}
// will create a pane if none exists, or load the conversation in the focused pane
// Open the conversation in the newly created or updated pane
openConversationInFocusedPane(conversationId);
if (DEBUG_PANES_MANAGER)
@@ -171,21 +208,18 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
// Clone the pane at the specified index, including a deep copy of the history array
const paneToDuplicate = chatPanes[_srcIndex];
const duplicatedPane = {
...paneToDuplicate,
history: [...paneToDuplicate.history], // Deep copy of the history array
};
const dstIndex = _srcIndex + 1;
// Insert the duplicated pane into the array, right after the original pane
const newPanes = [
...chatPanes.slice(0, _srcIndex + 1),
duplicatedPane,
...chatPanes.slice(_srcIndex + 1),
...chatPanes.slice(0, dstIndex),
duplicatePane(paneToDuplicate),
...chatPanes.slice(dstIndex),
];
return {
chatPanes: newPanes,
chatPaneFocusIndex: _srcIndex + 1,
chatPaneFocusIndex: dstIndex,
};
}),
@@ -217,7 +251,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
};
}),
setFocusedPane: (paneIndex: number) =>
setFocusedPaneIndex: (paneIndex: number) =>
_set(state => {
if (state.chatPaneFocusIndex === paneIndex)
return state;
@@ -232,7 +266,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
* It takes care of `creating the first pane` as well as `removing invalid history items, reassiging
* conversationIds, and re-focusing the pane`.
*/
onConversationsChanged: (conversationIds: DConversationId[]) =>
_onConversationsChanged: (conversationIds: DConversationId[]) =>
_set(state => {
const { chatPanes, chatPaneFocusIndex } = state;
@@ -284,7 +318,8 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
}),
}), {
name: 'app-app-chat-panes',
// note: added the '-2' suffix on 20240308 to invalidate the persisted state, as we are adding a paneId
name: 'app-app-chat-panes-2',
},
));
@@ -294,40 +329,29 @@ export function getInstantAppChatPanesCount() {
export function usePanesManager() {
// use Panes
const { onConversationsChanged, ...panesFunctions } = useAppChatPanesStore(state => {
const {
chatPaneFocusIndex,
chatPanes,
navigateHistoryInFocusedPane,
onConversationsChanged,
openConversationInFocusedPane,
openConversationInSplitPane,
removePane,
setFocusedPane,
} = state;
const focusedConversationId = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex]?.conversationId ?? null : null;
return {
chatPanes: chatPanes as Readonly<ChatPane[]>,
focusedConversationId,
navigateHistoryInFocusedPane,
onConversationsChanged,
openConversationInFocusedPane,
openConversationInSplitPane,
focusedPaneIndex: chatPaneFocusIndex,
removePane,
setFocusedPane,
};
}, shallow);
const { _onConversationsChanged, ...panesFunctions } = useAppChatPanesStore(useShallow(state => ({
// state
chatPanes: state.chatPanes as Readonly<ChatPane[]>,
focusedPaneIndex: state.chatPaneFocusIndex,
focusedPaneConversationId: state.chatPaneFocusIndex !== null ? state.chatPanes[state.chatPaneFocusIndex]?.conversationId ?? null : null,
// methods
openConversationInFocusedPane: state.openConversationInFocusedPane,
openConversationInSplitPane: state.openConversationInSplitPane,
navigateHistoryInFocusedPane: state.navigateHistoryInFocusedPane,
removePane: state.removePane,
setFocusedPaneIndex: state.setFocusedPaneIndex,
_onConversationsChanged: state._onConversationsChanged,
})));
// use Conversation IDs[]
const conversationIDs: DConversationId[] = useChatStore(state => {
return state.conversations.map(_c => _c.id);
}, shallow);
const conversationIDs: DConversationId[] = useChatStore(useShallow(state =>
state.conversations.map(_c => _c.id),
));
// [Effect] Ensure all Panes have a valid Conversation ID
React.useEffect(() => {
onConversationsChanged(conversationIDs);
}, [conversationIDs, onConversationsChanged]);
_onConversationsChanged(conversationIDs);
}, [conversationIDs, _onConversationsChanged]);
return {
...panesFunctions,
@@ -335,10 +359,12 @@ export function usePanesManager() {
}
export function usePaneDuplicateOrClose() {
return useAppChatPanesStore(state => ({
canAddPane: state.chatPanes.length < 4,
return useAppChatPanesStore(useShallow(state => ({
// state
canAddPane: state.chatPanes.length < MAX_CONCURRENT_PANES,
isMultiPane: state.chatPanes.length > 1,
// actions
duplicateFocusedPane: state.duplicateFocusedPane,
removeOtherPanes: state.removeOtherPanes,
}), shallow);
})));
}
@@ -1,34 +1,38 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { v4 as uuidv4 } from 'uuid';
import type { SxProps } from '@mui/joy/styles/types';
import { Alert, Avatar, Box, Button, Card, CardContent, Checkbox, IconButton, Input, List, ListItem, ListItemButton, Textarea, Tooltip, Typography } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import DoneIcon from '@mui/icons-material/Done';
import EditIcon from '@mui/icons-material/Edit';
import EditRoundedIcon from '@mui/icons-material/EditRounded';
import EditNoteIcon from '@mui/icons-material/EditNote';
import SearchIcon from '@mui/icons-material/Search';
import TelegramIcon from '@mui/icons-material/Telegram';
import { SystemPurposeData, SystemPurposeId, SystemPurposes } from '../../../../data';
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
import { useChatLLM } from '~/modules/llms/store-llms';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { DConversationId, DMessage, useChatStore } from '~/common/state/store-chats';
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
import { lineHeightTextareaMd } from '~/common/app.theme';
import { navigateToPersonas } from '~/common/app.routes';
import { useChipBoolean } from '~/common/components/useChipBoolean';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { SystemPurposeData, SystemPurposeId, SystemPurposes } from '../../../../data';
import { YouTubeURLInput } from './YouTubeURLInput';
import { usePurposeStore } from './store-purposes';
// 'special' purpose IDs, for tile hiding purposes
const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__';
const TILE_ACTIVE_COLOR = 'primary' as const;
// defined looks
const tileSize = 7.5; // rem
const tileSize = 7; // rem
const tileGap = 0.5; // rem
@@ -46,32 +50,36 @@ function Tile(props: {
return (
<Button
variant={(!props.isEditMode && props.isActive) ? 'solid' : props.isHighlighted ? 'soft' : 'soft'}
color={(!props.isEditMode && props.isActive) ? 'primary' : props.isHighlighted ? 'primary' : 'neutral'}
color={(!props.isEditMode && props.isActive) ? 'primary' : props.isHighlighted ? 'primary' : TILE_ACTIVE_COLOR}
onClick={props.onClick}
sx={{
aspectRatio: 1,
height: `${tileSize}rem`,
fontWeight: 'md',
lineHeight: 'xs',
...((props.isEditMode || !props.isActive) ? {
boxShadow: props.isHighlighted ? '0 2px 8px -2px rgb(var(--joy-palette-primary-mainChannel) / 50%)' : 'sm',
backgroundColor: props.isHighlighted ? undefined : 'background.surface',
...(props.imageUrl && {
backgroundImage: `linear-gradient(rgba(255 255 255 /0.85), rgba(255 255 255 /1)), url(${props.imageUrl})`,
backgroundPosition: 'center',
backgroundSize: 'cover',
'&:hover': {
backgroundImage: 'none',
},
}),
boxShadow: `0 2px 8px -3px rgb(var(--joy-palette-${TILE_ACTIVE_COLOR}-darkChannel) / 30%)`,
// boxShadow: props.isHighlighted
// ? '0 2px 8px -2px rgb(var(--joy-palette-primary-darkChannel) / 30%)'
// : 'sm',
backgroundColor: props.isHighlighted ? undefined : 'background.popup',
// ...(props.imageUrl && {
// backgroundImage: `linear-gradient(rgba(255 255 255 /0.85), rgba(255 255 255 /1)), url(${props.imageUrl})`,
// backgroundPosition: 'center',
// backgroundSize: 'cover',
// '&:hover': {
// backgroundImage: 'none',
// },
// }),
} : {}),
flexDirection: 'column', gap: 1,
flexDirection: 'column', gap: props.symbol === '🎭' ? 0.5 : 1.25, pt: 1.25,
...props.sx,
}}
>
{/* [Edit mode checkbox] */}
{props.isEditMode && (
<Checkbox
variant='soft' color='neutral'
variant='soft' color={TILE_ACTIVE_COLOR}
checked={!props.isHidden}
// label={<Typography level='body-xs'>show</Typography>}
sx={{ position: 'absolute', left: `${tileGap}rem`, top: `${tileGap}rem` }}
@@ -111,6 +119,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
const [searchQuery, setSearchQuery] = React.useState('');
const [filteredIDs, setFilteredIDs] = React.useState<SystemPurposeId[] | null>(null);
const [editMode, setEditMode] = React.useState(false);
const [isYouTubeTranscriberActive, setIsYouTubeTranscriberActive] = React.useState(false);
// external state
const showFinder = useUIPreferencesStore(state => state.showPersonaFinder);
@@ -148,11 +158,52 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
// Handlers
// Modify the handlePurposeChanged function to check for the YouTube Transcriber
const handlePurposeChanged = React.useCallback((purposeId: SystemPurposeId | null) => {
if (purposeId && setSystemPurposeId)
setSystemPurposeId(props.conversationId, purposeId);
if (purposeId) {
if (purposeId === 'YouTubeTranscriber') {
// If the YouTube Transcriber tile is clicked, set the state accordingly
setIsYouTubeTranscriberActive(true);
} else {
setIsYouTubeTranscriberActive(false);
}
if (setSystemPurposeId) {
setSystemPurposeId(props.conversationId, purposeId);
}
}
}, [props.conversationId, setSystemPurposeId]);
React.useEffect(() => {
const isTranscriberActive = systemPurposeId === 'YouTubeTranscriber';
setIsYouTubeTranscriberActive(isTranscriberActive);
}, [systemPurposeId]);
// Implement handleAddMessage function
const handleAddMessage = (messageText: string) => {
// Retrieve the appendMessage action from the useChatStore
const { appendMessage } = useChatStore.getState();
const conversationId = props.conversationId;
// Create a new message object
const newMessage: DMessage = {
id: uuidv4(),
text: messageText,
sender: 'Bot',
avatar: null,
typing: false,
role: 'assistant' as 'assistant',
tokenCount: 0,
created: Date.now(),
updated: null,
};
// Append the new message to the conversation
appendMessage(conversationId, newMessage);
};
const handleCustomSystemMessageChange = React.useCallback((v: React.ChangeEvent<HTMLTextAreaElement>): void => {
// TODO: persist this change? Right now it's reset every time.
// maybe we shall have a "save" button just save on a state to persist between sessions
@@ -259,7 +310,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
</Typography>
<Tooltip disableInteractive title={editMode ? 'Done Editing' : 'Edit Tiles'}>
<IconButton size='sm' onClick={toggleEditMode} sx={{ my: '-0.25rem' /* absorb the button padding */ }}>
{editMode ? <DoneIcon /> : <EditIcon />}
{editMode ? <DoneIcon /> : <EditRoundedIcon />}
</IconButton>
</Tooltip>
</Box>
@@ -293,6 +344,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
isHidden={hidePersonaCreator}
onClick={() => editMode ? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR) : void navigateToPersonas()}
sx={{
fontSize: 'xs',
boxShadow: 'xs',
backgroundColor: 'neutral.softDisabledBg',
}}
@@ -326,24 +378,24 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
sx={{
// example items 2-col layout
display: 'grid',
gridTemplateColumns: `repeat(auto-fit, minmax(${tileSize * 2 + 1}rem, 1fr))`,
gridTemplateColumns: `repeat(auto-fit, minmax(${tileSize * 3 + 1}rem, 1fr))`,
gap: 1,
}}
>
{fourExamples?.map((example, idx) => (
<ListItem
key={idx}
variant='soft'
variant='outlined'
sx={{
// padding: '0.25rem 0.5rem',
backgroundColor: 'background.popup',
borderRadius: 'md',
// boxShadow: 'xs',
padding: '0.25rem 0.5rem',
backgroundColor: 'background.surface',
boxShadow: 'xs',
'& svg': { opacity: 0.1, transition: 'opacity 0.2s' },
'&:hover svg': { opacity: 1 },
}}
>
<ListItemButton onClick={() => props.runExample(example)} sx={{ justifyContent: 'space-between' }}>
<ListItemButton onClick={() => props.runExample(example)} sx={{ justifyContent: 'space-between', borderRadius: 'md' }}>
<Typography level='body-sm'>
{example}
</Typography>
@@ -412,6 +464,17 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
/>
)}
{/* [row -1] YouTube URL */}
{isYouTubeTranscriberActive && (
<YouTubeURLInput
onSubmit={(url) => handleAddMessage(url)}
isFetching={false}
sx={{
gridColumn: '1 / -1',
}}
/>
)}
</Box>
</Box>
@@ -0,0 +1,74 @@
import * as React from 'react';
import { Box, Button, Input } from '@mui/joy';
import YouTubeIcon from '@mui/icons-material/YouTube';
import type { SxProps } from '@mui/joy/styles/types';
import { useYouTubeTranscript, YTVideoTranscript } from '~/modules/youtube/useYouTubeTranscript';
interface YouTubeURLInputProps {
onSubmit: (transcript: string) => void;
isFetching: boolean;
sx?: SxProps;
}
export const YouTubeURLInput: React.FC<YouTubeURLInputProps> = ({ onSubmit, isFetching, sx }) => {
const [url, setUrl] = React.useState('');
const [submitFlag, setSubmitFlag] = React.useState(false);
// Function to extract video ID from URL
function extractVideoID(videoURL: string): string | null {
const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^#&?]*).*/;
const match = videoURL.match(regExp);
return (match && match[1]?.length == 11) ? match[1] : null;
}
const videoID = extractVideoID(url);
// Callback function to handle new transcript
const handleNewTranscript = (newTranscript: YTVideoTranscript) => {
onSubmit(newTranscript.transcript); // Pass the transcript text to the onSubmit handler
setSubmitFlag(false); // Reset submit flag after handling
};
const { transcript, isFetching: isTranscriptFetching, isError, error } = useYouTubeTranscript(videoID && submitFlag ? videoID : null, handleNewTranscript);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUrl(event.target.value);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault(); // Prevent form from causing a page reload
setSubmitFlag(true); // Set flag to indicate a submit action
};
return (
<Box sx={{ mb: 1, ...sx }}>
<form onSubmit={handleSubmit}>
<Input
required
type='url'
fullWidth
disabled={isFetching || isTranscriptFetching}
variant='outlined'
placeholder='Enter YouTube Video URL'
value={url}
onChange={handleChange}
startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />}
sx={{ mb: 1.5, backgroundColor: 'background.popup' }}
/>
<Button
type='submit'
variant='solid'
disabled={isFetching || isTranscriptFetching || !url}
loading={isFetching || isTranscriptFetching}
sx={{ minWidth: 140 }}
>
Get Transcript
</Button>
{isError && <div>Error fetching transcript. Please try again.</div>}
</form>
</Box>
);
};
@@ -18,7 +18,7 @@ export const usePurposeStore = create<PurposeStore>()(
(set) => ({
// default state
hiddenPurposeIDs: ['Developer', 'Designer'],
hiddenPurposeIDs: ['Developer', 'Designer', 'YouTubeTranscriber'],
toggleHiddenPurposeId: (purposeId: string) => {
set(state => {
@@ -37,14 +37,19 @@ export const usePurposeStore = create<PurposeStore>()(
/* versioning:
* 1: hide 'Developer' as 'DeveloperPreview' is best
* 2: add a hidden 'YouTubeTranscriber' purpose
*/
version: 1,
version: 2,
migrate: (state: any, fromVersion: number): PurposeStore => {
// 0 -> 1: rename 'enterToSend' to 'enterIsNewline' (flip the meaning)
if (state && fromVersion === 0)
if (!state.hiddenPurposeIDs.includes('Developer'))
state.hiddenPurposeIDs.push('Developer');
// 1 -> 2: add a hidden 'YouTubeTranscriber' purpose
if (state && fromVersion === 1)
if (!state.hiddenPurposeIDs.includes('YouTubeTranscriber'))
state.hiddenPurposeIDs.push('YouTubeTranscriber');
return state;
},
}),
@@ -1,7 +1,7 @@
import { shallow } from 'zustand/shallow';
import type { DFolder } from '~/common/state/store-folders';
import { conversationTitle, DConversationId, useChatStore } from '~/common/state/store-chats';
import { conversationTitle, DConversationId, DMessageUserFlag, messageHasUserFlag, messageUserFlagToEmoji, useChatStore } from '~/common/state/store-chats';
import type { ChatNavigationItemData } from './ChatDrawerItem';
@@ -12,6 +12,8 @@ const SEARCH_MIN_CHARS = 3;
export type ChatNavGrouping = false | 'date' | 'persona';
export type ChatSearchSorting = 'frequency' | 'date';
interface ChatNavigationGroupData {
type: 'nav-item-group',
title: string,
@@ -66,18 +68,28 @@ function getTimeBucketEn(currentTime: number, midnightTime: number): string {
}
}
export function isDrawerSearching(filterByQuery: string): { isSearching: boolean, lcTextQuery: string } {
const lcTextQuery = filterByQuery.trim().toLowerCase();
return {
isSearching: lcTextQuery.length >= SEARCH_MIN_CHARS,
lcTextQuery,
};
}
/*
* Optimization: return a reduced version of the DConversation object for 'Drawer Items' purposes,
* to avoid unnecessary re-renders on each new character typed by the assistant
*/
export function useChatNavRenderItems(
export function useChatDrawerRenderItems(
activeConversationId: DConversationId | null,
chatPanesConversationIds: DConversationId[],
filterByQuery: string,
activeFolder: DFolder | null,
allFolders: DFolder[],
filterHasStars: boolean,
grouping: ChatNavGrouping,
searchSorting: ChatSearchSorting,
showRelativeSize: boolean,
): {
renderNavItems: ChatRenderItemData[],
@@ -93,50 +105,59 @@ export function useChatNavRenderItems(
const selectedConversations = !activeFolder ? conversations : conversations.filter(_c => activeFolder.conversationIds.includes(_c.id));
// filter 2: preparation: lowercase the query
const lcTextQuery = filterByQuery.trim().toLowerCase();
const isSearching = lcTextQuery.length >= SEARCH_MIN_CHARS;
const { isSearching, lcTextQuery } = isDrawerSearching(filterByQuery);
// transform (the conversations into ChatNavigationItemData) + filter2 (if searching)
const chatNavItems = selectedConversations.map((_c): ChatNavigationItemData => {
// rich properties
const title = conversationTitle(_c);
const isAlsoOpen = findOpenInViewNumbers(chatPanesConversationIds, _c.id);
const chatNavItems = selectedConversations
.filter(_c => !filterHasStars || _c.messages.some(m => messageHasUserFlag(m, 'starred')))
.map((_c): ChatNavigationItemData => {
// rich properties
const title = conversationTitle(_c);
const isAlsoOpen = findOpenInViewNumbers(chatPanesConversationIds, _c.id);
// set the frequency counters if filtering is enabled
let searchFrequency: number = 0;
if (isSearching) {
const titleFrequency = title.toLowerCase().split(lcTextQuery).length - 1;
const messageFrequency = _c.messages.reduce((count, message) => count + (message.text.toLowerCase().split(lcTextQuery).length - 1), 0);
searchFrequency = titleFrequency + messageFrequency;
}
// set the frequency counters if filtering is enabled
let searchFrequency: number = 0;
if (isSearching) {
const titleFrequency = title.toLowerCase().split(lcTextQuery).length - 1;
const messageFrequency = _c.messages.reduce((count, message) => count + (message.text.toLowerCase().split(lcTextQuery).length - 1), 0);
searchFrequency = titleFrequency + messageFrequency;
}
// create the ChatNavigationData
return {
type: 'nav-item-chat-data',
conversationId: _c.id,
isActive: _c.id === activeConversationId,
isAlsoOpen,
isEmpty: !_c.messages.length && !_c.userTitle,
title,
folder: !allFolders.length
? undefined // don't show folder select if folders are disabled
: _c.id === activeConversationId // only show the folder for active conversation(s)
? allFolders.find(folder => folder.conversationIds.includes(_c.id)) ?? null
: null,
updatedAt: _c.updated || _c.created || 0,
messageCount: _c.messages.length,
assistantTyping: !!_c.abortController,
systemPurposeId: _c.systemPurposeId,
searchFrequency,
};
}).filter(item => !isSearching || item.searchFrequency > 0);
// union of message flags -> emoji string
const allFlags = new Set<DMessageUserFlag>();
_c.messages.forEach(_m => _m.userFlags?.forEach(flag => allFlags.add(flag)));
const userFlagsSummary = !allFlags.size ? undefined : Array.from(allFlags).map(messageUserFlagToEmoji).join('');
// create the ChatNavigationData
return {
type: 'nav-item-chat-data',
conversationId: _c.id,
isActive: _c.id === activeConversationId,
isAlsoOpen,
isEmpty: !_c.messages.length && !_c.userTitle,
title,
userFlagsSummary,
folder: !allFolders.length
? undefined // don't show folder select if folders are disabled
: _c.id === activeConversationId // only show the folder for active conversation(s)
? allFolders.find(folder => folder.conversationIds.includes(_c.id)) ?? null
: null,
updatedAt: _c.updated || _c.created || 0,
messageCount: _c.messages.length,
assistantTyping: !!_c.abortController,
systemPurposeId: _c.systemPurposeId,
searchFrequency,
};
})
.filter(item => !isSearching || item.searchFrequency > 0);
// check if the active conversation has an item in the list
const filteredChatsIncludeActive = chatNavItems.some(_c => _c.conversationId === activeConversationId);
// [sort by frequency, don't group] if there's a search query
chatNavItems.sort((a, b) => b.searchFrequency - a.searchFrequency);
if (isSearching && searchSorting === 'frequency')
chatNavItems.sort((a, b) => b.searchFrequency - a.searchFrequency);
// Render List
let renderNavItems: ChatRenderItemData[] = chatNavItems;
@@ -179,7 +200,12 @@ export function useChatNavRenderItems(
// [empty message] if there are no items
if (!renderNavItems.length)
renderNavItems.push({ type: 'nav-item-info-message', message: isSearching ? 'No results found' : 'No conversations in folder' });
renderNavItems.push({
type: 'nav-item-info-message',
message: filterHasStars ? 'No starred results'
: isSearching ? 'No results found'
: 'No conversations in folder',
});
// other derived state
const filteredChatIDs = chatNavItems.map(_c => _c.conversationId);
@@ -203,8 +229,10 @@ export function useChatNavRenderItems(
return a.renderNavItems.length === b.renderNavItems.length
&& a.renderNavItems.every((_a, i) => shallow(_a, b.renderNavItems[i]))
&& shallow(a.filteredChatIDs, b.filteredChatIDs)
// we also compare this, as it changes with a parameter
&& a.filteredChatsBarBasis === b.filteredChatsBarBasis;
&& a.filteredChatsCount === b.filteredChatsCount
&& a.filteredChatsAreEmpty === b.filteredChatsAreEmpty
&& a.filteredChatsBarBasis === b.filteredChatsBarBasis
&& a.filteredChatsIncludeActive === b.filteredChatsIncludeActive;
},
);
}
+151
View File
@@ -0,0 +1,151 @@
import { getChatLLMId } from '~/modules/llms/store-llms';
import { updateHistoryForReplyTo } from '~/modules/aifn/replyto/replyTo';
import { ConversationsManager } from '~/common/chats/ConversationsManager';
import { createDMessage, DConversationId, DMessage, getConversationSystemPurposeId } from '~/common/state/store-chats';
import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
import { extractChatCommand, findAllChatCommands } from '../commands/commands.registry';
import { getInstantAppChatPanesCount } from '../components/panes/usePanesManager';
import { runAssistantUpdatingState } from './chat-stream';
import { runBrowseGetPageUpdatingState } from './browse-load';
import { runImageGenerationUpdatingState } from './image-generate';
import { runReActUpdatingState } from './react-tangent';
import type { ChatModeId } from '../AppChat';
export async function _handleExecute(chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) {
// Handle missing conversation
if (!conversationId)
return 'err-no-conversation';
const chatLLMId = getChatLLMId();
// Update the system message from the active persona to the history
// NOTE: this does NOT call setMessages anymore (optimization). make sure to:
// 1. all the callers need to pass a new array
// 2. all the exit points need to call setMessages
const cHandler = ConversationsManager.getHandler(conversationId);
cHandler.inlineUpdatePurposeInHistory(history, chatLLMId || undefined);
// FIXME: shouldn't do this for all the code paths. The advantage for having it here (vs Composer output only) is re-executing history
// TODO: move this to the server side after transferring metadata?
updateHistoryForReplyTo(history);
// Handle unconfigured
if (!chatLLMId || !chatModeId) {
// set the history (e.g. the updated system prompt and the user prompt) at least, see #523
cHandler.messagesReplace(history);
return !chatLLMId ? 'err-no-chatllm' : 'err-no-chatmode';
}
// Valid /commands are intercepted here, and override chat modes, generally for mechanics or sidebars
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
if (lastMessage?.role === 'user') {
const chatCommand = extractChatCommand(lastMessage.text)[0];
if (chatCommand && chatCommand.type === 'cmd') {
switch (chatCommand.providerId) {
case 'ass-browse':
cHandler.messagesReplace(history); // show command
return await runBrowseGetPageUpdatingState(cHandler, chatCommand.params);
case 'ass-t2i':
cHandler.messagesReplace(history); // show command
return await runImageGenerationUpdatingState(cHandler, chatCommand.params);
case 'ass-react':
cHandler.messagesReplace(history); // show command
return await runReActUpdatingState(cHandler, chatCommand.params, chatLLMId);
case 'chat-alter':
// /clear
if (chatCommand.command === '/clear') {
if (chatCommand.params === 'all') {
cHandler.messagesReplace([]);
} else {
cHandler.messagesReplace(history);
cHandler.messageAppendAssistant('Issue: this command requires the \'all\' parameter to confirm the operation.', undefined, 'issue', false);
}
return true;
}
// /assistant, /system
Object.assign(lastMessage, {
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
sender: 'Bot',
text: chatCommand.params || '',
} satisfies Partial<DMessage>);
cHandler.messagesReplace(history);
return true;
case 'cmd-help':
const chatCommandsText = findAllChatCommands()
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
.join('\n');
cHandler.messagesReplace(history);
cHandler.messageAppendAssistant('Available Chat Commands:\n' + chatCommandsText, undefined, 'help', false);
return true;
case 'mode-beam':
if (chatCommand.isError) {
cHandler.messagesReplace(history);
return false;
}
// remove '/beam ', as we want to be a user chat message
Object.assign(lastMessage, { text: chatCommand.params || '' });
cHandler.messagesReplace(history);
ConversationsManager.getHandler(conversationId).beamInvoke(history, [], null);
return true;
default:
cHandler.messagesReplace([...history, createDMessage('assistant', 'This command is not supported.')]);
return false;
}
}
}
// get the system purpose (note: we don't react to it, or it would invalidate half UI components..)
if (!getConversationSystemPurposeId(conversationId)) {
cHandler.messagesReplace(history);
cHandler.messageAppendAssistant('Issue: no Persona selected.', undefined, 'issue', false);
return 'err-no-persona';
}
// synchronous long-duration tasks, which update the state as they go
switch (chatModeId) {
case 'generate-text':
cHandler.messagesReplace(history);
return await runAssistantUpdatingState(conversationId, history, chatLLMId, getUXLabsHighPerformance() ? 0 : getInstantAppChatPanesCount());
case 'generate-text-beam':
cHandler.messagesReplace(history);
cHandler.beamInvoke(history, [], null);
return true;
case 'append-user':
cHandler.messagesReplace(history);
return true;
case 'generate-image':
if (!lastMessage?.text) break;
// also add a 'fake' user message with the '/draw' command
cHandler.messagesReplace(history.map(message => (message.id !== lastMessage.id) ? message : {
...message,
text: `/draw ${lastMessage.text}`,
}));
return await runImageGenerationUpdatingState(cHandler, lastMessage.text);
case 'generate-react':
if (!lastMessage?.text) break;
cHandler.messagesReplace(history);
return await runReActUpdatingState(cHandler, lastMessage.text, chatLLMId);
}
// ISSUE: if we're here, it means we couldn't do the job, at least sync the history
console.log('Chat execute: issue running', chatModeId, conversationId, lastMessage);
cHandler.messagesReplace(history);
return false;
}
+11 -5
View File
@@ -1,20 +1,26 @@
import { callBrowseFetchPage } from '~/modules/browse/browse.client';
import { ConversationManager } from '~/common/chats/ConversationHandler';
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
export const runBrowseGetPageUpdatingState = async (conversationId: string, url: string) => {
const cHandler = ConversationManager.getHandler(conversationId);
export const runBrowseGetPageUpdatingState = async (cHandler: ConversationHandler, url?: string) => {
if (!url) {
cHandler.messageAppendAssistant('Issue: no URL provided.', undefined, 'issue', false);
return false;
}
// noinspection HttpUrlsUsage
const shortUrl = url.replace('https://www.', '').replace('https://', '').replace('http://', '').replace('www.', '');
const assistantMessageId = cHandler.messageAppendAssistant(`Loading page at ${shortUrl}...`, 'web', undefined);
const assistantMessageId = cHandler.messageAppendAssistant(`Loading page at ${shortUrl}...`, undefined, 'web', true);
try {
const page = await callBrowseFetchPage(url);
cHandler.messageEdit(assistantMessageId, { text: page.content || 'Issue: page load did not produce an answer: no text found', typing: false }, true);
const pageContent = page.content.markdown || page.content.text || page.content.html || 'Issue: page load did not produce an answer: no text found';
cHandler.messageEdit(assistantMessageId, { text: pageContent, typing: false }, true);
return true;
} catch (error: any) {
console.error(error);
cHandler.messageEdit(assistantMessageId, { text: 'Issue: browse did not produce an answer (error: ' + (error?.message || error?.toString() || 'unknown') + ').', typing: false }, true);
return false;
}
};
+34 -19
View File
@@ -1,40 +1,41 @@
import type { DLLMId } from '~/modules/llms/store-llms';
import type { StreamingClientUpdate } from '~/modules/llms/vendors/unifiedStreamingClient';
import { SystemPurposeId } from '../../../data';
import { autoSuggestions } from '~/modules/aifn/autosuggestions/autoSuggestions';
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
import { llmStreamingChatGenerate } from '~/modules/llms/llm.client';
import { llmStreamingChatGenerate, VChatContextRef, VChatContextName, VChatMessageIn } from '~/modules/llms/llm.client';
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
import type { DMessage } from '~/common/state/store-chats';
import { ConversationManager } from '~/common/chats/ConversationHandler';
import { ConversationsManager } from '~/common/chats/ConversationsManager';
import { ChatAutoSpeakType, getChatAutoAI } from '../store-app-chat';
export const STREAM_TEXT_INDICATOR = '...';
/**
* The main "chat" function. TODO: this is here so we can soon move it to the data model.
*/
export async function runAssistantUpdatingState(conversationId: string, history: DMessage[], assistantLlmId: DLLMId, systemPurpose: SystemPurposeId, parallelViewCount: number) {
const cHandler = ConversationManager.getHandler(conversationId);
export async function runAssistantUpdatingState(conversationId: string, history: DMessage[], assistantLlmId: DLLMId, parallelViewCount: number) {
const cHandler = ConversationsManager.getHandler(conversationId);
// ai follow-up operations (fire/forget)
const { autoSpeak, autoSuggestDiagrams, autoSuggestQuestions, autoTitleChat } = getChatAutoAI();
// update the system message from the active Purpose, if not manually edited
history = cHandler.resyncPurposeInHistory(history, assistantLlmId, systemPurpose);
// create a blank and 'typing' message for the assistant
const assistantMessageId = cHandler.messageAppendAssistant('...', assistantLlmId, history[0].purposeId);
const assistantMessageId = cHandler.messageAppendAssistant(STREAM_TEXT_INDICATOR, history[0].purposeId, assistantLlmId, true);
// when an abort controller is set, the UI switches to the "stop" mode
const abortController = new AbortController();
cHandler.setAbortController(abortController);
// stream the assistant's messages
await streamAssistantMessage(
const messageStatus = await streamAssistantMessage(
assistantLlmId,
history,
history.map((m): VChatMessageIn => ({ role: m.role, content: m.text })),
'conversation',
conversationId,
parallelViewCount,
autoSpeak,
(update) => cHandler.messageEdit(assistantMessageId, update, false),
@@ -42,6 +43,7 @@ export async function runAssistantUpdatingState(conversationId: string, history:
);
// clear to send, again
// FIXME: race condition?
cHandler.setAbortController(null);
if (autoTitleChat) {
@@ -51,24 +53,32 @@ export async function runAssistantUpdatingState(conversationId: string, history:
if (autoSuggestDiagrams || autoSuggestQuestions)
autoSuggestions(conversationId, assistantMessageId, autoSuggestDiagrams, autoSuggestQuestions);
return messageStatus.outcome === 'success';
}
type StreamMessageOutcome = 'success' | 'aborted' | 'errored';
type StreamMessageStatus = { outcome: StreamMessageOutcome, errorMessage?: string };
async function streamAssistantMessage(
export async function streamAssistantMessage(
llmId: DLLMId,
history: DMessage[],
messagesHistory: VChatMessageIn[],
contextName: VChatContextName,
contextRef: VChatContextRef,
throttleUnits: number, // 0: disable, 1: default throttle (12Hz), 2+ reduce the message frequency with the square root
autoSpeak: ChatAutoSpeakType,
editMessage: (update: Partial<DMessage>) => void,
abortSignal: AbortSignal,
) {
): Promise<StreamMessageStatus> {
const returnStatus: StreamMessageStatus = {
outcome: 'success',
errorMessage: undefined,
};
// speak once
let spokenLine = false;
const messages = history.map(({ role, text }) => ({ role, content: text }));
// Throttling setup
let lastCallTime = 0;
let throttleDelay = 1000 / 12; // 12 messages per second works well for 60Hz displays (single chat, and 24 in 4 chats, see the square root below)
@@ -86,7 +96,7 @@ async function streamAssistantMessage(
const incrementalAnswer: Partial<DMessage> = { text: '' };
try {
await llmStreamingChatGenerate(llmId, messages, null, null, abortSignal, (update: StreamingClientUpdate) => {
await llmStreamingChatGenerate(llmId, messagesHistory, contextName, contextRef, null, null, abortSignal, (update: StreamingClientUpdate) => {
const textSoFar = update.textSoFar;
// grow the incremental message
@@ -116,7 +126,10 @@ async function streamAssistantMessage(
console.error('Fetch request error:', error);
const errorText = ` [Issue: ${error.message || (typeof error === 'string' ? error : 'Chat stopped.')}]`;
incrementalAnswer.text = (incrementalAnswer.text || '') + errorText;
}
returnStatus.outcome = 'errored';
returnStatus.errorMessage = error.message;
} else
returnStatus.outcome = 'aborted';
}
// Optimized:
@@ -127,4 +140,6 @@ async function streamAssistantMessage(
// 📢 TTS: all
if ((autoSpeak === 'all' || autoSpeak === 'firstLine') && incrementalAnswer.text && !spokenLine && !abortSignal.aborted)
void speakText(incrementalAnswer.text);
return returnStatus;
}
+15 -12
View File
@@ -1,23 +1,25 @@
import { getActiveTextToImageProviderOrThrow, t2iGenerateImageOrThrow } from '~/modules/t2i/t2i.client';
import { ConversationManager } from '~/common/chats/ConversationHandler';
import { TextToImageProvider } from '~/common/components/useCapabilities';
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
import type { TextToImageProvider } from '~/common/components/useCapabilities';
/**
* Text to image, appended as an 'assistant' message
*/
export async function runImageGenerationUpdatingState(conversationId: string, imageText: string) {
const handler = ConversationManager.getHandler(conversationId);
export async function runImageGenerationUpdatingState(cHandler: ConversationHandler, imageText?: string) {
if (!imageText) {
cHandler.messageAppendAssistant('Issue: no image description provided.', undefined, 'issue', false);
return false;
}
// Acquire the active TextToImageProvider
let t2iProvider: TextToImageProvider | undefined = undefined;
try {
t2iProvider = getActiveTextToImageProviderOrThrow();
} catch (error: any) {
const assistantErrorMessageId = handler.messageAppendAssistant(`[Issue] Sorry, I can't generate images right now. ${error?.message || error?.toString() || 'Unknown error'}.`, 'issue', undefined);
handler.messageEdit(assistantErrorMessageId, { typing: false }, true);
return;
cHandler.messageAppendAssistant(`[Issue] Sorry, I can't generate images right now. ${error?.message || error?.toString() || 'Unknown error'}.`, undefined, 'issue', false);
return 'err-t2i-unconfigured';
}
// if the imageText ends with " xN" or " [N]" (where N is a number), then we'll generate N images
@@ -26,17 +28,18 @@ export async function runImageGenerationUpdatingState(conversationId: string, im
if (repeat > 1)
imageText = imageText.replace(/x(\d+)$|\[(\d+)]$/, '').trim(); // Remove the "xN" or "[N]" part from the imageText
const assistantMessageId = handler.messageAppendAssistant(
const assistantMessageId = cHandler.messageAppendAssistant(
`Give me ${t2iProvider.vendor === 'openai' ? 'a dozen' : 'a few'} seconds while I draw ${imageText?.length > 20 ? 'that' : '"' + imageText + '"'}...`,
'', undefined,
undefined, t2iProvider.painter, true,
);
handler.messageEdit(assistantMessageId, { originLLM: t2iProvider.painter }, false);
try {
const imageUrls = await t2iGenerateImageOrThrow(t2iProvider, imageText, repeat);
handler.messageEdit(assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
cHandler.messageEdit(assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
return true;
} catch (error: any) {
const errorMessage = error?.message || error?.toString() || 'Unknown error';
handler.messageEdit(assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
cHandler.messageEdit(assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
return false;
}
}
+12 -5
View File
@@ -2,7 +2,9 @@ import { Agent } from '~/modules/aifn/react/react';
import { DLLMId } from '~/modules/llms/store-llms';
import { useBrowseStore } from '~/modules/browse/store-module-browsing';
import { ConversationManager } from '~/common/chats/ConversationHandler';
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
import { STREAM_TEXT_INDICATOR } from './chat-stream';
const EPHEMERAL_DELETION_DELAY = 5 * 1000;
@@ -10,12 +12,15 @@ const EPHEMERAL_DELETION_DELAY = 5 * 1000;
/**
* Synchronous ReAct chat function - TODO: event loop, auto-ui, cleanups, etc.
*/
export async function runReActUpdatingState(conversationId: string, question: string, assistantLlmId: DLLMId) {
const cHandler = ConversationManager.getHandler(conversationId);
export async function runReActUpdatingState(cHandler: ConversationHandler, question: string | undefined, assistantLlmId: DLLMId) {
if (!question) {
cHandler.messageAppendAssistant('Issue: no question provided.', undefined, 'issue', false);
return false;
}
// create a blank and 'typing' message for the assistant - to be filled when we're done
const assistantModelLabel = 'react-' + assistantLlmId.slice(4, 7); // HACK: this is used to change the Avatar animation
const assistantMessageId = cHandler.messageAppendAssistant('...', assistantModelLabel, undefined);
const assistantModelLabel = 'react-' + assistantLlmId; //.slice(4, 7); // HACK: this is used to change the Avatar animation
const assistantMessageId = cHandler.messageAppendAssistant(STREAM_TEXT_INDICATOR, undefined, assistantModelLabel, true);
const { enableReactTool: enableBrowse } = useBrowseStore.getState();
// create an ephemeral space
@@ -37,9 +42,11 @@ export async function runReActUpdatingState(conversationId: string, question: st
cHandler.messageEdit(assistantMessageId, { text: reactResult, typing: false }, false);
setTimeout(() => eHandler.delete(), EPHEMERAL_DELETION_DELAY);
return true;
} catch (error: any) {
console.error(error);
logToEphemeral(ephemeralText + `\nIssue: ${error || 'unknown'}`);
cHandler.messageEdit(assistantMessageId, { text: 'Issue: ReAct did not produce an answer.', typing: false }, false);
return false;
}
}
+25 -4
View File
@@ -1,6 +1,7 @@
import { create } from 'zustand';
import { shallow } from 'zustand/shallow';
import { persist } from 'zustand/middleware';
import { useShallow } from 'zustand/react/shallow';
export type ChatAutoSpeakType = 'off' | 'firstLine' | 'all';
@@ -26,9 +27,15 @@ interface AppChatStore {
// chat UI
filterHasStars: boolean;
setFilterHasStars: (filterHasStars: boolean) => void;
micTimeoutMs: number;
setMicTimeoutMs: (micTimeoutMs: number) => void;
showPersonaIcons: boolean;
setShowPersonaIcons: (showPersonaIcons: boolean) => void;
showRelativeSize: boolean;
setShowRelativeSize: (showRelativeSize: boolean) => void;
@@ -56,9 +63,15 @@ const useAppChatStore = create<AppChatStore>()(persist(
autoTitleChat: true,
setAutoTitleChat: (autoTitleChat: boolean) => _set({ autoTitleChat }),
filterHasStars: false,
setFilterHasStars: (filterHasStars: boolean) => _set({ filterHasStars }),
micTimeoutMs: 2000,
setMicTimeoutMs: (micTimeoutMs: number) => _set({ micTimeoutMs }),
showPersonaIcons: true,
setShowPersonaIcons: (showPersonaIcons: boolean) => _set({ showPersonaIcons }),
showRelativeSize: false,
setShowRelativeSize: (showRelativeSize: boolean) => _set({ showRelativeSize }),
@@ -113,10 +126,18 @@ export const useChatMicTimeoutMsValue = (): number =>
export const useChatMicTimeoutMs = (): [number, (micTimeoutMs: number) => void] =>
useAppChatStore(state => [state.micTimeoutMs, state.setMicTimeoutMs], shallow);
export const useChatShowRelativeSize = (): { showRelativeSize: boolean, toggleRelativeSize: () => void } => {
const showRelativeSize = useAppChatStore(state => state.showRelativeSize);
const toggleRelativeSize = () => useAppChatStore.getState().setShowRelativeSize(!showRelativeSize);
return { showRelativeSize, toggleRelativeSize };
export const useChatDrawerFilters = () => {
const values = useAppChatStore(useShallow(state => ({
filterHasStars: state.filterHasStars,
showPersonaIcons: state.showPersonaIcons,
showRelativeSize: state.showRelativeSize,
})));
return {
...values,
toggleFilterHasStars: () => useAppChatStore.getState().setFilterHasStars(!values.filterHasStars),
toggleShowPersonaIcons: () => useAppChatStore.getState().setShowPersonaIcons(!values.showPersonaIcons),
toggleShowRelativeSize: () => useAppChatStore.getState().setShowRelativeSize(!values.showRelativeSize),
};
};
export const useChatShowTextDiff = (): [boolean, (showDiff: boolean) => void] =>
+1 -1
View File
@@ -4,7 +4,7 @@ import * as React from 'react';
export function Gallery() {
return (
<AppPlaceholder text='Drawing App is under development. v1.13 or v1.14.' />
<AppPlaceholder text='Drawing App is under development. v1.16.' />
);
}
+3 -1
View File
@@ -137,7 +137,9 @@ export function TextToImage(props: {
// layout
display: 'grid',
gridTemplateColumns: props.isMobile ? 'repeat(auto-fit, minmax(320px, 1fr))' : 'repeat(auto-fit, minmax(400px, 1fr))',
gridTemplateColumns: props.isMobile
? 'repeat(auto-fit, minmax(320px, 1fr))'
: 'repeat(auto-fit, minmax(max(min(100%, 400px), 100%/5), 1fr))',
gap: { xs: 2, md: 2 },
}}>
{prompts.map((prompt, index) => {
@@ -1,7 +1,7 @@
import * as React from 'react';
import { Button, ButtonGroup, IconButton, Tooltip } from '@mui/joy';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
import LightbulbOutlinedIcon from '@mui/icons-material/LightbulbOutlined';
// const desktopButtonLegend =
@@ -48,7 +48,7 @@ export function ButtonPromptFromIdea(props: {
</Button>
<Tooltip disableInteractive title='Use Idea'>
<IconButton size='sm' onClick={onIdeaUse}>
<ArrowForwardIcon />
<ArrowForwardRoundedIcon />
</IconButton>
</Tooltip>
</ButtonGroup>
+4 -4
View File
@@ -2,9 +2,9 @@ import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Button, ButtonGroup, Chip, Divider, IconButton, Typography } from '@mui/joy';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
import { niceShadowKeyframes } from '../../call/Contacts';
import { animationShadowRingLimey } from '~/common/util/animUtils';
export function DrawHeading(props: {
@@ -30,9 +30,9 @@ export function DrawHeading(props: {
borderRadius: '50%',
pointerEvents: 'none',
backgroundColor: 'background.popup',
animation: `${niceShadowKeyframes} 5s infinite`,
animation: `${animationShadowRingLimey} 5s infinite`,
}}>
<FormatPaintIcon />
<FormatPaintTwoToneIcon />
</IconButton>
{/* Messaging */}
+8 -9
View File
@@ -4,17 +4,16 @@ import { v4 as uuidv4 } from 'uuid';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Button, ButtonGroup, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Typography } from '@mui/joy';
import AddIcon from '@mui/icons-material/Add';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import MoreTimeIcon from '@mui/icons-material/MoreTime';
import RemoveIcon from '@mui/icons-material/Remove';
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
import { animationStopEnter } from '../../chat/components/composer/Composer';
import { animationEnterBelow } from '~/common/util/animUtils';
import { lineHeightTextareaMd } from '~/common/app.theme';
import { useUIPreferencesStore } from '~/common/state/store-ui';
@@ -219,7 +218,7 @@ export function PromptDesigner(props: {
<Dropdown>
<MenuButton slots={{ root: IconButton }}>
<ArrowForwardIcon />
<ArrowForwardRoundedIcon />
</MenuButton>
<Menu placement='top'>
{/* Add From History? */}
@@ -288,10 +287,10 @@ export function PromptDesigner(props: {
<Button
key='draw-queue'
variant='solid' color='primary'
endDecorator={<FormatPaintIcon />}
endDecorator={<FormatPaintTwoToneIcon />}
onClick={handlePromptEnqueue}
sx={{
animation: `${animationStopEnter} 0.1s ease-out`,
animation: `${animationEnterBelow} 0.1s ease-out`,
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)` : 'none',
justifyContent: 'space-between',
}}
@@ -306,7 +305,7 @@ export function PromptDesigner(props: {
endDecorator={<StopOutlinedIcon sx={{ fontSize: 18 }} />}
onClick={handleDrawStop}
sx={{
// animation: `${animationStopEnter} 0.1s ease-out`,
// animation: `${animationEnterBelow} 0.1s ease-out`,
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-warning-mainChannel) / 20%)` : 'none',
justifyContent: 'space-between',
}}
@@ -321,7 +320,7 @@ export function PromptDesigner(props: {
endDecorator={<MoreTimeIcon sx={{ fontSize: 18 }} />}
onClick={handlePromptEnqueue}
sx={{
animation: `${animationStopEnter} 0.1s ease-out`,
animation: `${animationEnterBelow} 0.1s ease-out`,
boxShadow: !props.isMobile ? `0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)` : 'none',
justifyContent: 'space-between',
}}
+3 -3
View File
@@ -1,7 +1,7 @@
import * as React from 'react';
import { FormControl, FormLabel, ListItemDecorator, Option, Select } from '@mui/joy';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
import type { TextToImageProvider } from '~/common/components/useCapabilities';
import { OpenAIIcon } from '~/common/components/icons/vendors/OpenAIIcon';
@@ -22,7 +22,7 @@ export function ProviderSelect(props: {
label: provider.label + (provider.painter !== provider.label ? ` ${provider.painter}` : ''),
value: provider.id,
configured: provider.configured,
Icon: provider.vendor === 'openai' ? OpenAIIcon : FormatPaintIcon,
Icon: provider.vendor === 'openai' ? OpenAIIcon : FormatPaintTwoToneIcon,
});
});
return options;
@@ -41,7 +41,7 @@ export function ProviderSelect(props: {
value={props.activeProviderId}
placeholder='Select a service'
onChange={(_event, value) => value && props.setActiveProviderId(value)}
// startDecorator={<FormatPaintIcon sx={{ display: { xs: 'none', sm: 'inherit' } }} />}
// startDecorator={<FormatPaintTwoToneIcon sx={{ display: { xs: 'none', sm: 'inherit' } }} />}
sx={{
minWidth: '12rem',
}}
+5 -12
View File
@@ -5,10 +5,10 @@ import { Box, Button, Card, CardContent, List, ListItem, Tooltip, Typography } f
import TelegramIcon from '@mui/icons-material/Telegram';
import { ChatMessageMemo } from '../chat/components/message/ChatMessage';
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
import { useChatShowSystemMessages } from '../chat/store-app-chat';
import { Brand } from '~/common/app.config';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats';
import { launchAppChat } from '~/common/app.routes';
@@ -63,7 +63,7 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
const handleClone = async (canOverwrite: boolean) => {
setCloning(true);
const importedId = useChatStore.getState().importConversation({ ...props.conversation }, !canOverwrite);
await launchAppChat(importedId);
void launchAppChat(importedId);
setCloning(false);
};
@@ -108,17 +108,10 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
p: 0,
}}>
<ScrollToBottom
bootToBottom bootSmoothly
sx={{
// allows the content to be scrolled (all browsers)
overflowY: 'auto',
// actually make sure this scrolls & fills
height: '100%',
}}
>
<ScrollToBottom bootToBottom bootSmoothly>
<List sx={{
minHeight: '100%',
p: 0,
display: 'flex', flexDirection: 'column',
flexGrow: 1,
@@ -142,7 +135,7 @@ export function LinkChatViewer(props: { conversation: DConversation, storedAt: D
key={'msg-' + message.id}
message={message}
fitScreen={isMobile}
blocksShowDate={idx === 0 || idx === filteredMessages.length - 1 /* first and last message */}
showBlocksDate={idx === 0 || idx === filteredMessages.length - 1 /* first and last message */}
onMessageEdit={(_messageId, text: string) => message.text = text}
/>,
)}
+36 -40
View File
@@ -1,40 +1,26 @@
import * as React from 'react';
import { keyframes } from '@emotion/react';
import NextImage from 'next/image';
import TimeAgo from 'react-timeago';
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, Grid, IconButton, Typography } from '@mui/joy';
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, Grid, Typography } from '@mui/joy';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import LaunchIcon from '@mui/icons-material/Launch';
import { Brand } from '~/common/app.config';
import { Link } from '~/common/components/Link';
import { ROUTE_INDEX } from '~/common/app.routes';
import { animationColorBlues, animationColorRainbow } from '~/common/util/animUtils';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { cssRainbowColorKeyframes } from '~/common/app.theme';
import { NewsItems } from './news.data';
import { beamNewsCallout } from './beam.data';
// number of news items to show by default, before the expander
const DEFAULT_NEWS_COUNT = 3;
export const cssColorKeyframes = keyframes`
0%, 100% {
color: #636B74; /* Neutral main color (500) */
}
25% {
color: #12467B; /* Primary darker shade (700) */
}
50% {
color: #0B6BCB; /* Primary main color (500) */
}
75% {
color: #083e75; /* Primary lighter shade (300) */
}`;
const NEWS_INITIAL_COUNT = 3;
const NEWS_LOAD_STEP = 2;
// callout, for special occasions
export const newsRoadmapCallout =
<Card variant='solid' invertedColors>
<CardContent sx={{ gap: 2 }}>
@@ -69,12 +55,15 @@ export const newsRoadmapCallout =
export function AppNews() {
// state
const [lastNewsIdx, setLastNewsIdx] = React.useState<number>(DEFAULT_NEWS_COUNT - 1);
const [lastNewsIdx, setLastNewsIdx] = React.useState<number>(NEWS_INITIAL_COUNT - 1);
// news selection
const news = NewsItems.filter((_, idx) => idx <= lastNewsIdx);
const firstNews = news[0] ?? null;
// show expander
const canExpand = news.length < NewsItems.length;
return (
<Box sx={{
@@ -90,7 +79,7 @@ export function AppNews() {
}}>
<Typography level='h1' sx={{ fontSize: '2.9rem', mb: 4 }}>
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${cssColorKeyframes} 10s infinite`, zIndex: 1 }}>{firstNews?.versionCode}</Box>!
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${animationColorBlues} 10s infinite`, zIndex: 1 /* perf-opt */ }}>{firstNews?.versionCode}</Box>!
</Typography>
<Typography sx={{ mb: 2 }} level='title-sm'>
@@ -118,11 +107,16 @@ export function AppNews() {
<Container disableGutters maxWidth='sm'>
{news?.map((ni, idx) => {
// const firstCard = idx === 0;
const hasCardAfter = news.length < NewsItems.length;
const showExpander = hasCardAfter && (idx === news.length - 1);
const addPadding = false; //!firstCard; // || showExpander;
return <React.Fragment key={idx}>
{/* Inject the Beam item here*/}
{idx === 2 && (
<Box sx={{ mb: 3 }}>
{beamNewsCallout}
</Box>
)}
{/* News Item */}
<Card key={'news-' + idx} sx={{ mb: 3, minHeight: 32, gap: 1 }}>
<CardContent sx={{ position: 'relative', pr: addPadding ? 4 : 0 }}>
@@ -132,9 +126,9 @@ export function AppNews() {
<Box
component='span'
sx={idx ? {} : {
animation: `${cssRainbowColorKeyframes} 5s infinite`,
animation: `${animationColorRainbow} 5s infinite`,
fontWeight: 'lg',
zIndex: 1,
zIndex: 1, /* perf-opt */
}}
>
{ni.versionName}
@@ -148,7 +142,7 @@ export function AppNews() {
{!!ni.items && (ni.items.length > 0) && (
<ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: '1.5rem', listStyleType: '"- "' }}>
{ni.items.filter(item => item.dev !== true).map((item, idx) => (
<li key={idx} style={{ listStyle: item.icon ? '" "' : '"- "', marginLeft: item.icon ? '-1.125rem' : undefined }}>
<li key={idx} style={{ listStyle: (item.icon || item.noBullet) ? '" "' : '"- "', marginLeft: item.icon ? '-1.125rem' : undefined }}>
<Typography component='div' sx={{ fontSize: 'sm' }}>
{item.icon && <item.icon sx={{ fontSize: 'xs', mr: 0.75 }} />}
{item.text}
@@ -158,19 +152,6 @@ export function AppNews() {
</ul>
)}
{showExpander && (
<IconButton
variant='solid'
onClick={() => setLastNewsIdx(idx + 1)}
sx={{
position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1,
// backgroundColor: 'background.surface',
borderRadius: '50%',
}}
>
<ExpandMoreIcon />
</IconButton>
)}
</CardContent>
{!!ni.versionCoverImage && (
@@ -184,14 +165,16 @@ export function AppNews() {
// commented: we scale the images to 600px wide (>300 px tall)
// sizes='(max-width: 1200px) 100vw, 50vw'
priority={idx === 0}
quality={90}
/>
</AspectRatio>
</CardOverflow>
)}
</Card>
{/* Inject the roadmap item here*/}
{idx === 0 && (
{idx === 3 && (
<Box sx={{ mb: 3 }}>
{newsRoadmapCallout}
</Box>
@@ -199,6 +182,19 @@ export function AppNews() {
</React.Fragment>;
})}
{canExpand && (
<Button
fullWidth
variant='soft'
color='neutral'
onClick={() => setLastNewsIdx(index => index + NEWS_LOAD_STEP)}
endDecorator={<ExpandMoreIcon />}
>
Previous News
</Button>
)}
</Container>
{/*<Typography sx={{ textAlign: 'center' }}>*/}
+43
View File
@@ -0,0 +1,43 @@
import * as React from 'react';
import { Button, Card, CardContent, Grid, Typography } from '@mui/joy';
import LaunchIcon from '@mui/icons-material/Launch';
import ThumbUpRoundedIcon from '@mui/icons-material/ThumbUpRounded';
import { Link } from '~/common/components/Link';
export const beamReleaseDate = '2024-04-01T22:00:00Z';
export const beamBlogUrl = 'https://big-agi.com/blog/beam-multi-model-ai-reasoning/';
export const beamNewsCallout =
<Card variant='solid' invertedColors>
<CardContent sx={{ gap: 2 }}>
<Typography level='title-lg'>
Beam - launched in 1.15
</Typography>
<Typography level='body-sm'>
Beam is a world-first, multi-model AI chat modality that accelerates the discovery of superior solutions by leveraging the collective strengths of diverse LLMs.
{/*Beam is a world-first, multi-model AI chat modality. By combining the strenghts of diverse LLMs, Beam allows you to find better answers, faster.*/}
</Typography>
<Grid container spacing={1}>
<Grid xs={12} sm={7}>
<Button
fullWidth variant='soft' color='primary' endDecorator={<LaunchIcon />}
component={Link} href={beamBlogUrl} noLinkStyle target='_blank'
>
Blog
</Button>
</Grid>
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
{/*<Button*/}
{/* fullWidth variant='outlined' color='primary' startDecorator={<ThumbUpRoundedIcon />}*/}
{/* // endDecorator={<LaunchIcon />}*/}
{/* component={Link} href={beamHNUrl} noLinkStyle target='_blank'*/}
{/*>*/}
{/* on Hackernews 🙏*/}
{/*</Button>*/}
</Grid>
</Grid>
</CardContent>
</Card>;
+64 -31
View File
@@ -1,13 +1,12 @@
import * as React from 'react';
import { StaticImageData } from 'next/image';
import { SxProps } from '@mui/joy/styles/types';
import { Box, Chip, SvgIconProps, Typography } from '@mui/joy';
import AutoStoriesOutlinedIcon from '@mui/icons-material/AutoStoriesOutlined';
import GoogleIcon from '@mui/icons-material/Google';
import LaunchIcon from '@mui/icons-material/Launch';
import { AnthropicIcon } from '~/common/components/icons/vendors/AnthropicIcon';
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
import { ExternalLink } from '~/common/components/ExternalLink';
import { GroqIcon } from '~/common/components/icons/vendors/GroqIcon';
import { LocalAIIcon } from '~/common/components/icons/vendors/LocalAIIcon';
import { MistralIcon } from '~/common/components/icons/vendors/MistralIcon';
@@ -18,8 +17,14 @@ import { Link } from '~/common/components/Link';
import { clientUtmSource } from '~/common/util/pwaUtils';
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
import { beamBlogUrl } from './beam.data';
// Images
// Cover Images
// A landscape image of a capybara made entirely of clear, translucent crystal, wearing oversized black sunglasses, sitting at a sleek, minimalist desk. The desk is bathed in a soft, ethereal light emanating from within the capybara, symbolizing clarity and transparency. The capybara is typing on a futuristic, holographic keyboard, with floating code snippets and diagrams surrounding it, illustrating an improved developer experience and Auto-Diagrams feature. The background is a clean, white space with subtle, geometric patterns. Close-up photography style with a bokeh effect.
import coverV116 from '../../../public/images/covers/release-cover-v1.16.0.png';
// (not exactly) Imagine a futuristic, holographically bounded space. Inside this space, four capybaras stand. Three of them are in various stages of materialization, their forms made up of thousands of tiny, vibrant particles of electric blues, purples, and greens. These particles represent the merging of different intelligent inputs, symbolizing the concept of 'Beaming'. Positioned slightly towards the center and ahead of the others, the fourth capybara is fully materialized and composed of shimmering golden cotton candy, representing the optimal solution the 'Beam' feature seeks to achieve. The golden capybara gazes forward confidently, embodying a target achieved. Illuminated grid lines softly glow on the floor and walls of the setting, amplifying the futuristic aspect. In front of the golden capybara, floating, holographic interfaces depict complex networks of points and lines symbolizing the solution space 'Beaming' explores. The capybara interacts with these interfaces, implying the user's ability to control and navigate towards the best outcomes.
import coverV115 from '../../../public/images/covers/release-cover-v1.15.0.png';
// An image of a capybara sculpted entirely from iridescent blue cotton candy, gazing into a holographic galaxy of floating AI model icons (representing various AI models like Perplexity, Groq, etc.). The capybara is wearing a lightweight, futuristic headset, and its paws are gesturing as if orchestrating the movement of the models in the galaxy. The backdrop is minimalist, with occasional bursts of neon light beams, creating a sense of depth and wonder. Close-up photography, bokeh effect, with a dark but vibrant background to make the colors pop.
import coverV114 from '../../../public/images/covers/release-cover-v1.14.0.png';
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is using a computer with split screen made of origami, split keyboard and is wearing origami sunglasses with very different split reflections. Split halves are very contrasting. Close up photography, bokeh, white background.
@@ -40,22 +45,60 @@ interface NewsItem {
dev?: boolean;
issue?: number;
icon?: React.FC<SvgIconProps>;
noBullet?: boolean;
}[];
}
// news and feature surfaces
export const NewsItems: NewsItem[] = [
/*{
versionCode: '1.15.0',
versionCode: '1.17.0',
items: [
Best-Of
Screen Capture (when removed from labs)
Auto-Merge
Draw
...
Screen Capture (when removed from labs)
]
}*/
{
versionCode: '1.14.1',
versionCode: '1.16.2',
versionName: 'Crystal Clear',
versionDate: new Date('2024-06-07T05:00:00Z'),
// versionDate: new Date('2024-05-13T19:00:00Z'),
// versionDate: new Date('2024-05-09T00:00:00Z'),
versionCoverImage: coverV116,
items: [
{ text: <><B href={beamBlogUrl} wow>Beam</B> core and UX improvements based on user feedback</>, issue: 470, icon: ChatBeamIcon },
{ text: <>Chat <B>Cost estimation</B> with supported models* 💰</> },
{ text: <>Major <B>Auto-Diagrams</B> enhancements</> },
{ text: <>Save/load chat files with Ctrl+S / O</>, issue: 466 },
{ text: <><B issue={500}>YouTube Transcriber</B> persona: chat with videos</>, issue: 500 },
{ text: <>Improved <B issue={508}>formula render</B>, dark-mode diagrams</>, issue: 508 },
{ text: <>More: <B issue={517}>code soft-wrap</B>, selection toolbar, <B issue={507}>3x faster</B> on Apple silicon</>, issue: 507 },
{ text: <>Updated <B>Anthropic</B>*, <B>Groq</B>, <B>Ollama</B>, <B>OpenAI</B>*, <B>OpenRouter</B>*, and <B>Perplexity</B></> },
{ text: <>Developers: update LLMs data structures</>, dev: true },
{ text: <>1.16.1: Support for <B>OpenAI</B> <B href='https://openai.com/index/hello-gpt-4o/'>GPT-4o</B> (refresh your OpenAI models)</> },
{ text: <>1.16.2: Proper <B>Gemini</B> support, <B>HTML/Markdown</B> downloads, and latest <B>Mistral</B></> },
],
},
{
versionCode: '1.15',
versionName: 'Beam',
versionDate: new Date('2024-04-10T08:00:00Z'),
versionCoverImage: coverV115,
items: [
{ text: <><B href={beamBlogUrl} wow>Beam</B>: Find better answers with multi-model AI reasoning</>, issue: 443, icon: ChatBeamIcon },
// { text: <><B>Explore diverse perspectives</B> and <B>synthesize optimal responses</B></>, noBullet: true },
{ text: <><B issue={436}>Auto-configure</B> models for managed deployments</>, issue: 436 },
{ text: <>Message <B issue={476}>starring </B>, filtering and attachment</>, issue: 476 },
{ text: <>Default persona improvements</> },
{ text: <>Fixes to Gemini models and SVGs, improvements to UI and icons, and more</> },
{ text: <>Developers: imperative LLM models discovery</>, dev: true },
{ text: <>1.15.1: Support for <B>Gemini Pro 1.5</B> and <B>OpenAI 2024-04-09</B> models</> },
],
},
{
versionCode: '1.14',
versionName: 'Modelmorphic',
versionCoverImage: coverV114,
versionDate: new Date('2024-03-07T08:00:00Z'),
@@ -74,7 +117,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.13.0',
versionCode: '1.13',
versionName: 'Multi + Mind',
versionMoji: '🧠🔀',
versionDate: new Date('2024-02-08T07:47:00Z'),
@@ -90,7 +133,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.12.0',
versionCode: '1.12',
versionName: 'AGI Hotline',
versionMoji: '✨🗣️',
versionDate: new Date('2024-01-26T12:30:00Z'),
@@ -109,7 +152,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.11.0',
versionCode: '1.11',
versionName: 'Singularity',
versionMoji: '🌌🌠',
versionDate: new Date('2024-01-16T06:30:00Z'),
@@ -123,7 +166,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.10.0',
versionCode: '1.10',
versionName: 'The Year of AGI',
// versionMoji: '🎊✨',
versionDate: new Date('2024-01-06T08:00:00Z'),
@@ -137,7 +180,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.9.0',
versionCode: '1.9',
versionName: 'Creative Horizons',
// versionMoji: '🎨🌌',
versionDate: new Date('2023-12-28T22:30:00Z'),
@@ -152,7 +195,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.8.0',
versionCode: '1.8',
versionName: 'To The Moon And Back',
// versionMoji: '🚀🌕🔙❤️',
versionDate: new Date('2023-12-20T09:30:00Z'),
@@ -169,7 +212,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.7.0',
versionCode: '1.7',
versionName: 'Attachment Theory',
// versionDate: new Date('2023-12-11T06:00:00Z'), // 1.7.3
versionDate: new Date('2023-12-10T12:00:00Z'), // 1.7.0
@@ -185,7 +228,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.6.0',
versionCode: '1.6',
versionName: 'Surf\'s Up',
versionDate: new Date('2023-11-28T21:00:00Z'),
items: [
@@ -200,7 +243,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.5.0',
versionCode: '1.5',
versionName: 'Loaded!',
versionDate: new Date('2023-11-19T21:00:00Z'),
items: [
@@ -216,7 +259,7 @@ export const NewsItems: NewsItem[] = [
],
},
{
versionCode: '1.4.0',
versionCode: '1.4',
items: [
{ text: <><B>Share and clone</B> conversations, with public links</> },
{ text: <><B code='/docs/config-azure-openai.md'>Azure</B> models, incl. gpt-4-32k</> },
@@ -258,15 +301,6 @@ export const NewsItems: NewsItem[] = [
];
const wowStyle: SxProps = {
textDecoration: 'underline',
textDecorationThickness: '0.4em',
textDecorationColor: 'rgba(var(--joy-palette-primary-lightChannel) / 1)',
// textDecorationColor: 'rgba(0 255 0 / 0.5)',
textDecorationSkipInk: 'none',
// textUnderlineOffset: '-0.5em',
};
function B(props: {
// one-of
href?: string,
@@ -280,7 +314,6 @@ function B(props: {
props.issue ? `${Brand.URIs.OpenRepo}/issues/${props.issue}`
: props.code ? `${Brand.URIs.OpenRepo}/blob/main/${props.code}`
: props.href;
const isExtIcon = !props.issue;
const boldText = (
<Typography component='span' color={!!href ? 'primary' : 'neutral'} sx={{ fontWeight: 'lg' }}>
{props.children}
@@ -289,8 +322,8 @@ function B(props: {
if (!href)
return boldText;
return (
<Link href={href + clientUtmSource()} target='_blank' sx={props.wow ? wowStyle : undefined}>
{boldText} {isExtIcon ? <LaunchIcon sx={{ mx: 0.5, fontSize: 16 }} /> : <AutoStoriesOutlinedIcon sx={{ mx: 0.5, fontSize: 16 }} />}
</Link>
<ExternalLink href={href + clientUtmSource()} highlight={props.wow} icon={props.issue ? 'issue' : undefined}>
{boldText}
</ExternalLink>
);
}
+24 -5
View File
@@ -1,21 +1,40 @@
// NOTE: this is a separate file to help with bundle tracing, as it's included by the ProviderBootstrapLogic (i.e. by All pages)
// update this variable every time you want to broadcast a new version to clients
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { useAppStateStore } from '~/common/state/store-appstate';
export const incrementalNewsVersion: number = 14.1;
// update this variable every time you want to broadcast a new version to clients
export const incrementalNewsVersion: number = 16.1; // not notifying for 16.2
interface NewsState {
lastSeenNewsVersion: number;
}
export const useAppNewsStateStore = create<NewsState>()(
persist(
(set) => ({
lastSeenNewsVersion: 0,
}),
{
name: 'app-news',
},
),
);
export function shallRedirectToNews() {
const { usageCount, lastSeenNewsVersion } = useAppStateStore.getState();
const { lastSeenNewsVersion } = useAppNewsStateStore.getState();
const { usageCount } = useAppStateStore.getState();
const isNewsOutdated = (lastSeenNewsVersion || 0) < incrementalNewsVersion;
return isNewsOutdated && usageCount > 2;
}
export function markNewsAsSeen() {
const { setLastSeenNewsVersion } = useAppStateStore.getState();
setLastSeenNewsVersion(incrementalNewsVersion);
useAppNewsStateStore.setState({ lastSeenNewsVersion: incrementalNewsVersion });
}

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