Compare commits

..

1111 Commits

Author SHA1 Message Date
Enrico Ros 5c44804d50 Desktop: innovate 2024-07-08 15:48:26 -07:00
Enrico Ros 8589376c66 Desktop: improve looks 2024-07-08 15:33:31 -07:00
Enrico Ros d53a8b4941 Desktop: move files around 2024-07-08 15:08:16 -07:00
Enrico Ros af819da623 Desktop: electron build 2024-07-08 15:02:02 -07:00
Enrico Ros 3addc4e2ac AIX: fix build 2024-07-07 05:02:37 -07:00
Enrico Ros 7ff7e489ab Merge branch 'refs/heads/main' into feature-multipart
# Conflicts:
#	src/apps/chat/AppChat.tsx
#	src/apps/chat/components/ChatMessageList.tsx
#	src/apps/personas/creator/Creator.tsx
2024-07-07 04:43:21 -07:00
Enrico Ros 95aa0da014 Merge branch 'fork/mapringg/mac-shortcuts' 2024-07-07 04:21:24 -07:00
Enrico Ros b12637267b Small cleanups with shortcut fixes 2024-07-07 04:20:23 -07:00
Enrico Ros 3a44f70db9 AI Persona Creator: Update the react state after 575 2024-07-07 03:55:55 -07:00
Enrico Ros 92206d9740 Merge pull request #575
[BUG] Fixes text on AI Personas Creator panel
2024-07-07 03:52:30 -07:00
Enrico Ros bddd91df2a Merge branch 'fork/mapringg/feature/deepseek-vendor' 2024-07-07 03:36:51 -07:00
Enrico Ros 144ead8cfe Deepseek: cleanups
Mostly reordered the properties in alphabetical order
and made sure that models are listed dynamically (for future changes)
2024-07-07 03:34:45 -07:00
Enrico Ros 185f8e7f44 roll tRPC w/ #5851 2024-07-07 02:52:39 -07:00
Enrico Ros 1538cd83af roll packages 2024-07-07 02:49:06 -07:00
Sorawit Kongnurat 027f7deb3a feat: implement deepseek vendor 2024-07-07 16:02:08 +07:00
Enrico Ros 4043a6098b Aix: handle abort signals on both server and client 2024-07-06 06:24:01 -07:00
Enrico Ros 92b913be98 Aix: use backend fetchers 2024-07-06 05:52:11 -07:00
Enrico Ros 8505ba6b84 Backend Fetchers: improve 2024-07-06 05:38:26 -07:00
Enrico Ros c6973f6b4e OpenRouter: remove non-free 2024-07-06 05:21:10 -07:00
Enrico Ros 94eddaff3f Backend fetchers: allow signals 2024-07-06 04:35:58 -07:00
Sorawit Kongnurat f38be4aff3 feat: replace useGlobalShortcut with useGlobalShortcuts
Ignore alt key for mac users.
2024-07-06 15:41:45 +07:00
Sorawit Kongnurat 3ea78fcf9f Merge branch 'main' into mac-shortcuts 2024-07-06 14:31:58 +07:00
Enrico Ros 78cfcc6206 AIX: improve Gemini and issue reporting 2024-07-05 18:45:23 -07:00
Enrico Ros 9c5d4a18ce Audio: Wire Chime (disabled) 2024-07-05 18:31:05 -07:00
Enrico Ros aa48b4d596 Audio: Add Generator 2024-07-05 17:11:10 -07:00
Enrico Ros 265acd9345 Audio: Add Generator 2024-07-05 16:38:41 -07:00
Enrico Ros 34ec1d5671 Audio: Port Player 2024-07-05 16:33:19 -07:00
Enrico Ros 4a1f4f0a01 Bits 2024-07-05 13:42:47 -07:00
Enrico Ros 850528820f AIX: define new low-level APIs 2024-07-05 02:55:34 -07:00
Enrico Ros 4dc8197c51 paste over 2024-07-04 16:28:04 -07:00
Enrico Ros 42e97eed4c Persona: port Throttle 2024-07-04 03:18:07 -07:00
Enrico Ros 065f30ac38 Persona: port AutoSpeak 2024-07-04 02:55:10 -07:00
Enrico Ros 9e705a12b1 Bits 2024-07-04 02:09:43 -07:00
Enrico Ros b8144f0748 AIX: flow cleanup 1 2024-07-04 01:29:56 -07:00
Enrico Ros e5b5faad3e Style2 2024-07-03 22:36:11 -07:00
Enrico Ros f840c1d424 Style 2024-07-03 20:30:36 -07:00
Enrico Ros eabd268874 AIX: client-side wire 2024-07-03 20:19:52 -07:00
Enrico Ros 06aadc543a AIX: redo all uplink parsers 2024-07-03 19:58:11 -07:00
Enrico Ros 2a410f52b5 AIX: improved all uplinks 2024-07-03 19:57:09 -07:00
Enrico Ros eb7a32ed16 AIX: redo the Upstream engine 2024-07-03 15:55:19 -07:00
Enrico Ros 14118d3056 Tokens: rationalize 2024-07-03 11:42:46 -07:00
Enrico Ros c8b3d8ad9b AIX: exception trap 2024-07-03 01:52:15 -07:00
Enrico Ros a097b32d5c AIX: types migration 2024-07-03 01:31:13 -07:00
Enrico Ros 0a88a9cee6 Cleanup Execute Mode labels 2024-07-03 00:17:36 -07:00
Enrico Ros bef1c0c5fc Extract ChatExecuteMode 2024-07-02 23:51:24 -07:00
Enrico Ros 52e6ef436f change ChatModeIds 2024-07-02 22:48:17 -07:00
Enrico Ros ad0617de90 roll PDFjs 2024-07-02 21:40:09 -07:00
Enrico Ros 1753c1a40a AutoSuggestions: controllable LLM 2024-07-02 21:37:49 -07:00
Enrico Ros 13b7004959 roll packages 2024-07-02 21:33:25 -07:00
Enrico Ros 3b9a21bbf7 AIX: remove tRPC router: not ready - will hand-roll it 2024-07-02 21:32:02 -07:00
Enrico Ros 5f0beb9d00 AIX: move stream debug 2024-07-02 00:38:03 -07:00
Enrico Ros 8411a73589 AIX: test router and client 2024-07-01 23:29:24 -07:00
Enrico Ros 009a3751c0 AIX: update fc schema 2024-07-01 23:28:52 -07:00
Enrico Ros adef88e358 roll packages 2024-07-01 22:44:14 -07:00
Enrico Ros f8b9df7bf0 AIX: function calling types 2024-07-01 21:58:18 -07:00
Enrico Ros c6fa3e1d24 Export: update backup file name 2024-07-01 21:26:31 -07:00
Enrico Ros ae24dd1e28 Bits 2024-07-01 21:26:31 -07:00
Enrico Ros 1efca7dd48 Ollama: update models 2024-06-28 02:32:35 -07:00
Jason Baker 3178f4e7e9 Fixes text on AI Personas Creator panel 2024-06-28 00:12:20 -07:00
Enrico Ros e00f61dcd0 MP: bits 2024-06-25 12:18:02 -07:00
Enrico Ros 6a5774aae7 MP: bifurcate persona generation 2024-06-25 03:27:38 -07:00
Enrico Ros 5119061861 MP: improve history editing 2024-06-25 03:11:52 -07:00
Enrico Ros fdfbae334a MP: re-enable reply to... 2024-06-25 03:03:59 -07:00
Enrico Ros e3fce43e62 Merge pull request #572 from JeremiLorenti/patch-1
Update DallESettings.tsx
2024-06-25 02:15:25 -07:00
Enrico Ros 9251f8ff0e MP: re-enable auto-draw... 2024-06-25 01:49:16 -07:00
Enrico Ros 18ef40f6f4 MP: bifurcate generate-text 2024-06-25 01:42:32 -07:00
Enrico Ros 46887d1d9f MP: reuse more fragment functions 2024-06-25 01:28:57 -07:00
Enrico Ros 632d10e9e3 MP: fix the execution pipeline 2024-06-25 01:08:56 -07:00
Enrico Ros 9fa33eea73 MP: fix editing 2024-06-24 22:29:21 -07:00
Enrico Ros 2c4c13bc2c MP: fix 'ph' reducing and token counting 2024-06-24 22:16:03 -07:00
Enrico Ros 33f8a4eb3a Message pricing: show min, and max on hover 2024-06-24 22:15:29 -07:00
Jeremi Lorenti aa7959a970 Update DallESettings.tsx
Changed the label for the "Hyper-Real" setting from "Relistic" to "Realistic" to correct the spelling mistake and improve clarity in the UI.
2024-06-25 01:12:42 -04:00
Enrico Ros 7471bc0bb2 roll packages 2024-06-24 21:41:22 -07:00
Enrico Ros b257f75e53 Screen Capture: enable for everyone 2024-06-24 20:25:13 -07:00
Enrico Ros 455e279216 MP: enter Docs 2024-06-24 20:19:43 -07:00
Enrico Ros 7fd359852a Browse: get page title too 2024-06-24 18:44:34 -07:00
Enrico Ros 82ecfdbd37 DMessage: remove Sender 2024-06-24 17:41:50 -07:00
Enrico Ros 478452983f MP: port to Embeds 2024-06-24 17:19:13 -07:00
Enrico Ros 5c1a7d485f Merge pull request #570 from blakkd/patch-1
Correct LocalAI URL
2024-06-24 04:39:56 -07:00
Enrico Ros 39c4ce9240 bits 2024-06-24 03:16:58 -07:00
Enrico Ros da49585df5 MP: improve Ego-Fragments 2024-06-24 01:34:54 -07:00
Enrico Ros 0b9bee02fe MP: begin converting Attachments 2024-06-23 21:33:40 -07:00
Enrico Ros 00e5d1ae27 MP: move towards typed parts 2024-06-23 21:16:03 -07:00
blakkd b290d63926 Correct LocalAI URL 2024-06-24 06:10:30 +02:00
Enrico Ros 1b5438cc6c MP: renames 2024-06-23 20:44:48 -07:00
Enrico Ros 17323facce Blocks: cleanups 2024-06-23 20:24:27 -07:00
Enrico Ros bc9dedeea4 MP: cleanup Content fragments 2024-06-23 20:22:05 -07:00
Enrico Ros 1b3a383b53 Blocks: rename to AutoBlocksRenderer 2024-06-23 20:09:27 -07:00
Enrico Ros 4e0a535402 Blocks: cleanups 2024-06-23 19:56:55 -07:00
Enrico Ros 0005db1b33 Blocks: extract iframe rendering 2024-06-23 19:18:28 -07:00
Enrico Ros 5cd74031be Blocks: rename 2024-06-23 19:07:24 -07:00
Enrico Ros facb85b5da Rename to an Anthropic-compatible naming (part 1) 2024-06-23 18:46:41 -07:00
Enrico Ros 5f97d17837 anthropic model naming 2024-06-23 01:10:53 -07:00
Enrico Ros af722e09f8 bits 2024-06-23 00:55:57 -07:00
Enrico Ros 959edf6010 MP: cleanups/enablers 2024-06-22 23:58:15 -07:00
Enrico Ros d08f183394 roll trpc 2024-06-22 23:25:20 -07:00
Enrico Ros da541ae182 MP: Extract Fragments to own file 2024-06-22 23:12:12 -07:00
Enrico Ros 4582c4c03d MP: Typesystem Sentinels 2024-06-22 21:52:30 -07:00
Enrico Ros 8c7d70d434 Debug 2024-06-22 18:01:59 -07:00
Enrico Ros fcf9f9e562 MP: disable images editing 2024-06-22 17:31:21 -07:00
Enrico Ros 7bb0fb294a MP: full document editing 2024-06-22 17:01:50 -07:00
Enrico Ros 2e7b5ba5f0 Fix enter is newline on editing 2024-06-22 16:41:08 -07:00
Enrico Ros 6b017f3678 style 2024-06-22 16:34:41 -07:00
Enrico Ros a303d00900 style 2024-06-22 16:34:26 -07:00
Enrico Ros aaa351dca4 Revert "code block detection: assume the ``` has a newline before the end of block"
Because the Diagrams stopped working in the modal.

This reverts commit ee5fb5361c.
2024-06-22 16:25:17 -07:00
Enrico Ros ee5fb5361c code block detection: assume the ``` has a newline before the end of block 2024-06-22 16:18:37 -07:00
Enrico Ros aaffcdbfeb Style 2024-06-22 16:13:09 -07:00
Enrico Ros a8fefb5a90 Document Editor: inline editing 2024-06-22 16:05:54 -07:00
Enrico Ros 8e3b07fa49 Document Fragment Button: variable height 2024-06-22 15:57:35 -07:00
Enrico Ros 36ac618e88 bits 2024-06-22 15:56:24 -07:00
Enrico Ros ab0eeae1e3 BlocksRenderer: plain code rendering mode 2024-06-22 15:55:32 -07:00
Enrico Ros f74adffa12 MP: Fix overall layout 2024-06-22 15:08:52 -07:00
Enrico Ros 8f23f41e2f bits 2024-06-22 13:20:05 -07:00
Enrico Ros 7d04844c6a DocumentFragment: improve 2024-06-21 03:47:53 -07:00
Enrico Ros c301dcc226 TextAttachmentFragment: works 2024-06-21 03:13:30 -07:00
Enrico Ros 8dd4ece730 PartImageRefDBlob: extract 2024-06-21 02:49:27 -07:00
Enrico Ros 75bd68f9fe PartImageRef: improve bifurcation 2024-06-21 02:18:58 -07:00
Enrico Ros 96af022afa BlocksRenderer: remove isBottom 2024-06-21 02:18:58 -07:00
Enrico Ros c570c68f1b TextAttachments: begin 2024-06-21 02:18:58 -07:00
Enrico Ros 21a226a486 Restore the Bubble menu 2024-06-21 00:06:01 -07:00
Enrico Ros 2695cb8e46 MP: align items correctly 2024-06-21 00:06:01 -07:00
Enrico Ros 2207405ebc MP: improve text part addition 2024-06-20 23:34:42 -07:00
Enrico Ros 3802123147 Double-click to edit: on again (differentiator) 2024-06-20 20:24:05 -07:00
Enrico Ros c6c630f5c6 Auto-UI: add one more message 2024-06-20 20:22:11 -07:00
Enrico Ros 7c76a17c08 RenderCode: borderless 2024-06-20 20:08:43 -07:00
Enrico Ros 5ba7723fa0 shift to delete without confirmation 2024-06-20 19:12:35 -07:00
Enrico Ros 87ff07c850 Debug: print DMessage 2024-06-20 19:05:34 -07:00
Enrico Ros 71e1a2eeec MP: render Image Attachments 2024-06-20 18:48:32 -07:00
Enrico Ros 88fba0f53a MP: cluster fragments 2024-06-20 17:40:54 -07:00
Enrico Ros 07260a8e06 ChatMessage: pass mobile 2024-06-20 17:37:26 -07:00
Enrico Ros c1d155b569 ImageRender(DataRef): Change buttons order 2024-06-20 17:36:10 -07:00
Enrico Ros 7e7cfe1db1 ContentPartImage: introduce a lighter variant 2024-06-20 17:35:27 -07:00
Enrico Ros d27a44ab7f TS 5.5.2 fixes 2024-06-20 14:55:02 -07:00
Enrico Ros 2adcca1cda roll packages 2024-06-20 14:43:43 -07:00
Enrico Ros cf854b7262 Merge branch 'refs/heads/main' into feature-multipart 2024-06-20 12:43:06 -07:00
Enrico Ros ecb0e07312 Merge branch 'refs/heads/main-stable' 2024-06-20 12:42:51 -07:00
Enrico Ros 7d6d7e619b Anthropic: hardcode date 2024-06-20 12:42:10 -07:00
Enrico Ros 8b2b88c7cb Merge branch 'refs/heads/main' into feature-multipart 2024-06-20 12:28:53 -07:00
Enrico Ros 9af1a6a16b Merge branch 'refs/heads/main-stable' 2024-06-20 12:28:36 -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 d1ac9adc7e executor: bits 2024-06-20 11:41:31 -07:00
Enrico Ros 513edf90f7 executor: begin cleanups 2024-06-19 19:48:48 -07:00
Enrico Ros 60d47510ab Auto-UI: further improve 2024-06-19 19:33:31 -07:00
Enrico Ros 5b7b9837f0 Chats: group by conversation size 2024-06-19 19:27:29 -07:00
Enrico Ros 333c3327c4 Text areas: deprecate enterIsNewline on text edits that have buttons 2024-06-19 18:59:55 -07:00
Enrico Ros 9723c98940 Auto-suggestions: cleanups 2024-06-19 18:47:38 -07:00
Enrico Ros 97604f3c5b Auto-UI: update 2024-06-19 18:40:30 -07:00
Enrico Ros 044f18da46 bits 2024-06-19 18:08:44 -07:00
Enrico Ros 53946b9523 Llms: begin cleaning up 2024-06-19 18:07:16 -07:00
Enrico Ros fd8f88c5e4 Chat: reorg 2024-06-19 13:29:01 -07:00
Enrico Ros e7d15ce2b0 bits 2024-06-19 12:55:02 -07:00
Enrico Ros ff1d98a87e MP: Improve ownership, restore GC 2024-06-19 11:58:09 -07:00
Enrico Ros accc68cd28 Attachments: cleanup of ownership 2024-06-19 11:57:45 -07:00
Enrico Ros b2c7bc980f DBlobs: type Context and Scope 2024-06-19 11:51:49 -07:00
Enrico Ros 75fbe8d5d8 PDFUtils: skip load in dev 2024-06-19 11:34:18 -07:00
Enrico Ros 13ebf3b3aa Composer: cleanups 2024-06-19 11:13:44 -07:00
Enrico Ros 916d3812db MP: Update follow-up prompts 2024-06-19 09:44:04 -07:00
Enrico Ros 90610c819b MP: AppChat: send logic 2024-06-19 09:22:23 -07:00
Enrico Ros a5f6f62559 Hide the block title when rendering html. 2024-06-19 08:49:16 -07:00
Enrico Ros bfb3501dec MP: Composer: go out in multipart 2024-06-18 16:30:26 -07:00
Enrico Ros c0513c50b1 MP: Attach: cleanups 2024-06-18 15:54:16 -07:00
Enrico Ros bcf4baf004 MP: Attach: rationalize 2024-06-18 15:35:20 -07:00
Enrico Ros 53bf948a04 ChatMessage: extract avatars 2024-06-18 14:47:13 -07:00
Enrico Ros 2186d91f89 MP: ChatMessage: Attachments begin 2024-06-18 14:47:13 -07:00
Enrico Ros aaf856a503 MP: ChatMessage: extract Fragments list 2024-06-18 14:19:57 -07:00
Enrico Ros 8af625b7dc Code: fix render lines suppression 2024-06-18 14:03:30 -07:00
Enrico Ros 4690891757 Code: render lines 2024-06-18 12:57:24 -07:00
Enrico Ros bb3e17c0fa Cleanups 2024-06-18 11:31:49 -07:00
Enrico Ros 7965df5ff2 Text Parts Editor: buttons to edit 2024-06-18 03:55:34 -07:00
Enrico Ros 5b5f0a5a8d Text Parts Editor: style 2024-06-18 02:56:32 -07:00
Enrico Ros fdb087a39b Text Parts Editor: works! 2024-06-18 02:43:34 -07:00
Enrico Ros 97749378d6 Text Parts Editor: begin 2024-06-18 02:43:26 -07:00
Enrico Ros 63dc2301ff Bits 2024-06-18 01:13:00 -07:00
Enrico Ros 5659c0bc70 Auto-UI: Remove bloat 2024-06-18 00:11:32 -07:00
Enrico Ros 1e288ab0fd Auto-UI: Update prompts 2024-06-17 21:37:04 -07:00
Enrico Ros 4f058a0174 RenderHTML: use a simple CSS reset and coalesce timers 2024-06-17 21:06:52 -07:00
Enrico Ros 7284114565 Code render: improve fallback while loading 2024-06-17 20:13:07 -07:00
Enrico Ros 0b2592dbd7 tRPC: un-batch for now 2024-06-17 19:53:38 -07:00
Enrico Ros edfaf6f002 Cleaner errors 2024-06-17 19:49:20 -07:00
Enrico Ros da3990b614 Save toolbar space 2024-06-17 19:49:16 -07:00
Enrico Ros 25740ae13c MP: Image parts support deletion 2024-06-17 19:48:37 -07:00
Enrico Ros fb4c05f698 Bits 2024-06-17 18:48:22 -07:00
Enrico Ros a0c4e37c94 MP: Remove duplicate Placeholdering 2024-06-17 18:23:17 -07:00
Enrico Ros 278caf6f0c MP: Rename PH 2024-06-17 16:33:43 -07:00
Enrico Ros 2ce0c61f83 MP: Add Placeholder and Error parts 2024-06-17 16:23:10 -07:00
Enrico Ros afb25324a7 DMessage: logic to index by fragmentIds 2024-06-17 11:23:47 -07:00
Enrico Ros ba1b761c08 DMessage: add fragment Id (not unique) and a type sentinel 2024-06-17 09:04:56 -07:00
Enrico Ros 0e2d4af617 tRPC-11: workaround a transformation bug in SuperJSON/tRPC/NextJS
May be related to:
- https://github.com/blitz-js/superjson/issues/242
- https://github.com/blitz-js/superjson/issues/283
2024-06-17 00:36:08 -07:00
Enrico Ros 1b0b54a072 tRPC-11: Loading states with React-Query 2024-06-17 00:12:47 -07:00
Enrico Ros 9c629d3c5c tRPC-11: Server 2024-06-17 00:12:47 -07:00
Enrico Ros 173af4e459 Bits 2024-06-17 00:12:47 -07:00
Enrico Ros c0f12c0a5d tRPC-11: Client 2024-06-17 00:12:47 -07:00
Enrico Ros 390605fe66 Roll packages 2024-06-16 17:42:26 -07:00
Enrico Ros e4bd5f865c Migrate to scoped nanoid() 2024-06-16 17:33:46 -07:00
Enrico Ros b31c891772 Fix compile 2024-06-16 17:26:32 -07:00
Enrico Ros 08e4016972 Auto-ui: include prompt auto-mix-in 2024-06-15 17:38:37 -07:00
Enrico Ros aea7eb6ba3 Auto-ui: improve rendering and integration 2024-06-15 17:24:10 -07:00
Enrico Ros 5496750085 Auto-ui: better execution security 2024-06-15 17:23:54 -07:00
Enrico Ros 4b9709898c Auto-ui: improve settings 2024-06-15 16:49:35 -07:00
Enrico Ros 705daac737 RenderHTML: do not scroll the parent frame when keys are pressed within 2024-06-15 02:51:09 -07:00
Enrico Ros a802b32f47 Auto-HTML UI: auto-render
Added DANGER disclaimers in the code and UI. Just a testing mode for fun.
2024-06-15 01:09:32 -07:00
Enrico Ros 8b8db5e447 Auto-HTML UI test 2024-06-15 00:15:58 -07:00
Enrico Ros 3ee44599c7 MP: Auto-diagrams (chat) fix. 2024-06-14 23:52:20 -07:00
Enrico Ros 2955a41ed5 Attachments: lower the image quality a tad (very large files) 2024-06-14 22:46:23 -07:00
Enrico Ros a52802c882 Attachments: when resizing (openai-*), force format conversion 2024-06-14 22:46:10 -07:00
Enrico Ros b46c70512a Beam: fix #552 2024-06-14 18:29:12 -07:00
Enrico Ros 18f91e2eeb Beam: flatter design 2024-06-14 17:53:52 -07:00
Enrico Ros 9296984569 Layout: preserve state on resizes 2024-06-14 15:24:14 -07:00
Enrico Ros 7b835d9855 Draw: use the queue 2024-06-14 08:52:40 -07:00
Enrico Ros ce23b9169b Misc 2024-06-14 01:16:45 -07:00
Enrico Ros 47a535d309 Chat: show has drawings icon 2024-06-13 23:17:53 -07:00
Enrico Ros 6342801aa0 Bits 2024-06-13 22:55:46 -07:00
Enrico Ros 50c00f5516 Bootup to the right drawer opening state 2024-06-13 22:53:15 -07:00
Enrico Ros 4a49678fb6 T2I: rename providerId 2024-06-13 22:34:54 -07:00
Enrico Ros 0f10b8f677 MP: filter chats for images 2024-06-13 22:21:45 -07:00
Enrico Ros d8433b79cc MP: regenerate in place 2024-06-13 21:40:49 -07:00
Enrico Ros f94f640212 MP: share common draw/store logic 2024-06-13 18:19:02 -07:00
Enrico Ros 5cf779757f MP: improve fetch logic? 2024-06-13 17:40:19 -07:00
Enrico Ros d49acf379e DBlobs: show usage if storage persistence fails 2024-06-13 16:36:06 -07:00
Enrico Ros b9bff4abc0 DBlobs: try to persist with the browser 2024-06-13 16:24:06 -07:00
Enrico Ros 6fc4dbe9d1 CSS fix 2024-06-13 16:09:31 -07:00
Enrico Ros cca8132a2c DBlobs: rationalize image usage and GC 2024-06-13 16:02:21 -07:00
Enrico Ros 91654ca219 Chat: option to regenerate images in-place 2024-06-13 03:57:48 -07:00
Enrico Ros 547d7eca59 Draw: gallery empty state 2024-06-13 03:13:41 -07:00
Enrico Ros b86bf31baa DBlob: fixup post-rename 2024-06-13 02:54:43 -07:00
Enrico Ros 5b5b4efe42 Chat symbol: render support 2024-06-13 02:47:21 -07:00
Enrico Ros e9fb65edba DBlobs: refactor DBlobAssetId 2024-06-13 02:42:43 -07:00
Enrico Ros cc1cba9aa8 DBlobs: refactor 2024-06-13 02:06:10 -07:00
Enrico Ros a765c566c8 DBlobs: simplify 2024-06-13 01:19:13 -07:00
Enrico Ros 63e9022b84 add Nanoid 2024-06-13 00:29:16 -07:00
Enrico Ros 368a995e7f DBlobs: rename the table 2024-06-13 00:29:16 -07:00
Enrico Ros c844c66b5a DBlobs: survive hot reloads 2024-06-12 23:27:24 -07:00
Enrico Ros 73b18313e9 Draw: layout 2024-06-12 23:09:48 -07:00
Enrico Ros bdd68dc6c9 DBlobs: sanity fix 2024-06-12 23:08:08 -07:00
Enrico Ros 3901b94382 Draw: add rounded 2024-06-12 22:11:06 -07:00
Enrico Ros 82ac276338 Draw: fix layout 2024-06-12 22:07:16 -07:00
Enrico Ros 02c9f3ebdb Draw: bare bones enhancer 2024-06-12 15:52:10 -07:00
Enrico Ros 364ad63877 Draw: Merge T2I back into DrawCreate 2024-06-12 02:43:44 -07:00
Enrico Ros 5fc4196d01 Draw: fix fallbacks 2024-06-12 02:24:56 -07:00
Enrico Ros 3a1e10bd21 Fix wrapping of radios 2024-06-12 02:10:59 -07:00
Enrico Ros 73519ec562 Draw: configure provider opens up top 2024-06-12 01:15:33 -07:00
Enrico Ros bf9c9916b1 Draw: improve prompt composer 2024-06-12 01:15:04 -07:00
Enrico Ros 01d017c6cd Draw: dynamic header 2024-06-12 00:37:44 -07:00
Enrico Ros ca98ab02d8 Draw: improve numbers 2024-06-12 00:37:36 -07:00
Enrico Ros 347804a02e Draw: fix dom 2024-06-11 23:43:47 -07:00
Enrico Ros 4c80f8dbf4 Draw: improve 2024-06-11 23:41:16 -07:00
Enrico Ros 73ee96040f Draw: heading 2024-06-11 22:09:04 -07:00
Enrico Ros 6180da1333 MP: cleanups 2024-06-11 19:53:44 -07:00
Enrico Ros 2756ff6ad0 MP: fix 2024-06-11 19:50:03 -07:00
Enrico Ros e57491b812 MP: DBlob GC on message/chat deletion 2024-06-11 19:50:00 -07:00
Enrico Ros 9d8ae538d9 Draw: thumbnail in gallery 2024-06-11 19:41:12 -07:00
Enrico Ros dd7defd2c7 DBlobs: auto-gen image thumbnail 2024-06-11 19:41:04 -07:00
Enrico Ros e79ec45b5b Draw: bare bones gallery 2024-06-11 19:13:48 -07:00
Enrico Ros 1a138bbc16 DesktopDrawer: do not get in the way when closed 2024-06-11 19:11:09 -07:00
Enrico Ros b067165471 Draw: cleans 2024-06-11 18:31:37 -07:00
Enrico Ros 6fbcbb9399 Draw: renames 2024-06-11 15:11:22 -07:00
Enrico Ros aaf77b4e20 Draw: renames 2024-06-11 15:06:18 -07:00
Enrico Ros f5cc2e952b Draw: sections 2024-06-11 14:50:02 -07:00
Enrico Ros eeab362567 cleanups 2024-06-11 14:45:49 -07:00
Enrico Ros 834205c426 DBlobs: renames 2024-06-11 14:13:17 -07:00
Enrico Ros fbad8ca62e DBlobs: gc on chat images 2024-06-11 13:48:21 -07:00
Enrico Ros 1e4c6f13c5 MP: show images 2024-06-11 12:56:12 -07:00
Enrico Ros b7c2b3d4cb RenderImageURL: improve greatly 2024-06-11 12:53:55 -07:00
Enrico Ros 0d5b7d36f1 Message Fragments: update v-layout 2024-06-11 12:10:47 -07:00
Enrico Ros 059886fede Mobile menu: shrink a bit 2024-06-11 12:08:22 -07:00
Enrico Ros db7dd0ca43 DBlobs: 'ot' for the origin type 2024-06-11 12:07:53 -07:00
Enrico Ros f4c611b47d DBlobs: reactive live hooks 2024-06-11 01:51:08 -07:00
Enrico Ros 39c32646c5 Merge branch 'refs/heads/main' into feature-multipart 2024-06-10 23:57:05 -07:00
Enrico Ros 1720fffbdc Merge remote-tracking branch 'refs/remotes/opensource/main-stable' 2024-06-10 23:56:43 -07:00
Enrico Ros b4d8e39d56 Gemini: acknowledge the new capability to createCachedContent. Fixes #565 2024-06-10 23:56:02 -07:00
Enrico Ros 6c51cd0d1d Fix Tooltip on errors 2024-06-10 23:54:59 -07:00
Enrico Ros cb9cdc508a MP: rename Content Parts 2024-06-10 23:35:53 -07:00
Enrico Ros 7d037a206f MP: enable multipart edits 2024-06-10 22:41:46 -07:00
Enrico Ros ace10ab4be MP: begin Fragment extraction and simplify BlockRenderer 2024-06-10 21:46:45 -07:00
Enrico Ros bc0a7b6ac3 Rename RenderImageURL 2024-06-10 21:45:40 -07:00
Enrico Ros e77e2045e3 MP: Message porting skel 2024-06-10 19:29:01 -07:00
Enrico Ros abbd55c740 Message: renames 2024-06-09 19:16:46 -07:00
Enrico Ros bf5e80a462 MP: adapt ego attachment, messageSingleTextOrThrow-- 2024-06-09 19:00:01 -07:00
Enrico Ros 121deaae5f bits 2024-06-09 18:01:49 -07:00
Enrico Ros 80317232ba MP: improve fragment typings 2024-06-09 17:44:25 -07:00
Enrico Ros 22f815dcd1 do not load 'pdfjs-dist' during development, to make '--turbo' work 2024-06-09 17:07:36 -07:00
Enrico Ros fb96c3ab47 Merge branch 'refs/heads/main' into feature-multipart
# Conflicts:
#	src/modules/aifn/autosuggestions/autoSuggestions.ts
#	src/modules/aifn/autotitle/autoTitle.ts
2024-06-07 14:24:03 -07:00
Enrico Ros 3b15ad51a1 Merge branch 'refs/heads/main-stable' 2024-06-07 14:23:21 -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 5066336c75 Merge branch 'refs/heads/main-stable' 2024-06-07 12:16:49 -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 0807744577 Option to see model interfaces 2024-06-07 12:09:38 -07:00
Enrico Ros 59f871d3ec Merge branch 'refs/heads/main' into feature-multipart
# Conflicts:
#	src/apps/chat/editors/chat-stream.ts
#	src/modules/beam/gather/instructions/ChatGenerateInstruction.tsx
#	src/modules/beam/scatter/beam.scatter.ts
2024-06-06 22:15:17 -07:00
Enrico Ros fed351a2fc Merge branch 'refs/heads/main-stable'
# Conflicts:
#	package-lock.json
#	package.json
#	src/common/util/token-counter.ts
2024-06-06 22:11:43 -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 aeb129e422 roll: minor packages 2024-06-06 11:21:23 -07:00
Enrico Ros 3050b546ac Gemini: update 2024-06-06 10:13:37 -07:00
Enrico Ros 1429726ba6 Gemini: update 2024-06-06 10:10:06 -07:00
Enrico Ros 4075581acd Add Codestral - Fixes #558 2024-06-06 09:59:31 -07:00
Enrico Ros 56774fd974 roll: mermaid 10.9.1 from CDN 2024-06-06 09:36:08 -07:00
Enrico Ros 5e674d2299 Dynamic won't be required from Next 15 2024-06-04 20:49:05 -07:00
Enrico Ros 06f5b6d6ff Silence warnings by declaring asyncFunction available on the browsers side 2024-06-04 20:44:01 -07:00
Enrico Ros b25b4e6c8f roll: next 14.2 2024-06-04 20:05:06 -07:00
Enrico Ros 645e07dba8 roll: pdfjs-dist 2024-06-04 20:04:51 -07:00
Enrico Ros 46181fcaa2 roll: misc 2024-06-04 19:32:08 -07:00
Enrico Ros 8d7ae425f9 roll: cloudflare/puppeteer 2024-06-04 18:34:56 -07:00
Enrico Ros 7d572334a1 roll: misc 2024-06-04 18:19:38 -07:00
Enrico Ros 5dab6f68e6 roll: mui 2024-06-04 18:18:04 -07:00
Enrico Ros d1c595d8db Text 2024-06-04 07:09:56 -07:00
Enrico Ros eaa2635b51 T2I: port TextToImage (partial) 2024-06-03 13:55:11 -07:00
Enrico Ros dc2d226ddb T2I: port image-generate 2024-06-03 13:41:57 -07:00
Enrico Ros 336a4e1f35 T2I: separate data/mime 2024-06-03 13:40:45 -07:00
Enrico Ros 4d3b6b4f43 MP: cleaner contentFragments additions 2024-06-03 12:57:59 -07:00
Enrico Ros a12601b49c Merge remote-tracking branch 'feature-t2i-b64' into feature-multipart
# Conflicts:
#	src/apps/chat/editors/image-generate.ts
2024-06-03 12:05:48 -07:00
Enrico Ros 15a895064e ChatMessage: bits 2024-06-03 12:02:35 -07:00
Enrico Ros 8bd1507ace ChatMessage: Fix Avatar on Mobile 2024-06-03 11:42:46 -07:00
Enrico Ros 89d7ec5d0b ChatMessage: disable diffing 2024-06-03 10:16:55 -07:00
Enrico Ros 670e57735a MP: relax singleTextOrThrow 2024-06-03 10:10:54 -07:00
Enrico Ros fa703c25e8 Attachments: disable GC drafts for now 2024-06-03 09:34:57 -07:00
Enrico Ros f58161b1d1 Attachments: GC drafts 2024-06-03 09:33:53 -07:00
Enrico Ros 8db2a37a59 Attachments: remove DBlobs when setting outputFragments, until GC comes 2024-06-03 09:21:58 -07:00
Enrico Ros bfdb9c2624 Attachments: remove race condition on conversion 2024-06-03 09:15:11 -07:00
Enrico Ros 240e984737 DBlob: types 2024-06-03 09:05:46 -07:00
Enrico Ros fe128c18b1 MP: improve dmessage part/reference functions and cloning 2024-06-03 08:48:42 -07:00
Enrico Ros b208d8c40d MP: fix streaming text 2024-05-26 02:55:55 -07:00
Enrico Ros 556641e1f4 MP: fix V3 chats porting 2024-05-26 02:47:11 -07:00
Enrico Ros 464eb671db MP: upgrade V3 to V4 (store, links, jsons) 2024-05-25 21:13:45 -07:00
Enrico Ros 12b8f1e3ef MP: refine DMessage/DMessageFragment
Once again, large change.
2024-05-25 20:17:43 -07:00
Enrico Ros ab199afe0d Beam: reminder the save/restore on a per-message basis, #483 2024-05-25 15:53:56 -07:00
Enrico Ros fe1a498da0 Beam: also enable method selection on AutoStart 2024-05-25 15:51:37 -07:00
Enrico Ros 4f9d55eb42 Attachments: +Plain Mime 2024-05-25 03:49:21 -07:00
Enrico Ros 70f450f547 Attachments: restored take and copy 2024-05-25 02:56:17 -07:00
Enrico Ros 28fc7deefc MP: rename to contentRef 2024-05-25 00:40:39 -07:00
Enrico Ros 428babf856 Tokens: estimate images tokens 2024-05-25 00:28:15 -07:00
Enrico Ros b824ddf2e3 Attachments: uniform remove 2024-05-24 22:33:33 -07:00
Enrico Ros 2396966740 Tokens: rename the text methods 2024-05-24 22:25:14 -07:00
Enrico Ros 23ca49128a Attachments: consolidation 2024-05-24 19:35:26 -07:00
Enrico Ros ec6bdede20 Attachments: improvements 2024-05-24 18:06:45 -07:00
Enrico Ros 4ada2013d2 Attachments: correct rescaling 2024-05-24 17:31:36 -07:00
Enrico Ros 79afef6bc1 Attachments: more rescaling pains 2024-05-24 16:52:49 -07:00
Enrico Ros e7000df89f Attachments: no upsizing of small images 2024-05-24 16:30:10 -07:00
Enrico Ros 59f77a64ea Attachments: open images from the menu in new tabs 2024-05-24 16:13:29 -07:00
Enrico Ros 8be152666e Attachments: improve conversions/quality 2024-05-24 15:14:50 -07:00
Enrico Ros 10488854ce Attachments: low/high detail modes 2024-05-24 03:44:59 -07:00
Enrico Ros 6586aafed8 Attachments: resize modes for OpenAI, Google, Anthropic 2024-05-24 03:32:25 -07:00
Enrico Ros 4568a60be3 Attachments: image resize 2024-05-24 03:18:12 -07:00
Enrico Ros 193bc8bb8e Attachments: fix multi-image pdf 2024-05-24 02:20:53 -07:00
Enrico Ros ce381b7690 Attachments: rename main to AttachmentDraft, convert to (overlayed) Store Slice 2024-05-24 01:52:25 -07:00
Enrico Ros b238428816 Chat search: sort by date by default 2024-05-24 00:27:31 -07:00
Enrico Ros 0ac37f50cf Attachments: remove old code 2024-05-23 22:29:44 -07:00
Enrico Ros 54b9389b77 MP: Attachments full port, incl. Storage 2024-05-22 02:56:23 -07:00
Enrico Ros a183c26e51 PDFUtils: improve PDF to image 2024-05-22 02:56:23 -07:00
Enrico Ros 01a03d164c ImageUtils: add dimension guessers and converters 2024-05-22 02:56:23 -07:00
Enrico Ros cdff1fde2d TextUtils: update to new uuid 2024-05-22 02:56:23 -07:00
Enrico Ros c38b9998a6 TextUtils: add uuidv4 base64 2024-05-22 02:56:23 -07:00
Enrico Ros 77c1a335ad MP: data at rest cleanup 2024-05-22 02:56:23 -07:00
Enrico Ros 07a0fe6249 MP: text separation 2024-05-22 02:56:23 -07:00
Enrico Ros 204bc46976 MP: append parts to messages 2024-05-22 02:56:23 -07:00
Enrico Ros b910506519 MP: cleanup MP Beam 2024-05-22 02:56:23 -07:00
Enrico Ros 3cef39da17 MP: fix Beam, Rays and Fusions 2024-05-22 02:56:23 -07:00
Enrico Ros 3aea29bcb5 MP: remove typing, support placeholder, improve streaming of updates 2024-05-22 02:56:23 -07:00
Enrico Ros dd0d19168b MP: cleanup conversions 2024-05-22 02:56:22 -07:00
Enrico Ros 6727fcd111 MP: cleanup conversions 2024-05-22 02:56:22 -07:00
Enrico Ros 9d347f4a5a Multi-Part refactor
Partial still. Does not build.
2024-05-22 02:56:22 -07:00
Enrico Ros 084e48ddc2 Desktop Nav: improve menu 2024-05-22 02:55:04 -07:00
Enrico Ros 31e89ce9a1 App: Tokenizer 2024-05-22 02:54:56 -07:00
Enrico Ros baad3ae1c3 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
2024-05-21 23:50:32 -07:00
Enrico Ros 7c099cab94 Fix TimeoutError issue 2024-05-18 03:07:04 -07:00
Enrico Ros 811875dd2e idb-keyval: bits 2024-05-16 18:04:27 -07:00
Enrico Ros 127443d550 idb-keyval: crud console fn 2024-05-16 18:03:20 -07:00
Enrico Ros d2064605bf Bits 2024-05-16 04:14:41 -07:00
Enrico Ros 4c6fb61ca8 Draw: enable nav 2024-05-16 04:11:04 -07:00
Enrico Ros 608ba8bcb4 T2I: Adapt callers (fixme) 2024-05-16 04:11:04 -07:00
Enrico Ros b53c054dee T2I: Enrich Generated Output 2024-05-16 04:11:04 -07:00
Enrico Ros 05aa4b547f Mistral: update pricing 2024-05-16 04:02:04 -07:00
Enrico Ros 6afb61d25d Mistral: update
#518
2024-05-16 04:02:03 -07:00
Enrico Ros a7ce5c1ca6 Already Set 2024-05-16 03:25:25 -07:00
Enrico Ros 952bd2bd93 Start opened 2024-05-16 03:03:57 -07:00
Enrico Ros f9d33d4888 Page download: improve 2024-05-16 02:57:50 -07:00
Enrico Ros 81d99f19d4 Beam: bits 2024-05-16 01:41:44 -07:00
Enrico Ros 454a4257da Beam: recall importing rays 2024-05-16 01:35:04 -07:00
Enrico Ros e513b42786 Beam: fix reactive bug 2024-05-16 01:30:37 -07:00
Enrico Ros b607e3c034 Beam: if auto-start, give the chance to change merge model 2024-05-16 01:00:21 -07:00
Enrico Ros d5c3f5012b Tiktoken: in the future, show tokens 2024-05-16 00:55:58 -07:00
Enrico Ros 21d045be59 Update TikToken for perfect token computation on 'o' models. 2024-05-16 00:53:08 -07:00
Enrico Ros a9c1c34dc9 DBlobs: subsystem for storing blobs
Uses Dexie.js fro IndexedDB access.
2024-05-15 23:44:33 -07:00
Enrico Ros 44ab0483b6 DChat: remove IDB migration 2024-05-15 23:43:31 -07:00
Enrico Ros 9eb0cc0b62 Gemini: improve support (incl. interfaces, cost, visibility) 2024-05-14 15:15:53 -07:00
Enrico Ros 2db74867f5 (bits) 2024-05-13 16:24:50 -07:00
Enrico Ros fd30baafb8 Default to the full context window 2024-05-13 16:24:30 -07:00
Enrico Ros 3623eef47f Browse: improve markdown transform 2024-05-13 15:59:46 -07:00
Enrico Ros 7b07bb7884 Browse: full support for markdown transform 2024-05-13 15:38:08 -07:00
Enrico Ros 7946cd6614 Browse: markdown transform as default 2024-05-13 14:49:29 -07:00
Enrico Ros 51b6e30986 Browse: support transform (skel) 2024-05-13 14:34:25 -07:00
Enrico Ros 002df7b0f9 Hold Shift to delete without confirmation: fixes #537 2024-05-13 14:00:43 -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
Sorawit Kongnurat 2ac1789312 Use ctrl and remove alt usage with certain hotkeys for mac shortcuts 2024-05-06 16:34:06 +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
Enrico Ros 2f59e12e20 Remove log 2024-03-06 22:20:40 -08:00
Enrico Ros 30e8652c2a 1.14.1: Release for Claude-3 2024-03-06 22:10:41 -08:00
Enrico Ros 5ee6aceb60 cleanups 2024-03-06 21:51:15 -08:00
Enrico Ros 6940b6a6d1 Anthropic: Full support for Claude-3 models. Closes #443, #450
Thanks to @slapglif in #450 for a reference implementation.
2024-03-06 21:50:24 -08:00
Enrico Ros 4e33ce9415 misc 2024-03-06 20:56:32 -08:00
Enrico Ros 944e22bde6 Anthropic: if there's a single system message, treat it as-if it was a user message 2024-03-06 20:49:59 -08:00
Enrico Ros 6054fa0a26 Anthropic: use the new Messages format (thanks @slapglif #450) 2024-03-06 20:42:33 -08:00
Enrico Ros 4db13cfed4 Anthropic: wire types (fully switch to the new Messages API) 2024-03-06 20:33:59 -08:00
Enrico Ros 6a6adda2e0 misc 2024-03-06 20:33:12 -08:00
Enrico Ros 4afa55c0db Anthropic: update models 2024-03-06 18:36:07 -08:00
Enrico Ros bc120bfb2b Merge branch 'release-1.14.0' 2024-03-05 22:41:40 -08:00
Enrico Ros 88966699e7 1.14.0: Changelog and README 2024-03-05 22:40:49 -08:00
Enrico Ros 9a5db3dcfb 1.14.0: release date 2024-03-05 22:38:56 -08:00
Enrico Ros 392aa1e654 1.14.0: README and Changelog 2024-03-05 22:16:02 -08:00
Enrico Ros f2b32e47ff 1.14.0: README and Changelog 2024-03-05 22:13:59 -08:00
Enrico Ros 58136d0181 maintainers: release name and cover 2024-03-05 21:58:04 -08:00
Enrico Ros 02733e55cb 1.14.0: News items 2024-03-05 21:55:44 -08:00
Enrico Ros 60df8456a7 1.14.0: Icons support 2024-03-05 21:54:48 -08:00
Enrico Ros 6d0ecc805c 1.14.0: Cover Image 2024-03-05 21:54:38 -08:00
Enrico Ros a0e9dd24a3 Explain the Debug page 2024-03-05 20:12:40 -08:00
Enrico Ros d1eb89057d Update flow 2024-03-05 14:51:00 -08:00
Enrico Ros 161c6dc83a 1.14.0: News Version 2024-03-05 14:50:51 -08:00
Enrico Ros 54848b8a7e 1.14.0: news: move around 2024-03-05 14:50:46 -08:00
Enrico Ros 990563c604 1.14.0: Version 2024-03-05 14:45:35 -08:00
Enrico Ros 8489ca8c8d Anthropic: add status update tracking #443 2024-03-05 14:40:43 -08:00
Enrico Ros b57e2c89e3 miniroll 2024-03-05 14:32:44 -08:00
Enrico Ros 66bedf78ac anthropic: cutoff dates 2024-03-04 22:30:09 -08:00
Enrico Ros 592c5cce60 roll packages 2024-03-04 22:01:19 -08:00
Enrico Ros 2ccf9a4e92 swap items 2024-03-04 21:38:30 -08:00
Enrico Ros ed333c0513 make 127.0.0.1 work on airgaped connections 2024-03-04 16:26:20 -08:00
Enrico Ros 89b65b7009 unbreak build #444 2024-03-04 15:20:08 -08:00
Enrico Ros 0cc2d346af Merge pull request #444 from jacksongoode/claude-3
Add Claude 3 models
2024-03-04 14:18:19 -08:00
Jackson 5f81e78bc4 Add Claude 2 to old models 2024-03-04 12:29:04 -08:00
Jackson 554b5fd4b5 Add Claude 3 models 2024-03-04 12:24:12 -08:00
460 changed files with 31248 additions and 9780 deletions
+24 -2
View File
@@ -21,8 +21,9 @@ assignees: enricoros
- [ ] Create a temporary tag `git tag v1.2.3 && git push opensource --tags`
- [ ] Create a [New Draft GitHub Release](https://github.com/enricoros/big-agi/releases/new), and generate the automated changelog (for new contributors)
- [ ] Update the release version in package.json, and `npm i`
- [ ] Update in-app News [src/apps/news/news.data.tsx](/src/apps/news/news.data.tsx)
- [ ] Update the in-app News version number
- [ ] Update in-app News [src/apps/news/news.data.tsx](/src/apps/news/news.data.tsx)
- [ ] Update in-app Cover graphics
- [ ] Update the README.md with the new release
- [ ] Copy the highlights to the [docs/changelog.md](/docs/changelog.md)
- Release:
@@ -79,11 +80,32 @@ I need the following from you:
1. a table summarizing all the new features in 1.2.3 with the following columns: 4 words description (exactly what it is), short description, usefulness (what it does for the user), significance, link to the issue number (not the commit)), which will be used for the artifacts later
2. then double-check the git log to see if there are any features of significance that are not in the table
3. then score each feature in terms of importance for users (1-10), relative impact of the feature (1-10, where 10 applies to the broadest user base), and novelty and uniqueness (1-10, where 10 is truly unique and novel from what exists already)
3. then score each feature in terms of importance for users (1-10), relative impact of the feature (1-10, where 10 applies to the broadest user base), and novelty and uniqueness (1-10, where 10 is truly unique and novel from what exists already)
4. then improve the table, in decreasing order of importance for features, fixing any detail that's missing, in particular check if there are commits of significance from a user or developer point of view, which are not contained in the table
5. then I want you then to update the news.data.tsx for the new release
```
### release name
```markdown
please brainstorm 10 different names for this release. see the former names here: https://big-agi.com/blog
```
You can follow with 'What do you think of Modelmorphic?' or other selected name
### cover images
```markdown
Great, now I need to generate images for this. Before I used the following prompts (2 releases before).
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is using a computer with split screen made of origami, split keyboard and is wearing origami sunglasses with very different split reflections. Split halves are very contrasting. Close up photography, bokeh, white background.
import coverV113 from '../../../public/images/covers/release-cover-v1.13.0.png';
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is calling on a 3D origami old-school pink telephone and the camera is zooming on the telephone. Close up photography, bokeh, white background.
import coverV112 from '../../../public/images/covers/release-cover-v1.12.0.png';
What can I do now as far as images? Give me 4 prompt ideas with the same style as looks as the former, but different scene or action
```
### Readme (and Changelog)
```markdown
+9 -1
View File
@@ -32,6 +32,12 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to the Container registry
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
with:
@@ -49,13 +55,15 @@ jobs:
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main-stable' }}
type=ref,event=tag # Use the tag name as a tag for tag builds
type=semver,pattern={{version}} # Generate semantic versioning tags for tag builds
type=sha # Just in case none of the above applies
- name: Build and push Docker image
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
with:
context: .
file: Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
build-args: NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
+85 -87
View File
@@ -1,11 +1,13 @@
# BIG-AGI 🧠✨
Welcome to big-AGI 👋, the GPT application for professionals that need function, form,
Welcome to big-AGI, the AI suite for professionals that need function, form,
simplicity, and speed. Powered by the latest models from 12 vendors and
open-source model servers, `big-AGI` offers best-in-class Voice and Chat with AI Personas,
visualizations, coding, drawing, calling, and quite more -- all in a polished UX.
open-source servers, `big-AGI` offers best-in-class Chats,
[Beams](https://github.com/enricoros/big-AGI/issues/470),
and [Calls](https://github.com/enricoros/big-AGI/issues/354) with AI personas,
visualizations, coding, drawing, side-by-side chatting, and more -- all wrapped in a polished UX.
Pros use big-AGI. 🚀 Developers love big-AGI. 🤖
Stay ahead of the curve with big-AGI. 🚀 Pros & Devs love big-AGI. 🤖
[![Official Website](https://img.shields.io/badge/BIG--AGI.com-%23096bde?style=for-the-badge&logo=vercel&label=launch)](https://big-agi.com)
@@ -13,11 +15,65 @@ Or fork & run on Vercel
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
## 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [documentation](docs/README.md)
## 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [installation](docs/installation.md) 👉 [documentation](docs/README.md)
big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap
> Note: bigger better features (incl. Beam-2) are being cooked outside of `main`.
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
[//]: # (big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap)
### What's New in 1.16.1...1.16.3 · Jun 20, 2024 (patch releases)
- 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)
#### 3,000 Commits Milestone · April 7, 2024
![big-AGI Milestone](https://github.com/enricoros/big-AGI/assets/32999/47fddbb1-9bd6-4b58-ace4-781dfcb80923)
- 🥇 Today we <b>celebrate commit 3000</b> in just over one year, and going stronger 🚀
- 📢️ Thanks everyone for your support and words of love for Big-AGI, we are committed to creating the best AI experiences for everyone.
### What's New in 1.15.0 · April 1, 2024 · Beam
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
- Managed Deployments **Auto-Configuration**: simplify the UI models setup with backend-set models. [#436](https://github.com/enricoros/big-AGI/issues/436)
- Message **Starring ⭐**: star important messages within chats, to attach them later. [#476](https://github.com/enricoros/big-AGI/issues/476)
- Enhanced the default Persona
- Fixes to Gemini models and SVGs, improvements to UI and icons
- 1.15.1: Support for Gemini Pro 1.5 and OpenAI Turbo models
- Beast release, over 430 commits, 10,000+ lines changed: [release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.15.0), and changes [v1.14.1...v1.15.0](https://github.com/enricoros/big-AGI/compare/v1.14.1...v1.15.0)
<details>
<summary>What's New in 1.14.1 · March 7, 2024 · Modelmorphic</summary>
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
- New **[Perplexity](https://www.perplexity.ai/)** and **[Groq](https://groq.com/)** integration (thanks @Penagwin). [#407](https://github.com/enricoros/big-AGI/issues/407), [#427](https://github.com/enricoros/big-AGI/issues/427)
- **[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
</details>
<details>
<summary>What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind</summary>
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
@@ -29,6 +85,8 @@ https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385
- Better looking chats with improved spacing, fonts, and menus
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-lmstudio.md) (thanks @aj47), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/deploy-database.md) (thanks @ranfysvalle02), and speedups
</details>
<details>
<summary>What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline</summary>
@@ -73,11 +131,11 @@ https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cf
For full details and former releases, check out the [changelog](docs/changelog.md).
## Key Features 👊
## 👉 Key Features
| ![Advanced AI](https://img.shields.io/badge/Advanced%20AI-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![100+ AI Models](https://img.shields.io/badge/100%2B%20AI%20Models-32383e?style=for-the-badge&logo=ai&logoColor=white) | ![Flow-state UX](https://img.shields.io/badge/Flow--state%20UX-32383e?style=for-the-badge&logo=flow&logoColor=white) | ![Privacy First](https://img.shields.io/badge/Privacy%20First-32383e?style=for-the-badge&logo=privacy&logoColor=white) | ![Advanced Tools](https://img.shields.io/badge/Fun%20To%20Use-f22a85?style=for-the-badge&logo=tools&logoColor=white) |
|---------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------|
| **Chat**<br/>**Call** AGI<br/>**Draw** images<br/>**Agents**, ... | Local & Cloud<br/>Open & Closed<br/>Cheap & Heavy<br/>Google, Mistral, ... | Attachments<br/>Diagrams<br/>Multi-Chat<br/>Mobile-first UI | Stored Locally<br/>Easy self-Host<br/>Local actions<br/>Data = Gold | AI Personas<br/>Voice Modes<br/>Screen Capture<br/>Camera + OCR |
| **Chat**<br/>**Call**<br/>**Beam**<br/>**Draw**, ... | Local & Cloud<br/>Open & Closed<br/>Cheap & Heavy<br/>Google, Mistral, ... | Attachments<br/>Diagrams<br/>Multi-Chat<br/>Mobile-first UI | Stored Locally<br/>Easy self-Host<br/>Local actions<br/>Data = Gold | AI Personas<br/>Voice Modes<br/>Screen Capture<br/>Camera + OCR |
![big-AGI screenshot](docs/pixels/big-AGI-compo-20240201_small.png)
@@ -85,7 +143,7 @@ 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) |
| Opensource Servers | [LocalAI](https://localai.io/) (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/) |
@@ -120,6 +178,22 @@ Add extra functionality with these integrations:
<br/>
## 🚀 Installation
To get started with big-AGI, follow our comprehensive [Installation Guide](docs/installation.md).
The guide covers various installation options, whether you're spinning it up on
your local computer, deploying on Vercel, on Cloudflare, or rolling it out
through Docker.
Whether you're a developer, system integrator, or enterprise user, you'll find step-by-step instructions
to set up big-AGI quickly and easily.
[![Installation Guide](https://img.shields.io/badge/Installation%20Guide-blue?style=for-the-badge&logo=read-the-docs&logoColor=white)](docs/installation.md)
Or bring your API keys and jump straight into our free instance on [big-AGI.com](https://big-agi.com).
<br/>
# 🌟 Get Involved!
[//]: # ([![Official Discord]&#40;https://img.shields.io/discord/1098796266906980422?label=discord&logo=discord&logoColor=%23fff&style=for-the-badge&#41;]&#40;https://discord.gg/MkH4qj2Jp9&#41;)
@@ -129,86 +203,10 @@ Add extra functionality with these integrations:
- [ ]**Give us a star** on GitHub 👆
- [ ] 🚀 **Do you like code**? You'll love this gem of a project! [_Pick up a task!_](https://github.com/users/enricoros/projects/4/views/4) - _easy_ to _pro_
- [ ] 💡 Got a feature suggestion? [_Add your roadmap ideas_](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
- [ ]Deploy your [fork](docs/customizations.md) for your friends and family, or [customize it for work](docs/customizations.md)
- [ ] Check out some of the big-AGI [**community projects**](docs/customizations.md)
| Project | Features | GitHub |
|---------|----------------------------------------------------|-------------------------------------------------------------------------------------|
| CoolAGI | Code Interpreter, Vision, Mind maps, and much more | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
- [ ][Deploy](docs/installation.md) your [fork](docs/customizations.md) for your friends and family, or [customize it for work](docs/customizations.md)
<br/>
# 🧩 Develop
[//]: # (![TypeScript]&#40;https://img.shields.io/badge/TypeScript-007ACC?style=&logo=typescript&logoColor=white&#41;)
[//]: # (![React]&#40;https://img.shields.io/badge/React-61DAFB?style=&logo=react&logoColor=black&#41;)
[//]: # (![Next.js]&#40;https://img.shields.io/badge/Next.js-000000?style=&logo=vercel&logoColor=white&#41;)
To download and run this Typescript/React/Next.js project locally, the only prerequisite is Node.js with the `npm` package manager.
Clone this repo, install the dependencies (all local), and run the development server (which auto-watches the
files for changes):
```bash
git clone https://github.com/enricoros/big-agi.git
cd big-agi
npm install
npm run dev
# You will see something like:
#
# ▲ Next.js 14.1.0
# - Local: http://localhost:3000
# ✓ Ready in 2.6s
```
The development app will be running on `http://localhost:3000`. Development builds have the advantage of not requiring
a build step, but can be slower than production builds. Also, development builds won't have timeout on edge functions.
## 🛠️ Deploy from source
The _production_ build of the application is optimized for performance and is performed by the `npm run build` command,
after installing the required dependencies.
```bash
# .. repeat the steps above up to `npm install`, then:
npm run build
next start --port 3000
```
The app will be running on the specified port, e.g. `http://localhost:3000`.
Want to deploy with username/password? See the [Authentication](docs/deploy-authentication.md) guide.
## 🐳 Deploy with Docker
For more detailed information on deploying with Docker, please refer to the [docker deployment documentation](docs/deploy-docker.md).
Build and run:
```bash
docker build -t big-agi .
docker run -d -p 3000:3000 big-agi
```
Or run the official container:
- manually: `docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi`
- or, with docker-compose: `docker-compose up` or see [the documentation](docs/deploy-docker.md) for a composer file with integrated browsing
## ☁️ Deploy on Cloudflare Pages
Please refer to the [Cloudflare deployment documentation](docs/deploy-cloudflare.md).
## 🚀 Deploy on Vercel
Create your GitHub fork, create a Vercel project over that fork, and deploy it. Or press the button below for convenience.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
[//]: # ([![GitHub stars]&#40;https://img.shields.io/github/stars/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/stargazers&#41;)
[//]: # ([![GitHub forks]&#40;https://img.shields.io/github/forks/enricoros/big-agi&#41;]&#40;https://github.com/enricoros/big-agi/network&#41;)
+2 -2
View File
@@ -5,13 +5,13 @@ import { createTRPCFetchContext } from '~/server/api/trpc.server';
const handlerEdgeRoutes = (req: Request) =>
fetchRequestHandler({
router: appRouterEdge,
endpoint: '/api/trpc-edge',
router: appRouterEdge,
req,
createContext: createTRPCFetchContext,
onError:
process.env.NODE_ENV === 'development'
? ({ path, error }) => console.error(`❌ tRPC-edge failed on ${path ?? "<no-path>"}: ${error.message}`)
? ({ path, error }) => console.error(`❌ tRPC-edge failed on ${path ?? 'unk-path'}: ${error.message}`)
: undefined,
});
+8 -2
View File
@@ -5,15 +5,21 @@ import { createTRPCFetchContext } from '~/server/api/trpc.server';
const handlerNodeRoutes = (req: Request) =>
fetchRequestHandler({
router: appRouterNode,
endpoint: '/api/trpc-node',
router: appRouterNode,
req,
createContext: createTRPCFetchContext,
onError:
process.env.NODE_ENV === 'development'
? ({ path, error }) => console.error(`❌ tRPC-node failed on ${path ?? '<no-path>'}: ${error.message}`)
? ({ path, error }) => console.error(`❌ tRPC-node failed on ${path ?? 'unk-path'}: ${error.message}`)
: undefined,
});
// 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 runtime = 'nodejs';
export const dynamic = 'force-dynamic';
export { handlerNodeRoutes as GET, handlerNodeRoutes as POST };
+9 -15
View File
@@ -28,7 +28,7 @@ Detailed guides to configure your big-AGI interface and models.
- **Advanced Feature Configuration**:
- **[Browse](config-feature-browse.md)**: Enable web page download through third-party services or your own cloud (advanced)
- **ElevenLabs API**: Voice and cutom voice generation, only requires their API key
- **Google Search API**: guide not yet available, see the Google options in 'Environment Variables'
- **Google Search API**: guide not yet available, see the Google options in '[Environment Variables](environment-variables.md)'
- **Prodia API**: Stable Diffusion XL image generation, only requires their API key, alternative to DALL·E
## Deployment
@@ -37,22 +37,16 @@ System integrators, administrators, whitelabelers: instead of using the public b
Step-by-step deployment and system configuration instructions.
- **Deploy Your Own**
- straightforward: **Local development**, **Vercel 1-Click**
- **[Cloudflare Deployment](deploy-cloudflare.md)**
- **[Docker Deployment](deploy-docker.md)**: Containers for Local or Cloud deployments
- **[Installation](installation.md)**: Set up your own instance of big-AGI and related products
- build from source or use pre-built
- locally, in the public cloud, or on your own servers
- **Deployment Server Features**
- **[Database Setup](deploy-database.md)**: Optional, only required to enable "Chat Link Sharing"
- **[Environment Variables](environment-variables.md)**: 📌 Set server-side API keys and special features in your deployments
- **[HTTP Basic Authentication](deploy-authentication.md)**: Optional, Secure your big-AGI instance with a username and password
## Customization & Derivative UIs
👏 Customize big-AGI to fit your needs.
- **[Customizing big-AGI](customizations.md)**: how to alter source code and server-side configuration
- **Advanced Customizations**:
- **[Source code alterations guide](customizations.md)**: source code primer and alterations guidelines
- **[Basic Authentication](deploy-authentication.md)**: Optional, adds a username and password wall
- **[Database Setup](deploy-database.md)**: Optional, enables "Chat Link Sharing"
- **[Environment Variables](environment-variables.md)**: 📌 Pre-configures models and services
## Support and Community
+52 -4
View File
@@ -5,12 +5,60 @@ by release.
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
### 1.13.0 - Feb 2024
### 1.17.0 - Jun 2024
- milestone: [1.13.0](https://github.com/enricoros/big-agi/milestone/13)
- milestone: [1.17.0](https://github.com/enricoros/big-agi/milestone/17)
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
## What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
### What's New in 1.16.1...1.16.3 · Jun 20, 2024 (patch releases)
- 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
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
- New **[Perplexity](https://www.perplexity.ai/)** and **[Groq](https://groq.com/)** integration (thanks @Penagwin). [#407](https://github.com/enricoros/big-AGI/issues/407), [#427](https://github.com/enricoros/big-AGI/issues/427)
- **[LocalAI](https://localai.io/models/)** deep integration, including support for [model galleries](https://github.com/enricoros/big-AGI/issues/411)
- **Mistral** Large and Google **Gemini 1.5** support
- Performance optimizations: runs [much faster](https://twitter.com/enricoros/status/1756553038293303434?utm_source=localhost:3000&utm_medium=big-agi), saves lots of power, reduces memory usage
- Enhanced UX with auto-sizing charts, refined search and folder functionalities, perfected scaling
- And with more UI improvements, documentation, bug fixes (20 tickets), and developer enhancements
- [Release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.14.0), and changes [v1.13.1...v1.14.0](https://github.com/enricoros/big-AGI/compare/v1.13.1...v1.14.0) (233 commits, 8,000+ lines changed)
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
@@ -22,7 +70,7 @@ https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385
- Better looking chats with improved spacing, fonts, and menus
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-lmstudio.md) (thanks @aj47), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/deploy-database.md) (thanks @ranfysvalle02), and speedups
## What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline
### What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline
https://github.com/enricoros/big-AGI/assets/32999/95ceb03c-945d-4fdd-9a9f-3317beb54f3f
+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.
+29 -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.
@@ -43,20 +62,24 @@ Test your application thoroughly using local development (refer to README.md for
- [deploy-cloudflare.md](deploy-cloudflare.md): for Cloudflare Workers deployment
- [deploy-docker.md](deploy-docker.md): for Docker deployment instructions and examples
<br/>
## Debugging
We introduced the `/info/debug` page that provides a detailed overview of the application's environment, including the API keys, environment variables, and other configuration settings.
<br/>
## Community Projects - Share Your Project
After deployment, share your project with the community. We will link to your project to help others discover and learn from your work.
| Project | Features | GitHub |
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| 🚀 CoolAGI: Where AI meets Imagination<br/>![CoolAGI Logo](https://github.com/nextgen-user/freegpt4plus/assets/150797204/9b0e1232-4791-4d61-b949-16f9eb284c22) | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
| Project | Features | GitHub |
|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
| 🚀 CoolAGI: Where AI meets Imagination<br/>![CoolAGI Logo](https://github.com/nextgen-user/freegpt4plus/assets/150797204/9b0e1232-4791-4d61-b949-16f9eb284c22) | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
For public projects, update your README.md with your modifications and submit a pull request to add your project to our list, aiding in its discovery.
<br/>
<br/>
## Best Practices
+1 -1
View File
@@ -53,7 +53,7 @@ As of Feb 27, 2024, this feature is in development.
## Configurations
| Scope | Default | Description / Instructions |
| Scope | Default | Description / Instructions |
|-----------------------------------------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------|
| Your source builds of big-AGI | None | **Vercel**: enable Vercel Analytics from the dashboard. · **Google Analytics**: set environment variable at build time. |
| Your docker builds of big-AGI | None | **Vercel**: n/a. · **Google Analytics**: set environment variable at `docker build` time. |
+1 -1
View File
@@ -9,7 +9,7 @@ Docker ensures faster development cycles, easier collaboration, and seamless env
```bash
git clone https://github.com/enricoros/big-agi.git
cd big-agi
```
```
2. **Build the Docker Image**: Build a local docker image from the provided Dockerfile:
```bash
docker build -t big-agi .
+3 -3
View File
@@ -91,7 +91,7 @@ requiring the user to enter an API key
| `ANTHROPIC_API_HOST` | Changes the backend host for the Anthropic vendor, to enable platforms such as [config-aws-bedrock.md](config-aws-bedrock.md) | Optional |
| `GEMINI_API_KEY` | The API key for Google AI's Gemini | Optional |
| `GROQ_API_KEY` | The API key for Groq Cloud | Optional |
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
| `LOCALAI_API_KEY` | The (Optional) API key for LocalAI | Optional |
| `MISTRAL_API_KEY` | The API key for Mistral | Optional |
| `OLLAMA_API_HOST` | Changes the backend host for the Ollama vendor. See [config-local-ollama.md](config-local-ollama) | |
@@ -128,7 +128,7 @@ Enable the app to Talk, Draw, and Google things up.
| `GOOGLE_CSE_ID` | Google Custom/Programmable Search Engine ID - [Link to PSE](https://programmablesearchengine.google.com/) |
| **Browse** | |
| `PUPPETEER_WSS_ENDPOINT` | Puppeteer WebSocket endpoint - used for browsing (pade downloadeing), etc. |
| **Backend** | |
| **Backend** | |
| `BACKEND_ANALYTICS` | Semicolon-separated list of analytics flags (see backend.analytics.ts). Flags: `domain` logs the responding domain. |
| `HTTP_BASIC_AUTH_USERNAME` | See the [Authentication](deploy-authentication.md) guide. Username for HTTP Basic Authentication. |
| `HTTP_BASIC_AUTH_PASSWORD` | Password for HTTP Basic Authentication. |
@@ -147,5 +147,5 @@ The value of these variables are passed to the frontend (Web UI) - make sure the
---
For a higher level overview of backend code and environemnt customization,
For a higher level overview of backend code and environment customization,
see the [big-AGI Customization](customizations.md) guide.
+119
View File
@@ -0,0 +1,119 @@
# Installation Guide
Welcome to the big-AGI Installation Guide - Whether you're a developer
eager to explore, a system integrator, or an enterprise looking for a
white-label solution, this comprehensive guide ensures a smooth setup
process for your own instance of big-AGI and related products.
**Try big-AGI** - You don't need to install anything if you want to play with big-AGI
and have your API keys to various model services. You can access our free instance on [big-AGI.com](https://big-agi.com).
The free instance runs the latest `main-stable` branch from this repository.
## 🧩 Build-your-own
If you want to change the code, have a deeper configuration,
add your own models, or run your own instance, follow the steps below.
### Local Development
**Prerequisites:**
- Node.js and npm installed on your machine.
**Steps:**
1. Clone the big-AGI repository:
```bash
git clone https://github.com/enricoros/big-AGI.git
cd big-AGI
```
2. Install dependencies:
```bash
npm install
```
3. Run the development server:
```bash
npm run dev
```
Your big-AGI instance is now running at `http://localhost:3000`.
### Local Production build
The production build is optimized for performance and follows
the same steps 1 and 2 as for [local development](#local-development).
3. Build the production version:
```bash
# .. repeat the steps above up to `npm install`, then:
npm run build
```
4. Start the production server (`npx` may be optional):
```bash
npx next start --port 3000
```
Your big-AGI production instance is on `http://localhost:3000`.
### Advanced Customization
Want to pre-enable models, customize the interface, or deploy with username/password or alter code to your needs?
Check out the [Customizations Guide](README.md) for detailed instructions.
## ☁️ Cloud Deployment Options
To deploy big-AGI on a public server, you have several options. Choose the one that best fits your needs.
### Deploy on Vercel
Install big-AGI on Vercel with just a few clicks.
Create your GitHub fork, create a Vercel project over that fork, and deploy it. Or press the button below for convenience.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
### Deploy on Cloudflare
Deploy on Cloudflare's global network by installing big-AGI on
Cloudflare Pages. Check out the [Cloudflare Installation Guide](deploy-cloudflare.md)
for step-by-step instructions.
### Docker Deployments
Containerize your big-AGI installation using Docker for portability and scalability.
Our [Docker Deployment Guide](deploy-docker.md) will walk you through the process,
or follow the steps below for a quick start.
1. (optional) Build the Docker image - if you do not want to use the [pre-built Docker images](https://github.com/enricoros/big-AGI/pkgs/container/big-agi):
```bash
docker build -t big-agi .
```
2. Run the Docker container with either:
```bash
# 2A. if you built the image yourself:
docker run -d -p 3000:3000 big-agi
# 2B. or use the pre-built image:
docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi
# 2C. or use docker-compose:
docker-compose up
```
Access your big-AGI instance at `http://localhost:3000`.
### Midori AI Subsystem for Docker Deployment
Follow the instructions found on [Midori AI Subsystem Site](https://io.midori-ai.xyz/subsystem/manager/)
for your host OS. After completing the setup process, install the Big-AGI docker backend to the Midori AI Subsystem.
## Enterprise-Grade Installation
For businesses seeking a fully-managed, scalable solution, consider our managed installations.
Enjoy all the features of big-AGI without the hassle of infrastructure management. [hello@big-agi.com](mailto:hello@big-agi.com) to learn more.
## Support
Join our vibrant community of developers, researchers, and AI enthusiasts. Share your projects, get help, and collaborate with others.
- [Discord Community](https://discord.gg/MkH4qj2Jp9)
- [Twitter](https://twitter.com/yourusername)
For any questions or inquiries, please don't hesitate to [reach out to our team](mailto:hello@big-agi.com).
+5
View File
@@ -0,0 +1,5 @@
From root:
```bash
BIG_AGI_BUILD=standalone next build
electron . --enable-logging
```
+61
View File
@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html>
<head>
<style>
body {
background: #2e2c29;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
font-family: Arial, sans-serif;
}
.loader-container {
position: relative;
width: 100px;
height: 100px;
}
.spinner {
position: absolute;
top: 0;
left: 0;
border: 5px solid rgba(255, 255, 255, 0.3);
border-top: 5px solid #3498db;
border-radius: 50%;
width: 100px;
height: 100px;
animation: spin 2s linear infinite;
}
.logo {
position: absolute;
top: 15px;
left: 15px;
width: 80px;
height: 80px;
background: url('tray-icon.png') no-repeat center center;
background-size: contain;
animation: counter-spin 3.33s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
@keyframes counter-spin {
0% { transform: rotate(360deg); }
100% { transform: rotate(0deg); }
}
</style>
</head>
<body>
<div class="loader-container">
<div class="spinner"></div>
<div class="logo"></div>
</div>
</body>
</html>
+178
View File
@@ -0,0 +1,178 @@
const { app, BrowserWindow, Tray, Menu, ipcMain, screen, nativeTheme, shell } = require('electron');
const path = require('path');
const startServer = require('./server.js');
const { autoUpdater } = require('electron-updater');
let mainWindow;
let tray;
const port = 3000;
async function createWindow() {
try {
console.log('Starting server...');
await startServer(port);
console.log('Server started successfully');
const { width, height } = screen.getPrimaryDisplay().workAreaSize;
// // Set up a loading screen
// loadingScreen = new BrowserWindow({
// // width: 150,
// // height: 150,
// frame: false,
// transparent: false,
// alwaysOnTop: true,
// webPreferences: {
// nodeIntegration: true,
// },
// backgroundColor: '#2e2c29',
// });
//
// loadingScreen.loadFile(path.join(__dirname, 'loading.html'));
// loadingScreen.center();
// console.log('Loading screen created');
console.log('Preload script path:', path.join(__dirname, 'preload.js'));
mainWindow = new BrowserWindow({
width: Math.min(1280, width * 0.8),
height: Math.min(800, height * 0.8),
minWidth: 430,
minHeight: 600,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, 'preload.js'),
sandbox: false,
devTools: false,
},
backgroundColor: nativeTheme.shouldUseDarkColors ? '#1a1a1a' : '#ffffff',
show: true,
frame: false,
titleBarStyle: 'hidden',
icon: path.join(__dirname, 'tray-icon.png'),
// New "insane" features:
// transparent: true, // Enable window transparency
vibrancy: 'under-window', // Add vibrancy effect (macOS only)
visualEffectState: 'active', // Keep vibrancy active even when not focused (macOS only)
roundedCorners: true, // Enable rounded corners (macOS only)
// thickFrame: false, // Use a thinner frame on Windows
autoHideMenuBar: true, // Auto-hide the menu bar, press Alt to show it
scrollBounce: true, // Enable bounce effect when scrolling (macOS only)
});
mainWindow.removeMenu();
mainWindow.setTitle('Your Professional App Name');
console.log('Attempting to load main window URL...');
await mainWindow.loadURL(`http://localhost:${port}`);
console.log('Main window URL loaded successfully');
mainWindow.once('ready-to-show', () => {
console.log('Main window ready to show');
// if (loadingScreen) {
// loadingScreen.close();
// }
mainWindow.show();
mainWindow.focus();
});
createTray();
autoUpdater.checkForUpdatesAndNotify();
// Handle window state
let isQuitting = false;
mainWindow.on('close', (event) => {
if (!isQuitting) {
event.preventDefault();
mainWindow.hide();
}
});
app.on('before-quit', () => {
isQuitting = true;
});
// Adjust window behavior
mainWindow.on('maximize', () => {
mainWindow.webContents.send('window-maximized');
});
mainWindow.on('unmaximize', () => {
mainWindow.webContents.send('window-unmaximized');
});
// Warn if preloads fail
mainWindow.webContents.on('preload-error', (event, preloadPath, error) => {
console.error('Preload error:', preloadPath, error);
});
mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription) => {
console.error('Failed to load:', errorCode, errorDescription);
});
// Handle external links
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});
} catch (err) {
console.error('Error in createWindow:', err);
app.quit();
}
}
function createTray() {
tray = new Tray(path.join(__dirname, 'tray-icon.png'));
const contextMenu = Menu.buildFromTemplate([
{ label: 'Show App', click: () => mainWindow.show() },
{ type: 'separator' },
{ label: 'Quit', click: () => app.quit() },
]);
tray.setToolTip('Your Professional App Name');
tray.setContextMenu(contextMenu);
tray.on('click', () => {
mainWindow.isVisible() ? mainWindow.hide() : mainWindow.show();
});
}
app.whenReady().then(() => {
console.log('App is ready, creating window...');
createWindow().catch((err) => {
console.error('Failed to create window:', err);
app.quit();
});
app.on('activate', function() {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});
});
app.on('window-all-closed', function() {
if (process.platform !== 'darwin') app.quit();
});
// IPC handlers for window controls
ipcMain.on('minimize-window', () => mainWindow.minimize());
ipcMain.on('maximize-window', () => {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize();
} else {
mainWindow.maximize();
}
});
ipcMain.on('close-window', () => mainWindow.close());
// Auto-updater events
autoUpdater.on('update-available', () => {
mainWindow.webContents.send('update_available');
});
autoUpdater.on('update-downloaded', () => {
mainWindow.webContents.send('update_downloaded');
});
+36
View File
@@ -0,0 +1,36 @@
const { contextBridge, desktopCapturer, ipcRenderer } = require('electron');
const { readFileSync } = require('fs');
const { join } = require('path');
// Main bridge
contextBridge.exposeInMainWorld('electron', {
sendEvent: (event) => ipcRenderer.send('app-event', event),
onUpdateAvailable: (callback) => ipcRenderer.on('update_available', callback),
onUpdateDownloaded: (callback) => ipcRenderer.on('update_downloaded', callback),
});
// Screen Capture: inject renderer.js into the web page
window.addEventListener('DOMContentLoaded', () => {
console.log('Screen Capture: Injecting renderer.js into the web page');
const rendererScript = document.createElement('script');
rendererScript.text = readFileSync(join(__dirname, 'renderer.js'), 'utf8');
document.body.appendChild(rendererScript);
});
// Screen Capture: expose desktopCapturer to the web page
contextBridge.exposeInMainWorld('myCustomGetDisplayMedia', async () => {
console.log('Screen Capture: Calling desktopCapturer.getSources');
const sources = await desktopCapturer.getSources({
types: ['window', 'screen'],
});
console.log('Available sources:', sources);
// you should create some kind of UI to prompt the user
// to select the correct source like Google Chrome does
// this is just for testing purposes
return sources[0];
});
console.log('Preload script loaded');
+30
View File
@@ -0,0 +1,30 @@
// https://github.com/aabuhijleh/override-getDisplayMedia/blob/main/renderer.js
// This file is required by the index.html file and will
// be executed in the renderer process for that window.
// No Node.js APIs are available in this process because
// `nodeIntegration` is turned off. Use `preload.js` to
// selectively enable features needed in the rendering
// process.
// override getDisplayMedia
navigator.mediaDevices.getDisplayMedia = async () => {
const selectedSource = await globalThis.myCustomGetDisplayMedia();
// create MediaStream
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: {
mandatory: {
chromeMediaSource: 'desktop',
chromeMediaSourceId: selectedSource.id,
minWidth: 1280,
maxWidth: 1280,
minHeight: 720,
maxHeight: 720,
},
},
});
return stream;
};
+71
View File
@@ -0,0 +1,71 @@
const { createServer } = require('http');
const { parse } = require('url');
const next = require('next');
const path = require('path');
// const dev = process.env.NODE_ENV !== 'production';
const dir = path.join(__dirname, '..'); // This points to the root of your project
const app = next({ dev: false, dir });
const handle = app.getRequestHandler();
function startServer(port) {
return new Promise((resolve, reject) => {
app.prepare()
.then(() => {
const server = createServer((req, res) => {
// Basic request logging
console.log(`${new Date().toISOString()} - ${req.method} ${req.url}`);
// Simple rate limiting
if (rateLimiter(req)) {
res.statusCode = 429;
res.end('Too Many Requests');
return;
}
// Handle the request
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
});
server.listen(port, (err) => {
if (err) reject(err);
console.log(`> Ready on http://localhost:${port}`);
resolve(server);
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
server.close(() => {
console.log('HTTP server closed');
});
});
})
.catch(err => reject(err));
});
}
// Simple in-memory rate limiter
const MAX_REQUESTS_PER_MINUTE = 100;
const requestCounts = new Map();
function rateLimiter(req) {
const ip = req.socket.remoteAddress;
const now = Date.now();
const windowStart = now - 60000; // 1 minute ago
const requestTimestamps = requestCounts.get(ip) || [];
const requestsInWindow = requestTimestamps.filter(timestamp => timestamp > windowStart);
if (requestsInWindow.length >= MAX_REQUESTS_PER_MINUTE) {
return true; // Rate limit exceeded
}
requestTimestamps.push(now);
requestCounts.set(ip, requestTimestamps);
return false; // Rate limit not exceeded
}
module.exports = startServer;
Binary file not shown.

After

Width:  |  Height:  |  Size: 993 B

+11 -3
View File
@@ -13,7 +13,7 @@ let nextConfig = {
// [exports] https://nextjs.org/docs/advanced-features/static-html-export
...buildType && {
output: buildType,
distDir: 'dist',
// distDir: 'dist',
// disable image optimization for exports
images: { unoptimized: true },
@@ -27,7 +27,7 @@ let nextConfig = {
serverComponentsExternalPackages: ['puppeteer-core'],
},
webpack: (config, _options) => {
webpack: (config, { isServer }) => {
// @mui/joy: anything material gets redirected to Joy
config.resolve.alias['@mui/material'] = '@mui/joy';
@@ -37,9 +37,17 @@ let nextConfig = {
layers: true,
};
// fix warnings for async functions in the browser (https://github.com/vercel/next.js/issues/64792)
if (!isServer) {
config.output.environment = { ...config.output.environment, asyncFunction: true };
}
// prevent too many small chunks (40kb min) on 'client' packs (not 'server' or 'edge-server')
if (typeof config.optimization.splitChunks === 'object' && config.optimization.splitChunks.minSize)
// noinspection JSUnresolvedReference
if (typeof config.optimization.splitChunks === 'object' && config.optimization.splitChunks.minSize) {
// noinspection JSUnresolvedReference
config.optimization.splitChunks.minSize = 40 * 1024;
}
return config;
},
+5301 -1125
View File
File diff suppressed because it is too large Load Diff
+74 -42
View File
@@ -1,87 +1,119 @@
{
"name": "big-agi",
"version": "1.13.0",
"version": "1.16.0",
"private": true,
"author": "Enrico Ros <enrico.ros@gmail.com>",
"repository": "https://github.com/enricoros/big-agi",
"main": "electron/main.js",
"scripts": {
"dev": "next dev",
"dev": "node electron/server.js",
"build": "next build",
"start": "next start",
"start": "NODE_ENV=production node electron/server.js",
"lint": "next lint",
"postinstall": "prisma generate",
"db:push": "prisma db push",
"db:studio": "prisma studio",
"vercel:env:pull": "npx vercel env pull .env.development.local"
"vercel:env:pull": "npx vercel env pull .env.development.local",
"electron": "electron .",
"electron-dev": "concurrently \"npm run dev\" \"electron .\"",
"electron-build": "next build && electron-builder",
"electron-start": "npm run build && electron ."
},
"prisma": {
"schema": "src/server/prisma/schema.prisma"
},
"dependencies": {
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.3",
"@emotion/react": "^11.11.4",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.11",
"@mui/joy": "^5.0.0-beta.29",
"@next/bundle-analyzer": "^14.1.0",
"@next/third-parties": "^14.1.0",
"@prisma/client": "^5.10.2",
"@emotion/styled": "^11.11.5",
"@mui/icons-material": "^5.16.0",
"@mui/joy": "^5.0.0-beta.47",
"@mui/material": "^5.16.0",
"@next/bundle-analyzer": "^14.2.4",
"@next/third-parties": "^14.2.4",
"@prisma/client": "^5.16.1",
"@sanity/diff-match-patch": "^3.1.1",
"@t3-oss/env-nextjs": "^0.9.2",
"@tanstack/react-query": "~4.36.1",
"@trpc/client": "10.44.1",
"@trpc/next": "10.44.1",
"@trpc/react-query": "10.44.1",
"@trpc/server": "10.44.1",
"@vercel/analytics": "^1.2.2",
"@vercel/speed-insights": "^1.0.10",
"@t3-oss/env-nextjs": "^0.10.1",
"@tanstack/react-query": "^5.50.1",
"@trpc/client": "11.0.0-alpha-tmp-issues-5851-take-two.496",
"@trpc/next": "11.0.0-alpha-tmp-issues-5851-take-two.496",
"@trpc/react-query": "11.0.0-alpha-tmp-issues-5851-take-two.496",
"@trpc/server": "11.0.0-alpha-tmp-issues-5851-take-two.496",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"browser-fs-access": "^0.35.0",
"cheerio": "^1.0.0-rc.12",
"dexie": "^4.0.7",
"dexie-react-hooks": "^1.1.7",
"electron-updater": "^6.2.1",
"eventsource-parser": "^1.1.2",
"idb-keyval": "^6.2.1",
"next": "^14.1.0",
"nanoid": "^5.0.7",
"next": "~14.2.4",
"nprogress": "^0.2.0",
"pdfjs-dist": "4.0.379",
"pdfjs-dist": "4.4.168",
"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.14.1",
"react-resizable-panels": "^2.0.11",
"react-player": "^2.16.0",
"react-resizable-panels": "^2.0.20",
"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.4",
"superjson": "^2.2.1",
"tesseract.js": "^5.0.5",
"tiktoken": "^1.0.13",
"uuid": "^9.0.1",
"zod": "^3.22.4",
"zustand": "^4.5.1"
"tesseract.js": "^5.1.0",
"tiktoken": "^1.0.15",
"turndown": "^7.2.0",
"zod": "^3.23.8",
"zustand": "^4.5.4"
},
"devDependencies": {
"@cloudflare/puppeteer": "^0.0.5",
"@types/node": "^20.11.20",
"@cloudflare/puppeteer": "0.0.11",
"@types/node": "^20.14.10",
"@types/nprogress": "^0.2.3",
"@types/plantuml-encoder": "^1.4.2",
"@types/prismjs": "^1.26.3",
"@types/react": "^18.2.59",
"@types/prismjs": "^1.26.4",
"@types/react": "^18.3.3",
"@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/uuid": "^9.0.8",
"@types/turndown": "^5.0.4",
"concurrently": "^8.2.2",
"electron": "^31.1.0",
"electron-builder": "^24.13.3",
"eslint": "^8.57.0",
"eslint-config-next": "^14.1.0",
"prettier": "^3.2.5",
"prisma": "^5.10.2",
"typescript": "^5.3.3"
"eslint-config-next": "^14.2.4",
"prettier": "^3.3.2",
"prisma": "^5.16.1",
"typescript": "^5.5.3"
},
"engines": {
"node": "^20.0.0 || ^18.0.0"
},
"build": {
"appId": "com.yourcompany.yourappname",
"productName": "Your App Name",
"files": [
"electron/**/*",
".next/**/*",
"public/**/*",
"next.config.js"
],
"directories": {
"buildResources": "electron"
},
"extraMetadata": {
"main": "electron/main.js"
}
}
}
}
+12 -10
View File
@@ -11,13 +11,14 @@ import 'katex/dist/katex.min.css';
import '~/common/styles/CodePrism.css';
import '~/common/styles/GithubMarkdown.css';
import '~/common/styles/NProgress.css';
import '~/common/styles/agi.effects.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 +34,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 />);
}
+9 -7
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';
@@ -25,13 +25,14 @@ import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs, useCapa
// stores access
import { getLLMsDebugInfo } from '~/modules/llms/store-llms';
import { useAppStateStore } from '~/common/state/store-appstate';
import { useChatStore } from '~/common/state/store-chats';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { useFolderStore } from '~/common/state/store-folders';
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));
+2 -2
View File
@@ -13,7 +13,7 @@ import { withLayout } from '~/common/layout/withLayout';
function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
// external state
const { data, isError, error, isLoading } = apiQuery.backend.exchangeOpenRouterKey.useQuery({ code: props.openRouterCode || '' }, {
const { data, isError, error, isPending } = apiQuery.backend.exchangeOpenRouterKey.useQuery({ code: props.openRouterCode || '' }, {
enabled: !!props.openRouterCode,
refetchOnWindowFocus: false,
staleTime: Infinity,
@@ -56,7 +56,7 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
Welcome Back
</Typography>
{isLoading && <Typography level='body-sm'>Loading...</Typography>}
{isPending && <Typography level='body-sm'>Loading...</Typography>}
{isErrorInput && <InlineError error='There was an issue retrieving the code from OpenRouter.' />}
+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'))
+10
View File
@@ -0,0 +1,10 @@
import * as React from 'react';
import { AppTokens } from '../src/apps/tokens/AppTokens';
import { withLayout } from '~/common/layout/withLayout';
export default function PersonasPage() {
return withLayout({ type: 'optima' }, <AppTokens />);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 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}
+106
View File
@@ -0,0 +1,106 @@
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, DConversation } from '~/common/stores/chat/chat.conversation';
import { createDMessageTextContent, DMessage } from '~/common/stores/chat/chat.message';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
function initTestConversation(): DConversation {
const conversation = createDConversation();
conversation.messages.push(createDMessageTextContent('system', 'You are a helpful assistant.')); // Beam Test - seed1
conversation.messages.push(createDMessageTextContent('user', 'Hello, who are you? (please expand...)')); // Beam Test - seed2
return conversation;
}
function initTestBeamStore(messages: DMessage[], beamStore: BeamStoreApi = createBeamVanillaStore()): BeamStoreApi {
beamStore.getState().open(messages, useModelsStore.getState().chatLLMId, (content) => alert(content));
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>
);
}
+1 -1
View File
@@ -2,7 +2,7 @@ import * as React from 'react';
import { Container, Sheet } from '@mui/joy';
import type { DConversationId } from '~/common/state/store-chats';
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
import { useRouterQuery } from '~/common/app.routes';
import { CallWizard } from './CallWizard';
+9 -47
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 { useChatStore } from '~/common/stores/chat/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>
+6 -28
View File
@@ -1,13 +1,13 @@
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 { DConversation, DConversationId, conversationTitle } from '~/common/stores/chat/chat.conversation';
import { GitHubProjectIssueCard } from '~/common/components/GitHubProjectIssueCard';
import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats';
import { animationShadowRingLimey } from '~/common/util/animUtils';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import type { AppCallIntent } from './AppCall';
@@ -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'
@@ -81,7 +60,7 @@ const ContactCardConversationCall = (props: { conversation: DConversation, onCon
function CallContactCard(props: {
persona: MockPersona,
callGrayUI: boolean,
conversations: DConversation[],
conversations: Readonly<DConversation[]>,
setCallIntent: (intent: AppCallIntent) => void,
}) {
@@ -125,7 +104,6 @@ function CallContactCard(props: {
sx={{
mx: 'auto',
mt: '-2.5rem',
zIndex: 1,
}}
/>
@@ -211,7 +189,7 @@ function CallContactCard(props: {
function useConversationsByPersona() {
const conversations = useChatStore(state => state.conversations, shallow);
const conversations = useChatStore(state => state.conversations);
return React.useMemo(() => {
// group by personaId
@@ -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>
+36 -38
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,22 +7,24 @@ 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 { useChatLLMDropdown } from '../chat/components/useLLMDropdown';
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
import { useChatLLMDropdown } from '../chat/components/layout-bar/useLLMDropdown';
import { EXPERIMENTAL_speakTextStream } from '~/modules/elevenlabs/elevenlabs.client';
import { SystemPurposeId, SystemPurposes } from '../../data';
import { llmStreamingChatGenerate, VChatMessageIn } from '~/modules/llms/llm.client';
import { useElevenLabsVoiceDropdown } from '~/modules/elevenlabs/useElevenLabsVoiceDropdown';
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
import { Link } from '~/common/components/Link';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { conversationTitle, createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
import { conversationTitle } from '~/common/stores/chat/chat.conversation';
import { createDMessageTextContent, DMessage, messageFragmentsReduceText, messageSingleTextOrThrow } from '~/common/stores/chat/chat.message';
import { launchAppChat, navigateToIndex } from '~/common/app.routes';
import { playSoundUrl, usePlaySoundUrl } from '~/common/util/audioUtils';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import type { AppCallIntent } from './AppCall';
@@ -57,7 +59,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 +101,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 +109,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);
@@ -118,9 +120,9 @@ export function Telephone(props: {
const onSpeechResultCallback = React.useCallback((result: SpeechResult) => {
setSpeechInterim(result.done ? null : { ...result });
if (result.done) {
const transcribed = result.transcript.trim();
if (transcribed.length >= 1)
setCallMessages(messages => [...messages, createDMessage('user', transcribed)]);
const userSpeechTranscribed = result.transcript.trim();
if (userSpeechTranscribed.length >= 1)
setCallMessages(messages => [...messages, createDMessageTextContent('user', userSpeechTranscribed)]); // [state] append user:speech
}
}, []);
const { isSpeechEnabled, isRecording, isRecordingAudio, isRecordingSpeech, startRecording, stopRecording, toggleRecording } = useSpeechRecognition(onSpeechResultCallback, 1000);
@@ -136,11 +138,11 @@ export function Telephone(props: {
// pickup / hangup
React.useEffect(() => {
!isRinging && playSoundUrl(isConnected ? '/sounds/chat-begin.mp3' : '/sounds/chat-end.mp3');
!isRinging && AudioPlayer.playUrl(isConnected ? '/sounds/chat-begin.mp3' : '/sounds/chat-end.mp3');
}, [isRinging, isConnected]);
// ringtone
usePlaySoundUrl(isRinging ? '/sounds/chat-ringtone.mp3' : null, 300, 2800 * 2);
AudioPlayer.usePlayUrl(isRinging ? '/sounds/chat-ringtone.mp3' : null, 300, 2800 * 2);
/// CONNECTED
@@ -169,7 +171,8 @@ export function Telephone(props: {
const phoneMessages = personaCallStarters || ['Hello?', 'Hey!'];
const firstMessage = phoneMessages[Math.floor(Math.random() * phoneMessages.length)];
setCallMessages([createDMessage('assistant', firstMessage)]);
setCallMessages([createDMessageTextContent('assistant', firstMessage)]); // [state] set assistant:hello message
// fire/forget
void EXPERIMENTAL_speakTextStream(firstMessage, personaVoiceId);
@@ -179,22 +182,30 @@ export function Telephone(props: {
// [E] persona streaming response - upon new user message
React.useEffect(() => {
// only act when we have a new user message
if (!isConnected || callMessages.length < 1 || callMessages[callMessages.length - 1].role !== 'user')
if (!isConnected || callMessages.length < 1)
return;
switch (callMessages[callMessages.length - 1].text) {
// Voice commands
const lastUserMessage = callMessages[callMessages.length - 1];
if (lastUserMessage.role !== 'user')
return;
switch (messageFragmentsReduceText(lastUserMessage.fragments)) {
// do not respond
case 'Stop.':
return;
// command: close the call
case 'Goodbye.':
setStage('ended');
setTimeout(launchAppChat, 2000);
return;
// command: regenerate answer
case 'Retry.':
case 'Try again.':
setCallMessages(messages => messages.slice(0, messages.length - 2));
return;
// command: restart chat
case 'Restart.':
setCallMessages([]);
@@ -206,7 +217,7 @@ export function Telephone(props: {
// temp fix: when the chat has no messages, only assume a single system message
const chatMessages: { role: VChatMessageIn['role'], text: string }[] = (reMessages && reMessages.length > 0)
? reMessages
? reMessages.map(message => ({ role: message.role, text: messageSingleTextOrThrow(message) }))
: personaSystemMessage
? [{ role: 'system', text: personaSystemMessage }]
: [];
@@ -217,7 +228,7 @@ export function Telephone(props: {
{ role: 'system', content: 'You are having a phone call. Your response style is brief and to the point, and according to your personality, defined below.' },
...chatMessages.map(message => ({ role: message.role, content: message.text })),
{ role: 'system', content: 'You are now on the phone call related to the chat above. Respect your personality and answer with short, friendly and accurate thoughtful lines.' },
...callMessages.map(message => ({ role: message.role, content: message.text })),
...callMessages.map(message => ({ role: message.role, content: messageSingleTextOrThrow(message) })),
];
// perform completion
@@ -225,7 +236,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;
@@ -237,7 +248,7 @@ export function Telephone(props: {
}).finally(() => {
setPersonaTextInterim(null);
if (finalText || error)
setCallMessages(messages => [...messages, createDMessage('assistant', finalText + (error ? ` (ERROR: ${error.message || error.toString()})` : ''))]);
setCallMessages(messages => [...messages, createDMessageTextContent('assistant', finalText + (error ? ` (ERROR: ${error.message || error.toString()})` : ''))]); // [state] append assistant:call_response
// fire/forget
if (finalText?.length >= 1)
void EXPERIMENTAL_speakTextStream(finalText, personaVoiceId);
@@ -331,28 +342,15 @@ 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) =>
<CallMessage
key={message.id}
text={message.text}
text={messageSingleTextOrThrow(message)}
variant={message.role === 'assistant' ? 'solid' : 'soft'}
color={message.role === 'assistant' ? 'neutral' : 'primary'}
role={message.role}
+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` }
: {}),
}}
>
+280 -291
View File
@@ -1,62 +1,75 @@
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 { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
import { ConversationsManager } from '~/common/chats/ConversationsManager';
import { DConversation, DConversationId } from '~/common/stores/chat/chat.conversation';
import { DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragments } from '~/common/stores/chat/chat.fragments';
import { GlobalShortcutDefinition, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcuts';
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 { createDMessageFromFragments, createDMessageTextContent, DMessageMetadata, duplicateDMessageMetadata } from '~/common/stores/chat/chat.message';
import { getConversation, getConversationSystemPurposeId, useConversation } from '~/common/stores/chat/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 { ChatDrawerMemo } from './components/ChatDrawer';
import { ChatDropdowns } from './components/ChatDropdowns';
import { ChatBarAltBeam } from './components/layout-bar/ChatBarAltBeam';
import { ChatBarAltTitle } from './components/layout-bar/ChatBarAltTitle';
import { ChatBarDropdowns } from './components/layout-bar/ChatBarDropdowns';
import { ChatBeamWrapper } from './components/ChatBeamWrapper';
import { ChatDrawerMemo } from './components/layout-drawer/ChatDrawer';
import { ChatMessageList } from './components/ChatMessageList';
import { ChatPageMenuItems } from './components/ChatPageMenuItems';
import { ChatTitle } from './components/ChatTitle';
import { ChatPageMenuItems } from './components/layout-menu/ChatPageMenuItems';
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 type { ChatExecuteMode } from './execute-mode/execute-mode.types';
import { _handleExecute } from './editors/_handleExecute';
import { gcChatImageAssets } from './editors/image-generate';
// what to say when a chat is new and has no title
export const CHAT_NOVEL_TITLE = 'Chat';
/**
* Mode: how to treat the input from the Composer
*/
export type ChatModeId =
| 'generate-text'
| 'generate-text-beam'
| 'append-user'
| 'generate-image'
| '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() {
@@ -78,35 +91,59 @@ 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 { paneUniqueConversationIds, paneHandlers, paneBeamStores } = React.useMemo(() => {
const paneConversationIds: (DConversationId | null)[] = chatPanes.map(pane => pane.conversationId || null);
const paneHandlers = paneConversationIds.map(cId => cId ? ConversationsManager.getHandler(cId) : null);
const paneBeamStores = paneHandlers.map(handler => handler?.getBeamStore() ?? null);
const paneUniqueConversationIds = Array.from(new Set(paneConversationIds.filter(Boolean))) as DConversationId[];
return {
paneHandlers: paneHandlers,
paneBeamStores: paneBeamStores,
paneUniqueConversationIds: paneUniqueConversationIds,
};
}, [chatPanes]);
const beamsOpens = useAreBeamsOpen(paneBeamStores);
const beamOpenStoreInFocusedPane = React.useMemo(() => {
const open = focusedPaneIndex !== null ? (beamsOpens?.[focusedPaneIndex] ?? false) : false;
return open ? paneBeamStores?.[focusedPaneIndex!] ?? null : null;
}, [beamsOpens, focusedPaneIndex, paneBeamStores]);
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();
@@ -123,27 +160,29 @@ export function AppChat() {
const isMultiPane = chatPanes.length >= 2;
const isMultiAddable = chatPanes.length < 4;
const isMultiConversationId = isMultiPane && new Set(chatPanes.map((pane) => pane.conversationId)).size >= 2;
const isMultiConversationId = paneUniqueConversationIds.length >= 2;
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,160 +195,88 @@ 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 (chatExecuteMode: ChatExecuteMode, conversationId: DConversationId, callerNameDebug: string) => {
const outcome = await _handleExecute(chatExecuteMode, conversationId, callerNameDebug);
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' });
else if (outcome === 'err-no-last-message')
addSnackbar({ key: 'chat-no-conversation', message: 'No conversation history.', 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);
const handleComposerAction = React.useCallback((conversationId: DConversationId, chatExecuteMode: ChatExecuteMode, fragments: (DMessageContentFragment | DMessageAttachmentFragment)[], metadata?: DMessageMetadata): boolean => {
case 'ass-browse':
setMessages(conversationId, history);
return await runBrowseGetPageUpdatingState(conversationId, chatCommand.params!);
// [multicast] send the message to all the panes
const uniqueConversationIds = willMulticast
? Array.from(new Set([conversationId, ...paneUniqueConversationIds]))
: [conversationId];
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 => {
// validate inputs
if (multiPartMessage.length !== 1 || multiPartMessage[0].type !== 'text-block') {
addSnackbar({
key: 'chat-composer-action-invalid',
message: 'Only a single text part is supported for now.',
type: 'issue',
overrides: {
autoHideDuration: 2000,
},
});
// validate conversation existence
const uniqueConverations = uniqueConversationIds.map(cId => getConversation(cId)).filter(Boolean) as DConversation[];
if (!uniqueConverations.length)
return false;
}
const userText = multiPartMessage[0].text;
// multicast: send the message to all the panes
const uniqueIds = new Set([conversationId]);
if (willMulticast)
chatPanes.forEach(pane => pane.conversationId && uniqueIds.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;
}
for (const conversation of uniqueConverations) {
// create the user:message
// NOTE: this can lead to multiple chat messages with data refs that are referring to the same dblobs,
// however, we already got transferred ownership of the dblobs at this point.
const userMessage = createDMessageFromFragments('user', duplicateDMessageFragments(fragments)); // [chat] create user:message
if (metadata) userMessage.metadata = duplicateDMessageMetadata(metadata);
ConversationsManager.getHandler(conversation.id).messageAppend(userMessage); // [chat] append user message in each conversation
// fire/forget
void handleExecuteAndOutcome(chatExecuteMode /* various */, conversation.id, 'chat-composer-action'); // append user message, then '*-*'
}
return enqueued;
}, [chatPanes, willMulticast, _handleExecute]);
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[], chatEffectBeam: boolean): Promise<void> => {
await _handleExecute(!chatEffectBeam ? 'generate-text' : 'generate-text-beam', conversationId, history);
}, [_handleExecute]);
return true;
}, [paneUniqueConversationIds, handleExecuteAndOutcome, willMulticast]);
const handleMessageRegenerateLast = React.useCallback(async () => {
const focusedConversation = getConversation(focusedConversationId);
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId) => {
await handleExecuteAndOutcome('generate-content', conversationId, 'chat-execute-history'); // replace with 'history', then 'generate-text'
}, [handleExecuteAndOutcome]);
const handleMessageRegenerateLastInFocusedPane = React.useCallback(async () => {
const focusedConversation = getConversation(focusedPaneConversationId);
if (focusedPaneConversationId && focusedConversation?.messages?.length) {
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
if (lastMessage.role === 'assistant')
ConversationsManager.getHandler(focusedPaneConversationId).historyTruncateTo(lastMessage.id, -1);
await handleExecuteAndOutcome('generate-content', focusedConversation.id, 'chat-regenerate-last'); // truncate if assistant, then gen-text
}
}, [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];
return await _handleExecute('generate-text', focusedConversation.id, lastMessage.role === 'assistant'
? focusedConversation.messages.slice(0, -1)
: [...focusedConversation.messages],
);
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);
}
}, [focusedConversationId, _handleExecute]);
}, [focusedPaneConversationId]);
const handleTextDiagram = React.useCallback((diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig), []);
const handleTextImagine = React.useCallback(async (conversationId: DConversationId, messageText: string): Promise<void> => {
const handleImagineFromText = 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, [
...conversation.messages,
createDMessage('user', imaginedPrompt),
]);
}, [_handleExecute]);
const imaginedPrompt = await imaginePromptFromText(messageText, conversationId) || 'An error sign.';
const imaginePrompMessage = createDMessageTextContent('user', imaginedPrompt);
ConversationsManager.getHandler(conversationId).messageAppend(imaginePrompMessage); // [chat] append user:imagine prompt
await handleExecuteAndOutcome('generate-image', conversationId, 'chat-imagine-from-text'); // append message for 'imagine', then generate-image
}, [handleExecuteAndOutcome]);
const handleTextSpeak = React.useCallback(async (text: string): Promise<void> => {
await speakText(text);
@@ -318,13 +285,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 +302,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 +310,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 +346,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).historyClear();
setClearConversationId(null);
}
}, [clearConversationId, setMessages]);
}, [clearConversationId]);
const handleConversationClear = React.useCallback((conversationId: DConversationId) => setClearConversationId(conversationId), []);
@@ -374,13 +369,17 @@ 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]);
// run GC for dblobs in this conversation
void gcChatImageAssets(); // fire/forget
}, [deleteConversations, handleOpenConversationInFocusedPane]);
const handleConfirmedDeleteConversations = React.useCallback(() => {
!!deleteConversationIds?.length && handleDeleteConversations(deleteConversationIds, true);
@@ -395,18 +394,23 @@ export function AppChat() {
openLlmOptions(chatLLMId);
}, [openLlmOptions]);
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
const shortcuts = React.useMemo((): GlobalShortcutDefinition[] => [
// 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 +418,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[]}
chatPanesConversationIds={paneUniqueConversationIds}
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, disableNewButton, focusedPaneConversationId, handleConversationBranch, handleConversationExport, handleConversationImportDialog, handleConversationNewInFocusedPane, handleDeleteConversations, handleOpenConversationInFocusedPane, isMobile, paneUniqueConversationIds],
);
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 +471,14 @@ export function AppChat() {
>
{chatPanes.map((pane, idx) => {
const _paneIsFocused = idx === focusedPaneIndex;
const _paneConversationId = pane.conversationId;
const _paneChatHandler = chatHandlers[idx] ?? null;
const _paneChatHandler = paneHandlers[idx] ?? null;
const _paneBeamStore = paneBeamStores[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 +489,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 +502,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 +522,47 @@ 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}
isMobile={isMobile}
isMessageSelectionMode={isMessageSelectionMode}
setIsMessageSelectionMode={setIsMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationExecuteHistory={handleConversationExecuteHistory}
onTextDiagram={handleTextDiagram}
onTextImagine={handleImagineFromText}
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 +581,14 @@ export function AppChat() {
isMobile={isMobile}
chatLLM={chatLLM}
composerTextAreaRef={composerTextAreaRef}
conversationId={focusedConversationId}
targetConversationId={focusedPaneConversationId}
capabilityHasT2I={capabilityHasT2I}
isMulticast={!isMultiConversationId ? null : isComposerMulticast}
isDeveloperMode={isFocusedChatDeveloper}
onAction={handleComposerAction}
onTextImagine={handleTextImagine}
onTextImagine={handleImagineFromText}
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 +607,7 @@ export function AppChat() {
{!!tradeConfig && (
<TradeModal
config={tradeConfig}
onConversationActivate={setFocusedConversationId}
onConversationActivate={handleOpenConversationInFocusedPane}
onClose={() => setTradeConfig(null)}
/>
)}
+3 -3
View File
@@ -3,18 +3,18 @@ import ClearIcon from '@mui/icons-material/Clear';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsAlter: ICommandsProvider = {
id: 'chat-alter',
id: 'cmd-chat-alter',
rank: 25,
getCommands: () => [{
primary: '/assistant',
alternatives: ['/a'],
arguments: ['text'],
arguments: ['text...'],
description: 'Injects assistant response',
}, {
primary: '/system',
alternatives: ['/s'],
arguments: ['text'],
arguments: ['text...'],
description: 'Injects system message',
}, {
primary: '/clear',
+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: 'cmd-mode-beam',
rank: 9,
getCommands: () => getUXLabsChatBeam() ? [{
getCommands: () => [{
primary: '/beam',
arguments: ['prompt'],
description: 'Best of multiple replies',
description: 'Combine the smarts of models',
Icon: ChatBeamIcon,
}] : [],
}],
};
+1 -1
View File
@@ -3,7 +3,7 @@ import LanguageIcon from '@mui/icons-material/Language';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsBrowse: ICommandsProvider = {
id: 'ass-browse',
id: 'cmd-ass-browse',
rank: 20,
getCommands: () => [{
+7 -3
View File
@@ -1,9 +1,13 @@
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
import type { ICommandsProvider } from './ICommandsProvider';
export function textToDrawCommand(text: string): string {
return `/draw ${text}`;
}
export const CommandsDraw: ICommandsProvider = {
id: 'ass-t2i',
id: 'cmd-ass-t2i',
rank: 10,
getCommands: () => [{
@@ -11,7 +15,7 @@ export const CommandsDraw: ICommandsProvider = {
alternatives: ['/imagine', '/img'],
arguments: ['prompt'],
description: 'Assistant will draw the text',
Icon: FormatPaintIcon,
Icon: FormatPaintTwoToneIcon,
}],
};
+1 -1
View File
@@ -3,7 +3,7 @@ import PsychologyIcon from '@mui/icons-material/Psychology';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsReact: ICommandsProvider = {
id: 'ass-react',
id: 'cmd-mode-react',
rank: 15,
getCommands: () => [{
+40 -20
View File
@@ -8,20 +8,20 @@ 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 = 'cmd-ass-browse' | 'cmd-ass-t2i' | 'cmd-chat-alter' | 'cmd-help' | 'cmd-mode-beam' | 'cmd-mode-react';
type TextCommandPiece =
| { type: 'text'; value: string; }
| { type: 'cmd'; providerId: CommandsProviderId, command: string; params?: string, isError?: boolean };
| { type: 'nocmd'; value: string; }
| { type: 'cmd'; providerId: CommandsProviderId, command: string; params?: string, isErrorNoArgs?: boolean };
const ChatCommandsProviders: Record<CommandsProviderId, ICommandsProvider> = {
'ass-beam': CommandsBeam,
'ass-browse': CommandsBrowse,
'ass-react': CommandsReact,
'ass-t2i': CommandsDraw,
'chat-alter': CommandsAlter,
'cmd-ass-browse': CommandsBrowse,
'cmd-ass-t2i': CommandsDraw,
'cmd-chat-alter': CommandsAlter,
'cmd-help': CommandsHelp,
'cmd-mode-beam': CommandsBeam,
'cmd-mode-react': CommandsReact,
};
export function findAllChatCommands(): ChatCommand[] {
@@ -31,16 +31,25 @@ export function findAllChatCommands(): ChatCommand[] {
.flat();
}
export function helpPrettyChatCommands() {
return findAllChatCommands()
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
.join('\n');
}
export function extractChatCommand(input: string): TextCommandPiece[] {
const inputTrimmed = input.trim();
// quick exit: command does not start with '/'
if (!inputTrimmed.startsWith('/'))
return [{ type: 'text', value: input }];
return [{ type: 'nocmd', value: input }];
// 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 +57,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,
isErrorNoArgs: !textAfterCommand,
}];
// 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: 'nocmd',
value: textAfterCommand,
});
return pieces;
}
}
}
// No command found, return the entire input as text
return [{ type: 'text', value: input }];
return [{
type: 'nocmd',
value: input,
}];
}
@@ -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>
);
}
+122 -71
View File
@@ -1,26 +1,29 @@
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';
import type { ConversationHandler } from '~/common/chats/ConversationHandler';
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
import type { DMessageFragment, DMessageFragmentId } from '~/common/stores/chat/chat.fragments';
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 { ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcuts';
import { createDMessageTextContent, DMessageId, DMessageUserFlag, messageToggleUserFlag } from '~/common/stores/chat/chat.message';
import { getConversation, useChatStore } from '~/common/stores/chat/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';
import { useChatAutoSuggestHTMLUI, useChatShowSystemMessages } from '../store-app-chat';
/**
@@ -32,9 +35,10 @@ export function ChatMessageList(props: {
capabilityHasT2I: boolean,
chatLLMContextTokens: number | null,
fitScreen: boolean,
isMobile: boolean,
isMessageSelectionMode: boolean,
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[], chatEffectBeam: boolean) => Promise<void>,
onConversationExecuteHistory: (conversationId: DConversationId) => Promise<void>,
onTextDiagram: (diagramConfig: DiagramConfig | null) => void,
onTextImagine: (conversationId: DConversationId, selectedText: string) => Promise<void>,
onTextSpeak: (selectedText: string) => Promise<void>,
@@ -50,63 +54,101 @@ export function ChatMessageList(props: {
// external state
const { notifyBooting } = useScrollToBottom();
const { openPreferencesTab } = useOptimaLayout();
const danger_experimentalHtmlWebUi = useChatAutoSuggestHTMLUI();
const [showSystemMessages] = useChatShowSystemMessages();
const optionalTranslationWarning = useBrowserTranslationWarning();
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
const { conversationMessages, historyTokenCount } = useChatStore(useShallow(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
return {
conversationMessages: conversation ? conversation.messages : [],
historyTokenCount: conversation ? conversation.tokenCount : 0,
deleteMessage: state.deleteMessage,
editMessage: state.editMessage,
setMessages: state.setMessages,
};
}, shallow);
}));
const ephemerals = useEphemerals(props.conversationHandler);
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
// derived state
const { conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
const { conversationHandler, conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
// text actions
const handleRunExample = React.useCallback(async (text: string) => {
conversationId && await onConversationExecuteHistory(conversationId, [...conversationMessages, createDMessage('user', text)], false);
}, [conversationId, conversationMessages, onConversationExecuteHistory]);
const handleRunExample = React.useCallback(async (examplePrompt: string) => {
if (conversationId && conversationHandler) {
conversationHandler.messageAppend(createDMessageTextContent('user', examplePrompt)); // [chat] append user:persona question
await onConversationExecuteHistory(conversationId);
}
}, [conversationHandler, conversationId, onConversationExecuteHistory]);
// message menu methods proxy
const handleConversationBranch = React.useCallback((messageId: string) => {
const handleMessageAssistantFrom = React.useCallback(async (messageId: DMessageId, offset: number) => {
if (conversationId && conversationHandler) {
conversationHandler.historyTruncateTo(messageId, offset);
await onConversationExecuteHistory(conversationId);
}
}, [conversationHandler, conversationId, onConversationExecuteHistory]);
const handleMessageBeam = React.useCallback(async (messageId: DMessageId) => {
// 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: DMessageId) => {
conversationId && onConversationBranch(conversationId, messageId);
}, [conversationId, onConversationBranch]);
const handleConversationRestartFrom = React.useCallback(async (messageId: string, offset: number, chatEffectBeam: boolean) => {
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, onConversationExecuteHistory]);
const handleMessageTruncate = React.useCallback((messageId: DMessageId) => {
props.conversationHandler?.historyTruncateTo(messageId, 0);
}, [props.conversationHandler]);
const handleConversationTruncate = React.useCallback((messageId: string) => {
const messages = getConversation(conversationId)?.messages;
if (conversationId && messages) {
const truncatedHistory = messages.slice(0, messages.findIndex(m => m.id === messageId) + 1);
setMessages(conversationId, truncatedHistory);
}
}, [conversationId, setMessages]);
const handleMessageDelete = React.useCallback((messageId: DMessageId) => {
props.conversationHandler?.messagesDelete([messageId]);
}, [props.conversationHandler]);
const handleMessageDelete = React.useCallback((messageId: string) => {
conversationId && deleteMessage(conversationId, messageId);
}, [conversationId, deleteMessage]);
const handleMessageAppendFragment = React.useCallback((messageId: DMessageId, fragment: DMessageFragment) => {
props.conversationHandler?.messageFragmentAppend(messageId, fragment, false, false);
}, [props.conversationHandler]);
const handleMessageEdit = React.useCallback((messageId: string, newText: string) => {
conversationId && editMessage(conversationId, messageId, { text: newText }, true);
}, [conversationId, editMessage]);
const handleMessageDeleteFragment = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId) => {
props.conversationHandler?.messageFragmentDelete(messageId, fragmentId, false, true);
}, [props.conversationHandler]);
const handleTextDiagram = React.useCallback(async (messageId: string, text: string) => {
const handleMessageReplaceFragment = React.useCallback((messageId: DMessageId, fragmentId: DMessageFragmentId, newFragment: DMessageFragment) => {
props.conversationHandler?.messageFragmentReplace(messageId, fragmentId, newFragment, false);
}, [props.conversationHandler]);
const handleMessageToggleUserFlag = React.useCallback((messageId: DMessageId, userFlag: DMessageUserFlag) => {
props.conversationHandler?.messageEdit(messageId, (message) => ({
userFlags: messageToggleUserFlag(message, userFlag),
}), false, false);
}, [props.conversationHandler]);
const handleReplyTo = React.useCallback((_messageId: DMessageId, text: string) => {
props.conversationHandler?.getOverlayStore().getState().setReplyToText(text);
}, [props.conversationHandler]);
const handleTextDiagram = React.useCallback(async (messageId: DMessageId, text: string) => {
conversationId && onTextDiagram({ conversationId: conversationId, messageId, text });
}, [conversationId, onTextDiagram]);
@@ -139,36 +181,35 @@ export function ChatMessageList(props: {
setSelectedMessages(newSelected);
};
const handleSelectMessage = (messageId: string, selected: boolean) => {
const handleSelectMessage = (messageId: DMessageId, selected: boolean) => {
const newSelected = new Set(selectedMessages);
selected ? newSelected.add(messageId) : newSelected.delete(messageId);
setSelectedMessages(newSelected);
};
const handleSelectionDelete = () => {
if (conversationId)
for (const selectedMessage of selectedMessages)
deleteMessage(conversationId, selectedMessage);
const handleSelectionDelete = React.useCallback(() => {
props.conversationHandler?.messagesDelete(Array.from(selectedMessages));
setSelectedMessages(new Set());
};
}, [props.conversationHandler, selectedMessages]);
useGlobalShortcut(props.isMessageSelectionMode && ShortcutKeyName.Esc, false, false, false, () => {
useGlobalShortcuts([[props.isMessageSelectionMode && ShortcutKeyName.Esc, false, false, false, () => {
props.setIsMessageSelectionMode(false);
});
}]]);
// text-diff functionality: only diff the last message and when it's complete (not typing), and they're similar in size
// text-diff functionality: only diff the last complete message, and they're similar in size
const { diffTargetMessage, diffPrevText } = React.useMemo(() => {
const [msgB, msgA] = conversationMessages.filter(m => m.role === 'assistant').reverse();
if (msgB?.text && msgA?.text && !msgB?.typing) {
const textA = msgA.text, textB = msgB.text;
const lenA = textA.length, lenB = textB.length;
if (lenA > 80 && lenB > 80 && lenA > lenB / 3 && lenB > lenA / 3)
return { diffTargetMessage: msgB, diffPrevText: textA };
}
return { diffTargetMessage: undefined, diffPrevText: undefined };
}, [conversationMessages]);
// const { diffTargetMessage, diffPrevText } = React.useMemo(() => {
// const [msgB, msgA] = conversationMessages.filter(m => m.role === 'assistant').reverse();
// const textB = msgB ? singleTextOrThrow(msgB) : undefined;
// const textA = msgA ? singleTextOrThrow(msgA) : undefined;
// if (textB && textA && !msgB?.pendingIncomplete) {
// const lenA = textA.length, lenB = textB.length;
// if (lenA > 80 && lenB > 80 && lenA > lenB / 3 && lenB > lenA / 3)
// return { diffTargetMessage: msgB, diffPrevText: textA };
// }
// return { diffTargetMessage: undefined, diffPrevText: undefined };
// }, [conversationMessages]);
// scroll to the very bottom of a new chat
@@ -194,13 +235,16 @@ 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',
<List role='chat-messages-list' sx={{
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}
@@ -217,8 +261,8 @@ export function ChatMessageList(props: {
{filteredMessages.map((message, idx, { length: count }) => {
// Optimization: if the component is going to change (e.g. the message is typing), we don't want to memoize it to not throw garbage in memory
const ChatMessageMemoOrNot = message.typing ? ChatMessage : ChatMessageMemo;
// Optimization: only memo complete components, or we'd be memoizing garbage
const ChatMessageMemoOrNot = !message.pendingIncomplete ? ChatMessageMemo : ChatMessage;
return props.isMessageSelectionMode ? (
@@ -234,19 +278,26 @@ export function ChatMessageList(props: {
<ChatMessageMemoOrNot
key={'msg-' + message.id}
message={message}
diffPreviousText={message === diffTargetMessage ? diffPrevText : undefined}
// diffPreviousText={message === diffTargetMessage ? diffPrevText : undefined}
fitScreen={props.fitScreen}
isMobile={props.isMobile}
isBottom={idx === count - 1}
isImagining={isImagining}
isSpeaking={isSpeaking}
onConversationBranch={handleConversationBranch}
onConversationRestartFrom={handleConversationRestartFrom}
onConversationTruncate={handleConversationTruncate}
showUnsafeHtml={danger_experimentalHtmlWebUi}
onMessageAssistantFrom={handleMessageAssistantFrom}
onMessageBeam={handleMessageBeam}
onMessageBranch={handleMessageBranch}
onMessageDelete={handleMessageDelete}
onMessageEdit={handleMessageEdit}
onMessageFragmentAppend={handleMessageAppendFragment}
onMessageFragmentDelete={handleMessageDeleteFragment}
onMessageFragmentReplace={handleMessageReplaceFragment}
onMessageToggleUserFlag={handleMessageToggleUserFlag}
onMessageTruncate={handleMessageTruncate}
onReplyTo={handleReplyTo}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImagine}
onTextSpeak={handleTextSpeak}
onTextImagine={capabilityHasT2I ? handleTextImagine : undefined}
onTextSpeak={isSpeakable ? handleTextSpeak : undefined}
/>
);
+4 -4
View File
@@ -4,9 +4,9 @@ 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 { DConversationId } from '~/common/state/store-chats';
import { DEphemeral } from '~/common/chats/EphemeralsStore';
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
import type { DEphemeral } from '~/common/chats/EphemeralsStore';
import { ConversationsManager } from '~/common/chats/ConversationsManager';
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 }}>*/}
@@ -1,94 +0,0 @@
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 { 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;
shortcut?: string;
requiresTTI?: boolean;
}
const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
'generate-text': {
label: 'Chat',
description: 'Persona replies',
},
'append-user': {
label: 'Write',
description: 'Appends a message',
shortcut: 'Alt + Enter',
},
'generate-image': {
label: 'Draw',
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',
},
};
function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
if (shortcut === 'ENTER')
return enterIsNewLine ? 'Shift + Enter' : 'Enter';
return shortcut;
}
export function ChatModeMenu(props: {
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 (
<CloseableMenu
placement='top-end'
open anchorEl={props.anchorEl} onClose={props.onClose}
sx={{ minWidth: 320 }}
>
{/*<MenuItem color='neutral' selected>*/}
{/* Conversation Mode*/}
{/*</MenuItem>*/}
{/**/}
{/*<ListDivider />*/}
{/* ChatMode items */}
{Object.entries(ChatModeItems)
.filter(([key, data]) => key !== 'generate-text-beam' || labsChatBeam)
.map(([key, data]) =>
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
<Radio checked={key === props.chatModeId} />
<Box sx={{ flexGrow: 1 }}>
<Typography>{data.label}</Typography>
<Typography level='body-xs'>{data.description}{(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)} />
)}
</Box>
</MenuItem>)}
</CloseableMenu>
);
}
+350 -242
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,69 +9,73 @@ 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';
import TelegramIcon from '@mui/icons-material/Telegram';
import type { ChatModeId } from '../../AppChat';
import { useChatMicTimeoutMsValue } from '../../store-app-chat';
import type { DLLM } from '~/modules/llms/store-llms';
import type { LLMOptionsOpenAI } from '~/modules/llms/vendors/openai/openai.vendor';
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { ConversationsManager } from '~/common/chats/ConversationsManager';
import { DMessageMetadata, messageFragmentsReduceText } from '~/common/stores/chat/chat.message';
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { countModelTokens } from '~/common/util/token-counter';
import { animationEnterBelow } from '~/common/util/animUtils';
import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation';
import { copyToClipboard, supportsClipboardRead } from '~/common/util/clipboardUtils';
import { createTextContentFragment, DMessageAttachmentFragment, DMessageContentFragment, duplicateDMessageFragments, isContentFragment } from '~/common/stores/chat/chat.fragments';
import { estimateTextTokens, glueForMessageTokens, marshallWrapDocFragments } from '~/common/stores/chat/chat.tokens';
import { getConversation, isValidConversation, useChatStore } from '~/common/stores/chat/store-chats';
import { isMacUser } from '~/common/util/pwaUtils';
import { launchAppCall } from '~/common/app.routes';
import { lineHeightTextareaMd } from '~/common/app.theme';
import { playSoundUrl } from '~/common/util/audioUtils';
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
import { useAppStateStore } from '~/common/state/store-appstate';
import { useChatOverlayStore } from '~/common/chats/store-chat-overlay';
import { useDebouncer } from '~/common/components/useDebouncer';
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
import { useGlobalShortcuts } from '~/common/components/useGlobalShortcuts';
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 { useAttachments } from './attachments/useAttachments';
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import { LLMAttachmentDraftsAction, LLMAttachmentsList } from './llmattachments/LLMAttachmentsList';
import { useAttachmentDrafts } from '~/common/attachment-drafts/useAttachmentDrafts';
import { useLLMAttachmentDrafts } from './llmattachments/useLLMAttachmentDrafts';
import type { ChatExecuteMode } from '../../execute-mode/execute-mode.types';
import { chatExecuteModeCanAttach, useChatExecuteMode } from '../../execute-mode/useChatExecuteMode';
import type { ComposerOutputMultiPart } from './composer.types';
import { ButtonAttachCameraMemo, useCameraCaptureModal } from './buttons/ButtonAttachCamera';
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 +84,7 @@ const dropperCardSx: SxProps = {
border: '2px dashed',
borderRadius: 'xs',
boxShadow: 'none',
zIndex: 10,
zIndex: zIndexComposerOverlayDrop,
} as const;
const dropppedCardDraggingSx: SxProps = {
@@ -97,11 +100,11 @@ export function Composer(props: {
isMobile?: boolean;
chatLLM: DLLM | null;
composerTextAreaRef: React.RefObject<HTMLTextAreaElement>;
conversationId: DConversationId | null;
targetConversationId: DConversationId | null;
capabilityHasT2I: boolean;
isMulticast: boolean | null;
isDeveloperMode: boolean;
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
onAction: (conversationId: DConversationId, chatExecuteMode: ChatExecuteMode, fragments: (DMessageContentFragment | DMessageAttachmentFragment)[], metadata?: DMessageMetadata) => boolean;
onTextImagine: (conversationId: DConversationId, text: string) => void;
setIsMulticast: (on: boolean) => void;
sx?: SxProps;
@@ -112,56 +115,85 @@ export function Composer(props: {
const [micContinuation, setMicContinuation] = React.useState(false);
const [speechInterimResult, setSpeechInterimResult] = React.useState<SpeechResult | null>(null);
const [isDragging, setIsDragging] = React.useState(false);
const [chatModeMenuAnchor, setChatModeMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
const {
chatExecuteMode,
chatExecuteModeSendColor, chatExecuteModeSendLabel,
chatExecuteMenuComponent, chatExecuteMenuShown, showChatExecuteMenu,
} = useChatExecuteMode(props.capabilityHasT2I, !!props.isMobile);
// 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 conversation = state.conversations.find(_c => _c.id === props.conversationId);
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, abortConversationTemp } = useChatStore(useShallow(state => {
const conversation = state.conversations.find(_c => _c.id === props.targetConversationId);
return {
assistantAbortible: conversation ? !!conversation.abortController : false,
systemPurposeId: conversation?.systemPurposeId ?? null,
tokenCount: conversation ? conversation.tokenCount : 0,
stopTyping: state.stopTyping,
abortConversationTemp: state.abortConversationTemp,
};
}, shallow);
const { inComposer: browsingInComposer } = useBrowseCapability();
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
useAttachments(browsingInComposer && !composeText.startsWith('/'));
}));
// external overlay state (extra conversationId-dependent state)
const conversationOverlayStore = props.targetConversationId
? ConversationsManager.getHandler(props.targetConversationId)?.getOverlayStore() || null
: null;
// composer-overlay: for the reply-to state, comes from the conversation overlay
const { replyToGenerateText } = useChatOverlayStore(conversationOverlayStore, useShallow(store => ({
replyToGenerateText: (chatExecuteMode === 'generate-content' || chatExecuteMode === 'generate-text-v1') ? store.replyToText?.trim() || null : null,
})));
// don't load URLs if the user is typing a command or there's no capability
const enableLoadURLsInComposer = useBrowseCapability().inComposer && !composeText.startsWith('/');
// attachments-overlay: comes from the attachments slice of the conversation overlay
const {
/* items */ attachmentDrafts,
/* append */ attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoFragments, attachAppendFile,
/* take */ attachmentsRemoveAll, attachmentsTakeAllFragments, attachmentsTakeFragmentsByType,
} = useAttachmentDrafts(conversationOverlayStore, enableLoadURLsInComposer);
// attachments derived state
const llmAttachmentDrafts = useLLMAttachmentDrafts(attachmentDrafts, props.chatLLM);
// derived state
const { composerTextAreaRef, targetConversationId, onAction, onTextImagine } = props;
const isMobile = !!props.isMobile;
const isDesktop = !props.isMobile;
const chatLLMId = props.chatLLM?.id || null;
const noConversation = !targetConversationId;
const noLLM = !props.chatLLM;
const showLLMAttachments = chatExecuteModeCanAttach(chatExecuteMode);
// attachments derived state
const llmAttachments = useLLMAttachments(_attachments, chatLLMId);
// tokens derived state
const tokensComposerText = React.useMemo(() => {
if (!debouncedText || !chatLLMId)
return 0;
return countModelTokens(debouncedText, chatLLMId, 'composer text') ?? 0;
}, [chatLLMId, debouncedText]);
let tokensComposer = tokensComposerText + llmAttachments.tokenCountApprox;
if (tokensComposer > 0)
tokensComposer += 4; // every user message has this many surrounding tokens (note: shall depend on llm..)
const tokensComposerTextDebounced = React.useMemo(() => {
return (debouncedText && props.chatLLM)
? estimateTextTokens(debouncedText, props.chatLLM, 'composer text')
: 0;
}, [props.chatLLM, debouncedText]);
let tokensComposer = tokensComposerTextDebounced + (llmAttachmentDrafts.llmTokenCountApprox || 0);
if (props.chatLLM && tokensComposer > 0)
tokensComposer += glueForMessageTokens(props.chatLLM);
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,77 +205,96 @@ export function Composer(props: {
}, [setComposeText, setStartupText, startupText]);
// Overlay actions
const handleReplyToClear = React.useCallback(() => {
conversationOverlayStore?.getState().setReplyToText(null);
}, [conversationOverlayStore]);
React.useEffect(() => {
if (replyToGenerateText)
setTimeout(() => composerTextAreaRef.current?.focus(), 1 /* prevent focus theft */);
}, [composerTextAreaRef, replyToGenerateText]);
// Primary button
const { conversationId, onAction } = props;
const handleClear = React.useCallback(() => {
setComposeText('');
attachmentsRemoveAll();
handleReplyToClear();
}, [attachmentsRemoveAll, handleReplyToClear, setComposeText]);
const handleSendAction = React.useCallback((_chatModeId: ChatModeId, composerText: string): boolean => {
if (!conversationId)
const handleSendAction = React.useCallback(async (_chatExecuteMode: ChatExecuteMode, composerText: string): Promise<boolean> => {
if (!isValidConversation(targetConversationId)) return false;
// validate some chat mode inputs
const isDraw = _chatExecuteMode === 'generate-image';
const isBlank = !composerText.trim();
if (isDraw && isBlank)
return false;
// get attachments
const multiPartMessage = llmAttachments.getAttachmentsOutputs(composerText || null);
if (!multiPartMessage.length)
return false;
// prepare the fragments: content (if any) and attachments (if allowed, and any)
const fragments: (DMessageContentFragment | DMessageAttachmentFragment)[] = [];
if (composerText)
fragments.push(createTextContentFragment(composerText));
// send the message
const enqueued = onAction(_chatModeId, conversationId, multiPartMessage);
if (enqueued) {
clearAttachments();
setComposeText('');
const canAttach = chatExecuteModeCanAttach(_chatExecuteMode);
if (canAttach) {
const attachmentFragments = await attachmentsTakeAllFragments('global', 'app-chat');
fragments.push(...attachmentFragments);
}
return enqueued;
}, [clearAttachments, conversationId, llmAttachments, onAction, setComposeText]);
if (!fragments.length) {
// addSnackbar({ key: 'chat-composer-empty', message: 'Nothing to send', type: 'info' });
return false;
}
const handleSendClicked = React.useCallback(() => {
handleSendAction(chatModeId, composeText);
}, [chatModeId, composeText, handleSendAction]);
// send the message - NOTE: if successful, the ownership of the fragments is transferred to the receiver, so we just clear them
const metadata = replyToGenerateText ? { inReplyToText: replyToGenerateText } : undefined;
const enqueued = onAction(targetConversationId, _chatExecuteMode, fragments, metadata);
if (enqueued)
handleClear();
return enqueued;
}, [attachmentsTakeAllFragments, handleClear, onAction, replyToGenerateText, targetConversationId]);
const handleSendClicked = React.useCallback(async () => {
await handleSendAction(chatExecuteMode, composeText); // 'chat/write/...' button
}, [chatExecuteMode, composeText, handleSendAction]);
const handleSendTextBeamClicked = React.useCallback(async () => {
await handleSendAction('beam-content', composeText); // 'beam' button
}, [composeText, handleSendAction]);
const handleStopClicked = React.useCallback(() => {
!!props.conversationId && stopTyping(props.conversationId);
}, [props.conversationId, stopTyping]);
targetConversationId && abortConversationTemp(targetConversationId);
}, [abortConversationTemp, targetConversationId]);
// Secondary buttons
const handleCallClicked = React.useCallback(() => {
props.conversationId && systemPurposeId && launchAppCall(props.conversationId, systemPurposeId);
}, [props.conversationId, systemPurposeId]);
targetConversationId && systemPurposeId && launchAppCall(targetConversationId, systemPurposeId);
}, [systemPurposeId, targetConversationId]);
const handleDrawOptionsClicked = React.useCallback(() => {
openPreferencesTab(PreferencesTab.Draw);
}, [openPreferencesTab]);
const handleTextImagineClicked = React.useCallback(() => {
if (!composeText || !props.conversationId)
return;
props.onTextImagine(props.conversationId, composeText);
if (!composeText || !targetConversationId) return;
onTextImagine(targetConversationId, composeText);
setComposeText('');
}, [composeText, props, setComposeText]);
// Mode menu
const handleModeSelectorHide = React.useCallback(() => {
setChatModeMenuAnchor(null);
}, []);
const handleModeSelectorShow = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
setChatModeMenuAnchor(anchor => anchor ? null : event.currentTarget);
}, []);
const handleModeChange = React.useCallback((_chatModeId: ChatModeId) => {
handleModeSelectorHide();
setChatModeId(_chatModeId);
}, [handleModeSelectorHide]);
}, [composeText, onTextImagine, setComposeText, targetConversationId]);
// Actiles
const onActileCommandSelect = React.useCallback((item: ActileItem) => {
if (props.composerTextAreaRef.current) {
const textArea = props.composerTextAreaRef.current;
const onActileCommandPaste = React.useCallback((item: ActileItem) => {
if (composerTextAreaRef.current) {
const textArea = composerTextAreaRef.current;
const currentText = textArea.value;
const cursorPos = textArea.selectionStart;
@@ -260,23 +311,39 @@ export function Composer(props: {
const newCursorPos = commandStart + item.label.length + 1;
textArea.setSelectionRange(newCursorPos, newCursorPos);
}
}, [props.composerTextAreaRef, setComposeText]);
}, [composerTextAreaRef, setComposeText]);
const actileProviders: ActileProvider[] = React.useMemo(() => {
return [providerCommands(onActileCommandSelect)];
}, [onActileCommandSelect]);
const onActileEmbedMessage = React.useCallback(async ({ conversationId, messageId }: StarredMessageItem) => {
// get the message
const conversation = getConversation(conversationId);
const messageToEmbed = conversation?.messages.find(m => m.id === messageId);
if (conversation && messageToEmbed) {
const fragmentsCopy = duplicateDMessageFragments(messageToEmbed.fragments)
.filter(isContentFragment);
if (fragmentsCopy.length) {
const chatTitle = conversationTitle(conversation);
const messageText = messageFragmentsReduceText(fragmentsCopy);
const label = `${chatTitle} > ${messageText.slice(0, 10)}...`;
await attachAppendEgoFragments(fragmentsCopy, label, chatTitle, conversationId, messageId);
}
}
}, [attachAppendEgoFragments]);
const { actileComponent, actileInterceptKeydown, actileInterceptTextChange } = useActileManager(actileProviders, props.composerTextAreaRef);
const actileProviders = React.useMemo(() => {
return [providerCommands(onActileCommandPaste), providerStarredMessage(onActileEmbedMessage)];
}, [onActileCommandPaste, onActileEmbedMessage]);
const { actileComponent, actileInterceptKeydown, actileInterceptTextChange } = useActileManager(actileProviders, composerTextAreaRef);
// Text typing
// Type...
const handleTextareaTextChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setComposeText(e.target.value);
isMobile && actileInterceptTextChange(e.target.value);
}, [actileInterceptTextChange, isMobile, setComposeText]);
const handleTextareaKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const handleTextareaKeyDown = React.useCallback(async (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// disable keyboard handling if the actile is visible
if (actileInterceptKeydown(e))
return;
@@ -284,9 +351,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 (await handleSendAction('append-user', composeText)) // 'alt+enter' -> write
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 (await handleSendAction('beam-content', composeText)) // 'ctrl+enter' -> beam
touchCtrlEnter();
return e.preventDefault();
}
@@ -295,12 +370,12 @@ export function Composer(props: {
touchShiftEnter();
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
if (!assistantAbortible)
handleSendAction(chatModeId, composeText);
await handleSendAction(chatExecuteMode, composeText); // enter -> send
return e.preventDefault();
}
}
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchShiftEnter]);
}, [actileInterceptKeydown, assistantAbortible, chatExecuteMode, composeText, enterIsNewline, handleSendAction, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
// Focus mode
@@ -326,26 +401,26 @@ export function Composer(props: {
nextText = nextText ? nextText + ' ' + transcript : transcript;
// auto-send (mic continuation mode) if requested
const autoSend = micContinuation && nextText.length >= 1 && !!props.conversationId; //&& assistantAbortible;
const autoSend = micContinuation && nextText.length >= 1 && !noConversation; //&& assistantAbortible;
const notUserStop = result.doneReason !== 'manual';
if (autoSend) {
if (notUserStop)
playSoundUrl('/sounds/mic-off-mid.mp3');
handleSendAction(chatModeId, nextText);
void AudioPlayer.playUrl('/sounds/mic-off-mid.mp3');
void handleSendAction(chatExecuteMode, nextText); // fire/forget
} else {
if (!micContinuation && notUserStop)
playSoundUrl('/sounds/mic-off-mid.mp3');
void AudioPlayer.playUrl('/sounds/mic-off-mid.mp3');
if (nextText) {
props.composerTextAreaRef.current?.focus();
composerTextAreaRef.current?.focus();
setComposeText(nextText);
}
}
}, [chatModeId, composeText, handleSendAction, micContinuation, props.composerTextAreaRef, props.conversationId, setComposeText]);
}, [chatExecuteMode, composeText, composerTextAreaRef, handleSendAction, micContinuation, noConversation, setComposeText]);
const { isSpeechEnabled, isSpeechError, isRecordingAudio, isRecordingSpeech, toggleRecording } =
useSpeechRecognition(onSpeechResultCallback, chatMicTimeoutMs || 2000);
useGlobalShortcut('m', true, false, false, toggleRecording);
useGlobalShortcuts([['m', true, false, false, toggleRecording]]);
const micIsRunning = !!speechInterimResult;
const micContinuationTrigger = micContinuation && !micIsRunning && !assistantAbortible && !isSpeechError;
@@ -369,7 +444,7 @@ export function Composer(props: {
}, [toggleRecording, micContinuationTrigger]);
// Attachments
// Attachment Up
const handleAttachCtrlV = React.useCallback((event: React.ClipboardEvent) => {
if (attachAppendDataTransfer(event.clipboardData, 'paste', false) === 'as_files')
@@ -380,12 +455,12 @@ export function Composer(props: {
void attachAppendFile('camera', file);
}, [attachAppendFile]);
const { openCamera, cameraCaptureComponent } = useCameraCaptureModal(handleAttachCameraImage);
const handleAttachScreenCapture = React.useCallback((file: File) => {
void attachAppendFile('screencapture', file);
}, [attachAppendFile]);
const { openCamera, cameraCaptureComponent } = useCameraCaptureModal(handleAttachCameraImage);
const handleAttachFilePicker = React.useCallback(async () => {
try {
const selectedFiles: FileWithHandle[] = await fileOpen({ multiple: true });
@@ -397,25 +472,24 @@ export function Composer(props: {
}
}, [attachAppendFile]);
useGlobalShortcut(supportsClipboardRead ? 'v' : false, true, true, false, attachAppendClipboardItems);
useGlobalShortcuts([[supportsClipboardRead ? 'v' : false, true, true, false, attachAppendClipboardItems]]);
const handleAttachmentInlineText = React.useCallback((attachmentId: AttachmentId) => {
setComposeText(currentText => {
const attachmentOutputs = llmAttachments.getAttachmentOutputs(currentText, attachmentId);
const inlinedText = getTextBlockText(attachmentOutputs) || '';
removeAttachment(attachmentId);
return inlinedText;
});
}, [llmAttachments, removeAttachment, setComposeText]);
const handleAttachmentsInlineText = React.useCallback(() => {
setComposeText(currentText => {
const attachmentsOutputs = llmAttachments.getAttachmentsOutputs(currentText);
const inlinedText = getTextBlockText(attachmentsOutputs) || '';
clearAttachments();
return inlinedText;
});
}, [clearAttachments, llmAttachments, setComposeText]);
// Attachments Down
const handleAttachmentDraftsAction = React.useCallback((attachmentDraftIdOrAll: AttachmentDraftId | null, action: LLMAttachmentDraftsAction) => {
switch (action) {
case 'copy-text':
const copyFragments = attachmentsTakeFragmentsByType('doc', attachmentDraftIdOrAll, false);
const copyString = marshallWrapDocFragments(null, copyFragments, false, '\n\n---\n\n');
copyToClipboard(copyString, attachmentDraftIdOrAll ? 'Attachment Text' : 'Attachments Text');
break;
case 'inline-text':
const inlineFragments = attachmentsTakeFragmentsByType('doc', attachmentDraftIdOrAll, true);
setComposeText(currentText => marshallWrapDocFragments(currentText, inlineFragments, 'markdown-code', '\n\n'));
break;
}
}, [attachmentsTakeFragmentsByType, setComposeText]);
// Drag & Drop
@@ -444,7 +518,7 @@ export function Composer(props: {
const handleOverlayDragOver = React.useCallback((e: React.DragEvent) => {
eatDragEvent(e);
// this makes sure we don't "transfer" (or move) the attachment, but we tell the sender we'll copy it
// this makes sure we don't "transfer" (or move) the item, but we tell the sender we'll copy it
e.dataTransfer.dropEffect = 'copy';
}, [eatDragEvent]);
@@ -463,89 +537,94 @@ export function Composer(props: {
}, [attachAppendDataTransfer, eatDragEvent, setComposeText]);
const isText = chatModeId === 'generate-text';
const isTextBeam = chatModeId === 'generate-text-beam';
const isAppend = chatModeId === 'append-user';
const isReAct = chatModeId === 'generate-react';
const isDraw = chatModeId === 'generate-image';
const isText = chatExecuteMode === 'generate-content' || chatExecuteMode === 'generate-text-v1';
const isTextBeam = chatExecuteMode === 'beam-content';
const isAppend = chatExecuteMode === 'append-user';
const isReAct = chatExecuteMode === 'react-content';
const isDraw = chatExecuteMode === 'generate-image';
const showCall = isText || isAppend;
const showChatReplyTo = !!replyToGenerateText;
const showChatExtras = isText && !showChatReplyTo;
const buttonColor: ColorPaletteProp =
assistantAbortible ? 'warning'
: isReAct ? 'success'
: isTextBeam ? 'success'
: isDraw ? 'warning'
: 'primary';
const sendButtonVariant: VariantProp = (isAppend || (isMobile && isTextBeam)) ? 'outlined' : 'solid';
const buttonText =
isAppend ? 'Write'
: isReAct ? 'ReAct'
: isTextBeam ? 'Best-Of'
: isDraw ? 'Draw'
: 'Chat';
const sendButtonColor: ColorPaletteProp = assistantAbortible ? 'warning' : chatExecuteModeSendColor;
const buttonIcon =
const sendButtonLabel = chatExecuteModeSendLabel;
const sendButtonIcon =
micContinuation ? <AutoModeIcon />
: 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}>
<Grid container spacing={{ xs: 1, md: 2 }}>
{/* [Mobile: top, Desktop: left] */}
<Grid xs={12} md={9}><Box sx={{ display: 'flex', gap: { xs: 1, md: 2 }, alignItems: 'flex-start' }}>
{/* Start buttons column */}
<Box sx={{
flexGrow: 0,
display: 'grid', gap: 1,
}}>
{isMobile ? <>
{/* [Mobile, Col1] Mic, Insert Multi-modal content, and Broadcast buttons */}
{isMobile && (
<Box sx={{ flexGrow: 0, display: 'grid', gap: 1 }}>
{/* [mobile] Mic button */}
{isSpeechEnabled && <ButtonMicMemo variant={micVariant} color={micColor} onClick={handleToggleMic} />}
{/* [mobile] [+] button */}
<Dropdown>
<MenuButton slots={{ root: IconButton }}>
<AddCircleOutlineIcon />
</MenuButton>
<Menu>
{/* Responsive Camera OCR button */}
<MenuItem>
<ButtonAttachCameraMemo onOpenCamera={openCamera} />
</MenuItem>
{showLLMAttachments && (
<Dropdown>
<MenuButton slots={{ root: IconButton }}>
<AddCircleOutlineIcon />
</MenuButton>
<Menu>
{/* Responsive Camera OCR button */}
<MenuItem>
<ButtonAttachCameraMemo onOpenCamera={openCamera} />
</MenuItem>
{/* Responsive Open Files button */}
<MenuItem>
<ButtonAttachFileMemo onAttachFilePicker={handleAttachFilePicker} />
</MenuItem>
{/* Responsive Open Files button */}
<MenuItem>
<ButtonAttachFileMemo onAttachFilePicker={handleAttachFilePicker} />
</MenuItem>
{/* Responsive Paste button */}
{supportsClipboardRead && <MenuItem>
<ButtonAttachClipboardMemo onClick={attachAppendClipboardItems} />
</MenuItem>}
</Menu>
</Dropdown>
{/* Responsive Paste button */}
{supportsClipboardRead && <MenuItem>
<ButtonAttachClipboardMemo onClick={attachAppendClipboardItems} />
</MenuItem>}
</Menu>
</Dropdown>
)}
{/* [Mobile] MultiChat button */}
{props.isMulticast !== null && <ButtonMultiChatMemo isMobile multiChat={props.isMulticast} onSetMultiChat={props.setIsMulticast} />}
</> : <>
</Box>
)}
{/* [Desktop, Col1] Insert Multi-modal content buttons */}
{isDesktop && showLLMAttachments && (
<Box sx={{ flexGrow: 0, display: 'grid', gap: 1 }}>
{/*<FormHelperText sx={{ mx: 'auto' }}>*/}
{/* Attach*/}
@@ -563,28 +642,30 @@ export function Composer(props: {
{/* Responsive Camera OCR button */}
{labsCameraDesktop && <ButtonAttachCameraMemo onOpenCamera={openCamera} />}
</>}
</Box>
</Box>)}
{/* [ Textarea + Overlays + Mic | Attachments ] */}
{/* Top: Textarea & Mic & Overlays, Bottom, Attachment Drafts */}
<Box sx={{
flexGrow: 1,
// layout
display: 'flex', flexDirection: 'column', gap: 1,
minWidth: 200, // flex: enable X-scrolling (resetting any possible minWidth due to the attachments)
display: 'flex',
flexDirection: 'column',
gap: 1,
minWidth: 200, // flex: enable X-scrolling (resetting any possible minWidth due to the attachment drafts)
}}>
{/* Textarea + Mic buttons + Mic/Drag overlay */}
<Box sx={{ position: 'relative' }}>
{/* Text Edit + Mic buttons + MicOverlay & DragOverlay */}
<Box sx={{ position: 'relative' /* for overlays */ }}>
{/* Edit box with inner Token Progress bar */}
<Box sx={{ position: 'relative' }}>
<Box sx={{ position: 'relative' /* for TokenBadge & TokenProgress */ }}>
<Textarea
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 +676,7 @@ export function Composer(props: {
onPasteCapture={handleAttachCtrlV}
// onFocusCapture={handleFocusModeOn}
// onBlurCapture={handleFocusModeOff}
endDecorator={showChatReplyTo && <ReplyToBubble replyToText={replyToGenerateText} onClear={handleReplyToClear} className='reply-to-bubble' />}
slotProps={{
textarea: {
enterKeyHint: enterIsNewline ? 'enter' : 'send',
@@ -602,21 +684,21 @@ export function Composer(props: {
...(isSpeechEnabled && { pr: { md: 5 } }),
// mb: 0.5, // no need; the outer container already has enough p (for TokenProgressbar)
},
ref: props.composerTextAreaRef,
ref: composerTextAreaRef,
},
}}
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} enableHover={!isMobile} showExcess absoluteBottomRight />
)}
</Box>
@@ -625,7 +707,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 +726,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>
)}
@@ -681,27 +775,30 @@ export function Composer(props: {
</Box>
{/* Render any Attachments & menu items */}
<Attachments
llmAttachments={llmAttachments}
onAttachmentInlineText={handleAttachmentInlineText}
onAttachmentsClear={clearAttachments}
onAttachmentsInlineText={handleAttachmentsInlineText}
/>
{!!conversationOverlayStore && showLLMAttachments && (
<LLMAttachmentsList
attachmentDraftsStoreApi={conversationOverlayStore}
llmAttachmentDrafts={llmAttachmentDrafts}
onAttachmentDraftsAction={handleAttachmentDraftsAction}
/>
)}
</Box>
</Box></Grid>
{/* [Mobile: bottom, Desktop: right] */}
<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
? <ButtonCallMemo isMobile disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />
{isMobile && (showChatExtras
? <ButtonCallMemo isMobile disabled={noConversation || noLLM} onClick={handleCallClicked} />
: isDraw
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
: <IconButton disabled sx={{ mr: { xs: 1, md: 2 } }} />
@@ -709,38 +806,46 @@ export function Composer(props: {
{/* Responsive Send/Stop buttons */}
<ButtonGroup
variant={isAppend ? 'outlined' : 'solid'}
color={buttonColor}
variant={sendButtonVariant}
color={sendButtonColor}
sx={{
flexGrow: 1,
boxShadow: isMobile ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
backgroundColor: (isMobile && sendButtonVariant === 'outlined') ? 'background.popup' : undefined,
boxShadow: (isMobile && sendButtonVariant !== 'outlined') ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${sendButtonColor}-mainChannel) / 20%)`,
}}
>
{!assistantAbortible ? (
<Button
key='composer-act'
fullWidth disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
fullWidth disabled={noConversation || noLLM || !llmAttachmentDrafts.canAttachAllFragments}
onClick={handleSendClicked}
endDecorator={buttonIcon}
endDecorator={sendButtonIcon}
sx={{ '--Button-gap': '1rem' }}
>
{micContinuation && 'Voice '}{buttonText}
{micContinuation && 'Voice '}{sendButtonLabel}
</Button>
) : (
<Button
key='composer-stop'
fullWidth variant='soft' disabled={!props.conversationId}
fullWidth variant='soft' disabled={noConversation}
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={noConversation || noLLM} onClick={handleSendTextBeamClicked}>*/}
{/* <ChatBeamIcon />*/}
{/* </IconButton>*/}
{/*</Tooltip>}*/}
{/* [Draw] Imagine */}
{isDraw && !!composeText && <Tooltip title='Imagine a drawing prompt'>
<IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleTextImagineClicked}>
<IconButton variant='outlined' disabled={noConversation || noLLM} onClick={handleTextImagineClicked}>
<AutoAwesomeIcon />
</IconButton>
</Tooltip>}
@@ -748,23 +853,32 @@ export function Composer(props: {
{/* Mode expander */}
<IconButton
variant={assistantAbortible ? 'soft' : isDraw ? undefined : undefined}
disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor}
onClick={handleModeSelectorShow}
disabled={noConversation || noLLM || chatExecuteMenuShown}
onClick={showChatExecuteMenu}
>
<ExpandLessIcon />
</IconButton>
</ButtonGroup>
{/* [desktop] secondary-top buttons */}
{isDesktop && showChatExtras && !assistantAbortible && (
<ButtonBeamMemo
disabled={noConversation || noLLM || !llmAttachmentDrafts.canAttachAllFragments}
hasContent={!!composeText}
onClick={handleSendTextBeamClicked}
/>
)}
</Box>
{/* [desktop] Multicast switch (under the Chat button) */}
{isDesktop && props.isMulticast !== null && <ButtonMultiChatMemo multiChat={props.isMulticast} onSetMultiChat={props.setIsMulticast} />}
{/* [desktop] secondary buttons (aligned to bottom for now, and mutually exclusive) */}
{/* [desktop] secondary bottom-buttons (aligned to bottom for now, and mutually exclusive) */}
{isDesktop && <Box sx={{ mt: 'auto', display: 'grid', gap: 1 }}>
{/* [desktop] Call secondary button */}
{showCall && <ButtonCallMemo disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
{showChatExtras && <ButtonCallMemo disabled={noConversation || noLLM} onClick={handleCallClicked} />}
{/* [desktop] Draw Options secondary button */}
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
@@ -776,19 +890,13 @@ export function Composer(props: {
</Grid>
{/* Mode selector */}
{!!chatModeMenuAnchor && (
<ChatModeMenu
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
capabilityHasTTI={props.capabilityHasT2I}
/>
)}
{/* Execution Mode Menu */}
{chatExecuteMenuComponent}
{/* Camera */}
{/* Camera (when open) */}
{cameraCaptureComponent}
{/* Actile */}
{/* Actile (when open) */}
{actileComponent}
</Box>
+125 -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,80 @@ 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,
enableHover?: boolean,
showCost?: boolean
showExcess?: boolean,
absoluteBottomRight?: boolean,
inline?: boolean,
}) {
const { message, color, remainingTokens } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax);
// state
const [isHovering, setIsHovering] = React.useState(false);
// 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;
const { message, color, remainingTokens, costMax, costMin } =
tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut);
// handlers
const handleHoverEnter = React.useCallback(() => setIsHovering(true), []);
const handleHoverLeave = React.useCallback(() => setIsHovering(false), []);
let badgeValue: string;
const showAltCosts = !!props.showCost && !!costMax && costMin !== undefined;
if (showAltCosts) {
badgeValue = (!props.enableHover || isHovering)
? '< ' + formatCost(costMax)
: '> ' + formatCost(costMin);
} 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}
onMouseEnter={props.enableHover ? handleHoverEnter : undefined}
onMouseLeave={props.enableHover ? handleHoverLeave : undefined}
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,48 @@
import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation';
import { messageFragmentsReduceText, messageHasUserFlag } from '~/common/stores/chat/chat.message';
import { useChatStore } from '~/common/stores/chat/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) + ' - ' + messageFragmentsReduceText(message.fragments).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,
@@ -1,186 +0,0 @@
import * as React from 'react';
import { Box, ListDivider, ListItemDecorator, MenuItem, Radio, Typography } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import type { LLMAttachment } from './useLLMAttachments';
import { useAttachmentsStore } from './store-attachments';
// enable for debugging
export const DEBUG_ATTACHMENTS = true;
export function AttachmentMenu(props: {
llmAttachment: LLMAttachment,
menuAnchor: HTMLAnchorElement,
isPositionFirst: boolean,
isPositionLast: boolean,
onAttachmentInlineText: (attachmentId: string) => void,
onClose: () => void,
}) {
// derived state
const isPositionFixed = props.isPositionFirst && props.isPositionLast;
const {
attachment,
attachmentOutputs,
isUnconvertible,
isOutputMissing,
isOutputTextInlineable,
tokenCountApprox,
} = props.llmAttachment;
const {
id: aId,
input: aInput,
converters: aConverters,
converterIdx: aConverterIdx,
outputs: aOutputs,
} = attachment;
// operations
const { onClose, onAttachmentInlineText } = props;
const handleInlineText = React.useCallback(() => {
onClose();
onAttachmentInlineText(aId);
}, [aId, onAttachmentInlineText, onClose]);
const handleMoveUp = React.useCallback(() => {
useAttachmentsStore.getState().moveAttachment(aId, -1);
}, [aId]);
const handleMoveDown = React.useCallback(() => {
useAttachmentsStore.getState().moveAttachment(aId, 1);
}, [aId]);
const handleRemove = React.useCallback(() => {
onClose();
useAttachmentsStore.getState().removeAttachment(aId);
}, [aId, onClose]);
const handleSetConverterIdx = React.useCallback(async (converterIdx: number | null) => {
return useAttachmentsStore.getState().setConverterIdx(aId, converterIdx);
}, [aId]);
// const handleSummarizeText = React.useCallback(() => {
// onAttachmentSummarizeText(aId);
// }, [aId, onAttachmentSummarizeText]);
const handleCopyOutputToClipboard = React.useCallback(() => {
if (attachmentOutputs.length >= 1) {
const concat = attachmentOutputs.map(output => {
if (output.type === 'text-block')
return output.text;
else if (output.type === 'image-part')
return output.base64Url;
else
return null;
}).join('\n\n---\n\n');
copyToClipboard(concat.trim(), 'Converted attachment');
}
}, [attachmentOutputs]);
return (
<CloseableMenu
dense placement='top'
open anchorEl={props.menuAnchor} onClose={props.onClose}
sx={{ minWidth: 200 }}
>
{/* Move Arrows */}
{!isPositionFixed && <Box sx={{ display: 'flex', alignItems: 'center' }}>
<MenuItem
disabled={props.isPositionFirst}
onClick={handleMoveUp}
sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}
>
<KeyboardArrowLeftIcon />
</MenuItem>
<MenuItem
disabled={props.isPositionLast}
onClick={handleMoveDown}
sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}
>
<KeyboardArrowRightIcon />
</MenuItem>
</Box>}
{!isPositionFixed && <ListDivider sx={{ mt: 0 }} />}
{/* Render Converters as menu items */}
{/*{!isUnconvertible && <ListItem>*/}
{/* <Typography level='body-md'>*/}
{/* Attach as:*/}
{/* </Typography>*/}
{/*</ListItem>}*/}
{!isUnconvertible && aConverters.map((c, idx) =>
<MenuItem
disabled={c.disabled}
key={'c-' + c.id}
onClick={async () => idx !== aConverterIdx && await handleSetConverterIdx(idx)}
>
<ListItemDecorator>
<Radio checked={idx === aConverterIdx} />
</ListItemDecorator>
{c.unsupported
? <Box>Unsupported 🤔 <Typography level='body-xs'>{c.name}</Typography></Box>
: c.name}
</MenuItem>,
)}
{!isUnconvertible && <ListDivider />}
{DEBUG_ATTACHMENTS && !!aInput && (
<MenuItem onClick={handleCopyOutputToClipboard} disabled={!isOutputTextInlineable}>
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
<Box>
{!!aInput && <Typography level='body-xs'>
🡐 {aInput.mimeType}, {aInput.dataSize.toLocaleString()} bytes
</Typography>}
{/*<Typography level='body-xs'>*/}
{/* 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(' · ')}
</Typography>
{!!tokenCountApprox && <Typography level='body-xs'>
🡒 {tokenCountApprox.toLocaleString()} tokens
</Typography>}
</Box>
</MenuItem>
)}
{DEBUG_ATTACHMENTS && !!aInput && <ListDivider />}
{/* Destructive Operations */}
{/*<MenuItem onClick={handleCopyOutputToClipboard} disabled={!isOutputTextInlineable}>*/}
{/* <ListItemDecorator><ContentCopyIcon /></ListItemDecorator>*/}
{/* Copy*/}
{/*</MenuItem>*/}
{/*<MenuItem onClick={handleSummarizeText} disabled={!isOutputTextInlineable}>*/}
{/* <ListItemDecorator><CompressIcon color='success' /></ListItemDecorator>*/}
{/* Shrink*/}
{/*</MenuItem>*/}
<MenuItem onClick={handleInlineText} disabled={!isOutputTextInlineable}>
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
Inline text
</MenuItem>
<MenuItem onClick={handleRemove}>
<ListItemDecorator><ClearIcon /></ListItemDecorator>
Remove
</MenuItem>
</CloseableMenu>
);
}
@@ -1,169 +0,0 @@
import * as React from 'react';
import { Box, IconButton, ListItemDecorator, MenuItem } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import type { AttachmentId } from './store-attachments';
import type { LLMAttachments } from './useLLMAttachments';
import { AttachmentItem } from './AttachmentItem';
import { AttachmentMenu } from './AttachmentMenu';
/**
* Renderer of attachments, with menus, etc.
*/
export function Attachments(props: {
llmAttachments: LLMAttachments,
onAttachmentInlineText: (attachmentId: AttachmentId) => void,
onAttachmentsClear: () => void,
onAttachmentsInlineText: () => void,
}) {
// state
const [confirmClearAttachments, setConfirmClearAttachments] = React.useState<boolean>(false);
const [itemMenu, setItemMenu] = React.useState<{ anchor: HTMLAnchorElement, attachmentId: AttachmentId } | null>(null);
const [overallMenuAnchor, setOverallMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
// derived state
const { llmAttachments, onAttachmentsClear, onAttachmentInlineText, onAttachmentsInlineText } = props;
const { attachments, isOutputTextInlineable } = llmAttachments;
const hasAttachments = attachments.length >= 1;
// derived item menu state
const itemMenuAnchor = itemMenu?.anchor;
const itemMenuAttachmentId = itemMenu?.attachmentId;
const itemMenuAttachment = itemMenuAttachmentId ? attachments.find(la => la.attachment.id === itemMenu.attachmentId) : undefined;
const itemMenuIndex = itemMenuAttachment ? attachments.indexOf(itemMenuAttachment) : -1;
// item menu
const handleItemMenuToggle = React.useCallback((attachmentId: AttachmentId, anchor: HTMLAnchorElement) => {
handleOverallMenuHide();
setItemMenu(prev => prev?.attachmentId === attachmentId ? null : { anchor, attachmentId });
}, []);
const handleItemMenuHide = React.useCallback(() => {
setItemMenu(null);
}, []);
// item menu operations
const handleAttachmentInlineText = React.useCallback((attachmentId: string) => {
handleItemMenuHide();
onAttachmentInlineText(attachmentId);
}, [handleItemMenuHide, onAttachmentInlineText]);
// menu
const handleOverallMenuHide = () => setOverallMenuAnchor(null);
const handleOverallMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) =>
setOverallMenuAnchor(anchor => anchor ? null : event.currentTarget);
// overall operations
const handleAttachmentsInlineText = React.useCallback(() => {
handleOverallMenuHide();
onAttachmentsInlineText();
}, [onAttachmentsInlineText]);
const handleClearAttachments = () => setConfirmClearAttachments(true);
const handleClearAttachmentsConfirmed = React.useCallback(() => {
handleOverallMenuHide();
setConfirmClearAttachments(false);
onAttachmentsClear();
}, [onAttachmentsClear]);
// no components without attachments
if (!hasAttachments)
return null;
return <>
{/* Attachments bar */}
<Box sx={{ position: 'relative' }}>
{/* Horizontally scrollable Attachments */}
<Box sx={{ display: 'flex', overflowX: 'auto', gap: 1, height: '100%', pr: 5 }}>
{attachments.map((llmAttachment) =>
<AttachmentItem
key={llmAttachment.attachment.id}
llmAttachment={llmAttachment}
menuShown={llmAttachment.attachment.id === itemMenuAttachmentId}
onItemMenuToggle={handleItemMenuToggle}
/>,
)}
</Box>
{/* Overall Menu button */}
<IconButton
onClick={handleOverallMenuToggle}
sx={{
// borderRadius: 'sm',
borderRadius: 0,
position: 'absolute', right: 0, top: 0,
backgroundColor: 'neutral.softDisabledBg',
}}
>
<ExpandLessIcon />
</IconButton>
</Box>
{/* Attachment Menu */}
{!!itemMenuAnchor && !!itemMenuAttachment && (
<AttachmentMenu
llmAttachment={itemMenuAttachment}
menuAnchor={itemMenuAnchor}
isPositionFirst={itemMenuIndex === 0}
isPositionLast={itemMenuIndex === attachments.length - 1}
onAttachmentInlineText={handleAttachmentInlineText}
onClose={handleItemMenuHide}
/>
)}
{/* Overall Menu */}
{!!overallMenuAnchor && (
<CloseableMenu
dense placement='top-start'
open anchorEl={overallMenuAnchor} onClose={handleOverallMenuHide}
>
<MenuItem onClick={handleAttachmentsInlineText} disabled={!isOutputTextInlineable}>
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
Inline <span style={{ opacity: 0.5 }}>text attachments</span>
</MenuItem>
<MenuItem onClick={handleClearAttachments}>
<ListItemDecorator><ClearIcon /></ListItemDecorator>
Clear
</MenuItem>
</CloseableMenu>
)}
{/* 'Clear' Confirmation */}
{confirmClearAttachments && (
<ConfirmationModal
open onClose={() => setConfirmClearAttachments(false)} onPositive={handleClearAttachmentsConfirmed}
title='Confirm Removal'
positiveActionText='Remove All'
confirmationText={`This action will remove all (${attachments.length}) attachments. Do you want to proceed?`}
/>
)}
</>;
}
@@ -1,346 +0,0 @@
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 type { Attachment, AttachmentConverter, AttachmentId, AttachmentInput, AttachmentSource } from './store-attachments';
import type { ComposerOutputMultiPart } from '../composer.types';
// extensions to treat as plain text
const PLAIN_TEXT_EXTENSIONS: string[] = ['.ts', '.tsx'];
// mimetypes to treat as plain text
const PLAIN_TEXT_MIMETYPES: string[] = [
'text/plain',
'text/html',
'text/markdown',
'text/csv',
'text/css',
'text/javascript',
'application/json',
];
/**
* Creates a new Attachment object.
*/
export function attachmentCreate(source: AttachmentSource, checkDuplicates: AttachmentId[]): Attachment {
return {
id: createBase36Uid(checkDuplicates),
source: source,
label: 'Loading...',
ref: '',
inputLoading: false,
inputError: null,
input: undefined,
converters: [],
converterIdx: null,
outputsConverting: false,
outputs: [],
// metadata: {},
};
}
/**
* Asynchronously loads the input for an Attachment object.
*
* @param {Readonly<AttachmentSource>} source - The source of the attachment.
* @param {(changes: Partial<Attachment>) => void} edit - A function to edit the Attachment object.
*/
export async function attachmentLoadInputAsync(source: Readonly<AttachmentSource>, edit: (changes: Partial<Attachment>) => void) {
edit({ inputLoading: true });
switch (source.media) {
// Download URL (page, file, ..) and attach as input
case 'url':
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' });
} catch (error: any) {
edit({ inputError: `Issue downloading page: ${error?.message || (typeof error === 'string' ? error : JSON.stringify(error))}` });
}
break;
// Attach file as input
case 'file':
edit({ label: source.refPath, ref: source.refPath });
// fix missing/wrong mimetypes
let mimeType = source.fileWithHandle.type;
if (!mimeType) {
// see note on 'attachAppendDataTransfer'; this is a fallback for drag/drop missing Mimes sometimes
console.warn('Assuming the attachment is text/plain. From:', source.origin, ', name:', source.refPath);
mimeType = 'text/plain';
} else {
// possibly fix wrongly assigned mimetypes (from the extension alone)
if (!mimeType.startsWith('text/') && PLAIN_TEXT_EXTENSIONS.some(ext => source.refPath.endsWith(ext)))
mimeType = 'text/plain';
}
// UX: just a hint of a loading state
await new Promise(resolve => setTimeout(resolve, 100));
try {
const fileArrayBuffer = await source.fileWithHandle.arrayBuffer();
edit({
input: {
mimeType,
data: fileArrayBuffer,
dataSize: fileArrayBuffer.byteLength,
},
});
} catch (error: any) {
edit({ inputError: `Issue loading file: ${error?.message || (typeof error === 'string' ? error : JSON.stringify(error))}` });
}
break;
case 'text':
if (source.textHtml && source.textPlain) {
edit({
label: 'Rich Text',
ref: '',
input: {
mimeType: 'text/plain',
data: source.textPlain,
dataSize: source.textPlain!.length,
altMimeType: 'text/html',
altData: source.textHtml,
},
});
} else {
const text = source.textHtml || source.textPlain || '';
edit({
label: 'Text',
ref: '',
input: {
mimeType: 'text/plain',
data: text,
dataSize: text.length,
},
});
}
break;
}
edit({ inputLoading: false });
}
/**
* Defines the possible converters for an Attachment object based on its input type.
*
* @param {AttachmentSource['media']} sourceType - The media type of the attachment source.
* @param {Readonly<AttachmentInput>} input - The input of the attachment.
* @param {(changes: Partial<Attachment>) => void} edit - A function to edit the Attachment object.
*/
export function attachmentDefineConverters(sourceType: AttachmentSource['media'], input: Readonly<AttachmentInput>, edit: (changes: Partial<Attachment>) => void) {
// return all the possible converters for the input
const converters: AttachmentConverter[] = [];
switch (true) {
// plain text types
case PLAIN_TEXT_MIMETYPES.includes(input.mimeType):
// handle a secondary layer of HTML 'text' origins: drop, paste, and clipboard-read
const textOriginHtml = sourceType === 'text' && input.altMimeType === 'text/html' && !!input.altData;
const isHtmlTable = !!input.altData?.startsWith('<table');
// p1: Tables
if (textOriginHtml && isHtmlTable) {
converters.push({
id: 'rich-text-table',
name: 'Markdown Table',
});
}
// p2: Text
converters.push({
id: 'text',
name: 'Text',
});
// p3: Html
if (textOriginHtml) {
converters.push({
id: 'rich-text',
name: 'HTML',
});
}
break;
// PDF
case ['application/pdf', 'application/x-pdf', 'application/acrobat'].includes(input.mimeType):
converters.push({ id: 'pdf-text', name: `PDF To Text` });
converters.push({ id: 'pdf-images', name: `PDF To Images`, disabled: true });
break;
// images
case input.mimeType.startsWith('image/'):
converters.push({ id: 'image', name: `Image (coming soon)` });
converters.push({ id: 'image-ocr', name: 'As Text (OCR)' });
break;
// catch-all
default:
converters.push({ id: 'unhandled', name: `${input.mimeType}`, unsupported: true });
converters.push({ id: 'text', name: 'As Text' });
break;
}
edit({ converters });
}
/**
* Converts the input of an Attachment object based on the selected converter.
*
* @param {Readonly<Attachment>} attachment - The Attachment object to convert.
* @param {number | null} converterIdx - The index of the selected conversion in the Attachment object's converters array.
* @param {(changes: Partial<Attachment>) => void} edit - A function to edit the Attachment object.
*/
export async function attachmentPerformConversion(attachment: Readonly<Attachment>, converterIdx: number | null, edit: (changes: Partial<Attachment>) => void) {
// set converter index
converterIdx = (converterIdx !== null && converterIdx >= 0 && converterIdx < attachment.converters.length) ? converterIdx : null;
edit({
converterIdx: converterIdx,
outputs: [],
});
// get converter
const { ref, input } = attachment;
const converter = converterIdx !== null ? attachment.converters[converterIdx] : null;
if (!converter || !input)
return;
edit({
outputsConverting: true,
});
// input datacould be a string or an ArrayBuffer
function inputDataToString(data: string | ArrayBuffer | null | undefined): string {
if (typeof data === 'string')
return data;
if (data instanceof ArrayBuffer)
return new TextDecoder().decode(data);
return '';
}
// apply converter to the input
const outputs: ComposerOutputMultiPart = [];
switch (converter.id) {
// text as-is
case 'text':
outputs.push({
type: 'text-block',
text: inputDataToString(input.data),
title: ref,
collapsible: true,
});
break;
// html as-is
case 'rich-text':
outputs.push({
type: 'text-block',
text: input.altData!,
title: ref || '\n<!DOCTYPE html>',
collapsible: true,
});
break;
// html to markdown table
case 'rich-text-table':
let mdTable: string;
try {
mdTable = htmlTableToMarkdown(input.altData!, false);
} catch (error) {
// fallback to text/plain
mdTable = inputDataToString(input.data);
}
outputs.push({
type: 'text-block',
text: mdTable,
title: ref,
collapsible: true,
});
break;
case 'pdf-text':
if (!(input.data instanceof ArrayBuffer)) {
console.log('Expected ArrayBuffer for PDF converter, got:', typeof input.data);
break;
}
// duplicate the ArrayBuffer to avoid mutation
const pdfData = new Uint8Array(input.data.slice(0));
const pdfText = await pdfToText(pdfData);
outputs.push({
type: 'text-block',
text: pdfText,
title: ref,
collapsible: true,
});
break;
case 'pdf-images':
// TODO: extract all pages as individual images
break;
case 'image':
// TODO: continue here
/*outputs.push({
type: 'image-part',
base64Url: `data:notImplemented.yet:)`,
collapsible: false,
});*/
break;
case 'image-ocr':
if (!(input.data instanceof ArrayBuffer)) {
console.log('Expected ArrayBuffer for Image OCR converter, got:', typeof input.data);
break;
}
try {
const { recognize } = await import('tesseract.js');
const buffer = Buffer.from(input.data);
const result = await recognize(buffer, undefined, {
errorHandler: e => console.error(e),
logger: (message) => {
if (message.status === 'recognizing text')
console.log('OCR progress:', message.progress);
},
});
outputs.push({
type: 'text-block',
text: result.data.text,
title: ref,
collapsible: true,
});
} catch (error) {
console.error(error);
}
break;
case 'unhandled':
// force the user to explicitly select 'as text' if they want to proceed
break;
}
// update
edit({
outputsConverting: false,
outputs,
});
}
@@ -1,42 +0,0 @@
/*
/// REDUCER
import { ContentReducer } from '~/modules/aifn/summarize/ContentReducer';
const [reducerText, setReducerText] = React.useState('');
const [reducerTextTokens, setReducerTextTokens] = React.useState(0);
{reducerText?.length >= 1 &&
<ContentReducer
initialText={reducerText} initialTokens={reducerTextTokens} tokenLimit={remainingTokens}
onReducedText={handleReducedText} onClose={handleReducerClose}
/>
}
const handleReducerClose = () => setReducerText('');
const handleReducedText = (text: string) => {
handleReducerClose();
setComposeText(_t => _t + text);
};
const handleAttachFiles = async (files: FileList, overrideFileNames?: string[]): Promise<void> => {
// see how we fare on budget
if (chatLLMId) {
const newTextTokens = countModelTokens(newText, chatLLMId, 'reducer trigger') ?? 0;
// simple trigger for the reduction dialog
if (newTextTokens > remainingTokens) {
setReducerTextTokens(newTextTokens);
setReducerText(newText);
return;
}
}
// within the budget, so just append
setComposeText(text => expandPromptTemplate(PromptTemplates.Concatenate, { text: newText })(text));
*/
@@ -1,201 +0,0 @@
import { create } from 'zustand';
import type { FileWithHandle } from 'browser-fs-access';
import type { ComposerOutputMultiPart } from '../composer.types';
import { attachmentCreate, attachmentDefineConverters, attachmentLoadInputAsync, attachmentPerformConversion } from './pipeline';
// Attachment Types
export type AttachmentSourceOriginDTO = 'drop' | 'paste';
export type AttachmentSourceOriginFile = 'camera' | 'screencapture' | 'file-open' | 'clipboard-read' | AttachmentSourceOriginDTO;
export type AttachmentSource = {
media: 'url';
url: string;
refUrl: string;
} | {
media: 'file';
origin: AttachmentSourceOriginFile,
fileWithHandle: FileWithHandle;
refPath: string;
} | {
media: 'text';
method: 'clipboard-read' | AttachmentSourceOriginDTO;
textPlain?: string;
textHtml?: string;
};
export type AttachmentInput = {
mimeType: string; // Original MIME type of the file
data: string | ArrayBuffer; // The original data of the attachment
dataSize: number; // Size of the original data in bytes
altMimeType?: string; // Alternative MIME type for the input
altData?: string; // Alternative data for the input
// preview?: AttachmentPreview; // Preview of the input
};
export type AttachmentConverterType =
| 'text' | 'rich-text' | 'rich-text-table'
| 'pdf-text' | 'pdf-images'
| 'image' | 'image-ocr'
| 'unhandled';
export type AttachmentConverter = {
id: AttachmentConverterType;
name: string;
disabled?: boolean;
unsupported?: boolean;
// outputType: ComposerOutputPartType; // The type of the output after conversion
// isAutonomous: boolean; // Whether the conversion does not require user input
// isAsync: boolean; // Whether the conversion is asynchronous
// progress: number; // Conversion progress percentage (0..1)
// errorMessage?: string; // Error message if the conversion failed
}
export type AttachmentId = string;
export type Attachment = {
readonly id: AttachmentId;
readonly source: AttachmentSource,
label: string;
ref: string;
inputLoading: boolean;
inputError: string | null;
input?: AttachmentInput;
// options to convert the input
converters: AttachmentConverter[]; // List of available converters for this attachment
converterIdx: number | null; // Index of the selected converter
outputsConverting: boolean;
outputs: ComposerOutputMultiPart; // undefined: not yet converted, []: conversion failed, [ {}+ ]: conversion succeeded
// metadata: {
// size?: number; // Size of the attachment in bytes
// creationDate?: Date; // Creation date of the file
// modifiedDate?: Date; // Last modified date of the file
// altText?: string; // Alternative text for images for screen readers
// };
};
/*export type AttachmentPreview = {
renderer: 'noPreview',
title: string; // A title for the preview
} | {
renderer: 'textPreview'
fileName: string; // The name of the file
snippet: string; // A text snippet for documents
tooltip?: string; // A tooltip for the preview
} | {
renderer: 'imagePreview'
thumbnail: string; // A thumbnail preview for images, videos, etc.
tooltip?: string; // A tooltip for the preview
};*/
/// Store
interface AttachmentsStore {
attachments: Attachment[];
createAttachment: (source: AttachmentSource) => Promise<void>;
clearAttachments: () => void;
removeAttachment: (attachmentId: AttachmentId) => void;
moveAttachment: (attachmentId: AttachmentId, delta: 1 | -1) => void;
setConverterIdx: (attachmentId: AttachmentId, converterIdx: number | null) => Promise<void>;
_editAttachment: (attachmentId: AttachmentId, update: Partial<Attachment> | ((attachment: Attachment) => Partial<Attachment>)) => void;
_getAttachment: (attachmentId: AttachmentId) => Attachment | undefined;
}
export const useAttachmentsStore = create<AttachmentsStore>()(
(_set, _get) => ({
attachments: [],
createAttachment: async (source: AttachmentSource) => {
const { attachments, _getAttachment, _editAttachment, setConverterIdx } = _get();
const attachment = attachmentCreate(source, attachments.map(a => a.id));
_set({
attachments: [...attachments, attachment],
});
const editFn = (changes: Partial<Attachment>) => _editAttachment(attachment.id, changes);
// 1.Resolve the Input
await attachmentLoadInputAsync(source, editFn);
const loaded = _getAttachment(attachment.id);
if (!loaded || !loaded.input)
return;
// 2. Define the I->O Converters
attachmentDefineConverters(source.media, loaded.input, editFn);
const defined = _getAttachment(attachment.id);
if (!defined || !defined.converters.length || defined.converterIdx !== null)
return;
// 3. Select the first Converter
const firstEnabledIndex = defined.converters.findIndex(_c => !_c.disabled);
await setConverterIdx(attachment.id, firstEnabledIndex > -1 ? firstEnabledIndex : 0);
},
clearAttachments: () => _set({
attachments: [],
}),
removeAttachment: (attachmentId: AttachmentId) =>
_set(state => ({
attachments: state.attachments.filter(attachment => attachment.id !== attachmentId),
})),
moveAttachment: (attachmentId: AttachmentId, delta: 1 | -1) =>
_set(state => {
const attachments = [...state.attachments];
const currentIdx = attachments.findIndex(a => a.id === attachmentId);
// If the attachment is not found, or if trying to move beyond the array boundaries, no move is needed
if (currentIdx === -1 || (currentIdx === 0 && delta === -1) || (currentIdx === attachments.length - 1 && delta === 1))
return state;
// Swap the attachment with the adjacent one in the direction of delta
const targetIdx = currentIdx + delta;
[attachments[currentIdx], attachments[targetIdx]] = [attachments[targetIdx], attachments[currentIdx]];
return { attachments };
}),
setConverterIdx: async (attachmentId: AttachmentId, converterIdx: number | null) => {
const { _getAttachment, _editAttachment } = _get();
const attachment = _getAttachment(attachmentId);
if (!attachment || attachment.converterIdx === converterIdx)
return;
const editFn = (changes: Partial<Attachment>) => _editAttachment(attachmentId, changes);
await attachmentPerformConversion(attachment, converterIdx, editFn);
},
_editAttachment: (attachmentId: AttachmentId, update: Partial<Attachment> | ((attachment: Attachment) => Partial<Attachment>)) =>
_set(state => ({
attachments: state.attachments.map((attachment: Attachment): Attachment =>
attachment.id === attachmentId
? { ...attachment, ...(typeof update === 'function' ? update(attachment) : update) }
: attachment,
),
})),
_getAttachment: (attachmentId: AttachmentId) =>
_get().attachments.find(a => a.id === attachmentId),
}),
);
@@ -1,147 +0,0 @@
import * as React from 'react';
import type { DLLMId } from '~/modules/llms/store-llms';
import { countModelTokens } from '~/common/util/token-counter';
import type { Attachment, AttachmentId } from './store-attachments';
import type { ComposerOutputMultiPart, ComposerOutputPartType } from '../composer.types';
export interface LLMAttachments {
attachments: LLMAttachment[];
getAttachmentOutputs: (initialTextBlockText: string | null, attachmentId: AttachmentId) => ComposerOutputMultiPart;
getAttachmentsOutputs: (initialTextBlockText: string | null) => ComposerOutputMultiPart;
isOutputAttacheable: boolean;
isOutputTextInlineable: boolean;
tokenCountApprox: number;
}
export interface LLMAttachment {
attachment: Attachment;
attachmentOutputs: ComposerOutputMultiPart;
isUnconvertible: boolean;
isOutputMissing: boolean;
isOutputAttachable: boolean;
isOutputTextInlineable: boolean;
tokenCountApprox: number | null;
}
export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId | null): LLMAttachments {
return React.useMemo(() => {
// HACK: in the future, switch to LLM capabilities (LLM_IF_OAI_Chat, LLM_IF_OAI_Vision, etc.)
const supportsImages = !!chatLLMId?.endsWith('-vision-preview');
const supportedOutputPartTypes: ComposerOutputPartType[] = supportsImages ? ['text-block', 'image-part'] : ['text-block'];
const llmAttachments = attachments.map(attachment => toLLMAttachment(attachment, supportedOutputPartTypes, chatLLMId));
const getAttachmentOutputs = (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 => {
// accumulate all outputs of all attachments
const allOutputs = llmAttachments.reduce((acc, a) => acc.concat(a.attachment.outputs), [] as ComposerOutputMultiPart);
return attachmentCollapseOutputs(initialTextBlockText, allOutputs);
};
return {
attachments: llmAttachments,
getAttachmentOutputs,
getAttachmentsOutputs,
isOutputAttacheable: llmAttachments.every(a => a.isOutputAttachable),
isOutputTextInlineable: llmAttachments.every(a => a.isOutputTextInlineable),
tokenCountApprox: llmAttachments.reduce((acc, a) => acc + (a.tokenCountApprox || 0), 0),
};
}, [attachments, chatLLMId]);
}
export function getTextBlockText(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;
}
function toLLMAttachment(attachment: Attachment, supportedOutputPartTypes: ComposerOutputPartType[], llmForTokenCount: DLLMId | null): LLMAttachment {
const { converters, outputs } = attachment;
const isUnconvertible = converters.length === 0;
const isOutputMissing = outputs.length === 0;
const isOutputAttachable = areAllOutputsSupported(outputs, supportedOutputPartTypes);
const isOutputTextInlineable = areAllOutputsSupported(outputs, supportedOutputPartTypes.filter(pt => pt === 'text-block'));
const attachmentOutputs = attachmentCollapseOutputs(null, outputs);
const tokenCountApprox = llmForTokenCount
? attachmentOutputs.reduce((acc, output) => {
if (output.type === 'text-block')
return acc + (countModelTokens(output.text, llmForTokenCount, 'attachments tokens count') ?? 0);
console.warn('Unhandled token preview for output type:', output.type);
return acc;
}, 0)
: null;
return {
attachment,
attachmentOutputs,
isUnconvertible,
isOutputMissing,
isOutputAttachable,
isOutputTextInlineable,
tokenCountApprox,
};
}
function areAllOutputsSupported(outputs: ComposerOutputMultiPart, supportedOutputPartTypes: ComposerOutputPartType[]) {
return outputs.length
? outputs.every(output => supportedOutputPartTypes.includes(output.type))
: false;
}
function attachmentCollapseOutputs(initialTextBlockText: string | null, outputs: ComposerOutputMultiPart): ComposerOutputMultiPart {
const accumulatedOutputs: ComposerOutputMultiPart = [];
// if there's initial text, make it a collapsible default (unquited) text block
if (initialTextBlockText !== null) {
accumulatedOutputs.push({
type: 'text-block',
text: initialTextBlockText,
title: null,
collapsible: true,
});
}
// Accumulate attachment outputs of the same type and 'collapsible' into a single object of that type.
for (const output of outputs) {
const last = accumulatedOutputs[accumulatedOutputs.length - 1];
// accumulationg over an existing part of the same type
if (last && last.type === output.type && output.collapsible) {
switch (last.type) {
case 'text-block':
last.text += `\n\n\`\`\`${output.title}\n${output.text}\n\`\`\``;
break;
default:
console.warn('Unhandled collapsing for output type:', output.type);
}
}
// start a new part
else {
if (output.type === 'text-block') {
accumulatedOutputs.push({
type: 'text-block',
text: `\n\n\`\`\`${output.title}\n${output.text}\n\`\`\``,
title: null,
collapsible: false,
});
} else {
accumulatedOutputs.push(output);
}
}
}
return accumulatedOutputs;
}
@@ -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}>
@@ -1,15 +0,0 @@
export type ComposerOutputPartType = 'text-block' | 'image-part';
export type ComposerOutputPart = {
type: 'text-block',
text: string,
title: string | null,
collapsible: boolean,
} | {
// TODO: not implemented yet
type: 'image-part',
base64Url: string,
collapsible: false,
};
export type ComposerOutputMultiPart = ComposerOutputPart[];
@@ -4,8 +4,12 @@ import { Box, Button, CircularProgress, ColorPaletteProp, Sheet, Typography } fr
import AbcIcon from '@mui/icons-material/Abc';
import CodeIcon from '@mui/icons-material/Code';
import ImageOutlinedIcon from '@mui/icons-material/ImageOutlined';
import PermMediaOutlinedIcon from '@mui/icons-material/PermMediaOutlined';
import PhotoSizeSelectLargeOutlinedIcon from '@mui/icons-material/PhotoSizeSelectLargeOutlined';
import PhotoSizeSelectSmallOutlinedIcon from '@mui/icons-material/PhotoSizeSelectSmallOutlined';
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';
@@ -13,8 +17,8 @@ import WarningRoundedIcon from '@mui/icons-material/WarningRounded';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { ellipsizeFront, ellipsizeMiddle } from '~/common/util/textUtils';
import type { Attachment, AttachmentConverterType, AttachmentId } from './store-attachments';
import type { LLMAttachment } from './useLLMAttachments';
import type { AttachmentDraft, AttachmentDraftConverterType, AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import type { LLMAttachmentDraft } from './useLLMAttachmentDrafts';
// default attachment width
@@ -65,19 +69,23 @@ const InputErrorIndicator = () =>
<WarningRoundedIcon sx={{ color: 'danger.solidBg' }} />;
const converterTypeToIconMap: { [key in AttachmentConverterType]: React.ComponentType<any> } = {
const converterTypeToIconMap: { [key in AttachmentDraftConverterType]: React.ComponentType<any> } = {
'text': TextFieldsIcon,
'rich-text': CodeIcon,
'rich-text-table': PivotTableChartIcon,
'pdf-text': PictureAsPdfIcon,
'pdf-images': PictureAsPdfIcon,
'image': ImageOutlinedIcon,
'pdf-images': PermMediaOutlinedIcon,
'image-original': ImageOutlinedIcon,
'image-resized-high': PhotoSizeSelectLargeOutlinedIcon,
'image-resized-low': PhotoSizeSelectSmallOutlinedIcon,
'image-to-default': ImageOutlinedIcon,
'image-ocr': AbcIcon,
'ego-fragments-inlined': TelegramIcon,
'unhandled': TextureIcon,
};
function attachmentConverterIcon(attachment: Attachment) {
const converter = attachment.converterIdx !== null ? attachment.converters[attachment.converterIdx] ?? null : null;
function attachmentConverterIcon(attachmentDraft: AttachmentDraft) {
const converter = attachmentDraft.converterIdx !== null ? attachmentDraft.converters[attachmentDraft.converterIdx] ?? null : null;
if (converter && converter.id) {
const Icon = converterTypeToIconMap[converter.id] ?? null;
if (Icon)
@@ -86,56 +94,51 @@ function attachmentConverterIcon(attachment: Attachment) {
return null;
}
function attachmentLabelText(attachment: Attachment): string {
const converter = attachment.converterIdx !== null ? attachment.converters[attachment.converterIdx] ?? null : null;
if (converter && attachment.label === 'Rich Text') {
function attachmentLabelText(attachmentDraft: AttachmentDraft): string {
const converter = attachmentDraft.converterIdx !== null ? attachmentDraft.converters[attachmentDraft.converterIdx] ?? null : null;
if (converter && attachmentDraft.label === 'Rich Text') {
if (converter.id === 'rich-text-table')
return 'Rich Table';
if (converter.id === 'rich-text')
return 'Rich HTML';
}
return ellipsizeFront(attachment.label, 24);
return ellipsizeFront(attachmentDraft.label, 24);
}
export function AttachmentItem(props: {
llmAttachment: LLMAttachment,
export function LLMAttachmentItem(props: {
llmAttachment: LLMAttachmentDraft,
menuShown: boolean,
onItemMenuToggle: (attachmentId: AttachmentId, anchor: HTMLAnchorElement) => void,
onToggleMenu: (attachmentDraftId: AttachmentDraftId, anchor: HTMLAnchorElement) => void,
}) {
// derived state
const { attachmentDraft: draft, llmSupportsAllFragments } = props.llmAttachment;
const { onItemMenuToggle } = props;
const isInputLoading = draft.inputLoading;
const isInputError = !!draft.inputError;
const isUnconvertible = !draft.converters.length;
const isOutputLoading = draft.outputsConverting;
const isOutputMissing = !draft.outputFragments.length;
const {
attachment,
isUnconvertible,
isOutputMissing,
isOutputAttachable,
} = props.llmAttachment;
const showWarning = isUnconvertible || (isOutputMissing || !llmSupportsAllFragments);
const {
inputError,
inputLoading: isInputLoading,
outputsConverting: isOutputLoading,
} = attachment;
const isInputError = !!inputError;
const showWarning = isUnconvertible || isOutputMissing || !isOutputAttachable;
// handlers
const { onToggleMenu } = props;
const handleToggleMenu = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
event.stopPropagation();
onItemMenuToggle(attachment.id, event.currentTarget);
}, [attachment, onItemMenuToggle]);
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
onToggleMenu(draft.id, event.currentTarget);
}, [draft.id, onToggleMenu]);
// compose tooltip
let tooltip: string | null = '';
if (attachment.source.media !== 'text')
tooltip += attachment.source.media + ': ';
tooltip += attachment.label;
if (draft.source.media !== 'text')
tooltip += draft.source.media + ': ';
tooltip += draft.label;
// if (hasInput)
// tooltip += `\n(${aInput.mimeType}: ${aInput.dataSize.toLocaleString()} bytes)`;
// if (aOutputs && aOutputs.length >= 1)
@@ -147,15 +150,15 @@ export function AttachmentItem(props: {
if (isInputLoading || isOutputLoading) {
color = 'success';
} else if (isInputError) {
tooltip = `Issue loading the attachment: ${attachment.inputError}\n\n${tooltip}`;
color = 'danger';
tooltip = props.menuShown ? null
: `Issue loading the attachment: ${draft.inputError}\n\n${tooltip}`;
} else if (showWarning) {
tooltip = props.menuShown
? null
: isUnconvertible
? `Attachments of type '${attachment.input?.mimeType}' are not supported yet. You can open a feature request on GitHub.\n\n${tooltip}`
: `Not compatible with the selected LLM or not supported. Please select another format.\n\n${tooltip}`;
color = 'warning';
tooltip = props.menuShown ? null
: isUnconvertible
? `Attachments of type '${draft.input?.mimeType}' are not supported yet. You can open a feature request on GitHub.\n\n${tooltip}`
: `Not compatible with the selected LLM or file not supported. Please try another format.\n\n${tooltip}`;
} else {
// all good
tooltip = null;
@@ -173,12 +176,13 @@ export function AttachmentItem(props: {
sx={{ p: 1, whiteSpace: 'break-spaces' }}
>
{isInputLoading
? <LoadingIndicator label={attachment.label} />
? <LoadingIndicator label={draft.label} />
: (
<Button
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,
@@ -192,11 +196,11 @@ export function AttachmentItem(props: {
{isInputError
? <InputErrorIndicator />
: <>
{attachmentConverterIcon(attachment)}
{attachmentConverterIcon(draft)}
{isOutputLoading
? <>Converting <CircularProgress color='success' size='sm' /></>
: <Typography level='title-sm' sx={{ whiteSpace: 'nowrap' }}>
{attachmentLabelText(attachment)}
{attachmentLabelText(draft)}
</Typography>}
</>}
</Button>
@@ -0,0 +1,216 @@
import * as React from 'react';
import { Box, CircularProgress, Link, ListDivider, ListItem, ListItemDecorator, MenuItem, Radio, Typography } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import LaunchIcon from '@mui/icons-material/Launch';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import { showImageDataRefInNewTab } from '~/modules/blocks/image/RenderImageRefDBlob';
import { DMessageAttachmentFragment, isImageRefPart } from '~/common/stores/chat/chat.fragments';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import type { AttachmentDraftsStoreApi } from '~/common/attachment-drafts/store-attachment-drafts-slice';
import type { LLMAttachmentDraft } from './useLLMAttachmentDrafts';
import type { LLMAttachmentDraftsAction } from './LLMAttachmentsList';
// enable for debugging
export const DEBUG_LLMATTACHMENTS = true;
export function LLMAttachmentMenu(props: {
attachmentDraftsStoreApi: AttachmentDraftsStoreApi,
llmAttachmentDraft: LLMAttachmentDraft,
menuAnchor: HTMLAnchorElement,
isPositionFirst: boolean,
isPositionLast: boolean,
onDraftAction: (attachmentDraftId: AttachmentDraftId, actionId: LLMAttachmentDraftsAction) => void,
onClose: () => void,
}) {
// derived state
const {
attachmentDraft: draft,
llmSupportsTextFragments,
llmTokenCountApprox,
} = props.llmAttachmentDraft;
const draftId = draft.id;
const draftInput = draft.input;
const isConverting = draft.outputsConverting;
const isUnconvertible = !draft.converters.length;
const isOutputMissing = !draft.outputFragments.length;
const isUnmoveable = props.isPositionFirst && props.isPositionLast;
// operations
const { attachmentDraftsStoreApi, onDraftAction, onClose } = props;
const handleMoveUp = React.useCallback(() => {
attachmentDraftsStoreApi.getState().moveAttachmentDraft(draftId, -1);
}, [draftId, attachmentDraftsStoreApi]);
const handleMoveDown = React.useCallback(() => {
attachmentDraftsStoreApi.getState().moveAttachmentDraft(draftId, 1);
}, [draftId, attachmentDraftsStoreApi]);
const handleRemove = React.useCallback(() => {
onClose();
attachmentDraftsStoreApi.getState().removeAttachmentDraft(draftId);
}, [draftId, attachmentDraftsStoreApi, onClose]);
const handleSetConverterIdx = React.useCallback(async (converterIdx: number | null) => {
return attachmentDraftsStoreApi.getState().setAttachmentDraftConverterIdxAndConvert(draftId, converterIdx);
}, [draftId, attachmentDraftsStoreApi]);
// const handleSummarizeText = React.useCallback(() => {
// onAttachmentDraftSummarizeText(draftId);
// }, [draftId, onAttachmentDraftSummarizeText]);
return (
<CloseableMenu
dense placement='top'
open anchorEl={props.menuAnchor} onClose={props.onClose}
sx={{ minWidth: 260 }}
>
{/* Move Arrows */}
{!isUnmoveable && <Box sx={{ display: 'flex', alignItems: 'center' }}>
<MenuItem
disabled={props.isPositionFirst}
onClick={handleMoveUp}
sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}
>
<KeyboardArrowLeftIcon />
</MenuItem>
<MenuItem
disabled={props.isPositionLast}
onClick={handleMoveDown}
sx={{ flex: 1, display: 'flex', justifyContent: 'center' }}
>
<KeyboardArrowRightIcon />
</MenuItem>
</Box>}
{!isUnmoveable && <ListDivider sx={{ mt: 0 }} />}
{/* Render Converters as menu items */}
{!isUnconvertible && (
<ListItem>
<Typography level='body-sm'>
Attach as:
</Typography>
</ListItem>
)}
{!isUnconvertible && draft.converters.map((c, idx) =>
<MenuItem
disabled={c.disabled || isConverting}
key={'c-' + c.id}
onClick={async () => idx !== draft.converterIdx && await handleSetConverterIdx(idx)}
>
<ListItemDecorator>
{(isConverting && idx === draft.converterIdx)
? <CircularProgress size='sm' sx={{ '--CircularProgress-size': '1.25rem' }} />
: <Radio checked={idx === draft.converterIdx} disabled={isConverting} />}
</ListItemDecorator>
{c.unsupported
? <Box>Unsupported 🤔 <Typography level='body-xs'>{c.name}</Typography></Box>
: c.name}
</MenuItem>,
)}
{!isUnconvertible && <ListDivider />}
{DEBUG_LLMATTACHMENTS && !!draftInput && !isConverting && (
<ListItem>
<ListItemDecorator />
<Box>
{!!draftInput && (
<Typography level='body-sm'>
🡐 {draftInput.mimeType} · {draftInput.dataSize.toLocaleString()}
</Typography>
)}
{!!draftInput?.altMimeType && (
<Typography level='body-sm'>
<span style={{ color: 'transparent' }}>🡐</span> {draftInput.altMimeType} · {draftInput.altData?.length.toLocaleString()}
</Typography>
)}
{/*<Typography level='body-sm'>*/}
{/* Converters: {aConverters.map(((converter, idx) => ` ${converter.id}${(idx === draft.converterIdx) ? '*' : ''}`)).join(', ')}*/}
{/*</Typography>*/}
<Box>
{isOutputMissing ? (
<Typography level='body-sm'>🡒 ...</Typography>
) : (
draft.outputFragments.map(({ part }, index) => {
if (isImageRefPart(part)) {
const resolution = part.width && part.height ? `${part.width} x ${part.height}` : 'unknown resolution';
const mime = part.dataRef.reftype === 'dblob' ? part.dataRef.mimeType : 'unknown image';
return (
<Typography key={index} level='body-sm'>
🡒 {mime/*unic.replace('image/', 'img: ')*/} · {resolution} · {part.dataRef.reftype === 'dblob' ? part.dataRef.bytesSize?.toLocaleString() : '(remote)'}
{' · '}
<Link onClick={() => showImageDataRefInNewTab(part.dataRef)}>
open <LaunchIcon sx={{ mx: 0.5, fontSize: 16 }} />
</Link>
</Typography>
);
} else if (part.pt === 'doc') {
return (
<Typography key={index} level='body-sm'>
🡒 text: {part.data.text.length.toLocaleString()} bytes
</Typography>
);
} else {
return (
<Typography key={index} level='body-sm'>
🡒 {(part as DMessageAttachmentFragment['part']).pt}: (other)
</Typography>
);
}
})
)}
{!!llmTokenCountApprox && (
<Typography level='body-sm' sx={{ ml: 1.75 }}>
~ {llmTokenCountApprox.toLocaleString()} tokens
</Typography>
)}
</Box>
</Box>
</ListItem>
)}
{DEBUG_LLMATTACHMENTS && !!draftInput && !isConverting && <ListDivider />}
{/* Destructive Operations */}
{/*<MenuItem onClick={handleCopyToClipboard} disabled={!isOutputTextInlineable}>*/}
{/* <ListItemDecorator><ContentCopyIcon /></ListItemDecorator>*/}
{/* Copy*/}
{/*</MenuItem>*/}
{/*<MenuItem onClick={handleSummarizeText} disabled={!isOutputTextInlineable}>*/}
{/* <ListItemDecorator><CompressIcon color='success' /></ListItemDecorator>*/}
{/* Shrink*/}
{/*</MenuItem>*/}
<MenuItem onClick={() => onDraftAction(draftId, 'inline-text')} disabled={!llmSupportsTextFragments || isConverting}>
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
Inline text
</MenuItem>
<MenuItem onClick={() => onDraftAction(draftId, 'copy-text')} disabled={!llmSupportsTextFragments || isConverting}>
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
Copy text
</MenuItem>
<ListDivider />
<MenuItem onClick={handleRemove}>
<ListItemDecorator><ClearIcon /></ListItemDecorator>
Remove
</MenuItem>
</CloseableMenu>
);
}
@@ -0,0 +1,184 @@
import * as React from 'react';
import { Box, IconButton, ListDivider, ListItemDecorator, MenuItem } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import type { AttachmentDraftId } from '~/common/attachment-drafts/attachment.types';
import type { AttachmentDraftsStoreApi } from '~/common/attachment-drafts/store-attachment-drafts-slice';
import type { LLMAttachmentDrafts } from './useLLMAttachmentDrafts';
import { LLMAttachmentItem } from './LLMAttachmentItem';
import { LLMAttachmentMenu } from './LLMAttachmentMenu';
export type LLMAttachmentDraftsAction = 'inline-text' | 'copy-text';
/**
* Renderer of attachment drafts, with menus, etc.
*/
export function LLMAttachmentsList(props: {
attachmentDraftsStoreApi: AttachmentDraftsStoreApi,
llmAttachmentDrafts: LLMAttachmentDrafts,
onAttachmentDraftsAction: (attachmentDraftId: AttachmentDraftId | null, actionId: LLMAttachmentDraftsAction) => void,
}) {
// state
const [confirmClearAttachmentDrafts, setConfirmClearAttachmentDrafts] = React.useState<boolean>(false);
const [draftMenu, setDraftMenu] = React.useState<{ anchor: HTMLAnchorElement, attachmentDraftId: AttachmentDraftId } | null>(null);
const [overallMenuAnchor, setOverallMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
// derived state
const { llmAttachmentDrafts, canInlineSomeFragments } = props.llmAttachmentDrafts;
const hasAttachments = llmAttachmentDrafts.length >= 1;
// derived item menu state
const itemMenuAnchor = draftMenu?.anchor;
const itemMenuAttachmentDraftId = draftMenu?.attachmentDraftId;
const itemMenuAttachmentDraft = itemMenuAttachmentDraftId ? llmAttachmentDrafts.find(la => la.attachmentDraft.id === draftMenu.attachmentDraftId) : undefined;
const itemMenuIndex = itemMenuAttachmentDraft ? llmAttachmentDrafts.indexOf(itemMenuAttachmentDraft) : -1;
// overall menu
const { onAttachmentDraftsAction } = props;
const handleOverallMenuHide = React.useCallback(() => setOverallMenuAnchor(null), []);
const handleOverallMenuToggle = React.useCallback((event: React.MouseEvent<HTMLAnchorElement>) => {
event.shiftKey && console.log(llmAttachmentDrafts);
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
setOverallMenuAnchor(anchor => anchor ? null : event.currentTarget);
}, [llmAttachmentDrafts]);
const handleOverallCopyText = React.useCallback(() => {
handleOverallMenuHide();
onAttachmentDraftsAction(null, 'copy-text');
}, [handleOverallMenuHide, onAttachmentDraftsAction]);
const handleOverallInlineText = React.useCallback(() => {
handleOverallMenuHide();
onAttachmentDraftsAction(null, 'inline-text');
}, [handleOverallMenuHide, onAttachmentDraftsAction]);
const handleOverallClear = React.useCallback(() => setConfirmClearAttachmentDrafts(true), []);
const handleOverallClearConfirmed = React.useCallback(() => {
handleOverallMenuHide();
setConfirmClearAttachmentDrafts(false);
props.attachmentDraftsStoreApi.getState().removeAllAttachmentDrafts();
}, [handleOverallMenuHide, props.attachmentDraftsStoreApi]);
// item menu
const handleDraftMenuHide = React.useCallback(() => setDraftMenu(null), []);
const handleDraftMenuToggle = React.useCallback((attachmentDraftId: AttachmentDraftId, anchor: HTMLAnchorElement) => {
handleOverallMenuHide();
setDraftMenu(prev => prev?.attachmentDraftId === attachmentDraftId ? null : { anchor, attachmentDraftId });
}, [handleOverallMenuHide]);
const handleDraftAction = React.useCallback((attachmentDraftId: AttachmentDraftId, actionId: LLMAttachmentDraftsAction) => {
// pass-through, but close the menu as well, as the action is destructive for the caller
handleDraftMenuHide();
onAttachmentDraftsAction(attachmentDraftId, actionId);
}, [handleDraftMenuHide, onAttachmentDraftsAction]);
// no components without attachments
if (!hasAttachments)
return null;
return <>
{/* Attachment Drafts bar */}
<Box sx={{ position: 'relative' }}>
{/* Horizontally scrollable Attachments */}
<Box sx={{ display: 'flex', overflowX: 'auto', gap: 1, height: '100%', pr: 5 }}>
{llmAttachmentDrafts.map((llmAttachment) =>
<LLMAttachmentItem
key={llmAttachment.attachmentDraft.id}
llmAttachment={llmAttachment}
menuShown={llmAttachment.attachmentDraft.id === itemMenuAttachmentDraftId}
onToggleMenu={handleDraftMenuToggle}
/>,
)}
</Box>
{/* Overall Menu button */}
<IconButton
onClick={handleOverallMenuToggle}
onContextMenu={handleOverallMenuToggle}
sx={{
// borderRadius: 'sm',
borderRadius: 0,
position: 'absolute', right: 0, top: 0,
backgroundColor: 'neutral.softDisabledBg',
}}
>
<ExpandLessIcon />
</IconButton>
</Box>
{/* LLM Draft Menu */}
{!!itemMenuAnchor && !!itemMenuAttachmentDraft && !!props.attachmentDraftsStoreApi && (
<LLMAttachmentMenu
attachmentDraftsStoreApi={props.attachmentDraftsStoreApi}
llmAttachmentDraft={itemMenuAttachmentDraft}
menuAnchor={itemMenuAnchor}
isPositionFirst={itemMenuIndex === 0}
isPositionLast={itemMenuIndex === llmAttachmentDrafts.length - 1}
onDraftAction={handleDraftAction}
onClose={handleDraftMenuHide}
/>
)}
{/* All Drafts Menu */}
{!!overallMenuAnchor && (
<CloseableMenu
dense placement='top-start'
open anchorEl={overallMenuAnchor} onClose={handleOverallMenuHide}
sx={{ minWidth: 200 }}
>
<MenuItem onClick={handleOverallInlineText} disabled={!canInlineSomeFragments}>
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
Inline all text
</MenuItem>
<MenuItem onClick={handleOverallCopyText} disabled={!canInlineSomeFragments}>
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
Copy all text
</MenuItem>
<ListDivider />
<MenuItem onClick={handleOverallClear}>
<ListItemDecorator><ClearIcon /></ListItemDecorator>
Remove All{llmAttachmentDrafts.length > 5 ? <span style={{ opacity: 0.5 }}> {llmAttachmentDrafts.length} attachments</span> : null}
</MenuItem>
</CloseableMenu>
)}
{/* 'Clear' Confirmation */}
{confirmClearAttachmentDrafts && (
<ConfirmationModal
open onClose={() => setConfirmClearAttachmentDrafts(false)} onPositive={handleOverallClearConfirmed}
title='Confirm Removal'
positiveActionText='Remove All'
confirmationText={`This action will remove all (${llmAttachmentDrafts.length}) attachments. Do you want to proceed?`}
/>
)}
</>;
}
@@ -0,0 +1,58 @@
import * as React from 'react';
import { DLLM, LLM_IF_OAI_Vision } from '~/modules/llms/store-llms';
import type { AttachmentDraft } from '~/common/attachment-drafts/attachment.types';
import type { DMessageAttachmentFragment } from '~/common/stores/chat/chat.fragments';
import { estimateTokensForFragments } from '~/common/stores/chat/chat.tokens';
export interface LLMAttachmentDrafts {
llmAttachmentDrafts: LLMAttachmentDraft[];
canAttachAllFragments: boolean;
canInlineSomeFragments: boolean;
llmTokenCountApprox: number | null;
}
export interface LLMAttachmentDraft {
attachmentDraft: AttachmentDraft;
llmSupportsAllFragments: boolean;
llmSupportsTextFragments: boolean;
llmTokenCountApprox: number | null;
}
export function useLLMAttachmentDrafts(attachmentDrafts: AttachmentDraft[], chatLLM: DLLM | null): LLMAttachmentDrafts {
return React.useMemo(() => {
// LLM-dependent multi-modal enablement
const supportsImages = !!chatLLM?.interfaces?.includes(LLM_IF_OAI_Vision);
const supportedTypes: DMessageAttachmentFragment['part']['pt'][] = supportsImages ? ['image_ref', 'doc'] : ['doc'];
const supportedTextTypes: DMessageAttachmentFragment['part']['pt'][] = supportedTypes.filter(pt => pt === 'doc');
// Add LLM-specific properties to each attachment draft
const llmAttachmentDrafts = attachmentDrafts.map((a): LLMAttachmentDraft => ({
attachmentDraft: a,
llmSupportsAllFragments: !a.outputFragments ? false : a.outputFragments.every(op => supportedTypes.includes(op.part.pt)),
llmSupportsTextFragments: !a.outputFragments ? false : a.outputFragments.some(op => supportedTextTypes.includes(op.part.pt)),
llmTokenCountApprox: chatLLM
? estimateTokensForFragments(a.outputFragments, chatLLM, true, 'useLLMAttachmentDrafts')
: null,
}));
// Calculate the overall properties
const canAttachAllFragments = llmAttachmentDrafts.every(a => a.llmSupportsAllFragments);
const canInlineSomeFragments = llmAttachmentDrafts.some(a => a.llmSupportsTextFragments);
const llmTokenCountApprox = chatLLM
? llmAttachmentDrafts.reduce((acc, a) => acc + (a.llmTokenCountApprox || 0), 0)
: null;
return {
llmAttachmentDrafts,
canAttachAllFragments,
canInlineSomeFragments,
llmTokenCountApprox,
};
}, [attachmentDrafts, chatLLM]);
}
@@ -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>;
};
@@ -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, useGlobalShortcuts } from '~/common/components/useGlobalShortcuts';
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
useGlobalShortcuts([[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>
);
}
@@ -3,17 +3,17 @@ import * as React from 'react';
import { Box, Typography } from '@mui/joy';
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
import { autoConversationTitle } from '~/modules/aifn/autotitle/autoTitle';
import type { DConversationId } from '~/common/state/store-chats';
import { DConversationId } from '~/common/stores/chat/chat.conversation';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { CHAT_NOVEL_TITLE } from '../AppChat';
import { CHAT_NOVEL_TITLE } from '../../AppChat';
import { FadeInButton } from './ChatDrawerItem';
import { FadeInButton } from '../layout-drawer/ChatDrawerItem';
export function ChatTitle(props: {
export function ChatBarAltTitle(props: {
conversationId: DConversationId | null,
conversationTitle: string,
}) {
@@ -29,7 +29,7 @@ export function ChatTitle(props: {
const handleTitleEditAuto = React.useCallback(async () => {
if (!conversationId) return;
setIsEditingTitle(true);
await conversationAutoTitle(conversationId, true);
await autoConversationTitle(conversationId, true);
setIsEditingTitle(false);
}, [conversationId]);
@@ -1,13 +1,13 @@
import * as React from 'react';
import type { DConversationId } from '~/common/state/store-chats';
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
import { useChatLLMDropdown } from './useLLMDropdown';
import { usePersonaIdDropdown } from './usePersonaDropdown';
import { useFolderDropdown } from './folders/useFolderDropdown';
import { useFolderDropdown } from './useFolderDropdown';
export function ChatDropdowns(props: {
export function ChatBarDropdowns(props: {
conversationId: DConversationId | null
}) {
@@ -3,7 +3,7 @@ import * as React from 'react';
import ClearIcon from '@mui/icons-material/Clear';
import FolderIcon from '@mui/icons-material/Folder';
import type { DConversationId } from '~/common/state/store-chats';
import { DConversationId } from '~/common/stores/chat/chat.conversation';
import { DropdownItems, PageBarDropdownMemo } from '~/common/layout/optima/components/PageBarDropdown';
import { useFolderStore } from '~/common/state/store-folders';
@@ -1,13 +1,14 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';
import { SystemPurposeId, SystemPurposes } from '../../../data';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { DConversationId } from '~/common/stores/chat/chat.conversation';
import { PageBarDropdownMemo } from '~/common/layout/optima/components/PageBarDropdown';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { usePurposeStore } from './persona-selector/store-purposes';
import { usePurposeStore } from '../persona-selector/store-purposes';
function PersonaDropdown(props: {
@@ -17,9 +18,7 @@ function PersonaDropdown(props: {
// external state
const hiddenPurposeIDs = usePurposeStore(state => state.hiddenPurposeIDs);
const { zenMode } = useUIPreferencesStore(state => ({
zenMode: state.zenMode,
}), shallow);
const zenMode = useUIPreferencesStore(state => state.zenMode);
// filter by key in the object - must be missing the system purpose ids hidden by the user, or be the currently active one
@@ -54,12 +53,12 @@ function PersonaDropdown(props: {
export function usePersonaIdDropdown(conversationId: DConversationId | null) {
// external state
const { systemPurposeId } = useChatStore(state => {
const { systemPurposeId } = useChatStore(useShallow(state => {
const conversation = state.conversations.find(conversation => conversation.id === conversationId);
return {
systemPurposeId: conversation?.systemPurposeId ?? null,
};
}, shallow);
}));
const handleSetSystemPurposeId = React.useCallback((systemPurposeId: SystemPurposeId | null) => {
@@ -1,17 +1,19 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useShallow } from 'zustand/react/shallow';
import { Box, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
import { Box, Button, Dropdown, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip, Typography } from '@mui/joy';
import AddIcon from '@mui/icons-material/Add';
import CheckIcon from '@mui/icons-material/Check';
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
import ClearIcon from '@mui/icons-material/Clear';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
import FolderIcon from '@mui/icons-material/Folder';
import FormatPaintOutlinedIcon from '@mui/icons-material/FormatPaintOutlined';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
import type { DConversationId } from '~/common/state/store-chats';
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { DFolder, useFolderStore } from '~/common/state/store-folders';
import { DebounceInputMemo } from '~/common/components/DebounceInput';
@@ -26,9 +28,9 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
import { ChatDrawerItemMemo, FolderChangeRequest } from './ChatDrawerItem';
import { ChatFolderList } from './folders/ChatFolderList';
import { ChatNavGrouping, useChatNavRenderItems } from './useChatNavRenderItems';
import { ClearFolderText } from './folders/useFolderDropdown';
import { useChatShowRelativeSize } from '../store-app-chat';
import { ChatNavGrouping, ChatSearchSorting, isDrawerSearching, useChatDrawerRenderItems } from './useChatDrawerRenderItems';
import { ClearFolderText } from '../layout-bar/useFolderDropdown';
import { useChatDrawerFilters } from '../../store-app-chat';
// this is here to make shallow comparisons work on the next hook
@@ -37,7 +39,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 +52,7 @@ export const useFolders = (activeFolderId: string | null) => useFolderStore(({ e
enableFolders,
toggleEnableFolders,
};
}, shallow);
}));
export const ChatDrawerMemo = React.memo(ChatDrawer);
@@ -74,20 +76,26 @@ function ChatDrawer(props: {
// local state
const [navGrouping, setNavGrouping] = React.useState<ChatNavGrouping>('date');
const [searchSorting, setSearchSorting] = React.useState<ChatSearchSorting>('date');
const [debouncedSearchQuery, setDebouncedSearchQuery] = React.useState('');
const [folderChangeRequest, setFolderChangeRequest] = React.useState<FolderChangeRequest | null>(null);
// external state
const { closeDrawer, closeDrawerOnMobile } = useOptimaDrawers();
const { showRelativeSize, toggleRelativeSize } = useChatShowRelativeSize();
const {
filterHasStars, toggleFilterHasStars,
filterHasImageAssets, toggleFilterHasImageAssets,
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, filterHasImageAssets, navGrouping, searchSorting, showRelativeSize,
);
const { contentScaling, showSymbols } = useUIPreferencesStore(state => ({
const { contentScaling, showSymbols } = useUIPreferencesStore(useShallow(state => ({
contentScaling: state.contentScaling,
showSymbols: state.zenMode !== 'cleaner',
}), shallow);
})));
// New/Activate/Delete Conversation
@@ -140,6 +148,7 @@ function ChatDrawer(props: {
// memoize the group dropdown
const { isSearching } = isDrawerSearching(debouncedSearchQuery);
const groupingComponent = React.useMemo(() => (
<Dropdown>
<MenuButton
@@ -147,34 +156,74 @@ 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: 200, zIndex: themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */ }}>
<ListItem>
<Typography level='body-sm'>Group By</Typography>
</ListItem>
{(['date', 'persona', 'dimension'] as Exclude<ChatNavGrouping, false>[]).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>
<MenuItem onClick={toggleFilterHasImageAssets}>
<ListItemDecorator>{filterHasImageAssets && <CheckRoundedIcon />}</ListItemDecorator>
Has Images <FormatPaintOutlinedIcon />
</MenuItem>
<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]);
), [
filterHasImageAssets, filterHasStars, isSearching, navGrouping, searchSorting, showPersonaIcons, showRelativeSize,
toggleFilterHasImageAssets, toggleFilterHasStars, toggleShowPersonaIcons, toggleShowRelativeSize,
]);
return <>
@@ -182,13 +231,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 +254,12 @@ function ChatDrawer(props: {
contentScaling={contentScaling}
activeFolderId={props.activeFolderId}
onFolderSelect={props.setActiveFolderId}
sx={{
// shrink this at twice the rate as the Titles list
flexGrow: 0, flexShrink: 2, overflow: 'hidden',
minHeight: '7.5rem',
p: 2,
}}
/>
)}
{/*</Box>*/}
@@ -214,81 +269,81 @@ function ChatDrawer(props: {
{enableFolders && <ListDivider sx={{ mb: 0 }} />}
{/* Search Input Field */}
<DebounceInputMemo
minChars={2}
onDebounce={setDebouncedSearchQuery}
debounceTimeout={300}
placeholder='Search...'
aria-label='Search'
endDecorator={groupingComponent}
sx={{ m: 2 }}
/>
{/* Search / New Chat */}
<Box sx={{ display: 'flex', flexDirection: 'column', m: 2, gap: 2 }}>
{/* New Chat Button */}
<ListItem sx={{ mx: '0.25rem', mb: 0.5 }}>
<ListItemButton
{/* Search Input Field */}
<DebounceInputMemo
minChars={2}
onDebounce={setDebouncedSearchQuery}
debounceTimeout={300}
placeholder='Search...'
aria-label='Search'
endDecorator={groupingComponent}
/>
{/* New Chat Button */}
<Button
// variant='outlined'
variant={disableNewButton ? undefined : 'outlined'}
variant={disableNewButton ? undefined : 'soft'}
disabled={disableNewButton}
onClick={handleButtonNew}
sx={{
// ...PageDrawerTallItemSx,
px: 'calc(var(--ListItem-paddingX) - 0.25rem)',
// text size
fontSize: 'sm',
fontWeight: 'lg',
justifyContent: 'flex-start',
padding: '0px 0.75rem',
// style
borderRadius: 'md',
boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'sm',
backgroundColor: 'background.popup',
transition: 'box-shadow 0.2s',
border: '1px solid',
borderColor: 'neutral.outlinedBorder',
borderRadius: 'sm',
'--ListItemDecorator-size': 'calc(2.5rem - 1px)', // compensate for the border
// backgroundColor: 'background.popup',
// boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'xs',
// transition: 'box-shadow 0.2s',
}}
>
<ListItemDecorator><AddIcon sx={{ '--Icon-fontSize': 'var(--joy-fontSize-xl)', pl: '0.125rem' }} /></ListItemDecorator>
<ListItemDecorator><AddIcon sx={{ fontSize: '' }} /></ListItemDecorator>
New chat
</ListItemButton>
</ListItem>
</Button>
{/*<ListDivider sx={{ mt: 0 }} />*/}
{/* List of Chat Titles (and actions) */}
<Box sx={{ flex: 1, overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
{/*<ListItem sticky sx={{ justifyContent: 'space-between', boxShadow: 'sm' }}>*/}
{/* <Typography level='body-sm'>*/}
{/* Conversations*/}
{/* </Typography>*/}
{/* <ToggleButtonGroup variant='soft' size='sm' value={grouping} onChange={(_event, newValue) => newValue && setGrouping(newValue)}>*/}
{/* <IconButton value='off'>*/}
{/* <AccessTimeIcon />*/}
{/* </IconButton>*/}
{/* <IconButton value='persona'>*/}
{/* <PersonIcon />*/}
{/* </IconButton>*/}
{/* </ToggleButtonGroup>*/}
{/*</ListItem>*/}
</Box>
{/* Chat Titles List (shrink as half the rate as the Folders List) */}
<Box sx={{ flexGrow: 1, flexShrink: 1, flexBasis: '20rem', overflowY: 'auto', ...themeScalingMap[contentScaling].chatDrawerItemSx }}>
{renderNavItems.map((item, idx) => item.type === 'nav-item-chat-data' ? (
<ChatDrawerItemMemo
key={'nav-chat-' + item.conversationId}
item={item}
showSymbols={showSymbols}
showSymbols={showPersonaIcons && showSymbols}
bottomBarBasis={filteredChatsBarBasis}
onConversationActivate={handleConversationActivate}
onConversationBranch={onConversationBranch}
onConversationDelete={handleConversationDeleteNoConfirmation}
onConversationDeleteNoConfirmation={handleConversationDeleteNoConfirmation}
onConversationExport={onConversationsExportDialog}
onConversationFolderChange={handleConversationFolderChange}
/>
) : item.type === 'nav-item-group' ? (
<Typography key={'nav-divider-' + idx} level='body-xs' sx={{ textAlign: 'center', my: 'calc(var(--ListItem-minHeight) / 4)' }}>
<Typography key={'nav-divider-' + idx} level='body-xs' sx={{
textAlign: 'center',
my: 'calc(var(--ListItem-minHeight) / 4)',
// keeps the group header sticky to the top
position: 'sticky',
top: 0,
backgroundColor: 'background.popup',
zIndex: 1,
}}>
{item.title}
</Typography>
) : item.type === 'nav-item-info-message' ? (
<Typography key={'nav-info-' + idx} level='body-xs' sx={{ textAlign: 'center', my: 'calc(var(--ListItem-minHeight) / 2)' }}>
<Typography key={'nav-info-' + idx} level='body-xs' sx={{ textAlign: 'center', color: 'primary.softColor', my: 'calc(var(--ListItem-minHeight) / 4)' }}>
{filterHasStars && <StarOutlineRoundedIcon sx={{ color: 'primary.softColor', fontSize: 'xl', mb: -0.5, mr: 1 }} />}
{item.message}
{filterHasStars && <>
<Button variant='soft' size='sm' onClick={toggleFilterHasStars} sx={{ display: 'block', mt: 2, mx: 'auto' }}>
remove filters
</Button>
</>}
</Typography>
) : null,
)}
@@ -296,7 +351,8 @@ function ChatDrawer(props: {
<ListDivider sx={{ my: 0 }} />
<Box sx={{ display: 'flex', alignItems: 'center' }}>
{/* Bottom commands */}
<Box sx={{ flexShrink: 0, display: 'flex', alignItems: 'center' }}>
<ListItemButton onClick={props.onConversationsImportDialog} sx={{ flex: 1 }}>
<ListItemDecorator>
<FileUploadOutlinedIcon />
@@ -5,22 +5,24 @@ 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';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import { SystemPurposeId, SystemPurposes } from '../../../data';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
import { autoConversationTitle } from '~/modules/aifn/autotitle/autoTitle';
import type { DConversationId } from '~/common/stores/chat/chat.conversation';
import type { DFolder } from '~/common/state/store-folders';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { InlineTextarea } from '~/common/components/InlineTextarea';
import { isDeepEqual } from '~/common/util/jsUtils';
import { useChatStore } from '~/common/stores/chat/store-chats';
import { CHAT_NOVEL_TITLE } from '../AppChat';
import { ANIM_BUSY_TYPING } from '../message/messageUtils';
import { CHAT_NOVEL_TITLE } from '../../AppChat';
// set to true to display the conversation IDs
@@ -41,7 +43,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,10 +55,13 @@ export interface ChatNavigationItemData {
isAlsoOpen: string | false;
isEmpty: boolean;
title: string;
userSymbol: string | undefined;
userFlagsSummary: string | undefined;
containsImageAssets: boolean;
folder: DFolder | null | undefined; // null: 'All', undefined: do not show folder select
updatedAt: number;
messageCount: number;
assistantTyping: boolean;
beingGenerated: boolean;
systemPurposeId: SystemPurposeId;
searchFrequency: number;
}
@@ -74,7 +79,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 +91,20 @@ 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,
userSymbol,
userFlagsSummary,
containsImageAssets,
folder,
messageCount,
beingGenerated,
systemPurposeId,
searchFrequency,
} = props.item;
const isNew = messageCount === 0;
@@ -146,14 +164,23 @@ function ChatDrawerItem(props: {
const handleTitleEditAuto = React.useCallback(async () => {
setIsAutoEditingTitle(true);
await conversationAutoTitle(conversationId, true);
await autoConversationTitle(conversationId, true);
setIsAutoEditingTitle(false);
}, [conversationId]);
// 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) { // immediately delete:conversation
event.stopPropagation();
onConversationDeleteNoConfirmation(conversationId);
return;
}
setDeleteArmed(true);
}, [conversationId, onConversationDeleteNoConfirmation]);
const handleDeleteButtonHide = React.useCallback(() => setDeleteArmed(false), []);
@@ -161,12 +188,12 @@ 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 || '❓';
const textSymbol = userSymbol || SystemPurposes[systemPurposeId]?.symbol || '❓';
const progress = props.bottomBarBasis ? 100 * (searchFrequency || messageCount) / props.bottomBarBasis : 0;
@@ -174,11 +201,11 @@ function ChatDrawerItem(props: {
{/* Symbol, if globally enabled */}
{props.showSymbols && <ListItemDecorator>
{assistantTyping
{beingGenerated
? (
<Avatar
alt='typing' variant='plain'
src='https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'
alt='activity' variant='plain'
src={ANIM_BUSY_TYPING}
sx={{
width: '1.5rem',
height: '1.5rem',
@@ -200,11 +227,12 @@ function ChatDrawerItem(props: {
onDoubleClick={handleTitleEditBegin}
sx={{
color: isActive ? 'text.primary' : 'text.secondary',
overflowWrap: 'anywhere',
flex: 1,
}}
>
{/*{DEBUG_CONVERSATION_IDS && `${conversationId} - `}*/}
{title.trim() ? title : CHAT_NOVEL_TITLE}{assistantTyping && '...'}
{title.trim() ? title : CHAT_NOVEL_TITLE}{beingGenerated && ' ...'}
</Box>
) : (
<InlineTextarea
@@ -219,16 +247,26 @@ 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>
{/* Right text */}
{searchFrequency > 0 ? (
// Display search frequency if it exists and is greater than 0
<Typography level='body-sm'>
{searchFrequency}
</Typography>
) : (props.showSymbols && (userFlagsSummary || containsImageAssets)) ? (
<Box sx={{
fontSize: 'xs',
whiteSpace: 'nowrap',
pointerEvents: 'none',
}}>
{userFlagsSummary}{containsImageAssets && '🖍️'}
</Box>
)}
) : null}
</>, [assistantTyping, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive, isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title]);
</>, [
beingGenerated, containsImageAssets, handleTitleEditBegin, handleTitleEditCancel, handleTitleEditChange, isActive,
isEditingTitle, isNew, props.showSymbols, searchFrequency, textSymbol, title, userFlagsSummary,
]);
const progressBarFixedComponent = React.useMemo(() =>
progress > 0 && (
@@ -259,6 +297,7 @@ function ChatDrawerItem(props: {
}),
// style
fontSize: 'inherit',
backgroundColor: isActive ? 'neutral.solidActiveBg' : 'neutral.softBg',
borderRadius: 'md',
mx: '0.25rem',
@@ -278,7 +317,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 +339,7 @@ function ChatDrawerItem(props: {
<Tooltip disableInteractive title='Rename'>
<FadeInButton size='sm' disabled={isEditingTitle || isAutoEditingTitle} onClick={handleTitleEditBegin}>
<EditIcon />
<EditRoundedIcon />
</FadeInButton>
</Tooltip>
@@ -311,7 +350,7 @@ function ChatDrawerItem(props: {
</FadeInButton>
</Tooltip>
<Tooltip disableInteractive title='Branch'>
<Tooltip disableInteractive title='Duplicate (Branch)'>
<FadeInButton size='sm' onClick={handleConversationBranch}>
<ForkRightIcon />
</FadeInButton>
@@ -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>
@@ -0,0 +1,283 @@
import { shallow } from 'zustand/shallow';
import { useStoreWithEqualityFn } from 'zustand/traditional';
import type { DFolder } from '~/common/state/store-folders';
import { DMessage, DMessageUserFlag, messageFragmentsReduceText, messageHasUserFlag, messageUserFlagToEmoji } from '~/common/stores/chat/chat.message';
import { conversationTitle, DConversationId } from '~/common/stores/chat/chat.conversation';
import { isContentOrAttachmentFragment, isImageRefPart } from '~/common/stores/chat/chat.fragments';
import { useChatStore } from '~/common/stores/chat/store-chats';
import type { ChatNavigationItemData } from './ChatDrawerItem';
// configuration
const SEARCH_MIN_CHARS = 3;
export type ChatNavGrouping = false | 'date' | 'persona' | 'dimension';
export type ChatSearchSorting = 'frequency' | 'date';
interface ChatNavigationGroupData {
type: 'nav-item-group',
title: string,
}
interface ChatNavigationInfoMessage {
type: 'nav-item-info-message',
message: string,
}
type ChatRenderItemData = ChatNavigationItemData | ChatNavigationGroupData | ChatNavigationInfoMessage;
// Returns a string with the pane indices where the conversation is also open, or false if it's not
function findOpenInViewNumbers(chatPanesConversationIds: DConversationId[], ourId: DConversationId): string | false {
if (chatPanesConversationIds.length <= 1) return false;
return chatPanesConversationIds.reduce((acc: string[], id, idx) => {
if (id === ourId)
acc.push((idx + 1).toString());
return acc;
}, []).join(', ') || false;
}
function getNextMidnightTime(): number {
const midnight = new Date();
// midnight.setDate(midnight.getDate() - 1);
midnight.setHours(24, 0, 0, 0);
return midnight.getTime();
}
function getTimeBucketEn(currentTime: number, midnightTime: number): string {
const oneDay = 24 * 60 * 60 * 1000;
const oneWeek = oneDay * 7;
const oneMonth = oneDay * 30; // approximation
const diff = midnightTime - currentTime;
if (diff < oneDay) {
return 'Today';
} else if (diff < oneDay * 2) {
return 'Yesterday';
} else if (diff < oneWeek) {
return 'This Week';
} else if (diff < oneWeek * 2) {
return 'Last Week';
} else if (diff < oneMonth) {
return 'This Month';
} else if (diff < oneMonth * 2) {
return 'Last Month';
} else {
return 'Older';
}
}
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 useChatDrawerRenderItems(
activeConversationId: DConversationId | null,
chatPanesConversationIds: DConversationId[],
filterByQuery: string,
activeFolder: DFolder | null,
allFolders: DFolder[],
filterHasStars: boolean,
filterHasImageAssets: boolean,
grouping: ChatNavGrouping,
searchSorting: ChatSearchSorting,
showRelativeSize: boolean,
): {
renderNavItems: ChatRenderItemData[],
filteredChatIDs: DConversationId[],
filteredChatsCount: number,
filteredChatsAreEmpty: boolean,
filteredChatsBarBasis: number,
filteredChatsIncludeActive: boolean,
} {
return useStoreWithEqualityFn(useChatStore, ({ conversations }) => {
// filter 1: select all conversations or just the ones in the active folder
const selectedConversations = !activeFolder ? conversations : conversations.filter(_c => activeFolder.conversationIds.includes(_c.id));
// filter 2: preparation: lowercase the query
const { isSearching, lcTextQuery } = isDrawerSearching(filterByQuery);
function messageHasImageFragments(message: DMessage): boolean {
return message.fragments.some(fragment => isContentOrAttachmentFragment(fragment) && isImageRefPart(fragment.part) /*&& fragment.part.dataRef.reftype === 'dblob'*/);
}
// transform (the conversations into ChatNavigationItemData) + filter2 (if searching)
const chatNavItems = selectedConversations
.filter(_c => !filterHasStars || _c.messages.some(m => messageHasUserFlag(m, 'starred')))
.filter(_c => !filterHasImageAssets || _c.messages.some(messageHasImageFragments))
.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) => {
return count + messageFragmentsReduceText(message.fragments).toLowerCase().split(lcTextQuery).length - 1;
}, 0);
searchFrequency = titleFrequency + messageFrequency;
}
// 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('');
const containsImageAssets = filterHasImageAssets || _c.messages.some(messageHasImageFragments);
// create the ChatNavigationData
return {
type: 'nav-item-chat-data',
conversationId: _c.id,
isActive: _c.id === activeConversationId,
isAlsoOpen,
isEmpty: !_c.messages.length && !_c.userTitle,
title,
userSymbol: _c.userSymbol || undefined,
userFlagsSummary,
containsImageAssets,
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,
beingGenerated: !!_c.abortController, // FIXME: when the AbortController is moved at the message level, derive the state in the conv
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
if (isSearching && searchSorting === 'frequency')
chatNavItems.sort((a, b) => b.searchFrequency - a.searchFrequency);
// Render List
let renderNavItems: ChatRenderItemData[] = chatNavItems;
// [search] add a header if searching
if (isSearching) {
// only prepend a 'Results' group if there are results
if (chatNavItems.length)
renderNavItems = [{ type: 'nav-item-group', title: 'Search results' }, ...chatNavItems];
}
// [grouping] group by date or persona
else if (grouping) {
switch (grouping) {
// [grouping/date or persona]: sort by last updated
case 'date':
case 'persona':
chatNavItems.sort((a, b) => b.updatedAt - a.updatedAt);
break;
// [grouping/dimension]: sort by message count
case 'dimension':
chatNavItems.sort((a, b) => b.messageCount - a.messageCount);
break;
}
const midnightTime = getNextMidnightTime();
const grouped = chatNavItems.reduce((acc, item) => {
// derive the bucket name
let bucket: string;
switch (grouping) {
case 'date':
bucket = getTimeBucketEn(item.updatedAt || midnightTime, midnightTime);
break;
case 'persona':
bucket = item.systemPurposeId;
break;
case 'dimension':
if (item.messageCount > 20)
bucket = 'Large chats';
else if (item.messageCount > 10)
bucket = 'Medium chats';
else if (item.messageCount > 5)
bucket = 'Small chats';
else if (item.messageCount > 1)
bucket = 'Tiny chats';
else if (item.messageCount === 1)
bucket = 'Single message';
else
bucket = 'Empty chats';
break;
}
if (!acc[bucket])
acc[bucket] = [];
acc[bucket].push(item);
return acc;
}, {} as { [groupName: string]: ChatNavigationItemData[] });
// prepend group names as special items
renderNavItems = Object.entries(grouped).flatMap(([groupName, items]) => [
{ type: 'nav-item-group', title: groupName },
...items,
]);
}
// [empty message] if there are no items
if (!renderNavItems.length)
renderNavItems.push({
type: 'nav-item-info-message',
message: (filterHasStars && filterHasImageAssets) ? 'No starred results with images'
: filterHasImageAssets ? 'No image results'
: filterHasStars ? 'No starred results'
: isSearching ? 'No results found'
: 'No conversations in folder',
});
// other derived state
const filteredChatIDs = chatNavItems.map(_c => _c.conversationId);
const filteredChatsCount = chatNavItems.length;
const filteredChatsAreEmpty = !filteredChatsCount || (filteredChatsCount === 1 && chatNavItems[0].isEmpty);
const filteredChatsBarBasis = ((showRelativeSize && filteredChatsCount >= 2) || isSearching)
? chatNavItems.reduce((longest, _c) => Math.max(longest, isSearching ? _c.searchFrequency : _c.messageCount), 1)
: 0;
return {
renderNavItems,
filteredChatIDs,
filteredChatsCount,
filteredChatsAreEmpty,
filteredChatsBarBasis,
filteredChatsIncludeActive,
};
},
(a, b) => {
// we only compare the renderNavItems array, which shall be changed if the rest changes
return a.renderNavItems.length === b.renderNavItems.length
&& a.renderNavItems.every((_a, i) => shallow(_a, b.renderNavItems[i]))
&& shallow(a.filteredChatIDs, b.filteredChatIDs)
&& a.filteredChatsCount === b.filteredChatsCount
&& a.filteredChatsAreEmpty === b.filteredChatsAreEmpty
&& a.filteredChatsBarBasis === b.filteredChatsBarBasis
&& a.filteredChatsIncludeActive === b.filteredChatsIncludeActive;
},
);
}

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