Compare commits

...

154 Commits

Author SHA1 Message Date
Enrico Ros 4a8a2b9c5d First user experience - highlight the need to configure models 2024-01-06 02:57:05 -08: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
140 changed files with 3913 additions and 1365 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.'
+15 -7
View File
@@ -23,15 +23,15 @@ 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
@@ -71,7 +71,7 @@ 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
### news.data.tsx
```markdown
I need the following from you:
@@ -82,6 +82,13 @@ 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
@@ -96,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.
```
+9
View File
@@ -21,6 +21,15 @@ 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.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)
+11 -2
View File
@@ -5,11 +5,20 @@ by release.
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
### 1.10.0 - Jan 2024
### 1.11.0 - Jan 2024
- milestone: [1.10.0](https://github.com/enricoros/big-agi/milestone/10)
- milestone: [1.11.0](https://github.com/enricoros/big-agi/milestone/11)
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
### What's New in 1.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)
+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!
+262 -113
View File
@@ -1,31 +1,32 @@
{
"name": "big-agi",
"version": "1.9.0",
"version": "1.10.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "big-agi",
"version": "1.9.0",
"version": "1.10.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.1",
"@mui/joy": "^5.0.0-beta.19",
"@mui/icons-material": "^5.15.2",
"@mui/joy": "^5.0.0-beta.20",
"@next/bundle-analyzer": "^14.0.4",
"@prisma/client": "^5.7.1",
"@sanity/diff-match-patch": "^3.1.1",
"@t3-oss/env-nextjs": "^0.7.1",
"@tanstack/react-query": "~4.36.1",
"@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",
@@ -35,6 +36,7 @@
"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",
@@ -42,18 +44,19 @@
"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.5",
"@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": "^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.7",
@@ -258,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"
},
@@ -352,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",
@@ -375,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",
@@ -599,14 +602,14 @@
}
},
"node_modules/@mui/base": {
"version": "5.0.0-beta.28",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.28.tgz",
"integrity": "sha512-KIoSc5sUFceeCaZTq5MQBapFzhHqMo4kj+4azWaCAjorduhcRQtN+BCgVHmo+gvEjix74bUfxwTqGifnu2fNTg==",
"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.1",
"@mui/utils": "^5.15.2",
"@popperjs/core": "^2.11.8",
"clsx": "^2.0.0",
"prop-types": "^15.8.1"
@@ -630,20 +633,20 @@
}
},
"node_modules/@mui/core-downloads-tracker": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.1.tgz",
"integrity": "sha512-y/nUEsWHyBzaKYp9zLtqJKrLod/zMNEWpMj488FuQY9QTmqBiyUhI2uh7PVaLqLewXRtdmG6JV0b6T5exyuYRw==",
"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.1",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.1.tgz",
"integrity": "sha512-VPJdBSyap6uOxCb5BLbWbkvd6aeJCp1pQZm8DcZBITCH0NOSv8Mz9c8Zvo8xr4Od7+xyWHUAgvRSL4047pL2WQ==",
"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"
@@ -664,16 +667,16 @@
}
},
"node_modules/@mui/joy": {
"version": "5.0.0-beta.19",
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.19.tgz",
"integrity": "sha512-L3tutkCsJ4pI20LCiQthnXKMIv6GCJqbjPmzCjLvIUwp8kNhgQzauWc4ToYHP7FzZSoDt4HjwY52mvKhVrtytw==",
"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.28",
"@mui/core-downloads-tracker": "^5.15.1",
"@mui/system": "^5.15.1",
"@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.1",
"@mui/utils": "^5.15.2",
"clsx": "^2.0.0",
"prop-types": "^15.8.1"
},
@@ -704,17 +707,17 @@
}
},
"node_modules/@mui/material": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.1.tgz",
"integrity": "sha512-WA5DVyvacxDakVyAhNqu/rRT28ppuuUFFw1bLpmRzrCJ4uw/zLTATcd4WB3YbB+7MdZNEGG/SJNWTDLEIyn3xQ==",
"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.28",
"@mui/core-downloads-tracker": "^5.15.1",
"@mui/system": "^5.15.1",
"@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.1",
"@mui/utils": "^5.15.2",
"@types/react-transition-group": "^4.4.10",
"clsx": "^2.0.0",
"csstype": "^3.1.2",
@@ -749,12 +752,12 @@
}
},
"node_modules/@mui/private-theming": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.1.tgz",
"integrity": "sha512-wTbzuy5KjSvCPE9UVJktWHJ0b/tD5biavY9wvF+OpYDLPpdXK52vc1hTDxSbdkHIFMkJExzrwO9GvpVAHZBnFQ==",
"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.1",
"@babel/runtime": "^7.23.6",
"@mui/utils": "^5.15.2",
"prop-types": "^15.8.1"
},
"engines": {
@@ -775,11 +778,11 @@
}
},
"node_modules/@mui/styled-engine": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.1.tgz",
"integrity": "sha512-7WDZTJLqGexWDjqE9oAgjU8ak6hEtUw2yQU7SIYID5kLVO2Nj/Wi/KicbLsXnTsJNvSqePIlUIWTBSXwWJCPZw==",
"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"
@@ -806,15 +809,15 @@
}
},
"node_modules/@mui/system": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.1.tgz",
"integrity": "sha512-LAnP0ls69rqW9eBgI29phIx/lppv+WDGI7b3EJN7VZIqw0RezA0GD7NRpV12BgEYJABEii6z5Q9B5tg7dsX0Iw==",
"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.1",
"@mui/styled-engine": "^5.15.1",
"@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.1",
"@mui/utils": "^5.15.2",
"clsx": "^2.0.0",
"csstype": "^3.1.2",
"prop-types": "^15.8.1"
@@ -858,11 +861,11 @@
}
},
"node_modules/@mui/utils": {
"version": "5.15.1",
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.1.tgz",
"integrity": "sha512-V1/d0E3Bju5YdB59HJf2G0tnHrFEvWLN+f8hAXp9+JSNy/LC2zKyqUfPPahflR6qsI681P8G9r4mEZte/SrrYA==",
"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"
@@ -1326,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",
@@ -1346,9 +1358,9 @@
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
},
"node_modules/@types/node": {
"version": "20.10.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
"integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
"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"
@@ -1386,15 +1398,24 @@
"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.18",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz",
@@ -1413,6 +1434,17 @@
"@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.7",
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.7.tgz",
@@ -1448,15 +1480,15 @@
"dev": true
},
"node_modules/@typescript-eslint/parser": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.15.0.tgz",
"integrity": "sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA==",
"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.15.0",
"@typescript-eslint/types": "6.15.0",
"@typescript-eslint/typescript-estree": "6.15.0",
"@typescript-eslint/visitor-keys": "6.15.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": {
@@ -1476,13 +1508,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz",
"integrity": "sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg==",
"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.15.0",
"@typescript-eslint/visitor-keys": "6.15.0"
"@typescript-eslint/types": "6.16.0",
"@typescript-eslint/visitor-keys": "6.16.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@@ -1493,9 +1525,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.15.0.tgz",
"integrity": "sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ==",
"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"
@@ -1506,16 +1538,17 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz",
"integrity": "sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew==",
"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.15.0",
"@typescript-eslint/visitor-keys": "6.15.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"
},
@@ -1532,13 +1565,37 @@
}
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz",
"integrity": "sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==",
"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.15.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": {
@@ -1562,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",
@@ -1569,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"
},
@@ -1979,9 +2042,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001571",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz",
"integrity": "sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==",
"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",
@@ -2087,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"
}
@@ -2201,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",
@@ -4560,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",
@@ -5810,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",
@@ -5821,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",
@@ -5875,6 +5974,35 @@
"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",
@@ -5927,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",
@@ -6600,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",
@@ -6647,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",
@@ -6969,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",
+14 -11
View File
@@ -1,6 +1,6 @@
{
"name": "big-agi",
"version": "1.9.0",
"version": "1.10.0",
"private": true,
"scripts": {
"dev": "next dev",
@@ -15,21 +15,22 @@
"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.1",
"@mui/joy": "^5.0.0-beta.19",
"@mui/icons-material": "^5.15.2",
"@mui/joy": "^5.0.0-beta.20",
"@next/bundle-analyzer": "^14.0.4",
"@prisma/client": "^5.7.1",
"@sanity/diff-match-patch": "^3.1.1",
"@t3-oss/env-nextjs": "^0.7.1",
"@tanstack/react-query": "~4.36.1",
"@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",
@@ -39,6 +40,7 @@
"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",
@@ -46,18 +48,19 @@
"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.5",
"@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": "^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.7",
+5
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,6 +11,8 @@ 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 { ProviderBackendAndNoSSR } from '~/common/providers/ProviderBackendAndNoSSR';
import { ProviderBootstrapLogic } from '~/common/providers/ProviderBootstrapLogic';
@@ -41,6 +45,7 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
</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>
);
+4 -1
View File
@@ -6,9 +6,12 @@ import { useRedirectToNewsOnUpdates } from '../src/apps/news/news.hooks';
import { withLayout } from '~/common/layout/withLayout';
export default function ChatPage() {
export default function IndexPage() {
// show the News page if there are unseen updates
useRedirectToNewsOnUpdates();
// 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 />);
}
+4 -6
View File
@@ -1,5 +1,4 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { Box, Typography } from '@mui/joy';
@@ -7,7 +6,7 @@ import { useModelsStore } from '~/modules/llms/store-llms';
import { InlineError } from '~/common/components/InlineError';
import { apiQuery } from '~/common/util/trpc.client';
import { navigateToIndex } from '~/common/app.routes';
import { navigateToIndex, useRouterQuery } from '~/common/app.routes';
import { themeBgApp } from '~/common/app.theme';
import { withLayout } from '~/common/layout/withLayout';
@@ -86,9 +85,8 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
*/
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 withLayout({ type: 'plain' }, <CallbackOpenRouterPage openRouterCode={openRouterCode as (string | undefined)} />);
return withLayout({ type: 'plain' }, <CallbackOpenRouterPage openRouterCode={code} />);
}
+5 -4
View File
@@ -1,14 +1,15 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { AppChatLink } from '../../../src/apps/link/AppChatLink';
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 withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppChatLink linkId={chatLinkId} />);
// external state
const { chatLinkId } = useRouterQuery<{ chatLinkId: string | undefined }>();
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppChatLink linkId={chatLinkId || ''} />);
}
+9 -8
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';
@@ -10,7 +9,7 @@ import { callBrowseFetchPage } from '~/modules/browse/browse.client';
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';
@@ -32,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);
@@ -44,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];
@@ -59,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
+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}>
+3 -7
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';
@@ -28,7 +27,7 @@ import { CallAvatar } from './components/CallAvatar';
import { CallButton } from './components/CallButton';
import { CallMessage } from './components/CallMessage';
import { CallStatus } from './components/CallStatus';
import { ROUTE_APP_CHAT } from '~/common/app.routes';
import { launchAppChat, ROUTE_APP_CHAT } from '~/common/app.routes';
function CallMenuItems(props: {
@@ -89,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);
@@ -178,9 +176,7 @@ export function CallUI(props: {
// command: close the call
case 'Goodbye.':
setStage('ended');
setTimeout(() => {
void routerPush(ROUTE_APP_CHAT);
}, 2000);
setTimeout(launchAppChat, 2000);
return;
// command: regenerate answer
case 'Retry.':
@@ -236,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;
+3 -3
View File
@@ -81,7 +81,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
const [recognitionOverride, setRecognitionOverride] = React.useState(false);
// external state
const { openPreferences } = useOptimaLayout();
const { openPreferencesTab } = useOptimaLayout();
const recognition = useCapabilityBrowserSpeechRecognition();
const synthesis = useCapabilityElevenLabs();
const chatIsEmpty = useChatStore(state => {
@@ -104,7 +104,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
const handleOverrideRecognition = () => setRecognitionOverride(true);
const handleConfigureElevenLabs = () => {
openPreferences(3);
openPreferencesTab(3);
};
const handleFinishButton = () => {
@@ -200,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>
+168 -141
View File
@@ -1,46 +1,45 @@
import * as React from 'react';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { Panel, PanelGroup } from 'react-resizable-panels';
import { Box, useTheme } from '@mui/joy';
import { useTheme } from '@mui/joy';
import { CmdRunBrowse } from '~/modules/browse/browse.client';
import { CmdRunReact } from '~/modules/aifn/react/react';
import { CmdRunT2I, useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
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 { 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 { 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 { 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
*/
@@ -64,6 +63,7 @@ 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();
@@ -90,6 +90,7 @@ export function AppChat() {
isChatEmpty: isFocusedChatEmpty,
areChatsEmpty,
newConversationId,
conversationsLength,
_remove_systemPurposeId: focusedSystemPurposeId,
prependNewConversation,
branchConversation,
@@ -100,10 +101,19 @@ export function AppChat() {
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 panesConversationIDs = chatPanes.length > 0 ? chatPanes.map(pane => pane.conversationId) : [null];
const panesConversationIDs = chatPanes.length > 0 ? chatPanes.map((pane) => pane.conversationId) : [null];
const isSplitPane = chatPanes.length > 1;
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
@@ -135,7 +145,6 @@ export function AppChat() {
}
}, [focusedChatNumber, focusedChatTitle]);
// Execution
const _handleExecute = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) => {
@@ -145,29 +154,36 @@ export function AppChat() {
// "/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 (CmdRunT2I.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]);
}
}
}
@@ -184,9 +200,10 @@ export function AppChat() {
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: `${CmdRunT2I[0]} ${lastMessage.text}`,
text: `/draw ${lastMessage.text}`,
}));
return await runImageGenerationUpdatingState(conversationId, lastMessage.text);
@@ -204,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({
@@ -263,17 +279,24 @@ export function AppChat() {
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' });
@@ -301,7 +324,6 @@ export function AppChat() {
const handleConversationFlatten = (conversationId: DConversationId) => setFlattenConversationId(conversationId);
const handleConfirmedClearConversation = React.useCallback(() => {
if (clearConversationId) {
setMessages(clearConversationId, []);
@@ -311,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);
@@ -326,13 +347,13 @@ 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
@@ -346,15 +367,14 @@ export function AppChat() {
['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(() =>
@@ -366,17 +386,20 @@ export function AppChat() {
[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(() =>
@@ -394,93 +417,88 @@ export function AppChat() {
[areChatsEmpty, focusedConversationId, handleConversationBranch, isFocusedChatEmpty, isMessageSelectionMode],
);
usePluggableOptimaLayout(drawerItems, centerItems, menuItems, 'AppChat');
usePluggableOptimaLayout(drawerContent, centerItems, menuItems, 'AppChat');
return <>
<PanelGroup direction='horizontal'>
<PanelGroup
direction='horizontal'
id='app-chat-panels'
>
{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={() => setFocusedPane(idx)}
onCollapse={() => setTimeout(() => removePane(idx), 50)}
style={{
// for anchoring the scroll button in place
position: 'relative',
// border only for active pane (if two or more panes)
...(panesConversationIDs.length < 2 ? {}
: (_conversationId === focusedConversationId)
? { border: `2px solid ${theme.palette.primary.solidBg}` }
: { border: `2px solid ${theme.palette.background.level1}` }),
}}
>
<ScrollToBottom
bootToBottom
stickToBottom
sx={{
// allows the content to be scrolled (all browsers)
overflowY: 'auto',
// actually make sure this scrolls & fills
height: '100%',
{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}` }),
}}
>
<ChatMessageList
conversationId={_conversationId}
capabilityHasT2I={capabilityHasT2I}
chatLLMContextTokens={chatLLM?.contextTokens}
isMessageSelectionMode={isMessageSelectionMode}
setIsMessageSelectionMode={setIsMessageSelectionMode}
onConversationBranch={handleConversationBranch}
onConversationExecuteHistory={handleConversationExecuteHistory}
onTextDiagram={handleTextDiagram}
onTextImagine={handleTextImagine}
onTextSpeak={handleTextSpeak}
<ScrollToBottom
bootToBottom
stickToBottom
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,
// allows the content to be scrolled (all browsers)
overflowY: 'auto',
minHeight: 64,
}} />
// actually make sure this scrolls & fills
height: '100%',
}}
>
{/* Visibility and actions are handled via Context */}
<ScrollToBottomButton />
<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
}}
/>
</ScrollToBottom>
<Ephemerals
conversationId={_conversationId}
sx={{
// TODO: Fixme post panels?
// flexGrow: 0.1,
flexShrink: 0.5,
overflowY: 'auto',
minHeight: 64,
}}
/>
</Panel>
{/* Visibility and actions are handled via Context */}
<ScrollToBottomButton />
</ScrollToBottom>
</Panel>
{/* Panel Separators & Resizers */}
{idx < panels.length - 1 && (
<PanelResizeHandle>
<Box sx={{
backgroundColor: themeBgApp,
height: '100%',
width: '4px',
'&:hover': {
backgroundColor: 'primary.softActiveBg',
},
}} />
</PanelResizeHandle>
)}
{/* Panel Separators & Resizers */}
{idx < panels.length - 1 && <GoodPanelResizeHandler />}
</React.Fragment>)}
</React.Fragment>)}
</PanelGroup>
@@ -498,8 +516,8 @@ export function AppChat() {
borderTop: `1px solid`,
borderTopColor: 'divider',
p: { xs: 1, md: 2 },
}} />
}}
/>
{/* Diagrams */}
{!!diagramConfig && <DiagramsModal config={diagramConfig} onClose={() => setDiagramConfig(null)} />}
@@ -514,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'}
/>}
</>;
}
+17
View File
@@ -0,0 +1,17 @@
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsAlter: ICommandsProvider = {
id: 'chat-alter',
rank: 20,
getCommands: () => [{
primary: '/assistant',
alternatives: ['/a'],
description: 'Injects assistant response',
}, {
primary: '/system',
alternatives: ['/s'],
description: 'Injects system message',
}],
};
+15
View File
@@ -0,0 +1,15 @@
import LanguageIcon from '@mui/icons-material/Language';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsBrowse: ICommandsProvider = {
id: 'ass-browse',
rank: 25,
getCommands: () => [{
primary: '/browse',
description: 'Assistant will download the web page',
Icon: LanguageIcon,
}],
};
+16
View File
@@ -0,0 +1,16 @@
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'],
description: 'Generate an image from text',
Icon: FormatPaintIcon,
}],
};
+14
View File
@@ -0,0 +1,14 @@
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsHelp: ICommandsProvider = {
id: 'cmd-help',
rank: 99,
getCommands: () => [{
primary: '/help',
alternatives: ['/?'],
noArgs: true,
description: 'Display this list of commands',
}],
};
+15
View File
@@ -0,0 +1,15 @@
import PsychologyIcon from '@mui/icons-material/Psychology';
import type { ICommandsProvider } from './ICommandsProvider';
export const CommandsReact: ICommandsProvider = {
id: 'ass-react',
rank: 15,
getCommands: () => [{
primary: '/react',
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
noArgs?: boolean; // Whether the command requires arguments
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.noArgs !== true) {
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 }];
}
+7 -7
View File
@@ -25,7 +25,7 @@ import { useScrollToBottom } from './scroll-to-bottom/useScrollToBottom';
export function ChatMessageList(props: {
conversationId: DConversationId | null,
capabilityHasT2I: boolean,
chatLLMContextTokens?: number,
chatLLMContextTokens: number | null,
isMessageSelectionMode: boolean, setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[]) => void,
@@ -42,7 +42,7 @@ export function ChatMessageList(props: {
// external state
const { notifyBooting } = useScrollToBottom();
const { openPreferences } = useOptimaLayout();
const { openPreferencesTab } = useOptimaLayout();
const [showSystemMessages] = useChatShowSystemMessages();
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
@@ -102,21 +102,21 @@ export function ChatMessageList(props: {
const handleTextImagine = React.useCallback(async (text: string) => {
if (!capabilityHasT2I)
return openPreferences(2);
return openPreferencesTab(2);
if (conversationId) {
setIsImagining(true);
await onTextImagine(conversationId, text);
setIsImagining(false);
}
}, [capabilityHasT2I, conversationId, onTextImagine, openPreferences]);
}, [capabilityHasT2I, conversationId, onTextImagine, openPreferencesTab]);
const handleTextSpeak = React.useCallback(async (text: string) => {
if (!isSpeakable)
return openPreferences(3);
return openPreferencesTab(3);
setIsSpeaking(true);
await onTextSpeak(text);
setIsSpeaking(false);
}, [isSpeakable, onTextSpeak, openPreferences]);
}, [isSpeakable, onTextSpeak, openPreferencesTab]);
// operate on the local selection set
@@ -209,7 +209,7 @@ export function ChatMessageList(props: {
<CleanerMessage
key={'sel-' + message.id}
message={message}
remainingTokens={(props.chatLLMContextTokens || 0) - historyTokenCount}
remainingTokens={props.chatLLMContextTokens ? (props.chatLLMContextTokens - historyTokenCount) : undefined}
selected={selectedMessages.has(message.id)} onToggleSelected={handleSelectMessage}
/>
+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,62 +1,117 @@
import * as React from 'react';
import { shallow } from 'zustand/shallow';
import { Box, ListDivider, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
import { Box, IconButton, ListDivider, ListItemDecorator, MenuItem, 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 { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
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 { 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, 'New Title'),
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 [grouping] = React.useState<ListGrouping>('off');
const { onConversationDelete, onConversationNew, onConversationActivate } = props;
// external state
const { closeAppDrawer } = useOptimaLayout();
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 maxChatMessages = chatNavItems.reduce((longest, _c) => Math.max(longest, _c.messageCount), 1);
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();
closeAppDrawer();
}, [closeAppDrawer, onConversationNew]);
closeDrawerOnMobile();
}, [closeDrawerOnMobile, onConversationNew]);
const handleConversationActivate = React.useCallback((conversationId: DConversationId, closeMenu: boolean) => {
onConversationActivate(conversationId);
if (closeMenu)
closeAppDrawer();
}, [closeAppDrawer, onConversationActivate]);
closeDrawerOnMobile();
}, [closeDrawerOnMobile, onConversationActivate]);
const handleConversationDelete = React.useCallback((conversationId: DConversationId) => {
!singleChat && conversationId && onConversationDelete(conversationId, true);
@@ -85,66 +140,105 @@ 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 */}
{useFolders && (
<ChatFolderList
folders={folders}
selectedFolderId={props.selectedFolderId}
onFolderSelect={props.setSelectedFolderId}
/>
)}
{/* Chats List */}
<PageDrawerList variant='plain' noTopPadding noBottomPadding tallRows>
{useFolders && <ListDivider sx={{ mb: 0 }} />}
<MenuItem 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>
</MenuItem>
{/*<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>*/}
{chatNavItems.map(item =>
<ChatDrawerItemMemo
key={'nav-' + item.conversationId}
item={item}
isLonely={singleChat}
maxChatMessages={(labsEnhancedUI || softMaxReached) ? maxChatMessages : 0}
showSymbols={showSymbols}
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>
<MenuItem onClick={props.onConversationImportDialog} sx={{ flex: 1 }}>
<ListItemDecorator>
<FileUploadIcon />
</ListItemDecorator>
Import
{/*<OpenAIIcon sx={{ ml: 'auto' }} />*/}
</MenuItem>
<ListDivider sx={{ mt: 0 }} />
<MenuItem disabled={!nonEmptyChats} onClick={() => props.onConversationExportDialog(props.activeConversationId)} sx={{ flex: 1, display: 'flex', justifyContent: 'flex-end', gap: 2.5 }}>
Export
<FileDownloadIcon />
</MenuItem>
</Box>
<MenuItem onClick={props.onConversationImportDialog}>
<ListItemDecorator>
<FileUploadIcon />
</ListItemDecorator>
Import chats
<OpenAIIcon sx={{ fontSize: 'xl', ml: 'auto' }} />
</MenuItem>
<MenuItem disabled={!nonEmptyChats} onClick={props.onConversationsDeleteAll}>
<ListItemDecorator>
<DeleteOutlineIcon />
</ListItemDecorator>
Delete {selectConversationsCount >= 2 ? `all ${selectConversationsCount} chats` : 'chat'}
</MenuItem>
<MenuItem disabled={!hasChats} onClick={props.onConversationsDeleteAll}>
<ListItemDecorator><DeleteOutlineIcon /></ListItemDecorator>
<Typography>
Delete {totalConversations >= 2 ? `all ${totalConversations} chats` : 'chat'}
</Typography>
</MenuItem>
</PageDrawerList>
</>;
}
@@ -8,6 +8,7 @@ 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: {
@@ -19,26 +20,29 @@ export function ChatDropdowns(props: {
// 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' : 'plain'}
color='neutral'
variant={props.isSplitPanes ? 'solid' : undefined}
onClick={props.onToggleSplitPanes}
sx={{
ml: 'auto',
}}
// 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 { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
import { useUICounter } from '~/common/state/store-ui';
import { useChatShowSystemMessages } from '../../store-app-chat';
@@ -30,7 +30,7 @@ export function ChatMenuItems(props: {
}) {
// external state
const { closeAppMenu } = useOptimaLayout();
const { closePageMenu } = useOptimaDrawers();
const { touch: shareTouch } = useUICounter('export-share');
const [showSystemMessages, setShowSystemMessages] = useChatShowSystemMessages();
@@ -40,7 +40,7 @@ export function ChatMenuItems(props: {
const closeMenu = (event: React.MouseEvent) => {
event.stopPropagation();
closeAppMenu();
closePageMenu();
};
const handleConversationClear = (event: React.MouseEvent<HTMLDivElement>) => {
@@ -117,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,25 +1,34 @@
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;
}
function ChatNavigationItem(props: {
conversation: DConversation,
isActive: boolean,
item: ChatNavigationItemData,
isLonely: boolean,
maxChatMessages: number,
showSymbols: boolean,
@@ -27,8 +36,6 @@ function ChatNavigationItem(props: {
onConversationDelete: (conversationId: DConversationId) => void,
}) {
const { conversation, isActive } = props;
// state
const [isEditingTitle, setIsEditingTitle] = React.useState(false);
const [deleteArmed, setDeleteArmed] = React.useState(false);
@@ -37,13 +44,8 @@ function ChatNavigationItem(props: {
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 } = 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
@@ -88,16 +90,15 @@ function ChatNavigationItem(props: {
const progress = props.maxChatMessages ? 100 * messageCount / props.maxChatMessages : 0;
return (
<MenuItem
variant={isActive ? 'solid' : 'plain'} color='neutral'
selected={isActive}
<ListItemButton
variant={isActive ? 'soft' : 'plain'} color='neutral'
onClick={handleConversationActivate}
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' } : {}),
}}
>
@@ -145,7 +146,6 @@ function ChatNavigationItem(props: {
{/* // TODO: Commented code */}
{/* Edit */}
{/*<IconButton*/}
{/* variant='plain' color='neutral'*/}
{/* onClick={() => props.onEditTitle(props.conversationId)}*/}
{/* sx={{*/}
{/* opacity: 0, transition: 'opacity 0.3s', ml: 'auto',*/}
@@ -156,9 +156,11 @@ function ChatNavigationItem(props: {
{/* Delete Arming */}
{!props.isLonely && !deleteArmed && (
<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>
)}
@@ -172,7 +174,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, MenuList, 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 }}>
<MenuList
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>
</MenuList>
<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, GoodDropdown } from '~/common/components/GoodDropdown';
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 (
<GoodDropdown
items={folderItems}
value={currentFolderId}
onChange={handleFolderChange}
placeholder='Select a folder'
showSymbols
/>
);
}, [currentFolderId, folderItems, handleFolderChange, useFolders]);
return { folderDropdown };
}
@@ -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>
+59 -43
View File
@@ -26,6 +26,7 @@ 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 { lineHeightTextarea } from '~/common/app.theme';
import { playSoundUrl } from '~/common/util/audioUtils';
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
import { useDebouncer } from '~/common/components/useDebouncer';
@@ -41,13 +42,13 @@ import { getTextBlockText, useLLMAttachments } from './attachments/useLLMAttachm
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';
@@ -89,7 +90,7 @@ export function Composer(props: {
// external state
const isMobile = useIsMobile();
const { openPreferences } = useOptimaLayout();
const { openPreferencesTab, setIsFocusedMode } = useOptimaLayout();
const { labsCalling, labsCameraDesktop } = useUXLabsStore(state => ({
labsCalling: state.labsCalling,
labsCameraDesktop: state.labsCameraDesktop,
@@ -98,10 +99,10 @@ export function Composer(props: {
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,
@@ -167,6 +168,31 @@ export function Composer(props: {
return enqueued;
}, [clearAttachments, conversationId, llmAttachments, onAction, setComposeText]);
const handleSendClicked = () => handleSendAction(chatModeId, composeText);
const handleStopClicked = () => props.conversationId && stopTyping(props.conversationId);
// Secondary buttons
const handleCallClicked = () => props.conversationId && systemPurposeId && launchAppCall(props.conversationId, systemPurposeId);
const handleDrawOptionsClicked = () => openPreferencesTab(2);
const handleTextImagineClicked = () => {
if (!composeText || !props.conversationId)
return;
props.onTextImagine(props.conversationId, composeText);
setComposeText('');
};
// Text actions
const handleTextAreaTextChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
setComposeText(e.target.value);
}, [setComposeText]);
const handleTextareaKeyDown = React.useCallback((e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
@@ -178,30 +204,12 @@ export function Composer(props: {
// Shift: toggles the 'enter is newline'
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
if (!assistantTyping)
if (!assistantAbortible)
handleSendAction(chatModeId, composeText);
return e.preventDefault();
}
}
}, [assistantTyping, chatModeId, composeText, enterIsNewline, handleSendAction]);
const handleSendClicked = () => handleSendAction(chatModeId, composeText);
const handleStopClicked = () => props.conversationId && stopTyping(props.conversationId);
// Secondary buttons
const handleCallClicked = () => props.conversationId && systemPurposeId && launchAppCall(props.conversationId, systemPurposeId);
const handleDrawOptionsClicked = () => openPreferences(2);
const handleTextImagineClicked = () => {
if (!composeText || !props.conversationId)
return;
props.onTextImagine(props.conversationId, composeText);
setComposeText('');
};
}, [assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction]);
// Mode menu
@@ -233,7 +241,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)
@@ -255,7 +263,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');
@@ -367,7 +375,9 @@ export function Composer(props: {
const isChat = isText || isAppend;
const isReAct = chatModeId === 'generate-react';
const isDraw = chatModeId === 'generate-image';
const buttonColor: ColorPaletteProp = isReAct ? 'success' : isDraw ? 'warning' : 'primary';
const buttonColor: ColorPaletteProp = assistantAbortible
? 'warning'
: isReAct ? 'success' : isDraw ? 'warning' : 'primary';
const textPlaceholder: string =
isDraw
@@ -390,7 +400,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} />}
@@ -406,7 +416,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*/}
@@ -428,7 +438,7 @@ export function Composer(props: {
<Box sx={{
flexGrow: 1,
display: 'flex', flexDirection: 'column', gap: 1,
minWidth: 250, // enable X-scrolling (resetting any possible minWidth due to the attachments)
minWidth: 200, // enable X-scrolling (resetting any possible minWidth due to the attachments)
}}>
{/* Edit box + Overlays + Mic buttons */}
@@ -440,14 +450,16 @@ export function Composer(props: {
<Textarea
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',
@@ -463,7 +475,7 @@ export function Composer(props: {
'&:focus-within': {
backgroundColor: 'background.popup',
},
lineHeight: 1.75,
lineHeight: lineHeightTextarea,
}} />
{tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
@@ -567,7 +579,7 @@ export function Composer(props: {
? <ButtonCall isMobile disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} sx={{ mr: { xs: 1, md: 2 } }} />
: 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 */}
@@ -579,7 +591,7 @@ export function Composer(props: {
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}
@@ -598,7 +610,7 @@ export function Composer(props: {
) : (
<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` }}
@@ -615,7 +627,11 @@ export function Composer(props: {
</Tooltip>}
{/* Mode expander */}
<IconButton variant={isDraw ? undefined : undefined} disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor} onClick={handleModeSelectorShow}>
<IconButton
variant={assistantAbortible ? 'soft' : isDraw ? undefined : undefined}
disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor}
onClick={handleModeSelectorShow}
>
<ExpandLessIcon />
</IconButton>
</ButtonGroup>
@@ -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>
) : (
@@ -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';
@@ -29,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';
@@ -68,17 +68,18 @@ 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={iconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
return <Face6Icon sx={avatarIconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
case 'assistant':
// typing gif (people seem to love this, so keeping it after april fools')
@@ -97,7 +98,7 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
// text-to-image: icon
if (isTextToImage)
return <FormatPaintIcon sx={{
...iconSx,
...avatarIconSx,
animation: `${cssRainbowColorKeyframes} 1s linear 2.66`,
}} />;
@@ -106,13 +107,15 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
if (symbol) return <Box sx={{
fontSize: '24px',
textAlign: 'center',
width: '100%', minWidth: `${iconSx.width}px`, lineHeight: `${iconSx.height}px`,
width: '100%',
minWidth: `${avatarIconSx.width}px`,
lineHeight: `${avatarIconSx.height}px`,
}}>
{symbol}
</Box>;
// default assistant avatar
return <SmartToyOutlinedIcon sx={iconSx} />; // https://mui.com/static/images/avatar/2.jpg
return <SmartToyOutlinedIcon sx={avatarIconSx} />; // https://mui.com/static/images/avatar/2.jpg
}
return <Avatar alt={messageSender} />;
}
@@ -147,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"}}
@@ -409,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)',
};
@@ -450,39 +458,51 @@ export function ChatMessage(props: {
>
{/* 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}
@@ -526,12 +546,12 @@ export function ChatMessage(props: {
: block.type === 'image'
? <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>
@@ -551,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',
@@ -28,7 +28,7 @@ export const MessagesSelectionHeader = (props: { hasSelected: boolean, sumTokens
Delete
</Button>
<IconButton variant='plain' onClick={props.onClose}>
<IconButton onClick={props.onClose}>
<ClearIcon />
</IconButton>
</Sheet>;
@@ -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>
@@ -125,13 +125,13 @@ export const RenderImage = (props: { imageBlock: ImageBlock, isFirst: boolean, a
<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>
)}
<Tooltip title='Open in new tab'>
<IconButton component={Link} href={url} target='_blank' variant='solid' color='neutral'>
<IconButton component={Link} href={url} download={alt || 'image'} target='_blank' variant='solid'>
<OpenInNewIcon />
</IconButton>
</Tooltip>
@@ -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) =>
@@ -7,6 +7,7 @@ import SearchIcon from '@mui/icons-material/Search';
import TelegramIcon from '@mui/icons-material/Telegram';
import { DConversationId, useChatStore } from '~/common/state/store-chats';
import { lineHeightTextarea } from '~/common/app.theme';
import { navigateToPersonas } from '~/common/app.routes';
import { useUIPreferencesStore } from '~/common/state/store-ui';
@@ -129,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>
)}
@@ -247,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' }}
>
@@ -268,7 +269,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
'&:focus-within': {
backgroundColor: 'background.popup',
},
lineHeight: 1.75,
lineHeight: lineHeightTextarea,
mt: 1,
}} />
)}
@@ -30,6 +30,8 @@ import { ScrollToBottomState, UseScrollToBottomProvider } from './useScrollToBot
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
@@ -50,6 +52,7 @@ function DebugBorderBox(props: { heightPx: number, color: string }) {
export function ScrollToBottom(props: {
bootToBottom?: boolean
bootSmoothly?: boolean
stickToBottom?: boolean
sx?: SxProps
children: React.ReactNode,
@@ -73,7 +76,7 @@ export function ScrollToBottom(props: {
// derived state
const bootToBottom = props.bootToBottom || false;
const scrollBehavior: ScrollBehavior = state.booting ? 'auto' : 'smooth';
const scrollBehavior: ScrollBehavior = (state.booting && !props.bootSmoothly) ? 'auto' : 'smooth';
// [Debugging]
@@ -132,11 +135,24 @@ export function ScrollToBottom(props: {
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();
@@ -26,7 +26,7 @@ export function ScrollToBottomButton() {
// </Typography>
// }>
<IconButton
variant='outlined' color='neutral' size='md'
variant='outlined'
onClick={handleStickToBottom}
sx={{
// place this on the bottom-right corner (FAB-like)
-57
View File
@@ -1,57 +0,0 @@
import { CmdRunBrowse } from '~/modules/browse/browse.client';
import { CmdRunReact } from '~/modules/aifn/react/react';
import { CmdRunSearch } from '~/modules/google/search.client';
import { CmdRunT2I } from '~/modules/t2i/t2i.client';
import { Brand } from '~/common/app.config';
import { createDMessage, DMessage } from '~/common/state/store-chats';
export const CmdAddRoleMessage: string[] = ['/assistant', '/a', '/system', '/s'];
export const CmdHelp: string[] = ['/help', '/h', '/?'];
export const commands = [...CmdRunBrowse, ...CmdRunT2I, ...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
@@ -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) {
+3 -3
View File
@@ -16,7 +16,7 @@ import { conversationTitle } from '~/common/state/store-chats';
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';
@@ -84,9 +84,9 @@ export function AppChatLink(props: { linkId: string }) {
// pluggable UI
const drawerItems = React.useMemo(() => <AppChatLinkDrawerItems />, []);
const drawerContent = React.useMemo(() => <AppChatLinkDrawerContent />, []);
const menuItems = React.useMemo(() => <AppChatLinkMenuItems />, []);
usePluggableOptimaLayout(hasLinkItems ? drawerItems : null, null, menuItems, 'AppChatLink');
usePluggableOptimaLayout(hasLinkItems ? drawerContent : null, null, menuItems, 'AppChatLink');
const pageTitle = (data?.conversation && conversationTitle(data.conversation)) || 'Chat Link';
@@ -9,26 +9,27 @@ import { useChatLinkItems } from '~/modules/trade/store-module-trade';
import { Brand } from '~/common/app.config';
import { Link } from '~/common/components/Link';
import { getChatLinkRelativePath, ROUTE_INDEX } from '~/common/app.routes';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
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 { closeAppDrawer } = useOptimaLayout();
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={closeAppDrawer}
onClick={closeDrawerOnMobile}
component={Link} href={ROUTE_INDEX} noLinkStyle
>
<ListItemDecorator><ArrowBackIcon /></ListItemDecorator>
@@ -64,6 +65,6 @@ export function AppChatLinkDrawerItems() {
))}
</Box>}
</>;
</PageDrawerList>;
}
+46 -32
View File
@@ -5,6 +5,7 @@ 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';
@@ -97,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 */}
+25 -13
View File
@@ -10,12 +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 { themeBgApp } from '~/common/app.theme';
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% {
@@ -28,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) */
}`;
@@ -88,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 &&*/ (
@@ -98,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>
+17 -3
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 = 10;
export const incrementalVersion: number = 11;
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>
@@ -59,10 +59,24 @@ export const newsCallout =
// news and feature surfaces
export const NewsItems: NewsItem[] = [
// still unannounced: phone calls, split windows, ...
{
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: '🎨🌌',
// 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 },
+37 -26
View File
@@ -6,10 +6,13 @@ import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility
import TextFieldsIcon from '@mui/icons-material/TextFields';
import YouTubeIcon from '@mui/icons-material/YouTube';
import { RenderMarkdown } from '../chat/components/message/RenderMarkdown';
import { GoodModal } from '~/common/components/GoodModal';
import { GoodTooltip } from '~/common/components/GoodTooltip';
import { apiQuery } from '~/common/util/trpc.client';
import { copyToClipboard } from '~/common/util/clipboardUtils';
import { lineHeightTextarea } from '~/common/app.theme';
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
import { LLMChainStep, useLLMChain } from './useLLMChain';
@@ -66,32 +69,42 @@ const PersonaCreationSteps: LLMChainStep[] = [
export function PersonaCreator() {
// state
const [selectedTab, setSelectedTab] = React.useState(0);
const [inputText, setInputText] = React.useState<string | null>(null);
const [videoURL, setVideoURL] = React.useState('');
const [videoID, setVideoID] = React.useState('');
const [personaTranscript, setPersonaTranscript] = React.useState<string | null>(null);
const [personaText, setPersonaText] = React.useState('');
const [selectedTab, setSelectedTab] = React.useState(0);
// external state
const [personaLlm, llmComponent] = useFormRadioLlmType('Persona Creation Model');
// chain to convert a text input string (e.g. youtube transcript) into a persona prompt
const savePersona = React.useCallback((personaPrompt: string) => {
// TODO.. save the persona prompt here
}, []);
const { isFinished, isTransforming, chainProgress, chainIntermediates, chainStepName, chainOutput, chainError, abortChain } =
useLLMChain(PersonaCreationSteps, personaLlm?.id, inputText ?? undefined, savePersona);
// 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]);
React.useEffect(() => setInputText(transcript), [transcript]);
// Reset the relevant state when the selected tab changes
React.useEffect(() => {
// reset state
setVideoURL('');
setVideoID('');
setPersonaTranscript(null);
setPersonaText('');
// reset state
setVideoURL('');
setVideoID('');
setInputText(null);
setPersonaText('');
}, [selectedTab]);
// use the transformation sequence to create a persona
const { isFinished, isTransforming, chainProgress, chainIntermediates, chainStepName, chainOutput, chainError, abortChain } =
useLLMChain(PersonaCreationSteps, personaLlm?.id, personaTranscript ?? undefined);
// [Tab: 0] Video download
const handleVideoIdChange = (e: React.ChangeEvent<HTMLInputElement>) => setVideoURL(e.target.value);
@@ -101,15 +114,15 @@ export function PersonaCreator() {
if (!videoId) {
setVideoURL('Invalid');
} else {
setPersonaTranscript(null);
setInputText(null);
setVideoID(videoId);
}
};
// New handler for persona text change
const handlePersonaTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setPersonaText(e.target.value);
};
// [Tab: 1] Text input
const handlePersonaTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => setPersonaText(e.target.value);
return <>
@@ -118,8 +131,8 @@ export function PersonaCreator() {
</Typography>
<Tabs defaultValue={0} variant='outlined'
value={selectedTab}
onChange={(event, newValue) => setSelectedTab(newValue as number)}>
value={selectedTab}
onChange={(_event, newValue) => setSelectedTab(newValue as number)}>
<TabList sx={{ minHeight: 48 }}>
<Tab>From YouTube Video</Tab>
<Tab>From Text</Tab>
@@ -174,12 +187,12 @@ export function PersonaCreator() {
'&:focus-within': {
backgroundColor: 'background.popup',
},
lineHeight: 1.75,
lineHeight: lineHeightTextarea,
mb: 1.5,
}}
/>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Button variant='solid' disabled={isFetching || isTransforming || !personaText} onClick={() => setPersonaTranscript(personaText)} sx={{ minWidth: 140 }}>
<Button variant='solid' disabled={isFetching || isTransforming || !personaText} onClick={() => setInputText(personaText)} sx={{ minWidth: 140 }}>
Create
</Button>
{!!personaText?.length && <Typography level='body-sm'>{personaText.length.toLocaleString()}</Typography>}
@@ -219,15 +232,13 @@ export function PersonaCreator() {
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
You may now copy the text below and use it as Custom prompt!
</Alert>
<Typography level='title-sm' sx={{ lineHeight: 1.75 }}>
{chainOutput}
</Typography>
<RenderMarkdown textBlock={{ type: 'text', content: chainOutput }} />
</CardContent>
</Card>
</>}
{/* Input: Transcript*/}
{personaTranscript && <>
{/* Input: Transcript/Text */}
{inputText && <>
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
Input Data
</Typography>
@@ -240,7 +251,7 @@ export function PersonaCreator() {
<Box>
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Thumbnail' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
<Typography level='body-sm'>
{personaTranscript.slice(0, 280)}...
{inputText.slice(0, 280)}...
</Typography>
</Box>
</CardContent>
+15 -10
View File
@@ -16,7 +16,7 @@ export interface LLMChainStep {
/**
* React hook to manage a chain of LLM transformations.
*/
export function useLLMChain(steps: LLMChainStep[], llmId: DLLMId | undefined, chainInput: string | undefined) {
export function useLLMChain(steps: LLMChainStep[], llmId: DLLMId | undefined, chainInput: string | undefined, onSuccess?: (output: string) => void) {
const [chain, setChain] = React.useState<ChainState | null>(null);
const [error, setError] = React.useState<string | null>(null);
const chainAbortController = React.useRef(new AbortController());
@@ -80,11 +80,15 @@ export function useLLMChain(steps: LLMChainStep[], llmId: DLLMId | undefined, ch
_chainAbortController.signal.addEventListener('abort', globalToStepListener);
// LLM call
llmChatGenerateOrThrow(llmId, llmChatInput, null, null, chain.overrideResponseTokens)
llmChatGenerateOrThrow(llmId, llmChatInput, null, null, chain.overrideResponseTokens ?? undefined)
.then(({ content }) => {
stepDone = true;
if (!stepAbortController.signal.aborted)
setChain(updateChainState(chain, llmChatInput, stepIdx, content));
if (stepAbortController.signal.aborted)
return;
const chainState = updateChainState(chain, llmChatInput, stepIdx, content);
if (chainState.output && onSuccess)
onSuccess(chainState.output);
setChain(chainState);
})
.catch((err) => {
stepDone = true;
@@ -121,8 +125,8 @@ interface ChainState {
steps: StepState[];
chatHistory: VChatMessageIn[];
progress: number;
safeInputLength: number;
overrideResponseTokens: number;
safeInputLength: number | null;
overrideResponseTokens: number | null;
input: string;
output: string | null;
}
@@ -142,8 +146,9 @@ function initChainState(llmId: DLLMId, input: string, steps: LLMChainStep[]): Ch
throw new Error(`LLM ${llmId} not found`);
const overrideResponseTokens = llm.maxOutputTokens;
const inputTokens = llm.contextTokens - overrideResponseTokens;
const safeInputLength = Math.floor(inputTokens * 2); // it's deemed around 4
const safeInputLength = (llm.contextTokens && overrideResponseTokens)
? Math.floor((llm.contextTokens - overrideResponseTokens) * 2)
: null;
return {
steps: steps.map((step, i) => ({
@@ -177,8 +182,8 @@ function updateChainState(chain: ChainState, history: VChatMessageIn[], stepIdx:
};
}
function implodeText(text: string, maxLength: number) {
if (text.length <= maxLength) return text;
function implodeText(text: string, maxLength: number | null) {
if (!maxLength || text.length <= maxLength) return text;
const halfLength = Math.floor(maxLength / 2);
return `${text.substring(0, halfLength)}\n...\n${text.substring(text.length - halfLength)}`;
}
+1 -1
View File
@@ -34,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}
/>
+180
View File
@@ -0,0 +1,180 @@
import type { FunctionComponent } from 'react';
import AccountTreeIcon from '@mui/icons-material/AccountTree';
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
import BuildCircleIcon from '@mui/icons-material/BuildCircle';
import CallIcon from '@mui/icons-material/Call';
import Diversity2Icon from '@mui/icons-material/Diversity2';
import EventNoteIcon from '@mui/icons-material/EventNote';
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
import GitHubIcon from '@mui/icons-material/GitHub';
import IosShareIcon from '@mui/icons-material/IosShare';
import SettingsIcon from '@mui/icons-material/Settings';
import TelegramIcon from '@mui/icons-material/Telegram';
import WorkspacesIcon from '@mui/icons-material/Workspaces';
import { Brand } from '~/common/app.config';
import { DiscordIcon } from '~/common/components/icons/DiscordIcon';
// enable to show all items, for layout development
const SHOW_ALL_APPS = false;
// Nav items
interface ItemBase {
name: string,
icon: FunctionComponent,
tooltip?: string,
}
export interface NavItemApp extends ItemBase {
type: 'app',
route: string,
drawer?: string | true, // true: can make use of the drawer, string: also set the title
hideBar?: boolean, // set to true to hide the page bar
hideNav?: boolean, // set to hide the Nav bar (note: must have a way to navigate back)
automatic?: boolean, // only accessible by the machine
fullWidth?: boolean, // set to true to override the user preference
hide?: boolean, // delete from the UI
}
export interface NavItemModal extends ItemBase {
type: 'modal',
overlayId: 'settings' | 'models',
}
export interface NavItemExtLink extends ItemBase {
type: 'extLink',
href: string,
}
// interface MenuItemAction extends ItemBase {
// type: 'action',
// action: () => void,
// }
export const navItems: {
apps: NavItemApp[]
modals: NavItemModal[]
links: NavItemExtLink[],
} = {
// User-chosen apps
apps: [
{
name: 'Chat',
icon: TelegramIcon,
type: 'app',
route: '/',
drawer: true,
},
{
name: 'Call',
icon: CallIcon,
type: 'app',
route: '/call',
drawer: 'Recent Calls',
automatic: true,
fullWidth: true,
},
{
name: 'Draw',
icon: FormatPaintIcon,
type: 'app',
route: '/draw',
hide: true,
},
{
name: 'Cortex',
icon: AutoAwesomeIcon,
type: 'app',
route: '/cortex',
automatic: true,
hide: true,
},
{
name: 'Patterns',
icon: AccountTreeIcon,
type: 'app',
route: '/patterns',
hide: true,
},
{
name: 'Workspace',
icon: WorkspacesIcon,
type: 'app',
route: '/workspace',
hide: true,
},
{
name: 'Personas',
icon: Diversity2Icon,
type: 'app',
route: '/personas',
hideBar: true,
},
{
name: 'News',
icon: EventNoteIcon,
type: 'app',
route: '/news',
hideBar: true,
},
// non-user-selectable ('automatic') Apps
{
name: 'Shared Chat',
icon: IosShareIcon,
type: 'app',
route: '/link/chat/[chatLinkId]',
drawer: 'Shared Chats',
automatic: true,
hideNav: true,
},
],
// Modals
modals: [
{
name: 'Manage Models',
icon: BuildCircleIcon,
type: 'modal',
overlayId: 'models',
},
{
name: 'Preferences',
icon: SettingsIcon,
type: 'modal',
overlayId: 'settings',
},
],
// External links
links: [
// {
// type: 'extLink',
// name: 'X',
// icon: TwitterIcon,
// href: 'https://twitter.com',
// },
{
type: 'extLink',
name: 'Discord',
icon: DiscordIcon,
href: Brand.URIs.SupportInvite,
},
{
type: 'extLink',
name: 'GitHub',
icon: GitHubIcon,
href: Brand.URIs.OpenRepo,
},
],
};
// apply UI filtering right away - do it here, once, and for all
navItems.apps = navItems.apps.filter(app => !app.hide || SHOW_ALL_APPS);
+11 -1
View File
@@ -4,7 +4,7 @@
// We will centralize them here, for UI and routing purposes.
//
import Router from 'next/router';
import Router, { useRouter } from 'next/router';
import type { DConversationId } from '~/common/state/store-chats';
import { isBrowser } from './util/pwaUtils';
@@ -35,6 +35,16 @@ export const getCallbackUrl = (source: 'openrouter') => {
export const getChatLinkRelativePath = (chatLinkId: string) => ROUTE_APP_LINK_CHAT.replace(':linkId', chatLinkId);
export function useRouterQuery<TQuery>(): TQuery {
const { query } = useRouter();
return query as TQuery;
}
export function useRouterRoute(): string {
const { route } = useRouter();
return route;
}
/// Simple Navigation
+10 -24
View File
@@ -69,37 +69,17 @@ export const appTheme = extendTheme({
},
background: {
// New
popup: '#24292c', // 3: #32383E, 1: #171A1C, 2: #25282B
surface: 'var(--joy-palette-neutral-800, #171A1C)',
level1: 'var(--joy-palette-neutral-900, #0B0D0E)',
level2: 'var(--joy-palette-neutral-800, #171A1C)',
body: 'var(--joy-palette-common-black, #000)',
// Former: surface [900] > level 1 [black], level 2 [800] > body [black]
body: '#060807',
// Former: popup > surface [900] > level 1 [black], level 2 [800] > body [black]
},
},
},
},
components: {
/**
* IconButton
* - enlarge 'md' a bit: https://github.com/mui/material-ui/commit/7f81475ea148a416ec8fab252120ce6567c62897#diff-45dca083057933d78377b59e031146804cfedb68fe1514955bc8a5b3c38d7c44
*/
JoyIconButton: {
styleOverrides: {
root: ({ ownerState }) => ({
...(ownerState.instanceSize && {
'--IconButton-size': { sm: '2rem', md: '2.5rem', lg: '3rem' }[ownerState.instanceSize],
}),
...(ownerState.size === 'md' && {
'--Icon-fontSize': 'calc(var(--IconButton-size, 2.5rem) / 1.667)',
'--CircularProgress-size': '24px',
'--CircularProgress-thickness': '3px',
minWidth: 'var(--IconButton-size, 2.5rem)',
minHeight: 'var(--IconButton-size, 2.5rem)',
}),
}),
},
},
/**
* Input
* - remove the box-shadow: https://github.com/mui/material-ui/commit/8d4728df8a66d710660af96ac7ff3f86d2d26382
@@ -146,7 +126,13 @@ export const themeBgApp = 'background.level1';
export const themeBgAppDarker = 'background.level2';
export const themeBgAppChatComposer = 'background.surface';
export const bodyFontClassName = inter.className;
export const lineHeightChatText = 1.75;
export const lineHeightTextarea = 1.75;
export const themeZIndexPageBar = 25;
export const themeZIndexDesktopDrawer = 26;
export const themeZIndexDesktopNav = 27;
export const themeBreakpoints = appTheme.breakpoints.values;
export const cssRainbowColorKeyframes = keyframes`
+1 -1
View File
@@ -75,7 +75,7 @@ export function CloseableMenu(props: {
onKeyDown={handleListKeyDown}
sx={{
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
'--ListItem-minHeight': props.dense ? '2.5rem' : '3rem',
'--ListItem-minHeight': props.dense ? '2.25rem' : '3rem',
'--ListItemDecorator-size': '2.75rem', // icon width
backgroundColor: 'background.popup',
boxShadow: 'md',
+6 -2
View File
@@ -80,8 +80,12 @@ export function GoodDropdown<TValue extends string>(props: {
return (
<Option key={'key-' + idx} value={key} sx={{ whiteSpace: 'nowrap' }}>
{props.showSymbols && <ListItemDecorator sx={{ fontSize: 'xl' }}>{item?.symbol + ' '}</ListItemDecorator>}
{props.showSymbols && !!item.icon && <ListItemDecorator>{item?.icon}</ListItemDecorator>}
{props.showSymbols && (!!item.icon
? <ListItemDecorator>{item?.icon}</ListItemDecorator>
: item?.symbol
? <ListItemDecorator sx={{ fontSize: 'xl' }}>{item?.symbol + ' '}</ListItemDecorator>
: null
)}
{item.title}
{/*{key === props.value && (*/}
{/* <IconButton variant='soft' onClick={() => alert('aa')} sx={{ ml: 'auto' }}>*/}
+14 -3
View File
@@ -1,12 +1,17 @@
import * as React from 'react';
import { Textarea } from '@mui/joy';
import { ColorPaletteProp, SxProps } from '@mui/joy/styles/types';
import { SxProps } from '@mui/joy/styles/types';
import { useUIPreferencesStore } from '~/common/state/store-ui';
export function InlineTextarea(props: { initialText: string, color?: ColorPaletteProp, onEdit: (text: string) => void, sx?: SxProps }) {
export function InlineTextarea(props: {
initialText: string, placeholder?: string,
onEdit: (text: string) => void,
onCancel?: () => void,
sx?: SxProps,
}) {
const [text, setText] = React.useState(props.initialText);
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
@@ -20,6 +25,9 @@ export function InlineTextarea(props: { initialText: string, color?: ColorPalett
e.preventDefault();
props.onEdit(text);
}
} else if (e.key === 'Escape') {
e.preventDefault();
props.onCancel?.();
}
};
@@ -27,7 +35,10 @@ export function InlineTextarea(props: { initialText: string, color?: ColorPalett
return (
<Textarea
variant='soft' color={props.color || 'warning'} autoFocus minRows={1}
variant='soft' color='warning'
autoFocus
minRows={1}
placeholder={props.placeholder}
value={text} onChange={handleEditTextChanged}
onKeyDown={handleEditKeyDown} onBlur={handleEditBlur}
slotProps={{
+1 -1
View File
@@ -23,7 +23,7 @@ export function Section(props: { title?: string; collapsible?: boolean, collapse
</FormLabel>
)}
{!!props.collapsible && !props.asLink && (
<IconButton size='md' variant='plain' color='neutral' onClick={() => setCollapsed(!collapsed)} sx={{ ml: 1 }}>
<IconButton onClick={() => setCollapsed(!collapsed)} sx={{ ml: 1 }}>
{!collapsed ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
</IconButton>
)}
+1 -1
View File
@@ -22,7 +22,7 @@ export function FormInputKey(props: {
const handleChange = (e: React.ChangeEvent) => props.onChange((e.target as HTMLInputElement).value);
const endDecorator = React.useMemo(() => !!props.value && !props.noKey && (
<IconButton variant='plain' color='neutral' onClick={() => setIsVisible(!isVisible)}>
<IconButton onClick={() => setIsVisible(!isVisible)}>
{isVisible ? <VisibilityIcon /> : <VisibilityOffIcon />}
</IconButton>
), [props.value, props.noKey, isVisible]);
@@ -1,28 +1,33 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { SvgIcon } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
export const LogoSquircle = (props: {
import { Brand } from '~/common/app.config';
import { capitalizeFirstLetter } from '~/common/util/textUtils';
export const AgiSquircleIcon = (props: {
sx?: SxProps
inverted?: boolean
}) =>
<SvgIcon
titleAccess='Application logo'
titleAccess={`${capitalizeFirstLetter(Brand.Title.Base)} logo mark`}
viewBox='0 0 6.3500006 6.3499996' width='24' height='24'
fill='currentColor'
stroke='none' strokeWidth={0.691986} strokeLinecap='round' strokeLinejoin='round'
{...props}
sx={props.sx}
>
<g transform='translate(51.117939,-42.425365)'>
<g transform='matrix(0.07058825,0,0,0.07058823,-47.509613,39.430634)'>
<rect
fill={props.inverted ? '#000000' : 'currentColor'}
width='89.958321'
height='89.958321'
x='-51.117939'
y='42.425365'
ry='20' />
<path
fill='#000000'
fill={props.inverted ? 'currentColor' : '#000000'}
fillOpacity={1}
d='m 4.0021675,49.836309 -5.2e-4,5.1e-4 c -2.16855,0.54197 -4.18593997,1.53104 -6.17895,2.51871 -1.76404,0.94102 -3.50957,1.96574 -4.97024,3.34864 -1.01158,0.95772 -1.10871,1.18722 -1.9327,2.2934 -1.3708395,2.32511 -1.8551195,4.78906 -1.7817995,7.46827 0.0135,0.49268 0.0709,0.98327 0.10645,1.47485 0.16185,1.68282 0.58029,3.32246 0.8893495,4.98006 0.21008,1.12671 0.202,1.19346 0.3514,2.34404 0.16811,1.66717 0.20579,3.37783 -0.27285,5.00331 -0.3570295,1.21244 -0.5419495,1.37085 -1.1849395,2.46755 -1.08741,1.46507 -2.46987,2.7206 -4.05712,3.62975 -0.6278,0.35961 -1.54734,0.75252 -2.21226,1.05007 -0.14695,0.0581 -1.32968,0.50363 -1.5658,0.66559 -0.2541,0.17428 -0.83933,1.09089 -0.49868,1.31 7.95779,5.11884 6.04665,4.96385 10.3476895,5.0121 0.0404,0.005 0.0678,0.008 0.10749,0.0129 0.32616,0.47435 0.62621,0.96314 0.85524,1.48931 0.37498,0.86147 0.50729,1.81227 0.6718,2.7373 0.28715,1.61456 0.45546,3.251761 0.54673,4.889111 0.20491,3.67585 0,11.0448 0,11.0448 0,0 -0.05519,9.95619 -0.08888,18.80712 h 8.17676 c 0.01797,-5.68374 0.03669,-12.27573 0.03669,-12.27573 v -7.44658 -5.50302 c 0,0 0.0305,-3.26895 0,-4.90358 -0.0285,-1.06103 -0.0599,-1.85635 -0.25373,-2.759521 -0.12533997,-0.58405 -0.33168997,-1.15279 -0.58238997,-1.69499 -0.32913,-0.7118 -0.76001,-1.37445 -1.19993,-2.02364 -0.43112003,-0.63621 -0.86978003,-1.27635 -1.40611003,-1.82677 -0.49313,-0.50609 -1.06346,-0.93626 -1.6459,-1.33635 -0.92028,-0.63214 -1.76336,-1.21583 -2.62258,-1.77663 1.65441,-1.03826 3.09438,-2.40552 4.23334,-3.98839 0.71381,-1.21567 0.92029,-1.40723 1.34255003,-2.73937 0.54445,-1.71766 0.58905,-3.53149 0.43822,-5.3113 -0.11668,-1.06503 -0.13236,-1.36869 -0.3142,-2.41433 -0.28433,-1.63494 -0.74152003,-3.23741 -0.94723003,-4.88704 -0.0502,-0.46371 -0.1235,-0.92552 -0.15089,-1.39113 -0.14629,-2.48733 0.20532,-4.71974 1.46813003,-6.89363 0.77186,-1.01913 0.85052,-1.21803 1.80814997,-2.09238 1.42824,-1.30403 3.1314,-2.25506 4.837439,-3.13934 1.92892,-0.92218 3.8955405,-1.89816 6.0301205,-2.22312 z' />
</g>
@@ -0,0 +1,13 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { SvgIcon } from '@mui/joy';
// missing from MUI, using Tabler for Discord
export function DiscordIcon(props: { sx?: SxProps }) {
return <SvgIcon viewBox='0 0 24 24' width='24' height='24' stroke='currentColor' fill='none' strokeLinecap='round' strokeLinejoin='round' {...props}>
<path stroke='none' d='M0 0h24v24H0z' fill='none'></path>
<path d='M14.983 3l.123 .006c2.014 .214 3.527 .672 4.966 1.673a1 1 0 0 1 .371 .488c1.876 5.315 2.373 9.987 1.451 12.28c-1.003 2.005 -2.606 3.553 -4.394 3.553c-.94 0 -2.257 -1.596 -2.777 -2.969l-.02 .005c.838 -.131 1.69 -.323 2.572 -.574a1 1 0 1 0 -.55 -1.924c-3.32 .95 -6.13 .95 -9.45 0a1 1 0 0 0 -.55 1.924c.725 .207 1.431 .373 2.126 .499l.444 .074c-.477 1.37 -1.695 2.965 -2.627 2.965c-1.743 0 -3.276 -1.555 -4.267 -3.644c-.841 -2.206 -.369 -6.868 1.414 -12.174a1 1 0 0 1 .358 -.49c1.392 -1.016 2.807 -1.475 4.717 -1.685a1 1 0 0 1 .938 .435l.063 .107l.652 1.288l.16 -.019c.877 -.09 1.718 -.09 2.595 0l.158 .019l.65 -1.287a1 1 0 0 1 .754 -.54l.123 -.01zm-5.983 6a2 2 0 0 0 -1.977 1.697l-.018 .154l-.005 .149l.005 .15a2 2 0 1 0 1.995 -2.15zm6 0a2 2 0 0 0 -1.977 1.697l-.018 .154l-.005 .149l.005 .15a2 2 0 1 0 1.995 -2.15z' strokeWidth='0' fill='currentColor'></path>
</SvgIcon>;
}
@@ -0,0 +1,15 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { SvgIcon } from '@mui/joy';
export function LMStudioIcon(props: { sx?: SxProps }) {
return <SvgIcon viewBox='0 0 24 24' width='24' height='24' stroke='none' fill='currentColor' {...props}>
<rect width='11' height='2' x='3' y='4' ry='1' />
<rect width='10.5' height='2' x='7.5' y='7' ry='1' />
<rect width='10.5' height='2' x='5' y='10' ry='1' />
<rect width='10.5' height='2' x='2' y='13' ry='1' />
<rect width='10.5' height='2' x='5' y='16' ry='1' />
<rect width='7' height='2' x='11.5' y='19' ry='1' />
</SvgIcon>;
}
@@ -0,0 +1,22 @@
import * as React from 'react';
import { PanelResizeHandle } from 'react-resizable-panels';
import { Box } from '@mui/joy';
import { themeBgApp } from '~/common/app.theme';
export function GoodPanelResizeHandler() {
return (
<PanelResizeHandle>
<Box sx={{
backgroundColor: themeBgApp,
height: '100%',
width: '4px',
'&:hover': {
backgroundColor: 'primary.softActiveBg',
},
}} />
</PanelResizeHandle>
);
}
+3 -1
View File
@@ -4,7 +4,9 @@ import { themeBreakpoints } from '../app.theme';
import { isBrowser } from '~/common/util/pwaUtils';
export const isMobileQuery = () => `(max-width: ${themeBreakpoints.md - 1}px)`;
const isMobileQuery = () => `(max-width: ${themeBreakpoints.md - 1}px)`;
export const getIsMobile = () => isBrowser ? window.matchMedia(isMobileQuery()).matches : false;
export const useIsMobile = (): boolean => useMatchMedia(isMobileQuery(), false);
@@ -3,13 +3,11 @@ import { useRouter } from 'next/router';
import { default as NProgress } from 'nprogress';
import 'nprogress/nprogress.css';
/**
* Not show the bar for very fast loads (with a delay), and for the same route
* NOTE: make sure that the applicatio is importing nprogress.css!
*/
export function NextRouterProgress(props: { color: string, delay?: number }) {
export function useNextLoadProgress(delay: number = 250) {
// external state
const router = useRouter();
@@ -22,19 +20,20 @@ export function NextRouterProgress(props: { color: string, delay?: number }) {
});
// timeout to not show the progress bar for very fast loads
let timeout: number;
let timeoutId: ReturnType<typeof setTimeout>;
const handleStop = () => {
clearTimeout(timeout);
clearTimeout(timeoutId);
NProgress.done();
};
const handleStart = (newRoute: string) => {
handleStop();
if (newRoute == router.route)
return;
timeout = window.setTimeout(
() => NProgress.start(),
props.delay === undefined ? 250 : props.delay,
);
if (newRoute === router.route) return;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
NProgress.start();
}, delay);
};
router.events.on('routeChangeStart', handleStart);
@@ -47,19 +46,5 @@ export function NextRouterProgress(props: { color: string, delay?: number }) {
router.events.off('routeChangeComplete', handleStop);
router.events.off('routeChangeError', handleStop);
};
}, [props.delay, router]);
return (
<style>
{`
#nprogress .bar {
height: 4px;
background: ${props.color};
}
#nprogress .peg {
box-shadow: 0 0 10px ${props.color}, 0 0 5px ${props.color};
}
`}
</style>
);
}, [delay, router]);
}
-165
View File
@@ -1,165 +0,0 @@
import * as React from 'react';
import { Box, IconButton, ListDivider, ListItemDecorator, MenuItem, Sheet, Typography, useColorScheme } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import MenuIcon from '@mui/icons-material/Menu';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import { Brand } from '~/common/app.config';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { Link } from '~/common/components/Link';
import { LogoSquircle } from '~/common/components/LogoSquircle';
import { ROUTE_INDEX } from '~/common/app.routes';
import { AppBarSwitcherItem } from './AppBarSwitcherItem';
import { useOptimaLayout } from './useOptimaLayout';
function AppBarTitle() {
return (
<Link href={ROUTE_INDEX}>
<LogoSquircle sx={{
width: 32,
height: 32,
color: 'white',
// filter: 'invert(1)',
}} />
<Typography sx={{
ml: { xs: 1, md: 2 },
color: 'white',
}}>
{Brand.Title.Base}
</Typography>
</Link>
);
}
function CommonMenuItems(props: { onClose: () => void }) {
// external state
const { openPreferences } = useOptimaLayout();
const { mode: colorMode, setMode: setColorMode } = useColorScheme();
const handleShowSettings = (event: React.MouseEvent) => {
event.stopPropagation();
openPreferences();
props.onClose();
};
const handleToggleDarkMode = (event: React.MouseEvent) => {
event.stopPropagation();
setColorMode(colorMode === 'dark' ? 'light' : 'dark');
};
return <>
{/*<MenuItem onClick={handleToggleDarkMode}>*/}
{/* <ListItemDecorator><DarkModeIcon /></ListItemDecorator>*/}
{/* Dark*/}
{/* <Switch checked={colorMode === 'dark'} onChange={handleToggleDarkMode} sx={{ ml: 'auto' }} />*/}
{/*</MenuItem>*/}
{/* Preferences |...| Dark Mode Toggle */}
{/*<Tooltip title={<KeyStroke combo='Ctrl + Shift + P' />}>*/}
<MenuItem onClick={handleShowSettings}>
<ListItemDecorator><SettingsOutlinedIcon /></ListItemDecorator>
Preferences
<IconButton
variant='outlined' color='neutral'
onClick={handleToggleDarkMode}
sx={{ ml: 'auto' }}
>
{colorMode !== 'dark' ? <DarkModeIcon /> : <LightModeIcon />}
</IconButton>
</MenuItem>
{/*</Tooltip>*/}
</>;
}
// type ContainedAppType = 'chat' | 'data' | 'news';
/**
* The top bar of the application, with pluggable Left and Right menus, and Center component
*/
export function AppBar(props: { sx?: SxProps }) {
// state
// const [value, setValue] = React.useState<ContainedAppType>('chat');
// external state
const {
appBarItems, appDrawerAnchor, appPaneContent, appMenuAnchor, appMenuItems,
closeAppMenu, closeAppDrawer,
setAppDrawerAnchor, setAppMenuAnchor,
} = useOptimaLayout();
const commonMenuItems = React.useMemo(() =>
<CommonMenuItems onClose={closeAppMenu} />, [closeAppMenu]);
return <>
{/* Top Bar */}
<Sheet
variant='solid' color='neutral' invertedColors
sx={{
p: 1,
display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center',
...(props.sx || {}),
}}>
{/* Drawer Anchor */}
{!appPaneContent ? (
<IconButton component={Link} href={ROUTE_INDEX} noLinkStyle variant='plain'>
<ArrowBackIcon />
</IconButton>
) : (
<IconButton disabled={!!appDrawerAnchor || !appPaneContent} variant='plain' onClick={event => setAppDrawerAnchor(event.currentTarget)}>
<MenuIcon />
</IconButton>
)}
{/* Center Items */}
<Box sx={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', my: 'auto' }}>
{!!appBarItems ? appBarItems : <AppBarTitle />}
</Box>
{/* Menu Anchor */}
<IconButton disabled={!!appMenuAnchor /*|| !appMenuItems*/} variant='plain' onClick={event => setAppMenuAnchor(event.currentTarget)}>
<MoreVertIcon />
</IconButton>
</Sheet>
{/* Drawer Menu */}
{!!appPaneContent && <CloseableMenu
maxHeightGapPx={56 + 24} sx={{ minWidth: 320 }}
open={!!appDrawerAnchor} anchorEl={appDrawerAnchor} onClose={closeAppDrawer}
placement='bottom-start'
>
{appPaneContent}
</CloseableMenu>}
{/* Menu Menu */}
<CloseableMenu
maxHeightGapPx={56 + 24} noBottomPadding noTopPadding sx={{ minWidth: 320 }}
open={!!appMenuAnchor} anchorEl={appMenuAnchor} onClose={closeAppMenu}
placement='bottom-end'
>
{commonMenuItems}
{!!appMenuItems && <ListDivider sx={{ mt: 0 }} />}
{!!appMenuItems && <Box sx={{ overflowY: 'auto' }}>{appMenuItems}</Box>}
{!!appMenuItems && <ListDivider sx={{ mb: 0 }} />}
<AppBarSwitcherItem />
{/*<AppBarSupportItem />*/}
</CloseableMenu>
</>;
}
@@ -1,58 +0,0 @@
import * as React from 'react';
import { Button, SvgIcon, Tooltip } from '@mui/joy';
import { SxProps } from '@mui/joy/styles/types';
// import GitHubIcon from '@mui/icons-material/GitHub';
// import { Brand } from '..//common/app.brand';
import { Link } from '~/common/components/Link';
import { cssRainbowColorKeyframes } from '~/common/app.theme';
// missing from MUI, using Tabler for Discord
export function DiscordIcon(props: { sx?: SxProps }) {
return <SvgIcon viewBox='0 0 24 24' width='24' height='24' stroke='currentColor' fill='none' strokeLinecap='round' strokeLinejoin='round' {...props}>
<path stroke='none' d='M0 0h24v24H0z' fill='none'></path>
<path d='M14.983 3l.123 .006c2.014 .214 3.527 .672 4.966 1.673a1 1 0 0 1 .371 .488c1.876 5.315 2.373 9.987 1.451 12.28c-1.003 2.005 -2.606 3.553 -4.394 3.553c-.94 0 -2.257 -1.596 -2.777 -2.969l-.02 .005c.838 -.131 1.69 -.323 2.572 -.574a1 1 0 1 0 -.55 -1.924c-3.32 .95 -6.13 .95 -9.45 0a1 1 0 0 0 -.55 1.924c.725 .207 1.431 .373 2.126 .499l.444 .074c-.477 1.37 -1.695 2.965 -2.627 2.965c-1.743 0 -3.276 -1.555 -4.267 -3.644c-.841 -2.206 -.369 -6.868 1.414 -12.174a1 1 0 0 1 .358 -.49c1.392 -1.016 2.807 -1.475 4.717 -1.685a1 1 0 0 1 .938 .435l.063 .107l.652 1.288l.16 -.019c.877 -.09 1.718 -.09 2.595 0l.158 .019l.65 -1.287a1 1 0 0 1 .754 -.54l.123 -.01zm-5.983 6a2 2 0 0 0 -1.977 1.697l-.018 .154l-.005 .149l.005 .15a2 2 0 1 0 1.995 -2.15zm6 0a2 2 0 0 0 -1.977 1.697l-.018 .154l-.005 .149l.005 .15a2 2 0 1 0 1.995 -2.15z' strokeWidth='0' fill='currentColor'></path>
</SvgIcon>;
}
export function BringTheLove(props: { text: string, link: string, icon?: React.JSX.Element, sx?: SxProps }) {
const [loved, setLoved] = React.useState(false);
const icon = loved ? '❤️' : props.icon ?? null; // '❤️' : '🤍';
return <Tooltip title={props.text}><Button
variant='solid' color='neutral' size='md'
component={Link} noLinkStyle href={props.link} target='_blank'
onClick={() => setLoved(true)}
// endDecorator={icon}
sx={{
background: 'transparent',
// '&:hover': { background: props.theme.palette.neutral.solidBg },
'&:hover': { animation: `${cssRainbowColorKeyframes} 5s linear infinite` },
...(props.sx ? props.sx : {}),
}}>
{/*{props.text || icon}*/}
{icon}
</Button></Tooltip>;
}
/*
export function AppBarSupportItem() {
const theme = useTheme();
const fadedColor = theme.palette.neutral.plainDisabledColor;
const iconColor = '';
return (
<ListItem
variant='solid' color='neutral'
sx={{
// background: theme.palette.neutral.solidActiveBg,
display: 'flex', flexDirection: 'row', gap: 1,
justifyContent: 'space-between',
}}>
<BringTheLove text={Brand.Title.Base} link={Brand.URIs.Home} sx={{ color: fadedColor }} />
<BringTheLove text='Discord' icon={<DiscordIcon sx={{ color: iconColor }} />} link={Brand.URIs.SupportInvite} />
<BringTheLove text='GitHub' icon={<GitHubIcon sx={{ color: iconColor }} />} link={Brand.URIs.OpenRepo} />
</ListItem>
);
}
*/
@@ -1,98 +0,0 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { Box, Button, ButtonGroup, ListItem } from '@mui/joy';
import GitHubIcon from '@mui/icons-material/GitHub';
import { Brand } from '~/common/app.config';
import { ROUTE_APP_CHAT, ROUTE_APP_NEWS } from '~/common/app.routes';
import { BringTheLove, DiscordIcon } from './AppBarSupportItem';
import { useOptimaLayout } from './useOptimaLayout';
// routes for the quick switcher menu item
type ContainedAppType = 'chat' | /*'data' |*/ 'news';
const AppItems: ContainedAppType[] = ['chat', 'news'];
const AppRouteMap: { [key in ContainedAppType]: { name: string, route: string } } = {
'chat': {
name: 'Chat',
route: ROUTE_APP_CHAT,
},
// 'data': {
// name: 'Data',
// route: '/data',
// },
'news': {
name: 'News',
route: ROUTE_APP_NEWS,
},
};
export function AppBarSwitcherItem() {
// external state
const { closeAppMenu } = useOptimaLayout();
const { route, push: routerPush } = useRouter();
// find the current ContainedAppType or null
const currentApp: ContainedAppType | null = AppItems.find(app => AppRouteMap[app].route == route) || null;
// switcher
const switchApp = (app: ContainedAppType) => {
if (currentApp !== app) {
closeAppMenu();
void routerPush(AppRouteMap[app].route);
}
};
return (
<ListItem
variant='solid' color='neutral'
sx={{
'--ListItem-minHeight': '52px',
gap: 1,
}}
>
{/* Group 1: Apps */}
<ButtonGroup
variant='solid'
sx={{
'--ButtonGroup-separatorSize': '0px',
'--ButtonGroup-connected': '0',
gap: 1,
}}
>
{AppItems.map((app: ContainedAppType) => (
<Button
key={'app-' + app}
size='sm' variant={app == currentApp ? 'soft' : 'solid'} color={app == currentApp ? 'neutral' : 'neutral'}
onClick={() => switchApp(app)}
>
{AppRouteMap[app].name}
</Button>
))}
</ButtonGroup>
<Box sx={{ flex: 1 }} />
{/* Group 2: Social Links */}
<ButtonGroup
variant='solid'
sx={{
'--ButtonGroup-separatorSize': '0px',
'--ButtonGroup-connected': '0',
gap: 0,
}}
>
<BringTheLove text='Discord' icon={<DiscordIcon />} link={Brand.URIs.SupportInvite} />
<BringTheLove text='GitHub' icon={<GitHubIcon />} link={Brand.URIs.OpenRepo} />
</ButtonGroup>
</ListItem>
);
}
+112
View File
@@ -0,0 +1,112 @@
import * as React from 'react';
import { Box, Sheet, styled } from '@mui/joy';
import type { NavItemApp } from '~/common/app.nav';
import { themeZIndexDesktopDrawer } from '~/common/app.theme';
import { PageDrawer } from './PageDrawer';
import { useOptimaDrawers } from './useOptimaDrawers';
import { useOptimaLayout } from './useOptimaLayout';
// set to 0 to always keep the drawer mounted (smoother on/off)
const UNMOUNT_DELAY_MS = 0;
// Desktop Drawer
const DesktopDrawerFixRoot = styled(Box)({
// fix the drawer size
width: 'var(--AGI-Desktop-Drawer-width)',
flexShrink: 0,
flexGrow: 0,
});
const DesktopDrawerTranslatingSheet = styled(Sheet)(({ theme }) => ({
// layouting
width: '100%',
height: '100dvh',
// sliding
transition: 'transform 0.42s cubic-bezier(.17,.84,.44,1)',
zIndex: themeZIndexDesktopDrawer,
// styling
boxShadow: theme.shadow.md,
// content layout
display: 'flex',
flexDirection: 'column',
}));
export function DesktopDrawer(props: { currentApp?: NavItemApp }) {
// external state
const { isDrawerOpen, closeDrawer, openDrawer } = useOptimaDrawers();
const { appPaneContent } = useOptimaLayout();
// local state
const [softDrawerUnmount, setSoftDrawerUnmount] = React.useState(false);
// 'soft unmount': remove contents after a delay
React.useEffect(() => {
if (!UNMOUNT_DELAY_MS)
return;
// drawer open: do not unmount
if (isDrawerOpen) {
setSoftDrawerUnmount(false);
return;
}
// drawer closed: delayed unmount
const unmountTimeoutId = setTimeout(() =>
setSoftDrawerUnmount(true)
, UNMOUNT_DELAY_MS);
return () => clearTimeout(unmountTimeoutId);
}, [isDrawerOpen]);
// Desktop-only?: close the drawer if the current app doesn't use it
const currentAppUsesDrawer = !!props.currentApp?.drawer;
React.useEffect(() => {
if (!currentAppUsesDrawer)
closeDrawer();
}, [closeDrawer, currentAppUsesDrawer]);
// [special case] remove in the future
const shallOpenNavForSharedLink = !!props.currentApp?.drawer && !!props.currentApp?.hideNav;
React.useEffect(() => {
if (shallOpenNavForSharedLink)
openDrawer();
}, [openDrawer, shallOpenNavForSharedLink]);
return (
<DesktopDrawerFixRoot
sx={{
contain: isDrawerOpen ? undefined : 'strict',
}}
>
<DesktopDrawerTranslatingSheet
sx={{
transform: isDrawerOpen ? 'none' : 'translateX(-100%)',
}}
>
{/* [UX Responsiveness] Keep Mounted for now */}
{(!softDrawerUnmount || isDrawerOpen || !UNMOUNT_DELAY_MS) && (
<PageDrawer currentApp={props.currentApp} onClose={closeDrawer}>
{appPaneContent}
</PageDrawer>
)}
</DesktopDrawerTranslatingSheet>
</DesktopDrawerFixRoot>
);
}
+206
View File
@@ -0,0 +1,206 @@
import * as React from 'react';
import Router from 'next/router';
import { Box, IconButton, styled, Tooltip } from '@mui/joy';
import MenuIcon from '@mui/icons-material/Menu';
import { useModelsStore } from '~/modules/llms/store-llms';
import { AgiSquircleIcon } from '~/common/components/icons/AgiSquircleIcon';
import { NavItemApp, navItems } from '~/common/app.nav';
import { cssRainbowColorKeyframes, themeZIndexDesktopNav } from '~/common/app.theme';
import { InvertedBar, InvertedBarCornerItem } from './components/InvertedBar';
import { useOptimaDrawers } from './useOptimaDrawers';
import { useOptimaLayout } from './useOptimaLayout';
import { BringTheLove } from '~/common/layout/optima/components/BringTheLove';
// Nav Group
const DesktopNavGroupButton = styled(Box)({
// flex column
display: 'flex',
flexDirection: 'column',
flexWrap: 'wrap',
justifyContent: 'center',
alignItems: 'center',
// nav items, reduce the marginBlock a little
'--GroupMarginY': '0.125rem',
// style
// backgroundColor: 'rgba(0 0 0 / 0.5)',
// borderRadius: '1rem',
// paddingBlock: '0.5rem',
// overflow: 'hidden',
});
// Nav Item
const navItemClasses = {
active: 'NavButton-active',
paneOpen: 'NavButton-paneOpen',
attractive: 'NavButton-attractive',
};
const DesktopNavItem = styled(IconButton)(({ theme }) => ({
// --Bar is defined in InvertedBar
'--MarginX': '0.25rem',
// IconButton customization: the objective is to have a square button, with a smaller group margin,
// and with the nice little animation on pane open and hover
'--IconButton-size': 'calc(var(--Bar) - 2 * var(--MarginX))',
'--Icon-fontSize': '1.5rem',
// border: '1px solid red',
borderRadius: 'calc(var(--IconButton-size) / 2)',
marginBlock: 'var(--GroupMarginY)',
//marginInline: .. not needd because we center the items
padding: 0,
transition: 'border-radius 0.4s, margin 0.2s, padding 0.2s',
[`&:hover`]: {
// backgroundColor: theme.palette.primary.softHoverBg,
},
// pane open: show a connected half
[`&.${navItemClasses.paneOpen}`]: {
// squircle animation
borderStartEndRadius: 0,
borderEndEndRadius: 0,
marginLeft: 'calc(2 * var(--MarginX))',
paddingRight: 'calc(2 * var(--MarginX))',
},
[`&.${navItemClasses.paneOpen}:hover`]: {
borderRadius: 'calc(var(--IconButton-size) / 2)',
marginLeft: 0,
paddingRight: 0,
},
// attractive: attract the user to click on this element
[`&.${navItemClasses.attractive}`]: {
animation: `${cssRainbowColorKeyframes} 5s infinite`,
transform: 'scale(1.4)',
},
}));
export function DesktopNav(props: { currentApp?: NavItemApp }) {
// external state
const {
isDrawerOpen, toggleDrawer,
} = useOptimaDrawers();
const {
showPreferencesTab, openPreferencesTab,
showModelsSetup, openModelsSetup,
} = useOptimaLayout();
const noLLMs = useModelsStore(state => !state.llms.length);
// show/hide the pane when clicking on the logo
const appUsesPane = !!props.currentApp?.drawer;
const logoButtonTogglesPane = (appUsesPane && !isDrawerOpen) || isDrawerOpen;
const handleLogoButtonClick = React.useCallback(() => {
if (logoButtonTogglesPane)
toggleDrawer();
}, [logoButtonTogglesPane, toggleDrawer]);
// App items
const navAppItems = React.useMemo(() => {
return navItems.apps.filter(app => !app.hideNav /* .automatic */).map(item => {
const isActive = item === props.currentApp;
const isPanelable = isActive && !!item.drawer;
const isPaneOpen = isPanelable && isDrawerOpen;
const isNotForUser = !!item.automatic && !isActive;
return (
<Tooltip disableInteractive enterDelay={600} key={'n-m-' + item.route.slice(1)} title={item.name}>
<DesktopNavItem
disabled={isNotForUser}
variant={isActive ? 'soft' : undefined}
onClick={isPanelable ? toggleDrawer : () => Router.push(item.route)}
className={`${isActive ? navItemClasses.active : ''} ${isPaneOpen ? navItemClasses.paneOpen : ''}`}
>
<item.icon />
</DesktopNavItem>
</Tooltip>
);
});
}, [props.currentApp, isDrawerOpen, toggleDrawer]);
// External link items
const navExtLinkItems = React.useMemo(() => {
return navItems.links.map((item, index) =>
<BringTheLove
key={'nav-ext-' + item.name}
asIcon
text={item.name}
icon={item.icon}
link={item.href}
sx={{
p: 1,
mb: index > 0 ? 1 : 0,
}}
/>,
);
}, []);
// Modal items
const navModalItems = React.useMemo(() => {
return navItems.modals.map(item => {
// map the overlayId to the corresponding state and action
const stateActionMap: { [key: string]: { isActive: boolean, showModal: () => void } } = {
settings: { isActive: !!showPreferencesTab, showModal: () => openPreferencesTab() },
models: { isActive: showModelsSetup, showModal: openModelsSetup },
0: { isActive: false, showModal: () => console.log('Action missing for ', item.overlayId) },
};
const { isActive, showModal } = stateActionMap[item.overlayId] ?? stateActionMap[0];
// attract the attention to the models configuration when no LLMs are available (a bit hardcoded here)
const isAttractive = noLLMs && item.overlayId === 'models';
return (
<Tooltip followCursor key={'n-m-' + item.overlayId} title={isAttractive ? 'Add Language Models - REQUIRED' : item.name}>
<DesktopNavItem
variant={isActive ? 'soft' : undefined}
onClick={showModal}
className={`${isActive ? navItemClasses.active : ''} ${isAttractive ? navItemClasses.attractive : ''}`}
>
<item.icon />
</DesktopNavItem>
</Tooltip>
);
});
}, [noLLMs, openModelsSetup, openPreferencesTab, showModelsSetup, showPreferencesTab]);
return (
<InvertedBar id='desktop-nav' direction='vertical' sx={{ zIndex: themeZIndexDesktopNav }}>
<InvertedBarCornerItem>
<Tooltip title={isDrawerOpen ? 'Close' : 'Open Drawer'}>
<DesktopNavItem disabled={!logoButtonTogglesPane} onClick={handleLogoButtonClick}>
{logoButtonTogglesPane ? <MenuIcon /> : <AgiSquircleIcon inverted sx={{ color: 'white' }} />}
</DesktopNavItem>
</Tooltip>
</InvertedBarCornerItem>
<DesktopNavGroupButton>
{navAppItems}
</DesktopNavGroupButton>
<DesktopNavGroupButton>
{navExtLinkItems}
{navModalItems}
</DesktopNavGroupButton>
</InvertedBar>
);
}
+38
View File
@@ -0,0 +1,38 @@
import * as React from 'react';
import { Drawer } from '@mui/joy';
import type { NavItemApp } from '~/common/app.nav';
import { PageDrawer } from './PageDrawer';
import { useOptimaDrawers } from './useOptimaDrawers';
import { useOptimaLayout } from './useOptimaLayout';
export function MobileDrawer(props: { currentApp?: NavItemApp }) {
// external state
const { appPaneContent } = useOptimaLayout();
const { isDrawerOpen, closeDrawer } = useOptimaDrawers();
return (
<Drawer
open={isDrawerOpen}
onClose={closeDrawer}
sx={{
'--Drawer-horizontalSize': 'clamp(var(--AGI-Drawer-width), 30%, 100%)',
'--Drawer-transitionDuration': '0.2s',
// '& .MuiDrawer-paper': {
// width: 256,
// boxSizing: 'border-box',
// },
}}
>
<PageDrawer currentApp={props.currentApp} onClose={closeDrawer}>
{appPaneContent}
</PageDrawer>
</Drawer>
);
}
+37
View File
@@ -0,0 +1,37 @@
import * as React from 'react';
import { Typography } from '@mui/joy';
import type { NavItemApp } from '~/common/app.nav';
import { InvertedBar, InvertedBarCornerItem } from './components/InvertedBar';
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
export function MobileNav(props: { currentApp?: NavItemApp, hideOnFocusMode?: boolean }) {
// external state
const { isFocusedMode } = useOptimaLayout();
// NOTE: this may be abrupt a little
if (isFocusedMode && props.hideOnFocusMode)
return null;
return (
<InvertedBar
id='mobile-nav' direction='horizontal'
sx={{
justifyContent: 'space-around',
}}
>
<InvertedBarCornerItem sx={{ width: 'auto' }}>
<Typography level='title-sm'>
Chat
</Typography>
</InvertedBarCornerItem>
<Typography>
FIXME: MobileNav
</Typography>
</InvertedBar>
);
}
@@ -0,0 +1,59 @@
import * as React from 'react';
import Router from 'next/router';
import { Button, ButtonGroup, ListItem } from '@mui/joy';
import { NavItemApp, navItems } from '~/common/app.nav';
import { BringTheLove } from './components/BringTheLove';
export function MobileNavListItem(props: { currentApp?: NavItemApp }) {
return (
<ListItem
variant='solid'
sx={{
'--ListItem-minHeight': 'var(--AGI-Nav-width)',
gap: 1,
}}
>
{/* Group 1: Apps */}
<ButtonGroup
variant='solid'
sx={{
'--ButtonGroup-separatorSize': 0,
'--ButtonGroup-connected': 0,
gap: 1,
}}
>
{navItems.apps.filter(app => ['Chat', 'News'].includes(app.name)).map(app =>
<Button
key={'app-' + app.name}
disabled={!!app.automatic}
size='sm'
variant={app == props.currentApp ? 'soft' : 'solid'}
onClick={() => Router.push(app.route)}
>
{app.name}
</Button>,
)}
</ButtonGroup>
{/* Group 2: Social Links */}
<ButtonGroup
variant='solid'
sx={{
'--ButtonGroup-separatorSize': 0,
'--ButtonGroup-connected': 0,
ml: 'auto',
}}
>
{navItems.links.map(item =>
<BringTheLove key={'love-' + item.name} text={item.name} icon={item.icon} link={item.href} />,
)}
</ButtonGroup>
</ListItem>
);
}
+29
View File
@@ -0,0 +1,29 @@
import * as React from 'react';
import { ModelsModal } from '~/modules/llms/models-modal/ModelsModal';
import { SettingsModal } from '../../../apps/settings-modal/SettingsModal';
import { ShortcutsModal } from '../../../apps/settings-modal/ShortcutsModal';
import { useOptimaLayout } from './useOptimaLayout';
export function Modals(props: { suspendAutoModelsSetup?: boolean }) {
// external state
const {
showPreferencesTab, closePreferences,
showShortcuts, openShortcuts, closeShortcuts,
} = useOptimaLayout();
return <>
{/* Overlay Settings */}
<SettingsModal open={!!showPreferencesTab} tabIndex={showPreferencesTab} onClose={closePreferences} onOpenShortcuts={openShortcuts} />
{/* Overlay Models + LLM Options */}
<ModelsModal suspendAutoModelsSetup={props.suspendAutoModelsSetup} />
{/* Overlay Shortcuts */}
{showShortcuts && <ShortcutsModal onClose={closeShortcuts} />}
</>;
}
+39 -92
View File
@@ -1,43 +1,17 @@
import * as React from 'react';
import { useRouter } from 'next/router';
import { PanelGroup } from 'react-resizable-panels';
import { Box, Container } from '@mui/joy';
import { ModelsModal } from '~/modules/llms/models-modal/ModelsModal';
import { SettingsModal } from '../../../apps/settings-modal/SettingsModal';
import { ShortcutsModal } from '../../../apps/settings-modal/ShortcutsModal';
import { isPwa } from '~/common/util/pwaUtils';
import { navItems } from '~/common/app.nav';
import { useIsMobile } from '~/common/components/useMatchMedia';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { AppBar } from './AppBar';
import { NextRouterProgress } from './NextLoadProgress';
import { useOptimaLayout } from './useOptimaLayout';
/*function ResponsiveNavigation() {
return <>
<Drawer
open={false}
variant='solid'
anchor='left'
onClose={() => {
}}
sx={{
'& .MuiDrawer-paper': {
width: 256,
boxSizing: 'border-box',
},
}}
>
<Box sx={{ width: 256, height: '100%' }}>
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', height: '100%' }}>
<Box sx={{ flexGrow: 1 }} />
</Box>
</Box>
</Drawer>
</>;
}*/
import { DesktopDrawer } from './DesktopDrawer';
import { DesktopNav } from './DesktopNav';
import { MobileDrawer } from './MobileDrawer';
import { Modals } from './Modals';
import { OptimaDrawerProvider } from './useOptimaDrawers';
import { OptimaLayoutProvider } from './useOptimaLayout';
import { PageContainer } from './PageContainer';
/**
@@ -51,77 +25,50 @@ import { useOptimaLayout } from './useOptimaLayout';
* - overlays and displays various modals
* - flicker free
*/
export function OptimaLayout(props: { suspendAutoModelsSetup?: boolean, children: React.ReactNode, }) {
export function OptimaLayout(props: { suspendAutoModelsSetup?: boolean, children: React.ReactNode }) {
// external state
const { route } = useRouter();
const isMobile = useIsMobile();
let centerMode = useUIPreferencesStore(state => (isPwa() || isMobile) ? 'full' : state.centerMode);
// derived state
const currentApp = navItems.apps.find(item => item.route === route);
const {
closePreferences, closeShortcuts,
openShortcuts,
showPreferencesTab, showShortcuts,
} = useOptimaLayout();
return (
<OptimaLayoutProvider>
<OptimaDrawerProvider>
return <>
{isMobile ? <>
{/*<Box sx={{*/}
{/* display: 'flex', flexDirection: 'row',*/}
{/* maxWidth: '100%', flexWrap: 'nowrap',*/}
{/* // overflowX: 'hidden',*/}
{/* background: 'lime',*/}
{/*}}>*/}
<PageContainer isMobile currentApp={currentApp}>
{props.children}
</PageContainer>
{/*<Box sx={{ background: 'rgba(100 0 0 / 0.5)' }}>a</Box>*/}
<MobileDrawer currentApp={currentApp} />
{/*<ResponsiveNavigation />*/}
</> : (
<Container
disableGutters
maxWidth={centerMode === 'full' ? false : centerMode === 'narrow' ? 'md' : 'xl'}
sx={{
// minWidth: 0,
boxShadow: {
xs: 'none',
md: centerMode === 'narrow' ? 'md' : 'none',
xl: centerMode !== 'full' ? 'lg' : 'none',
},
}}>
<PanelGroup direction='horizontal' id='desktop-layout'>
<Box sx={{
display: 'flex', flexDirection: 'column',
height: '100dvh',
}}>
{!currentApp?.hideNav && <DesktopNav currentApp={currentApp} />}
<AppBar sx={{
zIndex: 20,
}} />
<DesktopDrawer currentApp={currentApp} />
{/* Children must make the assumption they're in a flex-col layout */}
{props.children}
{/*<Panel defaultSize={100}>*/}
<PageContainer currentApp={currentApp}>
{props.children}
</PageContainer>
{/*</Panel>*/}
</Box>
</PanelGroup>
</Container>
)}
{/*<Box sx={{ background: 'rgba(100 0 0 / 0.5)' }}>bb</Box>*/}
</OptimaDrawerProvider>
{/*</Box>*/}
{/* Overlay Modals */}
<Modals suspendAutoModelsSetup={props.suspendAutoModelsSetup} />
{/* Overlay Settings */}
<SettingsModal open={!!showPreferencesTab} tabIndex={showPreferencesTab} onClose={closePreferences} onOpenShortcuts={openShortcuts} />
{/* Overlay Models + LLM Options */}
<ModelsModal suspendAutoModelsSetup={props.suspendAutoModelsSetup} />
{/* Overlay Shortcuts */}
{showShortcuts && <ShortcutsModal onClose={closeShortcuts} />}
{/* Route loading progress overlay */}
<NextRouterProgress color='var(--joy-palette-neutral-700, #32383E)' />
</>;
}
</OptimaLayoutProvider>
);
}
+191
View File
@@ -0,0 +1,191 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Box, IconButton, ListDivider, ListItemDecorator, MenuItem, Typography, useColorScheme } from '@mui/joy';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import DarkModeIcon from '@mui/icons-material/DarkMode';
import LightModeIcon from '@mui/icons-material/LightMode';
import MenuIcon from '@mui/icons-material/Menu';
import MoreVertIcon from '@mui/icons-material/MoreVert';
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
import type { NavItemApp } from '~/common/app.nav';
import { AgiSquircleIcon } from '~/common/components/icons/AgiSquircleIcon';
import { Brand } from '~/common/app.config';
import { CloseableMenu } from '~/common/components/CloseableMenu';
import { Link } from '~/common/components/Link';
import { ROUTE_INDEX } from '~/common/app.routes';
import { InvertedBar, InvertedBarCornerItem } from './components/InvertedBar';
import { MobileNavListItem } from './MobileNavListItem';
import { useOptimaDrawers } from './useOptimaDrawers';
import { useOptimaLayout } from './useOptimaLayout';
function PageBarItemsFallback() {
return (
<Link href={ROUTE_INDEX}>
<AgiSquircleIcon inverted sx={{
width: 32,
height: 32,
color: 'white',
}} />
<Typography sx={{
ml: { xs: 1, md: 2 },
color: 'white',
textDecoration: 'none',
}}>
{Brand.Title.Base}
</Typography>
</Link>
);
}
function CommonMenuItems(props: { onClose: () => void }) {
// external state
const { openPreferencesTab } = useOptimaLayout();
const { mode: colorMode, setMode: setColorMode } = useColorScheme();
const handleShowSettings = (event: React.MouseEvent) => {
event.stopPropagation();
openPreferencesTab();
props.onClose();
};
const handleToggleDarkMode = (event: React.MouseEvent) => {
event.stopPropagation();
setColorMode(colorMode === 'dark' ? 'light' : 'dark');
};
return <>
{/*<MenuItem onClick={handleToggleDarkMode}>*/}
{/* <ListItemDecorator><DarkModeIcon /></ListItemDecorator>*/}
{/* Dark*/}
{/* <Switch checked={colorMode === 'dark'} onChange={handleToggleDarkMode} sx={{ ml: 'auto' }} />*/}
{/*</MenuItem>*/}
{/* Preferences |...| Dark Mode Toggle */}
{/*<Tooltip title={<KeyStroke combo='Ctrl + Shift + P' />}>*/}
<MenuItem onClick={handleShowSettings}>
<ListItemDecorator><SettingsOutlinedIcon /></ListItemDecorator>
Preferences
<IconButton
variant='outlined'
onClick={handleToggleDarkMode}
sx={{ ml: 'auto' }}
>
{colorMode !== 'dark' ? <DarkModeIcon /> : <LightModeIcon />}
</IconButton>
</MenuItem>
{/*</Tooltip>*/}
</>;
}
// type ContainedAppType = 'chat' | 'data' | 'news';
/**
* The top bar of the application, with pluggable Left and Right menus, and Center component
*/
export function PageBar(props: { currentApp?: NavItemApp, isMobile?: boolean, sx?: SxProps }) {
// state
// const [value, setValue] = React.useState<ContainedAppType>('chat');
const pageMenuAnchor = React.useRef<HTMLButtonElement>(null);
// external state
const {
appBarItems, appPaneContent, appMenuItems,
} = useOptimaLayout();
const {
openDrawer,
isPageMenuOpen, openPageMenu, closePageMenu,
} = useOptimaDrawers();
const commonMenuItems = React.useMemo(() => {
return <CommonMenuItems onClose={closePageMenu} />;
}, [closePageMenu]);
// [Desktop] hide the app bar if the current app doesn't use it
const desktopHide = !!props.currentApp?.hideBar && !props.isMobile;
if (desktopHide)
return null;
return <>
{/* This will animate the height from 0 to auto (and the bar is overflow:hidden */}
{/* But we're not using it yet as a NextJS page transition is a full removal */}
{/*<Box sx={{*/}
{/* display: 'grid',*/}
{/* gridTemplateRows: desktopHide ? '0fr' : '1fr',*/}
{/* transition: 'grid-template-rows 1.42s linear',*/}
{/*}}>*/}
<InvertedBar direction='horizontal' sx={props.sx}>
{/* [Mobile] Drawer button */}
{(!!props.isMobile || props.currentApp?.hideNav) && (
<InvertedBarCornerItem>
{(!appPaneContent || props.currentApp?.hideNav) ? (
<IconButton component={Link} href={ROUTE_INDEX} noLinkStyle>
<ArrowBackIcon />
</IconButton>
) : (
<IconButton disabled={!appPaneContent} onClick={openDrawer}>
<MenuIcon />
</IconButton>
)}
</InvertedBarCornerItem>
)}
{/* Center Items */}
<Box sx={{
flexGrow: 1,
minHeight: 'var(--Bar)',
display: 'flex', flexFlow: 'row wrap', justifyContent: 'center', alignItems: 'center',
my: 'auto',
}}>
{desktopHide ? null : !!appBarItems ? appBarItems : <PageBarItemsFallback />}
</Box>
{/* Page Menu Anchor */}
<InvertedBarCornerItem>
<IconButton disabled={!pageMenuAnchor || (!appMenuItems && !props.isMobile)} onClick={openPageMenu} ref={pageMenuAnchor}>
<MoreVertIcon />
</IconButton>
</InvertedBarCornerItem>
</InvertedBar>
{/*</Box>*/}
{/* Page Menu */}
<CloseableMenu
maxHeightGapPx={56 + 24} noBottomPadding noTopPadding sx={{ minWidth: 320 }}
open={isPageMenuOpen && !!pageMenuAnchor.current} anchorEl={pageMenuAnchor.current} onClose={closePageMenu}
placement='bottom-end'
>
{/* Common (Preferences) */}
{commonMenuItems}
{/* App Menu Items */}
{!!appMenuItems && <ListDivider sx={{ mt: 0 }} />}
{!!appMenuItems && <Box sx={{ overflowY: 'auto' }}>{appMenuItems}</Box>}
{/* [Mobile] Nav is implemented at the bottom of the Page Menu (for now) */}
{!!props.isMobile && !!appMenuItems && <ListDivider sx={{ mb: 0 }} />}
{!!props.isMobile && <MobileNavListItem currentApp={props.currentApp} />}
</CloseableMenu>
</>;
}
@@ -0,0 +1,98 @@
import * as React from 'react';
import { Box, Container } from '@mui/joy';
import type { NavItemApp } from '~/common/app.nav';
import { isPwa } from '~/common/util/pwaUtils';
import { themeZIndexPageBar } from '~/common/app.theme';
import { useUIPreferencesStore } from '~/common/state/store-ui';
import { PageBar } from './PageBar';
import { useOptimaDrawers } from './useOptimaDrawers';
const PageCore = (props: { currentApp?: NavItemApp, isMobile?: boolean, children: React.ReactNode }) =>
<Box sx={{
display: 'flex', flexDirection: 'column',
height: '100dvh',
}}>
{/* Responsive page bar (pluggable App Center Items and App Menu) */}
<PageBar
currentApp={props.currentApp}
isMobile={props.isMobile}
sx={{
zIndex: themeZIndexPageBar,
}}
/>
{/* Page (NextJS) must make the assumption they're in a flex-col layout */}
{props.children}
{/* [Mobile] Nav bar at the bottom */}
{/* FIXME: TEMP: Disable mobilenav */}
{/*{props.isMobile && <MobileNav hideOnFocusMode currentApp={props.currentApp} />}*/}
</Box>;
/**
* Loaded Application component, fromt the NextJS page router, wrapped in a Container for centering.
*/
export function PageContainer(props: { currentApp?: NavItemApp, isMobile?: boolean, children: React.ReactNode }) {
// external state
const { isDrawerOpen } = useOptimaDrawers();
const amplitude = useUIPreferencesStore(state =>
(isPwa() || props.isMobile || props.currentApp?.fullWidth) ? 'full' : state.centerMode,
);
// mobile: no outer containers
if (props.isMobile)
return (
<PageCore isMobile currentApp={props.currentApp}>
{props.children}
</PageCore>
);
return (
// This wrapper widens the Container/PageCore when the drawer is closed
<Box
sx={{
// full width (this is to the right of the fixed-size desktop drawer)
flex: '1 1 0px',
overflow: 'hidden',
// when the drawer is off, compensate with a negative margin
// NOTE: this will cause a transition on the container as well, meaning when we
// resize the window, the contents will wobble slightly
marginLeft: !isDrawerOpen
? 'calc(-1 * var(--AGI-Desktop-Drawer-width))'
: 0,
transition: 'margin-left 0.42s cubic-bezier(.17,.84,.44,1)',
willChange: 'margin-left',
}}
>
<Container
disableGutters
maxWidth={amplitude === 'full' ? false : amplitude === 'narrow' ? 'md' : 'xl'}
sx={{
boxShadow: {
xs: 'none',
md: amplitude === 'narrow' ? 'md' : 'none',
xl: amplitude !== 'full' ? 'lg' : 'none',
},
}}
>
<PageCore currentApp={props.currentApp}>
{props.children}
</PageCore>
</Container>
</Box>
);
}
+26
View File
@@ -0,0 +1,26 @@
import * as React from 'react';
import type { NavItemApp } from '~/common/app.nav';
import { PageDrawerHeader } from './components/PageDrawerHeader';
export function PageDrawer(props: {
currentApp?: NavItemApp,
onClose: () => void,
children?: React.ReactNode,
}) {
// derived state
const drawerTitle = typeof props.currentApp?.drawer === 'string' ? props.currentApp.drawer : false;
return <>
{/* Drawer Header */}
{drawerTitle && <PageDrawerHeader title={drawerTitle} onClose={props.onClose} />}
{/* Pluggable Drawer Content */}
{props.children}
</>;
}
@@ -0,0 +1,55 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { Button, IconButton, Tooltip } from '@mui/joy';
import { Link } from '~/common/components/Link';
import { cssRainbowColorKeyframes } from '~/common/app.theme';
export function BringTheLove(props: { text: string, link: string, asIcon?: boolean, icon: React.FC, sx?: SxProps }) {
// state
const [loved, setLoved] = React.useState(false);
const icon = loved ? '❤️' : <props.icon /> ?? null; // '❤️' : '🤍';
// reset loved after 5 seconds
React.useEffect(() => {
if (loved) {
const timer = setTimeout(() => setLoved(false), 5000);
return () => clearTimeout(timer);
}
}, [loved]);
return (
<Tooltip followCursor title={props.text}>
{props.asIcon ? (
<IconButton
variant='solid'
size='sm'
onClick={() => setLoved(true)}
component={Link} href={props.link} target='_blank' noLinkStyle
sx={{
'&:hover': { animation: `${cssRainbowColorKeyframes} 5s linear infinite` },
background: 'transparent',
textDecoration: 'none',
...props.sx,
}}
>
{icon}
</IconButton>
) : (
<Button
onClick={() => setLoved(true)}
component={Link} href={props.link} target='_blank' noLinkStyle
sx={{
'&:hover': { animation: `${cssRainbowColorKeyframes} 5s linear infinite` },
background: 'transparent',
...props.sx,
}}
>
{icon}
</Button>
)}
</Tooltip>
);
}
@@ -0,0 +1,61 @@
import * as React from 'react';
import type { SxProps, VariantProp } from '@mui/joy/styles/types';
import { Box, Sheet, styled, useTheme } from '@mui/joy';
export const InvertedBarCornerItem = styled(Box)({
width: 'var(--Bar)',
height: 'var(--Bar)',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
const InvertedBarBase = styled(Sheet)({
// customization
'--Bar': 'var(--AGI-Nav-width)',
// layout
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
});
// This is the AppBar and the MobileAppNav and DesktopNav
export const InvertedBar = (props: {
id?: string,
direction: 'horizontal' | 'vertical',
variant?: VariantProp,
sx?: SxProps
children: React.ReactNode,
}) => {
// check for dark mode
const theme = useTheme();
const isDark = theme?.palette.mode === 'dark';
const variant = isDark ? 'soft' : props.variant || 'solid';
return <InvertedBarBase
id={props.id}
variant={variant}
invertedColors={variant === 'solid' ? true : undefined}
sx={
props.direction === 'horizontal'
? {
// minHeight: 'var(--Bar)',
flexDirection: 'row',
// overflow: 'hidden',
...props.sx,
} : {
// minWidth: 'var(--Bar)',
flexDirection: 'column',
...props.sx,
}
}
>
{props.children}
</InvertedBarBase>;
};
@@ -0,0 +1,45 @@
import * as React from 'react';
import type { SxProps } from '@mui/joy/styles/types';
import { IconButton, Sheet, Typography } from '@mui/joy';
import CloseIcon from '@mui/icons-material/Close';
export const PageDrawerHeader = (props: {
title: string,
onClose: () => void,
startButton?: React.ReactNode,
sx?: SxProps
}) =>
<Sheet
variant='outlined'
// invertedColors
sx={{
minHeight: 'var(--AGI-Nav-width)',
// content
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
px: 1,
// style
borderTop: 'none',
borderLeft: 'none',
borderRight: 'none',
}}
>
{props.startButton
? props.startButton
: <IconButton disabled />}
<Typography level='title-md'>
{props.title}
</Typography>
<IconButton onClick={props.onClose}>
<CloseIcon />
</IconButton>
</Sheet>;

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