Compare commits

...

318 Commits

Author SHA1 Message Date
Enrico Ros ff4857b9ac Merge branch 'release-1.11.0' 2024-01-15 22:11:12 -08:00
Enrico Ros 5b557705e7 1.11.0: Readme and Changelog 2024-01-15 22:10:39 -08:00
Enrico Ros cd70c4dd84 1.11.0: news.data.tsx 2024-01-15 21:39:14 -08:00
Enrico Ros 9eb2ef05de 1.11.0: Version 2024-01-15 17:07:32 -08:00
Enrico Ros 8fae15d343 Together AI: improve icon 2024-01-15 16:42:13 -08:00
Enrico Ros bca5a1ac78 Update vendors count 2024-01-15 16:02:03 -08:00
Enrico Ros d899fb7e3b Persona Creator Drawer: selection mode 2024-01-15 16:00:29 -08:00
Enrico Ros 0f05b70e3b Bits 2024-01-15 15:01:22 -08:00
Enrico Ros 7b121a3a95 Together AI: implement free-tier rate limiting 2024-01-15 14:49:45 -08:00
Enrico Ros d4e414f99c Together AI: add popular models (with context window sizes) 2024-01-15 14:23:43 -08:00
Enrico Ros a7f322ef38 Together AI Vendor support 2024-01-15 14:10:21 -08:00
Enrico Ros d4494bf2e0 OpenAI transports: do not include n=1 in the payload 2024-01-11 09:35:09 -08:00
Enrico Ros 78cf74e3f2 Persona Creator: Drawer/Drawer Items - storage OK. Closes #301 2024-01-10 02:57:03 -08:00
Enrico Ros cfaed03603 PageDrawerList: add onClick for list callbacks 2024-01-10 02:52:29 -08:00
Enrico Ros a8e3183733 Persona Creator: store 2024-01-10 01:48:34 -08:00
Enrico Ros 9395db0fd5 Persona Creator: move Creator stuff to ./creator 2024-01-10 00:26:03 -08:00
Enrico Ros 8c75061178 Move useFormEditTextArray 2024-01-10 00:23:24 -08:00
Enrico Ros de0cdded87 Persona Creator: move the YouTube module 2024-01-10 00:23:11 -08:00
Enrico Ros d225541da2 bits 2024-01-09 23:13:06 -08:00
Enrico Ros 7a0008de5a Move useLLMChain 2024-01-09 22:57:46 -08:00
Enrico Ros 0bdd817d6d Persona Creator: bits 2024-01-09 21:48:19 -08:00
Enrico Ros d606975584 Persona Creator: improve LLM selection 2024-01-09 21:22:05 -08:00
Enrico Ros af56c2c1af GoodDropdown -> PageBarDropdown 2024-01-09 20:14:23 -08:00
Enrico Ros 73de7df0fb Mobile Nav: add Personas 2024-01-09 19:56:07 -08:00
Enrico Ros 3ca80d6a6e This is much better 2024-01-09 19:43:36 -08:00
Enrico Ros eb9e5362fe Begin reducing LLMs dependencies 2024-01-09 19:42:07 -08:00
Enrico Ros 45d1ca7437 PersonaCreator: debug (find issues) 2024-01-09 15:22:01 -08:00
Enrico Ros e492ccfb04 Improve the useLLMChain hook 2024-01-09 15:20:39 -08:00
Enrico Ros d01b6acd51 Persona Creation: enable user prompts, fixes #336 2024-01-09 04:35:32 -08:00
Enrico Ros eec81d5d73 Persona Creation: improve layout 2024-01-09 03:28:08 -08:00
Enrico Ros 03423ce58c Persona Creation: improve progress 2024-01-09 02:36:55 -08:00
Enrico Ros e2e7ea972d Persona Creation: use cancelable streaming, - Fixes #316, #328. 2024-01-09 02:31:04 -08:00
Enrico Ros 91b770d2c8 Persona Creation: extract the Tabs 2024-01-09 00:56:48 -08:00
Enrico Ros 79500e6d8b Persona Creation: extract YouTube Transcript downloader 2024-01-09 00:30:18 -08:00
Enrico Ros 4ede66cf2b Improve OpenAI API Endpoint Tooltip #323 2024-01-08 21:00:59 -08:00
Enrico Ros 40bff32442 Allow up to 5 OpenAI Endpoints. Fixes #323 2024-01-08 20:49:30 -08:00
Enrico Ros 3fc8e8efa0 LLM Source re-numbering, #323 2024-01-08 20:30:45 -08:00
Enrico Ros 12ea5f218d LLM auto-selection: ignore hidden, unless there's nothing else 2024-01-08 19:47:04 -08:00
Enrico Ros d47c0e45af AutoTitle: fix exception when an immediate call to chat-gen fails 2024-01-08 19:44:06 -08:00
Enrico Ros 298d0201d2 (disabled) Folder reveal animation 2024-01-08 19:24:53 -08:00
Enrico Ros a6bde2377e Reduce MenuList usage 2024-01-08 19:24:53 -08:00
Enrico Ros 76778c5ab7 Action Tiles framework - for commands and attachments 2024-01-08 00:42:54 -08:00
Enrico Ros 11565f5ac8 Commands: add arguments 2024-01-08 00:42:49 -08:00
Enrico Ros 6c5131996b Drawer width: less than half a percent skinnier 2024-01-07 22:14:33 -08:00
Enrico Ros 9b4301cd90 Export: undo the flip 2024-01-07 22:05:46 -08:00
Enrico Ros c73bbaf0d4 Chat Drawer Item: frequency as bar basis, and move the frequency count at the env (stable items) 2024-01-07 22:01:37 -08:00
Enrico Ros 163257e052 Bits 2024-01-07 21:49:30 -08:00
Enrico Ros cf689ca9a9 Chat Titles fixes 2024-01-07 21:41:15 -08:00
Enrico Ros 4a65389b71 Mobile Chat Drawer: do not close when clicking the active item 2024-01-07 21:22:59 -08:00
Enrico Ros 5de7762238 Fix a layout bug introduced moving away from MenuList to List 2024-01-07 21:10:58 -08:00
Enrico Ros 06655ced46 Title Edit - cancellation 2024-01-07 13:39:35 -08:00
Enrico Ros 60a775b869 Fix keyboard de-focus on Search chats.
Move away from MenuList to List - as the Menu does some focus stealing behind the scenes.
Will minimize or remove MenuList usage going forward.
2024-01-07 13:29:45 -08:00
Enrico Ros 5a3645bd43 Merge pull request #330 from joriskalz/dev-fixes
Refactored DebounceInput as external component, added clear input functionality with keyboard navigation support
2024-01-07 12:37:05 -08:00
Joris Kalz 54d37e663a Create component and add clear icon for search input 2024-01-07 12:26:42 +01:00
Enrico Ros f4c056fa9f Update README.md 2024-01-06 10:18:37 -08:00
Enrico Ros 8f53fa7407 Update README.md 2024-01-06 10:17:22 -08:00
Enrico Ros 2f9a4ea00f Merge pull request #329 from joriskalz/main
Enhanced Search: Frequency Ranking and In-Message Querying #324
2024-01-06 03:33:07 -08:00
Joris Kalz ee7dae827e Merge branch 'enricoros:main' into main 2024-01-06 12:23:24 +01:00
Joris Kalz 6fe94e344a Show number of results 2024-01-06 12:20:42 +01:00
Joris Kalz 3376867966 Debounced Input field 300ms 2024-01-06 12:05:31 +01:00
Enrico Ros 4a8a2b9c5d First user experience - highlight the need to configure models 2024-01-06 02:57:05 -08:00
Joris Kalz 7f84160a62 Enable Search 2024-01-06 11:55:26 +01:00
Enrico Ros fb5b349866 News: improve 2024-01-06 02:22:10 -08:00
Enrico Ros f5c7b96ff6 Chat Drawer: Import and Export 2024-01-06 01:37:40 -08:00
Enrico Ros 7c430cc5c8 Export: improve dialog 2024-01-06 01:29:45 -08:00
Enrico Ros 8c7d069189 Style: Chat List: soft instead of solid - for now 2024-01-06 00:57:44 -08:00
Enrico Ros f50d040d8a Update maintainers/release 2024-01-06 00:39:54 -08:00
Enrico Ros aa10f87c7d Update maintainers/release 2024-01-06 00:39:06 -08:00
Enrico Ros 4e96a5b5e5 Merge branch 'release-1.10.0' 2024-01-05 23:08:59 -08:00
Enrico Ros 329456f287 1.10.0: README/Changelog 2024-01-05 23:08:30 -08:00
Enrico Ros 6f8368d7cb 1.10.0: news.data.tsx 2024-01-05 23:00:43 -08:00
Enrico Ros 9c2b0cb7ca 1.10.0: version 2024-01-05 22:18:16 -08:00
Enrico Ros 1e15c4c4d1 Auto-Scrolling to the bottom on /link/chat. Fixes #319 2024-01-05 22:00:42 -08:00
Enrico Ros 9f209526a0 Update bug template 2024-01-05 21:50:42 -08:00
Enrico Ros 60ab9bd239 Update bug template 2024-01-05 21:50:08 -08:00
Enrico Ros 70e51b2e71 Trying out the Vercel Speed Insights functionality on Vercel deployments. +3kb 2024-01-05 21:47:36 -08:00
Enrico Ros 2d6edde12c Ani: Revert Bits 2024-01-04 11:48:55 -08:00
Enrico Ros d2fb0c2425 Ani: Bits 2024-01-04 11:47:48 -08:00
Enrico Ros 122bbf0034 LLMs: make maxTokens optional 2024-01-04 03:38:40 -08:00
Enrico Ros e79449b38c OpenAI Transport: make maxTokens optional 2024-01-04 03:38:22 -08:00
Enrico Ros fcad6495e1 Anthropic: relax max tokens 2024-01-04 03:38:06 -08:00
Enrico Ros 330d35a24c LM Studio: improve model name 2024-01-04 03:36:38 -08:00
Enrico Ros a8ec58c732 Fix mouse jumpiness on avatar icon and improve spacing 2024-01-04 02:14:37 -08:00
Enrico Ros 8054c8b328 Cleanup 2024-01-04 02:07:40 -08:00
Enrico Ros 7d6f2317e4 Clenup mobile Nav, and social links 2024-01-04 02:07:25 -08:00
Enrico Ros 10dd83bb2b Mobile Nav: make it dynamic 2024-01-04 01:43:04 -08:00
Enrico Ros 7bf285f26a Bits 2024-01-04 01:07:37 -08:00
Enrico Ros fde7a8cd9b Style: improve dark color scheme, with consistent shading 2024-01-04 01:07:32 -08:00
Enrico Ros 49ae5abba5 Dark theme improvement. Much better bars. 2024-01-04 00:53:18 -08:00
Enrico Ros f50ae4e7e2 Persona Creator: slight cleanup 2024-01-04 00:34:22 -08:00
Enrico Ros 99ff5cd7ad Persona Creator: render as markdown 2024-01-03 23:57:22 -08:00
Enrico Ros f80facb191 LLMs: support context window/max tokens not provided, and handle 'fallbacks' more explicitly 2024-01-03 23:38:01 -08:00
Enrico Ros ea8d2fff3e Fix parsing of OpenAI tokens message 2024-01-03 22:55:11 -08:00
Enrico Ros e3f1a5c54d LM Studio: actually, don't replace the hyphen 2024-01-03 22:22:57 -08:00
Enrico Ros fdafc1207b Re-rank local model providers 2024-01-03 22:21:43 -08:00
Enrico Ros 5d3971c21f Support LM Studio 2024-01-03 22:21:31 -08:00
Enrico Ros f8a4002a41 Fix Folder options on Mobile, #321 2024-01-03 16:16:39 -08:00
Enrico Ros 38a3eeef21 Folders: Toggle support.
This makes sure the folders can be disabled with a single button press in
case there are unexpected issues. Will get user testing and feedback.
Also very important on mobile, where the "select folder" UX
component makes the toolbar wrap.
2024-01-03 15:32:40 -08:00
Enrico Ros bf54807fb2 New UI: Improve the 'new title' 2024-01-03 15:15:23 -08:00
Enrico Ros 1aaabec28f New UI: Improve the 'new chat' button 2024-01-03 15:15:14 -08:00
Enrico Ros 8ec3927f02 New UI: Drawer: extract the PageDrawerHeader 2024-01-03 15:10:22 -08:00
Enrico Ros 73f201b8ac Hand-optimize the Chat items, for faster display and avoid refresh-while-type 2024-01-03 06:38:05 -08:00
Enrico Ros 0b61c9a49e Folders: use our Closeable menus instead of the Dropdown 2024-01-03 05:31:01 -08:00
Enrico Ros ee82911d8f Merge branch 'joriskalz-folders'
# Conflicts:
#	src/common/layout/optima/PageDrawer.tsx
2024-01-03 05:08:42 -08:00
Enrico Ros 89fa3fe633 New UI: Improve Drawer Names 2024-01-03 05:03:03 -08:00
Enrico Ros da56db7502 New UI: show the back arrow on desktop/no-nav 2024-01-03 05:03:01 -08:00
Enrico Ros 1d0f99a9a5 New UI: fix bug with desktop h-layout 2024-01-03 05:02:58 -08:00
Enrico Ros 8254443d29 New UI: Popups: denser 'dense' looks 2024-01-03 05:02:54 -08:00
Enrico Ros e1d6536102 New UI: Nav: support /link/chat 2024-01-03 05:02:50 -08:00
Enrico Ros c9fbbc1ab1 Folders: Complete review 2024-01-03 04:59:22 -08:00
Enrico Ros ae2e9b8f56 Folders: AppChat - review, simplify 2024-01-03 04:13:52 -08:00
Enrico Ros 64ca896ea7 Folders: store: cleanup, looks good 2024-01-03 03:15:04 -08:00
Enrico Ros 9bed685fe2 Folders: dropdown: ability to remove a folder association 2024-01-03 03:14:29 -08:00
Enrico Ros 9432084342 Folders: pre-select a folder color 2024-01-03 02:42:19 -08:00
Enrico Ros 0b7ffd16ab Folders: Reuse InlineTextArea both in New Folder & Edit Title. 2024-01-03 02:18:28 -08:00
Enrico Ros 3437888bf4 Folders: Style More: AddFolderButton 2024-01-03 02:14:41 -08:00
Enrico Ros 9b02be8861 Folders: Style: AddFolderButton and ChatFolderList 2024-01-03 01:40:28 -08:00
Enrico Ros 953d8434c3 Folders: Style: auto-size 2024-01-03 00:19:19 -08:00
Enrico Ros f9484ee3e9 Folders: Style: re-z-order 2024-01-03 00:11:34 -08:00
Enrico Ros 4a3956d743 Folders: Style: transfer shadow 2024-01-03 00:09:50 -08:00
Enrico Ros 785139e7bc New UI: Improve Drawer Names 2024-01-03 00:06:40 -08:00
Enrico Ros d45fbff28d New UI: show the back arrow on desktop/no-nav 2024-01-03 00:06:17 -08:00
Enrico Ros fce6ecaf5f New UI: fix bug with desktop h-layout 2024-01-02 23:58:34 -08:00
Enrico Ros 847d199dd8 New UI: Popups: denser 'dense' looks 2024-01-02 23:53:03 -08:00
Enrico Ros 274525a727 New UI: Nav: support /link/chat 2024-01-02 23:52:25 -08:00
Enrico Ros 4d807ecf5c New UI: transfer App Drawer lists into the Plugged 2024-01-02 23:49:21 -08:00
Enrico Ros 37a25f0117 Preferences 2024-01-02 18:14:18 -08:00
Enrico Ros 7d5ab95c20 Merge branch 'folders' of https://github.com/joriskalz/big-AGI-dev into joriskalz-folders 2024-01-02 18:11:10 -08:00
Enrico Ros 7fe8dd776f ScrollToBottom: fix edge case of the edge case fix 2024-01-02 18:09:58 -08:00
Enrico Ros 0a85d8d104 Desktop: back to 5 rows, we have the space 2024-01-02 17:57:19 -08:00
Enrico Ros cfd563b200 Apps: override fullWidth (for Call only, for now) 2024-01-02 17:56:32 -08:00
Enrico Ros 311a8d0ba0 Use Anybburger menu 2024-01-02 17:56:14 -08:00
Enrico Ros 06cd386c6e Only close the drawer when clicking items within a mobile drawer 2024-01-02 17:45:25 -08:00
Enrico Ros 2632133ba4 New UI: Buttery-smooth transitions 2024-01-02 17:34:31 -08:00
Enrico Ros 1fe43cdc2e Theme: centralized zIndex 2024-01-02 17:12:49 -08:00
Enrico Ros e76939fb5d Root style: change some var names 2024-01-02 17:11:43 -08:00
Enrico Ros 5f4250e3d2 Squircle: fix 2024-01-02 17:10:55 -08:00
Joris Kalz 5653044b1e Fancy colors 2024-01-02 00:33:08 +01:00
Joris Kalz d4da34561d Removed unused items 2024-01-02 00:27:11 +01:00
Joris Kalz fa25e830d5 Removed Folder Title 2024-01-02 00:25:13 +01:00
Joris Kalz c90139923c Add folder selector 2024-01-02 00:23:32 +01:00
Joris Kalz fa5007cb3b Assign new conversation to selected folder 2024-01-02 00:12:38 +01:00
Joris Kalz b979e1313c Enable deletion of all items in a folder 2024-01-02 00:05:00 +01:00
Joris Kalz 1f1bf65c14 Filter by selected folder 2024-01-01 23:50:18 +01:00
Joris Kalz 2bc6a15256 Enable Selection of folders 2024-01-01 23:39:54 +01:00
Joris Kalz dbcdbaa893 Display Folders 2024-01-01 23:28:31 +01:00
Joris Kalz d0ac1d8e1a Refactor "Add Folder" button into a separate file 2024-01-01 23:09:46 +01:00
Joris Kalz 3929e501d8 Add Folder Button 2024-01-01 23:02:47 +01:00
Joris Kalz fa3ae7b821 UI, adding FolderList 2024-01-01 22:49:42 +01:00
Joris Kalz 79052f988c add store to persist folders in local storage 2024-01-01 22:33:47 +01:00
Enrico Ros 18e6e235f3 Merge New UI - details inside:
- OptimaLayout: new responsive UI framework, with nav and drawer for desktop and mobile
 - Nav: new top-level navigation framework (will replace 'routes' going forward)
 - The new (App) Panel is more stable for UI operations (vs. the former Popup)
 - Improved looks on desktop, and uses Drawer on mobile
 - Missing bottom-Nav on mobile, to replace the PageMenu nav
 - Closes #298, #201.

Landing as-is on `main`, will fix smaller bits later.
2023-12-31 17:55:43 -08:00
Enrico Ros 388e897466 New UI: disable mobilenav 2023-12-31 17:43:17 -08:00
Enrico Ros e05a3bc3e9 New UI: bits 2023-12-31 17:33:59 -08:00
Enrico Ros 5bb832f83d New UI: Add Personas 2023-12-31 17:30:36 -08:00
Enrico Ros 43cb19df83 New UI: PageBar hide when not needed on desktop 2023-12-31 17:30:29 -08:00
Enrico Ros 1d770ce012 New UI: desktop nav button 2023-12-31 17:30:10 -08:00
Enrico Ros 550e3e0173 News: gradient 2023-12-31 17:30:01 -08:00
Enrico Ros 043a5f48e8 New UI: enable split branch toggle 2023-12-31 16:28:21 -08:00
Enrico Ros 0b69e0a9d1 New UI: revert show split branching 2023-12-31 16:27:50 -08:00
Enrico Ros 5d8d752693 New UI: fix desktop drawer 2023-12-31 16:26:06 -08:00
Enrico Ros e7067ed4fb New UI: Page Menu working best 2023-12-31 16:17:43 -08:00
Enrico Ros d181e27555 ScrollToBottom: restore 60 2023-12-31 16:17:42 -08:00
Enrico Ros 47d8b220a3 UI: Mobile: improve PageBar 2023-12-31 15:53:34 -08:00
Enrico Ros cc5e310174 UI: Fix layout/2 2023-12-31 15:53:17 -08:00
Enrico Ros 8006f578cd UI: Fix layout 2023-12-31 15:52:57 -08:00
Enrico Ros a303bf7224 Small layout fixes 2023-12-31 06:06:32 -08:00
Enrico Ros dc0ca6d5bc Nav: +News 2023-12-31 05:53:19 -08:00
Enrico Ros 2db3917c1c New UI Layout - #299
Full skeleton of the new 2.0 structure.
2023-12-31 05:53:05 -08:00
Enrico Ros 0c2ae290b0 New UI: uniform inverted bar 2023-12-31 03:53:17 -08:00
Enrico Ros 24dcfeb952 [Nav] #299 2023-12-31 03:51:59 -08:00
Enrico Ros acd7a24cff SquircleIcon: support inversion 2023-12-31 03:51:13 -08:00
Enrico Ros 88c29cf32c Composer: desktop: less gap between buttons 2023-12-31 03:50:54 -08:00
Enrico Ros 26f472b396 Try to use a second React Context for the Optima drawer, to optimize state changes 2023-12-31 03:46:34 -08:00
Enrico Ros 68c5e0b940 Move Optima Layout providers in optima layout 2023-12-31 03:45:58 -08:00
Enrico Ros 03fca40b74 Use a Focused mode on mobile 2023-12-31 03:45:16 -08:00
Enrico Ros 35aff7798e Rename to PreferencesTab 2023-12-31 03:43:44 -08:00
Enrico Ros 6a8cf08ef0 Nav: placeholder application 2023-12-31 01:25:28 -08:00
Enrico Ros 53a9f9acef Love magic numbers 2023-12-31 01:24:11 -08:00
Enrico Ros d4c02dde1d Move the NextLoading progress bar after the single-page check, but before the backend roundtrip 2023-12-31 01:19:47 -08:00
Enrico Ros 660fda8485 Support CSS mime for file attachments. 2023-12-31 00:33:39 -08:00
Enrico Ros 049dfec794 ScrollToBottom: use a smaller sticky margin 2023-12-31 00:26:37 -08:00
Enrico Ros 2e6f1939dc UI: useNextLoadProgress as hook, and import style 2023-12-30 22:51:32 -08:00
Enrico Ros f3b1e4698a UI: extract and move icons 2023-12-30 18:44:50 -08:00
Enrico Ros 34e0102d82 UI: Add app CSS 2023-12-30 18:43:16 -08:00
Enrico Ros 3f5aed6f9b Merge pull request #318 from kursad-k/patch-1
Update config-browse.md
2023-12-30 17:32:21 -08:00
kursad-k 29647ad106 Update config-browse.md
added internet proxy settings
2023-12-30 19:31:19 -06:00
Enrico Ros 9426a45b88 Reduce uses of useRouter()
Note: the Link component is still using them really aggressively.
2023-12-30 16:24:21 -08:00
Enrico Ros 5b52544c6c Composer: some style fixes 2023-12-30 04:07:59 -08:00
Enrico Ros fc1c15ba87 Small image download hint 2023-12-30 03:57:03 -08:00
Enrico Ros e973fce3f7 UI: restyle IconButtons
The size of the picture inside the icon stays the same, 24x24, but the overall IconButton
and Button go down to 36x36 (was 40).

This includes a revert from a style change that originated from:
https://github.com/mui/material-ui/commit/7f81475ea148a416ec8fab252120ce6567c62897#diff-45dca083057933d78377b59e031146804cfedb68fe1514955bc8a5b3c38d7c44

The overall layout is getting smaller, so let's adapt to smaller IconButtons
2023-12-30 03:44:18 -08:00
Enrico Ros 99759654f2 Bug_report: improve copy 2023-12-29 23:33:41 -08:00
Enrico Ros 390a1effb1 Bug_report: test newlines 2023-12-29 23:31:41 -08:00
Enrico Ros f357291560 Improve placeholders 2023-12-29 23:30:59 -08:00
Enrico Ros c3a8b7e859 Update label 2023-12-29 23:28:15 -08:00
Enrico Ros 8931544349 Removed steps to reproduce 2023-12-29 23:27:43 -08:00
Enrico Ros 865e420e34 Update the BUG issue template, following the great example of tRPC 2023-12-29 23:26:27 -08:00
Enrico Ros 574c2b936e Create bug_test.yml 2023-12-29 23:18:10 -08:00
Enrico Ros 4f6a596cc7 Hold trpc back - bundle size increased by 20k 2023-12-29 22:54:44 -08:00
Enrico Ros edd36ea780 Roll packages 2023-12-29 19:01:37 -08:00
Enrico Ros 5a325b98ee Merge 'feature-newi' Phase 2 - details below:
- ChatCommands abstraction and registration (execution still specialized)
 - ReAct: improve display of steps, and UI
 - lineHeight: use unified number for consistency of rhythm
 - OptimaLayout: begin breakdown
2023-12-29 18:30:35 -08:00
Enrico Ros 8f6e2a3b5f Commands: registration framework 2023-12-29 18:05:27 -08:00
Enrico Ros cf2fc96107 Composer: move text interception 2023-12-29 02:45:06 -08:00
Enrico Ros 8837a1fc65 Chat Panes: alt+click to remove focus 2023-12-29 02:45:06 -08:00
Enrico Ros 91970f088e Composer: move buttons 2023-12-29 02:11:08 -08:00
Enrico Ros f59f77e50a Ephemerals: improve looks 2023-12-29 02:08:15 -08:00
Enrico Ros 50b1f00b5a Commands: much improve the parser 2023-12-29 01:38:32 -08:00
Enrico Ros 4f98a8a319 UI: github markdown: deviate from upstream, and don't redefine font properties 2023-12-29 01:02:38 -08:00
Enrico Ros fb8aa3936b UI: font: removed from html 2023-12-29 01:02:16 -08:00
Enrico Ros 335876555f UI: chat message: smaller avatar text 2023-12-29 01:01:42 -08:00
Enrico Ros 7da3b1f4c4 UI: rhythm (line heights) 2023-12-29 01:01:10 -08:00
Enrico Ros e80bc4cea7 reAct: improve logging 2023-12-29 00:39:03 -08:00
Enrico Ros 448755ff8d OptimaL: destructure 2023-12-28 22:41:14 -08:00
Enrico Ros 3a4c23840a Panes: extract Resize handler 2023-12-28 21:20:28 -08:00
Enrico Ros 13c69111f9 Stop button: soft warning 2023-12-28 20:43:53 -08:00
Enrico Ros 0b9feb9fda Scroll To Bottom: fix one edge case #312 2023-12-28 20:42:48 -08:00
Enrico Ros 677facb867 Merge branch 'release-1.9.0' 2023-12-28 14:49:58 -08:00
Enrico Ros 494086765b 1.9.0: README and Changelog 2023-12-28 14:47:04 -08:00
Enrico Ros 59ca03e17d Release: update template 2023-12-28 14:32:19 -08:00
Enrico Ros e0e56d70c9 1.9.0: News 2023-12-28 14:29:11 -08:00
Enrico Ros b408267e6e DALL·E: reorder options 2023-12-28 14:09:17 -08:00
Enrico Ros 6385d7aa84 DALL·E: raw prompting for DALL·E 3 as well 2023-12-28 14:04:14 -08:00
Enrico Ros fa811c951c 1.9.0: Version 2023-12-28 13:26:40 -08:00
Enrico Ros 7085c3a7aa DALL·E: temporary image notice 2023-12-28 13:22:53 -08:00
Enrico Ros 2333318cb4 Release: update template 2023-12-28 13:00:54 -08:00
Enrico Ros 3aebcb360c Release: update template 2023-12-28 12:56:59 -08:00
Enrico Ros bf60d699e3 Release: update template 2023-12-28 12:51:19 -08:00
Enrico Ros d775d47623 New UI - Part 1 - Details inside:
- Optima Layout: new Context based pluggable layout system
   - Now children have context functions, for better behaviors
   - Removed `store-applayout`
   - using withLayout on top-level Pages
 - ScrollToBottom: grounds-up subsystem for smooth scrolling with snap-to-bottom
 - Panes subsystem: use react-resizeable-panels together with our Panes subsystem
   - New: Split window chats, Drag to close windows, Button to split
   - using: https://github.com/bvaughn/react-resizable-panels
 - Cosmetic: Colors: update Light and Dark themes
 - Bootstrap Logic provider: will enable Mobile use cases
 - Removed NoSSR (the backend provided natually acts as the same)
 - Next load progress: loading indicator for slower pages (>300ms)
 - withLayout() system

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

Closes #308, #304, #255, #59.
Progress on #305, #201, #296, #233, #208, #203.
2023-12-28 02:16:55 -08:00
Enrico Ros 2eb3397394 Scroll-To-Bottom: complete Framework. Fixes #304, Fixes #60, Fixes #59 2023-12-28 02:13:14 -08:00
Enrico Ros e27c35373d Update year, almost there 2023-12-28 02:02:11 -08:00
Enrico Ros 5e1966af5f Scroller: begins to work well 2023-12-28 00:36:41 -08:00
Enrico Ros 7cbcf01ca9 Scroller: vastly improve the framework 2023-12-27 23:41:50 -08:00
Enrico Ros 6898fa6cc1 Scroller: framework (incomplete), Fixes #59 2023-12-27 20:08:28 -08:00
Enrico Ros 1e796299a2 Scroller: straighten messages (remove bottoms) 2023-12-27 19:50:07 -08:00
Enrico Ros 7026024da5 Scroller: straighten messages 2023-12-27 18:16:59 -08:00
Enrico Ros 3ed52fa92f Panes: move 2023-12-27 16:55:22 -08:00
Enrico Ros a3e04f5973 Panes: duplicate current 2023-12-27 16:16:27 -08:00
Enrico Ros 8bf90e3622 Use react-resizable-panels instead of the flexbox
Also Fix #255 due to the large layout restructuring.
2023-12-27 05:21:07 -08:00
Enrico Ros cdc2de5018 Composer: fix attachments layout 2023-12-27 02:29:53 -08:00
Enrico Ros b26370a85a OptimaLayout: begin 2023-12-27 00:52:49 -08:00
Enrico Ros adf0197a9e Disable debug 2023-12-26 22:29:21 -08:00
Enrico Ros c00c41a160 AppBar: begin cleanup 2023-12-26 22:28:43 -08:00
Enrico Ros 09c74e6cf4 OptimaLayout: migrate to Context for better React usage 2023-12-26 22:23:50 -08:00
Enrico Ros 304e66b098 Routing shall be homogeneous now 2023-12-26 20:04:12 -08:00
Enrico Ros 64b6b08652 Routing bits 2023-12-26 19:59:30 -08:00
Enrico Ros cbea304a97 Improve routing 2023-12-26 19:58:01 -08:00
Enrico Ros c3e73fa9c8 BootstrapProvider: check for mobile 2023-12-26 19:46:12 -08:00
Enrico Ros 4c978020d9 Add the Bootstrap Logic provider 2023-12-26 19:39:49 -08:00
Enrico Ros 481b85bdad Providers into a dedicated folder 2023-12-26 19:32:20 -08:00
Enrico Ros b80fd0494a Move this (unused) utility 2023-12-26 19:31:22 -08:00
Enrico Ros c7dea43d1a Move providers 2023-12-26 19:30:51 -08:00
Enrico Ros 726053ffcd GoodDropdown shared 2023-12-26 13:01:17 -08:00
Enrico Ros ee4e2c265b Next Router Loading Progress 2023-12-26 12:55:43 -08:00
Enrico Ros a5332d2c82 Deflate bundle by reverting to per-page Layouts (keep the typings at least) 2023-12-26 01:11:21 -08:00
Enrico Ros 2f45ce48fa Fix 2023-12-26 00:43:15 -08:00
Enrico Ros 104922dc20 Dynamic layouting 2023-12-26 00:36:18 -08:00
Enrico Ros d68ccd9dfb Optimize 2023-12-25 23:57:59 -08:00
Enrico Ros 676bcadd17 Remove NoSSR: the Backend provider does the same and doesn't seem to flash the screen that much 2023-12-25 22:36:59 -08:00
Enrico Ros c08e83c618 More uniform App backgrounds 2023-12-25 22:16:06 -08:00
Enrico Ros 7a69b32506 Theme: update background shades for Light and Dark 2023-12-25 22:12:50 -08:00
Enrico Ros a9e1a968e8 DallE3: support multiple parallel image request for count>1 2023-12-23 03:36:06 -08:00
Enrico Ros dc30a7a55a Pixels 2023-12-23 03:08:43 -08:00
Enrico Ros f570627b09 Settings: light outline 2023-12-23 02:44:46 -08:00
Enrico Ros e601302db8 Dall-E: show pricing when changing settings 2023-12-23 02:35:46 -08:00
Enrico Ros f9e207ff7c RenderImage: larger tooltip 2023-12-23 02:07:33 -08:00
Enrico Ros 8100c5cfd1 Merge branch 'feature-dalle'
Fixes #212
2023-12-23 02:00:02 -08:00
Enrico Ros 0b0c3891bb Bits 2023-12-23 01:58:58 -08:00
Enrico Ros b4cdd5546d T2I: Final Naming and Cleanups. Closes #212 2023-12-23 01:55:32 -08:00
Enrico Ros 8444b32db2 T2I: tti -> t2i 2023-12-23 01:45:52 -08:00
Enrico Ros 69098273bf TTI: return Markdown Image References.
Will be rendered neatly with or without markdown on.
2023-12-23 01:20:04 -08:00
Enrico Ros 5cd5702b83 Dalle: improve configuration 2023-12-23 01:11:42 -08:00
Enrico Ros 605d288da6 Dalle: improve typedefs 2023-12-23 01:10:24 -08:00
Enrico Ros 499840cae3 Parse the new markdown image blocks 2023-12-23 01:09:54 -08:00
Enrico Ros 4529fc325b RenderImage: vastly improve the Image Block, incl. the ALT Text 2023-12-23 01:09:35 -08:00
Enrico Ros 4769e9b900 T2I: move store 2023-12-22 23:22:09 -08:00
Enrico Ros 64d13a0d52 T2I: remove auto-set from OpenAI setup 2023-12-22 23:15:57 -08:00
Enrico Ros 7df1517b23 T2I: Settings (choose active) 2023-12-22 23:15:21 -08:00
Enrico Ros 56c372455d T2I: fix OpenAI DallE path 2023-12-22 23:13:13 -08:00
Enrico Ros 2e649ea12b Image Block: add Dalle 2023-12-22 23:12:03 -08:00
Enrico Ros 2a67315504 FormRadioControl: improve mobile, support undefined 2023-12-22 22:20:02 -08:00
Enrico Ros b53ceb70c4 T2I: improvements 2023-12-22 19:10:20 -08:00
Enrico Ros 3c9d06aac7 T2I: misc 2023-12-22 19:00:49 -08:00
Enrico Ros 77e7c1d467 T2I: move methods around 2023-12-22 19:00:35 -08:00
Enrico Ros eb38e119b8 misc: rename file 2023-12-22 18:48:00 -08:00
Enrico Ros 06402cc5c1 T2I: capability checks 2023-12-22 18:40:26 -08:00
Enrico Ros ddf631cdfc T2I: integrate with OpenAI Access credentials 2023-12-22 18:31:46 -08:00
Enrico Ros f7e89ae65c bits 2023-12-22 18:31:19 -08:00
Enrico Ros 07e1e1c580 T2I: client (capabilities, immediate generation) 2023-12-22 18:30:51 -08:00
Enrico Ros f6eb2aecee T2I: capabilities update 2023-12-22 18:29:38 -08:00
Enrico Ros f416b1df97 T2I: openAI generation 2023-12-22 18:29:17 -08:00
Enrico Ros 29d17795b8 T2I: cmd change 2023-12-22 18:28:30 -08:00
Enrico Ros 3b30f649c6 T2I: move Prodia in the Text2Image module 2023-12-22 18:27:33 -08:00
Enrico Ros ba9a9714a7 OpenAI: router: generate images 2023-12-22 16:34:06 -08:00
Enrico Ros c304ab5f3b Llms: Cleanup some type definitions 2023-12-22 15:43:49 -08:00
Enrico Ros cd4d5042e9 Roll packages 2023-12-22 15:22:01 -08:00
Enrico Ros 6c4d177bfc Metadata: update 2023-12-22 06:06:55 -08:00
Enrico Ros 5d1620b5c1 OpenRouter: limit free model calls to 1/5s. Closes #291 2023-12-22 03:42:42 -08:00
Enrico Ros bd78808950 Implement Rate limiting framework 2023-12-22 03:40:48 -08:00
Enrico Ros 6aee6aeac1 DLLM: add a 'Free' attribute (only on OpenRouter for now)
Shall have this on Local models as well?
2023-12-22 03:23:05 -08:00
Enrico Ros 5ae970a526 LLM Options: display Free models 2023-12-22 03:22:26 -08:00
Enrico Ros 87718d73d2 Models Loading progress 2023-12-22 03:04:42 -08:00
Enrico Ros 7c8498573e LLMOptions: improve display (add tooltips and advanced) 2023-12-22 02:58:01 -08:00
Enrico Ros f6e82d0c0c OpenRouter: drop the hardcoded list 2023-12-22 02:57:46 -08:00
Enrico Ros f7f827660d Merge pull request #290 from joriskalz/fix-reset-values-when-switching-mode
[BUG] Reset values when switching between text and youtube mode
2023-12-21 14:31:14 -08:00
Enrico Ros 664b221e67 Imagine: unified pipeline. Adds to #289 2023-12-21 14:29:11 -08:00
Enrico Ros f184a4bf97 Imagine: remove former 'mode' 2023-12-21 14:21:41 -08:00
Joris Kalz e442816c15 fix to reset state when switching between modes. 2023-12-21 18:37:31 +01:00
Enrico Ros aaa3b65cd8 Merge branch 'joriskalz-Persona-From-Text'. Fixes #282 2023-12-21 04:31:58 -08:00
Enrico Ros c6441662b0 Persona Creator: on by default, can be hidden like other tiles 2023-12-21 04:30:29 -08:00
Enrico Ros b902a7bce8 Persona Creator: consistent naming 2023-12-21 04:30:01 -08:00
Enrico Ros 87a916ba09 Persona Creator: remove the 'Labs' flag 2023-12-21 04:29:31 -08:00
Enrico Ros 35a85ed2fa Persona Creator: final fix I swear 2023-12-21 03:32:01 -08:00
Enrico Ros 75d56bfb56 Persona Creator: change the 'copy' location and improve paddings 2023-12-21 03:15:03 -08:00
Enrico Ros d0a125fad5 Persona Creator: rename model selector label 2023-12-21 02:51:53 -08:00
Enrico Ros 2af8437f6d Persona Creator: reorder blocks, and show the LLM name 2023-12-21 02:49:30 -08:00
Enrico Ros 0c3e65575c Persona Creator: remove YT -> renamed to PersonaCreator.tsx 2023-12-21 02:12:25 -08:00
Enrico Ros 1c15057fca Persona Creator: style: update TextArea and margins 2023-12-21 02:11:33 -08:00
Joris Kalz 44da928489 Optimzed text to cover both use cases 2023-12-20 21:54:46 +01:00
Joris Kalz 85027d3e3a Added Persona from Text 2023-12-20 21:46:19 +01:00
237 changed files with 8426 additions and 2537 deletions
-25
View File
@@ -1,25 +0,0 @@
---
name: Bug report
about: Omg what's happening?
title: "[BUG]"
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
Where is it happening?
- Which device [Mobile/Desktop, os version]:
- Which browser:
- Which website:
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots / context**
If applicable, please add screenshots or additional context
+32
View File
@@ -0,0 +1,32 @@
name: 🐞 Bug Report
description: Create a report to help us improve
title: '[BUG]'
labels: [ 'type: bug' ]
body:
- type: markdown
attributes:
value: Thank you for reporting a bug.
- type: textarea
attributes:
label: Description
description: (required) Please provide a clear description. Please also provide the steps to reproduce.
placeholder: 'Concise description + steps to reproduce.'
validations:
required: true
- type: textarea
attributes:
label: Device and browser
description: '(required) Please specify your Mobile/Desktop device, OS version, browser.'
placeholder: 'Device: (e.g., iPhone 16, Pixel 9, PC, Macbook...), OS: (e.g., iOS 17, Windows 12), Browser: (e.g., Chrome 119, Safari 18, Firefox..)'
validations:
required: true
- type: textarea
attributes:
label: Screenshots and more
placeholder: 'Attach screenshots, or add any additional context here.'
- type: checkboxes
attributes:
label: Willingness to Contribute
description: We appreciate contributions - would you be willing to submit a pull request?
options:
- label: '🙋‍♂️ Yes, I would like to contribute a fix.'
+50 -21
View File
@@ -9,6 +9,8 @@ assignees: enricoros
## Release checklist:
- [x] Create a new [Release Issue](https://github.com/enricoros/big-AGI/issues/new?assignees=enricoros&projects=enricoros/4&template=maintainers-release.md&title=Release+1.2.3)
- [ ] Replace 1.1.0 with the _former_ release, and _1.2.3_ with THIS
- [ ] Update the [Roadmap](https://github.com/users/enricoros/projects/4/views/2) calling out shipped features
- [ ] Create and update a [Milestone](https://github.com/enricoros/big-agi/milestones) for the release
- [ ] Assign this task
@@ -21,38 +23,55 @@ assignees: enricoros
- [ ] 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 the readme with the new release
- [ ] Update the README.md with the new release
- [ ] Copy the highlights to the [docs/changelog.md](/docs/changelog.md)
- Release:
- [ ] merge onto main
- [ ] merge onto main `git checkout main && git merge --no-ff release-1.2.3`
- [ ] re-tag `git tag -f v1.2.3 && git push opensource --tags -f'
- [ ] verify deployment on Vercel
- [ ] verify container on GitHub Packages
- create a GitHub release
- [ ] name it 'vX.Y.Z'
- [ ] copy the release notes and link appropriate artifacts
- [ ] update the GitHub release
- [ ] push as stable `git push opensource main:main-stable`
- Announce:
- [ ] Discord announcement
- [ ] Twitter announcement
### Links
## Links
Milestone:
Former release task:
GitHub release:
- Milestone: https://github.com/enricoros/big-AGI/milestone/X
- GitHub release: https://github.com/enricoros/big-AGI/releases/tag/vX.Y.Z
- Former release task: https://github.com/enricoros/big-AGI/issues/XXX
## Artifacts Generation
1) The following is my opensource application
- paste README.md
2) I am announcing a new version, 1.7.0. The following were the announcements for 1.6.0. Discord announcement, GitHub Release, in-app news.data.tsx, changelog.md.
- paste the former: `discord announcement`, `GitHub release`, `news.data.tsx`, `changelog.md`
3) The following is the new data I have for 1.7.0
- paste the link to the milestone (closed) and each individual issue (content will be downloaded)
- paste the git changelog `git log v1.6.0..v1.7.0 | clip`
```markdown
You help me generate the following collateral for the new release of my opensource application
called big-AGI. The new release is 1.2.3.
To familiarize yourself with the application, the following are the Website and the GitHub README.md.
```
### news.data.TSX
- paste the URL: https://big-agi.com
- drag & drop: [README.md](https://raw.githubusercontent.com/enricoros/big-AGI/main/README.md)
```markdown
I am announcing a new version, 1.2.3.
For reference, the following was the collateral for 1.1.0 (Discord announcement,
GitHub Release, in-app-news file news.data.tsx, changelog.md).
```
- paste the former: `discord announcement`,
- `GitHub release`,
- `news.data.tsx`,
- `changelog.md`
```markdown
The following are the new developments for 1.2.3:
```
- paste the link to the milestone (closed) and each individual issue (content will be downloaded)
- paste the git changelog `git log v1.1.0..v1.2.3 | clip`
### news.data.tsx
```markdown
I need the following from you:
@@ -63,10 +82,19 @@ I need the following from you:
4. I want you then to update the news.data.tsx for the new release
```
### Readme (and Changelog)
```markdown
I need you to update the README.md and the with the new release.
Attaching the in-app news, with my language for you to improve on, but keep the tone.
```
### GitHub release
```markdown
Please create the 1.2.3 Release Notes for GitHub. The following were the Release Notes for 1.1.0. Use a truthful and honest tone, undestanding that people's time and attention span is short. Today is 2023-12-20.
Please create the 1.2.3 Release Notes for GitHub.
Use a truthful and honest tone, understanding that people's time and attention span is short.
Today is 2024-1-1.
```
Now paste-attachment the former release notes (or 1.5.0 which was accurate and great), including the new contributors and
@@ -75,5 +103,6 @@ some stats (# of commits, etc.), and roll it for the new release.
### Discord announcement
```markdown
Can you generate my 1.2.3 big-AGI discord announcement from the GitHub Release announcement, and the in-app News?
Can you generate my 1.2.3 big-AGI discord announcement from the GitHub Release announcement?
Please keep the formatting and stye of the discord announcement for 1.1.0, but with the new messaging above.
```
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2023 Enrico Ros
Copyright (c) 2023-2024 Enrico Ros
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+26 -40
View File
@@ -1,7 +1,7 @@
# BIG-AGI 🧠✨
Welcome to big-AGI 👋, the GPT application for professionals that need function, form,
simplicity, and speed. Powered by the latest models from 8 vendors and
simplicity, and speed. Powered by the latest models from 11 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.
@@ -21,53 +21,39 @@ shows the current developments and future ideas.
- Got a suggestion? [_Add your roadmap ideas_](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
- Want to contribute? [_Pick up a task!_](https://github.com/users/enricoros/projects/4/views/4) - _easy_ to _pro_
### What's New in 1.8.0 · Dec 20, 2023 · To The Moon And Back · 🚀🌕🔙
### What's New in 1.11.0 · Jan 16, 2024 · Singularity
- **Google Gemini Support**: Use the newest Google models. [#275](https://github.com/enricoros/big-agi/issues/275)
- **Mistral Platform**: Mixtral and future models support. [#273](https://github.com/enricoros/big-agi/issues/273)
- **Diagram Instructions**. Thanks to @joriskalz! [#280](https://github.com/enricoros/big-agi/pull/280)
- Ollama Chats: Enhanced chatting experience. [#270](https://github.com/enricoros/big-agi/issues/270)
- Mac Shortcuts Fix: Improved UX on Mac
- **Single-Tab Mode**: Data integrity with single window. [#268](https://github.com/enricoros/big-agi/issues/268)
- **Updated Models**: Latest Ollama (v0.1.17) and OpenRouter models
- Official Downloads: Easy access to the latest big-AGI on [big-AGI.com](https://big-agi.com)
- For developers: [troubleshot networking](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483), fixed Vercel deployment, cleaned up the LLMs/Streaming framework
https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cfcb110c68
### What's New in 1.7.0 · Dec 11, 2023
- **Find chats**: search in titles and content, with frequency ranking. [#329](https://github.com/enricoros/big-AGI/issues/329)
- **Commands**: command auto-completion (type '/'). [#327](https://github.com/enricoros/big-AGI/issues/327)
- **[Together AI](https://www.together.ai/products#inference)** inference platform support (good speed and newer models). [#346](https://github.com/enricoros/big-AGI/issues/346)
- Persona Creator history, deletion, custom creation, fix llm API timeouts
- Enable adding up to five custom OpenAI-compatible endpoints
- Developer enhancements: new 'Actiles' framework
- **Attachments System Overhaul**: Drag, paste, link, snap, text, images, PDFs and more. [#251](https://github.com/enricoros/big-agi/issues/251)
- **Desktop Webcam Capture**: Image capture now available as Labs feature. [#253](https://github.com/enricoros/big-agi/issues/253)
- **Independent Browsing**: Full browsing support with Browserless. [Learn More](https://github.com/enricoros/big-agi/blob/main/docs/config-browse.md)
- **Overheat LLMs**: Push the creativity with higher LLM temperatures. [#256](https://github.com/enricoros/big-agi/issues/256)
- **Model Options Shortcut**: Quick adjust with `Ctrl+Shift+O`
- Optimized Voice Input and Performance
- Latest Ollama and Oobabooga models
- For developers: **Password Protection**: HTTP Basic Auth. [Learn How](https://github.com/enricoros/big-agi/blob/main/docs/deploy-authentication.md)
### What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI
### What's New in 1.6.0 - Nov 28, 2023
https://github.com/enricoros/big-AGI/assets/32999/fbb1be49-5c38-49c8-86fa-3705700f6c39
- **Web Browsing**: Download web pages within chats - [browsing guide](https://github.com/enricoros/big-agi/blob/main/docs/config-browse.md)
- **Branching Discussions**: Create new conversations from any message
- **Keyboard Navigation**: Swift chat navigation with new shortcuts (e.g. ctrl+alt+left/right)
- **Performance Boost**: Faster rendering for a smoother experience
- **UI Enhancements**: Refined interface based on user feedback
- **New Features**: Anthropic Claude 2.1, `/help` command, and Flattener tool
- **For Developers**: Code quality upgrades and snackbar notifications
- **New UI**: for both desktop and mobile, sets the stage for future scale. [#201](https://github.com/enricoros/big-AGI/issues/201)
- **Conversation Folders**: enhanced conversation organization. [#321](https://github.com/enricoros/big-AGI/issues/321)
- **[LM Studio](https://lmstudio.ai/)** support and improved token management
- Resizable panes in split-screen conversations.
- Large performance optimizations
- Developer enhancements: new UI framework, updated documentation for proxy settings on browserless/docker
### What's New in 1.5.0 - Nov 19, 2023
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
- **Continued Voice**: Engage with hands-free interaction for a seamless experience
- **Visualization Tool**: Create data representations with our new visualization capabilities
- **Ollama Local Models**: Leverage local models support with our comprehensive guide
- **Text Tools**: Enjoy tools including highlight differences to refine your content
- **Mermaid Diagramming**: Render complex diagrams with our Mermaid language support
- **OpenAI 1106 Chat Models**: Experience the cutting-edge capabilities of the latest OpenAI models
- **SDXL Support**: Enhance your image generation with SDXL support for Prodia
- **Cloudflare OpenAI API Gateway**: Integrate with Cloudflare for a robust API gateway
- **Helicone for Anthropic**: Utilize Helicone's tools for Anthropic models
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
- **Perfect scrolling mechanics** across devices. [#304](https://github.com/enricoros/big-AGI/issues/304)
- Persona creation now supports **text input**. [#287](https://github.com/enricoros/big-AGI/pull/287)
- Openrouter updates for better model management and rate limit handling
- Image drawing UX improvements
- Layout fix for Firefox users
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
Check out the [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), or
the [past releases changelog](docs/changelog.md).
For full details and former releases, check out the [changelog](docs/changelog.md).
## ✨ Key Features 👊
+31 -3
View File
@@ -5,12 +5,40 @@ by release.
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
### 1.9.0 - Dec 2023
### 1.12.0 - Jan 2024
- milestone: [1.12.0](https://github.com/enricoros/big-agi/milestone/12)
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
- milestone: [1.9.0](https://github.com/enricoros/big-agi/milestone/9)
### What's New in 1.8.0 · Dec 20, 2023 · To The Moon And Back · 🚀🌕🔙
### What's New in 1.11.0 · Jan 16, 2024 · Singularity
- **Find chats**: search in titles and content, with frequency ranking. [#329](https://github.com/enricoros/big-AGI/issues/329)
- **Commands**: command auto-completion (type '/'). [#327](https://github.com/enricoros/big-AGI/issues/327)
- **[Together AI](https://www.together.ai/products#inference)** inference platform support. [#346](https://github.com/enricoros/big-AGI/issues/346)
- Persona Creator history, deletion, custom creation, fix llm API timeouts
- Enable adding up to five custom OpenAI-compatible endpoints
- Developer enhancements: new 'Actiles' framework
### What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI
- **New UI**: for both desktop and mobile, sets the stage for future scale. [#201](https://github.com/enricoros/big-AGI/issues/201)
- **Conversation Folders**: enhanced conversation organization. [#321](https://github.com/enricoros/big-AGI/issues/321)
- **[LM Studio](https://lmstudio.ai/)** support and improved token management
- Resizable panes in split-screen conversations.
- Large performance optimizations
- Developer enhancements: new UI framework, updated documentation for proxy settings on browserless/docker
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
- **Perfect scrolling mechanics** across devices. [#304](https://github.com/enricoros/big-AGI/issues/304)
- Persona creation now supports **text input**. [#287](https://github.com/enricoros/big-AGI/pull/287)
- Openrouter updates for better model management and rate limit handling
- Image drawing UX improvements
- Layout fix for Firefox users
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
### What's New in 1.8.0 · Dec 20, 2023 · To The Moon And Back
- **Google Gemini Support**: Use the newest Google models. [#275](https://github.com/enricoros/big-agi/issues/275)
- **Mistral Platform**: Mixtral and future models support. [#273](https://github.com/enricoros/big-agi/issues/273)
+25 -1
View File
@@ -50,10 +50,34 @@ Now you can use the following connection string in `big-AGI`: `ws://127.0.0.1:92
You can also browse to [http://127.0.0.1:9222](http://127.0.0.1:9222) to see the Browserless debug viewer
and configure some options.
The chat agent won't be able to access the web sites if the browserless container does not have direct Internet access. You can resolve the issue by defining internet proxy for the running container. You can then use the evironment file in the a `docker-compose.yaml
```
browserless:
image: browserless/chrome:latest
env_file:
- .env
ports:
- "9222:3000" # Map host's port 9222 to container's port 3000
environment:
- MAX_CONCURRENT_SESSIONS=10
```
You can then add the proyy lines to your `.env` file.
```
https_proxy=http://PROXY-IP:PROXY-PORT
http_proxy=http://PROXY-IP:PROXY-PORT
```
This is how you can define it in a one liner docker
`docker run --env https_proxy=http://PROXY-IP:PROXY-PORT --env http_proxy=http://PROXY-IP:PROXY-PORT -p 9222:3000 browserless/chrome:latest `
Note: if you are using `docker-compose`, please see the
[docker/docker-compose-browserless.yaml](docker/docker-compose-browserless.yaml) file for an example
on how to run `big-AGI` and Browserless simultaneously in a single application.
### 🌐 Your own Chrome browser
***EXPERIMENTAL - UNTESTED*** - You can use your own Chrome browser as a browsing service, by configuring it to expose
@@ -84,4 +108,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!
Enjoy the enhanced browsing experience within `big-AGI` and explore the web without ever leaving your chat!
+2
View File
@@ -28,6 +28,7 @@ GEMINI_API_KEY=
MISTRAL_API_KEY=
OLLAMA_API_HOST=
OPENROUTER_API_KEY=
TOGETHERAI_API_KEY=
# Model Observability: Helicone
HELICONE_API_KEY=
@@ -85,6 +86,7 @@ requiring the user to enter an API key
| `MISTRAL_API_KEY` | The API key for Mistral | Optional |
| `OLLAMA_API_HOST` | Changes the backend host for the Ollama vendor. See [config-ollama.md](config-ollama.md) | |
| `OPENROUTER_API_KEY` | The API key for OpenRouter | Optional |
| `TOGETHERAI_API_KEY` | The API key for Together AI | Optional |
### Model Observability: Helicone
+350 -178
View File
@@ -1,64 +1,70 @@
{
"name": "big-agi",
"version": "1.8.0",
"version": "1.11.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "big-agi",
"version": "1.8.0",
"version": "1.11.0",
"hasInstallScript": true,
"dependencies": {
"@dqbd/tiktoken": "^1.0.7",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/react": "^11.11.3",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.0",
"@mui/joy": "^5.0.0-beta.18",
"@mui/icons-material": "^5.15.2",
"@mui/joy": "^5.0.0-beta.20",
"@next/bundle-analyzer": "^14.0.4",
"@prisma/client": "^5.7.0",
"@prisma/client": "^5.7.1",
"@sanity/diff-match-patch": "^3.1.1",
"@t3-oss/env-nextjs": "^0.7.1",
"@tanstack/react-query": "~4.36.1",
"@trpc/client": "^10.44.1",
"@trpc/next": "^10.44.1",
"@trpc/react-query": "^10.44.1",
"@trpc/server": "^10.44.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.1.1",
"@vercel/speed-insights": "^1.0.2",
"browser-fs-access": "^0.35.0",
"eventsource-parser": "^1.1.1",
"idb-keyval": "^6.2.1",
"next": "^14.0.4",
"nprogress": "^0.2.0",
"pdfjs-dist": "4.0.269",
"plantuml-encoder": "^1.4.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
"react-katex": "^3.0.1",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^1.0.5",
"react-timeago": "^7.2.0",
"remark-gfm": "^4.0.0",
"superjson": "^2.2.1",
"tesseract.js": "^5.0.3",
"tesseract.js": "^5.0.4",
"uuid": "^9.0.1",
"zod": "^3.22.4",
"zustand": "^4.4.7"
},
"devDependencies": {
"@cloudflare/puppeteer": "^0.0.5",
"@types/node": "^20.10.4",
"@types/node": "^20.10.6",
"@types/nprogress": "^0.2.3",
"@types/plantuml-encoder": "^1.4.2",
"@types/prismjs": "^1.26.3",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"@types/react": "^18.2.46",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.2.18",
"@types/react-katex": "^3.0.4",
"@types/react-timeago": "^4.1.6",
"@types/react-timeago": "^4.1.7",
"@types/uuid": "^9.0.7",
"eslint": "^8.55.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"prettier": "^3.1.1",
"prisma": "^5.7.0",
"prisma": "^5.7.1",
"typescript": "^5.3.3"
},
"engines": {
@@ -255,9 +261,9 @@
}
},
"node_modules/@babel/runtime": {
"version": "7.23.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz",
"integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==",
"version": "7.23.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz",
"integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
@@ -349,14 +355,14 @@
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
},
"node_modules/@emotion/react": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz",
"integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==",
"version": "11.11.3",
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz",
"integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==",
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.11.0",
"@emotion/cache": "^11.11.0",
"@emotion/serialize": "^1.1.2",
"@emotion/serialize": "^1.1.3",
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
"@emotion/utils": "^1.2.1",
"@emotion/weak-memoize": "^0.3.1",
@@ -372,9 +378,9 @@
}
},
"node_modules/@emotion/serialize": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz",
"integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==",
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz",
"integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==",
"dependencies": {
"@emotion/hash": "^0.9.1",
"@emotion/memoize": "^0.8.1",
@@ -500,9 +506,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@@ -596,14 +602,14 @@
}
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.27.tgz",
"integrity": "sha512-duL37qxihT1N0pW/gyXVezP7SttLkF+cLAs/y6g6ubEFmVadjbnZ45SeF12/vAiKzqwf5M0uFH1cczIPXFZygA==",
"version": "5.0.0-beta.29",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.29.tgz",
"integrity": "sha512-OXfUssYrB6ch/xpBVHMKAjThPlI9VyGGKdvQLMXef2j39wXfcxPlUVQlwia/lmE3rxWIGvbwkZsDtNYzLMsDUg==",
"dependencies": {
"@babel/runtime": "^7.23.5",
"@babel/runtime": "^7.23.6",
"@floating-ui/react-dom": "^2.0.4",
"@mui/types": "^7.2.11",
"@mui/utils": "^5.15.0",
"@mui/utils": "^5.15.2",
"@popperjs/core": "^2.11.8",
"clsx": "^2.0.0",
"prop-types": "^15.8.1"
@@ -627,20 +633,20 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.0.tgz",
"integrity": "sha512-NpGtlHwuyLfJtdrlERXb8qRqd279O0VnuGaZAor1ehdNhUJOD1bSxHDeXKZkbqNpvi50hasFj7lsbTpluworTQ==",
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.2.tgz",
"integrity": "sha512-0vk4ckS2w1F5PmkSXSd7F/QuRlNcPqWTJ8CPl+HQRLTIhJVS/VKEI+3dQufOdKfn2wS+ecnvlvXerbugs+xZ8Q==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.0.tgz",
"integrity": "sha512-zHY6fOkaK7VfhWeyxO8MjO3IAjEYpYMXuqUhX7TkUZJ9+TSH/9dn4ClG4K2j6hdgBU5Yrq2Z/89Bo6BHHp7AdQ==",
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.2.tgz",
"integrity": "sha512-Vs0Z6cd6ieTavMjqPvIJJfwsKaCLdRSErk5LjKdZlBqk7r2SR6roDyhVTQuZOeCzjEFj0qZ4iVPp2DJZRwuYbw==",
"dependencies": {
"@babel/runtime": "^7.23.5"
"@babel/runtime": "^7.23.6"
},
"engines": {
"node": ">=12.0.0"
@@ -661,16 +667,16 @@
}
},
"node_modules/@mui/joy": {
"version": "5.0.0-beta.18",
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.18.tgz",
"integrity": "sha512-TxEo7kqEnbjB5S8cyFrytWjzhxW12UxkEJOT0QM8WpwaBN3Ie1okFuo2bnFW94vYFZperW97/H/08cqqS/2JPA==",
"version": "5.0.0-beta.20",
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.20.tgz",
"integrity": "sha512-w0BjmY8XKdca0s7yRZiURhSlhiqDtSnhNFl6GHixYytNB5u8Al6GMdYH0aLB2w5+QP8ojPueYQ7oXkS/qo0skQ==",
"dependencies": {
"@babel/runtime": "^7.23.5",
"@mui/base": "5.0.0-beta.27",
"@mui/core-downloads-tracker": "^5.15.0",
"@mui/system": "^5.15.0",
"@babel/runtime": "^7.23.6",
"@mui/base": "5.0.0-beta.29",
"@mui/core-downloads-tracker": "^5.15.2",
"@mui/system": "^5.15.2",
"@mui/types": "^7.2.11",
"@mui/utils": "^5.15.0",
"@mui/utils": "^5.15.2",
"clsx": "^2.0.0",
"prop-types": "^15.8.1"
},
@@ -701,18 +707,18 @@
}
},
"node_modules/@mui/material": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.0.tgz",
"integrity": "sha512-60CDI/hQNwJv9a3vEZtFG7zz0USdQhVwpBd3fZqrzhuXSdiMdYMaZcCXeX/KMuNq0ZxQEAZd74Pv+gOb408QVA==",
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.2.tgz",
"integrity": "sha512-JnoIrpNmEHG5uC1IyEdgsnDiaiuCZnUIh7f9oeAr87AvBmNiEJPbo7XrD7kBTFWwp+b97rQ12QdSs9CLhT2n/A==",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.23.5",
"@mui/base": "5.0.0-beta.27",
"@mui/core-downloads-tracker": "^5.15.0",
"@mui/system": "^5.15.0",
"@babel/runtime": "^7.23.6",
"@mui/base": "5.0.0-beta.29",
"@mui/core-downloads-tracker": "^5.15.2",
"@mui/system": "^5.15.2",
"@mui/types": "^7.2.11",
"@mui/utils": "^5.15.0",
"@types/react-transition-group": "^4.4.9",
"@mui/utils": "^5.15.2",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.0.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1",
@@ -746,12 +752,12 @@
}
},
"node_modules/@mui/private-theming": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.0.tgz",
"integrity": "sha512-7WxtIhXxNek0JjtsYy+ut2LtFSLpsUW5JSDehQO+jF7itJ8ehy7Bd9bSt2yIllbwGjCFowLfYpPk2Ykgvqm1tA==",
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.2.tgz",
"integrity": "sha512-KlXx5TH1Mw9omSY+Q6rz5TA/P71meSYaAOeopiW8s6o433+fnOxS17rZbmd1RnDZGCo+j24TfCavQuCMBAZnQA==",
"dependencies": {
"@babel/runtime": "^7.23.5",
"@mui/utils": "^5.15.0",
"@babel/runtime": "^7.23.6",
"@mui/utils": "^5.15.2",
"prop-types": "^15.8.1"
},
"engines": {
@@ -772,11 +778,11 @@
}
},
"node_modules/@mui/styled-engine": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.0.tgz",
"integrity": "sha512-6NysIsHkuUS2lF+Lzv1jiK3UjBJk854/vKVcJQVGKlPiqNEVZJNlwaSpsaU5xYXxWEZYfbVFSAomLOS/LV/ovQ==",
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.2.tgz",
"integrity": "sha512-fYEN3IZzbebeHwAmQHhxwruiOIi8W74709qXg/7tgtHV4byQSmPgnnKsZkg0hFlzjEbcJIRZyZI0qEecgpR2cg==",
"dependencies": {
"@babel/runtime": "^7.23.5",
"@babel/runtime": "^7.23.6",
"@emotion/cache": "^11.11.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
@@ -803,15 +809,15 @@
}
},
"node_modules/@mui/system": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.0.tgz",
"integrity": "sha512-8TPjfTlYBNB7/zBJRL4QOD9kImwdZObbiYNh0+hxvhXr2koezGx8USwPXj8y/JynbzGCkIybkUztCdWlMZe6OQ==",
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.2.tgz",
"integrity": "sha512-I7CzLiHDtU/BTobJgSk+wPGGWG95K8lYfdFEnq//wOgSrLDAdOVvl2gleDxJWO+yAbGz4RKEOnR9KuD+xQZH4A==",
"dependencies": {
"@babel/runtime": "^7.23.5",
"@mui/private-theming": "^5.15.0",
"@mui/styled-engine": "^5.15.0",
"@babel/runtime": "^7.23.6",
"@mui/private-theming": "^5.15.2",
"@mui/styled-engine": "^5.15.2",
"@mui/types": "^7.2.11",
"@mui/utils": "^5.15.0",
"@mui/utils": "^5.15.2",
"clsx": "^2.0.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
@@ -855,11 +861,11 @@
}
},
"node_modules/@mui/utils": {
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.0.tgz",
"integrity": "sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA==",
"version": "5.15.2",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.2.tgz",
"integrity": "sha512-6dGM9/guFKBlFRHA7/mbM+E7wE7CYDy9Ny4JLtD3J+NTyhi8nd8YxlzgAgTaTVqY0BpdQ2zdfB/q6+p2EdGM0w==",
"dependencies": {
"@babel/runtime": "^7.23.5",
"@babel/runtime": "^7.23.6",
"@types/prop-types": "^15.7.11",
"prop-types": "^15.8.1",
"react-is": "^18.2.0"
@@ -1088,9 +1094,9 @@
}
},
"node_modules/@prisma/client": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.0.tgz",
"integrity": "sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==",
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.1.tgz",
"integrity": "sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==",
"hasInstallScript": true,
"engines": {
"node": ">=16.13"
@@ -1105,54 +1111,54 @@
}
},
"node_modules/@prisma/debug": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.0.tgz",
"integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==",
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.1.tgz",
"integrity": "sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==",
"devOptional": true
},
"node_modules/@prisma/engines": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.0.tgz",
"integrity": "sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==",
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.1.tgz",
"integrity": "sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/debug": "5.7.0",
"@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
"@prisma/fetch-engine": "5.7.0",
"@prisma/get-platform": "5.7.0"
"@prisma/debug": "5.7.1",
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
"@prisma/fetch-engine": "5.7.1",
"@prisma/get-platform": "5.7.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz",
"integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==",
"version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5.tgz",
"integrity": "sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==",
"devOptional": true
},
"node_modules/@prisma/fetch-engine": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz",
"integrity": "sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==",
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.1.tgz",
"integrity": "sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.7.0",
"@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
"@prisma/get-platform": "5.7.0"
"@prisma/debug": "5.7.1",
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
"@prisma/get-platform": "5.7.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.0.tgz",
"integrity": "sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==",
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.1.tgz",
"integrity": "sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==",
"devOptional": true,
"dependencies": {
"@prisma/debug": "5.7.0"
"@prisma/debug": "5.7.1"
}
},
"node_modules/@rushstack/eslint-patch": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz",
"integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==",
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz",
"integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==",
"dev": true
},
"node_modules/@sanity/diff-match-patch": {
@@ -1323,6 +1329,15 @@
"@types/unist": "*"
}
},
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/json5": {
"version": "0.0.29",
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
@@ -1343,14 +1358,20 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
"version": "20.10.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
"version": "20.10.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
"integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/nprogress": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz",
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
"dev": true
},
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
@@ -1377,19 +1398,28 @@
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
},
"node_modules/@types/react": {
"version": "18.2.45",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.45.tgz",
"integrity": "sha512-TtAxCNrlrBp8GoeEp1npd5g+d/OejJHFxS3OWmrPBMFaVQMSN0OFySozJio5BHxTuTeug00AVXVAjfDSfk+lUg==",
"version": "18.2.46",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.46.tgz",
"integrity": "sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==",
"dependencies": {
"@types/prop-types": "*",
"@types/scheduler": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-beautiful-dnd": {
"version": "13.1.8",
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz",
"integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.17",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz",
"integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==",
"version": "18.2.18",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz",
"integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==",
"dev": true,
"dependencies": {
"@types/react": "*"
@@ -1404,10 +1434,21 @@
"@types/react": "*"
}
},
"node_modules/@types/react-redux": {
"version": "7.1.33",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz",
"integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==",
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0",
"redux": "^4.0.0"
}
},
"node_modules/@types/react-timeago": {
"version": "4.1.6",
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.6.tgz",
"integrity": "sha512-BFUH7FEple9m0K8dNvOqhba3+iCyrgsLcRtAnaZ6HlXvG8AJfC/7NCDcLaXfB1jvpAezwDRi/BflzdaLI4+Fow==",
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.7.tgz",
"integrity": "sha512-ogD4Ror/hDG+pQggCX+TgPgJ8W2jeeUxsgNU485Qpm0Ma+E2TND2EJuKwK5+sxlkDXDEgsHradO0zWBkTgLzNg==",
"dev": true,
"dependencies": {
"@types/react": "*"
@@ -1439,15 +1480,15 @@
"dev": true
},
"node_modules/@typescript-eslint/parser": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz",
"integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==",
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz",
"integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.14.0",
"@typescript-eslint/types": "6.14.0",
"@typescript-eslint/typescript-estree": "6.14.0",
"@typescript-eslint/visitor-keys": "6.14.0",
"@typescript-eslint/scope-manager": "6.16.0",
"@typescript-eslint/types": "6.16.0",
"@typescript-eslint/typescript-estree": "6.16.0",
"@typescript-eslint/visitor-keys": "6.16.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1467,13 +1508,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz",
"integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==",
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz",
"integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.14.0",
"@typescript-eslint/visitor-keys": "6.14.0"
"@typescript-eslint/types": "6.16.0",
"@typescript-eslint/visitor-keys": "6.16.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -1484,9 +1525,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz",
"integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==",
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.16.0.tgz",
"integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -1497,16 +1538,17 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz",
"integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==",
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz",
"integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.14.0",
"@typescript-eslint/visitor-keys": "6.14.0",
"@typescript-eslint/types": "6.16.0",
"@typescript-eslint/visitor-keys": "6.16.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
"minimatch": "9.0.3",
"semver": "^7.5.4",
"ts-api-utils": "^1.0.1"
},
@@ -1523,13 +1565,37 @@
}
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz",
"integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==",
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.14.0",
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "6.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz",
"integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.16.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@@ -1553,6 +1619,12 @@
"server-only": "^0.0.1"
}
},
"node_modules/@vercel/speed-insights": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.0.2.tgz",
"integrity": "sha512-y5HWeB6RmlyVYxJAMrjiDEz8qAIy2cit0fhBq+MD78WaUwQvuBnQlX4+5MuwVTWi46bV3klaRMq83u9zUy1KOg==",
"hasInstallScript": true
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@@ -1560,9 +1632,9 @@
"optional": true
},
"node_modules/acorn": {
"version": "8.11.2",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"bin": {
"acorn": "bin/acorn"
},
@@ -1970,9 +2042,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001568",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz",
"integrity": "sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A==",
"version": "1.0.30001572",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz",
"integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==",
"funding": [
{
"type": "opencollective",
@@ -2078,9 +2150,9 @@
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
},
"node_modules/clsx": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
"engines": {
"node": ">=6"
}
@@ -2192,6 +2264,14 @@
"node": ">= 8"
}
},
"node_modules/css-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
"dependencies": {
"tiny-invariant": "^1.0.6"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@@ -2563,15 +2643,15 @@
}
},
"node_modules/eslint": {
"version": "8.55.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
"version": "8.56.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.4",
"@eslint/js": "8.55.0",
"@eslint/js": "8.56.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@@ -2715,9 +2795,9 @@
}
},
"node_modules/eslint-plugin-import": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz",
"integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==",
"version": "2.29.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
"dev": true,
"dependencies": {
"array-includes": "^3.1.7",
@@ -2736,7 +2816,7 @@
"object.groupby": "^1.0.1",
"object.values": "^1.1.7",
"semver": "^6.3.1",
"tsconfig-paths": "^3.14.2"
"tsconfig-paths": "^3.15.0"
},
"engines": {
"node": ">=4"
@@ -3050,9 +3130,9 @@
"dev": true
},
"node_modules/fastq": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
"integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
"dev": true,
"dependencies": {
"reusify": "^1.0.4"
@@ -4551,6 +4631,11 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -5329,6 +5414,11 @@
"set-blocking": "^2.0.0"
}
},
"node_modules/nprogress": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -5705,13 +5795,13 @@
}
},
"node_modules/prisma": {
"version": "5.7.0",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.0.tgz",
"integrity": "sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==",
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.1.tgz",
"integrity": "sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
"@prisma/engines": "5.7.0"
"@prisma/engines": "5.7.1"
},
"bin": {
"prisma": "build/index.js"
@@ -5796,6 +5886,11 @@
}
]
},
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
},
"node_modules/react": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
@@ -5807,6 +5902,24 @@
"node": ">=0.10.0"
}
},
"node_modules/react-beautiful-dnd": {
"version": "13.1.1",
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
"integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
"dependencies": {
"@babel/runtime": "^7.9.2",
"css-box-model": "^1.2.0",
"memoize-one": "^5.1.1",
"raf-schd": "^4.0.2",
"react-redux": "^7.2.0",
"redux": "^4.0.4",
"use-memo-one": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -5861,6 +5974,44 @@
"react": ">=18"
}
},
"node_modules/react-redux": {
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
"hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0",
"prop-types": "^15.7.2",
"react-is": "^17.0.2"
},
"peerDependencies": {
"react": "^16.8.3 || ^17 || ^18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
}
},
"node_modules/react-redux/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
},
"node_modules/react-resizable-panels": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-1.0.5.tgz",
"integrity": "sha512-OP0whNQCko+f4BgoptGaeIc7StBRyeMeJ+8r/7rXACBDf9W5EcMWuM32hfqPDMenS2HFy/eZVi/r8XqK+ZIEag==",
"peerDependencies": {
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-ssr-prepass": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
@@ -5904,6 +6055,14 @@
"string_decoder": "~0.10.x"
}
},
"node_modules/redux": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"dependencies": {
"@babel/runtime": "^7.9.2"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
@@ -5925,9 +6084,9 @@
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.0",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
},
"node_modules/regexp.prototype.flags": {
"version": "1.5.1",
@@ -6577,9 +6736,9 @@
}
},
"node_modules/tesseract.js": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.0.3.tgz",
"integrity": "sha512-UcEaIRQ+KSjxl57SS2WQgnac8hON0uJgqR6418MYLBeFPGqrAooNwcKVIoSVCzflO3CxTKPg5Ic7z7JuW7wOGA==",
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.0.4.tgz",
"integrity": "sha512-GCIoSQMZlvTP2AaHrjUOH29/oyO7ZyHVe+BhTexEcO7/nDClRVDRjl2sYJLOWSSNbTDrm5q2m1+gfaf3lUrZ5Q==",
"hasInstallScript": true,
"dependencies": {
"bmp-js": "^0.1.0",
@@ -6624,6 +6783,11 @@
"xtend": "~2.1.1"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@@ -6688,9 +6852,9 @@
}
},
"node_modules/tsconfig-paths": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
"integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
"version": "3.15.0",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
"integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
"dev": true,
"dependencies": {
"@types/json5": "^0.0.29",
@@ -6946,6 +7110,14 @@
"integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==",
"dev": true
},
"node_modules/use-memo-one": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
+22 -16
View File
@@ -1,6 +1,6 @@
{
"name": "big-agi",
"version": "1.8.0",
"version": "1.11.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -15,54 +15,60 @@
"dependencies": {
"@dqbd/tiktoken": "^1.0.7",
"@emotion/cache": "^11.11.0",
"@emotion/react": "^11.11.1",
"@emotion/react": "^11.11.3",
"@emotion/server": "^11.11.0",
"@emotion/styled": "^11.11.0",
"@mui/icons-material": "^5.15.0",
"@mui/joy": "^5.0.0-beta.18",
"@mui/icons-material": "^5.15.2",
"@mui/joy": "^5.0.0-beta.20",
"@next/bundle-analyzer": "^14.0.4",
"@prisma/client": "^5.7.0",
"@prisma/client": "^5.7.1",
"@sanity/diff-match-patch": "^3.1.1",
"@t3-oss/env-nextjs": "^0.7.1",
"@tanstack/react-query": "~4.36.1",
"@trpc/client": "^10.44.1",
"@trpc/next": "^10.44.1",
"@trpc/react-query": "^10.44.1",
"@trpc/server": "^10.44.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.1.1",
"@vercel/speed-insights": "^1.0.2",
"browser-fs-access": "^0.35.0",
"eventsource-parser": "^1.1.1",
"idb-keyval": "^6.2.1",
"next": "^14.0.4",
"nprogress": "^0.2.0",
"pdfjs-dist": "4.0.269",
"plantuml-encoder": "^1.4.0",
"prismjs": "^1.29.0",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-dom": "^18.2.0",
"react-katex": "^3.0.1",
"react-markdown": "^9.0.1",
"react-resizable-panels": "^1.0.5",
"react-timeago": "^7.2.0",
"remark-gfm": "^4.0.0",
"superjson": "^2.2.1",
"tesseract.js": "^5.0.3",
"tesseract.js": "^5.0.4",
"uuid": "^9.0.1",
"zod": "^3.22.4",
"zustand": "^4.4.7"
},
"devDependencies": {
"@cloudflare/puppeteer": "^0.0.5",
"@types/node": "^20.10.4",
"@types/node": "^20.10.6",
"@types/nprogress": "^0.2.3",
"@types/plantuml-encoder": "^1.4.2",
"@types/prismjs": "^1.26.3",
"@types/react": "^18.2.45",
"@types/react-dom": "^18.2.17",
"@types/react": "^18.2.46",
"@types/react-beautiful-dnd": "^13.1.8",
"@types/react-dom": "^18.2.18",
"@types/react-katex": "^3.0.4",
"@types/react-timeago": "^4.1.6",
"@types/react-timeago": "^4.1.7",
"@types/uuid": "^9.0.7",
"eslint": "^8.55.0",
"eslint": "^8.56.0",
"eslint-config-next": "^14.0.4",
"prettier": "^3.1.1",
"prisma": "^5.7.0",
"prisma": "^5.7.1",
"typescript": "^5.3.3"
},
"engines": {
+20 -12
View File
@@ -2,6 +2,8 @@ import * as React from 'react';
import Head from 'next/head';
import { MyAppProps } from 'next/app';
import { Analytics as VercelAnalytics } from '@vercel/analytics/react';
import { SpeedInsights as VercelSpeedInsights } from '@vercel/speed-insights/next';
import { Brand } from '~/common/app.config';
import { apiQuery } from '~/common/util/trpc.client';
@@ -9,12 +11,15 @@ import { apiQuery } from '~/common/util/trpc.client';
import 'katex/dist/katex.min.css';
import '~/common/styles/CodePrism.css';
import '~/common/styles/GithubMarkdown.css';
import '~/common/styles/NProgress.css';
import '~/common/styles/app.styles.css';
import { ProviderBackend } from '~/common/state/ProviderBackend';
import { ProviderSingleTab } from '~/common/state/ProviderSingleTab';
import { ProviderSnacks } from '~/common/state/ProviderSnacks';
import { ProviderTRPCQueryClient } from '~/common/state/ProviderTRPCQueryClient';
import { ProviderTheming } from '~/common/state/ProviderTheming';
import { ProviderBackendAndNoSSR } from '~/common/providers/ProviderBackendAndNoSSR';
import { ProviderBootstrapLogic } from '~/common/providers/ProviderBootstrapLogic';
import { ProviderSingleTab } from '~/common/providers/ProviderSingleTab';
import { ProviderSnacks } from '~/common/providers/ProviderSnacks';
import { ProviderTRPCQueryClient } from '~/common/providers/ProviderTRPCQueryClient';
import { ProviderTheming } from '~/common/providers/ProviderTheming';
const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
@@ -27,17 +32,20 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
<ProviderTheming emotionCache={emotionCache}>
<ProviderSingleTab>
<ProviderTRPCQueryClient>
<ProviderSnacks>
<ProviderBackend>
<Component {...pageProps} />
</ProviderBackend>
</ProviderSnacks>
</ProviderTRPCQueryClient>
<ProviderBootstrapLogic>
<ProviderTRPCQueryClient>
<ProviderSnacks>
<ProviderBackendAndNoSSR>
<Component {...pageProps} />
</ProviderBackendAndNoSSR>
</ProviderSnacks>
</ProviderTRPCQueryClient>
</ProviderBootstrapLogic>
</ProviderSingleTab>
</ProviderTheming>
<VercelAnalytics debug={false} />
<VercelSpeedInsights debug={false} />
</>;
+5 -5
View File
@@ -5,7 +5,7 @@ import createEmotionServer from '@emotion/server/create-instance';
import { getInitColorSchemeScript } from '@mui/joy/styles';
import { Brand } from '~/common/app.config';
import { bodyFontClassName, createEmotionCache } from '~/common/app.theme';
import { createEmotionCache } from '~/common/app.theme';
interface MyDocumentProps extends DocumentProps {
@@ -14,7 +14,7 @@ interface MyDocumentProps extends DocumentProps {
export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
return (
<Html lang='en' className={bodyFontClassName}>
<Html lang='en'>
<Head>
{/* Meta (missing Title, set by the App or Page) */}
<meta name='description' content={Brand.Meta.Description} />
@@ -51,9 +51,9 @@ export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
{emotionStyleTags}
</Head>
<body>
{getInitColorSchemeScript()}
<Main />
<NextScript />
{getInitColorSchemeScript()}
<Main />
<NextScript />
</body>
</Html>
);
+2 -6
View File
@@ -2,13 +2,9 @@ import * as React from 'react';
import { AppCall } from '../src/apps/call/AppCall';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function CallPage() {
return (
<AppLayout>
<AppCall />
</AppLayout>
);
return withLayout({ type: 'optima' }, <AppCall />);
}
+9 -10
View File
@@ -1,18 +1,17 @@
import * as React from 'react';
import { AppChat } from '../src/apps/chat/AppChat';
import { useShowNewsOnUpdate } from '../src/apps/news/news.hooks';
import { useRedirectToNewsOnUpdates } from '../src/apps/news/news.hooks';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function ChatPage() {
// show the News page on updates
useShowNewsOnUpdate();
export default function IndexPage() {
// show the News page if there are unseen updates
useRedirectToNewsOnUpdates();
return (
<AppLayout>
<AppChat />
</AppLayout>
);
// TODO: This Index page will point to the Dashboard (or a landing page) soon
// For now it offers the chat experience, but this will change. #299
return withLayout({ type: 'optima' }, <AppChat />);
}
+9 -15
View File
@@ -1,15 +1,14 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { Box, Typography } from '@mui/joy';
import { useModelsStore } from '~/modules/llms/store-llms';
import { AppLayout } from '~/common/layout/AppLayout';
import { InlineError } from '~/common/components/InlineError';
import { apiQuery } from '~/common/util/trpc.client';
import { navigateToIndex } from '~/common/app.routes';
import { openLayoutModelsSetup } from '~/common/layout/store-applayout';
import { navigateToIndex, useRouterQuery } from '~/common/app.routes';
import { themeBgApp } from '~/common/app.theme';
import { withLayout } from '~/common/layout/withLayout';
function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
@@ -36,14 +35,14 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
useModelsStore.getState().setOpenRoutersKey(openRouterKey);
// 2. Navigate to the chat app
navigateToIndex(true).then(() => openLayoutModelsSetup());
void navigateToIndex(true); //.then(openModelsSetup);
}, [isSuccess, openRouterKey]);
return (
<Box sx={{
flexGrow: 1,
backgroundColor: 'background.level1',
backgroundColor: themeBgApp,
overflowY: 'auto',
display: 'flex', justifyContent: 'center',
p: { xs: 3, md: 6 },
@@ -84,15 +83,10 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
* Docs: https://openrouter.ai/docs#oauth
* Example URL: https://localhost:3000/link/callback_openrouter?code=SomeCode
*/
export default function Page() {
export default function CallbackPage() {
// get the 'code=...' from the URL
const { query } = useRouter();
const { code: openRouterCode } = query;
// external state - get the 'code=...' from the URL
const { code } = useRouterQuery<{ code: string | undefined }>();
return (
<AppLayout suspendAutoModelsSetup>
<CallbackOpenRouterPage openRouterCode={openRouterCode as (string | undefined)} />
</AppLayout>
);
return withLayout({ type: 'plain' }, <CallbackOpenRouterPage openRouterCode={code} />);
}
+6 -9
View File
@@ -1,18 +1,15 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { AppChatLink } from '../../../src/apps/link/AppChatLink';
import { AppLayout } from '~/common/layout/AppLayout';
import { useRouterQuery } from '~/common/app.routes';
import { withLayout } from '~/common/layout/withLayout';
export default function ChatLinkPage() {
const { query } = useRouter();
const chatLinkId = query?.chatLinkId as string ?? '';
return (
<AppLayout suspendAutoModelsSetup>
<AppChatLink linkId={chatLinkId} />
</AppLayout>
);
// external state
const { chatLinkId } = useRouterQuery<{ chatLinkId: string | undefined }>();
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppChatLink linkId={chatLinkId || ''} />);
}
+14 -16
View File
@@ -1,5 +1,4 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { Alert, Box, Button, Typography } from '@mui/joy';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -8,10 +7,11 @@ import { setComposerStartupText } from '../../src/apps/chat/components/composer/
import { callBrowseFetchPage } from '~/modules/browse/browse.client';
import { AppLayout } from '~/common/layout/AppLayout';
import { LogoProgress } from '~/common/components/LogoProgress';
import { asValidURL } from '~/common/util/urlUtils';
import { navigateToIndex } from '~/common/app.routes';
import { navigateToIndex, useRouterQuery } from '~/common/app.routes';
import { themeBgApp } from '~/common/app.theme';
import { withLayout } from '~/common/layout/withLayout';
/**
@@ -31,8 +31,10 @@ function AppShareTarget() {
const [isDownloading, setIsDownloading] = React.useState(false);
// external state
const { query } = useRouter();
const { url: queryUrl, text: queryText } = useRouterQuery<{
url: string | string[] | undefined,
text: string | string[] | undefined,
}>();
const queueComposerTextAndLaunchApp = React.useCallback((text: string) => {
setComposerStartupText(text);
@@ -43,11 +45,11 @@ function AppShareTarget() {
// Detect the share Intent from the query
React.useEffect(() => {
// skip when query is not parsed yet
if (!Object.keys(query).length)
let queryTextItem = queryUrl || queryText || null;
if (!queryTextItem)
return;
// single item from the query
let queryTextItem: string[] | string | null = query.url || query.text || null;
if (Array.isArray(queryTextItem))
queryTextItem = queryTextItem[0];
@@ -58,9 +60,9 @@ function AppShareTarget() {
else if (queryTextItem)
setIntentText(queryTextItem);
else
setErrorMessage('No text or url. Received: ' + JSON.stringify(query));
setErrorMessage('No text or url. Received: ' + JSON.stringify({ queryText, queryUrl }));
}, [query.url, query.text, query]);
}, [queryText, queryUrl]);
// Text -> Composer
@@ -90,7 +92,7 @@ function AppShareTarget() {
return (
<Box sx={{
backgroundColor: 'background.level2',
backgroundColor: themeBgApp,
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
flexGrow: 1,
}}>
@@ -132,10 +134,6 @@ function AppShareTarget() {
* This page will be invoked on mobile when sharing Text/URLs/Files from other APPs
* Example URL: https://localhost:3000/link/share_target?title=This+Title&text=https%3A%2F%2Fexample.com%2Fapp%2Fpath
*/
export default function LaunchPage() {
return (
<AppLayout>
<AppShareTarget />
</AppLayout>
);
export default function ShareTargetPage() {
return withLayout({ type: 'plain' }, <AppShareTarget />);
}
+3 -7
View File
@@ -3,16 +3,12 @@ import * as React from 'react';
import { AppNews } from '../src/apps/news/AppNews';
import { useMarkNewsAsSeen } from '../src/apps/news/news.hooks';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function NewsPage() {
// update the last seen news version
// 'touch' the last seen news version
useMarkNewsAsSeen();
return (
<AppLayout suspendAutoModelsSetup>
<AppNews />
</AppLayout>
);
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppNews />);
}
+2 -6
View File
@@ -2,13 +2,9 @@ import * as React from 'react';
import { AppPersonas } from '../src/apps/personas/AppPersonas';
import { AppLayout } from '~/common/layout/AppLayout';
import { withLayout } from '~/common/layout/withLayout';
export default function PersonasPage() {
return (
<AppLayout>
<AppPersonas />
</AppLayout>
);
return withLayout({ type: 'optima' }, <AppPersonas />);
}
+48
View File
@@ -0,0 +1,48 @@
import * as React from 'react';
import { Box, Typography } from '@mui/joy';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { themeBgApp } from '~/common/app.theme';
import { useRouterRoute } from '~/common/app.routes';
/**
* https://github.com/enricoros/big-AGI/issues/299
*/
export function AppPlaceholder() {
// external state
const route = useRouterRoute();
// derived state
const placeholderAppName = capitalizeFirstLetter(route.replace('/', '') || 'Home');
return (
<Box sx={{
flexGrow: 1,
backgroundColor: themeBgApp,
overflowY: 'auto',
p: { xs: 3, md: 6 },
border: '1px solid blue',
}}>
<Box sx={{
my: 'auto',
display: 'flex', flexDirection: 'column', alignItems: 'center',
gap: 4,
border: '1px solid red',
}}>
<Typography level='h1'>
{placeholderAppName}
</Typography>
<Typography>
Intelligent applications to help you learn, think, and do
</Typography>
</Box>
</Box>
);
}
+4 -5
View File
@@ -1,9 +1,8 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { Container, Sheet } from '@mui/joy';
import { AppCallQueryParams } from '~/common/app.routes';
import { AppCallQueryParams, useRouterQuery } from '~/common/app.routes';
import { InlineError } from '~/common/components/InlineError';
import { CallUI } from './CallUI';
@@ -11,11 +10,11 @@ import { CallWizard } from './CallWizard';
export function AppCall() {
// external state
const { query } = useRouter();
const { conversationId, personaId } = useRouterQuery<AppCallQueryParams>();
// derived state
const { conversationId, personaId } = query as any as AppCallQueryParams;
const validInput = !!conversationId && !!personaId;
return (
@@ -33,7 +32,7 @@ export function AppCall() {
gap: { xs: 2, md: 4 },
}}>
{!validInput && <InlineError error={`Something went wrong. ${JSON.stringify(query)}`} />}
{!validInput && <InlineError error={`Something went wrong. ${conversationId}:${personaId}`} />}
{validInput && (
<CallWizard conversationId={conversationId}>
+6 -9
View File
@@ -1,6 +1,5 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { useRouter } from 'next/router';
import { Box, Card, ListItemDecorator, MenuItem, Switch, Typography } from '@mui/joy';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
@@ -22,12 +21,13 @@ import { Link } from '~/common/components/Link';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { conversationTitle, createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
import { playSoundUrl, usePlaySoundUrl } from '~/common/util/audioUtils';
import { useLayoutPluggable } from '~/common/layout/store-applayout';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { CallAvatar } from './components/CallAvatar';
import { CallButton } from './components/CallButton';
import { CallMessage } from './components/CallMessage';
import { CallStatus } from './components/CallStatus';
import { launchAppChat, ROUTE_APP_CHAT } from '~/common/app.routes';
function CallMenuItems(props: {
@@ -88,7 +88,6 @@ export function CallUI(props: {
const responseAbortController = React.useRef<AbortController | null>(null);
// external state
const { push: routerPush } = useRouter();
const { chatLLMId, chatLLMDropdown } = useChatLLMDropdown();
const { chatTitle, messages } = useChatStore(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
@@ -177,9 +176,7 @@ export function CallUI(props: {
// command: close the call
case 'Goodbye.':
setStage('ended');
setTimeout(() => {
void routerPush('/');
}, 2000);
setTimeout(launchAppChat, 2000);
return;
// command: regenerate answer
case 'Retry.':
@@ -235,7 +232,7 @@ export function CallUI(props: {
responseAbortController.current?.abort();
responseAbortController.current = null;
};
}, [isConnected, callMessages, chatLLMId, messages, personaVoiceId, personaSystemMessage, routerPush]);
}, [isConnected, callMessages, chatLLMId, messages, personaVoiceId, personaSystemMessage]);
// [E] Message interrupter
const abortTrigger = isConnected && isRecordingSpeech;
@@ -272,7 +269,7 @@ export function CallUI(props: {
, [overridePersonaVoice, pushToTalk],
);
useLayoutPluggable(chatLLMDropdown, null, menuItems);
usePluggableOptimaLayout(null, chatLLMDropdown, menuItems, 'CallUI');
return <>
@@ -366,7 +363,7 @@ export function CallUI(props: {
)}
{/* [ended] Back / Call Again */}
{(isEnded || isDeclined) && <Link noLinkStyle href='/'><CallButton Icon={ArrowBackIcon} text='Back' variant='soft' /></Link>}
{(isEnded || isDeclined) && <Link noLinkStyle href={ROUTE_APP_CHAT}><CallButton Icon={ArrowBackIcon} text='Back' variant='soft' /></Link>}
{(isEnded || isDeclined) && <CallButton Icon={CallIcon} text='Call Again' color='success' variant='soft' onClick={() => setStage('connected')} />}
</Box>
+4 -3
View File
@@ -11,9 +11,9 @@ import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
import WarningIcon from '@mui/icons-material/Warning';
import { navigateBack } from '~/common/app.routes';
import { openLayoutPreferences } from '~/common/layout/store-applayout';
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
import { useChatStore } from '~/common/state/store-chats';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUICounter } from '~/common/state/store-ui';
@@ -81,6 +81,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
const [recognitionOverride, setRecognitionOverride] = React.useState(false);
// external state
const { openPreferencesTab } = useOptimaLayout();
const recognition = useCapabilityBrowserSpeechRecognition();
const synthesis = useCapabilityElevenLabs();
const chatIsEmpty = useChatStore(state => {
@@ -103,7 +104,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
const handleOverrideRecognition = () => setRecognitionOverride(true);
const handleConfigureElevenLabs = () => {
openLayoutPreferences(3);
openPreferencesTab(3);
};
const handleFinishButton = () => {
@@ -199,7 +200,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
<IconButton
size='lg' variant={allGood ? 'soft' : 'solid'} color={allGood ? 'success' : 'danger'}
onClick={handleFinishButton} sx={{ borderRadius: '50px' }}
onClick={handleFinishButton} sx={{ borderRadius: '50px', mr: 0.5 }}
>
{allGood ? <ArrowForwardIcon sx={{ fontSize: '1.5em' }} /> : <CloseIcon sx={{ fontSize: '1.5em' }} />}
</IconButton>
+243 -155
View File
@@ -1,46 +1,53 @@
import * as React from 'react';
import { Box } from '@mui/joy';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import { Panel, PanelGroup } from 'react-resizable-panels';
import { useTheme } from '@mui/joy';
import { CmdRunBrowse } from '~/modules/browse/browse.client';
import { CmdRunProdia } from '~/modules/prodia/prodia.client';
import { CmdRunReact } from '~/modules/aifn/react/react';
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
import { getChatLLMId, useChatLLM } from '~/modules/llms/store-llms';
import { imaginePromptFromText } from '~/modules/aifn/imagine/imaginePromptFromText';
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
import { useBrowseStore } from '~/modules/browse/store-module-browsing';
import { useChatLLM, useModelsStore } from '~/modules/llms/store-llms';
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
import { Brand } from '~/common/app.config';
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
import { GoodPanelResizeHandler } from '~/common/components/panes/GoodPanelResizeHandler';
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
import { createDMessage, DConversationId, DMessage, getConversation, useConversation } from '~/common/state/store-chats';
import { openLayoutLLMOptions, useLayoutPluggable } from '~/common/layout/store-applayout';
import { themeBgApp, themeBgAppChatComposer } from '~/common/app.theme';
import { useFolderStore } from '~/common/state/store-folders';
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
import { ChatDrawerItemsMemo } from './components/applayout/ChatDrawerItems';
import { ChatDrawerContentMemo } from './components/applayout/ChatDrawerItems';
import { ChatDropdowns } from './components/applayout/ChatDropdowns';
import { ChatMenuItems } from './components/applayout/ChatMenuItems';
import { ChatMessageList } from './components/ChatMessageList';
import { CmdAddRoleMessage, CmdHelp, createCommandsHelpMessage, extractCommands } from './editors/commands';
import { Composer } from './components/composer/Composer';
import { Ephemerals } from './components/Ephemerals';
import { usePanesManager } from './components/usePanesManager';
import { ScrollToBottom } from './components/scroll-to-bottom/ScrollToBottom';
import { ScrollToBottomButton } from './components/scroll-to-bottom/ScrollToBottomButton';
import { usePanesManager } from './components/panes/usePanesManager';
import { extractChatCommand, findAllChatCommands } from './commands/commands.registry';
import { runAssistantUpdatingState } from './editors/chat-stream';
import { runBrowseUpdatingState } from './editors/browse-load';
import { runImageGenerationUpdatingState } from './editors/image-generate';
import { runReActUpdatingState } from './editors/react-tangent';
/**
* Mode: how to treat the input from the Composer
*/
export type ChatModeId = 'immediate' | 'write-user' | 'react' | 'draw-imagine' | 'draw-imagine-plus';
export type ChatModeId =
| 'generate-text'
| 'append-user'
| 'generate-image'
| 'generate-react';
const SPECIAL_ID_WIPE_ALL: DConversationId = 'wipe-chats';
@@ -56,8 +63,13 @@ export function AppChat() {
const [flattenConversationId, setFlattenConversationId] = React.useState<DConversationId | null>(null);
const showNextTitle = React.useRef(false);
const composerTextAreaRef = React.useRef<HTMLTextAreaElement>(null);
const [_selectedFolderId, setSelectedFolderId] = React.useState<string | null>(null);
// external state
const theme = useTheme();
const { openLlmOptions } = useOptimaLayout();
const { chatLLM } = useChatLLM();
const {
@@ -66,7 +78,10 @@ export function AppChat() {
navigateHistoryInFocusedPane,
openConversationInFocusedPane,
openConversationInSplitPane,
setFocusedPaneIndex,
paneIndex,
duplicatePane,
removePane,
setFocusedPane,
} = usePanesManager();
const {
@@ -75,6 +90,7 @@ export function AppChat() {
isChatEmpty: isFocusedChatEmpty,
areChatsEmpty,
newConversationId,
conversationsLength,
_remove_systemPurposeId: focusedSystemPurposeId,
prependNewConversation,
branchConversation,
@@ -83,14 +99,22 @@ export function AppChat() {
setMessages,
} = useConversation(focusedConversationId);
const { mayWork: capabilityHasT2I } = useCapabilityTextToImage();
const { folderConversationsCount, selectedFolderId } = useFolderStore(state => {
const selectedFolderId = state.useFolders ? _selectedFolderId : null;
return {
folderConversationsCount: selectedFolderId
? state.folders.find(folder => folder.id === selectedFolderId)?.conversationIds.length || 0
: conversationsLength,
selectedFolderId,
};
});
// Window actions
const chatPaneIDs = chatPanes.length > 0 ? chatPanes.map(pane => pane.conversationId) : [null];
const setActivePaneIndex = React.useCallback((idx: number) => {
setFocusedPaneIndex(idx);
}, [setFocusedPaneIndex]);
const panesConversationIDs = chatPanes.length > 0 ? chatPanes.map((pane) => pane.conversationId) : [null];
const isSplitPane = chatPanes.length > 1;
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
conversationId && openConversationInFocusedPane(conversationId);
@@ -100,6 +124,13 @@ export function AppChat() {
conversationId && openConversationInSplitPane(conversationId);
}, [openConversationInSplitPane]);
const toggleSplitPane = React.useCallback(() => {
if (isSplitPane)
removePane(paneIndex ?? chatPanes.length - 1);
else
duplicatePane(paneIndex ?? chatPanes.length - 1);
}, [chatPanes.length, duplicatePane, isSplitPane, paneIndex, removePane]);
const handleNavigateHistory = React.useCallback((direction: 'back' | 'forward') => {
if (navigateHistoryInFocusedPane(direction))
showNextTitle.current = true;
@@ -114,39 +145,45 @@ export function AppChat() {
}
}, [focusedChatNumber, focusedChatTitle]);
// Execution
const _handleExecute = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) => {
const { chatLLMId } = useModelsStore.getState();
const chatLLMId = getChatLLMId();
if (!chatModeId || !conversationId || !chatLLMId) return;
// "/command ...": overrides the chat mode
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
if (lastMessage?.role === 'user') {
const pieces = extractCommands(lastMessage.text);
if (pieces.length == 2 && pieces[0].type === 'cmd' && pieces[1].type === 'text') {
const [command, prompt] = [pieces[0].value, pieces[1].value];
if (CmdRunProdia.includes(command)) {
setMessages(conversationId, history);
return await runImageGenerationUpdatingState(conversationId, prompt);
}
if (CmdRunReact.includes(command) && chatLLMId) {
setMessages(conversationId, history);
return await runReActUpdatingState(conversationId, prompt, chatLLMId);
}
if (CmdRunBrowse.includes(command) && prompt?.trim() && useBrowseStore.getState().enableCommandBrowse) {
setMessages(conversationId, history);
return await runBrowseUpdatingState(conversationId, prompt);
}
if (CmdAddRoleMessage.includes(command)) {
lastMessage.role = command.startsWith('/s') ? 'system' : command.startsWith('/a') ? 'assistant' : 'user';
lastMessage.sender = 'Bot';
lastMessage.text = prompt;
return setMessages(conversationId, history);
}
if (CmdHelp.includes(command)) {
return setMessages(conversationId, [...history, createCommandsHelpMessage()]);
const chatCommand = extractChatCommand(lastMessage.text)[0];
if (chatCommand && chatCommand.type === 'cmd') {
switch (chatCommand.providerId) {
case 'ass-browse':
setMessages(conversationId, history);
return await runBrowseUpdatingState(conversationId, chatCommand.params!);
case 'ass-t2i':
setMessages(conversationId, history);
return await runImageGenerationUpdatingState(conversationId, chatCommand.params!);
case 'ass-react':
setMessages(conversationId, history);
return await runReActUpdatingState(conversationId, chatCommand.params!, chatLLMId);
case 'chat-alter':
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]);
}
}
}
@@ -154,27 +191,27 @@ export function AppChat() {
// synchronous long-duration tasks, which update the state as they go
if (chatLLMId && focusedSystemPurposeId) {
switch (chatModeId) {
case 'immediate':
case 'generate-text':
return await runAssistantUpdatingState(conversationId, history, chatLLMId, focusedSystemPurposeId);
case 'write-user':
case 'append-user':
return setMessages(conversationId, history);
case 'react':
case 'generate-image':
if (!lastMessage?.text)
break;
// 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);
case 'draw-imagine':
case 'draw-imagine-plus':
if (!lastMessage?.text)
break;
const imagePrompt = chatModeId == 'draw-imagine-plus'
? await imaginePromptFromText(lastMessage.text) || 'An error sign.'
: lastMessage.text;
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
...message,
text: `${CmdRunProdia[0]} ${imagePrompt}`,
}));
return await runImageGenerationUpdatingState(conversationId, imagePrompt);
}
}
@@ -184,7 +221,6 @@ export function AppChat() {
}, [focusedSystemPurposeId, setMessages]);
const handleComposerAction = (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart): boolean => {
// validate inputs
if (multiPartMessage.length !== 1 || multiPartMessage[0].type !== 'text-block') {
addSnackbar({
@@ -213,13 +249,13 @@ export function AppChat() {
};
const handleConversationExecuteHistory = async (conversationId: DConversationId, history: DMessage[]) =>
await _handleExecute('immediate', conversationId, history);
await _handleExecute('generate-text', conversationId, history);
const handleMessageRegenerateLast = React.useCallback(async () => {
const focusedConversation = getConversation(focusedConversationId);
if (focusedConversation?.messages?.length) {
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
return await _handleExecute('immediate', focusedConversation.id, lastMessage.role === 'assistant'
return await _handleExecute('generate-text', focusedConversation.id, lastMessage.role === 'assistant'
? focusedConversation.messages.slice(0, -1)
: [...focusedConversation.messages],
);
@@ -228,30 +264,39 @@ export function AppChat() {
const handleTextDiagram = async (diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig);
const handleTextImaginePlus = async (conversationId: DConversationId, messageText: string) => {
const handleTextImagine = async (conversationId: DConversationId, messageText: string) => {
const conversation = getConversation(conversationId);
if (conversation)
return await _handleExecute('draw-imagine-plus', conversationId, [
...conversation.messages,
createDMessage('user', messageText),
]);
if (!conversation)
return;
const imaginedPrompt = await imaginePromptFromText(messageText) || 'An error sign.';
return await _handleExecute('generate-image', conversationId, [
...conversation.messages,
createDMessage('user', imaginedPrompt),
]);
};
const handleTextSpeak = async (text: string) => {
await speakText(text);
};
// Chat actions
const handleConversationNew = React.useCallback(() => {
// activate an existing new conversation if present, or create another
setFocusedConversationId(newConversationId
const conversationId = newConversationId
? newConversationId
: prependNewConversation(focusedSystemPurposeId ?? undefined),
);
: prependNewConversation(focusedSystemPurposeId ?? undefined);
setFocusedConversationId(conversationId);
// if a folder is selected, add the new conversation to the folder
if (selectedFolderId && conversationId)
useFolderStore.getState().addConversationToFolder(selectedFolderId, conversationId);
// focus the composer
composerTextAreaRef.current?.focus();
}, [focusedSystemPurposeId, newConversationId, prependNewConversation, setFocusedConversationId]);
}, [focusedSystemPurposeId, newConversationId, prependNewConversation, selectedFolderId, setFocusedConversationId]);
const handleConversationImportDialog = () => setTradeConfig({ dir: 'import' });
@@ -279,7 +324,6 @@ export function AppChat() {
const handleConversationFlatten = (conversationId: DConversationId) => setFlattenConversationId(conversationId);
const handleConfirmedClearConversation = React.useCallback(() => {
if (clearConversationId) {
setMessages(clearConversationId, []);
@@ -289,12 +333,11 @@ export function AppChat() {
const handleConversationClear = (conversationId: DConversationId) => setClearConversationId(conversationId);
const handleConfirmedDeleteConversation = () => {
if (deleteConversationId) {
let nextConversationId: DConversationId | null;
if (deleteConversationId === SPECIAL_ID_WIPE_ALL)
nextConversationId = wipeAllConversations(focusedSystemPurposeId ?? undefined);
nextConversationId = wipeAllConversations(focusedSystemPurposeId ?? undefined, selectedFolderId);
else
nextConversationId = deleteConversation(deleteConversationId);
setFocusedConversationId(nextConversationId);
@@ -304,53 +347,59 @@ export function AppChat() {
const handleConversationsDeleteAll = () => setDeleteConversationId(SPECIAL_ID_WIPE_ALL);
const handleConversationDelete = React.useCallback((conversationId: DConversationId, bypassConfirmation: boolean) => {
if (bypassConfirmation)
setFocusedConversationId(deleteConversation(conversationId));
else
setDeleteConversationId(conversationId);
}, [deleteConversation, setFocusedConversationId]);
const handleConversationDelete = React.useCallback(
(conversationId: DConversationId, bypassConfirmation: boolean) => {
if (bypassConfirmation) setFocusedConversationId(deleteConversation(conversationId));
else setDeleteConversationId(conversationId);
},
[deleteConversation, setFocusedConversationId],
);
// Shortcuts
const handleOpenChatLlmOptions = React.useCallback(() => {
const { chatLLMId } = useModelsStore.getState();
const chatLLMId = getChatLLMId();
if (!chatLLMId) return;
openLayoutLLMOptions(chatLLMId);
}, []);
openLlmOptions(chatLLMId);
}, [openLlmOptions]);
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
['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)],
['b', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationBranch(focusedConversationId, null))],
['x', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationClear(focusedConversationId))],
['d', true, false, true, () => focusedConversationId && handleConversationDelete(focusedConversationId, false)],
[ShortcutKeyName.Left, true, false, true, () => handleNavigateHistory('back')],
[ShortcutKeyName.Right, true, false, true, () => handleNavigateHistory('forward')],
], [focusedConversationId, handleConversationBranch, handleConversationDelete, handleConversationNew, handleMessageRegenerateLast, handleNavigateHistory, handleOpenChatLlmOptions, isFocusedChatEmpty]);
useGlobalShortcuts(shortcuts);
// Pluggable ApplicationBar components
const centerItems = React.useMemo(() =>
<ChatDropdowns conversationId={focusedConversationId} />,
[focusedConversationId],
<ChatDropdowns
conversationId={focusedConversationId}
isSplitPanes={isSplitPane}
onToggleSplitPanes={toggleSplitPane}
/>,
[focusedConversationId, isSplitPane, toggleSplitPane],
);
const drawerItems = React.useMemo(() =>
<ChatDrawerItemsMemo
const drawerContent = React.useMemo(() =>
<ChatDrawerContentMemo
activeConversationId={focusedConversationId}
disableNewButton={isFocusedChatEmpty}
onConversationActivate={setFocusedConversationId}
onConversationDelete={handleConversationDelete}
onConversationExportDialog={handleConversationExport}
onConversationImportDialog={handleConversationImportDialog}
onConversationNew={handleConversationNew}
onConversationsDeleteAll={handleConversationsDeleteAll}
selectedFolderId={selectedFolderId}
setSelectedFolderId={setSelectedFolderId}
/>,
[focusedConversationId, handleConversationDelete, handleConversationNew, isFocusedChatEmpty, setFocusedConversationId],
[focusedConversationId, handleConversationDelete, handleConversationNew, isFocusedChatEmpty, selectedFolderId, setFocusedConversationId],
);
const menuItems = React.useMemo(() =>
@@ -368,77 +417,107 @@ export function AppChat() {
[areChatsEmpty, focusedConversationId, handleConversationBranch, isFocusedChatEmpty, isMessageSelectionMode],
);
useLayoutPluggable(centerItems, drawerItems, menuItems);
usePluggableOptimaLayout(drawerContent, centerItems, menuItems, 'AppChat');
return <>
<Box sx={{
flexGrow: 1,
display: 'flex', flexDirection: { xs: 'column', md: 'row' },
overflow: 'clip',
}}>
<PanelGroup
direction='horizontal'
id='app-chat-panels'
>
{chatPaneIDs.map((_conversationId, idx) => (
<Box key={'chat-pane-' + idx} onClick={() => setActivePaneIndex(idx)} sx={{
flexGrow: 1, flexBasis: 1,
display: 'flex', flexDirection: 'column',
overflow: 'clip',
}}>
<ChatMessageList
conversationId={_conversationId}
chatLLMContextTokens={chatLLM?.contextTokens}
isMessageSelectionMode={isMessageSelectionMode}
setIsMessageSelectionMode={setIsMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationExecuteHistory={handleConversationExecuteHistory}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImaginePlus}
onTextSpeak={handleTextSpeak}
sx={{
flexGrow: 1,
backgroundColor: 'background.level1',
overflowY: 'auto',
minHeight: 96,
// outline the current focused pane
...(chatPaneIDs.length < 2 ? {}
: (_conversationId === focusedConversationId)
? {
border: '2px solid',
borderColor: 'primary.solidBg',
} : {
padding: '2px',
}),
{panesConversationIDs.map((_conversationId, idx, panels) =>
<React.Fragment key={`chat-pane-${idx}-${panels.length}-${_conversationId}`}>
<Panel
id={'chat-pane-' + _conversationId}
order={idx}
collapsible
defaultSize={panels.length > 0 ? Math.round(100 / panels.length) : undefined}
minSize={20}
onClick={(event) => {
const setFocus = chatPanes.length < 2 || !event.altKey;
setFocusedPane(setFocus ? idx : -1);
}}
/>
onCollapse={() => setTimeout(() => removePane(idx), 50)}
style={{
// for anchoring the scroll button in place
position: 'relative',
// border only for active pane (if two or more panes)
...(panesConversationIDs.length < 2
? {}
: (_conversationId === focusedConversationId)
? { border: `2px solid ${theme.palette.primary.solidBg}` }
: { border: `2px solid ${theme.palette.background.level1}` }),
}}
>
<Ephemerals
conversationId={_conversationId}
sx={{
// flexGrow: 0.1,
flexShrink: 0.5,
overflowY: 'auto',
minHeight: 64,
}} />
<ScrollToBottom
bootToBottom
stickToBottom
sx={{
// allows the content to be scrolled (all browsers)
overflowY: 'auto',
// actually make sure this scrolls & fills
height: '100%',
}}
>
</Box>
))}
</Box>
<ChatMessageList
conversationId={_conversationId}
capabilityHasT2I={capabilityHasT2I}
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
isMessageSelectionMode={isMessageSelectionMode}
setIsMessageSelectionMode={setIsMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationExecuteHistory={handleConversationExecuteHistory}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImagine}
onTextSpeak={handleTextSpeak}
sx={{
backgroundColor: themeBgApp,
minHeight: '100%', // ensures filling of the blank space on newer chats
}}
/>
<Ephemerals
conversationId={_conversationId}
sx={{
// TODO: Fixme post panels?
// flexGrow: 0.1,
flexShrink: 0.5,
overflowY: 'auto',
minHeight: 64,
}}
/>
{/* Visibility and actions are handled via Context */}
<ScrollToBottomButton />
</ScrollToBottom>
</Panel>
{/* Panel Separators & Resizers */}
{idx < panels.length - 1 && <GoodPanelResizeHandler />}
</React.Fragment>)}
</PanelGroup>
<Composer
chatLLM={chatLLM}
composerTextAreaRef={composerTextAreaRef}
conversationId={focusedConversationId}
capabilityHasT2I={capabilityHasT2I}
isDeveloperMode={focusedSystemPurposeId === 'Developer'}
onAction={handleComposerAction}
onTextImagine={handleTextImagine}
sx={{
zIndex: 21, // position: 'sticky', bottom: 0,
backgroundColor: 'background.surface',
backgroundColor: themeBgAppChatComposer,
borderTop: `1px solid`,
borderTopColor: 'divider',
p: { xs: 1, md: 2 },
}} />
}}
/>
{/* Diagrams */}
{!!diagramConfig && <DiagramsModal config={diagramConfig} onClose={() => setDiagramConfig(null)} />}
@@ -453,25 +532,34 @@ export function AppChat() {
)}
{/* Import / Export */}
{!!tradeConfig && <TradeModal config={tradeConfig} onConversationActivate={setFocusedConversationId} onClose={() => setTradeConfig(null)} />}
{!!tradeConfig && (
<TradeModal
config={tradeConfig}
onConversationActivate={setFocusedConversationId}
onClose={() => setTradeConfig(null)}
/>
)}
{/* [confirmation] Reset Conversation */}
{!!clearConversationId && <ConfirmationModal
open onClose={() => setClearConversationId(null)} onPositive={handleConfirmedClearConversation}
confirmationText={'Are you sure you want to discard all messages?'} positiveActionText={'Clear conversation'}
/>}
{!!clearConversationId && (
<ConfirmationModal
open
onClose={() => setClearConversationId(null)}
onPositive={handleConfirmedClearConversation}
confirmationText='Are you sure you want to discard all messages?'
positiveActionText='Clear conversation'
/>
)}
{/* [confirmation] Delete All */}
{!!deleteConversationId && <ConfirmationModal
open onClose={() => setDeleteConversationId(null)} onPositive={handleConfirmedDeleteConversation}
confirmationText={deleteConversationId === SPECIAL_ID_WIPE_ALL
? 'Are you absolutely sure you want to delete ALL conversations? This action cannot be undone.'
? `Are you absolutely sure you want to delete ${selectedFolderId ? 'ALL conversations in this folder' : 'ALL conversations'}? This action cannot be undone.`
: 'Are you sure you want to delete this conversation?'}
positiveActionText={deleteConversationId === SPECIAL_ID_WIPE_ALL
? 'Yes, delete all'
? `Yes, delete all ${folderConversationsCount} conversations`
: 'Delete conversation'}
/>}
</>;
}
+19
View File
@@ -0,0 +1,19 @@
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsAlter: ICommandsProvider = {
id: 'chat-alter',
rank: 20,
getCommands: () => [{
primary: '/assistant',
alternatives: ['/a'],
arguments: ['text'],
description: 'Injects assistant response',
}, {
primary: '/system',
alternatives: ['/s'],
arguments: ['text'],
description: 'Injects system message',
}],
};
+16
View File
@@ -0,0 +1,16 @@
import LanguageIcon from '@mui/icons-material/Language';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsBrowse: ICommandsProvider = {
id: 'ass-browse',
rank: 25,
getCommands: () => [{
primary: '/browse',
arguments: ['URL'],
description: 'Assistant will download the web page',
Icon: LanguageIcon,
}],
};
+17
View File
@@ -0,0 +1,17 @@
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsDraw: ICommandsProvider = {
id: 'ass-t2i',
rank: 10,
getCommands: () => [{
primary: '/draw',
alternatives: ['/imagine', '/img'],
arguments: ['prompt'],
description: 'Assistant will draw the text',
Icon: FormatPaintIcon,
}],
};
+13
View File
@@ -0,0 +1,13 @@
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsHelp: ICommandsProvider = {
id: 'cmd-help',
rank: 99,
getCommands: () => [{
primary: '/help',
alternatives: ['/?'],
description: 'Display this list of commands',
}],
};
+16
View File
@@ -0,0 +1,16 @@
import PsychologyIcon from '@mui/icons-material/Psychology';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsReact: ICommandsProvider = {
id: 'ass-react',
rank: 15,
getCommands: () => [{
primary: '/react',
arguments: ['prompt'],
description: 'Use the AI ReAct strategy to answer your query (as sidebar)',
Icon: PsychologyIcon,
}],
};
@@ -0,0 +1,24 @@
import type { FunctionComponent } from 'react';
import type { CommandsProviderId } from './commands.registry';
export interface ChatCommand {
primary: string; // The primary command
alternatives?: string[]; // Alternative commands
arguments?: string[]; // Arguments for the command
description: string; // Description of what the command does
// usage?: string; // Example of how to use the command
Icon?: FunctionComponent; // Icon to display next to the command
}
export interface ICommandsProvider {
id: CommandsProviderId; // Unique identifier for the command provider
rank: number; // Rank of the provider, used to sort the providers in the UI
// Function to get commands with their alternatives and details
getCommands: () => ChatCommand[];
// Function to execute a command with optional parameters
// executeCommand: (command: string, params?: string[]) => Promise<boolean>;
}
@@ -0,0 +1,67 @@
import { ChatCommand, ICommandsProvider } from './ICommandsProvider';
import { CommandsAlter } from './CommandsAlter';
import { CommandsBrowse } from './CommandsBrowse';
import { CommandsDraw } from './CommandsDraw';
import { CommandsHelp } from './CommandsHelp';
import { CommandsReact } from './CommandsReact';
export type CommandsProviderId = 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help';
type TextCommandPiece =
| { type: 'text'; value: string; }
| { type: 'cmd'; providerId: CommandsProviderId, command: string; params?: string, isError?: boolean };
const ChatCommandsProviders: Record<CommandsProviderId, ICommandsProvider> = {
'ass-browse': CommandsBrowse,
'ass-react': CommandsReact,
'ass-t2i': CommandsDraw,
'chat-alter': CommandsAlter,
'cmd-help': CommandsHelp,
};
export function findAllChatCommands(): ChatCommand[] {
return Object.values(ChatCommandsProviders)
.sort((a, b) => a.rank - b.rank)
.map(p => p.getCommands())
.flat();
}
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 }];
// 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);
// Check if the potential command is an actual command
for (const provider of Object.values(ChatCommandsProviders)) {
for (const cmd of provider.getCommands()) {
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 }];
}
// 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 });
return pieces;
}
}
}
// No command found, return the entire input as text
return [{ type: 'text', value: input }];
}
+38 -28
View File
@@ -9,13 +9,14 @@ import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
import { InlineError } from '~/common/components/InlineError';
import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats';
import { openLayoutPreferences } from '~/common/layout/store-applayout';
import { useCapabilityElevenLabs, useCapabilityProdia } from '~/common/components/useCapabilities';
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { ChatMessageMemo } from './message/ChatMessage';
import { CleanerMessage, MessagesSelectionHeader } from './message/CleanerMessage';
import { PersonaSelector } from './persona-selector/PersonaSelector';
import { useChatShowSystemMessages } from '../store-app-chat';
import { useScrollToBottom } from './scroll-to-bottom/useScrollToBottom';
/**
@@ -23,7 +24,8 @@ import { useChatShowSystemMessages } from '../store-app-chat';
*/
export function ChatMessageList(props: {
conversationId: DConversationId | null,
chatLLMContextTokens?: number,
capabilityHasT2I: boolean,
chatLLMContextTokens: number | null,
isMessageSelectionMode: boolean, setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[]) => void,
@@ -39,6 +41,8 @@ export function ChatMessageList(props: {
const [selectedMessages, setSelectedMessages] = React.useState<Set<string>>(new Set());
// external state
const { notifyBooting } = useScrollToBottom();
const { openPreferencesTab } = useOptimaLayout();
const [showSystemMessages] = useChatShowSystemMessages();
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
@@ -50,11 +54,10 @@ export function ChatMessageList(props: {
setMessages: state.setMessages,
};
}, shallow);
const { mayWork: isImaginable } = useCapabilityProdia();
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
// derived state
const { conversationId, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
const { conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
// text actions
@@ -98,22 +101,22 @@ export function ChatMessageList(props: {
}, [conversationId, onTextDiagram]);
const handleTextImagine = React.useCallback(async (text: string) => {
if (!isImaginable)
return openLayoutPreferences(2);
if (!capabilityHasT2I)
return openPreferencesTab(2);
if (conversationId) {
setIsImagining(true);
await onTextImagine(conversationId, text);
setIsImagining(false);
}
}, [conversationId, isImaginable, onTextImagine]);
}, [capabilityHasT2I, conversationId, onTextImagine, openPreferencesTab]);
const handleTextSpeak = React.useCallback(async (text: string) => {
if (!isSpeakable)
return openLayoutPreferences(3);
return openPreferencesTab(3);
setIsSpeaking(true);
await onTextSpeak(text);
setIsSpeaking(false);
}, [isSpeakable, onTextSpeak]);
}, [isSpeakable, onTextSpeak, openPreferencesTab]);
// operate on the local selection set
@@ -157,11 +160,19 @@ export function ChatMessageList(props: {
return { diffMessage: undefined, diffText: undefined };
}, [conversationMessages]);
// scroll to the very bottom of a new chat
React.useEffect(() => {
if (conversationId)
notifyBooting();
}, [conversationId, notifyBooting]);
// no content: show the persona selector
const filteredMessages = conversationMessages
.filter(m => m.role !== 'system' || showSystemMessages) // hide the System message if the user choses to
.reverse(); // 'reverse' is because flexDirection: 'column-reverse' to auto-snap-to-bottom
.filter(m => m.role !== 'system' || showSystemMessages); // hide the System message if the user choses to
if (!filteredMessages.length)
return (
@@ -176,18 +187,29 @@ export function ChatMessageList(props: {
<List sx={{
p: 0, ...(props.sx || {}),
// this makes sure that the the window is scrolled to the bottom (column-reverse)
display: 'flex', flexDirection: 'column-reverse',
display: 'flex',
flexDirection: 'column',
// fix for the double-border on the last message (one by the composer, one to the bottom of the message)
// marginBottom: '-1px',
}}>
{filteredMessages.map((message, idx) =>
{props.isMessageSelectionMode && (
<MessagesSelectionHeader
hasSelected={selectedMessages.size > 0}
sumTokens={historyTokenCount}
onClose={() => props.setIsMessageSelectionMode(false)}
onSelectAll={handleSelectAll}
onDeleteMessages={handleSelectionDelete}
/>
)}
{filteredMessages.map((message, idx, { length: count }) =>
props.isMessageSelectionMode ? (
<CleanerMessage
key={'sel-' + message.id}
message={message}
isBottom={idx === 0} remainingTokens={(props.chatLLMContextTokens || 0) - historyTokenCount}
remainingTokens={props.chatLLMContextTokens ? (props.chatLLMContextTokens - historyTokenCount) : undefined}
selected={selectedMessages.has(message.id)} onToggleSelected={handleSelectMessage}
/>
@@ -197,7 +219,7 @@ export function ChatMessageList(props: {
key={'msg-' + message.id}
message={message}
diffPreviousText={message === diffMessage ? diffText : undefined}
isBottom={idx === 0}
isBottom={idx === count - 1}
isImagining={isImagining} isSpeaking={isSpeaking}
onConversationBranch={handleConversationBranch}
onConversationRestartFrom={handleConversationRestartFrom}
@@ -212,18 +234,6 @@ export function ChatMessageList(props: {
),
)}
{/* Header at the bottom because of 'row-reverse' */}
{props.isMessageSelectionMode && (
<MessagesSelectionHeader
hasSelected={selectedMessages.size > 0}
isBottom={filteredMessages.length === 0}
sumTokens={historyTokenCount}
onClose={() => props.setIsMessageSelectionMode(false)}
onSelectAll={handleSelectAll}
onDeleteMessages={handleSelectionDelete}
/>
)}
</List>
);
}
+22 -15
View File
@@ -1,11 +1,12 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { Box, Grid, IconButton, Sheet, Stack, styled, Typography, useTheme } from '@mui/joy';
import { Box, Grid, IconButton, Sheet, styled, Typography } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import CloseIcon from '@mui/icons-material/Close';
import { DConversationId, DEphemeral, useChatStore } from '~/common/state/store-chats';
import { lineHeightChatText } from '~/common/app.theme';
const StateLine = styled(Typography)(({ theme }) => ({
@@ -15,7 +16,7 @@ const StateLine = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSize.xs,
fontFamily: theme.fontFamily.code,
marginLeft: theme.spacing(1),
lineHeight: 2,
lineHeight: lineHeightChatText,
}));
function isPrimitive(value: any): boolean {
@@ -52,11 +53,11 @@ function StateRenderer(props: { state: object }) {
const entries = Object.entries(props.state);
return (
<Stack>
<Typography level='body-sm' sx={{ mb: 1 }}>
Internal State
<Box>
<Typography fontSize='smaller' sx={{ mb: 1 }}>
## Internal State
</Typography>
<Sheet>
<Sheet sx={{ p: 1 }}>
{!entries && <Typography level='body-sm'>No state variables</Typography>}
{entries.map(([key, value]) =>
isPrimitive(value)
@@ -68,13 +69,12 @@ function StateRenderer(props: { state: object }) {
: <Typography key={'state-' + key} level='body-sm'>{key}: {value}</Typography>,
)}
</Sheet>
</Stack>
</Box>
);
}
function EphemeralItem({ conversationId, ephemeral }: { conversationId: string, ephemeral: DEphemeral }) {
const theme = useTheme();
return <Box
sx={{
p: { xs: 1, md: 2 },
@@ -84,8 +84,8 @@ function EphemeralItem({ conversationId, ephemeral }: { conversationId: string,
}}>
{/* Title */}
{ephemeral.title && <Typography>
{ephemeral.title} <b>Development Tools</b>
{ephemeral.title && <Typography level='title-sm' sx={{ mb: 1.5 }}>
{ephemeral.title} Development Tools
</Typography>}
{/* Vertical | split */}
@@ -93,7 +93,7 @@ function EphemeralItem({ conversationId, ephemeral }: { conversationId: string,
{/* Left pane (console) */}
<Grid xs={12} md={ephemeral.state ? 6 : 12}>
<Typography fontSize='smaller' sx={{ overflowWrap: 'anywhere', whiteSpace: 'break-spaces', lineHeight: 1.75 }}>
<Typography fontSize='smaller' sx={{ overflowWrap: 'anywhere', whiteSpace: 'break-spaces', lineHeight: lineHeightChatText }}>
{ephemeral.text}
</Typography>
</Grid>
@@ -102,8 +102,8 @@ function EphemeralItem({ conversationId, ephemeral }: { conversationId: string,
{!!ephemeral.state && <Grid
xs={12} md={6}
sx={{
borderLeft: { md: `1px solid ${theme.palette.divider}` },
borderTop: { xs: `1px solid ${theme.palette.divider}`, md: 'none' },
borderLeft: { md: `1px dashed` },
borderTop: { xs: `1px dashed`, md: 'none' },
}}>
<StateRenderer state={ephemeral.state} />
</Grid>}
@@ -123,10 +123,15 @@ function EphemeralItem({ conversationId, ephemeral }: { conversationId: string,
</Box>;
}
// const dashedBorderSVG = encodeURIComponent(`
// <svg xmlns='http://www.w3.org/2000/svg' width='100%' height='100%'>
// <rect x='0' y='0' width='100%' height='100%' fill='none' stroke='currentColor' stroke-width='2' stroke-dasharray='16, 2' />
// </svg>
// `);
export function Ephemerals(props: { conversationId: DConversationId | null, sx?: SxProps }) {
// global state
const theme = useTheme();
const ephemerals = useChatStore(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
return conversation ? conversation.ephemerals : [];
@@ -138,7 +143,9 @@ export function Ephemerals(props: { conversationId: DConversationId | null, sx?:
<Sheet
variant='soft' color='success' invertedColors
sx={{
border: `4px dashed ${theme.palette.divider}`,
// backgroundImage: `url("data:image/svg+xml,${dashedBorderSVG.replace('currentColor', '%23A1E8A1')}")`,
// backgroundSize: '100% 100%',
// backgroundRepeat: 'no-repeat',
...(props.sx || {}),
}}>
@@ -1,67 +1,154 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { Box, ListDivider, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
import { Box, IconButton, ListDivider, ListItemButton, ListItemDecorator, Tooltip } from '@mui/joy';
import AddIcon from '@mui/icons-material/Add';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import FileDownloadIcon from '@mui/icons-material/FileDownload';
import FileUploadIcon from '@mui/icons-material/FileUpload';
import FolderOpenOutlinedIcon from '@mui/icons-material/FolderOpenOutlined';
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
import { closeLayoutDrawer } from '~/common/layout/store-applayout';
import { DFolder, useFoldersToggle, useFolderStore } from '~/common/state/store-folders';
import { PageDrawerHeader } from '~/common/layout/optima/components/PageDrawerHeader';
import { PageDrawerList, PageDrawerTallItemSx } from '~/common/layout/optima/components/PageDrawerList';
import { conversationTitle, DConversationId, useChatStore } from '~/common/state/store-chats';
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import DebounceInput from '~/common/components/DebounceInput';
import { ChatNavigationItemMemo } from './ChatNavigationItem';
import { ChatFolderList } from './folder/ChatFolderList';
import { ChatDrawerItemMemo, ChatNavigationItemData } from './ChatNavigationItem';
// type ListGrouping = 'off' | 'persona';
export const ChatDrawerItemsMemo = React.memo(ChatDrawerItems);
/*
* 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 const useChatNavigationItems = (activeConversationId: DConversationId | null, folderId: string | null): {
chatNavItems: ChatNavigationItemData[],
folders: DFolder[],
} => {
// monitor folder changes
// NOTE: we're not checking for state.useFolders, as we strongly assume folderId to be null when folders are disabled
const { currentFolder, folders } = useFolderStore(state => {
const currentFolder = folderId ? state.folders.find(_f => _f.id === folderId) ?? null : null;
return {
folders: state.folders,
currentFolder,
};
}, shallow);
// transform (folder) selected conversation into optimized 'navigation item' data
const chatNavItems: ChatNavigationItemData[] = useChatStore(state => {
const selectConversations = currentFolder
? state.conversations.filter(_c => currentFolder.conversationIds.includes(_c.id))
: state.conversations;
return selectConversations.map(_c => ({
conversationId: _c.id,
isActive: _c.id === activeConversationId,
isEmpty: !_c.messages.length && !_c.userTitle,
title: conversationTitle(_c),
messageCount: _c.messages.length,
assistantTyping: !!_c.abortController,
systemPurposeId: _c.systemPurposeId,
}));
}, (a: ChatNavigationItemData[], b: ChatNavigationItemData[]) => {
// custom equality function to avoid unnecessary re-renders
return a.length === b.length && a.every((_a, i) => shallow(_a, b[i]));
});
return { chatNavItems, folders };
};
export const ChatDrawerContentMemo = React.memo(ChatDrawerItems);
function ChatDrawerItems(props: {
activeConversationId: DConversationId | null,
disableNewButton: boolean,
onConversationActivate: (conversationId: DConversationId) => void,
onConversationDelete: (conversationId: DConversationId, bypassConfirmation: boolean) => void,
onConversationExportDialog: (conversationId: DConversationId | null) => void,
onConversationImportDialog: () => void,
onConversationNew: () => void,
onConversationsDeleteAll: () => void,
selectedFolderId: string | null,
setSelectedFolderId: (folderId: string | null) => void,
}) {
// local state
const { onConversationDelete, onConversationNew, onConversationActivate } = props;
const [debouncedSearchQuery, setDebouncedSearchQuery] = React.useState('');
// const [grouping] = React.useState<ListGrouping>('off');
const { onConversationDelete, onConversationNew, onConversationActivate } = props;
// external state
const conversations = useChatStore(state => state.conversations, shallow);
const { closeDrawer, closeDrawerOnMobile } = useOptimaDrawers();
const { useFolders, toggleUseFolders } = useFoldersToggle();
const { chatNavItems, folders } = useChatNavigationItems(props.activeConversationId, props.selectedFolderId);
const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner');
const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI);
// derived state
const maxChatMessages = conversations.reduce((longest, _c) => Math.max(longest, _c.messages.length), 1);
const totalConversations = conversations.length;
const hasChats = totalConversations > 0;
const singleChat = totalConversations === 1;
const softMaxReached = totalConversations >= 50;
const selectConversationsCount = chatNavItems.length;
const nonEmptyChats = selectConversationsCount > 1 || (selectConversationsCount === 1 && !chatNavItems[0].isEmpty);
const singleChat = selectConversationsCount === 1;
const softMaxReached = selectConversationsCount >= 50;
const handleButtonNew = React.useCallback(() => {
onConversationNew();
closeLayoutDrawer();
}, [onConversationNew]);
closeDrawerOnMobile();
}, [closeDrawerOnMobile, onConversationNew]);
const handleConversationActivate = React.useCallback((conversationId: DConversationId, closeMenu: boolean) => {
onConversationActivate(conversationId);
if (closeMenu)
closeLayoutDrawer();
}, [onConversationActivate]);
closeDrawerOnMobile();
}, [closeDrawerOnMobile, onConversationActivate]);
const handleConversationDelete = React.useCallback((conversationId: DConversationId) => {
!singleChat && conversationId && onConversationDelete(conversationId, true);
}, [onConversationDelete, singleChat]);
// Filter chatNavItems based on the search query and rank them by search frequency
const filteredChatNavItems = React.useMemo(() => {
if (!debouncedSearchQuery) return chatNavItems;
return chatNavItems
.map(item => {
// Get the conversation by ID
const conversation = useChatStore.getState().conversations.find(c => c.id === item.conversationId);
// Calculate the frequency of the search term in the title and messages
const titleFrequency = (item.title.toLowerCase().match(new RegExp(debouncedSearchQuery.toLowerCase(), 'g')) || []).length;
const messageFrequency = conversation?.messages.reduce((count, message) => {
return count + (message.text.toLowerCase().match(new RegExp(debouncedSearchQuery.toLowerCase(), 'g')) || []).length;
}, 0) || 0;
// Return the item with the searchFrequency property
return {
...item,
searchFrequency: titleFrequency + messageFrequency,
};
})
// Exclude items with a searchFrequency of 0
.filter(item => item.searchFrequency > 0)
// Sort the items by searchFrequency in descending order
.sort((a, b) => b.searchFrequency! - a.searchFrequency!);
}, [chatNavItems, debouncedSearchQuery]);
// basis for the underline bar
const bottomBarBasis = filteredChatNavItems.reduce((longest, _c) => Math.max(longest, _c.searchFrequency ?? _c.messageCount), 1);
// grouping
/*let sortedIds = conversationIDs;
if (grouping === 'persona') {
@@ -84,66 +171,127 @@ function ChatDrawerItems(props: {
return <>
{/*<ListItem>*/}
{/* <Typography level='body-sm'>*/}
{/* Active chats*/}
{/* </Typography>*/}
{/*</ListItem>*/}
{/* Drawer Header */}
<PageDrawerHeader
title='Chats'
onClose={closeDrawer}
startButton={
<Tooltip title={useFolders ? 'Hide Folders' : 'Use Folders'}>
<IconButton onClick={toggleUseFolders}>
{useFolders ? <FolderOpenOutlinedIcon /> : <FolderOutlinedIcon />}
</IconButton>
</Tooltip>
}
/>
<MenuItem disabled={props.disableNewButton} onClick={handleButtonNew}>
<ListItemDecorator><AddIcon /></ListItemDecorator>
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
New
{/*<KeyStroke combo='Ctrl + Alt + N' />*/}
{/* Folders List */}
{/*<Box sx={{*/}
{/* display: 'grid',*/}
{/* gridTemplateRows: !useFolders ? '0fr' : '1fr',*/}
{/* transition: 'grid-template-rows 0.42s cubic-bezier(.17,.84,.44,1)',*/}
{/* '& > div': {*/}
{/* padding: useFolders ? 2 : 0,*/}
{/* transition: 'padding 0.42s cubic-bezier(.17,.84,.44,1)',*/}
{/* overflow: 'hidden',*/}
{/* },*/}
{/*}}>*/}
{useFolders && (
<ChatFolderList
folders={folders}
selectedFolderId={props.selectedFolderId}
onFolderSelect={props.setSelectedFolderId}
/>
)}
{/*</Box>*/}
{/* Chats List */}
<PageDrawerList variant='plain' noTopPadding noBottomPadding tallRows>
{useFolders && <ListDivider sx={{ mb: 0 }} />}
{/* Search Input Field */}
<DebounceInput
onDebounce={setDebouncedSearchQuery}
debounceTimeout={300}
placeholder='Search...'
aria-label='Search'
sx={{ m: 2 }}
/>
<ListItemButton disabled={props.disableNewButton} onClick={handleButtonNew} sx={PageDrawerTallItemSx}>
<ListItemDecorator><AddIcon /></ListItemDecorator>
<Box sx={{
// style
fontSize: 'sm',
fontWeight: 'lg',
// content
flexGrow: 1,
display: 'flex',
justifyContent: 'space-between',
gap: 1,
}}>
New chat
{/*<KeyStroke combo='Ctrl + Alt + N' sx={props.disableNewButton ? { opacity: 0.5 } : undefined} />*/}
</Box>
</ListItemButton>
{/*<ListDivider sx={{ mt: 0 }} />*/}
<Box sx={{ flex: 1, overflowY: 'auto' }}>
{/*<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>*/}
{filteredChatNavItems.map(item =>
<ChatDrawerItemMemo
key={'nav-' + item.conversationId}
item={item}
isLonely={singleChat}
showSymbols={showSymbols}
bottomBarBasis={(labsEnhancedUI || softMaxReached || debouncedSearchQuery) ? bottomBarBasis : 0}
onConversationActivate={handleConversationActivate}
onConversationDelete={handleConversationDelete}
/>)}
</Box>
</MenuItem>
<ListDivider sx={{ mb: 0 }} />
<ListDivider sx={{ mt: 0 }} />
<Box sx={{ flex: 1, overflowY: 'auto' }}>
{/*<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 sx={{ display: 'flex', alignItems: 'center' }}>
{conversations.map(conversation =>
<ChatNavigationItemMemo
key={'nav-' + conversation.id}
conversation={conversation}
isActive={conversation.id === props.activeConversationId}
isLonely={singleChat}
maxChatMessages={(labsEnhancedUI || softMaxReached) ? maxChatMessages : 0}
showSymbols={showSymbols}
onConversationActivate={handleConversationActivate}
onConversationDelete={handleConversationDelete}
/>)}
</Box>
<ListItemButton onClick={props.onConversationImportDialog} sx={{ flex: 1 }}>
<ListItemDecorator>
<FileUploadIcon />
</ListItemDecorator>
Import
{/*<OpenAIIcon sx={{ ml: 'auto' }} />*/}
</ListItemButton>
<ListDivider sx={{ mt: 0 }} />
<ListItemButton disabled={!nonEmptyChats} onClick={() => props.onConversationExportDialog(props.activeConversationId)} sx={{ flex: 1 }}>
<ListItemDecorator>
<FileDownloadIcon />
</ListItemDecorator>
Export
</ListItemButton>
</Box>
<MenuItem onClick={props.onConversationImportDialog}>
<ListItemDecorator>
<FileUploadIcon />
</ListItemDecorator>
Import chats
<OpenAIIcon sx={{ fontSize: 'xl', ml: 'auto' }} />
</MenuItem>
<ListItemButton disabled={!nonEmptyChats} onClick={props.onConversationsDeleteAll}>
<ListItemDecorator>
<DeleteOutlineIcon />
</ListItemDecorator>
Delete {selectConversationsCount >= 2 ? `all ${selectConversationsCount} chats` : 'chat'}
</ListItemButton>
<MenuItem disabled={!hasChats} onClick={props.onConversationsDeleteAll}>
<ListItemDecorator><DeleteOutlineIcon /></ListItemDecorator>
<Typography>
Delete {totalConversations >= 2 ? `all ${totalConversations} chats` : 'chat'}
</Typography>
</MenuItem>
</PageDrawerList>
</>;
}
@@ -1,26 +1,51 @@
import * as React from 'react';
import { IconButton } from '@mui/joy';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import type { DConversationId } from '~/common/state/store-chats';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { useChatLLMDropdown } from './useLLMDropdown';
import { usePersonaIdDropdown } from './usePersonaDropdown';
import { useFolderDropdown } from './folder/useFolderDropdown';
export function ChatDropdowns(props: {
conversationId: DConversationId | null
isSplitPanes: boolean
onToggleSplitPanes: () => void
}) {
// state
const { chatLLMDropdown } = useChatLLMDropdown();
const { personaDropdown } = usePersonaIdDropdown(props.conversationId);
const { folderDropdown } = useFolderDropdown(props.conversationId);
// external state
const labsSplitBranching = useUXLabsStore(state => state.labsSplitBranching);
return <>
{/* Model selector */}
{chatLLMDropdown}
{/* Persona selector */}
{personaDropdown}
{/* Model selector */}
{chatLLMDropdown}
{/* Folder selector */}
{folderDropdown}
{/* Split Panes button */}
{labsSplitBranching && <IconButton
variant={props.isSplitPanes ? 'solid' : undefined}
onClick={props.onToggleSplitPanes}
// sx={{
// ml: 'auto',
// }}
>
<VerticalSplitIcon />
</IconButton>}
</>;
}
@@ -11,7 +11,7 @@ import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
import type { DConversationId } from '~/common/state/store-chats';
import { KeyStroke } from '~/common/components/KeyStroke';
import { closeLayoutMenu } from '~/common/layout/store-applayout';
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
import { useUICounter } from '~/common/state/store-ui';
import { useChatShowSystemMessages } from '../../store-app-chat';
@@ -30,6 +30,7 @@ export function ChatMenuItems(props: {
}) {
// external state
const { closePageMenu } = useOptimaDrawers();
const { touch: shareTouch } = useUICounter('export-share');
const [showSystemMessages, setShowSystemMessages] = useChatShowSystemMessages();
@@ -39,7 +40,7 @@ export function ChatMenuItems(props: {
const closeMenu = (event: React.MouseEvent) => {
event.stopPropagation();
closeLayoutMenu();
closePageMenu();
};
const handleConversationClear = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -116,7 +117,7 @@ export function ChatMenuItems(props: {
<MenuItem disabled={disabled} onClick={handleConversationClear}>
<ListItemDecorator><ClearIcon /></ListItemDecorator>
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
Reset
Reset Chat
{!disabled && <KeyStroke combo='Ctrl + Alt + X' />}
</Box>
</MenuItem>
@@ -1,49 +1,48 @@
import * as React from 'react';
import { Avatar, Box, IconButton, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
import { Avatar, Box, IconButton, ListItemButton, ListItemDecorator, Typography } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import CloseIcon from '@mui/icons-material/Close';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import { SystemPurposes } from '../../../../data';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { InlineTextarea } from '~/common/components/InlineTextarea';
import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats';
import { useUIPreferencesStore } from '~/common/state/store-ui';
const DEBUG_CONVERSATION_IDs = false;
export const ChatNavigationItemMemo = React.memo(ChatNavigationItem);
export const ChatDrawerItemMemo = React.memo(ChatNavigationItem);
export interface ChatNavigationItemData {
conversationId: DConversationId;
isActive: boolean;
isEmpty: boolean;
title: string;
messageCount: number;
assistantTyping: boolean;
systemPurposeId: SystemPurposeId;
searchFrequency?: number;
}
function ChatNavigationItem(props: {
conversation: DConversation,
isActive: boolean,
item: ChatNavigationItemData,
isLonely: boolean,
maxChatMessages: number,
showSymbols: boolean,
bottomBarBasis: number,
onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void,
onConversationDelete: (conversationId: DConversationId) => void,
}) {
const { conversation, isActive } = props;
// state
const [isEditingTitle, setIsEditingTitle] = React.useState(false);
const [deleteArmed, setDeleteArmed] = React.useState(false);
// external state
const doubleClickToEdit = useUIPreferencesStore(state => state.doubleClickToEdit);
// derived state
const { id: conversationId } = conversation;
const isNew = conversation.messages.length === 0;
const messageCount = conversation.messages.length;
const assistantTyping = !!conversation.abortController;
const systemPurposeId = conversation.systemPurposeId;
const title = conversationTitle(conversation, 'new conversation');
// const setUserTitle = state.setUserTitle;
const { conversationId, isActive, title, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
const isNew = messageCount === 0;
// auto-close the arming menu when clicking away
// NOTE: there currently is a bug (race condition) where the menu closes on a new item right after opening
@@ -60,7 +59,11 @@ function ChatNavigationItem(props: {
const handleTitleEdited = (text: string) => {
setIsEditingTitle(false);
useChatStore.getState().setUserTitle(conversationId, text);
useChatStore.getState().setUserTitle(conversationId, text.trim());
};
const handleTitleEditCancel = () => {
setIsEditingTitle(false);
};
const handleDeleteButtonShow = (event: React.MouseEvent) => {
@@ -83,28 +86,27 @@ function ChatNavigationItem(props: {
const textSymbol = SystemPurposes[systemPurposeId]?.symbol || '❓';
const buttonSx: SxProps = { ml: 1, ...(isActive ? { color: 'white' } : {}) };
const buttonSx: SxProps = isActive ? { color: 'white' } : {};
const progress = props.maxChatMessages ? 100 * messageCount / props.maxChatMessages : 0;
const progress = props.bottomBarBasis ? 100 * (searchFrequency ?? messageCount) / props.bottomBarBasis : 0;
return (
<MenuItem
variant={isActive ? 'solid' : 'plain'} color='neutral'
selected={isActive}
onClick={handleConversationActivate}
<ListItemButton
variant={isActive ? 'soft' : 'plain'} color='neutral'
onClick={!isActive ? handleConversationActivate : event => event.preventDefault()}
sx={{
// py: 0,
position: 'relative',
border: 'none', // note, there's a default border of 1px and invisible.. hmm
cursor: 'pointer',
'&:hover > button': { opacity: 1 },
...(isActive ? { bgcolor: 'red' } : {}),
}}
>
{/* Optional progress bar, underlay */}
{progress > 0 && (
<Box sx={{
backgroundColor: 'neutral.softActiveBg',
backgroundColor: 'neutral.softBg',
position: 'absolute', left: 0, bottom: 0, width: progress + '%', height: 4,
}} />
)}
@@ -117,35 +119,39 @@ function ChatNavigationItem(props: {
alt='typing' variant='plain'
src='https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'
sx={{
width: 24,
height: 24,
width: '1.5rem',
height: '1.5rem',
borderRadius: 'var(--joy-radius-sm)',
}}
/>
) : (
<Typography sx={{ fontSize: '18px' }}>
<Typography>
{isNew ? '' : textSymbol}
</Typography>
)}
</ListItemDecorator>}
{/* Text */}
{!isEditingTitle ? (
<Box onDoubleClick={() => doubleClickToEdit ? handleTitleEdit() : null} sx={{ flexGrow: 1 }}>
{DEBUG_CONVERSATION_IDs ? conversationId.slice(0, 10) : title}{assistantTyping && '...'}
</Box>
<Typography
level={isActive ? 'title-md' : 'body-md'}
onDoubleClick={handleTitleEdit}
sx={{ flex: 1 }}
>
{DEBUG_CONVERSATION_IDs ? conversationId.slice(0, 10) : (title.trim() ? title : 'Chat')}{assistantTyping && '...'}
</Typography>
) : (
<InlineTextarea initialText={title} onEdit={handleTitleEdited} sx={{ ml: -1.5, mr: -0.5, flexGrow: 1 }} />
<InlineTextarea initialText={title} onEdit={handleTitleEdited} onCancel={handleTitleEditCancel} sx={{ ml: -1.5, mr: -0.5, flexGrow: 1 }} />
)}
{/* // TODO: Commented code */}
{/* Edit */}
{/*<IconButton*/}
{/* variant='plain' color='neutral'*/}
{/* onClick={() => props.onEditTitle(props.conversationId)}*/}
{/* sx={{*/}
{/* opacity: 0, transition: 'opacity 0.3s', ml: 'auto',*/}
@@ -153,18 +159,29 @@ function ChatNavigationItem(props: {
{/* <EditIcon />*/}
{/*</IconButton>*/}
{/* Display search frequency if it exists and is greater than 0 */}
{searchFrequency && searchFrequency > 0 && (
<Box sx={{ ml: 1 }}>
<Typography level='body-sm'>
{searchFrequency}
</Typography>
</Box>
)}
{/* Delete Arming */}
{!props.isLonely && !deleteArmed && (
{!props.isLonely && !deleteArmed && !searchFrequency && (
<IconButton
variant={isActive ? 'solid' : 'outlined'} color='neutral'
size='sm' sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.3s', ...buttonSx }}
onClick={handleDeleteButtonShow}>
variant={isActive ? 'solid' : 'outlined'}
size='sm'
sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.2s', ...buttonSx }}
onClick={handleDeleteButtonShow}
>
<DeleteOutlineIcon />
</IconButton>
)}
{/* Delete / Cancel buttons */}
{!props.isLonely && deleteArmed && <>
{!props.isLonely && deleteArmed && !searchFrequency && <>
<IconButton size='sm' variant='solid' color='danger' sx={buttonSx} onClick={handleConversationDelete}>
<DeleteOutlineIcon />
</IconButton>
@@ -172,7 +189,7 @@ function ChatNavigationItem(props: {
<CloseIcon />
</IconButton>
</>}
</MenuItem>
</ListItemButton>
);
}
@@ -0,0 +1,71 @@
import * as React from 'react';
import { Button, ListItem, ListItemDecorator } from '@mui/joy';
import AddIcon from '@mui/icons-material/Add';
import FolderIcon from '@mui/icons-material/Folder';
import { InlineTextarea } from '~/common/components/InlineTextarea';
import { getRotatingFolderColor, useFolderStore } from '~/common/state/store-folders';
export function AddFolderButton() {
// state
const [isAddingFolder, setIsAddingFolder] = React.useState(false);
const [newFolderColor, setNewFolderColor] = React.useState<string | null>(null);
const handleAddFolder = () => {
setNewFolderColor(getRotatingFolderColor());
setIsAddingFolder(true);
};
const handleCreateFolder = (name: string) => {
if (name.trim())
useFolderStore.getState().createFolder(name.trim(), newFolderColor || undefined);
setIsAddingFolder(false);
};
const handleCancelAddFolder = () => {
setIsAddingFolder(false);
};
return isAddingFolder ? (
<ListItem sx={{
'--ListItem-paddingLeft': '0.75rem',
'--ListItem-minHeight': '3rem', // --Folder-ListItem-height
display: 'flex', alignItems: 'center', gap: 1,
}}>
<ListItemDecorator>
<FolderIcon style={{ color: newFolderColor || 'inherit' }} />
</ListItemDecorator>
<InlineTextarea
initialText='' placeholder='Folder Name'
onEdit={handleCreateFolder}
onCancel={handleCancelAddFolder}
sx={{
flexGrow: 1,
}} />
{/*<IconButton color='danger' onClick={handleCancelAddFolder}>*/}
{/* <CloseIcon />*/}
{/*</IconButton>*/}
</ListItem>
) : (
<Button
color='neutral'
variant='plain'
startDecorator={<AddIcon />}
onClick={handleAddFolder}
sx={{
// display: 'flex', alignItems: 'center', justifyContent: 'flex-start',
// minHeight: '3rem', // --Folder-ListItem-height
// match the forder elements
paddingInline: '1.2rem',
gap: '0.75rem',
// fontWeight: 400,
}}
>
New folder
</Button>
);
}
@@ -0,0 +1,137 @@
import * as React from 'react';
import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd';
import { List, ListItem, ListItemButton, ListItemContent, ListItemDecorator, Sheet, Typography } from '@mui/joy';
import FolderIcon from '@mui/icons-material/Folder';
import { DFolder, useFolderStore } from '~/common/state/store-folders';
import { AddFolderButton } from './AddFolderButton';
import { FolderListItem } from './FolderListItem';
import { StrictModeDroppable } from './StrictModeDroppable';
export function ChatFolderList(props: {
folders: DFolder[];
onFolderSelect: (folderId: string | null) => void;
selectedFolderId: string | null;
}) {
// derived props
const { folders, onFolderSelect, selectedFolderId } = props;
// handlers
const onDragEnd = (result: DropResult) => {
if (!result.destination) return;
useFolderStore.getState().moveFolder(result.source.index, result.destination.index);
};
return (
<Sheet variant='soft' sx={{ p: 2 }}>
<List
variant='plain'
sx={(theme) => ({
'& ul': {
'--List-gap': '0px',
bgcolor: 'background.surface',
'& > li:first-of-type > [role="button"]': {
borderTopRightRadius: 'var(--List-radius)',
borderTopLeftRadius: 'var(--List-radius)',
},
'& > li:last-child > [role="button"]': {
borderBottomRightRadius: 'var(--List-radius)',
borderBottomLeftRadius: 'var(--List-radius)',
},
},
// copied from the former PageDrawerList as this was contained
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
'--ListItemDecorator-size': '2.75rem',
'--ListItem-minHeight': '3rem', // --Folder-ListItem-height
'--List-radius': '8px',
'--List-gap': '1rem',
'--ListDivider-gap': '0px',
// '--ListItem-paddingY': '0.5rem',
// override global variant tokens
'--joy-palette-neutral-plainHoverBg': 'rgba(0 0 0 / 0.08)',
'--joy-palette-neutral-plainActiveBg': 'rgba(0 0 0 / 0.12)',
[theme.getColorSchemeSelector('light')]: {
'--joy-palette-divider': 'rgba(0 0 0 / 0.08)',
},
[theme.getColorSchemeSelector('dark')]: {
'--joy-palette-neutral-plainHoverBg': 'rgba(255 255 255 / 0.1)',
'--joy-palette-neutral-plainActiveBg': 'rgba(255 255 255 / 0.16)',
},
})}
>
<ListItem nested>
<DragDropContext onDragEnd={onDragEnd}>
<StrictModeDroppable
droppableId='folder'
renderClone={(provided, snapshot, rubric) => (
<FolderListItem
folder={folders[rubric.source.index]}
provided={provided}
snapshot={snapshot}
onFolderSelect={onFolderSelect}
selectedFolderId={selectedFolderId}
/>
)}
>
{(provided) => (
<List ref={provided.innerRef} {...provided.droppableProps}>
{/* First item is the 'All' button */}
<ListItem>
<ListItemButton
// handle folder select
onClick={(event) => {
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
onFolderSelect(null);
}}
selected={selectedFolderId === null}
sx={{
border: 0,
justifyContent: 'space-between',
'&:hover .menu-icon': {
visibility: 'visible', // Hide delete icon for default folder
},
}}
>
<ListItemDecorator>
<FolderIcon />
</ListItemDecorator>
<ListItemContent>
<Typography>All</Typography>
</ListItemContent>
</ListItemButton>
</ListItem>
{folders.map((folder, index) => (
<Draggable key={folder.id} draggableId={folder.id} index={index}>
{(provided, snapshot) => (
<FolderListItem
folder={folder}
provided={provided}
snapshot={snapshot}
onFolderSelect={onFolderSelect}
selectedFolderId={selectedFolderId}
/>
)}
</Draggable>
))}
{provided.placeholder}
</List>
)}
</StrictModeDroppable>
</DragDropContext>
</ListItem>
</List>
<AddFolderButton />
</Sheet>
);
}
@@ -0,0 +1,319 @@
import React, { useState } from 'react';
import { DraggableProvided, DraggableStateSnapshot, DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd';
import { FormLabel, IconButton, ListItem, ListItemButton, ListItemContent, ListItemDecorator, MenuItem, Radio, radioClasses, RadioGroup, Sheet, Typography } from '@mui/joy';
import CloseIcon from '@mui/icons-material/Close';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import Done from '@mui/icons-material/Done';
import EditIcon from '@mui/icons-material/Edit';
import FolderIcon from '@mui/icons-material/Folder';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { DFolder, FOLDERS_COLOR_PALETTE, useFolderStore } from '~/common/state/store-folders';
import { InlineTextarea } from '~/common/components/InlineTextarea';
// Define the type for your props if you're using TypeScript
type RenderItemProps = {
folder: DFolder;
provided: DraggableProvided;
snapshot: DraggableStateSnapshot;
onFolderSelect: (folderId: string | null) => void;
selectedFolderId: string | null;
// Include any other props that RenderItem needs
};
export const FolderListItem: React.FC<RenderItemProps> = ({ folder, provided, snapshot, onFolderSelect, selectedFolderId }) => {
// internal state
const [deleteArmed, setDeleteArmed] = useState(false);
const [editingFolderId, setEditingFolderId] = useState<string | null>(null);
// State to control the open state of the Menu
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLAnchorElement>(null);
// Menu
const handleMenuOpen = (event: React.MouseEvent<HTMLAnchorElement>) => {
setMenuAnchorEl(event.currentTarget);
setDeleteArmed(false); // Reset delete armed state
};
const handleMenuClose = () => {
setMenuAnchorEl(null);
};
// Edit Title
const handleEditTitle = (event: React.MouseEvent<HTMLElement, MouseEvent>, folderId: string) => {
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
setEditingFolderId(folderId);
};
const handleCancelEditTitle = () => {
setEditingFolderId(null);
};
const handleSetTitle = (newTitle: string, folderId: string) => {
if (newTitle.trim())
useFolderStore.getState().setFolderName(folderId, newTitle.trim());
setEditingFolderId(null); // Exit edit mode
// Blur the input element if it's currently focused
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
};
// Deletion
const handleDeleteButtonShow = (event: React.MouseEvent) => {
event.stopPropagation();
setDeleteArmed(true);
};
const handleDeleteConfirmed = (event: React.MouseEvent) => {
if (deleteArmed) {
setDeleteArmed(false);
event.stopPropagation();
useFolderStore.getState().deleteFolder(folder.id);
handleMenuClose();
}
};
const handleDeleteCanceled = (event: React.MouseEvent) => {
if (deleteArmed) {
setDeleteArmed(false);
event.stopPropagation();
}
};
// Color
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
useFolderStore.getState().setFolderColor(folder.id, event.target.value);
handleMenuClose();
};
const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
userSelect: 'none',
borderRadius: '8px',
backgroundColor: isDragging ? 'rgba(0, 80, 80, 0.18)' : 'transparent',
...draggableStyle,
// Any additional styles you want to apply during dragging
...(isDragging &&
{
// Apply any drag-specific styles here
// marginLeft: '12px',
}),
});
const getListItemContentStyle = (isDragging: boolean, _draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
...(isDragging && {
// Apply any drag-specific styles here
marginLeft: '20px',
}),
});
const getListItemDecoratorStyle = (isDragging: boolean, _draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
...(isDragging && {
// Apply any drag-specific styles here
marginLeft: '12px',
}),
});
const handleFolderSelect = (folderId: string | null) => {
onFolderSelect(folderId);
};
return (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={{
...getItemStyle(snapshot.isDragging, provided.draggableProps.style),
userSelect: 'none',
}}
>
<ListItemButton
// handle folder select
onClick={(event) => {
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
handleFolderSelect(folder.id);
}}
selected={folder.id === selectedFolderId}
sx={{
border: 0,
justifyContent: 'space-between',
'&:hover .menu-icon': {
visibility: 'visible', // Hide delete icon for default folder
},
}}
>
<ListItemDecorator
style={{
...getListItemDecoratorStyle(snapshot.isDragging, provided.draggableProps.style),
userSelect: 'none',
}}
>
<FolderIcon style={{ color: folder.color || 'inherit' }} />
</ListItemDecorator>
{editingFolderId === folder.id ? (
<InlineTextarea
initialText={folder.title}
onEdit={newTitle => handleSetTitle(newTitle, folder.id)}
onCancel={handleCancelEditTitle}
sx={{ ml: -1.5, mr: -0.5, flexGrow: 1 }}
/>
) : (
<ListItemContent
onDoubleClick={event => handleEditTitle(event, folder.id)}
style={{
...getListItemContentStyle(snapshot.isDragging, provided.draggableProps.style),
userSelect: 'none',
}}
>
<Typography>{folder.title}</Typography>
</ListItemContent>
)}
{/* Icon to show the Popup menu */}
<IconButton
variant='outlined'
className='menu-icon'
onClick={handleMenuOpen}
sx={{
visibility: 'hidden',
}}
>
<MoreVertIcon />
</IconButton>
{!!menuAnchorEl && (
<CloseableMenu
open anchorEl={menuAnchorEl} onClose={handleMenuClose}
placement='top' zIndex={1301 /* need to be on top of the Modal on Mobile */}
sx={{ minWidth: 200 }}
>
<MenuItem
onClick={(event) => {
handleEditTitle(event, folder.id);
handleMenuClose();
}}
>
<ListItemDecorator>
<EditIcon />
</ListItemDecorator>
Edit
</MenuItem>
{!deleteArmed ? (
<MenuItem onClick={handleDeleteButtonShow}>
<ListItemDecorator>
<DeleteOutlineIcon />
</ListItemDecorator>
Delete
</MenuItem>
) : (
<>
<MenuItem onClick={handleDeleteCanceled}>
<ListItemDecorator>
<CloseIcon />
</ListItemDecorator>
Cancel
</MenuItem>
<MenuItem onClick={handleDeleteConfirmed} color='danger' sx={{ color: 'danger' }}>
<ListItemDecorator>
<DeleteOutlineIcon />
</ListItemDecorator>
Confirm Deletion
</MenuItem>
</>
)}
<MenuItem
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
p: 2,
minWidth: 200,
}}
>
<FormLabel
id='folder-color'
sx={{
mb: 1.5,
fontWeight: 'xl',
textTransform: 'uppercase',
fontSize: 'xs',
letterSpacing: '0.1em',
}}
>
Color
</FormLabel>
<RadioGroup
aria-labelledby='product-color-attribute'
defaultValue={folder.color || 'warning'}
onChange={handleColorChange}
sx={{ gap: 2, flexWrap: 'wrap', flexDirection: 'row', maxWidth: 240 }}
>
{FOLDERS_COLOR_PALETTE.map((color, index) => (
<Sheet
key={index}
sx={{
position: 'relative',
width: 20,
height: 20,
flexShrink: 0,
bgcolor: `${color}`,
borderRadius: '50%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Radio
overlay
variant='solid'
checkedIcon={<Done />}
value={color}
color='neutral'
slotProps={{
input: { 'aria-label': color },
radio: {
sx: {
display: 'contents',
'--variant-borderWidth': '2px',
},
},
}}
sx={{
'--joy-focus-outlineOffset': '4px',
'--joy-palette-focusVisible': color,
[`& .${radioClasses.action}.${radioClasses.focusVisible}`]: {
outlineWidth: '2px',
},
}}
/>
</Sheet>
))}
</RadioGroup>
</MenuItem>
</CloseableMenu>
)}
</ListItemButton>
</ListItem>
);
};
@@ -0,0 +1,22 @@
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,75 @@
import * as React from 'react';
import FolderIcon from '@mui/icons-material/Folder';
import type { DConversationId } from '~/common/state/store-chats';
import { DropdownItems, PageBarDropdown } from '~/common/layout/optima/components/PageBarDropdown';
import { useFolderStore } from '~/common/state/store-folders';
const SPECIAL_ID_REMOVE = '_REMOVE_';
export function useFolderDropdown(conversationId: DConversationId | null) {
// external state
const { folders, useFolders } = useFolderStore();
// Prepare items for the dropdown
const folderItems: DropdownItems = React.useMemo(() => {
// add one item per folder
const items = folders.reduce((items, folder) => {
items[folder.id] = {
title: folder.title,
icon: <FolderIcon sx={{ color: folder.color }} />,
};
return items;
}, {} as DropdownItems);
// add one item representing no folder
items[SPECIAL_ID_REMOVE] = {
title: 'No Folder',
};
return items;
}, [folders]);
// Handle dropdown folder change
const handleFolderChange = React.useCallback((_event: any, folderId: string | null) => {
if (conversationId && folderId) {
// Remove conversation from all folders
folders.forEach(folder => {
if (folder.conversationIds.includes(conversationId)) {
useFolderStore.getState().removeConversationFromFolder(folder.id, conversationId);
}
});
// Add conversation to the selected folder
useFolderStore.getState().addConversationToFolder(folderId, conversationId);
}
}, [conversationId, folders]);
// find the folder ID for the active Conversation
const currentFolderId = folders.find(folder => folder.conversationIds.includes(conversationId || ''))?.id || null;
// Create the dropdown component
const folderDropdown = React.useMemo(() => {
// don't show the dropdown if folders are not enabled
if (!useFolders)
return null;
return (
<PageBarDropdown
items={folderItems}
value={currentFolderId}
onChange={handleFolderChange}
placeholder='Select a folder'
showSymbols
/>
);
}, [currentFolderId, folderItems, handleFolderChange, useFolders]);
return { folderDropdown };
}
@@ -7,9 +7,9 @@ import SettingsIcon from '@mui/icons-material/Settings';
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
import { AppBarDropdown, DropdownItems } from '~/common/layout/AppBarDropdown';
import { PageBarDropdown, DropdownItems } from '~/common/layout/optima/components/PageBarDropdown';
import { KeyStroke } from '~/common/components/KeyStroke';
import { openLayoutLLMOptions, openLayoutModelsSetup } from '~/common/layout/store-applayout';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
function AppBarLLMDropdown(props: {
@@ -19,6 +19,9 @@ function AppBarLLMDropdown(props: {
placeholder?: string,
}) {
// external state
const { openLlmOptions, openModelsSetup } = useOptimaLayout();
// build model menu items, filtering-out hidden models, and add Source separators
const llmItems: DropdownItems = {};
let prevSourceId: DModelSourceId | null = null;
@@ -47,11 +50,11 @@ function AppBarLLMDropdown(props: {
const handleChatLLMChange = (_event: any, value: DLLMId | null) => value && props.setChatLlmId(value);
const handleOpenLLMOptions = () => props.chatLlmId && openLayoutLLMOptions(props.chatLlmId);
const handleOpenLLMOptions = () => props.chatLlmId && openLlmOptions(props.chatLlmId);
return (
<AppBarDropdown
<PageBarDropdown
items={llmItems}
value={props.chatLlmId} onChange={handleChatLLMChange}
placeholder={props.placeholder || 'Models …'}
@@ -67,7 +70,7 @@ function AppBarLLMDropdown(props: {
</ListItemButton>
)}
<ListItemButton key='menu-llms' onClick={openLayoutModelsSetup}>
<ListItemButton key='menu-llms' onClick={openModelsSetup}>
<ListItemDecorator><BuildCircleIcon color='success' /></ListItemDecorator>
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
Models
@@ -6,8 +6,8 @@ import CallIcon from '@mui/icons-material/Call';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { AppBarDropdown } from '~/common/layout/AppBarDropdown';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { PageBarDropdown } from '~/common/layout/optima/components/PageBarDropdown';
import { launchAppCall } from '~/common/app.routes';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
@@ -42,7 +42,7 @@ function AppBarPersonaDropdown(props: {
}
return (
<AppBarDropdown
<PageBarDropdown
items={SystemPurposes} showSymbols={zenMode !== 'cleaner'}
value={props.systemPurposeId} onChange={handleSystemPurposeChange}
appendOption={appendOption}
@@ -170,7 +170,7 @@ export function CameraCaptureModal(props: {
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'space-between' }}>
{/* Info */}
<IconButton disabled={!info} variant='soft' color='neutral' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
<InfoIcon />
</IconButton>
{/*<Button disabled={ocrProgress !== null} fullWidth variant='solid' size='lg' onClick={handleVideoOCRClicked} sx={{ flex: 1, maxWidth: 260 }}>*/}
@@ -189,7 +189,7 @@ export function CameraCaptureModal(props: {
</Button>
{/* Download */}
<IconButton variant='soft' color='neutral' onClick={handleVideoDownloadClicked}>
<IconButton size='lg' variant='soft' onClick={handleVideoDownloadClicked}>
<DownloadIcon />
</IconButton>
</Box>
@@ -5,7 +5,6 @@ import { Box, MenuItem, Radio, Typography } from '@mui/joy';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { KeyStroke } from '~/common/components/KeyStroke';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { ChatModeId } from '../../AppChat';
@@ -14,29 +13,25 @@ interface ChatModeDescription {
label: string;
description: string | React.JSX.Element;
shortcut?: string;
experimental?: boolean;
requiresTTI?: boolean;
}
const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
'immediate': {
'generate-text': {
label: 'Chat',
description: 'Persona replies',
},
'write-user': {
'append-user': {
label: 'Write',
description: 'Appends a message',
shortcut: 'Alt + Enter',
},
'draw-imagine': {
'generate-image': {
label: 'Draw',
description: 'AI Image Generation',
requiresTTI: true,
},
'draw-imagine-plus': {
label: 'Assisted Draw',
description: 'Assisted Image Generation',
experimental: true,
},
'react': {
'generate-react': {
label: 'Reason + Act · α',
description: 'Answers questions in multiple steps',
},
@@ -49,11 +44,14 @@ function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
return shortcut;
}
export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClose: () => void, chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void }) {
export function ChatModeMenu(props: {
anchorEl: HTMLAnchorElement | null, onClose: () => void,
chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void
capabilityHasTTI: boolean,
}) {
// external state
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
const labsMagicDraw = useUXLabsStore(state => state.labsMagicDraw);
return <CloseableMenu
placement='top-end' sx={{ minWidth: 320 }}
@@ -68,14 +66,13 @@ export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClos
{/* ChatMode items */}
{Object.entries(ChatModeItems)
.filter(([, { experimental }]) => labsMagicDraw || !experimental)
.map(([key, data]) =>
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
<Radio checked={key === props.chatModeId} />
<Box sx={{ flexGrow: 1 }}>
<Typography>{data.label}</Typography>
<Typography level='body-xs'>{data.description}</Typography>
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
</Box>
{(key === props.chatModeId || !!data.shortcut) && (
<KeyStroke combo={fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline)} />
+151 -66
View File
@@ -3,11 +3,13 @@ import { shallow } from 'zustand/shallow';
import { fileOpen, FileWithHandle } from 'browser-fs-access';
import { keyframes } from '@emotion/react';
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Typography } from '@mui/joy';
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Tooltip, Typography } from '@mui/joy';
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
import AttachFileIcon from '@mui/icons-material/AttachFile';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import AutoModeIcon from '@mui/icons-material/AutoMode';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import PsychologyIcon from '@mui/icons-material/Psychology';
import SendIcon from '@mui/icons-material/Send';
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
@@ -24,28 +26,33 @@ import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { countModelTokens } from '~/common/util/token-counter';
import { launchAppCall } from '~/common/app.routes';
import { openLayoutPreferences } from '~/common/layout/store-applayout';
import { lineHeightTextarea } from '~/common/app.theme';
import { playSoundUrl } from '~/common/util/audioUtils';
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
import { useDebouncer } from '~/common/components/useDebouncer';
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import type { ActileItem, ActileProvider } from './actile/ActileProvider';
import { providerCommands } from './actile/providerCommands';
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 { ComposerOutputMultiPart } from './composer.types';
import { ButtonAttachCameraMemo } from './ButtonAttachCamera';
import { ButtonAttachClipboardMemo } from './ButtonAttachClipboard';
import { ButtonAttachFileMemo } from './ButtonAttachFile';
import { ButtonCall } from './ButtonCall';
import { ButtonMicContinuationMemo } from './ButtonMicContinuation';
import { ButtonMicMemo } from './ButtonMic';
import { ButtonOptionsDraw } from './ButtonOptionsDraw';
import { ButtonAttachCameraMemo } from './buttons/ButtonAttachCamera';
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
import { ButtonCall } from './buttons/ButtonCall';
import { ButtonMicContinuationMemo } from './buttons/ButtonMicContinuation';
import { ButtonMicMemo } from './buttons/ButtonMic';
import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
import { ChatModeMenu } from './ChatModeMenu';
import { TokenBadgeMemo } from './TokenBadge';
import { TokenProgressbarMemo } from './TokenProgressbar';
@@ -71,8 +78,10 @@ export function Composer(props: {
chatLLM: DLLM | null;
composerTextAreaRef: React.RefObject<HTMLTextAreaElement>;
conversationId: DConversationId | null;
capabilityHasT2I: boolean;
isDeveloperMode: boolean;
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
onTextImagine: (conversationId: DConversationId, text: string) => void;
sx?: SxProps;
}) {
@@ -85,18 +94,19 @@ export function Composer(props: {
// external state
const isMobile = useIsMobile();
const { openPreferencesTab, setIsFocusedMode } = useOptimaLayout();
const { labsCalling, labsCameraDesktop } = useUXLabsStore(state => ({
labsCalling: state.labsCalling,
labsCameraDesktop: state.labsCameraDesktop,
}), shallow);
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('immediate');
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
const [startupText, setStartupText] = useComposerStartupText();
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
const { assistantTyping, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(state => {
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(state => {
const conversation = state.conversations.find(_c => _c.id === props.conversationId);
return {
assistantTyping: conversation ? !!conversation.abortController : false,
assistantAbortible: conversation ? !!conversation.abortController : false,
systemPurposeId: conversation?.systemPurposeId ?? null,
tokenCount: conversation ? conversation.tokenCount : 0,
stopTyping: state.stopTyping,
@@ -162,24 +172,6 @@ export function Composer(props: {
return enqueued;
}, [clearAttachments, conversationId, llmAttachments, onAction, setComposeText]);
const handleTextareaKeyDown = React.useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
// Alt: append the message instead
if (e.altKey) {
handleSendAction('write-user', composeText);
return e.preventDefault();
}
// Shift: toggles the 'enter is newline'
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
if (!assistantTyping)
handleSendAction(chatModeId, composeText);
return e.preventDefault();
}
}
}, [assistantTyping, chatModeId, composeText, enterIsNewline, handleSendAction]);
const handleSendClicked = () => handleSendAction(chatModeId, composeText);
const handleStopClicked = () => props.conversationId && stopTyping(props.conversationId);
@@ -189,7 +181,14 @@ export function Composer(props: {
const handleCallClicked = () => props.conversationId && systemPurposeId && launchAppCall(props.conversationId, systemPurposeId);
const handleDrawOptionsClicked = () => openLayoutPreferences(2);
const handleDrawOptionsClicked = () => openPreferencesTab(2);
const handleTextImagineClicked = () => {
if (!composeText || !props.conversationId)
return;
props.onTextImagine(props.conversationId, composeText);
setComposeText('');
};
// Mode menu
@@ -205,6 +204,67 @@ export function Composer(props: {
};
// Actiles
const onActileCommandSelect = React.useCallback((item: ActileItem) => {
if (props.composerTextAreaRef.current) {
const textArea = props.composerTextAreaRef.current;
const currentText = textArea.value;
const cursorPos = textArea.selectionStart;
// Find the position where the command starts
const commandStart = currentText.lastIndexOf('/', cursorPos);
// Construct the new text with the autocompleted command
const newText = currentText.substring(0, commandStart) + item.label + ' ' + currentText.substring(cursorPos);
// Update the text area with the new text
setComposeText(newText);
// Move the cursor to the end of the autocompleted command
const newCursorPos = commandStart + item.label.length + 1;
textArea.setSelectionRange(newCursorPos, newCursorPos);
}
}, [props.composerTextAreaRef, setComposeText]);
const actileProviders: ActileProvider[] = React.useMemo(() => {
return [providerCommands(onActileCommandSelect)];
}, [onActileCommandSelect]);
const { actileComponent, actileInterceptKeydown } = useActileManager(actileProviders, props.composerTextAreaRef);
// Text typing
const handleTextareaTextChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setComposeText(e.target.value);
}, [setComposeText]);
const handleTextareaKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
// disable keyboard handling if the actile is visible
if (actileInterceptKeydown(e))
return;
// Enter: primary action
if (e.key === 'Enter') {
// Alt: append the message instead
if (e.altKey) {
handleSendAction('append-user', composeText);
return e.preventDefault();
}
// Shift: toggles the 'enter is newline'
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
if (!assistantAbortible)
handleSendAction(chatModeId, composeText);
return e.preventDefault();
}
}
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction]);
// Mic typing & continuation mode
const onSpeechResultCallback = React.useCallback((result: SpeechResult) => {
@@ -221,7 +281,7 @@ export function Composer(props: {
nextText = nextText ? nextText + ' ' + transcript : transcript;
// auto-send (mic continuation mode) if requested
const autoSend = micContinuation && nextText.length >= 1 && !!props.conversationId; //&& assistantTyping;
const autoSend = micContinuation && nextText.length >= 1 && !!props.conversationId; //&& assistantAbortible;
const notUserStop = result.doneReason !== 'manual';
if (autoSend) {
if (notUserStop)
@@ -243,7 +303,7 @@ export function Composer(props: {
useGlobalShortcut('m', true, false, false, toggleRecording);
const micIsRunning = !!speechInterimResult;
const micContinuationTrigger = micContinuation && !micIsRunning && !assistantTyping;
const micContinuationTrigger = micContinuation && !micIsRunning && !assistantAbortible;
const micColor: ColorPaletteProp = isSpeechError ? 'danger' : isRecordingSpeech ? 'primary' : isRecordingAudio ? 'primary' : 'neutral';
const micVariant: VariantProp = isRecordingSpeech ? 'solid' : isRecordingAudio ? 'soft' : 'soft'; //(isDesktop ? 'soft' : 'plain');
@@ -350,24 +410,25 @@ export function Composer(props: {
}, [attachAppendDataTransfer, eatDragEvent, setComposeText]);
const isImmediate = chatModeId === 'immediate';
const isWriteUser = chatModeId === 'write-user';
const isChat = isImmediate || isWriteUser;
const isReAct = chatModeId === 'react';
const isDraw = chatModeId === 'draw-imagine';
const isDrawPlus = chatModeId === 'draw-imagine-plus';
const buttonColor: ColorPaletteProp = isReAct ? 'success' : (isDraw || isDrawPlus) ? 'warning' : 'primary';
const isText = chatModeId === 'generate-text';
const isAppend = chatModeId === 'append-user';
const isChat = isText || isAppend;
const isReAct = chatModeId === 'generate-react';
const isDraw = chatModeId === 'generate-image';
const buttonColor: ColorPaletteProp = assistantAbortible
? 'warning'
: isReAct ? 'success' : isDraw ? 'warning' : 'primary';
const textPlaceholder: string =
isDrawPlus
? 'Write a subject, and we\'ll add detail...'
: isDraw
? 'Describe an idea or a drawing...'
: isReAct
? 'Multi-step reasoning question...'
: props.isDeveloperMode
? 'Chat with me · drop source files · attach code...'
: /*isProdiaConfigured ?*/ 'Chat · /react · /imagine · drop text files...' /*: 'Chat · /react · drop text files...'*/;
isDraw
? 'Describe an idea or a drawing...'
: isReAct
? 'Multi-step reasoning question...'
: props.isDeveloperMode
? 'Chat with me · drop source files · attach code...'
: props.capabilityHasT2I
? 'Chat · /react · /draw · drop text files...'
: 'Chat · /react · drop text files...';
return (
@@ -379,7 +440,7 @@ export function Composer(props: {
{/* Vertical (insert) buttons */}
{isMobile ? (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { md: 2 } }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{/* [mobile] Mic button */}
{isSpeechEnabled && <ButtonMicMemo variant={micVariant} color={micColor} onClick={handleToggleMic} />}
@@ -395,7 +456,7 @@ export function Composer(props: {
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { md: 2 } }}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{/*<FormHelperText sx={{ mx: 'auto' }}>*/}
{/* Attach*/}
@@ -417,7 +478,7 @@ export function Composer(props: {
<Box sx={{
flexGrow: 1,
display: 'flex', flexDirection: 'column', gap: 1,
overflowX: 'clip',
minWidth: 200, // enable X-scrolling (resetting any possible minWidth due to the attachments)
}}>
{/* Edit box + Overlays + Mic buttons */}
@@ -427,16 +488,18 @@ export function Composer(props: {
<Box sx={{ position: 'relative' }}>
<Textarea
variant='outlined' color={(isDraw || isDrawPlus) ? 'warning' : isReAct ? 'success' : 'neutral'}
variant='outlined' color={isDraw ? 'warning' : isReAct ? 'success' : 'neutral'}
autoFocus
minRows={5} maxRows={10}
minRows={isMobile ? 5 : 5} maxRows={10}
placeholder={textPlaceholder}
value={composeText}
onChange={(event) => setComposeText(event.target.value)}
onChange={handleTextareaTextChange}
onDragEnter={handleTextareaDragEnter}
onDragStart={handleTextareaDragStart}
onKeyDown={handleTextareaKeyDown}
onPasteCapture={handleAttachCtrlV}
onFocusCapture={() => setIsFocusedMode(true)}
onBlurCapture={() => setIsFocusedMode(false)}
slotProps={{
textarea: {
enterKeyHint: enterIsNewline ? 'enter' : 'send',
@@ -452,8 +515,7 @@ export function Composer(props: {
'&:focus-within': {
backgroundColor: 'background.popup',
},
// fontSize: '16px',
lineHeight: 1.75,
lineHeight: lineHeightTextarea,
}} />
{tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
@@ -555,34 +617,40 @@ export function Composer(props: {
{/* [mobile] bottom-corner secondary button */}
{isMobile && (isChat
? <ButtonCall isMobile disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} sx={{ mr: { xs: 1, md: 2 } }} />
: (isDraw || isDrawPlus)
: isDraw
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
: <IconButton disabled variant='plain' color='neutral' sx={{ mr: { xs: 1, md: 2 } }} />
: <IconButton disabled sx={{ mr: { xs: 1, md: 2 } }} />
)}
{/* Responsive Send/Stop buttons */}
<ButtonGroup
variant={isWriteUser ? 'outlined' : 'solid'}
variant={isAppend ? 'outlined' : 'solid'}
color={buttonColor}
sx={{
flexGrow: 1,
boxShadow: isMobile ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
}}
>
{!assistantTyping ? (
{!assistantAbortible ? (
<Button
key='composer-act'
fullWidth disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
onClick={handleSendClicked}
endDecorator={micContinuation ? <AutoModeIcon /> : isWriteUser ? <SendIcon sx={{ fontSize: 18 }} /> : isReAct ? <PsychologyIcon /> : <TelegramIcon />}
endDecorator={
micContinuation ? <AutoModeIcon /> :
isAppend ? <SendIcon sx={{ fontSize: 18 }} /> :
isReAct ? <PsychologyIcon /> :
isDraw ? <FormatPaintIcon />
: <TelegramIcon />
}
>
{micContinuation && 'Voice '}
{isWriteUser ? 'Write' : isReAct ? 'ReAct' : isDraw ? 'Draw' : isDrawPlus ? 'Draw+' : 'Chat'}
{isAppend ? 'Write' : isReAct ? 'ReAct' : isDraw ? 'Draw' : 'Chat'}
</Button>
) : (
<Button
key='composer-stop'
fullWidth variant='soft' color={isReAct ? 'success' : 'primary'} disabled={!props.conversationId}
fullWidth variant='soft' disabled={!props.conversationId}
onClick={handleStopClicked}
endDecorator={<StopOutlinedIcon sx={{ fontSize: 18 }} />}
sx={{ animation: `${animationStopEnter} 0.1s ease-out` }}
@@ -590,7 +658,20 @@ export function Composer(props: {
Stop
</Button>
)}
<IconButton disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor} onClick={handleModeSelectorShow}>
{/* [Draw] Imagine */}
{isDraw && !!composeText && <Tooltip title='Imagine a drawing prompt'>
<IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleTextImagineClicked}>
<AutoAwesomeIcon />
</IconButton>
</Tooltip>}
{/* Mode expander */}
<IconButton
variant={assistantAbortible ? 'soft' : isDraw ? undefined : undefined}
disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor}
onClick={handleModeSelectorShow}
>
<ExpandLessIcon />
</IconButton>
</ButtonGroup>
@@ -605,7 +686,7 @@ export function Composer(props: {
{isChat && <ButtonCall disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
{/* [desktop] Draw Options secondary button */}
{(isDraw || isDrawPlus) && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
</Box>}
@@ -618,9 +699,13 @@ export function Composer(props: {
<ChatModeMenu
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
capabilityHasTTI={props.capabilityHasT2I}
/>
)}
{/* Actile */}
{actileComponent}
</Grid>
</Box>
);
@@ -0,0 +1,81 @@
import * as React from 'react';
import { Box, ListItem, ListItemButton, ListItemDecorator, Sheet, Typography } from '@mui/joy';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import type { ActileItem } from './ActileProvider';
export function ActilePopup(props: {
anchorEl: HTMLElement | null,
onClose: () => void,
title?: string,
items: ActileItem[],
activeItemIndex: number | undefined,
activePrefixLength: number,
onItemClick: (item: ActileItem) => void,
children?: React.ReactNode
}) {
const hasAnyIcon = props.items.some(item => !!item.Icon);
return (
<CloseableMenu open anchorEl={props.anchorEl} onClose={props.onClose} noTopPadding>
{!!props.title && (
<Sheet variant='soft' sx={{ p: 1, borderBottom: '1px solid', borderBottomColor: 'neutral.softActiveBg' }}>
{/*<ListItemDecorator/>*/}
<Typography level='title-md'>
{props.title}
</Typography>
</Sheet>
)}
{!props.items.length && (
<ListItem variant='soft' color='primary'>
<Typography level='body-md'>
No matching command
</Typography>
</ListItem>
)}
{props.items.map((item, idx) => {
const labelBold = item.label.slice(0, props.activePrefixLength);
const labelNormal = item.label.slice(props.activePrefixLength);
return (
<ListItemButton
key={item.id}
variant={idx === props.activeItemIndex ? 'soft' : undefined}
onClick={() => props.onItemClick(item)}
>
{hasAnyIcon && (
<ListItemDecorator>
{item.Icon ? <item.Icon /> : null}
</ListItemDecorator>
)}
<Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography level='title-md' color='primary'>
<span style={{ fontWeight: 600, textDecoration: 'underline' }}>{labelBold}</span>{labelNormal}
</Typography>
{item.argument && <Typography level='body-sm'>
{item.argument}
</Typography>}
</Box>
{!!item.description && <Typography level='body-xs'>
{item.description}
</Typography>}
</Box>
</ListItemButton>
);
},
)}
{props.children}
</CloseableMenu>
);
}
@@ -0,0 +1,21 @@
import type { FunctionComponent } from 'react';
export interface ActileItem {
id: string;
label: string;
argument?: string;
description?: string;
Icon?: FunctionComponent;
}
type ActileProviderIds = 'actile-commands' | 'actile-attach-reference';
export interface ActileProvider {
id: ActileProviderIds;
title: string;
checkTriggerText: (trailingText: string) => boolean;
fetchItems: () => Promise<ActileItem[]>;
onItemSelect: (item: ActileItem) => void;
}
@@ -0,0 +1,23 @@
import { ActileItem, ActileProvider } from './ActileProvider';
export const providerAttachReference: ActileProvider = {
id: 'actile-attach-reference',
title: 'Attach Reference',
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);
},
};
@@ -0,0 +1,23 @@
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',
checkTriggerText: (trailingText: string) =>
trailingText.trim() === '/',
fetchItems: async () => {
return findAllChatCommands().map((cmd) => ({
id: cmd.primary,
label: cmd.primary,
argument: cmd.arguments?.join(' ') ?? undefined,
description: cmd.description,
Icon: cmd.Icon,
}));
},
onItemSelect,
});
@@ -0,0 +1,118 @@
import * as React from 'react';
import { ActileItem, ActileProvider } from './ActileProvider';
import { ActilePopup } from './ActilePopup';
export const useActileManager = (providers: ActileProvider[], anchorRef: React.RefObject<HTMLElement>) => {
// state
const [popupOpen, setPopupOpen] = React.useState(false);
const [provider, setProvider] = React.useState<ActileProvider | null>(null);
const [items, setItems] = React.useState<ActileItem[]>([]);
const [activeSearchString, setActiveSearchString] = React.useState<string>('');
const [activeItemIndex, setActiveItemIndex] = React.useState<number>(0);
// derived state
const activeItems = React.useMemo(() => {
const search = activeSearchString.trim().toLowerCase();
return items.filter(item => item.label.toLowerCase().startsWith(search));
}, [items, activeSearchString]);
const activeItem = activeItemIndex >= 0 && activeItemIndex < activeItems.length ? activeItems[activeItemIndex] : null;
const handleClose = React.useCallback(() => {
setPopupOpen(false);
setProvider(null);
setItems([]);
setActiveSearchString('');
setActiveItemIndex(0);
}, []);
const handlePopupItemClicked = React.useCallback((item: ActileItem) => {
provider?.onItemSelect(item);
handleClose();
}, [handleClose, provider]);
const handleEnterKey = React.useCallback(() => {
activeItem && handlePopupItemClicked(activeItem);
}, [activeItem, handlePopupItemClicked]);
const actileInterceptKeydown = React.useCallback((_event: React.KeyboardEvent<HTMLTextAreaElement>): boolean => {
// Popup open: Intercept
const { key, currentTarget, ctrlKey, metaKey } = _event;
if (popupOpen) {
if (key === 'Escape' || key === 'ArrowLeft') {
_event.preventDefault();
handleClose();
} else if (key === 'ArrowUp') {
_event.preventDefault();
setActiveItemIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : activeItems.length - 1));
} else if (key === 'ArrowDown') {
_event.preventDefault();
setActiveItemIndex((prevIndex) => (prevIndex < activeItems.length - 1 ? prevIndex + 1 : 0));
} else if (key === 'Enter' || key === 'ArrowRight' || key === 'Tab' || (key === ' ' && activeItems.length === 1)) {
_event.preventDefault();
handleEnterKey();
} else if (key === 'Backspace') {
handleClose();
} else if (key.length === 1 && !ctrlKey && !metaKey) {
setActiveSearchString((prev) => prev + key);
setActiveItemIndex(0);
}
return true;
}
// Popup closed: Check for triggers
// optimization
if (key !== '/' && key !== '@')
return false;
const trailingText = (currentTarget.value || '') + key;
// check all rules to find one that triggers
for (const provider of providers) {
if (provider.checkTriggerText(trailingText)) {
setProvider(provider);
setPopupOpen(true);
setActiveSearchString(key);
provider
.fetchItems()
.then(items => setItems(items))
.catch(error => {
handleClose();
console.error('Failed to fetch popup items:', error);
});
return true;
}
}
return false;
}, [activeItems.length, handleClose, handleEnterKey, popupOpen, providers]);
const actileComponent = React.useMemo(() => {
return !popupOpen ? null : (
<ActilePopup
anchorEl={anchorRef.current}
onClose={handleClose}
title={provider?.title}
items={activeItems}
activeItemIndex={activeItemIndex}
activePrefixLength={activeSearchString.length}
onItemClick={handlePopupItemClicked}
/>
);
}, [activeItemIndex, activeItems, activeSearchString.length, anchorRef, handleClose, handlePopupItemClicked, popupOpen, provider?.title]);
return {
actileComponent,
actileInterceptKeydown,
};
};
@@ -111,7 +111,7 @@ export function Attachments(props: {
{/* Overall Menu button */}
<IconButton
variant='plain' onClick={handleOverallMenuToggle}
onClick={handleOverallMenuToggle}
sx={{
// borderRadius: 'sm',
borderRadius: 0,
@@ -11,6 +11,16 @@ 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',
'application/json',
];
/**
* Creates a new Attachment object.
*/
@@ -141,7 +151,7 @@ export function attachmentDefineConverters(sourceType: AttachmentSource['media']
switch (true) {
// plain text types
case ['text/plain', 'text/html', 'text/markdown', 'text/csv', 'application/json'].includes(input.mimeType):
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');
@@ -3,7 +3,7 @@ import * as React from 'react';
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
import { CameraCaptureModal } from './CameraCaptureModal';
import { CameraCaptureModal } from '../CameraCaptureModal';
const attachCameraLegend = (isMobile: boolean) =>
@@ -23,7 +23,7 @@ function ButtonAttachCamera(props: { isMobile?: boolean, onAttachImage: (file: F
{/* The Button */}
{props.isMobile ? (
<IconButton variant='plain' color='neutral' onClick={() => setOpen(true)}>
<IconButton onClick={() => setOpen(true)}>
<AddAPhotoIcon />
</IconButton>
) : (
@@ -11,7 +11,7 @@ export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => vo
<FormatPaintIcon />
</IconButton>
) : (
<Button variant='soft' color='warning' onClick={props.onClick} endDecorator={<FormatPaintIcon />} sx={props.sx}>
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
Options
</Button>
);
@@ -3,7 +3,7 @@ import TimeAgo from 'react-timeago';
import { shallow } from 'zustand/shallow';
import { cleanupEfficiency, Diff as TextDiff, makeDiff } from '@sanity/diff-match-patch';
import { Avatar, Box, Button, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Stack, Switch, Tooltip, Typography } from '@mui/joy';
import { Avatar, Box, Button, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import ClearIcon from '@mui/icons-material/Clear';
@@ -14,7 +14,6 @@ import Face6Icon from '@mui/icons-material/Face6';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import PaletteOutlinedIcon from '@mui/icons-material/PaletteOutlined';
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
import ReplayIcon from '@mui/icons-material/Replay';
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
@@ -30,7 +29,7 @@ import { KeyStroke } from '~/common/components/KeyStroke';
import { Link } from '~/common/components/Link';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { cssRainbowColorKeyframes } from '~/common/app.theme';
import { cssRainbowColorKeyframes, lineHeightChatText } from '~/common/app.theme';
import { prettyBaseModel } from '~/common/util/modelUtils';
import { useUIPreferencesStore } from '~/common/state/store-ui';
@@ -69,47 +68,54 @@ export function messageBackground(messageRole: DMessage['role'] | string, wasEdi
}
}
const avatarIconSx = { width: 36, height: 36 };
export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['role'] | string, messageOriginLLM: string | undefined, messagePurposeId: SystemPurposeId | undefined, messageSender: string, messageTyping: boolean, size: 'sm' | undefined = undefined): React.JSX.Element {
if (typeof messageAvatar === 'string' && messageAvatar)
return <Avatar alt={messageSender} src={messageAvatar} />;
const iconSx = { width: 40, height: 40 };
const mascotSx = size === 'sm' ? { width: 40, height: 40 } : { width: 64, height: 64 };
const mascotSx = size === 'sm' ? avatarIconSx : { width: 64, height: 64 };
switch (messageRole) {
case 'system':
return <SettingsSuggestIcon sx={iconSx} />; // https://em-content.zobj.net/thumbs/120/apple/325/robot_1f916.png
return <SettingsSuggestIcon sx={avatarIconSx} />; // https://em-content.zobj.net/thumbs/120/apple/325/robot_1f916.png
case 'user':
return <Face6Icon sx={avatarIconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
case 'assistant':
// display a gif avatar when the assistant is typing (people seem to love this, so keeping it after april fools')
// typing gif (people seem to love this, so keeping it after april fools')
const isTextToImage = messageOriginLLM === 'DALL·E' || messageOriginLLM === 'Prodia';
const isReact = messageOriginLLM?.startsWith('react-');
if (messageTyping) {
return <Avatar
alt={messageSender} variant='plain'
src={messageOriginLLM === 'prodia'
? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
: messageOriginLLM?.startsWith('react-')
? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
src={isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'}
sx={{ ...mascotSx, borderRadius: 'var(--joy-radius-sm)' }}
sx={{ ...mascotSx, borderRadius: 'sm' }}
/>;
}
// display the purpose symbol
if (messageOriginLLM === 'prodia')
return <PaletteOutlinedIcon sx={iconSx} />;
const symbol = SystemPurposes[messagePurposeId!]?.symbol;
if (symbol)
return <Box
sx={{
fontSize: '24px',
textAlign: 'center',
width: '100%', minWidth: `${iconSx.width}px`, lineHeight: `${iconSx.height}px`,
}}
>
{symbol}
</Box>;
// default assistant avatar
return <SmartToyOutlinedIcon sx={iconSx} />; // https://mui.com/static/images/avatar/2.jpg
case 'user':
return <Face6Icon sx={iconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
// text-to-image: icon
if (isTextToImage)
return <FormatPaintIcon sx={{
...avatarIconSx,
animation: `${cssRainbowColorKeyframes} 1s linear 2.66`,
}} />;
// purpose symbol (if present)
const symbol = SystemPurposes[messagePurposeId!]?.symbol;
if (symbol) return <Box sx={{
fontSize: '24px',
textAlign: 'center',
width: '100%',
minWidth: `${avatarIconSx.width}px`,
lineHeight: `${avatarIconSx.height}px`,
}}>
{symbol}
</Box>;
// default assistant avatar
return <SmartToyOutlinedIcon sx={avatarIconSx} />; // https://mui.com/static/images/avatar/2.jpg
}
return <Avatar alt={messageSender} />;
}
@@ -144,13 +150,14 @@ function explainErrorInMessage(text: string, isAssistant: boolean, modelId?: str
</>;
} else if (text.includes('"context_length_exceeded"')) {
// TODO: propose to summarize or split the input?
const pattern = /maximum context length is (\d+) tokens.+you requested (\d+) tokens/;
const pattern = /maximum context length is (\d+) tokens.+resulted in (\d+) tokens/;
const match = pattern.exec(text);
const usedText = match ? <b>{parseInt(match[2] || '0').toLocaleString()} tokens &gt; {parseInt(match[1] || '0').toLocaleString()}</b> : '';
errorMessage = <>
This thread <b>surpasses the maximum size</b> allowed for {modelId || 'this model'}. {usedText}.
Please consider removing some earlier messages from the conversation, start a new conversation,
choose a model with larger context, or submit a shorter new message.
{!usedText && ` -- ${text}`}
</>;
}
// [OpenAI] {"error":{"message":"Incorrect API key provided: ...","type":"invalid_request_error","param":null,"code":"invalid_api_key"}}
@@ -256,9 +263,9 @@ export function ChatMessage(props: {
const showAvatars = props.hideAvatars !== true && !cleanerLooks;
const textSel = selMenuText ? selMenuText : messageText;
const isSpecialProdia = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/imagine') || textSel.startsWith('/img');
const couldDiagram = textSel?.length >= 100 && !isSpecialProdia;
const couldImagine = textSel?.length >= 2 && !isSpecialProdia;
const isSpecialT2I = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/draw ') || textSel.startsWith('/imagine ') || textSel.startsWith('/img ');
const couldDiagram = textSel?.length >= 100 && !isSpecialT2I;
const couldImagine = textSel?.length >= 2 && !isSpecialT2I;
const couldSpeak = couldImagine;
@@ -406,15 +413,19 @@ export function ChatMessage(props: {
// per-blocks css
const blockSx: SxProps = {
my: 'auto',
lineHeight: lineHeightChatText,
};
const typographySx: SxProps = {
lineHeight: lineHeightChatText,
};
const codeSx: SxProps = {
// backgroundColor: fromAssistant ? 'background.level1' : 'background.level1',
backgroundColor: props.codeBackground ? props.codeBackground : fromAssistant ? 'neutral.plainHoverBg' : 'primary.plainActiveBg',
boxShadow: 'xs',
fontFamily: 'code',
fontSize: '14px',
fontSize: '0.875rem',
fontVariantLigatures: 'none',
lineHeight: 1.75,
lineHeight: lineHeightChatText,
borderRadius: 'var(--joy-radius-sm)',
};
@@ -441,46 +452,57 @@ export function ChatMessage(props: {
borderBottomColor: 'divider',
}),
...(ENABLE_COPY_MESSAGE_OVERLAY && { position: 'relative' }),
...(props.isBottom === true && { mb: 'auto' }),
'&:hover > button': { opacity: 1 },
...props.sx,
}}
>
{/* Avatar */}
{showAvatars && <Stack
sx={{ alignItems: 'center', minWidth: { xs: 50, md: 64 }, maxWidth: 80, textAlign: 'center' }}
onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}
onClick={event => setOpsMenuAnchor(event.currentTarget)}>
{showAvatars && (
<Box
onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}
onClick={event => setOpsMenuAnchor(event.currentTarget)}
sx={{
// flexBasis: 0, // this won't let the item grow
display: 'flex', flexDirection: 'column', alignItems: 'center',
minWidth: { xs: 50, md: 64 }, maxWidth: 80,
textAlign: 'center',
}}
>
{isHovering ? (
<IconButton variant='soft' color={fromAssistant ? 'neutral' : 'primary'}>
<MoreVertIcon />
</IconButton>
) : (
avatarEl
)}
{isHovering ? (
<IconButton variant='soft' color={fromAssistant ? 'neutral' : 'primary'} sx={avatarIconSx}>
<MoreVertIcon />
</IconButton>
) : (
avatarEl
)}
{/* Assistant model name */}
{fromAssistant && (
<Tooltip title={messageOriginLLM || 'unk-model'} variant='solid'>
<Typography level='body-sm' sx={{
fontSize: { xs: 'xs', sm: 'sm' }, fontWeight: 500,
overflowWrap: 'anywhere',
...(messageTyping ? { animation: `${cssRainbowColorKeyframes} 5s linear infinite` } : {}),
}}>
{prettyBaseModel(messageOriginLLM)}
</Typography>
</Tooltip>
)}
{/* Assistant model name */}
{fromAssistant && (
<Tooltip title={messageOriginLLM || 'unk-model'} variant='solid'>
<Typography level='body-xs' sx={{
overflowWrap: 'anywhere',
...(messageTyping ? { animation: `${cssRainbowColorKeyframes} 5s linear infinite` } : {}),
}}>
{prettyBaseModel(messageOriginLLM)}
</Typography>
</Tooltip>
)}
</Stack>}
</Box>
)}
{/* Edit / Blocks */}
{isEditing
? <InlineTextarea initialText={messageText} onEdit={handleTextEdited} sx={{ ...blockSx, lineHeight: 1.75, flexGrow: 1 }} />
? <InlineTextarea
initialText={messageText} onEdit={handleTextEdited}
sx={{
...blockSx,
flexGrow: 1,
}} />
: <Box
onContextMenu={(ENABLE_SELECTION_RIGHT_CLICK_MENU && props.onMessageEdit) ? event => handleMouseUp(event.nativeEvent) : undefined}
@@ -522,14 +544,14 @@ export function ChatMessage(props: {
: block.type === 'code'
? <RenderCode key={'code-' + index} codeBlock={block} sx={codeSx} noCopyButton={props.diagramMode} />
: block.type === 'image'
? <RenderImage key={'image-' + index} imageBlock={block} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
? <RenderImage key={'image-' + index} imageBlock={block} isFirst={!index} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
: block.type === 'latex'
? <RenderLatex key={'latex-' + index} latexBlock={block} />
? <RenderLatex key={'latex-' + index} latexBlock={block} sx={typographySx} />
: block.type === 'diff'
? <RenderTextDiff key={'latex-' + index} diffBlock={block} />
? <RenderTextDiff key={'latex-' + index} diffBlock={block} sx={typographySx} />
: (renderMarkdown && props.noMarkdown !== true && !fromSystem && !(fromUser && block.content.startsWith('/')))
? <RenderMarkdown key={'text-md-' + index} textBlock={block} />
: <RenderText key={'text-' + index} textBlock={block} />)}
? <RenderMarkdown key={'text-md-' + index} textBlock={block} sx={typographySx} />
: <RenderText key={'text-' + index} textBlock={block} sx={typographySx} />)}
{isCollapsed && (
<Button variant='plain' color='neutral' onClick={handleUncollapse}>... expand ...</Button>
@@ -549,7 +571,7 @@ export function ChatMessage(props: {
{ENABLE_COPY_MESSAGE_OVERLAY && !fromSystem && !isEditing && (
<Tooltip title={fromAssistant ? 'Copy message' : 'Copy input'} variant='solid'>
<IconButton
variant='outlined' color='neutral' onClick={handleOpsCopy}
variant='outlined' onClick={handleOpsCopy}
sx={{
position: 'absolute', ...(fromAssistant ? { right: { xs: 12, md: 28 } } : { left: { xs: 12, md: 28 } }), zIndex: 10,
opacity: 0, transition: 'opacity 0.3s',
@@ -617,7 +639,7 @@ export function ChatMessage(props: {
</MenuItem>}
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
Imagine
Draw ...
</MenuItem>}
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
@@ -13,7 +13,7 @@ import { makeAvatar, messageBackground } from './ChatMessage';
/**
* Header bar for controlling the operations during the Selection mode
*/
export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
export const MessagesSelectionHeader = (props: { hasSelected: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
<Sheet color='warning' variant='solid' invertedColors sx={{
display: 'flex', flexDirection: 'row', alignItems: 'center',
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101,
@@ -28,7 +28,7 @@ export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom:
Delete
</Button>
<IconButton variant='plain' onClick={props.onClose}>
<IconButton onClick={props.onClose}>
<ClearIcon />
</IconButton>
</Sheet>;
@@ -39,7 +39,7 @@ export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom:
*
* Shall look similarly to the main ChatMessage, for consistency, but just allow a simple checkbox selection
*/
export function CleanerMessage(props: { message: DMessage, isBottom: boolean, selected: boolean, remainingTokens?: number, onToggleSelected?: (messageId: string, selected: boolean) => void }) {
export function CleanerMessage(props: { message: DMessage, selected: boolean, remainingTokens?: number, onToggleSelected?: (messageId: string, selected: boolean) => void }) {
// derived state
const {
@@ -77,7 +77,6 @@ export function CleanerMessage(props: { message: DMessage, isBottom: boolean, se
borderBottom: '1px solid',
borderBottomColor: 'divider',
// position: 'relative',
...(props.isBottom && { mb: 'auto' }),
'&:hover > button': { opacity: 1 },
}}>
@@ -173,21 +173,21 @@ function RenderCodeImpl(props: {
)}
{isMermaid && (
<Tooltip title={renderMermaid ? 'Show Code' : 'Render Mermaid'} variant='solid'>
<IconButton variant={renderMermaid ? 'solid' : 'outlined'} color='neutral' onClick={() => setShowMermaid(!showMermaid)}>
<IconButton variant={renderMermaid ? 'solid' : 'outlined'} onClick={() => setShowMermaid(!showMermaid)}>
<SchemaIcon />
</IconButton>
</Tooltip>
)}
{isPlantUML && (
<Tooltip title={renderPlantUML ? 'Show Code' : 'Render PlantUML'} variant='solid'>
<IconButton variant={renderPlantUML ? 'solid' : 'outlined'} color='neutral' onClick={() => setShowPlantUML(!showPlantUML)}>
<IconButton variant={renderPlantUML ? 'solid' : 'outlined'} onClick={() => setShowPlantUML(!showPlantUML)}>
<SchemaIcon />
</IconButton>
</Tooltip>
)}
{isSVG && (
<Tooltip title={renderSVG ? 'Show Code' : 'Render SVG'} variant='solid'>
<IconButton variant={renderSVG ? 'solid' : 'outlined'} color='neutral' onClick={() => setShowSVG(!showSVG)}>
<IconButton variant={renderSVG ? 'solid' : 'outlined'} onClick={() => setShowSVG(!showSVG)}>
<ShapeLineOutlinedIcon />
</IconButton>
</Tooltip>
@@ -195,7 +195,7 @@ function RenderCodeImpl(props: {
{canCodepen && <OpenInCodepen codeBlock={{ code: blockCode, language: inferredCodeLanguage || undefined }} />}
{canReplit && <OpenInReplit codeBlock={{ code: blockCode, language: inferredCodeLanguage || undefined }} />}
{props.noCopyButton !== true && <Tooltip title='Copy Code' variant='solid'>
<IconButton variant='outlined' color='neutral' onClick={handleCopyToClipboard}>
<IconButton variant='outlined' onClick={handleCopyToClipboard}>
<ContentCopyIcon />
</IconButton>
</Tooltip>}
@@ -104,7 +104,7 @@ export function RenderHtml(props: { htmlBlock: HtmlBlock, sx?: SxProps }) {
</IconButton>
</Tooltip>
<Tooltip title='Copy Code' variant='solid'>
<IconButton variant='outlined' color='neutral' onClick={handleCopyToClipboard}>
<IconButton variant='outlined' onClick={handleCopyToClipboard}>
<ContentCopyIcon />
</IconButton>
</Tooltip>
+109 -16
View File
@@ -1,8 +1,8 @@
import * as React from 'react';
import { Box, IconButton, Tooltip } from '@mui/joy';
import { Alert, Box, IconButton, Tooltip, Typography } from '@mui/joy';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import ReplayIcon from '@mui/icons-material/Replay';
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
import { Link } from '~/common/components/Link';
@@ -10,39 +10,132 @@ import { ImageBlock } from './blocks';
import { overlayButtonsSx } from './RenderCode';
export const RenderImage = (props: { imageBlock: ImageBlock, allowRunAgain: boolean, onRunAgain?: (e: React.MouseEvent) => void }) => {
const imageUrls = props.imageBlock.url.split('\n');
const mdImageReferenceRegex = /^!\[([^\]]*)]\(([^)]+)\)$/;
const imageExtensions = /\.(jpg|jpeg|png|gif|bmp|svg)/i;
return imageUrls.map((url, index) => (
<Box
/**
* Checks if the entire content consists solely of Markdown image references.
* If so, returns an array of ImageBlock objects for each image reference.
* If any non-image content is present or if there are no image references, returns null.
*/
export function heuristicMarkdownImageReferenceBlocks(fullText: string) {
// Check if all lines are valid Markdown image references with image URLs
const imageBlocks: ImageBlock[] = [];
for (const line of fullText.split('\n')) {
if (line.trim() === '') continue; // skip empty lines
const match = mdImageReferenceRegex.exec(line);
if (match && imageExtensions.test(match[2])) {
const alt = match[1];
const url = match[2];
imageBlocks.push({ type: 'image', url, alt });
} else {
// if there is any outlier line, return null
return null;
}
}
// Return the image blocks if all lines are image references with valid image URLs
return imageBlocks.length > 0 ? imageBlocks : null;
}
const prodiaUrlRegex = /^(https?:\/\/images\.prodia\.\S+)$/i;
/**
* Legacy heuristic for detecting images from "images.prodia." URLs.
*/
export function heuristicLegacyImageBlocks(fullText: string): ImageBlock[] | null {
// Check if all lines are URLs starting with "http://images.prodia." or "https://images.prodia."
const imageBlocks: ImageBlock[] = [];
for (const line of fullText.split('\n')) {
const match = prodiaUrlRegex.exec(line);
if (match) {
const url = match[1];
imageBlocks.push({ type: 'image', url });
} else {
// if there is any outlier line, return null
return null;
}
}
// Return the image blocks if all lines are URLs from "images.prodia."
return imageBlocks.length > 0 ? imageBlocks : null;
}
export const RenderImage = (props: { imageBlock: ImageBlock, isFirst: boolean, allowRunAgain: boolean, onRunAgain?: (e: React.MouseEvent) => void }) => {
const { url, alt } = props.imageBlock;
const imageUrls = url.split('\n');
return imageUrls.map((url, index) => {
// display a notice for temporary images DallE
const isTempDalleUrl = url.startsWith('https://oaidalle');
return <Box
key={'gen-img-' + index}
sx={{
display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', position: 'relative',
mx: 1.5, mt: index > 0 ? 1.5 : 0,
mx: 1.5, mb: 1.5, // mt: (index > 0 || !props.isFirst) ? 1.5 : 0,
// p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1,
minWidth: 64, minHeight: 64, boxShadow: 'lg',
minWidth: 128, minHeight: 128,
boxShadow: 'md',
backgroundColor: 'neutral.solidBg',
'& picture': { display: 'flex' },
'& img': { maxWidth: '100%', maxHeight: '100%' },
'&:hover > .overlay-buttons': { opacity: 1 },
}}>
}}
>
{/* External Image */}
<picture><img src={url} alt='Generated Image' /></picture>
{alt ? (
<Tooltip
variant='outlined' color='neutral'
title={
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
{isTempDalleUrl && <Alert variant='soft' color='warning' sx={{ flexDirection: 'column', alignItems: 'start' }}>
<Typography level='title-sm'> Temporary Image</Typography>
<Typography level='body-sm'>
This image will be deleted from the OpenAI servers in one hour. <b>Please save it to your device</b>.
</Typography>
{/*<Typography level='body-xs'>*/}
{/* The following is the re-written DALL·E prompt that generated this image.*/}
{/*</Typography>*/}
</Alert>}
<Typography level='title-sm' sx={{ p: 2 }}>
{alt}
</Typography>
</Box>
}
placement='top-start'
sx={{
maxWidth: { sm: '90vw', md: '70vw' },
boxShadow: 'md',
}}
>
<picture><img src={url} alt={`Generated Image: ${alt}`} /></picture>
</Tooltip>
) : (
<picture><img src={url} alt='Generated Image' /></picture>
)}
{/* Image Buttons */}
<Box className='overlay-buttons' sx={{ ...overlayButtonsSx, pt: 0.5, px: 0.5, gap: 0.5 }}>
{props.allowRunAgain && !!props.onRunAgain && (
<Tooltip title='Draw again' variant='solid'>
<IconButton variant='solid' color='neutral' onClick={props.onRunAgain}>
<IconButton variant='solid' onClick={props.onRunAgain}>
<ReplayIcon />
</IconButton>
</Tooltip>
)}
<IconButton component={Link} href={url} target='_blank' variant='solid' color='neutral'>
<ZoomOutMapIcon />
</IconButton>
<Tooltip title='Open in new tab'>
<IconButton component={Link} href={url} download={alt || 'image'} target='_blank' variant='solid'>
<OpenInNewIcon />
</IconButton>
</Tooltip>
</Box>
</Box>
));
</Box>;
});
};
@@ -17,7 +17,6 @@ const RenderLatexDynamic = React.lazy(async () => {
export const RenderLatex = ({ latexBlock, sx }: { latexBlock: LatexBlock; sx?: SxProps; }) =>
<Box
sx={{
lineHeight: 1.75,
mx: 1.5,
...(sx || {}),
}}>
@@ -1,15 +1,16 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, useTheme } from '@mui/joy';
import { TextBlock } from './blocks';
import type { TextBlock } from './blocks';
// Dynamically import ReactMarkdown using React.lazy
const ReactMarkdown = React.lazy(async () => {
const [markdownModule, remarkGfmModule] = await Promise.all([
import('react-markdown'),
import('remark-gfm')
import('remark-gfm'),
]);
// Pass the dynamically imported remarkGfm as children
@@ -21,22 +22,26 @@ const ReactMarkdown = React.lazy(async () => {
});
export const RenderMarkdown = ({ textBlock }: { textBlock: TextBlock }) => {
export const RenderMarkdown = (props: { textBlock: TextBlock, sx?: SxProps }) => {
const theme = useTheme();
return (
<Box
className={`markdown-body ${theme.palette.mode === 'dark' ? 'markdown-body-dark' : 'markdown-body-light'}`}
sx={{
mx: '12px !important', // margin: 1.5 like other blocks
'& table': { width: 'inherit !important' }, // un-break auto-width (tables have 'max-content', which overflows)
mx: '0.75rem !important', // margin: 1.5 like other blocks
'& table': {
width: 'inherit !important', // un-break auto-width (tables have 'max-content', which overflows)
},
'--color-canvas-default': 'transparent !important', // remove the default background color
fontFamily: `inherit !important`, // use the default font family
lineHeight: '1.75 !important', // line-height: 1.75 like the text block
// NOTE: the following are not needed because the CSS is under our control, and we
// disabled the redefintions there
// fontFamily: `inherit !important`, // use the default font family
...(props.sx || {}),
}}>
{/* Using React.Suspense / React.Lazy loading this */}
<React.Suspense fallback={<div>Loading...</div>}>
<ReactMarkdown>{textBlock.content}</ReactMarkdown>
<ReactMarkdown>{props.textBlock.content}</ReactMarkdown>
</React.Suspense>
</Box>
);
+18 -10
View File
@@ -3,28 +3,36 @@ import * as React from 'react';
import { Chip, Typography } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import { extractCommands } from '../../editors/commands';
import { extractChatCommand } from '../../commands/commands.registry';
import { TextBlock } from './blocks';
import type { TextBlock } from './blocks';
export const RenderText = ({ textBlock, sx }: { textBlock: TextBlock; sx?: SxProps; }) => {
const elements = extractCommands(textBlock.content);
export const RenderText = (props: { textBlock: TextBlock; sx?: SxProps; }) => {
const elements = extractChatCommand(props.textBlock.content);
return (
<Typography
sx={{
lineHeight: 1.75,
mx: 1.5,
display: 'flex', alignItems: 'baseline',
// display: 'flex', // Commented on 2023-12-29: the commands were drawn as columns
alignItems: 'baseline',
overflowWrap: 'anywhere',
whiteSpace: 'break-spaces',
...(sx || {}),
...(props.sx || {}),
}}
>
{elements.map((element, index) =>
element.type === 'cmd'
? <Chip key={index} component='span' size='md' variant='solid' color='neutral' sx={{ mr: 1 }}>{element.value}</Chip>
: <span key={index}>{element.value}</span>,
<React.Fragment key={index}>
{element.type === 'cmd'
? <>
<Chip component='span' size='md' variant='solid' color={element.isError ? 'danger' : 'neutral'} sx={{ mr: 1 }}>
{element.command}
</Chip>
<span>{element.params}</span>
</>
: <span>{element.value}</span>
}
</React.Fragment>,
)}
</Typography>
);
@@ -38,14 +38,13 @@ export const RenderTextDiff = ({ diffBlock, sx }: { diffBlock: DiffBlock; sx?: S
return (
<Typography
sx={{
lineHeight: 1.75,
mx: 1.5,
// display: 'flex', alignItems: 'baseline',
overflowWrap: 'anywhere',
whiteSpace: 'break-spaces',
...(sx || {}),
display: 'block',
zIndex: 200,
...(sx || {}),
}}
>
{textDiffs.map(([op, text], index) =>
+12 -7
View File
@@ -1,12 +1,13 @@
import type { Diff as TextDiff } from '@sanity/diff-match-patch';
import { heuristicIsHtml } from './RenderHtml';
import { heuristicMarkdownImageReferenceBlocks, heuristicLegacyImageBlocks } from './RenderImage';
type Block = CodeBlock | DiffBlock | HtmlBlock | ImageBlock | LatexBlock | TextBlock;
export type CodeBlock = { type: 'code'; blockTitle: string; blockCode: string; complete: boolean; };
export type DiffBlock = { type: 'diff'; textDiffs: TextDiff[] };
export type HtmlBlock = { type: 'html'; html: string; };
export type ImageBlock = { type: 'image'; url: string; };
export type ImageBlock = { type: 'image'; url: string; alt?: string }; // Added optional alt property
export type LatexBlock = { type: 'latex'; latex: string; };
export type TextBlock = { type: 'text'; content: string; }; // for Text or Markdown
@@ -21,9 +22,18 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
if (heuristicIsHtml(text))
return [{ type: 'html', html: text }];
// special case: markdown image references (e.g. ![alt text](https://example.com/image.png))
const mdImageBlocks = heuristicMarkdownImageReferenceBlocks(text);
if (mdImageBlocks)
return mdImageBlocks;
// special case: legacy prodia images
const legacyImageBlocks = heuristicLegacyImageBlocks(text);
if (legacyImageBlocks)
return legacyImageBlocks;
const regexPatterns = {
codeBlock: /`{3,}([\w\\.+-_]+)?\n([\s\S]*?)(`{3,}\n?|$)/g,
imageBlock: /(https:\/\/images\.prodia\.xyz\/.*?\.png)/g, // NOTE: only Prodia for now - but this shall be expanded to markdown images ![alt](url) or any png/jpeg
latexBlock: /\$\$([\s\S]*?)\$\$/g,
// latexBlockOrInline: /\$\$([\s\S]*?)\$\$|\$([^$]*?)\$/g,
};
@@ -61,11 +71,6 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
blocks.push({ type: 'code', blockTitle, blockCode, complete: blockEnd.startsWith('```') });
break;
case 'imageBlock':
const url: string = match[1];
blocks.push({ type: 'image', url });
break;
case 'latexBlock':
const latex: string = match[1];
blocks.push({ type: 'latex', latex });
@@ -33,9 +33,9 @@ interface AppChatPanesStore {
openConversationInFocusedPane: (conversationId: DConversationId) => void;
openConversationInSplitPane: (conversationId: DConversationId) => void;
navigateHistoryInFocusedPane: (direction: 'back' | 'forward') => boolean;
setFocusedPaneIndex: (paneIndex: number) => void;
splitChatPane: (numberOfPanes: number) => void;
unsplitChatPane: (paneIndexToKeep: number) => void;
duplicatePane: (paneIndex: number) => void;
removePane: (paneIndex: number) => void;
setFocusedPane: (paneIndex: number) => void;
onConversationsChanged: (conversationIds: DConversationId[]) => void;
}
@@ -160,7 +160,52 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
return true;
},
setFocusedPaneIndex: (paneIndex: number) =>
duplicatePane: (paneIndex: number) =>
_set(state => {
const { chatPanes } = state;
// Validate index
if (paneIndex < 0 || paneIndex >= chatPanes.length) {
console.warn('Attempted to duplicate a pane with an out-of-range index:', paneIndex);
return state; // Return the existing state without changes
}
// Clone the pane at the specified index, including a deep copy of the history array
const paneToDuplicate = chatPanes[paneIndex];
const duplicatedPane = {
...paneToDuplicate,
history: [...paneToDuplicate.history], // Deep copy of the history array
};
// Insert the duplicated pane into the array, right after the original pane
const newPanes = [
...chatPanes.slice(0, paneIndex + 1),
duplicatedPane,
...chatPanes.slice(paneIndex + 1),
];
return {
chatPanes: newPanes,
chatPaneFocusIndex: paneIndex + 1,
};
}),
removePane: (paneIndex: number) =>
_set(state => {
const { chatPanes } = state;
if (paneIndex < 0 || paneIndex >= chatPanes.length)
return state;
const newPanes = chatPanes.toSpliced(paneIndex, 1);
// when a pane is removed, focus the pane 0, or null if no panes remain
return {
chatPanes: newPanes,
chatPaneFocusIndex: newPanes.length ? 0 : null,
};
}),
setFocusedPane: (paneIndex: number) =>
_set(state => {
if (state.chatPaneFocusIndex === paneIndex)
return state;
@@ -169,22 +214,6 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
};
}),
splitChatPane: (numberOfPanes: number) => {
const { chatPanes, chatPaneFocusIndex } = _get();
const focusedPane = (chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex] : null) ?? createPane();
_set({
chatPanes: Array.from({ length: numberOfPanes }, () => ({ ...focusedPane })),
chatPaneFocusIndex: 0,
});
},
unsplitChatPane: (paneIndexToKeep: number) =>
_set(state => ({
chatPanes: [state.chatPanes[paneIndexToKeep] || createPane()],
chatPaneFocusIndex: 0,
})),
/**
* This function is vital, as is invoked when the conversationId[] changes in the global chats store.
@@ -258,7 +287,9 @@ export function usePanesManager() {
onConversationsChanged,
openConversationInFocusedPane,
openConversationInSplitPane,
setFocusedPaneIndex,
duplicatePane,
removePane,
setFocusedPane,
} = state;
const focusedConversationId = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex]?.conversationId ?? null : null;
return {
@@ -268,7 +299,10 @@ export function usePanesManager() {
onConversationsChanged,
openConversationInFocusedPane,
openConversationInSplitPane,
setFocusedPaneIndex,
paneIndex: chatPaneFocusIndex,
duplicatePane,
removePane,
setFocusedPane,
};
}, shallow);
@@ -3,19 +3,21 @@ import { shallow } from 'zustand/shallow';
import { Box, Button, Checkbox, Grid, IconButton, Input, Stack, Textarea, Typography } from '@mui/joy';
import ClearIcon from '@mui/icons-material/Clear';
import ScienceIcon from '@mui/icons-material/Science';
import SearchIcon from '@mui/icons-material/Search';
import TelegramIcon from '@mui/icons-material/Telegram';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { Link } from '~/common/components/Link';
import { lineHeightTextarea } from '~/common/app.theme';
import { navigateToPersonas } from '~/common/app.routes';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { useUXLabsStore } from '~/common/state/store-ux-labs';
import { SystemPurposeId, SystemPurposes } from '../../../../data';
import { usePurposeStore } from './store-purposes';
// 'special' purpose IDs, for tile hiding purposes
const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__';
// Constants for tile sizes / grid width - breakpoints need to be computed here to work around
// the "flex box cannot shrink over wrapped content" issue
//
@@ -47,7 +49,6 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
// external state
const showFinder = useUIPreferencesStore(state => state.showPurposeFinder);
const labsPersonaYTCreator = useUXLabsStore(state => state.labsPersonaYTCreator);
const { systemPurposeId, setSystemPurposeId } = useChatStore(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
return {
@@ -113,6 +114,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
const unfilteredPurposeIDs = (filteredIDs && showFinder) ? filteredIDs : Object.keys(SystemPurposes);
const purposeIDs = editMode ? unfilteredPurposeIDs : unfilteredPurposeIDs.filter(id => !hiddenPurposeIDs.includes(id));
const hidePersonaCreator = hiddenPurposeIDs.includes(PURPOSE_ID_PERSONA_CREATOR);
const selectedPurpose = purposeIDs.length ? (SystemPurposes[systemPurposeId] ?? null) : null;
const selectedExample = selectedPurpose?.examples && getRandomElement(selectedPurpose.examples) || null;
@@ -127,7 +130,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
placeholder='Search for purpose…'
startDecorator={<SearchIcon />}
endDecorator={searchQuery && (
<IconButton variant='plain' color='neutral' onClick={handleSearchClear}>
<IconButton onClick={handleSearchClear}>
<ClearIcon />
</IconButton>
)}
@@ -156,10 +159,14 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
<Button
variant={(!editMode && systemPurposeId === spId) ? 'solid' : 'soft'}
color={(!editMode && systemPurposeId === spId) ? 'primary' : SystemPurposes[spId as SystemPurposeId]?.highlighted ? 'warning' : 'neutral'}
onClick={() => !editMode && handlePurposeChanged(spId as SystemPurposeId)}
onClick={() => editMode
? toggleHiddenPurposeId(spId)
: handlePurposeChanged(spId as SystemPurposeId)
}
sx={{
flexDirection: 'column',
fontWeight: 500,
// paddingInline: 1,
gap: bpTileGap,
height: bpTileSize,
width: bpTileSize,
@@ -171,9 +178,10 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
>
{editMode && (
<Checkbox
label={<Typography level='body-sm'>show</Typography>}
checked={!hiddenPurposeIDs.includes(spId)} onChange={() => toggleHiddenPurposeId(spId)}
sx={{ alignSelf: 'flex-start' }}
color='neutral'
checked={!hiddenPurposeIDs.includes(spId)}
// label={<Typography level='body-xs'>show</Typography>}
sx={{ position: 'absolute', left: 8, top: 8 }}
/>
)}
<div style={{ fontSize: '2rem' }}>
@@ -185,28 +193,43 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
</Button>
</Grid>
))}
{/* Button to start the YouTube persona creator */}
{labsPersonaYTCreator && <Grid>
{/* Button to start the Persona Creator */}
{(editMode || !hidePersonaCreator) && <Grid>
<Button
variant='soft' color='neutral'
component={Link} noLinkStyle href='/personas'
onClick={() => editMode
? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR)
: void navigateToPersonas()
}
sx={{
'--Icon-fontSize': '2rem',
flexDirection: 'column',
fontWeight: 500,
// gap: bpTileGap,
// paddingInline: 1,
gap: bpTileGap,
height: bpTileSize,
width: bpTileSize,
border: `1px dashed`,
boxShadow: 'md',
backgroundColor: 'background.surface',
// border: `1px dashed`,
// borderColor: 'neutral.softActiveBg',
boxShadow: 'xs',
backgroundColor: 'neutral.softDisabledBg',
}}
>
{editMode && (
<Checkbox
color='neutral'
checked={!hidePersonaCreator}
// label={<Typography level='body-xs'>show</Typography>}
sx={{ position: 'absolute', left: 8, top: 8 }}
/>
)}
<div>
<ScienceIcon />
<div style={{ fontSize: '2rem' }}>
🎭
</div>
{/*<SettingsAccessibilityIcon style={{ opacity: 0.5 }} />*/}
</div>
<div>
YouTube persona creator
<div style={{ textAlign: 'center' }}>
Persona Creator
</div>
</Button>
</Grid>}
@@ -225,7 +248,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
? <>
Example: {selectedExample}
<IconButton
variant='plain' color='primary' size='md'
color='primary'
onClick={() => props.runExample(selectedExample)}
sx={{ opacity: 0, transition: 'opacity 0.3s' }}
>
@@ -246,7 +269,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
'&:focus-within': {
backgroundColor: 'background.popup',
},
lineHeight: 1.75,
lineHeight: lineHeightTextarea,
mt: 1,
}} />
)}
@@ -0,0 +1,244 @@
/**
* Copyright (c) 2023-2024 Enrico Ros
*
* This subsystem is responsible for 'snap-to-bottom' and 'scroll-to-bottom' features,
* with an animated, gradual scroll.
*
* See the `ScrollToBottomButton` component for the button that triggers the scroll.
*
* Example usage:
* <ScrollToBottom bootToBottom stickToBottom sx={{ overflowY: 'auto', height: '100%' }}>
* <LongMessagesList />
* <ScrollToBottomButton />
* </ScrollToBottom>
*
* Within the Context (children components), functions are made available by using:
* const { notifyBooting, setStickToBottom } = useScrollToBottom();
*
*/
import * as React from 'react';
import { Box } from '@mui/joy';
import type { SxProps } from '@mui/joy/styles/types';
import { isBrowser } from '~/common/util/pwaUtils';
import { ScrollToBottomState, UseScrollToBottomProvider } from './useScrollToBottom';
// set this to true to debug this component
const DEBUG_SCROLL_TO_BOTTOM = false;
// NOTE: in Chrome a wheel scroll event is 100px
// If you make this too small, the button may show when jumping lines on mobile
// if you make it too large, the user would need a very large flick to unlock the view
const USER_STICKY_MARGIN = 60;
// during the 'booting' timeout, scrolls happen instantly instead of smoothly
const BOOTING_TIMEOUT = 400;
function DebugBorderBox(props: { heightPx: number, color: string }) {
return (
<Box sx={{
position: 'absolute', bottom: 0, right: 0, left: 0,
height: `${props.heightPx}px`,
border: `1px solid ${props.color}`,
pointerEvents: 'none',
}} />
);
}
export function ScrollToBottom(props: {
bootToBottom?: boolean
bootSmoothly?: boolean
stickToBottom?: boolean
sx?: SxProps
children: React.ReactNode,
}) {
// state
const [state, setState] = React.useState<ScrollToBottomState>({
stickToBottom: props.stickToBottom || false,
booting: props.bootToBottom || false,
atBottom: undefined,
});
// track scrollable (for events and to scroll it)
const scrollableElementRef = React.useRef<HTMLDivElement>(null);
// track programmatic scrolls
const isProgrammaticScroll = React.useRef(false);
// derived state
const bootToBottom = props.bootToBottom || false;
const scrollBehavior: ScrollBehavior = (state.booting && !props.bootSmoothly) ? 'auto' : 'smooth';
// [Debugging]
if (DEBUG_SCROLL_TO_BOTTOM)
console.log('ScrollToBottom', { ...state });
// main programmatic scroll to bottom function
const doScrollToBottom = React.useCallback(() => {
const scrollable = scrollableElementRef.current;
if (scrollable) {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log(' -> doScrollToBottom()', { scrollHeight: scrollable.scrollHeight, offsetHeight: scrollable.offsetHeight });
// eat the next scroll event
isProgrammaticScroll.current = true;
// smooth scrolling only after booting
scrollable.scrollTo({ top: scrollable.scrollHeight, behavior: scrollBehavior });
}
}, [scrollBehavior]);
/**
* Booting state reset (after BOOTING_TIMEOUT ms)
* - the "Booting" window will scroll instantly instead of smoothly
*/
React.useEffect(() => {
if (!state.booting || !isBrowser) return;
const _clearBootingHandler = () => {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log(' -> booting done');
setState(state => ({ ...state, booting: false }));
if (bootToBottom)
doScrollToBottom();
};
// cancelable listener
const timeout = window.setTimeout(_clearBootingHandler, BOOTING_TIMEOUT);
return () => clearTimeout(timeout);
}, [bootToBottom, doScrollToBottom, state.booting]);
/**
* Children elements resize event listener
* - note that the 'scrollable' will likely have a fixed size, while its children are the ones who become scrollable
*/
React.useEffect(() => {
const scrollable = scrollableElementRef.current;
if (!scrollable) return;
const _containerResizeObserver = new ResizeObserver(entries => {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log(' -> scrollable children resized', entries.length);
// Edge case: when the content is smaller, we need to reset the bottom state (#312)
const atTop = scrollable.scrollTop == 0;
const unScrollable = scrollable.scrollHeight <= scrollable.offsetHeight;
if (unScrollable && atTop) {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log(' -> large enough window', entries.length);
// udpate state only if this changed
setState(state => (state.atBottom !== true)
? ({ ...state, atBottom: true })
: state,
);
}
if (entries.length > 0 && state.stickToBottom)
doScrollToBottom();
});
// cancelable observer of resize of scrollable's children elements
Array.from(scrollable.children).forEach(child => _containerResizeObserver.observe(child));
return () => _containerResizeObserver.disconnect();
}, [state.stickToBottom, doScrollToBottom]);
/**
* (User) Scroll events listener
* - will cancel any state.stickToBottom, if the user dragged the scroll bar
*/
React.useEffect(() => {
if (state.booting) return;
const scrollable = scrollableElementRef.current;
if (!scrollable) return;
const _scrollEventsListener = () => {
// ignore scroll events during programmatic scrolls
// NOTE: some will go through, but somewhat the framework is stable
if (isProgrammaticScroll.current) {
isProgrammaticScroll.current = false;
return;
}
// compute intersections
const atBottom = scrollable.scrollHeight - scrollable.scrollTop <= scrollable.offsetHeight + USER_STICKY_MARGIN;
// assume this is = to the user intention
const stickToBottom = atBottom;
// update state only if anything changed
setState(state => (state.stickToBottom !== stickToBottom || state.atBottom !== atBottom)
? ({ ...state, stickToBottom, atBottom })
: state,
);
};
// _scrollEventsListener(true);
// cancelable listener (user and programatic scroll events)
scrollable.addEventListener('scroll', _scrollEventsListener);
return () => scrollable.removeEventListener('scroll', _scrollEventsListener);
}, [state.booting]);
// actions for this context
const notifyBooting = React.useCallback(() => {
if (bootToBottom)
setState(state => state.booting ? state : ({ ...state, booting: true }));
}, [bootToBottom]);
/*const notifyContentUpdated = React.useCallback(() => {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log('-= notifyContentUpdated');
if (state.stickToBottom)
doScrollToBottom();
}, [doScrollToBottom, state.stickToBottom]);*/
const setStickToBottom = React.useCallback((stickToBottom: boolean) => {
if (DEBUG_SCROLL_TO_BOTTOM)
console.log('-= setStickToBottom', stickToBottom);
setState(state => state.stickToBottom !== stickToBottom
? ({ ...state, stickToBottom })
: state,
);
if (stickToBottom)
doScrollToBottom();
}, [doScrollToBottom]);
return (
<UseScrollToBottomProvider value={{
...state,
notifyBooting,
setStickToBottom,
}}>
<Box ref={scrollableElementRef} sx={props.sx}>
{props.children}
{DEBUG_SCROLL_TO_BOTTOM && <DebugBorderBox heightPx={USER_STICKY_MARGIN} color='red' />}
{DEBUG_SCROLL_TO_BOTTOM && <DebugBorderBox heightPx={100} color='blue' />}
</Box>
</UseScrollToBottomProvider>
);
}
@@ -0,0 +1,56 @@
import * as React from 'react';
import { IconButton } from '@mui/joy';
import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown';
import { useScrollToBottom } from './useScrollToBottom';
export function ScrollToBottomButton() {
// state
const { atBottom, stickToBottom, setStickToBottom } = useScrollToBottom();
const handleStickToBottom = React.useCallback(() => {
setStickToBottom(true);
}, [setStickToBottom]);
// do not render the button at all if we're already snapping
if (atBottom || stickToBottom)
return null;
return (
// <Tooltip title={
// <Typography variant='solid' level='title-sm' sx={{ px: 1 }}>
// Scroll to bottom
// </Typography>
// }>
<IconButton
variant='outlined'
onClick={handleStickToBottom}
sx={{
// place this on the bottom-right corner (FAB-like)
position: 'absolute',
bottom: '2rem',
right: {
xs: '1rem',
md: '2rem',
},
// style it
backgroundColor: 'background.surface',
borderRadius: '50%',
boxShadow: 'md',
// fade it in when hovering
// transition: 'all 0.15s',
// '&:hover': {
// transform: 'scale(1.1)',
// },
}}
>
<KeyboardDoubleArrowDownIcon />
</IconButton>
// </Tooltip>
);
}
@@ -0,0 +1,34 @@
import * as React from 'react';
/**
* State is minimal - to keep state machinery stable and simple
*/
export interface ScrollToBottomState {
// config
stickToBottom: boolean;
// state
booting: boolean;
atBottom: boolean | undefined;
}
/**
* Actions are very simplified, for providing a minimal control surface from the outside
*/
export interface ScrollToBottomActions {
notifyBooting: () => void;
setStickToBottom: (stick: boolean) => void;
}
type ScrollToBottomContext = ScrollToBottomState & ScrollToBottomActions;
const UseScrollToBottom = React.createContext<ScrollToBottomContext | undefined>(undefined);
export const UseScrollToBottomProvider = UseScrollToBottom.Provider;
export const useScrollToBottom = (): ScrollToBottomContext => {
const context = React.useContext(UseScrollToBottom);
if (!context)
throw new Error('useScrollToBottom must be used within a ScrollToBottomProvider');
return context;
};
-57
View File
@@ -1,57 +0,0 @@
import { CmdRunBrowse } from '~/modules/browse/browse.client';
import { CmdRunProdia } from '~/modules/prodia/prodia.client';
import { CmdRunReact } from '~/modules/aifn/react/react';
import { CmdRunSearch } from '~/modules/google/search.client';
import { Brand } from '~/common/app.config';
import { createDMessage, DMessage } from '~/common/state/store-chats';
export const CmdAddRoleMessage: string[] = ['/assistant', '/a', '/system', '/s'];
export const CmdHelp: string[] = ['/help', '/h', '/?'];
export const commands = [...CmdRunBrowse, ...CmdRunProdia, ...CmdRunReact, ...CmdRunSearch, ...CmdAddRoleMessage, ...CmdHelp];
export interface SentencePiece {
type: 'text' | 'cmd';
value: string;
}
/**
* Sentence to pieces (must have a leading slash) from the provided text
* Used by rendering functions, as well as input processing functions.
*/
export function extractCommands(input: string): SentencePiece[] {
// 'help' commands are the only without a space and text after
if (CmdHelp.includes(input))
return [{ type: 'cmd', value: input }, { type: 'text', value: '' }];
const regexFromTags = commands.map(tag => `^\\${tag} `).join('\\b|') + '\\b';
const pattern = new RegExp(regexFromTags, 'g');
const result: SentencePiece[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = pattern.exec(input)) !== null) {
if (match.index !== lastIndex)
result.push({ type: 'text', value: input.substring(lastIndex, match.index) });
result.push({ type: 'cmd', value: match[0].trim() });
lastIndex = pattern.lastIndex;
// Remove the space after the matched tag
if (input[lastIndex] === ' ')
lastIndex++;
}
if (lastIndex !== input.length)
result.push({ type: 'text', value: input.substring(lastIndex) });
return result;
}
export function createCommandsHelpMessage(): DMessage {
let text = 'Available Chat Commands:\n';
text += commands.map(c => ` - ${c}`).join('\n');
const helpMessage = createDMessage('assistant', text);
helpMessage.originLLM = Brand.Title.Base;
return helpMessage;
}
+1 -1
View File
@@ -4,7 +4,7 @@ import { SystemPurposeId, SystemPurposes } from '../../../data';
import { createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
export function createAssistantTypingMessage(conversationId: string, assistantLlmLabel: DLLMId | 'prodia' | 'react-...' | 'web', assistantPurposeId: SystemPurposeId | undefined, text: string): string {
export function createAssistantTypingMessage(conversationId: string, assistantLlmLabel: DLLMId | string /* 'DALL·E' | 'Prodia' | 'react-...' | 'web' */, assistantPurposeId: SystemPurposeId | undefined, text: string): string {
const assistantMessage: DMessage = createDMessage('assistant', text);
assistantMessage.typing = true;
assistantMessage.purposeId = assistantPurposeId;
+10 -8
View File
@@ -1,4 +1,4 @@
import { prodiaGenerateImage } from '~/modules/prodia/prodia.client';
import { getActiveTextToImageProviderOrThrow, t2iGenerateImageOrThrow } from '~/modules/t2i/t2i.client';
import { useChatStore } from '~/common/state/store-chats';
@@ -6,7 +6,7 @@ import { createAssistantTypingMessage } from './editors';
/**
* The main 'image generation' function - for now specialized to the 'imagine' command.
* Text to image, appended as an 'assistant' message
*/
export async function runImageGenerationUpdatingState(conversationId: string, imageText: string) {
@@ -17,21 +17,23 @@ export async function runImageGenerationUpdatingState(conversationId: string, im
imageText = imageText.replace(/x(\d+)$|\[(\d+)]$/, '').trim(); // Remove the "xN" or "[N]" part from the imageText
// create a blank and 'typing' message for the assistant
const assistantMessageId = createAssistantTypingMessage(conversationId, 'prodia', undefined,
const assistantMessageId = createAssistantTypingMessage(conversationId, '', undefined,
`Give me a few seconds while I draw ${imageText?.length > 20 ? 'that' : '"' + imageText + '"'}...`);
// reference the state editing functions
const { editMessage } = useChatStore.getState();
try {
const imageUrls = await prodiaGenerateImage(count, imageText);
// Concatenate all the resulting URLs and update the assistant message with these URLs
const allImageUrls = imageUrls.join('\n');
editMessage(conversationId, assistantMessageId, { text: allImageUrls, typing: false }, false);
const t2iProvider = getActiveTextToImageProviderOrThrow();
editMessage(conversationId, assistantMessageId, { originLLM: t2iProvider.painter }, false);
const imageUrls = await t2iGenerateImageOrThrow(t2iProvider, imageText, count);
editMessage(conversationId, assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
} catch (error: any) {
const errorMessage = error?.message || error?.toString() || 'Unknown error';
editMessage(conversationId, assistantMessageId, { text: `Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
if (assistantMessageId)
editMessage(conversationId, assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
}
}
+1 -1
View File
@@ -40,7 +40,7 @@ export async function runReActUpdatingState(conversationId: string, question: st
const agent = new Agent();
const reactResult = await agent.reAct(question, assistantLlmId, 5, enableBrowse, logToEphemeral, showStateInEphemeral);
setTimeout(() => deleteEphemeral(conversationId, ephemeral.id), 2 * 1000);
setTimeout(() => deleteEphemeral(conversationId, ephemeral.id), 4 * 1000);
updateAssistantMessage({ text: reactResult, typing: false });
} catch (error: any) {
+9 -9
View File
@@ -5,7 +5,6 @@ import { useQuery } from '@tanstack/react-query';
import { Box, Typography } from '@mui/joy';
import { createConversationFromJsonV1 } from '~/modules/trade/trade.client';
import { useHasChatLinkItems } from '~/modules/trade/store-module-trade';
import { Brand } from '~/common/app.config';
import { InlineError } from '~/common/components/InlineError';
@@ -13,9 +12,10 @@ import { LogoProgress } from '~/common/components/LogoProgress';
import { apiAsyncNode } from '~/common/util/trpc.client';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { conversationTitle } from '~/common/state/store-chats';
import { useLayoutPluggable } from '~/common/layout/store-applayout';
import { themeBgAppDarker } from '~/common/app.theme';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { AppChatLinkDrawerItems } from './AppChatLinkDrawerItems';
import { AppChatLinkDrawerContent } from './AppChatLinkDrawerContent';
import { AppChatLinkMenuItems } from './AppChatLinkMenuItems';
import { ViewChatLink } from './ViewChatLink';
@@ -30,7 +30,7 @@ const Centerer = (props: { backgroundColor: string, children?: React.ReactNode }
</Box>;
const ShowLoading = () =>
<Centerer backgroundColor='background.level3'>
<Centerer backgroundColor={themeBgAppDarker}>
<LogoProgress showProgress={true} />
<Typography level='title-sm' sx={{ mt: 2 }}>
Loading Chat...
@@ -38,7 +38,7 @@ const ShowLoading = () =>
</Centerer>;
const ShowError = (props: { error: any }) =>
<Centerer backgroundColor='background.level2'>
<Centerer backgroundColor={themeBgAppDarker}>
<InlineError error={props.error} severity='warning' />
</Centerer>;
@@ -78,14 +78,14 @@ export function AppChatLink(props: { linkId: string }) {
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 60 * 24, // 24 hours
});
const hasLinkItems = useHasChatLinkItems();
// const hasLinkItems = useHasChatLinkItems();
// pluggable UI
const drawerItems = React.useMemo(() => <AppChatLinkDrawerItems />, []);
const drawerContent = React.useMemo(() => <AppChatLinkDrawerContent />, []);
const menuItems = React.useMemo(() => <AppChatLinkMenuItems />, []);
useLayoutPluggable(null, hasLinkItems ? drawerItems : null, menuItems);
usePluggableOptimaLayout(drawerContent, null, menuItems, 'AppChatLink');
const pageTitle = (data?.conversation && conversationTitle(data.conversation)) || 'Chat Link';
@@ -102,7 +102,7 @@ export function AppChatLink(props: { linkId: string }) {
? <ShowError error={error} />
: !!data?.conversation
? <ViewChatLink conversation={data.conversation} storedAt={data.storedAt} expiresAt={data.expiresAt} />
: <Centerer backgroundColor='background.level3' />}
: <Centerer backgroundColor={themeBgAppDarker} />}
</>;
}
@@ -1,51 +1,55 @@
import * as React from 'react';
import TimeAgo from 'react-timeago';
import { Box, ListDivider, ListItem, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
import { Box, ListDivider, ListItem, ListItemButton, ListItemDecorator, Typography } from '@mui/joy';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import { useChatLinkItems } from '~/modules/trade/store-module-trade';
import { Brand } from '~/common/app.config';
import { Link } from '~/common/components/Link';
import { closeLayoutDrawer } from '~/common/layout/store-applayout';
import { getChatLinkRelativePath, ROUTE_INDEX } from '~/common/app.routes';
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
import { PageDrawerList } from '~/common/layout/optima/components/PageDrawerList';
/**
* Drawer Items are all the links already shared, for quick access.
* This is stores in the Trade Store (local storage).
*/
export function AppChatLinkDrawerItems() {
export function AppChatLinkDrawerContent() {
// external state
const { closeDrawerOnMobile } = useOptimaDrawers();
const chatLinkItems = useChatLinkItems()
.slice()
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
const notEmpty = chatLinkItems.length > 0;
return <>
return <PageDrawerList>
<MenuItem
onClick={closeLayoutDrawer}
component={Link} href={ROUTE_INDEX} noLinkStyle
>
<ListItemDecorator><ArrowBackIcon /></ListItemDecorator>
{Brand.Title.Base}
</MenuItem>
{notEmpty && (
<ListItemButton
onClick={closeDrawerOnMobile}
component={Link} href={ROUTE_INDEX} noLinkStyle
>
<ListItemDecorator><ArrowBackIcon /></ListItemDecorator>
{Brand.Title.Base}
</ListItemButton>
)}
{notEmpty && <ListDivider />}
{notEmpty && <ListItem>
<ListItem>
<Typography level='body-sm'>
Links shared by you
{notEmpty ? 'Links shared by you' : 'No prior shared links'}
</Typography>
</ListItem>}
</ListItem>
{notEmpty && <Box sx={{ overflowY: 'auto' }}>
{chatLinkItems.map(item => (
<MenuItem
<ListItemButton
key={'chat-link-' + item.objectId}
component={Link} href={getChatLinkRelativePath(item.objectId)} noLinkStyle
sx={{
@@ -59,10 +63,10 @@ export function AppChatLinkDrawerItems() {
<Typography level='body-xs'>
<TimeAgo date={item.createdAt} />
</Typography>
</MenuItem>
</ListItemButton>
))}
</Box>}
</>;
</PageDrawerList>;
}
+50 -35
View File
@@ -5,11 +5,13 @@ import { Box, Button, Card, List, ListItem, Tooltip, Typography } from '@mui/joy
import TelegramIcon from '@mui/icons-material/Telegram';
import { ChatMessage } from '../chat/components/message/ChatMessage';
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
import { useChatShowSystemMessages } from '../chat/store-app-chat';
import { Brand } from '~/common/app.config';
import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats';
import { navigateToChat } from '~/common/app.routes';
import { launchAppChat } from '~/common/app.routes';
import { themeBgAppDarker } from '~/common/app.theme';
import { useUIPreferencesStore } from '~/common/state/store-ui';
@@ -58,7 +60,7 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
const handleClone = async (canOverwrite: boolean) => {
setCloning(true);
const importedId = useChatStore.getState().importConversation({ ...props.conversation }, !canOverwrite);
await navigateToChat(importedId);
await launchAppChat(importedId);
setCloning(false);
};
@@ -67,7 +69,7 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
<Box sx={{
flexGrow: 1,
backgroundColor: 'background.level3',
backgroundColor: themeBgAppDarker,
display: 'flex', flexFlow: 'column nowrap', minHeight: 96, alignItems: 'center',
gap: { xs: 4, md: 5, xl: 6 },
px: { xs: 2 },
@@ -96,46 +98,59 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
<Card sx={{
borderRadius: 'xl', boxShadow: 'md',
maxWidth: '100%', // fixes the card growing out of bounds
overflowY: 'auto',
overflowY: 'hidden',
p: 0,
}}>
<List sx={{
p: 0,
display: 'flex', flexDirection: 'column',
flexGrow: 1,
}}>
<ScrollToBottom
bootToBottom bootSmoothly
sx={{
// allows the content to be scrolled (all browsers)
overflowY: 'auto',
// actually make sure this scrolls & fills
height: '100%',
}}
>
<ListItem sx={{
// backgroundColor: 'background.surface',
borderBottom: '1px solid',
borderBottomColor: 'divider',
borderBottomStyle: 'dashed',
// justifyContent: 'center',
px: { xs: 2.5, md: 3.5 }, py: 2,
<List sx={{
p: 0,
display: 'flex', flexDirection: 'column',
flexGrow: 1,
}}>
<Typography level='body-md'>
Welcome to the chat! 👋
</Typography>
</ListItem>
{filteredMessages.map((message, idx) =>
<ChatMessage
key={'msg-' + message.id} message={message}
showDate={idx === 0 || idx === filteredMessages.length - 1}
onMessageEdit={text => message.text = text}
/>,
)}
<ListItem sx={{
// backgroundColor: 'background.surface',
borderBottom: '1px solid',
borderBottomColor: 'divider',
borderBottomStyle: 'dashed',
// justifyContent: 'center',
px: { xs: 2.5, md: 3.5 }, py: 2,
}}>
<Typography level='body-md'>
Welcome to the chat! 👋
</Typography>
</ListItem>
<ListItem sx={{
px: { xs: 2.5, md: 3.5 }, py: 2,
}}>
<Typography level='body-sm' ref={listBottomRef}>
Like the chat? Clone it and keep the talk going! 🚀
</Typography>
</ListItem>
{filteredMessages.map((message, idx) =>
<ChatMessage
key={'msg-' + message.id} message={message}
showDate={idx === 0 || idx === filteredMessages.length - 1}
onMessageEdit={text => message.text = text}
/>,
)}
<ListItem sx={{
px: { xs: 2.5, md: 3.5 }, py: 2,
}}>
<Typography level='body-sm' ref={listBottomRef}>
Like the chat? Clone it and keep the talk going! 🚀
</Typography>
</ListItem>
</List>
</ScrollToBottom>
</List>
</Card>
{/* Buttons */}
+26 -13
View File
@@ -10,11 +10,12 @@ import { GoodTooltip } from '~/common/components/GoodTooltip';
import { Link } from '~/common/components/Link';
import { ROUTE_INDEX } from '~/common/app.routes';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
import { cssRainbowColorKeyframes, themeBgApp } from '~/common/app.theme';
import { newsCallout, NewsItems } from './news.data';
// number of news items to show by default, before the expander
const DEFAULT_NEWS_COUNT = 2;
const DEFAULT_NEWS_COUNT = 3;
export const cssColorKeyframes = keyframes`
0%, 100% {
@@ -27,7 +28,7 @@ export const cssColorKeyframes = keyframes`
color: #0B6BCB; /* Primary main color (500) */
}
75% {
color: #97C3F0; /* Primary lighter shade (300) */
color: #083e75; /* Primary lighter shade (300) */
}`;
@@ -43,7 +44,7 @@ export function AppNews() {
<Box sx={{
flexGrow: 1,
backgroundColor: 'background.level1',
backgroundColor: themeBgApp,
overflowY: 'auto',
display: 'flex', justifyContent: 'center',
p: { xs: 3, md: 6 },
@@ -87,7 +88,13 @@ export function AppNews() {
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 0 }}>
<GoodTooltip title={ni.versionName ? `${ni.versionName} ${ni.versionMoji || ''}` : null} placement='top-start'>
<Typography level='title-sm' component='div' sx={{ flexGrow: 1 }}>
{ni.text ? ni.text : ni.versionName ? `${ni.versionCode} · ${ni.versionName}` : `Version ${ni.versionCode}:`}
{ni.text ? ni.text : ni.versionName ? `${ni.versionCode} · ` : `Version ${ni.versionCode}:`}
<Box component='span' sx={!idx ? {
animation: `${cssRainbowColorKeyframes} 5s infinite`,
fontWeight: 600,
} : {}}>
{ni.versionName}
</Box>
</Typography>
</GoodTooltip>
{/*!firstCard &&*/ (
@@ -97,19 +104,25 @@ export function AppNews() {
)}
</Box>
{!!ni.items && (ni.items.length > 0) && <ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: 24 }}>
{ni.items.filter(item => item.dev !== true).map((item, idx) => <li key={idx}>
<Typography component='div' level='body-sm'>
{item.text}
</Typography>
</li>)}
</ul>}
{!!ni.items && (ni.items.length > 0) && (
<ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: '1.5rem' }}>
{ni.items.filter(item => item.dev !== true).map((item, idx) => <li key={idx}>
< Typography component='div' level='body-sm'>
{item.text}
</Typography>
</li>)}
</ul>
)}
{showExpander && (
<IconButton
variant='plain' size='sm'
variant='outlined'
onClick={() => setLastNewsIdx(idx + 1)}
sx={{ position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1 }}
sx={{
position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1,
backgroundColor: 'background.surface',
borderRadius: '50%',
}}
>
<ExpandMoreIcon />
</IconButton>
+46 -10
View File
@@ -10,7 +10,7 @@ import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
// update this variable every time you want to broadcast a new version to clients
export const incrementalVersion: number = 9;
export const incrementalVersion: number = 12;
const B = (props: { href?: string, children: React.ReactNode }) => {
const boldText = <Typography color={!!props.href ? 'primary' : 'neutral'} sx={{ fontWeight: 600 }}>{props.children}</Typography>;
@@ -30,7 +30,7 @@ export const newsCallout =
<Typography level='title-lg'>
Open Roadmap
</Typography>
<Typography level='body-md'>
<Typography level='body-sm'>
Take a peek at our roadmap to see what&apos;s in the pipeline.
Discover upcoming features and let us know what excites you the most!
</Typography>
@@ -58,19 +58,54 @@ export const newsCallout =
// news and feature surfaces
export const NewsItems: NewsItem[] = [
/*{
// https://github.com/enricoros/big-agi/milestone/7
// https://github.com/users/enricoros/projects/4/views/2
versionName: '1.7.0',
// still unannounced: phone calls, split windows, ...
{
versionCode: '1.11.0',
versionName: 'Singularity',
versionMoji: '🌌🌠',
versionDate: new Date('2024-01-16T06:30:00Z'),
items: [
// multi-window support
// phone calls
{ text: <><B href={RIssues + '/329'}>Search</B> past conversations (@joriskalz) 🔍</>, issue: 329 },
{ text: <>Quick <B href={RIssues + '/327'}>commands pane</B> (open with &apos;/&apos;)</>, issue: 327 },
{ text: <><B>Together AI</B> Inference platform support</>, issue: 346 },
{ text: <>Persona creation: <B href={RIssues + '/301'}>history</B></>, issue: 301 },
{ text: <>Persona creation: fix <B href={RIssues + '/328'}>API timeouts</B></>, issue: 328 },
{ text: <>Support up to five <B href={RIssues + '/323'}>OpenAI-compatible</B> endpoints</>, issue: 323 },
],
},*/
},
{
versionCode: '1.10.0',
versionName: 'The Year of AGI',
// versionMoji: '🎊✨',
versionDate: new Date('2024-01-06T08:00:00Z'),
items: [
{ text: <><B href={RIssues + '/201'}>New UI</B> for desktop and mobile, enabling future expansions</>, issue: 201 },
{ text: <><B href={RIssues + '/321'}>Folder categorization</B> for conversation management</>, issue: 321 },
{ text: <><B>LM Studio</B> support and refined token management</> },
{ text: <>Draggable panes in split screen mode</>, issue: 308 },
{ text: <>Bug fixes and UI polish</> },
{ text: <>Developers: document proxy settings on docker</>, issue: 318, dev: true },
],
},
{
versionCode: '1.9.0',
versionName: 'Creative Horizons',
// versionMoji: '🎨🌌',
versionDate: new Date('2023-12-28T22:30:00Z'),
items: [
{ text: <><B href={RIssues + '/212'}>DALL·E 3</B> support (/draw), with advanced control</>, issue: 212 },
{ text: <><B href={RIssues + '/304'}>Perfect scrolling</B> UX, on all devices</>, issue: 304 },
{ text: <>Create personas <B href={RIssues + '/287'}>from text</B></>, issue: 287 },
{ text: <>Openrouter: auto-detect models, support free-tiers and rates</>, issue: 291 },
{ text: <>Image drawing: unified UX, including auto-prompting</> },
{ text: <>Fix layout on Firefox</>, issue: 255 },
{ text: <>Developers: new Text2Image subsystem, Optima layout subsystem, ScrollToBottom library, using new Panes library, improved Llms subsystem</>, dev: true },
],
},
{
versionCode: '1.8.0',
versionName: 'To The Moon And Back',
versionMoji: '🚀🌕🔙❤️',
// versionMoji: '🚀🌕🔙❤️',
versionDate: new Date('2023-12-20T09:30:00Z'),
items: [
{ text: <><B href={RIssues + '/275'}>Google Gemini</B> models support</> },
@@ -183,5 +218,6 @@ interface NewsItem {
items?: {
text: string | React.JSX.Element;
dev?: boolean;
issue?: number;
}[];
}
+3 -7
View File
@@ -1,5 +1,4 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { navigateToNews } from '~/common/app.routes';
import { useAppStateStore } from '~/common/state/store-appstate';
@@ -7,18 +6,15 @@ import { useAppStateStore } from '~/common/state/store-appstate';
import { incrementalVersion } from './news.data';
export function useShowNewsOnUpdate() {
const { usageCount, lastSeenNewsVersion } = useAppStateStore(state => ({
usageCount: state.usageCount,
lastSeenNewsVersion: state.lastSeenNewsVersion,
}), shallow);
export function useRedirectToNewsOnUpdates() {
React.useEffect(() => {
const { usageCount, lastSeenNewsVersion } = useAppStateStore.getState();
const isNewsOutdated = (lastSeenNewsVersion || 0) < incrementalVersion;
if (isNewsOutdated && usageCount > 2) {
// Disable for now
void navigateToNews();
}
}, [lastSeenNewsVersion, usageCount]);
}, []);
}
export function useMarkNewsAsSeen() {
+31 -13
View File
@@ -1,36 +1,54 @@
import * as React from 'react';
import { Box, Container, ListDivider, Sheet, Typography } from '@mui/joy';
import { Container, ListDivider, Sheet, Typography } from '@mui/joy';
import { YTPersonaCreator } from './YTPersonaCreator';
import ScienceIcon from '@mui/icons-material/Science';
import { themeBgApp } from '~/common/app.theme';
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { Creator } from './creator/Creator';
import { CreatorDrawer } from './creator/CreatorDrawer';
import { Viewer } from './creator/Viewer';
export function AppPersonas() {
// state
const [selectedSimplePersonaId, setSelectedSimplePersonaId] = React.useState<string | null>(null);
// pluggable UI
const drawerContent = React.useMemo(() => {
return (
<CreatorDrawer
selectedSimplePersonaId={selectedSimplePersonaId}
setSelectedSimplePersonaId={setSelectedSimplePersonaId}
/>
);
}, [selectedSimplePersonaId]);
usePluggableOptimaLayout(drawerContent, null, null, 'AppPersonas');
return (
<Sheet sx={{
flexGrow: 1,
overflowY: 'auto',
backgroundColor: 'background.level1',
backgroundColor: themeBgApp,
p: { xs: 3, md: 6 },
}}>
<Container disableGutters maxWidth='md' sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
<Typography level='title-lg' sx={{ textAlign: 'center' }}>
Advanced AI Personas
AI Personas Creator
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
<Typography>
Experimental
</Typography>
<ScienceIcon color='primary' />
</Box>
<ListDivider sx={{ my: 2 }} />
<YTPersonaCreator />
{!!selectedSimplePersonaId && <Viewer selectedSimplePersonaId={selectedSimplePersonaId} />}
<Creator display={!selectedSimplePersonaId} />
</Container>
-242
View File
@@ -1,242 +0,0 @@
import * as React from 'react';
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, IconButton, Input, LinearProgress, Tooltip, Typography } from '@mui/joy';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import WhatshotIcon from '@mui/icons-material/Whatshot';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { GoodModal } from '~/common/components/GoodModal';
import { apiQuery } from '~/common/util/trpc.client';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
import { LLMChainStep, useLLMChain } from './useLLMChain';
function extractVideoID(videoURL: string): string | null {
const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^#&?]*).*/;
const match = videoURL.match(regExp);
return (match && match[1]?.length == 11) ? match[1] : null;
}
function useTranscriptFromVideo(videoID: string | null) {
const { data, isFetching, isError, error } =
apiQuery.ytpersona.getTranscript.useQuery({ videoId: videoID || '' }, {
enabled: !!videoID,
refetchOnWindowFocus: false,
staleTime: Infinity,
});
return {
title: data?.videoTitle ?? null,
thumbnailUrl: data?.thumbnailUrl ?? null,
transcript: data?.transcript?.trim() ?? null,
isFetching,
isError, error,
};
}
const YouTubePersonaSteps: LLMChainStep[] = [
{
name: 'Analyzing the transcript',
setSystem: 'You are skilled in analyzing and embodying diverse characters. You meticulously study transcripts to capture key attributes, draft comprehensive character sheets, and refine them for authenticity. Feel free to make assumptions without hedging, be concise and be creative.',
addUserInput: true,
addUser: 'Conduct comprehensive research on the provided transcript. Identify key characteristics of the speaker, including age, professional field, distinct personality traits, style of communication, narrative context, and self-awareness. Additionally, consider any unique aspects such as their use of humor, their cultural background, core values, passions, fears, personal history, and social interactions. Your output for this stage is an in-depth written analysis that exhibits an understanding of both the superficial and more profound aspects of the speaker\'s persona.',
},
{
name: 'Defining the character',
addPrevAssistant: true,
addUser: 'Craft your documented analysis into a draft of the \'You are a...\' character sheet. It should encapsulate all crucial personality dimensions, along with the motivations and aspirations of the persona. Keep in mind to balance succinctness and depth of detail for each dimension. The deliverable here is a comprehensive draft of the character sheet that captures the speaker\'s unique essence.',
},
{
name: 'Crossing the t\'s',
addPrevAssistant: true,
addUser: 'Compare the draft character sheet with the original transcript, validating its content and ensuring it captures both the speakers overt characteristics and the subtler undertones. Omit unknown information, fine-tune any areas that require clarity, have been overlooked, or require more authenticity. Use clear and illustrative examples from the transcript to refine your sheet and offer meaningful, tangible reference points. Your output is a coherent, comprehensive, and nuanced instruction that begins with \'You are a...\' and serves as a go-to guide for an actor recreating the persona.',
},
// {
// name: 'Shrink',
// addPrevAssistant: true,
// addUser: 'Now remove all the uncertain information, omit unknown information, Your output is a coherent, comprehensive, and nuanced instruction that begins with \'You are a...\' and serves as a go-to guide for a recreating the persona.',
// },
];
export function YTPersonaCreator() {
// state
const [videoURL, setVideoURL] = React.useState('');
const [videoID, setVideoID] = React.useState('');
const [personaTranscript, setPersonaTranscript] = React.useState<string | null>(null);
// external state
const [diagramLlm, llmComponent] = useFormRadioLlmType();
// fetch transcript when the Video ID is ready, then store it
const { transcript, thumbnailUrl, title, isFetching, isError, error: transcriptError } =
useTranscriptFromVideo(videoID);
React.useEffect(() => setPersonaTranscript(transcript), [transcript]);
// use the transformation sequence to create a persona
const { isFinished, isTransforming, chainProgress, chainIntermediates, chainStepName, chainOutput, chainError, abortChain } =
useLLMChain(YouTubePersonaSteps, diagramLlm?.id, personaTranscript ?? undefined);
const handleVideoIdChange = (e: React.ChangeEvent<HTMLInputElement>) => setVideoURL(e.target.value);
const handleFetchTranscript = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // stop the form submit
const videoId = extractVideoID(videoURL);
if (!videoId) {
setVideoURL('Invalid');
} else {
setPersonaTranscript(null);
setVideoID(videoId);
}
};
return <>
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 1 }}>
<YouTubeIcon sx={{ color: '#f00' }} />
<Typography level='title-lg'>
YouTube -&gt; AI persona
</Typography>
</Box>
<form onSubmit={handleFetchTranscript}>
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
<Input
required
type='url'
fullWidth
variant='outlined'
placeholder='YouTube Video URL'
value={videoURL} onChange={handleVideoIdChange}
endDecorator={
<IconButton
variant='outlined' color='neutral'
onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}
>
<WhatshotIcon />
</IconButton>
}
/>
<Button
type='submit'
variant='solid' disabled={isFetching || isTransforming} loading={isFetching}
sx={{ minWidth: 120 }}>
Create
</Button>
</Box>
</form>
{/* LLM selector (chat vs fast) */}
{!isTransforming && !isFinished && llmComponent}
{/* 1. Transcript*/}
{personaTranscript && (
<Card sx={{ mt: 2, boxShadow: 'md' }}>
<CardContent>
<Typography level='title-md' sx={{ mb: 1 }}>
{title || 'Transcript'}
</Typography>
<Box>
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Image' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
<Typography level='body-sm'>
{personaTranscript.slice(0, 280)}...
</Typography>
</Box>
</CardContent>
</Card>
)}
{/* Errors */}
{isError && (
<Alert color='warning' sx={{ mt: 1 }}>
<Typography component='div'>{transcriptError?.message || 'Unknown error'}</Typography>
</Alert>
)}
{!!chainError && (
<Alert color='warning' sx={{ mt: 1 }}>
<Typography component='div'>{chainError}</Typography>
</Alert>
)}
{/* Persona! */}
{chainOutput && <Box sx={{ mt: 2 }}>
<Typography level='title-lg'>
YouTuber Persona System Prompt
</Typography>
<Card sx={{ boxShadow: 'md' }}>
<CardContent sx={{
position: 'relative',
'&:hover > button': { opacity: 1 },
}}>
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
You can now copy the following text and use it as Custom prompt!
</Alert>
<Tooltip title='Copy system prompt' variant='solid'>
<IconButton
variant='outlined' color='neutral' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')}
sx={{
position: 'absolute', right: 0, zIndex: 10,
// opacity: 0, transition: 'opacity 0.3s',
}}>
<ContentCopyIcon />
</IconButton>
</Tooltip>
<Typography level='body-sm'>
{chainOutput}
</Typography>
</CardContent>
</Card>
</Box>}
{/* Intermediate outputs rendered as cards in a grid */}
{chainIntermediates && chainIntermediates.length > 0 && <Box sx={{ mt: 2 }}>
<Typography level='title-lg'>
{isTransforming ? 'Working...' : 'Intermediate Work'}
</Typography>
<Grid container spacing={2}>
{chainIntermediates.map((intermediate, i) =>
<Grid xs={12} sm={6} md={4} key={i}>
<Card>
<CardContent>
<Typography level='title-sm' sx={{ mb: 1 }}>
{i + 1}. {YouTubePersonaSteps[i].name}
</Typography>
<Typography level='body-sm'>
{intermediate?.slice(0, 140)}...
</Typography>
</CardContent>
</Card>
</Grid>,
)}
</Grid>
</Box>}
{/* Embodiment Progress */}
{isTransforming && <GoodModal open>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 2 }}>
<CircularProgress color='primary' value={Math.max(10, 100 * chainProgress)} />
</Box>
<Typography color='success' level='title-lg' sx={{ mt: 1 }}>
Embodying Persona ...
</Typography>
<Typography color='success' level='title-sm' sx={{ mt: 1, fontWeight: 600 }}>
{chainStepName}
</Typography>
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1, mb: 2 }} />
<Typography level='title-sm'>
This may take 1-2 minutes. Do not close this window or the progress will be lost.
If you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
please try again with faster/smaller models.
</Typography>
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 5 }}>
Cancel
</Button>
</GoodModal>}
</>;
}
+298
View File
@@ -0,0 +1,298 @@
import * as React from 'react';
import { Alert, Box, Button, Card, CardContent, CircularProgress, Divider, FormLabel, Grid, IconButton, LinearProgress, Tab, TabList, TabPanel, Tabs, Typography } from '@mui/joy';
import AddIcon from '@mui/icons-material/Add';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
import { RenderMarkdown } from '../../chat/components/message/RenderMarkdown';
import { LLMChainStep, useLLMChain } from '~/modules/aifn/useLLMChain';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { useFormEditTextArray } from '~/common/components/forms/useFormEditTextArray';
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
import { useToggleableBoolean } from '~/common/util/useToggleableBoolean';
import { FromText } from './FromText';
import { FromYouTube } from './FromYouTube';
import { prependSimplePersona, SimplePersonaProvenance } from '../store-app-personas';
// delay to start a new chain after the previous one finishes
const CONTINUE_DELAY: number | false = false;
const Prompts: string[] = [
'You are skilled in analyzing and embodying diverse characters. You meticulously study transcripts to capture key attributes, draft comprehensive character sheets, and refine them for authenticity. Feel free to make assumptions without hedging, be concise and be creative.',
'Conduct comprehensive research on the provided transcript. Identify key characteristics of the speaker, including age, professional field, distinct personality traits, style of communication, narrative context, and self-awareness. Additionally, consider any unique aspects such as their use of humor, their cultural background, core values, passions, fears, personal history, and social interactions. Your output for this stage is an in-depth written analysis that exhibits an understanding of both the superficial and more profound aspects of the speaker\'s persona.',
'Craft your documented analysis into a draft of the \'You are a...\' character sheet. It should encapsulate all crucial personality dimensions, along with the motivations and aspirations of the persona. Keep in mind to balance succinctness and depth of detail for each dimension. The deliverable here is a comprehensive draft of the character sheet that captures the speaker\'s unique essence.',
'Compare the draft character sheet with the original transcript, validating its content and ensuring it captures both the speakers overt characteristics and the subtler undertones. Omit unknown information, fine-tune any areas that require clarity, have been overlooked, or require more authenticity. Use clear and illustrative examples from the transcript to refine your sheet and offer meaningful, tangible reference points. Your output is a coherent, comprehensive, and nuanced instruction that begins with \'You are a...\' and serves as a go-to guide for an actor recreating the persona.',
];
const PromptTitles: string[] = [
'Common: Creator System Prompt',
'Analyze the transcript',
'Define the character',
'Cross the t\'s',
];
// chain to convert a text input string (e.g. youtube transcript) into a persona prompt
function createChain(instructions: string[], titles: string[]): LLMChainStep[] {
return [
{
name: titles[1],
setSystem: instructions[0],
addUserInput: true,
addUser: instructions[1],
},
{
name: titles[2],
addPrevAssistant: true,
addUser: instructions[2],
},
{
name: titles[3],
addPrevAssistant: true,
addUser: instructions[3],
},
];
}
export const PersonaPromptCard = (props: { content: string }) =>
<Card sx={{ boxShadow: 'md', mt: 3 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography level='title-lg' color='success' startDecorator={<SettingsAccessibilityIcon color='success' />}>
Persona Prompt
</Typography>
<GoodTooltip title='Copy system prompt'>
<Button color='success' onClick={() => copyToClipboard(props.content, 'Persona prompt')} endDecorator={<ContentCopyIcon />} sx={{ minWidth: 120 }}>
Copy
</Button>
</GoodTooltip>
</Box>
<CardContent>
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
You may now copy the text below and use it as Custom prompt!
</Alert>
<RenderMarkdown textBlock={{ type: 'text', content: props.content }} />
</CardContent>
</Card>;
export function Creator(props: { display: boolean }) {
// state
const advanced = useToggleableBoolean();
const [selectedTab, setSelectedTab] = React.useState(0);
const [chainInputText, setChainInputText] = React.useState<string | null>(null);
const [inputProvenance, setInputProvenance] = React.useState<SimplePersonaProvenance | null>(null);
const [showIntermediates, setShowIntermediates] = React.useState(false);
// external state
const [personaLlm, llmComponent] = useLLMSelect(true, 'Persona Creation Model');
// editable prompts
const {
strings: editedInstructions, stringEditors: instructionEditors,
} = useFormEditTextArray(Prompts, PromptTitles);
const creationChainSteps = React.useMemo(() => {
return createChain(editedInstructions, PromptTitles);
}, [editedInstructions]);
const llmLabel = personaLlm?.label || undefined;
const savePersona = React.useCallback((personaPrompt: string, inputText: string) => {
prependSimplePersona(personaPrompt, inputText, inputProvenance ?? undefined, llmLabel);
}, [inputProvenance, llmLabel]);
const {
// isFinished,
isTransforming,
chainProgress,
chainIntermediates,
chainStepName,
chainStepInterimChars,
chainOutput,
chainError,
userCancelChain,
restartChain,
} = useLLMChain(creationChainSteps, personaLlm?.id, chainInputText ?? undefined, savePersona);
// Reset the relevant state when the selected tab changes
React.useEffect(() => {
setChainInputText(null);
}, [selectedTab]);
// [debug] Restart the chain when complete after a delay
const debugRestart = !!CONTINUE_DELAY && !isTransforming && (chainProgress === 1 || !!chainError);
React.useEffect(() => {
if (debugRestart) {
const timeout = setTimeout(restartChain, CONTINUE_DELAY);
return () => clearTimeout(timeout);
}
}, [debugRestart, restartChain]);
const handleCreate = React.useCallback((text: string, provenance: SimplePersonaProvenance) => {
setChainInputText(text);
setInputProvenance(provenance);
}, []);
const handleCancel = React.useCallback(() => {
setChainInputText(null);
setInputProvenance(null);
userCancelChain();
}, [userCancelChain]);
// Hide the GFX, but not the logic (hooks)
if (!props.display)
return null;
return <>
<Typography level='title-sm' mb={3}>
Create the <em>System Prompt</em> of an AI Persona from YouTube or Text.
</Typography>
{/* Inputs */}
<Tabs
variant='outlined'
defaultValue={0}
value={selectedTab}
onChange={(_event, newValue) => setSelectedTab(newValue as number)}
sx={{
// boxShadow: 'sm',
borderRadius: 'md',
// overflow: 'hidden',
display: isTransforming ? 'none' : undefined,
}}
>
<TabList sx={{ minHeight: '3rem' }}>
<Tab>From YouTube Video</Tab>
<Tab>From Text</Tab>
</TabList>
<TabPanel keepMounted value={0} sx={{ p: 3 }}>
<FromYouTube isTransforming={isTransforming} onCreate={handleCreate} />
</TabPanel>
<TabPanel keepMounted value={1} sx={{ p: 3 }}>
<FromText isCreating={isTransforming} onCreate={handleCreate} />
</TabPanel>
<Divider orientation='horizontal' />
<Box sx={{ p: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
{llmComponent}
{advanced.on && (
<Box sx={{ my: 1, display: 'flex', flexDirection: 'column', gap: 2 }}>
{instructionEditors}
</Box>
)}
<FormLabel onClick={advanced.toggle} sx={{ textDecoration: 'underline', cursor: 'pointer' }}>
{advanced.on ? 'Hide Advanced' : 'Advanced: Prompts'}
</FormLabel>
</Box>
</Tabs>
{/* Embodiment Progress */}
{/* <GoodModal open> */}
{isTransforming && <Card><CardContent sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 2 }}>
<CircularProgress color='primary' value={Math.max(10, 100 * chainProgress)} />
</Box>
<Box>
<Typography color='success' level='title-lg'>
Embodying Persona ...
</Typography>
<Typography level='title-sm' sx={{ mt: 1 }}>
Using: {personaLlm?.label}
</Typography>
</Box>
<Box>
<Typography color='success' level='title-sm' sx={{ fontWeight: 600 }}>
{chainStepName}
</Typography>
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1.5 }} />
<Typography level='body-sm' sx={{ mt: 1 }}>
{chainStepInterimChars === null ? 'Loading ...' : `Generating (${chainStepInterimChars.toLocaleString()} bytes) ...`}
</Typography>
</Box>
<Typography level='title-sm'>
This may take 1-2 minutes.
While larger models will produce higher quality prompts,
if you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
please try again with faster/smaller models.
</Typography>
<Button variant='soft' color='neutral' onClick={handleCancel} sx={{ ml: 'auto', minWidth: 100, mt: 3 }}>
Cancel
</Button>
</CardContent></Card>}
{/* Errors */}
{!!chainError && (
<Alert color='warning' sx={{ mt: 1 }}>
<Typography component='div'>{chainError}</Typography>
</Alert>
)}
{/* The Persona (Output) */}
{chainOutput && <>
<PersonaPromptCard content={chainOutput} />
</>}
{/* Input + Intermediate outputs (with expander) */}
{(isTransforming || chainIntermediates?.length > 0) && <>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', mt: 3, mb: 0.5, mx: 1 }}>
<Typography level='title-lg'>
{isTransforming ? 'Working ...' : 'Intermediate Work'}
</Typography>
<IconButton size='sm' variant={showIntermediates ? 'solid' : 'outlined'} onClick={() => setShowIntermediates(s => !s)}>
<AddIcon />
</IconButton>
</Box>
<Grid container spacing={2}>
<Grid xs={12} md={showIntermediates ? 12 : 6}>
<Card sx={{ height: '100%', overflow: 'hidden' }}>
<CardContent>
<Typography color='success' level='title-sm' sx={{ mb: 1 }}>
Input Text
</Typography>
<Typography level='body-sm'>
{showIntermediates ? chainInputText : (chainInputText?.slice(0, 280) + '...')}
</Typography>
</CardContent>
</Card>
</Grid>
{chainIntermediates.map((intermediate, i) =>
<Grid xs={12} md={showIntermediates ? 12 : 6} key={i}>
<Card sx={{ height: '100%', overflow: 'hidden' }}>
<CardContent>
<Typography color='success' level='title-sm' sx={{ mb: 1 }}>
{i + 1}. {intermediate.name}
</Typography>
<Typography level='body-sm'>
{showIntermediates ? intermediate.output : (intermediate.output?.slice(0, 280) + '...')}
</Typography>
</CardContent>
</Card>
</Grid>,
)}
</Grid>
</>}
</>;
}
+174
View File
@@ -0,0 +1,174 @@
import * as React from 'react';
import { Box, Button, IconButton, ListItemButton, ListItemDecorator, Sheet, Tooltip, Typography } from '@mui/joy';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import Diversity2Icon from '@mui/icons-material/Diversity2';
import DoneIcon from '@mui/icons-material/Done';
import { PageDrawerHeader } from '~/common/layout/optima/components/PageDrawerHeader';
import { PageDrawerList } from '~/common/layout/optima/components/PageDrawerList';
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
import { CreatorDrawerItem } from './CreatorDrawerItem';
import { deleteSimplePersona, useSimplePersonas } from '../store-app-personas';
export function CreatorDrawer(props: {
selectedSimplePersonaId: string | null,
setSelectedSimplePersonaId: (simplePersonaId: string | null) => void,
}) {
// selection mode
const [selectMode, setSelectMode] = React.useState(false);
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(new Set());
// external state
const { closeDrawer } = useOptimaDrawers();
const { simplePersonas } = useSimplePersonas();
// derived state
const hasPersonas = simplePersonas.length > 0;
// Simple Persona Operations
const { setSelectedSimplePersonaId } = props;
const handleSimplePersonaUnselect = React.useCallback(() => {
setSelectedSimplePersonaId(null);
}, [setSelectedSimplePersonaId]);
const handleSimplePersonaDelete = React.useCallback((simplePersonaId: string) => {
deleteSimplePersona(simplePersonaId);
handleSimplePersonaUnselect();
}, [handleSimplePersonaUnselect]);
// Selection
const handleSelectionClose = React.useCallback(() => {
setSelectMode(false);
setSelectedIds(new Set());
}, []);
const handleSelectionToggleId = React.useCallback((simplePersonaId: string) => {
setSelectedIds(prevSelectedIds => {
const newSelectedItems = new Set(prevSelectedIds);
if (newSelectedItems.has(simplePersonaId))
newSelectedItems.delete(simplePersonaId);
else
newSelectedItems.add(simplePersonaId);
return newSelectedItems;
});
}, []);
const handleSelectionInvert = React.useCallback(() => {
setSelectedIds(prevSelectedIds => {
const newSelectedIds = new Set(prevSelectedIds);
simplePersonas.forEach(persona => {
if (newSelectedIds.has(persona.id))
newSelectedIds.delete(persona.id);
else
newSelectedIds.add(persona.id);
});
return newSelectedIds;
});
}, [simplePersonas]);
const handleSelectionDelete = React.useCallback(() => {
selectedIds.forEach(simplePersonaId => {
deleteSimplePersona(simplePersonaId);
});
// clear the selection after deletion
setSelectedIds(new Set());
}, [selectedIds]);
return <>
{/* Drawer Header */}
<PageDrawerHeader
title={selectMode ? 'Selection Mode' : 'Recent'}
onClose={selectMode ? handleSelectionClose : closeDrawer}
startButton={(!hasPersonas || selectMode) ? undefined :
<Tooltip title={selectMode ? 'Done' : 'Select'}>
<IconButton onClick={selectMode ? handleSelectionClose : () => setSelectMode(true)}>
{selectMode ? <DoneIcon /> : <CheckBoxOutlineBlankIcon />}
</IconButton>
</Tooltip>
}
/>
<PageDrawerList
variant='plain'
noTopPadding noBottomPadding tallRows
onClick={handleSimplePersonaUnselect}
>
{selectMode ? (
// Selection Header
<Sheet variant='soft' color='warning' invertedColors>
<Box sx={{ display: 'flex', alignItems: 'center', px: 1, minHeight: '3rem' }}>
<Button
variant='plain'
color='warning'
startDecorator={selectedIds.size === simplePersonas.length ? <CheckBoxOutlineBlankIcon /> : <CheckBoxIcon />}
onClick={handleSelectionInvert}
>
{selectedIds.size === simplePersonas.length ? 'Select None' : selectedIds.size !== 0 ? 'Invert' : 'Select All'}
</Button>
<Button
variant='solid'
color='warning'
startDecorator={<DeleteOutlineIcon />}
onClick={handleSelectionDelete}
disabled={selectedIds.size === 0}
sx={{ ml: 'auto' }}
>
Delete
</Button>
</Box>
</Sheet>
) : (
// Create Button
<ListItemButton
variant={props.selectedSimplePersonaId ? 'plain' : 'soft'}
onClick={handleSimplePersonaUnselect}
>
<ListItemDecorator>
<Diversity2Icon />
</ListItemDecorator>
<Typography level='title-sm' sx={!props.selectedSimplePersonaId ? { fontWeight: 600 } : undefined}>
Create
</Typography>
</ListItemButton>
)}
{/* Personas [] */}
<Box sx={{ flex: 1, overflowY: 'auto' }}>
{simplePersonas.map(item =>
<CreatorDrawerItem
key={item.id}
item={item}
isActive={item.id === props.selectedSimplePersonaId}
isSelected={selectedIds.has(item.id)}
isSelection={selectMode}
onClick={(event) => {
event.stopPropagation();
if (selectMode)
handleSelectionToggleId(item.id);
else
props.setSelectedSimplePersonaId(item.id);
}}
onDelete={handleSimplePersonaDelete}
/>,
)}
</Box>
</PageDrawerList>
</>;
}
@@ -0,0 +1,100 @@
import * as React from 'react';
import TimeAgo from 'react-timeago';
import { Box, Checkbox, IconButton, ListItemButton, ListItemDecorator, Typography } from '@mui/joy';
import CloseIcon from '@mui/icons-material/Close';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import TextFieldsIcon from '@mui/icons-material/TextFields';
import YouTubeIcon from '@mui/icons-material/YouTube';
import type { SimplePersona } from '../store-app-personas';
export function CreatorDrawerItem(props: {
item: SimplePersona,
isActive: boolean,
isSelected: boolean,
isSelection: boolean,
onClick: (event: React.MouseEvent) => void,
onDelete: (simplePersonaId: string) => void,
}) {
// state
const [deleteArmed, setDeleteArmed] = React.useState(false);
// derived
const { item, isActive } = props;
const thumbnailUrl = item.pictureUrl || ((item.inputProvenance?.type === 'youtube' && item.inputProvenance.thumbnailUrl) ? item.inputProvenance.thumbnailUrl : undefined);
const icon = thumbnailUrl
? <picture style={{ lineHeight: 0 }}><img src={thumbnailUrl} alt='Simple Persona Thumbnail' width={20} height={20} /></picture>
: item.inputProvenance?.type === 'text'
? <TextFieldsIcon />
: item.inputProvenance?.type === 'youtube'
? <YouTubeIcon />
: undefined;
return (
<ListItemButton
variant={isActive ? 'soft' : undefined}
onClick={props.onClick}
sx={{
'&:hover > button': { opacity: 1 },
}}
>
{/* Symbol or Thumbnail picture */}
<ListItemDecorator>
{props.isSelection ? (
<Checkbox checked={props.isSelected} />
) : icon}
</ListItemDecorator>
<Box sx={{ overflow: 'hidden' }}>
{/* Title or System prompt (ellipsized) */}
<Typography level='title-sm' sx={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
{item.name || (item.systemPrompt?.slice(0, 40) + '...')}
</Typography>
{/* creation Model */}
{/*{!!item.llmLabel && <Typography level='body-xs' sx={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>*/}
{/* {item.llmLabel}*/}
{/*</Typography>}*/}
{/* creation Date */}
<Typography level='body-xs'>
{!!item.creationDate && <TimeAgo date={item.creationDate} />}
</Typography>
</Box>
{/* Delete Arming */}
{!props.isSelection && !deleteArmed && (
<IconButton
variant={isActive ? 'solid' : 'outlined'}
size='sm'
sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.2s' }}
onClick={() => setDeleteArmed(on => !on)}
>
<DeleteOutlineIcon />
</IconButton>
)}
{/* Delete / Cancel buttons */}
{!props.isSelection && deleteArmed && <>
<IconButton size='sm' variant='solid' color='danger' onClick={() => props.onDelete(item.id)}>
<DeleteOutlineIcon />
</IconButton>
<IconButton size='sm' variant='solid' color='neutral' onClick={() => setDeleteArmed(false)}>
<CloseIcon />
</IconButton>
</>}
</ListItemButton>
);
}
+68
View File
@@ -0,0 +1,68 @@
import * as React from 'react';
import { Box, Button, Textarea, Typography } from '@mui/joy';
import TextFieldsIcon from '@mui/icons-material/TextFields';
import { lineHeightTextarea } from '~/common/app.theme';
import type { SimplePersonaProvenance } from '../store-app-personas';
// minimum number of characters required to create from text
const MIN_CHARS = 100;
export function FromText(props: {
isCreating: boolean;
onCreate: (text: string, provenance: SimplePersonaProvenance) => void;
}) {
// state
const [text, setText] = React.useState('');
const handleCreateFromText = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // stop the form submit
props.onCreate(text, { type: 'text' });
};
return <>
<Typography level='title-md' startDecorator={<TextFieldsIcon />} sx={{ mb: 3 }}>
<b>Text</b> -&gt; Persona
</Typography>
<form onSubmit={handleCreateFromText}>
<Textarea
required
variant='outlined'
minRows={4} maxRows={8}
placeholder='Paste your text here...'
value={text}
onChange={event => setText(event.target.value)}
sx={{
backgroundColor: 'background.level1',
'&:focus-within': {
backgroundColor: 'background.popup',
},
lineHeight: lineHeightTextarea,
mb: 1.5,
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button
type='submit' variant='solid'
disabled={props.isCreating || text?.length < MIN_CHARS}
sx={{ minWidth: 140 }}
>
Create
</Button>
<Typography level='body-sm'>
{text.length < MIN_CHARS ? `(${MIN_CHARS - text.length})` : text.length.toLocaleString()}
</Typography>
</Box>
</form>
</>;
}
+165
View File
@@ -0,0 +1,165 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, Button, Card, IconButton, Input, Typography } from '@mui/joy';
import CloseIcon from '@mui/icons-material/Close';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { useYouTubeTranscript, YTVideoTranscript } from '~/modules/youtube/useYouTubeTranscript';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { InlineError } from '~/common/components/InlineError';
import type { SimplePersonaProvenance } from '../store-app-personas';
function extractVideoID(videoURL: string): string | null {
const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^#&?]*).*/;
const match = videoURL.match(regExp);
return (match && match[1]?.length == 11) ? match[1] : null;
}
function YouTubeVideoTranscriptCard(props: { transcript: YTVideoTranscript, onClose: () => void, sx?: SxProps }) {
const { transcript } = props;
return (
<Card
variant='soft'
sx={{
border: '1px dashed',
borderColor: 'neutral.solidBg',
p: 1,
...props.sx,
}}
>
<Box sx={{ position: 'relative' }}>
{!!transcript.thumbnailUrl && (
<picture style={{ lineHeight: 0 }}>
<img
src={transcript.thumbnailUrl}
alt='YouTube Video Thumbnail'
height={80}
style={{ float: 'left', marginRight: 8 }}
/>
</picture>
)}
{/*<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>*/}
<Typography level='title-sm'>
{transcript?.title}
</Typography>
<Typography level='body-xs' sx={{ mt: 0.75 }}>
{transcript?.transcript.slice(0, 280)}...
</Typography>
{/*</Box>*/}
<IconButton
size='sm'
onClick={props.onClose}
sx={{
position: 'absolute', top: -8, right: -8,
borderRadius: 'md',
}}>
<CloseIcon />
</IconButton>
</Box>
</Card>
);
}
export function FromYouTube(props: {
isTransforming: boolean;
onCreate: (text: string, provenance: SimplePersonaProvenance) => void;
}) {
// state
const [videoURL, setVideoURL] = React.useState('');
const [videoID, setVideoID] = React.useState<string | null>(null);
// external state
const { onCreate } = props;
const onNewTranscript = React.useCallback((transcript: YTVideoTranscript) => {
// setVideoID(null); // reset the video ID, to cycle the refetch
onCreate(
transcript.transcript,
{
type: 'youtube',
url: videoURL,
title: transcript.title,
thumbnailUrl: transcript.thumbnailUrl,
},
);
}, [onCreate, videoURL]);
const {
transcript,
isFetching, isError, error,
} = useYouTubeTranscript(videoID, onNewTranscript);
const handleVideoURLChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setVideoID(null);
setVideoURL(e.target.value);
};
const handleCreateFromTranscript = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault(); // stop the form submit
const videoId = extractVideoID(videoURL) || null;
if (!videoId)
setVideoURL('Invalid');
// kick-start the transcript fetch
setVideoID(videoId);
};
return <>
<Typography level='title-md' startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />} sx={{ mb: 3 }}>
YouTube -&gt; Persona
</Typography>
<form onSubmit={handleCreateFromTranscript}>
<Input
required
type='url'
fullWidth
disabled={isFetching || props.isTransforming}
variant='outlined'
placeholder='YouTube Video URL'
value={videoURL}
onChange={handleVideoURLChange}
sx={{ mb: 1.5 }}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button
type='submit' variant='solid'
disabled={isFetching || props.isTransforming || !videoURL}
loading={isFetching}
sx={{ minWidth: 140 }}
>
Create
</Button>
<GoodTooltip title='This example comes from the popular Fireship YouTube channel, which presents technical topics with irreverent humor.'>
<Button variant='outlined' color='neutral' onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}>
Example
</Button>
</GoodTooltip>
</Box>
</form>
{isError && (
<InlineError error={error} sx={{ mt: 3 }} />
)}
{!!transcript && !!videoID && (
<YouTubeVideoTranscriptCard transcript={transcript} onClose={() => setVideoID(null)} sx={{ mt: 3 }} />
)}
</>;
}
+36
View File
@@ -0,0 +1,36 @@
import * as React from 'react';
import TimeAgo from 'react-timeago';
import { Typography } from '@mui/joy';
import { Link } from '~/common/components/Link';
import { PersonaPromptCard } from './Creator';
import { useSimplePersona } from '../store-app-personas';
export function Viewer(props: { selectedSimplePersonaId: string }) {
// external state
const { simplePersona } = useSimplePersona(props.selectedSimplePersonaId);
if (!simplePersona)
return <Typography level='body-sm'>Loading Persona...</Typography>;
return <>
<Typography level='title-sm'>
This <em>System Prompt</em> was created <TimeAgo date={simplePersona.creationDate} />
using the <strong>{simplePersona.llmLabel}</strong> model.
</Typography>
<PersonaPromptCard content={simplePersona.systemPrompt || ''} />
{/* tell about the Provenances */}
<Typography level='body-sm' sx={{ mt: 3 }}>
{simplePersona.inputProvenance?.type === 'youtube' && <>The source was this YouTube video: <Link href={simplePersona.inputProvenance.url} target='_blank'>{simplePersona.inputProvenance.title}</Link>.</>}
{simplePersona.inputProvenance?.type === 'text' && <>The source was a text snippet of {simplePersona.inputText?.length.toLocaleString()} characters.</>}
</Typography>
</>;
}
+101
View File
@@ -0,0 +1,101 @@
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
import { shallow } from 'zustand/shallow';
import { createBase36Uid } from '~/common/util/textUtils';
/**
* Very simple personas store for the "Persona Creator" - note that we shall
* switch to a more complex personas store in the future, as for now we mainly
* save system prompts so that we don't lose what was created.
*/
export interface SimplePersona {
id: string;
name?: string;
systemPrompt: string; // The system prompt is very important and required
creationDate: string; // ISO string format
pictureUrl?: string; // Optional picture URL
// source material
inputProvenance?: SimplePersonaProvenance;
inputText: string;
// llm used
llmLabel?: string;
}
export type SimplePersonaProvenance = {
type: 'youtube';
url: string;
title?: string;
thumbnailUrl?: string;
} | {
type: 'text';
};
interface AppPersonasStore {
// state
simplePersonas: SimplePersona[];
// actions
prependSimplePersona: (systemPrompt: string, inputText: string, inputProvenance?: SimplePersonaProvenance, llmLabel?: string) => void;
deleteSimplePersona: (id: string) => void;
}
/**
* DO NOT USE outside of this application - this is a very simple store for Personas so that
* they're not immediately lost.
*/
const useAppPersonasStore = create<AppPersonasStore>()(persist(
(_set, _get) => ({
simplePersonas: [],
prependSimplePersona: (systemPrompt: string, inputText: string, inputProvenance?: SimplePersonaProvenance, llmLabel?: string) =>
_set(state => ({
simplePersonas: [
{
id: createBase36Uid(state.simplePersonas.map(persona => persona.id)),
systemPrompt,
creationDate: new Date().toISOString(),
inputProvenance,
inputText,
llmLabel,
},
...state.simplePersonas,
],
})),
deleteSimplePersona: (simplePersonaId: string) =>
_set(state => ({
simplePersonas: state.simplePersonas.filter(persona => persona.id !== simplePersonaId),
})),
}),
{
name: 'app-app-personas',
version: 1,
},
));
export function useSimplePersonas() {
const simplePersonas = useAppPersonasStore(state => state.simplePersonas, shallow);
return { simplePersonas };
}
export function useSimplePersona(simplePersonaId: string) {
const simplePersona = useAppPersonasStore(state => {
return state.simplePersonas.find(persona => persona.id === simplePersonaId) ?? null;
}, shallow);
return { simplePersona };
}
export function prependSimplePersona(systemPrompt: string, inputText: string, inputProvenance?: SimplePersonaProvenance, llmLabel?: string) {
useAppPersonasStore.getState().prependSimplePersona(systemPrompt, inputText, inputProvenance, llmLabel);
}
export function deleteSimplePersona(simplePersonaId: string) {
useAppPersonasStore.getState().deleteSimplePersona(simplePersonaId);
}
+10 -5
View File
@@ -9,8 +9,8 @@ import WidthWideIcon from '@mui/icons-material/WidthWide';
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
import { FormRadioControl } from '~/common/components/forms/FormRadioControl';
import { isPwa } from '~/common/util/pwaUtils';
import { openLayoutModelsSetup } from '~/common/layout/store-applayout';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useUIPreferencesStore } from '~/common/state/store-ui';
@@ -18,10 +18,14 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
const SHOW_PURPOSE_FINDER = false;
const ModelOptionsButton = () =>
<Button
const ModelsSetupButton = () => {
// external state
const { openModelsSetup } = useOptimaLayout();
return <Button
// variant='soft' color='success'
onClick={openLayoutModelsSetup}
onClick={openModelsSetup}
startDecorator={<BuildCircleIcon />}
sx={{
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
@@ -29,6 +33,7 @@ const ModelOptionsButton = () =>
>
Models
</Button>;
};
export function AppChatSettingsUI() {
@@ -64,7 +69,7 @@ export function AppChatSettingsUI() {
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<FormLabelStart title='AI Models'
description='Setup' />
<ModelOptionsButton />
<ModelsSetupButton />
</FormControl>
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between' }}>
+23 -12
View File
@@ -6,12 +6,13 @@ import ScienceIcon from '@mui/icons-material/Science';
import SearchIcon from '@mui/icons-material/Search';
import { BrowseSettings } from '~/modules/browse/BrowseSettings';
import { DallESettings } from '~/modules/t2i/dalle/DallESettings';
import { ElevenlabsSettings } from '~/modules/elevenlabs/ElevenlabsSettings';
import { GoogleSearchSettings } from '~/modules/google/GoogleSearchSettings';
import { ProdiaSettings } from '~/modules/prodia/ProdiaSettings';
import { ProdiaSettings } from '~/modules/t2i/prodia/ProdiaSettings';
import { T2ISettings } from '~/modules/t2i/T2ISettings';
import { GoodModal } from '~/common/components/GoodModal';
import { closeLayoutPreferences, openLayoutShortcuts, useLayoutPreferencesTab } from '~/common/layout/store-applayout';
import { settingsGap } from '~/common/app.theme';
import { useIsMobile } from '~/common/components/useMatchMedia';
@@ -100,20 +101,24 @@ function Topic(props: { title?: string, icon?: string | React.ReactNode, startCo
* Component that allows the User to modify the application settings,
* persisted on the client via localStorage.
*/
export function SettingsModal() {
export function SettingsModal(props: {
open: boolean,
tabIndex: number,
onClose: () => void,
onOpenShortcuts: () => void,
}) {
// external state
const isMobile = useIsMobile();
const settingsTabIndex = useLayoutPreferencesTab();
const tabFixSx = { fontFamily: 'body', flex: 1, p: 0, m: 0 };
return (
<GoodModal
title='Preferences' strongerTitle
open={!!settingsTabIndex} onClose={closeLayoutPreferences}
open={props.open} onClose={props.onClose}
startButton={isMobile ? undefined : (
<Button variant='soft' onClick={openLayoutShortcuts}>
<Button variant='soft' onClick={props.onOpenShortcuts}>
👉 See Shortcuts
</Button>
)}
@@ -124,7 +129,7 @@ export function SettingsModal() {
<Divider />
<Tabs aria-label='Settings tabbed menu' defaultValue={settingsTabIndex}>
<Tabs aria-label='Settings tabbed menu' defaultValue={props.tabIndex}>
<TabList
variant='soft'
disableUnderline
@@ -151,7 +156,7 @@ export function SettingsModal() {
<Tab disableIndicator value={4} sx={tabFixSx}>Tools</Tab>
</TabList>
<TabPanel value={1} sx={{ p: 'var(--Tabs-gap)' }}>
<TabPanel value={1} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
<Topics>
<Topic>
<AppChatSettingsUI />
@@ -165,7 +170,7 @@ export function SettingsModal() {
</Topics>
</TabPanel>
<TabPanel value={3} sx={{ p: 'var(--Tabs-gap)' }}>
<TabPanel value={3} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
<Topics>
<Topic icon='🎙️' title='Voice settings'>
<VoiceSettings />
@@ -176,15 +181,21 @@ export function SettingsModal() {
</Topics>
</TabPanel>
<TabPanel value={2} sx={{ p: 'var(--Tabs-gap)' }}>
<TabPanel value={2} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
<Topics>
<Topic icon='🖍️' title='Prodia API'>
<Topic>
<T2ISettings />
</Topic>
<Topic icon='🖍️' title='OpenAI DALL·E' startCollapsed>
<DallESettings />
</Topic>
<Topic icon='🖍️' title='Prodia API' startCollapsed>
<ProdiaSettings />
</Topic>
</Topics>
</TabPanel>
<TabPanel value={4} sx={{ p: 'var(--Tabs-gap)' }}>
<TabPanel value={4} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
<Topics>
<Topic icon={<SearchIcon />} title='Browsing' startCollapsed>
<BrowseSettings />
+2 -11
View File
@@ -3,7 +3,6 @@ import * as React from 'react';
import { ChatMessage } from '../chat/components/message/ChatMessage';
import { GoodModal } from '~/common/components/GoodModal';
import { closeLayoutShortcuts, useLayoutShortcuts } from '~/common/layout/store-applayout';
import { createDMessage } from '~/common/state/store-chats';
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
@@ -36,17 +35,9 @@ const shortcutsMd = `
const shortcutsMessage = createDMessage('assistant', platformAwareKeystrokes(shortcutsMd));
export function ShortcutsModal() {
// external state
const showShortcuts = useLayoutShortcuts();
export function ShortcutsModal(props: { onClose: () => void }) {
return (
<GoodModal
open={showShortcuts}
title='Desktop Shortcuts'
onClose={closeLayoutShortcuts}
>
<GoodModal open title='Desktop Shortcuts' onClose={props.onClose}>
<ChatMessage message={shortcutsMessage} hideAvatars noBottomBorder sx={{ p: 0, m: 0 }} />
</GoodModal>
);
+4 -16
View File
@@ -3,9 +3,7 @@ import * as React from 'react';
import { FormControl, Typography } from '@mui/joy';
import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
import CallIcon from '@mui/icons-material/Call';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
@@ -19,22 +17,12 @@ export function UxLabsSettings() {
// external state
const isMobile = useIsMobile();
const {
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsMagicDraw, labsPersonaYTCreator, labsSplitBranching,
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsMagicDraw, setLabsPersonaYTCreator, setLabsSplitBranching,
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsSplitBranching,
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsSplitBranching,
} = useUXLabsStore();
return <>
<FormSwitchControl
title={<><YouTubeIcon color={labsPersonaYTCreator ? 'primary' : undefined} sx={{ mr: 0.25 }} /> YouTube Personas</>} description={labsPersonaYTCreator ? 'Creator Enabled' : 'Disabled'}
checked={labsPersonaYTCreator} onChange={setLabsPersonaYTCreator}
/>
<FormSwitchControl
title={<><FormatPaintIcon color={labsMagicDraw ? 'primary' : undefined} sx={{ mr: 0.25 }} />Assisted Draw</>} description={labsMagicDraw ? 'Enabled' : 'Disabled'}
checked={labsMagicDraw} onChange={setLabsMagicDraw}
/>
<FormSwitchControl
title={<><CallIcon color={labsCalling ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Voice Calls</>} description={labsCalling ? 'Call AGI' : 'Disabled'}
checked={labsCalling} onChange={setLabsCalling}
@@ -46,7 +34,7 @@ export function UxLabsSettings() {
/>}
<FormSwitchControl
title={<><VerticalSplitIcon color={labsSplitBranching ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Split Branching</>} description={labsSplitBranching ? 'Enabled' : 'Disabled'} disabled
title={<><VerticalSplitIcon color={labsSplitBranching ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Split Branching</>} description={labsSplitBranching ? 'Enabled' : 'Disabled'}
checked={labsSplitBranching} onChange={setLabsSplitBranching}
/>
@@ -58,7 +46,7 @@ export function UxLabsSettings() {
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
<FormLabelStart title='Graduated' />
<Typography level='body-xs'>
<Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Relative chat size · Text Tools · LLM Overheat
<Link href='https://github.com/enricoros/big-AGI/issues/282' target='_blank'>Persona Creator</Link> · <Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Imagine · Relative chat size · Text Tools · LLM Overheat
</Typography>
</FormControl>
+2 -2
View File
@@ -12,8 +12,8 @@ export const Brand = {
Common: (process.env.NODE_ENV === 'development' ? '[DEV] ' : '') + 'big-AGI',
},
Meta: {
Description: 'Leading open-source AI web interface to help you learn, think, and do. AI personas, superior privacy, advanced features, and fun UX.',
SiteName: 'big-AGI | Harnessing AI for You',
Description: 'Launch big-AGI to unlock the full potential of AI, with precise control over your data and models. Voice interface, AI personas, advanced features, and fun UX.',
SiteName: 'big-AGI | Precision AI for You',
ThemeColor: '#32383E',
TwitterSite: '@enricoros',
},

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