Compare commits

...

743 Commits

Author SHA1 Message Date
Enrico Ros 0f86ad36ef Update Dockerfile for #840 2025-10-12 10:21:58 -07:00
Enrico Ros cd421b2f6e Update documentation for v1.16.11 final release and EOL 2025-10-09 14:33:22 -07:00
Enrico Ros 41b66e009a Finalize v1 README 2025-10-09 14:23:23 -07:00
Enrico Ros d96467f850 Finalize v1 legacy workflow - v1.1* only, as 'v1' 'v1-stable' 'v1.*' tags 2025-10-09 13:55:05 -07:00
Enrico Ros 258b19e2f4 Drawer: notice 2025-10-07 11:13:04 -07:00
Enrico Ros 01a87158b9 News: highlight first 2025-10-07 11:09:12 -07:00
Enrico Ros 46b768f9cf News: roll 2025-10-07 10:59:41 -07:00
Enrico Ros 4f0b6a5d09 Trade: rename exported file for convenience 2025-10-07 10:59:31 -07:00
Enrico Ros bfbcdb70fe News: show the links to 2.0 2025-10-07 10:59:09 -07:00
Enrico Ros 2c4602cf39 Add Node24 support 2025-10-07 09:46:34 -07:00
Enrico Ros 68f5d3946b Move tf pointers 2025-09-29 08:12:50 -07:00
Enrico Ros 14724a864c OpenRouter: compensate for older typescript 2025-04-05 10:30:15 -07:00
Enrico Ros 5e2b196c4d OpenRouter: models list: prevent schema changes from breaking working models. Fixes #787 2025-04-05 10:26:44 -07:00
Enrico Ros e7686f60b1 OpenRouter: models list: ignore missing fields on 'openrouter/auto'. Fixes #787 2025-04-05 10:25:35 -07:00
Enrico Ros 380f666d35 Roll Gemini descriptions. Fixes #783 2025-03-29 12:35:29 -07:00
Enrico Ros 3e277b1a35 Optional desc. #783 2025-03-29 12:35:05 -07:00
Enrico Ros 9bac46ea75 1.16.9 Release 2025-01-21 18:09:37 -08:00
Enrico Ros 2af4ee7dbe Remove v1-dev, fully absorbed into v2-dev. 2025-01-21 18:05:18 -08:00
Enrico Ros 590fc0d021 Gemini: relax parser - Fixes #700 2024-12-19 01:09:40 -08:00
Enrico Ros 746b0dad40 Update Node to 22 2024-12-19 01:08:41 -08:00
Enrico Ros b327da3ded Fix #675 (pre-v2) 2024-11-06 16:37:18 -08:00
Enrico Ros 7a818bdcd0 Update branch names 2024-10-28 20:09:53 -07:00
Enrico Ros c92ee2e22a v1: document branch names 2024-10-28 20:02:18 -07:00
Enrico Ros 632a4a565f [stable] OpenAI: update models 2024-10-25 10:13:13 -07:00
Enrico Ros d712c275a0 [stable] Anthropic: update models 2024-10-25 10:06:42 -07:00
Enrico Ros 1adff7481b Dev survey for Big-AGI 2. 2024-10-11 21:55:46 -07:00
Enrico Ros 393e19dda9 Vercel: fix timeout 2024-10-03 12:37:21 -07:00
Enrico Ros 39c5c7c9ba Call out to Big-AGI 2 2024-09-13 14:06:12 -07:00
Enrico Ros e64a5e59ef 1.16.8 Release 2024-09-13 13:50:00 -07:00
Enrico Ros 574c2cf0e3 Call out to Big-AGI 2 2024-09-13 13:49:11 -07:00
Enrico Ros 1d3321b336 OpenAI: o1 support label 2024-09-13 11:02:33 -07:00
Enrico Ros de25e5822d OpenAI: o1 relabel 2024-09-13 10:59:40 -07:00
Enrico Ros 6a904c9f37 OpenAI: 3.5 non legacy 2024-09-13 10:59:32 -07:00
Enrico Ros 30c3283572 OpenAI: add o1 2024-09-13 10:53:42 -07:00
Enrico Ros 10bba19079 OpenAI: add ChatGPT-4o-latest 2024-09-13 10:53:32 -07:00
Enrico Ros 713079f2f2 OpenAI: bits 2024-09-13 10:53:20 -07:00
Enrico Ros 6e16e989ac OpenAI: move 4o-mini 2024-09-13 10:53:09 -07:00
Enrico Ros 4e89e0b1e4 OpenAI: clean IDs 2024-09-13 10:52:19 -07:00
Enrico Ros 6067c289ab OpenAI: remove vision previews 2024-09-13 10:52:00 -07:00
Enrico Ros 32ebfea9cb OpenAI: reorder 2024-09-13 10:20:52 -07:00
Enrico Ros dec280d54d 1.16.7 Release
(cherry picked from commit 22b32d571d)
2024-08-07 02:51:59 -07:00
Enrico Ros 4823e97783 Mapping doc, for the future.
(cherry picked from commit a416cafc4e)
2024-08-07 02:51:59 -07:00
Enrico Ros 6a5685995f OpenAI: update models
(cherry picked from commit 5f5efe6133)
2024-08-07 02:51:59 -07:00
Enrico Ros 3b4d5691d7 1.16.6: Release. Fixes #604 2024-07-24 21:31:57 -07:00
Enrico Ros 45c09d021a Groq: update output tokens (max 8,000 for 3.1) 2024-07-24 21:27:20 -07:00
Enrico Ros 8ef759fe0f Groq: update Models 2024-07-24 21:27:12 -07:00
Enrico Ros c06735fdd2 1.16.5: Release 2024-07-18 16:15:53 -07:00
Enrico Ros cf4297a1af OpenAI: support 4o Mini (16384 token output) 2024-07-18 16:15:37 -07:00
Enrico Ros 5d458d68bd Warn devs. 2024-07-18 16:12:17 -07:00
Enrico Ros c3db077ae8 1.16.4: release 2024-07-15 14:13:36 -07:00
Enrico Ros 779b265b20 Anthropic: 8192 tokens 2024-07-15 14:08:02 -07:00
Enrico Ros 7d6d7e619b Anthropic: hardcode date 2024-06-20 12:42:10 -07:00
Enrico Ros 34caa16e39 1.16.3: release 2024-06-20 12:27:42 -07:00
Enrico Ros 976426dbd3 Anthropic: support Claude 3.5 Sonnet 2024-06-20 12:27:26 -07:00
Enrico Ros b4d8e39d56 Gemini: acknowledge the new capability to createCachedContent. Fixes #565 2024-06-10 23:56:02 -07:00
Enrico Ros 11c41e7381 Function call: increase debug verbosity 2024-06-07 14:18:01 -07:00
Enrico Ros 358d8a54ff Increase llms alignment before function calling. 2024-06-07 14:11:36 -07:00
Enrico Ros 3c8fedce68 Highlight issues with chatGenerateWithFunctions 2024-06-07 12:38:21 -07:00
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
316 changed files with 13862 additions and 4974 deletions
+22 -10
View File
@@ -11,11 +11,8 @@ name: Create and publish Docker images
on:
push:
branches:
- main
#- main-stable # Disabled as the v* tag is used for stable releases
tags:
- 'v*' # Trigger on version tags (e.g., v1.7.0)
- 'v1.1*' # V1 legacy tags only (v1.10.x - v1.19.x range)
env:
REGISTRY: ghcr.io
@@ -32,6 +29,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:
@@ -45,17 +48,26 @@ jobs:
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=development,enable=${{ github.ref == 'refs/heads/main' }}
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
# V1 legacy pointers
type=raw,value=v1
type=raw,value=v1-stable
# Exact version tags (v1.16.11 and 1.16.11)
type=ref,event=tag
type=semver,pattern={{version}}
- 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 }}
labels: |
org.opencontainers.image.title=Big-AGI v1 (Legacy)
org.opencontainers.image.description=Big-AGI v1 - Legacy version. For the latest version, see GitHub and big-agi.com
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
org.opencontainers.image.documentation=https://big-agi.com
build-args: |
NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
+12 -6
View File
@@ -1,6 +1,6 @@
# Base
FROM node:18-alpine AS base
ENV NEXT_TELEMETRY_DISABLED 1
FROM node:22-alpine AS base
ENV NEXT_TELEMETRY_DISABLED=1
# Dependencies
@@ -11,8 +11,11 @@ WORKDIR /app
COPY package*.json ./
COPY src/server/prisma ./src/server/prisma
# link ssl3 for latest Alpine
RUN sh -c '[ ! -e /lib/libssl.so.3 ] && ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3 || echo "Link already exists"'
# Install dependencies, including dev (release builds should use npm ci)
ENV NODE_ENV development
ENV NODE_ENV=development
RUN npm ci
@@ -28,8 +31,11 @@ ENV NEXT_PUBLIC_GA4_MEASUREMENT_ID=${NEXT_PUBLIC_GA4_MEASUREMENT_ID}
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# link ssl3 for latest Alpine
RUN sh -c '[ ! -e /lib/libssl.so.3 ] && ln -s /usr/lib/libssl.so.3 /lib/libssl.so.3 || echo "Link already exists"'
# Build the application
ENV NODE_ENV production
ENV NODE_ENV=production
RUN npm run build
# Reduce installed packages to production-only
@@ -51,8 +57,8 @@ COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/src/server/prisma ./src/server/prisma
# Minimal ENV for production
ENV NODE_ENV production
ENV PATH $PATH:/app/node_modules/.bin
ENV NODE_ENV=production
ENV PATH=$PATH:/app/node_modules/.bin
# Run as non-root user
USER nextjs
+38 -230
View File
@@ -1,233 +1,41 @@
# BIG-AGI 🧠
Welcome to big-AGI 👋, the GPT application 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.
Pros use big-AGI. 🚀 Developers 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)
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)
big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap
### What's New in 1.14.1 · March 7, 2024 · Modelmorphic
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
- New **[Perplexity](https://www.perplexity.ai/)** and **[Groq](https://groq.com/)** integration (thanks @Penagwin). [#407](https://github.com/enricoros/big-AGI/issues/407), [#427](https://github.com/enricoros/big-AGI/issues/427)
- **[LocalAI](https://localai.io/models/)** deep integration, including support for [model galleries](https://github.com/enricoros/big-AGI/issues/411)
- **Mistral** Large and Google **Gemini 1.5** support
- Performance optimizations: runs [much faster](https://twitter.com/enricoros/status/1756553038293303434?utm_source=localhost:3000&utm_medium=big-agi), saves lots of power, reduces memory usage
- Enhanced UX with auto-sizing charts, refined search and folder functionalities, perfected scaling
- And with more UI improvements, documentation, bug fixes (20 tickets), and developer enhancements
- [Release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.14.0), and changes [v1.13.1...v1.14.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
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
- **Side-by-Side Split Windows**: multitask with parallel conversations. [#208](https://github.com/enricoros/big-AGI/issues/208)
- **Multi-Chat Mode**: message everyone, all at once. [#388](https://github.com/enricoros/big-AGI/issues/388)
- **Export tables as CSV**: big thanks to @aj47. [#392](https://github.com/enricoros/big-AGI/pull/392)
- Adjustable text size: customize density. [#399](https://github.com/enricoros/big-AGI/issues/399)
- Dev2 Persona Technology Preview
- 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>
<summary>What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline</summary>
https://github.com/enricoros/big-AGI/assets/32999/95ceb03c-945d-4fdd-9a9f-3317beb54f3f
- **Voice Calls**: real-time voice call your personas out of the blue or in relation to a chat [#354](https://github.com/enricoros/big-AGI/issues/354)
- Support **OpenAI 0125** Models. [#364](https://github.com/enricoros/big-AGI/issues/364)
- Rename or Auto-Rename chats. [#222](https://github.com/enricoros/big-AGI/issues/222), [#360](https://github.com/enricoros/big-AGI/issues/360)
- More control over **Link Sharing** [#356](https://github.com/enricoros/big-AGI/issues/356)
- **Accessibility** to screen readers [#358](https://github.com/enricoros/big-AGI/issues/358)
- Export chats to Markdown [#337](https://github.com/enricoros/big-AGI/issues/337)
- Paste tables from Excel [#286](https://github.com/enricoros/big-AGI/issues/286)
- Ollama model updates and context window detection fixes [#309](https://github.com/enricoros/big-AGI/issues/309)
</details>
<details>
<summary>What's New in 1.11.0 · Jan 16, 2024 · Singularity</summary>
https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cfcb110c68
- **Find chats**: search in titles and content, with frequency ranking. [#329](https://github.com/enricoros/big-AGI/issues/329)
- **Commands**: command auto-completion (type '/'). [#327](https://github.com/enricoros/big-AGI/issues/327)
- **[Together AI](https://www.together.ai/products#inference)** inference platform support (good speed and newer models). [#346](https://github.com/enricoros/big-AGI/issues/346)
- Persona Creator history, deletion, custom creation, fix llm API timeouts
- Enable adding up to five custom OpenAI-compatible endpoints
- Developer enhancements: new 'Actiles' framework
</details>
<details>
<summary>What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI</summary>
- **New UI**: for both desktop and mobile, sets the stage for future scale. [#201](https://github.com/enricoros/big-AGI/issues/201)
- **Conversation Folders**: enhanced conversation organization. [#321](https://github.com/enricoros/big-AGI/issues/321)
- **[LM Studio](https://lmstudio.ai/)** support and improved token management
- Resizable panes in split-screen conversations.
- Large performance optimizations
- Developer enhancements: new UI framework, updated documentation for proxy settings on browserless/docker
</details>
For full details and former releases, check out the [changelog](docs/changelog.md).
## ✨ 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 |
![big-AGI screenshot](docs/pixels/big-AGI-compo-20240201_small.png)
You can easily configure 100s of AI models in big-AGI:
| **AI models** | _supported vendors_ |
|:--------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Opensource Servers | [LocalAI](https://localai.com) (multimodal) · [Ollama](https://ollama.com/) · [Oobabooga](https://github.com/oobabooga/text-generation-webui) |
| Local Servers | [LM Studio](https://lmstudio.ai/) |
| Multimodal services | [Azure](https://azure.microsoft.com/en-us/products/ai-services/openai-service) · [Google Gemini](https://ai.google.dev/) · [OpenAI](https://platform.openai.com/docs/overview) |
| Language services | [Anthropic](https://anthropic.com) · [Groq](https://wow.groq.com/) · [Mistral](https://mistral.ai/) · [OpenRouter](https://openrouter.ai/) · [Perplexity](https://www.perplexity.ai/) · [Together AI](https://www.together.ai/) |
| Image services | [Prodia](https://prodia.com/) (SDXL) |
| Speech services | [ElevenLabs](https://elevenlabs.io) (Voice synthesis / cloning) |
Add extra functionality with these integrations:
| **More** | _integrations_ |
|:-------------|:---------------------------------------------------------------------------------------------------------------|
| Web Browse | [Browserless](https://www.browserless.io/) · [Puppeteer](https://pptr.dev/)-based |
| Web Search | [Google CSE](https://programmablesearchengine.google.com/) |
| Code Editors | [CodePen](https://codepen.io/pen/) · [StackBlitz](https://stackblitz.com/) · [JSFiddle](https://jsfiddle.net/) |
| Sharing | [Paste.gg](https://paste.gg/) (Paste chats) |
| Tracking | [Helicone](https://www.helicone.ai) (LLM Observability) |
[//]: # (- [x] **Flow-state UX** for uncompromised productivity)
[//]: # (- [x] **AI Personas**: Tailor your AI interactions with customizable personas)
[//]: # (- [x] **Sleek UI/UX**: A smooth, intuitive, and mobile-responsive interface)
[//]: # (- [x] **Efficient Interaction**: Voice commands, OCR, and drag-and-drop file uploads)
[//]: # (- [x] **Privacy First**: Self-host and use your own API keys for full control)
[//]: # (- [x] **Advanced Tools**: Execute code, import PDFs, and summarize documents)
[//]: # (- [x] **Seamless Integrations**: Enhance functionality with various third-party services)
[//]: # (- [x] **Open Roadmap**: Contribute to the progress of big-AGI)
<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;)
[![Official Discord](https://discordapp.com/api/guilds/1098796266906980422/widget.png?style=banner2)](https://discord.gg/MkH4qj2Jp9)
- [ ] 📢️ [**Chat with us** on Discord](https://discord.gg/MkH4qj2Jp9)
- [ ]**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) |
<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;)
[//]: # ([![GitHub pull requests]&#40;https://img.shields.io/github/issues-pr/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/pulls&#41;)
[//]: # ([![License]&#40;https://img.shields.io/github/license/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/LICENSE&#41;)
# BIG-AGI Legacy
> **⚠️ IMPORTANT: `v1` Branch - End of Life**
>
> This is the **v1** (formerly **v1-stable**) branch, which reached end-of-life in October 2025.
> - **For the latest Big-AGI**, see the [**main** branch](https://github.com/enricoros/big-AGI/tree/main) and [**big-agi.com**](https://big-agi.com)
> - **v1.16.11 is the final legacy release** - No further updates will be provided
> - Docker users: `:stable`, `:development` and `:latest` point to the `main` branch
---
2023-2024 · Enrico Ros x [big-AGI](https://big-agi.com) · License: [MIT](LICENSE) · Made with 💙
**For the latest Big-AGI:**
- [**Big-AGI Open**](https://github.com/enricoros/big-AGI/tree/main) - Open Source, for self-hosting, with bleeding edge models support
- [**Big-AGI Pro**](https://big-agi.com) - Hosted for Professionals with extra services and Cloud Sync
---
### What's New in 1.16.11 · October 2025
- Final v1 legacy release. Branch reaches end-of-life.
### What's New in 1.16.1...1.16.10 · 2024-2025 (patch releases)
- 1.16.10: Openrouter models fixes
- 1.16.9: Docker Gemini fix (R1 models are supported in latest Big-AGI)
- 1.16.8: OpenAI ChatGPT-4o Latest (o1 models are supported in latest Big-AGI)
- 1.16.7: OpenAI support for GPT-4o 2024-08-06
- 1.16.6: Groq support for Llama 3.1 models
- 1.16.5: GPT-4o Mini support
- 1.16.4: 8192 tokens support for Claude 3.5 Sonnet
- 1.16.3: Anthropic Claude 3.5 Sonnet model support
- 1.16.2: Improve web downloads, as text, markdwon, or HTML
- 1.16.2: Proper support for Gemini models
- 1.16.2: Added the latest Mistral model
- 1.16.2: Tokenizer support for gpt-4o
- 1.16.2: Updates to Beam
- 1.16.1: Support for the new OpenAI GPT-4o 2024-05-13 model
---
2023-2025 · Enrico Ros x [Big-AGI](https://big-agi.com) · License: [MIT](LICENSE) · Made with 💙
+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 };
+15 -19
View File
@@ -1,6 +1,8 @@
# big-AGI Documentation
# big-AGI v1 Documentation (Legacy)
Find all the information you need to get started, configure, and effectively use big-AGI.
> **Note:** This is documentation for the **v1 legacy branch**. For the latest Big-AGI, see the [main branch](https://github.com/enricoros/big-AGI/tree/main) or visit [big-agi.com](https://big-agi.com).
Find all the information you need to get started, configure, and effectively use big-AGI v1.
[//]: # (## Quick Start)
@@ -28,38 +30,32 @@ 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
System integrators, administrators, whitelabelers: instead of using the public big-AGI instance on get.big-agi.com, you can deploy your own instance.
System integrators, administrators, whitelabelers: instead of using the public big-AGI instance on app.big-agi.com, you can deploy your own instance.
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
Join our community or get support:
- Visit our [GitHub repository](https://github.com/enricoros/big-AGI) for source code and issue tracking
- Check the latest updates and features on [Changelog](changelog.md) or the in-app [News](https://get.big-agi.com/news)
- Check the latest updates and features on [Changelog](changelog.md) or the in-app [News](https://big-agi.com/news)
- Connect with us and other users on [Discord](https://discord.gg/MkH4qj2Jp9) for discussions, help, and sharing your experiences with big-AGI
Thank you for choosing big-AGI. We're excited to see what you'll build.
+45 -8
View File
@@ -1,16 +1,53 @@
## Changelog
## Changelog (v1 Legacy Branch)
This is a high-level changelog. Calls out some of the high level features batched
by release.
This is a high-level changelog for the v1 legacy branch. For the latest Big-AGI, see the [main branch](https://github.com/enricoros/big-AGI).
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
### 1.16.11 · October 2025 (Final Release)
### 1.15.0 - Mar 2024
Final v1 legacy release. Branch reaches end-of-life.
Prediction: OpenAI will release GPT-5 on March 14, 2024. We will support it on day 1.
### What's New in 1.16.1...1.16.10 · 2024-2025 (patch releases)
- milestone: [1.15.0](https://github.com/enricoros/big-agi/milestone/15)
- 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)
- 1.16.9: Docker Gemini fix (R1 models are supported in Big-AGI 2)
- 1.16.8: OpenAI ChatGPT-4o Latest (o1 models are supported in Big-AGI 2)
- 1.16.7: OpenAI support for GPT-4o 2024-08-06
- 1.16.6: Groq support for Llama 3.1 models
- 1.16.5: GPT-4o Mini support
- 1.16.4: 8192 tokens support for Claude 3.5 Sonnet
- 1.16.3: Anthropic Claude 3.5 Sonnet model support
- 1.16.2: Improve web downloads, as text, markdwon, or HTML
- 1.16.2: Proper support for Gemini models
- 1.16.2: Added the latest Mistral model
- 1.16.2: Tokenizer support for gpt-4o
- 1.16.2: Updates to Beam
- 1.16.1: 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
+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. |
+6 -2
View File
@@ -1,4 +1,8 @@
# Deploying `big-AGI` with Docker
# Deploying `big-AGI` with Docker (v1 Legacy)
> **Note:** This documentation is for the **v1 legacy branch**. For the latest Big-AGI, use Docker tags `:latest`, `:stable`, or `:development` which point to the [main branch](https://github.com/enricoros/big-AGI/tree/main).
>
> To use v1 legacy specifically, use Docker tags `:v1` or `:v1-stable`.
Utilize Docker containers to deploy the big-AGI application for an efficient and automated deployment process.
Docker ensures faster development cycles, easier collaboration, and seamless environment management.
@@ -9,7 +13,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).
+1379 -776
View File
File diff suppressed because it is too large Load Diff
+33 -27
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,51 +38,56 @@
"@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"
"node": "^24.0.0 || ^22.0.0 || ^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` }
: {}),
}}
>
+248 -257
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, [
const imaginedPrompt = await imaginePromptFromText(messageText, conversationId) || 'An error sign.';
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>
);
}
+192 -87
View File
@@ -1,15 +1,17 @@
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, Card, CardContent, 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 RocketLaunchRoundedIcon from '@mui/icons-material/RocketLaunchRounded';
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
import type { DConversationId } from '~/common/state/store-chats';
import { CloseableMenu } from '~/common/components/CloseableMenu';
@@ -17,8 +19,10 @@ import { DFolder, useFolderStore } from '~/common/state/store-folders';
import { DebounceInputMemo } from '~/common/components/DebounceInput';
import { FoldersToggleOff } from '~/common/components/icons/FoldersToggleOff';
import { FoldersToggleOn } from '~/common/components/icons/FoldersToggleOn';
import { Link } from '~/common/components/Link';
import { PageDrawerHeader } from '~/common/layout/optima/components/PageDrawerHeader';
import { PageDrawerList } from '~/common/layout/optima/components/PageDrawerList';
import { ROUTE_APP_NEWS } from '~/common/app.routes';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { themeScalingMap, themeZIndexOverMobileDrawer } from '~/common/app.theme';
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
@@ -26,9 +30,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 +41,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 +54,7 @@ export const useFolders = (activeFolderId: string | null) => useFolderStore(({ e
enableFolders,
toggleEnableFolders,
};
}, shallow);
}));
export const ChatDrawerMemo = React.memo(ChatDrawer);
@@ -74,20 +78,32 @@ 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);
const [bigAgi2CalloutDismissed, setBigAgi2CalloutDismissed] = React.useState(() => {
try {
return localStorage.getItem('dismissedBA2ChatDrawerNotice') === 'true';
} catch {
return false;
}
});
// 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
@@ -119,6 +135,15 @@ function ChatDrawer(props: {
props.activeConversationId && onConversationsExportDialog(props.activeConversationId, true);
}, [onConversationsExportDialog, props.activeConversationId]);
const handleDismissBigAgi2Callout = React.useCallback(() => {
setBigAgi2CalloutDismissed(true);
try {
localStorage.setItem('dismissedBA2ChatDrawerNotice', 'true');
} catch {
// ignore
}
}, []);
// Folder change request
@@ -140,6 +165,7 @@ function ChatDrawer(props: {
// memoize the group dropdown
const { isSearching } = isDrawerSearching(debouncedSearchQuery);
const groupingComponent = React.useMemo(() => (
<Dropdown>
<MenuButton
@@ -147,34 +173,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 +241,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 +264,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,89 +279,129 @@ 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,
)}
</Box>
{/* Big-AGI 2.0 Callout */}
{!bigAgi2CalloutDismissed && (
<Box sx={{ p: 2 }}>
<Card variant='solid' color='primary' invertedColors>
<CardContent sx={{ gap: 1, position: 'relative' }}>
<IconButton
size='sm'
onClick={handleDismissBigAgi2Callout}
sx={{
position: 'absolute',
top: -4,
right: -8,
}}
>
<ClearIcon />
</IconButton>
<Typography level='title-sm'>
Big-AGI 2.0 is Live!
</Typography>
<Typography level='body-xs' sx={{ mb: 1 }}>
Experience Beam 2, Personas, and Cloud Sync.
</Typography>
<Button
fullWidth
size='sm'
variant='solid'
color='neutral'
endDecorator={<RocketLaunchRoundedIcon />}
component={Link}
href={ROUTE_APP_NEWS}
noLinkStyle
>
Learn More
</Button>
</CardContent>
</Card>
</Box>
)}
<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>;
};
+418 -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,22 @@ 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;
// WARNING: if you get an issue here, you're downgrading from the new Big-AGI 2 data format to 1.x.
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 +296,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 +349,9 @@ export function ChatMessage(props: {
e.preventDefault();
if (props.onTextDiagram) {
await props.onTextDiagram(messageId, textSel);
closeOpsMenu();
handleCloseOpsMenu();
closeSelectionMenu();
closeToolbar();
}
};
@@ -308,8 +359,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 +379,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 +422,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 +443,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 +523,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 +666,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 +680,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 +714,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 +757,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 +856,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, VChatMessageIn, VChatStreamContextName } 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: VChatStreamContextName,
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}
/>,
)}
+95 -49
View File
@@ -1,40 +1,31 @@
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 ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
import ArrowOutwardRoundedIcon from '@mui/icons-material/ArrowOutwardRounded';
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';
import { bigAgi2NewsCallout, bigAgi2Url } from './bigAgi2.data';
import { downloadAllConversationsJson } from '~/modules/trade/trade.client';
// 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 +60,17 @@ 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;
const currentVer = '1.6.9'; // firstNews?.versionCode;
return (
<Box sx={{
@@ -89,26 +85,41 @@ export function AppNews() {
display: 'flex', flexDirection: 'column', alignItems: 'center',
}}>
<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>!
<Typography level='h1' sx={{ fontSize: '2.7rem', mb: 4 }}>
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${animationColorBlues} 10s infinite`, zIndex: 1 /* perf-opt */ }}>{currentVer}</Box>!
</Typography>
<Typography sx={{ mb: 2 }} level='title-sm'>
{capitalizeFirstLetter(Brand.Title.Base)} has been updated to version {firstNews?.versionCode}
<Typography level='title-sm' sx={{ mb: 2, textAlign: 'center', lineHeight: 'lg' }} >
{capitalizeFirstLetter(Brand.Title.Base)} has been updated to version {currentVer}.<br/>
<b>And a whole-new 2.0 is waiting!</b>
</Typography>
<Box sx={{ mb: 5 }}>
<Box sx={{ mb: 5, display: 'flex', gap: 2, flexWrap: 'wrap', justifyContent: 'center' }}>
<Button
variant='solid' color='primary' size='lg'
variant='solid' color='neutral' size='lg'
component={Link} href={ROUTE_INDEX} noLinkStyle
endDecorator='✨'
endDecorator={<ArrowForwardRoundedIcon />}
// endDecorator='✨'
sx={{
boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
// boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
minWidth: 180,
}}
>
Continue
</Button>
<Button
variant='solid' color='primary' size='lg'
component={Link} href={bigAgi2Url} noLinkStyle
endDecorator={<><ArrowOutwardRoundedIcon /></>}
// endDecorator='✨'
sx={{
boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
minWidth: 180,
transform: 'translateY(-1px)',
}}
>
Big-AGI 2
</Button>
</Box>
{/*<Typography level='title-sm' sx={{ mb: 1, placeSelf: 'start', ml: 1 }}>*/}
@@ -117,14 +128,26 @@ 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 firstCard = idx === 0;
const addPadding = false; //!firstCard; // || showExpander;
return <React.Fragment key={idx}>
{/* Inject the Big-AGI 2.0 item here*/}
{idx === 0 && (
<Box sx={{ mb: 3 }}>
{bigAgi2NewsCallout}
</Box>
)}
{/* 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 }}>
<Card color={firstCard ? 'primary' : undefined} key={'news-' + idx} sx={{ mb: 3, minHeight: 32, gap: 1 }}>
<CardContent sx={{ position: 'relative', pr: addPadding ? 4 : 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Typography level='title-sm' component='div'>
@@ -132,9 +155,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 +171,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 +181,27 @@ 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>
)}
{firstCard && <Box sx={{ mt: 2 }}>
<Card variant='soft' color='primary' invertedColors>
<CardContent>
<Typography level='title-sm' sx={{ lineHeight: 'lg' }}>
<b>Migrate your chats:</b> you can download all your conversations at any time by clicking
on &quot;Export&quot; &gt; &quot;Download All&quot; or the button below. Then open Big-AGI 2 and
import the conversation by clicking &quot;Organize&quot; &gt; &quot;Import&quot;.
</Typography>
<Button
size='sm'
variant='soft'
color='primary'
sx={{ mt: 1 }}
onClick={downloadAllConversationsJson}
>
Download All Conversations
</Button>
</CardContent>
</Card>
</Box>}
</CardContent>
{!!ni.versionCoverImage && (
@@ -184,14 +215,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 +232,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' }}>*/}
+42
View File
@@ -0,0 +1,42 @@
import * as React from 'react';
import { Button, Card, CardContent, Grid, Typography } from '@mui/joy';
import LaunchIcon from '@mui/icons-material/Launch';
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>;
+52
View File
@@ -0,0 +1,52 @@
import * as React from 'react';
import { Button, Card, CardContent, Grid, Typography } from '@mui/joy';
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import LaunchIcon from '@mui/icons-material/Launch';
import RocketLaunchRounded from '@mui/icons-material/RocketLaunchRounded';
import SupportAgentIcon from '@mui/icons-material/SupportAgent';
import { Link } from '~/common/components/Link';
import { clientUtmSource } from '~/common/util/pwaUtils';
export const bigAgi2Url = 'https://app.big-agi.com' + clientUtmSource('upgrade');
const bigAgiSupport = 'https://form.typeform.com/to/nLf8gFmx?utm_source=big-agi-1&utm_medium=app&utm_campaign=support';
export const bigAgi2NewsCallout =
<Card variant='solid' color='primary' invertedColors>
<CardContent sx={{ gap: 2 }}>
<Typography level='title-lg'>
Big-AGI 2.0 - Now Live
</Typography>
<Typography level='title-sm' sx={{ lineHeight: 'xl' }}>
Experience the <b>next generation of Big-AGI</b> with <b>Beam 2</b>, <b>Personas</b>, and <b>Cloud Sync</b> to never lose data.
</Typography>
<Grid container spacing={1}>
<Grid xs={12} sm={7}>
<Button
size='lg'
fullWidth variant='solid' color='neutral' endDecorator={<RocketLaunchRounded />}
component={Link} href={bigAgi2Url} noLinkStyle target='_blank'
>
Big-AGI 2.0
</Button>
</Grid>
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
<Button
fullWidth variant='soft' color='primary' endDecorator={<SupportAgentIcon />}
component={Link} href={bigAgiSupport} noLinkStyle target='_blank'
// disabled
>
Support
</Button>
</Grid>
</Grid>
</CardContent>
</Card>;

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