mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-11 06:00:15 -07:00
Compare commits
215 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ff4857b9ac | |||
| 5b557705e7 | |||
| cd70c4dd84 | |||
| 9eb2ef05de | |||
| 8fae15d343 | |||
| bca5a1ac78 | |||
| d899fb7e3b | |||
| 0f05b70e3b | |||
| 7b121a3a95 | |||
| d4e414f99c | |||
| a7f322ef38 | |||
| d4494bf2e0 | |||
| 78cf74e3f2 | |||
| cfaed03603 | |||
| a8e3183733 | |||
| 9395db0fd5 | |||
| 8c75061178 | |||
| de0cdded87 | |||
| d225541da2 | |||
| 7a0008de5a | |||
| 0bdd817d6d | |||
| d606975584 | |||
| af56c2c1af | |||
| 73de7df0fb | |||
| 3ca80d6a6e | |||
| eb9e5362fe | |||
| 45d1ca7437 | |||
| e492ccfb04 | |||
| d01b6acd51 | |||
| eec81d5d73 | |||
| 03423ce58c | |||
| e2e7ea972d | |||
| 91b770d2c8 | |||
| 79500e6d8b | |||
| 4ede66cf2b | |||
| 40bff32442 | |||
| 3fc8e8efa0 | |||
| 12ea5f218d | |||
| d47c0e45af | |||
| 298d0201d2 | |||
| a6bde2377e | |||
| 76778c5ab7 | |||
| 11565f5ac8 | |||
| 6c5131996b | |||
| 9b4301cd90 | |||
| c73bbaf0d4 | |||
| 163257e052 | |||
| cf689ca9a9 | |||
| 4a65389b71 | |||
| 5de7762238 | |||
| 06655ced46 | |||
| 60a775b869 | |||
| 5a3645bd43 | |||
| 54d37e663a | |||
| f4c056fa9f | |||
| 8f53fa7407 | |||
| 2f9a4ea00f | |||
| ee7dae827e | |||
| 6fe94e344a | |||
| 3376867966 | |||
| 4a8a2b9c5d | |||
| 7f84160a62 | |||
| fb5b349866 | |||
| f5c7b96ff6 | |||
| 7c430cc5c8 | |||
| 8c7d069189 | |||
| f50d040d8a | |||
| aa10f87c7d | |||
| 4e96a5b5e5 | |||
| 329456f287 | |||
| 6f8368d7cb | |||
| 9c2b0cb7ca | |||
| 1e15c4c4d1 | |||
| 9f209526a0 | |||
| 60ab9bd239 | |||
| 70e51b2e71 | |||
| 2d6edde12c | |||
| d2fb0c2425 | |||
| 122bbf0034 | |||
| e79449b38c | |||
| fcad6495e1 | |||
| 330d35a24c | |||
| a8ec58c732 | |||
| 8054c8b328 | |||
| 7d6f2317e4 | |||
| 10dd83bb2b | |||
| 7bf285f26a | |||
| fde7a8cd9b | |||
| 49ae5abba5 | |||
| f50ae4e7e2 | |||
| 99ff5cd7ad | |||
| f80facb191 | |||
| ea8d2fff3e | |||
| e3f1a5c54d | |||
| fdafc1207b | |||
| 5d3971c21f | |||
| f8a4002a41 | |||
| 38a3eeef21 | |||
| bf54807fb2 | |||
| 1aaabec28f | |||
| 8ec3927f02 | |||
| 73f201b8ac | |||
| 0b61c9a49e | |||
| ee82911d8f | |||
| 89fa3fe633 | |||
| da56db7502 | |||
| 1d0f99a9a5 | |||
| 8254443d29 | |||
| e1d6536102 | |||
| c9fbbc1ab1 | |||
| ae2e9b8f56 | |||
| 64ca896ea7 | |||
| 9bed685fe2 | |||
| 9432084342 | |||
| 0b7ffd16ab | |||
| 3437888bf4 | |||
| 9b02be8861 | |||
| 953d8434c3 | |||
| f9484ee3e9 | |||
| 4a3956d743 | |||
| 785139e7bc | |||
| d45fbff28d | |||
| fce6ecaf5f | |||
| 847d199dd8 | |||
| 274525a727 | |||
| 4d807ecf5c | |||
| 37a25f0117 | |||
| 7d5ab95c20 | |||
| 7fe8dd776f | |||
| 0a85d8d104 | |||
| cfd563b200 | |||
| 311a8d0ba0 | |||
| 06cd386c6e | |||
| 2632133ba4 | |||
| 1fe43cdc2e | |||
| e76939fb5d | |||
| 5f4250e3d2 | |||
| 5653044b1e | |||
| d4da34561d | |||
| fa25e830d5 | |||
| c90139923c | |||
| fa5007cb3b | |||
| b979e1313c | |||
| 1f1bf65c14 | |||
| 2bc6a15256 | |||
| dbcdbaa893 | |||
| d0ac1d8e1a | |||
| 3929e501d8 | |||
| fa3ae7b821 | |||
| 79052f988c | |||
| 18e6e235f3 | |||
| 388e897466 | |||
| e05a3bc3e9 | |||
| 5bb832f83d | |||
| 43cb19df83 | |||
| 1d770ce012 | |||
| 550e3e0173 | |||
| 043a5f48e8 | |||
| 0b69e0a9d1 | |||
| 5d8d752693 | |||
| e7067ed4fb | |||
| d181e27555 | |||
| 47d8b220a3 | |||
| cc5e310174 | |||
| 8006f578cd | |||
| a303bf7224 | |||
| dc0ca6d5bc | |||
| 2db3917c1c | |||
| 0c2ae290b0 | |||
| 24dcfeb952 | |||
| acd7a24cff | |||
| 88c29cf32c | |||
| 26f472b396 | |||
| 68c5e0b940 | |||
| 03fca40b74 | |||
| 35aff7798e | |||
| 6a8cf08ef0 | |||
| 53a9f9acef | |||
| d4c02dde1d | |||
| 660fda8485 | |||
| 049dfec794 | |||
| 2e6f1939dc | |||
| f3b1e4698a | |||
| 34e0102d82 | |||
| 3f5aed6f9b | |||
| 29647ad106 | |||
| 9426a45b88 | |||
| 5b52544c6c | |||
| fc1c15ba87 | |||
| e973fce3f7 | |||
| 99759654f2 | |||
| 390a1effb1 | |||
| f357291560 | |||
| c3a8b7e859 | |||
| 8931544349 | |||
| 865e420e34 | |||
| 574c2b936e | |||
| 4f6a596cc7 | |||
| edd36ea780 | |||
| 5a325b98ee | |||
| 8f6e2a3b5f | |||
| cf2fc96107 | |||
| 8837a1fc65 | |||
| 91970f088e | |||
| f59f77e50a | |||
| 50b1f00b5a | |||
| 4f98a8a319 | |||
| fb8aa3936b | |||
| 335876555f | |||
| 7da3b1f4c4 | |||
| e80bc4cea7 | |||
| 448755ff8d | |||
| 3a4c23840a | |||
| 13c69111f9 | |||
| 0b9feb9fda |
@@ -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
|
||||
@@ -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.'
|
||||
@@ -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.
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# BIG-AGI 🧠✨
|
||||
|
||||
Welcome to big-AGI 👋, the GPT application for professionals that need function, form,
|
||||
simplicity, and speed. Powered by the latest models from 8 vendors and
|
||||
simplicity, and speed. Powered by the latest models from 11 vendors and
|
||||
open-source model servers, `big-AGI` offers best-in-class Voice and Chat with AI Personas,
|
||||
visualizations, coding, drawing, calling, and quite more -- all in a polished UX.
|
||||
|
||||
@@ -21,6 +21,28 @@ 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.11.0 · Jan 16, 2024 · Singularity
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cfcb110c68
|
||||
|
||||
- **Find chats**: search in titles and content, with frequency ranking. [#329](https://github.com/enricoros/big-AGI/issues/329)
|
||||
- **Commands**: command auto-completion (type '/'). [#327](https://github.com/enricoros/big-AGI/issues/327)
|
||||
- **[Together AI](https://www.together.ai/products#inference)** inference platform support (good speed and newer models). [#346](https://github.com/enricoros/big-AGI/issues/346)
|
||||
- Persona Creator history, deletion, custom creation, fix llm API timeouts
|
||||
- Enable adding up to five custom OpenAI-compatible endpoints
|
||||
- Developer enhancements: new 'Actiles' framework
|
||||
|
||||
### What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/fbb1be49-5c38-49c8-86fa-3705700f6c39
|
||||
|
||||
- **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)
|
||||
@@ -31,22 +53,7 @@ shows the current developments and future ideas.
|
||||
- Layout fix for Firefox users
|
||||
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
|
||||
|
||||
### What's New in 1.8.0 · Dec 20, 2023
|
||||
|
||||
- **Google Gemini Support**: Use the newest Google models. [#275](https://github.com/enricoros/big-agi/issues/275)
|
||||
- **Mistral Platform**: Mixtral and future models support. [#273](https://github.com/enricoros/big-agi/issues/273)
|
||||
- **Diagram Instructions**. Thanks to @joriskalz! [#280](https://github.com/enricoros/big-agi/pull/280)
|
||||
- Ollama Chats: Enhanced chatting experience. [#270](https://github.com/enricoros/big-agi/issues/270)
|
||||
- Mac Shortcuts Fix: Improved UX on Mac
|
||||
- **Single-Tab Mode**: Data integrity with single window. [#268](https://github.com/enricoros/big-agi/issues/268)
|
||||
- **Updated Models**: Latest Ollama (v0.1.17) and OpenRouter models
|
||||
- Official Downloads: Easy access to the latest big-AGI on [big-AGI.com](https://big-agi.com)
|
||||
- For developers: [troubleshot networking](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483), fixed Vercel deployment, cleaned up the LLMs/Streaming framework
|
||||
|
||||
### What's New in... ?
|
||||
|
||||
> [To The Moon And Back, Attachment Theory, Surf's Up, Loaded, and more releases...](docs/changelog.md).
|
||||
> Check out the [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2)
|
||||
For full details and former releases, check out the [changelog](docs/changelog.md).
|
||||
|
||||
## ✨ Key Features 👊
|
||||
|
||||
|
||||
+20
-2
@@ -5,11 +5,29 @@ 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.12.0 - Jan 2024
|
||||
|
||||
- milestone: [1.10.0](https://github.com/enricoros/big-agi/milestone/10)
|
||||
- milestone: [1.12.0](https://github.com/enricoros/big-agi/milestone/12)
|
||||
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
|
||||
|
||||
### What's New in 1.11.0 · Jan 16, 2024 · Singularity
|
||||
|
||||
- **Find chats**: search in titles and content, with frequency ranking. [#329](https://github.com/enricoros/big-AGI/issues/329)
|
||||
- **Commands**: command auto-completion (type '/'). [#327](https://github.com/enricoros/big-AGI/issues/327)
|
||||
- **[Together AI](https://www.together.ai/products#inference)** inference platform support. [#346](https://github.com/enricoros/big-AGI/issues/346)
|
||||
- Persona Creator history, deletion, custom creation, fix llm API timeouts
|
||||
- Enable adding up to five custom OpenAI-compatible endpoints
|
||||
- Developer enhancements: new 'Actiles' framework
|
||||
|
||||
### What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI
|
||||
|
||||
- **New UI**: for both desktop and mobile, sets the stage for future scale. [#201](https://github.com/enricoros/big-AGI/issues/201)
|
||||
- **Conversation Folders**: enhanced conversation organization. [#321](https://github.com/enricoros/big-AGI/issues/321)
|
||||
- **[LM Studio](https://lmstudio.ai/)** support and improved token management
|
||||
- Resizable panes in split-screen conversations.
|
||||
- Large performance optimizations
|
||||
- Developer enhancements: new UI framework, updated documentation for proxy settings on browserless/docker
|
||||
|
||||
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
|
||||
|
||||
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
|
||||
|
||||
+25
-1
@@ -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!
|
||||
|
||||
@@ -28,6 +28,7 @@ GEMINI_API_KEY=
|
||||
MISTRAL_API_KEY=
|
||||
OLLAMA_API_HOST=
|
||||
OPENROUTER_API_KEY=
|
||||
TOGETHERAI_API_KEY=
|
||||
|
||||
# Model Observability: Helicone
|
||||
HELICONE_API_KEY=
|
||||
@@ -85,6 +86,7 @@ requiring the user to enter an API key
|
||||
| `MISTRAL_API_KEY` | The API key for Mistral | Optional |
|
||||
| `OLLAMA_API_HOST` | Changes the backend host for the Ollama vendor. See [config-ollama.md](config-ollama.md) | |
|
||||
| `OPENROUTER_API_KEY` | The API key for OpenRouter | Optional |
|
||||
| `TOGETHERAI_API_KEY` | The API key for Together AI | Optional |
|
||||
|
||||
### Model Observability: Helicone
|
||||
|
||||
|
||||
Generated
+262
-113
@@ -1,31 +1,32 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.9.0",
|
||||
"version": "1.11.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "big-agi",
|
||||
"version": "1.9.0",
|
||||
"version": "1.11.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.7",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.9.0",
|
||||
"version": "1.11.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",
|
||||
|
||||
@@ -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
@@ -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
@@ -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 />);
|
||||
}
|
||||
@@ -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} />);
|
||||
}
|
||||
@@ -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 || ''} />);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
+171
-144
@@ -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 { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
|
||||
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
|
||||
import { getChatLLMId, useChatLLM } from '~/modules/llms/store-llms';
|
||||
import { imaginePromptFromText } from '~/modules/aifn/imagine/imaginePromptFromText';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { useBrowseStore } from '~/modules/browse/store-module-browsing';
|
||||
import { useChatLLM, useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { GoodPanelResizeHandler } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useConversation } from '~/common/state/store-chats';
|
||||
import { 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,39 +145,45 @@ export function AppChat() {
|
||||
}
|
||||
}, [focusedChatNumber, focusedChatTitle]);
|
||||
|
||||
|
||||
// Execution
|
||||
|
||||
const _handleExecute = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) => {
|
||||
const { chatLLMId } = useModelsStore.getState();
|
||||
const chatLLMId = getChatLLMId();
|
||||
if (!chatModeId || !conversationId || !chatLLMId) return;
|
||||
|
||||
// "/command ...": overrides the chat mode
|
||||
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
|
||||
if (lastMessage?.role === 'user') {
|
||||
const pieces = extractCommands(lastMessage.text);
|
||||
if (pieces.length == 2 && pieces[0].type === 'cmd' && pieces[1].type === 'text') {
|
||||
const [command, prompt] = [pieces[0].value, pieces[1].value];
|
||||
if (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,18 +347,18 @@ export function AppChat() {
|
||||
|
||||
const handleConversationsDeleteAll = () => setDeleteConversationId(SPECIAL_ID_WIPE_ALL);
|
||||
|
||||
const handleConversationDelete = React.useCallback((conversationId: DConversationId, bypassConfirmation: boolean) => {
|
||||
if (bypassConfirmation)
|
||||
setFocusedConversationId(deleteConversation(conversationId));
|
||||
else
|
||||
setDeleteConversationId(conversationId);
|
||||
}, [deleteConversation, setFocusedConversationId]);
|
||||
|
||||
const handleConversationDelete = React.useCallback(
|
||||
(conversationId: DConversationId, bypassConfirmation: boolean) => {
|
||||
if (bypassConfirmation) setFocusedConversationId(deleteConversation(conversationId));
|
||||
else setDeleteConversationId(conversationId);
|
||||
},
|
||||
[deleteConversation, setFocusedConversationId],
|
||||
);
|
||||
|
||||
// Shortcuts
|
||||
|
||||
const handleOpenChatLlmOptions = React.useCallback(() => {
|
||||
const { chatLLMId } = useModelsStore.getState();
|
||||
const chatLLMId = getChatLLMId();
|
||||
if (!chatLLMId) return;
|
||||
openLlmOptions(chatLLMId);
|
||||
}, [openLlmOptions]);
|
||||
@@ -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'}
|
||||
/>}
|
||||
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsAlter: ICommandsProvider = {
|
||||
id: 'chat-alter',
|
||||
rank: 20,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/assistant',
|
||||
alternatives: ['/a'],
|
||||
arguments: ['text'],
|
||||
description: 'Injects assistant response',
|
||||
}, {
|
||||
primary: '/system',
|
||||
alternatives: ['/s'],
|
||||
arguments: ['text'],
|
||||
description: 'Injects system message',
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsBrowse: ICommandsProvider = {
|
||||
id: 'ass-browse',
|
||||
rank: 25,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/browse',
|
||||
arguments: ['URL'],
|
||||
description: 'Assistant will download the web page',
|
||||
Icon: LanguageIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsDraw: ICommandsProvider = {
|
||||
id: 'ass-t2i',
|
||||
rank: 10,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/draw',
|
||||
alternatives: ['/imagine', '/img'],
|
||||
arguments: ['prompt'],
|
||||
description: 'Assistant will draw the text',
|
||||
Icon: FormatPaintIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsHelp: ICommandsProvider = {
|
||||
id: 'cmd-help',
|
||||
rank: 99,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/help',
|
||||
alternatives: ['/?'],
|
||||
description: 'Display this list of commands',
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsReact: ICommandsProvider = {
|
||||
id: 'ass-react',
|
||||
rank: 15,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/react',
|
||||
arguments: ['prompt'],
|
||||
description: 'Use the AI ReAct strategy to answer your query (as sidebar)',
|
||||
Icon: PsychologyIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { FunctionComponent } from 'react';
|
||||
import type { CommandsProviderId } from './commands.registry';
|
||||
|
||||
|
||||
export interface ChatCommand {
|
||||
primary: string; // The primary command
|
||||
alternatives?: string[]; // Alternative commands
|
||||
arguments?: string[]; // Arguments for the command
|
||||
description: string; // Description of what the command does
|
||||
// usage?: string; // Example of how to use the command
|
||||
Icon?: FunctionComponent; // Icon to display next to the command
|
||||
}
|
||||
|
||||
|
||||
export interface ICommandsProvider {
|
||||
id: CommandsProviderId; // Unique identifier for the command provider
|
||||
rank: number; // Rank of the provider, used to sort the providers in the UI
|
||||
|
||||
// Function to get commands with their alternatives and details
|
||||
getCommands: () => ChatCommand[];
|
||||
|
||||
// Function to execute a command with optional parameters
|
||||
// executeCommand: (command: string, params?: string[]) => Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ChatCommand, ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
import { CommandsAlter } from './CommandsAlter';
|
||||
import { CommandsBrowse } from './CommandsBrowse';
|
||||
import { CommandsDraw } from './CommandsDraw';
|
||||
import { CommandsHelp } from './CommandsHelp';
|
||||
import { CommandsReact } from './CommandsReact';
|
||||
|
||||
|
||||
export type CommandsProviderId = 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help';
|
||||
|
||||
type TextCommandPiece =
|
||||
| { type: 'text'; value: string; }
|
||||
| { type: 'cmd'; providerId: CommandsProviderId, command: string; params?: string, isError?: boolean };
|
||||
|
||||
|
||||
const ChatCommandsProviders: Record<CommandsProviderId, ICommandsProvider> = {
|
||||
'ass-browse': CommandsBrowse,
|
||||
'ass-react': CommandsReact,
|
||||
'ass-t2i': CommandsDraw,
|
||||
'chat-alter': CommandsAlter,
|
||||
'cmd-help': CommandsHelp,
|
||||
};
|
||||
|
||||
export function findAllChatCommands(): ChatCommand[] {
|
||||
return Object.values(ChatCommandsProviders)
|
||||
.sort((a, b) => a.rank - b.rank)
|
||||
.map(p => p.getCommands())
|
||||
.flat();
|
||||
}
|
||||
|
||||
export function extractChatCommand(input: string): TextCommandPiece[] {
|
||||
const inputTrimmed = input.trim();
|
||||
|
||||
// quick exit: command does not start with '/'
|
||||
if (!inputTrimmed.startsWith('/'))
|
||||
return [{ type: 'text', value: input }];
|
||||
|
||||
// Find the first space to separate the command from its parameters (if any)
|
||||
const firstSpaceIndex = inputTrimmed.indexOf(' ');
|
||||
const potentialCommand = inputTrimmed.substring(0, firstSpaceIndex >= 0 ? firstSpaceIndex : inputTrimmed.length);
|
||||
|
||||
// Check if the potential command is an actual command
|
||||
for (const provider of Object.values(ChatCommandsProviders)) {
|
||||
for (const cmd of provider.getCommands()) {
|
||||
if (cmd.primary === potentialCommand || cmd.alternatives?.includes(potentialCommand)) {
|
||||
|
||||
// command needs arguments: take the rest of the input as parameters
|
||||
if (cmd.arguments?.length) {
|
||||
const params = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
return [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: params || undefined, isError: !params || undefined }];
|
||||
}
|
||||
|
||||
// command without arguments, treat any text after as a separate text piece
|
||||
const pieces: TextCommandPiece[] = [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: undefined }];
|
||||
const textAfterCommand = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
if (textAfterCommand)
|
||||
pieces.push({ type: 'text', value: textAfterCommand });
|
||||
return pieces;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No command found, return the entire input as text
|
||||
return [{ type: 'text', value: input }];
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -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,68 +1,154 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, ListDivider, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
|
||||
import { Box, IconButton, ListDivider, ListItemButton, ListItemDecorator, Tooltip } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import FolderOpenOutlinedIcon from '@mui/icons-material/FolderOpenOutlined';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
|
||||
import { 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 DebounceInput from '~/common/components/DebounceInput';
|
||||
|
||||
import { ChatNavigationItemMemo } from './ChatNavigationItem';
|
||||
import { ChatFolderList } from './folder/ChatFolderList';
|
||||
import { ChatDrawerItemMemo, ChatNavigationItemData } from './ChatNavigationItem';
|
||||
|
||||
|
||||
// type ListGrouping = 'off' | 'persona';
|
||||
|
||||
export const ChatDrawerItemsMemo = React.memo(ChatDrawerItems);
|
||||
/*
|
||||
* Optimization: return a reduced version of the DConversation object for 'Drawer Items' purposes,
|
||||
* to avoid unnecessary re-renders on each new character typed by the assistant
|
||||
*/
|
||||
export const useChatNavigationItems = (activeConversationId: DConversationId | null, folderId: string | null): {
|
||||
chatNavItems: ChatNavigationItemData[],
|
||||
folders: DFolder[],
|
||||
} => {
|
||||
|
||||
// monitor folder changes
|
||||
// NOTE: we're not checking for state.useFolders, as we strongly assume folderId to be null when folders are disabled
|
||||
const { currentFolder, folders } = useFolderStore(state => {
|
||||
const currentFolder = folderId ? state.folders.find(_f => _f.id === folderId) ?? null : null;
|
||||
return {
|
||||
folders: state.folders,
|
||||
currentFolder,
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
// transform (folder) selected conversation into optimized 'navigation item' data
|
||||
const chatNavItems: ChatNavigationItemData[] = useChatStore(state => {
|
||||
|
||||
const selectConversations = currentFolder
|
||||
? state.conversations.filter(_c => currentFolder.conversationIds.includes(_c.id))
|
||||
: state.conversations;
|
||||
|
||||
return selectConversations.map(_c => ({
|
||||
conversationId: _c.id,
|
||||
isActive: _c.id === activeConversationId,
|
||||
isEmpty: !_c.messages.length && !_c.userTitle,
|
||||
title: conversationTitle(_c),
|
||||
messageCount: _c.messages.length,
|
||||
assistantTyping: !!_c.abortController,
|
||||
systemPurposeId: _c.systemPurposeId,
|
||||
}));
|
||||
|
||||
}, (a: ChatNavigationItemData[], b: ChatNavigationItemData[]) => {
|
||||
// custom equality function to avoid unnecessary re-renders
|
||||
return a.length === b.length && a.every((_a, i) => shallow(_a, b[i]));
|
||||
});
|
||||
|
||||
return { chatNavItems, folders };
|
||||
};
|
||||
|
||||
|
||||
export const ChatDrawerContentMemo = React.memo(ChatDrawerItems);
|
||||
|
||||
function ChatDrawerItems(props: {
|
||||
activeConversationId: DConversationId | null,
|
||||
disableNewButton: boolean,
|
||||
onConversationActivate: (conversationId: DConversationId) => void,
|
||||
onConversationDelete: (conversationId: DConversationId, bypassConfirmation: boolean) => void,
|
||||
onConversationExportDialog: (conversationId: DConversationId | null) => void,
|
||||
onConversationImportDialog: () => void,
|
||||
onConversationNew: () => void,
|
||||
onConversationsDeleteAll: () => void,
|
||||
selectedFolderId: string | null,
|
||||
setSelectedFolderId: (folderId: string | null) => void,
|
||||
}) {
|
||||
|
||||
// local state
|
||||
const { onConversationDelete, onConversationNew, onConversationActivate } = props;
|
||||
const [debouncedSearchQuery, setDebouncedSearchQuery] = React.useState('');
|
||||
|
||||
// const [grouping] = React.useState<ListGrouping>('off');
|
||||
const { onConversationDelete, onConversationNew, onConversationActivate } = props;
|
||||
|
||||
// external state
|
||||
const { 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 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);
|
||||
}, [onConversationDelete, singleChat]);
|
||||
|
||||
|
||||
// Filter chatNavItems based on the search query and rank them by search frequency
|
||||
const filteredChatNavItems = React.useMemo(() => {
|
||||
if (!debouncedSearchQuery) return chatNavItems;
|
||||
return chatNavItems
|
||||
.map(item => {
|
||||
// Get the conversation by ID
|
||||
const conversation = useChatStore.getState().conversations.find(c => c.id === item.conversationId);
|
||||
// Calculate the frequency of the search term in the title and messages
|
||||
const titleFrequency = (item.title.toLowerCase().match(new RegExp(debouncedSearchQuery.toLowerCase(), 'g')) || []).length;
|
||||
const messageFrequency = conversation?.messages.reduce((count, message) => {
|
||||
return count + (message.text.toLowerCase().match(new RegExp(debouncedSearchQuery.toLowerCase(), 'g')) || []).length;
|
||||
}, 0) || 0;
|
||||
// Return the item with the searchFrequency property
|
||||
return {
|
||||
...item,
|
||||
searchFrequency: titleFrequency + messageFrequency,
|
||||
};
|
||||
})
|
||||
// Exclude items with a searchFrequency of 0
|
||||
.filter(item => item.searchFrequency > 0)
|
||||
// Sort the items by searchFrequency in descending order
|
||||
.sort((a, b) => b.searchFrequency! - a.searchFrequency!);
|
||||
}, [chatNavItems, debouncedSearchQuery]);
|
||||
|
||||
|
||||
// basis for the underline bar
|
||||
const bottomBarBasis = filteredChatNavItems.reduce((longest, _c) => Math.max(longest, _c.searchFrequency ?? _c.messageCount), 1);
|
||||
|
||||
|
||||
// grouping
|
||||
/*let sortedIds = conversationIDs;
|
||||
if (grouping === 'persona') {
|
||||
@@ -85,66 +171,127 @@ function ChatDrawerItems(props: {
|
||||
|
||||
return <>
|
||||
|
||||
{/*<ListItem>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Active chats*/}
|
||||
{/* </Typography>*/}
|
||||
{/*</ListItem>*/}
|
||||
{/* Drawer Header */}
|
||||
<PageDrawerHeader
|
||||
title='Chats'
|
||||
onClose={closeDrawer}
|
||||
startButton={
|
||||
<Tooltip title={useFolders ? 'Hide Folders' : 'Use Folders'}>
|
||||
<IconButton onClick={toggleUseFolders}>
|
||||
{useFolders ? <FolderOpenOutlinedIcon /> : <FolderOutlinedIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<MenuItem disabled={props.disableNewButton} onClick={handleButtonNew}>
|
||||
<ListItemDecorator><AddIcon /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
New
|
||||
{/*<KeyStroke combo='Ctrl + Alt + N' />*/}
|
||||
{/* Folders List */}
|
||||
{/*<Box sx={{*/}
|
||||
{/* display: 'grid',*/}
|
||||
{/* gridTemplateRows: !useFolders ? '0fr' : '1fr',*/}
|
||||
{/* transition: 'grid-template-rows 0.42s cubic-bezier(.17,.84,.44,1)',*/}
|
||||
{/* '& > div': {*/}
|
||||
{/* padding: useFolders ? 2 : 0,*/}
|
||||
{/* transition: 'padding 0.42s cubic-bezier(.17,.84,.44,1)',*/}
|
||||
{/* overflow: 'hidden',*/}
|
||||
{/* },*/}
|
||||
{/*}}>*/}
|
||||
{useFolders && (
|
||||
<ChatFolderList
|
||||
folders={folders}
|
||||
selectedFolderId={props.selectedFolderId}
|
||||
onFolderSelect={props.setSelectedFolderId}
|
||||
/>
|
||||
)}
|
||||
{/*</Box>*/}
|
||||
|
||||
{/* Chats List */}
|
||||
<PageDrawerList variant='plain' noTopPadding noBottomPadding tallRows>
|
||||
|
||||
{useFolders && <ListDivider sx={{ mb: 0 }} />}
|
||||
|
||||
{/* Search Input Field */}
|
||||
<DebounceInput
|
||||
onDebounce={setDebouncedSearchQuery}
|
||||
debounceTimeout={300}
|
||||
placeholder='Search...'
|
||||
aria-label='Search'
|
||||
sx={{ m: 2 }}
|
||||
/>
|
||||
|
||||
<ListItemButton disabled={props.disableNewButton} onClick={handleButtonNew} sx={PageDrawerTallItemSx}>
|
||||
<ListItemDecorator><AddIcon /></ListItemDecorator>
|
||||
<Box sx={{
|
||||
// style
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'lg',
|
||||
// content
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1,
|
||||
}}>
|
||||
New chat
|
||||
{/*<KeyStroke combo='Ctrl + Alt + N' sx={props.disableNewButton ? { opacity: 0.5 } : undefined} />*/}
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
|
||||
{/*<ListDivider sx={{ mt: 0 }} />*/}
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
{/*<ListItem sticky sx={{ justifyContent: 'space-between', boxShadow: 'sm' }}>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Conversations*/}
|
||||
{/* </Typography>*/}
|
||||
{/* <ToggleButtonGroup variant='soft' size='sm' value={grouping} onChange={(_event, newValue) => newValue && setGrouping(newValue)}>*/}
|
||||
{/* <IconButton value='off'>*/}
|
||||
{/* <AccessTimeIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* <IconButton value='persona'>*/}
|
||||
{/* <PersonIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </ToggleButtonGroup>*/}
|
||||
{/*</ListItem>*/}
|
||||
|
||||
{filteredChatNavItems.map(item =>
|
||||
<ChatDrawerItemMemo
|
||||
key={'nav-' + item.conversationId}
|
||||
item={item}
|
||||
isLonely={singleChat}
|
||||
showSymbols={showSymbols}
|
||||
bottomBarBasis={(labsEnhancedUI || softMaxReached || debouncedSearchQuery) ? bottomBarBasis : 0}
|
||||
onConversationActivate={handleConversationActivate}
|
||||
onConversationDelete={handleConversationDelete}
|
||||
/>)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider sx={{ mb: 0 }} />
|
||||
<ListDivider sx={{ mt: 0 }} />
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
{/*<ListItem sticky sx={{ justifyContent: 'space-between', boxShadow: 'sm' }}>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Conversations*/}
|
||||
{/* </Typography>*/}
|
||||
{/* <ToggleButtonGroup variant='soft' size='sm' value={grouping} onChange={(_event, newValue) => newValue && setGrouping(newValue)}>*/}
|
||||
{/* <IconButton value='off'>*/}
|
||||
{/* <AccessTimeIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* <IconButton value='persona'>*/}
|
||||
{/* <PersonIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </ToggleButtonGroup>*/}
|
||||
{/*</ListItem>*/}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
{conversations.map(conversation =>
|
||||
<ChatNavigationItemMemo
|
||||
key={'nav-' + conversation.id}
|
||||
conversation={conversation}
|
||||
isActive={conversation.id === props.activeConversationId}
|
||||
isLonely={singleChat}
|
||||
maxChatMessages={(labsEnhancedUI || softMaxReached) ? maxChatMessages : 0}
|
||||
showSymbols={showSymbols}
|
||||
onConversationActivate={handleConversationActivate}
|
||||
onConversationDelete={handleConversationDelete}
|
||||
/>)}
|
||||
</Box>
|
||||
<ListItemButton onClick={props.onConversationImportDialog} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator>
|
||||
<FileUploadIcon />
|
||||
</ListItemDecorator>
|
||||
Import
|
||||
{/*<OpenAIIcon sx={{ ml: 'auto' }} />*/}
|
||||
</ListItemButton>
|
||||
|
||||
<ListDivider sx={{ mt: 0 }} />
|
||||
<ListItemButton disabled={!nonEmptyChats} onClick={() => props.onConversationExportDialog(props.activeConversationId)} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator>
|
||||
<FileDownloadIcon />
|
||||
</ListItemDecorator>
|
||||
Export
|
||||
</ListItemButton>
|
||||
</Box>
|
||||
|
||||
<MenuItem onClick={props.onConversationImportDialog}>
|
||||
<ListItemDecorator>
|
||||
<FileUploadIcon />
|
||||
</ListItemDecorator>
|
||||
Import chats
|
||||
<OpenAIIcon sx={{ fontSize: 'xl', ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
<ListItemButton disabled={!nonEmptyChats} onClick={props.onConversationsDeleteAll}>
|
||||
<ListItemDecorator>
|
||||
<DeleteOutlineIcon />
|
||||
</ListItemDecorator>
|
||||
Delete {selectConversationsCount >= 2 ? `all ${selectConversationsCount} chats` : 'chat'}
|
||||
</ListItemButton>
|
||||
|
||||
<MenuItem disabled={!hasChats} onClick={props.onConversationsDeleteAll}>
|
||||
<ListItemDecorator><DeleteOutlineIcon /></ListItemDecorator>
|
||||
<Typography>
|
||||
Delete {totalConversations >= 2 ? `all ${totalConversations} chats` : 'chat'}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</PageDrawerList>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -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,49 +1,48 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Avatar, Box, IconButton, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
|
||||
import { Avatar, Box, IconButton, ListItemButton, ListItemDecorator, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
|
||||
import { SystemPurposes } from '../../../../data';
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
const DEBUG_CONVERSATION_IDs = false;
|
||||
|
||||
|
||||
export const ChatNavigationItemMemo = React.memo(ChatNavigationItem);
|
||||
export const ChatDrawerItemMemo = React.memo(ChatNavigationItem);
|
||||
|
||||
export interface ChatNavigationItemData {
|
||||
conversationId: DConversationId;
|
||||
isActive: boolean;
|
||||
isEmpty: boolean;
|
||||
title: string;
|
||||
messageCount: number;
|
||||
assistantTyping: boolean;
|
||||
systemPurposeId: SystemPurposeId;
|
||||
searchFrequency?: number;
|
||||
}
|
||||
|
||||
function ChatNavigationItem(props: {
|
||||
conversation: DConversation,
|
||||
isActive: boolean,
|
||||
item: ChatNavigationItemData,
|
||||
isLonely: boolean,
|
||||
maxChatMessages: number,
|
||||
showSymbols: boolean,
|
||||
bottomBarBasis: number,
|
||||
onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void,
|
||||
onConversationDelete: (conversationId: DConversationId) => void,
|
||||
}) {
|
||||
|
||||
const { conversation, isActive } = props;
|
||||
|
||||
// state
|
||||
const [isEditingTitle, setIsEditingTitle] = React.useState(false);
|
||||
const [deleteArmed, setDeleteArmed] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const doubleClickToEdit = useUIPreferencesStore(state => state.doubleClickToEdit);
|
||||
|
||||
// derived state
|
||||
const { id: conversationId } = conversation;
|
||||
const isNew = conversation.messages.length === 0;
|
||||
const messageCount = conversation.messages.length;
|
||||
const assistantTyping = !!conversation.abortController;
|
||||
const systemPurposeId = conversation.systemPurposeId;
|
||||
const title = conversationTitle(conversation, 'new conversation');
|
||||
// const setUserTitle = state.setUserTitle;
|
||||
const { conversationId, isActive, title, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
|
||||
const isNew = messageCount === 0;
|
||||
|
||||
// auto-close the arming menu when clicking away
|
||||
// NOTE: there currently is a bug (race condition) where the menu closes on a new item right after opening
|
||||
@@ -60,7 +59,11 @@ function ChatNavigationItem(props: {
|
||||
|
||||
const handleTitleEdited = (text: string) => {
|
||||
setIsEditingTitle(false);
|
||||
useChatStore.getState().setUserTitle(conversationId, text);
|
||||
useChatStore.getState().setUserTitle(conversationId, text.trim());
|
||||
};
|
||||
|
||||
const handleTitleEditCancel = () => {
|
||||
setIsEditingTitle(false);
|
||||
};
|
||||
|
||||
const handleDeleteButtonShow = (event: React.MouseEvent) => {
|
||||
@@ -83,28 +86,27 @@ function ChatNavigationItem(props: {
|
||||
|
||||
|
||||
const textSymbol = SystemPurposes[systemPurposeId]?.symbol || '❓';
|
||||
const buttonSx: SxProps = { ml: 1, ...(isActive ? { color: 'white' } : {}) };
|
||||
const buttonSx: SxProps = isActive ? { color: 'white' } : {};
|
||||
|
||||
const progress = props.maxChatMessages ? 100 * messageCount / props.maxChatMessages : 0;
|
||||
const progress = props.bottomBarBasis ? 100 * (searchFrequency ?? messageCount) / props.bottomBarBasis : 0;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
variant={isActive ? 'solid' : 'plain'} color='neutral'
|
||||
selected={isActive}
|
||||
onClick={handleConversationActivate}
|
||||
<ListItemButton
|
||||
variant={isActive ? 'soft' : 'plain'} color='neutral'
|
||||
onClick={!isActive ? handleConversationActivate : event => event.preventDefault()}
|
||||
sx={{
|
||||
// py: 0,
|
||||
position: 'relative',
|
||||
border: 'none', // note, there's a default border of 1px and invisible.. hmm
|
||||
cursor: 'pointer',
|
||||
'&:hover > button': { opacity: 1 },
|
||||
...(isActive ? { bgcolor: 'red' } : {}),
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Optional progress bar, underlay */}
|
||||
{progress > 0 && (
|
||||
<Box sx={{
|
||||
backgroundColor: 'neutral.softActiveBg',
|
||||
backgroundColor: 'neutral.softBg',
|
||||
position: 'absolute', left: 0, bottom: 0, width: progress + '%', height: 4,
|
||||
}} />
|
||||
)}
|
||||
@@ -117,35 +119,39 @@ function ChatNavigationItem(props: {
|
||||
alt='typing' variant='plain'
|
||||
src='https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'
|
||||
sx={{
|
||||
width: 24,
|
||||
height: 24,
|
||||
width: '1.5rem',
|
||||
height: '1.5rem',
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography sx={{ fontSize: '18px' }}>
|
||||
<Typography>
|
||||
{isNew ? '' : textSymbol}
|
||||
</Typography>
|
||||
)}
|
||||
</ListItemDecorator>}
|
||||
|
||||
|
||||
{/* Text */}
|
||||
{!isEditingTitle ? (
|
||||
|
||||
<Box onDoubleClick={() => doubleClickToEdit ? handleTitleEdit() : null} sx={{ flexGrow: 1 }}>
|
||||
{DEBUG_CONVERSATION_IDs ? conversationId.slice(0, 10) : title}{assistantTyping && '...'}
|
||||
</Box>
|
||||
<Typography
|
||||
level={isActive ? 'title-md' : 'body-md'}
|
||||
onDoubleClick={handleTitleEdit}
|
||||
sx={{ flex: 1 }}
|
||||
>
|
||||
{DEBUG_CONVERSATION_IDs ? conversationId.slice(0, 10) : (title.trim() ? title : 'Chat')}{assistantTyping && '...'}
|
||||
</Typography>
|
||||
|
||||
) : (
|
||||
|
||||
<InlineTextarea initialText={title} onEdit={handleTitleEdited} sx={{ ml: -1.5, mr: -0.5, flexGrow: 1 }} />
|
||||
<InlineTextarea initialText={title} onEdit={handleTitleEdited} onCancel={handleTitleEditCancel} sx={{ ml: -1.5, mr: -0.5, flexGrow: 1 }} />
|
||||
|
||||
)}
|
||||
|
||||
{/* // TODO: Commented code */}
|
||||
{/* Edit */}
|
||||
{/*<IconButton*/}
|
||||
{/* variant='plain' color='neutral'*/}
|
||||
{/* onClick={() => props.onEditTitle(props.conversationId)}*/}
|
||||
{/* sx={{*/}
|
||||
{/* opacity: 0, transition: 'opacity 0.3s', ml: 'auto',*/}
|
||||
@@ -153,18 +159,29 @@ function ChatNavigationItem(props: {
|
||||
{/* <EditIcon />*/}
|
||||
{/*</IconButton>*/}
|
||||
|
||||
{/* Display search frequency if it exists and is greater than 0 */}
|
||||
{searchFrequency && searchFrequency > 0 && (
|
||||
<Box sx={{ ml: 1 }}>
|
||||
<Typography level='body-sm'>
|
||||
{searchFrequency}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Delete Arming */}
|
||||
{!props.isLonely && !deleteArmed && (
|
||||
{!props.isLonely && !deleteArmed && !searchFrequency && (
|
||||
<IconButton
|
||||
variant={isActive ? 'solid' : 'outlined'} color='neutral'
|
||||
size='sm' sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.3s', ...buttonSx }}
|
||||
onClick={handleDeleteButtonShow}>
|
||||
variant={isActive ? 'solid' : 'outlined'}
|
||||
size='sm'
|
||||
sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.2s', ...buttonSx }}
|
||||
onClick={handleDeleteButtonShow}
|
||||
>
|
||||
<DeleteOutlineIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{/* Delete / Cancel buttons */}
|
||||
{!props.isLonely && deleteArmed && <>
|
||||
{!props.isLonely && deleteArmed && !searchFrequency && <>
|
||||
<IconButton size='sm' variant='solid' color='danger' sx={buttonSx} onClick={handleConversationDelete}>
|
||||
<DeleteOutlineIcon />
|
||||
</IconButton>
|
||||
@@ -172,7 +189,7 @@ function ChatNavigationItem(props: {
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>}
|
||||
</MenuItem>
|
||||
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, ListItem, ListItemDecorator } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { getRotatingFolderColor, useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
|
||||
export function AddFolderButton() {
|
||||
|
||||
// state
|
||||
const [isAddingFolder, setIsAddingFolder] = React.useState(false);
|
||||
const [newFolderColor, setNewFolderColor] = React.useState<string | null>(null);
|
||||
|
||||
|
||||
const handleAddFolder = () => {
|
||||
setNewFolderColor(getRotatingFolderColor());
|
||||
setIsAddingFolder(true);
|
||||
};
|
||||
|
||||
const handleCreateFolder = (name: string) => {
|
||||
if (name.trim())
|
||||
useFolderStore.getState().createFolder(name.trim(), newFolderColor || undefined);
|
||||
setIsAddingFolder(false);
|
||||
};
|
||||
|
||||
const handleCancelAddFolder = () => {
|
||||
setIsAddingFolder(false);
|
||||
};
|
||||
|
||||
return isAddingFolder ? (
|
||||
<ListItem sx={{
|
||||
'--ListItem-paddingLeft': '0.75rem',
|
||||
'--ListItem-minHeight': '3rem', // --Folder-ListItem-height
|
||||
display: 'flex', alignItems: 'center', gap: 1,
|
||||
}}>
|
||||
<ListItemDecorator>
|
||||
<FolderIcon style={{ color: newFolderColor || 'inherit' }} />
|
||||
</ListItemDecorator>
|
||||
<InlineTextarea
|
||||
initialText='' placeholder='Folder Name'
|
||||
onEdit={handleCreateFolder}
|
||||
onCancel={handleCancelAddFolder}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
}} />
|
||||
{/*<IconButton color='danger' onClick={handleCancelAddFolder}>*/}
|
||||
{/* <CloseIcon />*/}
|
||||
{/*</IconButton>*/}
|
||||
</ListItem>
|
||||
) : (
|
||||
<Button
|
||||
color='neutral'
|
||||
variant='plain'
|
||||
startDecorator={<AddIcon />}
|
||||
onClick={handleAddFolder}
|
||||
sx={{
|
||||
// display: 'flex', alignItems: 'center', justifyContent: 'flex-start',
|
||||
// minHeight: '3rem', // --Folder-ListItem-height
|
||||
// match the forder elements
|
||||
paddingInline: '1.2rem',
|
||||
gap: '0.75rem',
|
||||
// fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
New folder
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import * as React from 'react';
|
||||
import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import { List, ListItem, ListItemButton, ListItemContent, ListItemDecorator, Sheet, Typography } from '@mui/joy';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
import { AddFolderButton } from './AddFolderButton';
|
||||
import { FolderListItem } from './FolderListItem';
|
||||
import { StrictModeDroppable } from './StrictModeDroppable';
|
||||
|
||||
|
||||
export function ChatFolderList(props: {
|
||||
folders: DFolder[];
|
||||
onFolderSelect: (folderId: string | null) => void;
|
||||
selectedFolderId: string | null;
|
||||
}) {
|
||||
|
||||
// derived props
|
||||
const { folders, onFolderSelect, selectedFolderId } = props;
|
||||
|
||||
// handlers
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
useFolderStore.getState().moveFolder(result.source.index, result.destination.index);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Sheet variant='soft' sx={{ p: 2 }}>
|
||||
<List
|
||||
variant='plain'
|
||||
sx={(theme) => ({
|
||||
'& ul': {
|
||||
'--List-gap': '0px',
|
||||
bgcolor: 'background.surface',
|
||||
'& > li:first-of-type > [role="button"]': {
|
||||
borderTopRightRadius: 'var(--List-radius)',
|
||||
borderTopLeftRadius: 'var(--List-radius)',
|
||||
},
|
||||
'& > li:last-child > [role="button"]': {
|
||||
borderBottomRightRadius: 'var(--List-radius)',
|
||||
borderBottomLeftRadius: 'var(--List-radius)',
|
||||
},
|
||||
},
|
||||
// copied from the former PageDrawerList as this was contained
|
||||
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
|
||||
'--ListItemDecorator-size': '2.75rem',
|
||||
'--ListItem-minHeight': '3rem', // --Folder-ListItem-height
|
||||
|
||||
'--List-radius': '8px',
|
||||
'--List-gap': '1rem',
|
||||
'--ListDivider-gap': '0px',
|
||||
// '--ListItem-paddingY': '0.5rem',
|
||||
// override global variant tokens
|
||||
'--joy-palette-neutral-plainHoverBg': 'rgba(0 0 0 / 0.08)',
|
||||
'--joy-palette-neutral-plainActiveBg': 'rgba(0 0 0 / 0.12)',
|
||||
[theme.getColorSchemeSelector('light')]: {
|
||||
'--joy-palette-divider': 'rgba(0 0 0 / 0.08)',
|
||||
},
|
||||
[theme.getColorSchemeSelector('dark')]: {
|
||||
'--joy-palette-neutral-plainHoverBg': 'rgba(255 255 255 / 0.1)',
|
||||
'--joy-palette-neutral-plainActiveBg': 'rgba(255 255 255 / 0.16)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<ListItem nested>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<StrictModeDroppable
|
||||
droppableId='folder'
|
||||
renderClone={(provided, snapshot, rubric) => (
|
||||
<FolderListItem
|
||||
folder={folders[rubric.source.index]}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
onFolderSelect={onFolderSelect}
|
||||
selectedFolderId={selectedFolderId}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(provided) => (
|
||||
<List ref={provided.innerRef} {...provided.droppableProps}>
|
||||
|
||||
{/* First item is the 'All' button */}
|
||||
<ListItem>
|
||||
<ListItemButton
|
||||
// handle folder select
|
||||
onClick={(event) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
onFolderSelect(null);
|
||||
}}
|
||||
selected={selectedFolderId === null}
|
||||
sx={{
|
||||
border: 0,
|
||||
justifyContent: 'space-between',
|
||||
'&:hover .menu-icon': {
|
||||
visibility: 'visible', // Hide delete icon for default folder
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<FolderIcon />
|
||||
</ListItemDecorator>
|
||||
|
||||
<ListItemContent>
|
||||
<Typography>All</Typography>
|
||||
</ListItemContent>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
{folders.map((folder, index) => (
|
||||
<Draggable key={folder.id} draggableId={folder.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<FolderListItem
|
||||
folder={folder}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
onFolderSelect={onFolderSelect}
|
||||
selectedFolderId={selectedFolderId}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</List>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</DragDropContext>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<AddFolderButton />
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DraggableProvided, DraggableStateSnapshot, DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd';
|
||||
|
||||
import { FormLabel, IconButton, ListItem, ListItemButton, ListItemContent, ListItemDecorator, MenuItem, Radio, radioClasses, RadioGroup, Sheet, Typography } from '@mui/joy';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import Done from '@mui/icons-material/Done';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { DFolder, FOLDERS_COLOR_PALETTE, useFolderStore } from '~/common/state/store-folders';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
|
||||
|
||||
// Define the type for your props if you're using TypeScript
|
||||
type RenderItemProps = {
|
||||
folder: DFolder;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
onFolderSelect: (folderId: string | null) => void;
|
||||
selectedFolderId: string | null;
|
||||
// Include any other props that RenderItem needs
|
||||
};
|
||||
|
||||
export const FolderListItem: React.FC<RenderItemProps> = ({ folder, provided, snapshot, onFolderSelect, selectedFolderId }) => {
|
||||
|
||||
// internal state
|
||||
const [deleteArmed, setDeleteArmed] = useState(false);
|
||||
const [editingFolderId, setEditingFolderId] = useState<string | null>(null);
|
||||
|
||||
// State to control the open state of the Menu
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLAnchorElement>(null);
|
||||
|
||||
|
||||
// Menu
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
setDeleteArmed(false); // Reset delete armed state
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setMenuAnchorEl(null);
|
||||
};
|
||||
|
||||
|
||||
// Edit Title
|
||||
|
||||
const handleEditTitle = (event: React.MouseEvent<HTMLElement, MouseEvent>, folderId: string) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
setEditingFolderId(folderId);
|
||||
};
|
||||
|
||||
const handleCancelEditTitle = () => {
|
||||
setEditingFolderId(null);
|
||||
};
|
||||
|
||||
const handleSetTitle = (newTitle: string, folderId: string) => {
|
||||
if (newTitle.trim())
|
||||
useFolderStore.getState().setFolderName(folderId, newTitle.trim());
|
||||
setEditingFolderId(null); // Exit edit mode
|
||||
// Blur the input element if it's currently focused
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Deletion
|
||||
|
||||
const handleDeleteButtonShow = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setDeleteArmed(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmed = (event: React.MouseEvent) => {
|
||||
if (deleteArmed) {
|
||||
setDeleteArmed(false);
|
||||
event.stopPropagation();
|
||||
useFolderStore.getState().deleteFolder(folder.id);
|
||||
handleMenuClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCanceled = (event: React.MouseEvent) => {
|
||||
if (deleteArmed) {
|
||||
setDeleteArmed(false);
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Color
|
||||
|
||||
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
useFolderStore.getState().setFolderColor(folder.id, event.target.value);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
|
||||
const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
userSelect: 'none',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: isDragging ? 'rgba(0, 80, 80, 0.18)' : 'transparent',
|
||||
|
||||
...draggableStyle,
|
||||
|
||||
// Any additional styles you want to apply during dragging
|
||||
...(isDragging &&
|
||||
{
|
||||
// Apply any drag-specific styles here
|
||||
// marginLeft: '12px',
|
||||
}),
|
||||
});
|
||||
|
||||
const getListItemContentStyle = (isDragging: boolean, _draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
...(isDragging && {
|
||||
// Apply any drag-specific styles here
|
||||
marginLeft: '20px',
|
||||
}),
|
||||
});
|
||||
|
||||
const getListItemDecoratorStyle = (isDragging: boolean, _draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
...(isDragging && {
|
||||
// Apply any drag-specific styles here
|
||||
marginLeft: '12px',
|
||||
}),
|
||||
});
|
||||
|
||||
const handleFolderSelect = (folderId: string | null) => {
|
||||
onFolderSelect(folderId);
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...getItemStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
// handle folder select
|
||||
onClick={(event) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
handleFolderSelect(folder.id);
|
||||
}}
|
||||
selected={folder.id === selectedFolderId}
|
||||
sx={{
|
||||
border: 0,
|
||||
justifyContent: 'space-between',
|
||||
'&:hover .menu-icon': {
|
||||
visibility: 'visible', // Hide delete icon for default folder
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator
|
||||
style={{
|
||||
...getListItemDecoratorStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<FolderIcon style={{ color: folder.color || 'inherit' }} />
|
||||
</ListItemDecorator>
|
||||
|
||||
{editingFolderId === folder.id ? (
|
||||
<InlineTextarea
|
||||
initialText={folder.title}
|
||||
onEdit={newTitle => handleSetTitle(newTitle, folder.id)}
|
||||
onCancel={handleCancelEditTitle}
|
||||
sx={{ ml: -1.5, mr: -0.5, flexGrow: 1 }}
|
||||
/>
|
||||
) : (
|
||||
<ListItemContent
|
||||
onDoubleClick={event => handleEditTitle(event, folder.id)}
|
||||
style={{
|
||||
...getListItemContentStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<Typography>{folder.title}</Typography>
|
||||
</ListItemContent>
|
||||
)}
|
||||
|
||||
{/* Icon to show the Popup menu */}
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
className='menu-icon'
|
||||
onClick={handleMenuOpen}
|
||||
sx={{
|
||||
visibility: 'hidden',
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
|
||||
{!!menuAnchorEl && (
|
||||
<CloseableMenu
|
||||
open anchorEl={menuAnchorEl} onClose={handleMenuClose}
|
||||
placement='top' zIndex={1301 /* need to be on top of the Modal on Mobile */}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
handleEditTitle(event, folder.id);
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<EditIcon />
|
||||
</ListItemDecorator>
|
||||
Edit
|
||||
</MenuItem>
|
||||
|
||||
{!deleteArmed ? (
|
||||
<MenuItem onClick={handleDeleteButtonShow}>
|
||||
<ListItemDecorator>
|
||||
<DeleteOutlineIcon />
|
||||
</ListItemDecorator>
|
||||
Delete
|
||||
</MenuItem>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem onClick={handleDeleteCanceled}>
|
||||
<ListItemDecorator>
|
||||
<CloseIcon />
|
||||
</ListItemDecorator>
|
||||
Cancel
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDeleteConfirmed} color='danger' sx={{ color: 'danger' }}>
|
||||
<ListItemDecorator>
|
||||
<DeleteOutlineIcon />
|
||||
</ListItemDecorator>
|
||||
Confirm Deletion
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
p: 2,
|
||||
minWidth: 200,
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
id='folder-color'
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
fontWeight: 'xl',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 'xs',
|
||||
letterSpacing: '0.1em',
|
||||
}}
|
||||
>
|
||||
Color
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-labelledby='product-color-attribute'
|
||||
defaultValue={folder.color || 'warning'}
|
||||
onChange={handleColorChange}
|
||||
sx={{ gap: 2, flexWrap: 'wrap', flexDirection: 'row', maxWidth: 240 }}
|
||||
>
|
||||
{FOLDERS_COLOR_PALETTE.map((color, index) => (
|
||||
<Sheet
|
||||
key={index}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: 20,
|
||||
height: 20,
|
||||
flexShrink: 0,
|
||||
bgcolor: `${color}`,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
overlay
|
||||
variant='solid'
|
||||
checkedIcon={<Done />}
|
||||
value={color}
|
||||
color='neutral'
|
||||
slotProps={{
|
||||
input: { 'aria-label': color },
|
||||
radio: {
|
||||
sx: {
|
||||
display: 'contents',
|
||||
'--variant-borderWidth': '2px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
'--joy-focus-outlineOffset': '4px',
|
||||
'--joy-palette-focusVisible': color,
|
||||
[`& .${radioClasses.action}.${radioClasses.focusVisible}`]: {
|
||||
outlineWidth: '2px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Sheet>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</MenuItem>
|
||||
|
||||
</CloseableMenu>
|
||||
)}
|
||||
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Droppable, DroppableProps } from "react-beautiful-dnd";
|
||||
|
||||
export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const animation = requestAnimationFrame(() => setEnabled(true));
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animation);
|
||||
setEnabled(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Droppable {...props}>{children}</Droppable>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { DropdownItems, PageBarDropdown } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
|
||||
const SPECIAL_ID_REMOVE = '_REMOVE_';
|
||||
|
||||
|
||||
export function useFolderDropdown(conversationId: DConversationId | null) {
|
||||
|
||||
// external state
|
||||
const { folders, useFolders } = useFolderStore();
|
||||
|
||||
|
||||
// Prepare items for the dropdown
|
||||
const folderItems: DropdownItems = React.useMemo(() => {
|
||||
// add one item per folder
|
||||
const items = folders.reduce((items, folder) => {
|
||||
items[folder.id] = {
|
||||
title: folder.title,
|
||||
icon: <FolderIcon sx={{ color: folder.color }} />,
|
||||
};
|
||||
return items;
|
||||
}, {} as DropdownItems);
|
||||
|
||||
// add one item representing no folder
|
||||
items[SPECIAL_ID_REMOVE] = {
|
||||
title: 'No Folder',
|
||||
};
|
||||
|
||||
return items;
|
||||
}, [folders]);
|
||||
|
||||
|
||||
// Handle dropdown folder change
|
||||
const handleFolderChange = React.useCallback((_event: any, folderId: string | null) => {
|
||||
if (conversationId && folderId) {
|
||||
// Remove conversation from all folders
|
||||
folders.forEach(folder => {
|
||||
if (folder.conversationIds.includes(conversationId)) {
|
||||
useFolderStore.getState().removeConversationFromFolder(folder.id, conversationId);
|
||||
}
|
||||
});
|
||||
// Add conversation to the selected folder
|
||||
useFolderStore.getState().addConversationToFolder(folderId, conversationId);
|
||||
}
|
||||
}, [conversationId, folders]);
|
||||
|
||||
// find the folder ID for the active Conversation
|
||||
const currentFolderId = folders.find(folder => folder.conversationIds.includes(conversationId || ''))?.id || null;
|
||||
|
||||
// Create the dropdown component
|
||||
const folderDropdown = React.useMemo(() => {
|
||||
|
||||
// don't show the dropdown if folders are not enabled
|
||||
if (!useFolders)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<PageBarDropdown
|
||||
items={folderItems}
|
||||
value={currentFolderId}
|
||||
onChange={handleFolderChange}
|
||||
placeholder='Select a folder'
|
||||
showSymbols
|
||||
/>
|
||||
);
|
||||
}, [currentFolderId, folderItems, handleFolderChange, useFolders]);
|
||||
|
||||
return { folderDropdown };
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { GoodDropdown, DropdownItems } from '~/common/components/GoodDropdown';
|
||||
import { PageBarDropdown, DropdownItems } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
@@ -54,7 +54,7 @@ function AppBarLLMDropdown(props: {
|
||||
|
||||
|
||||
return (
|
||||
<GoodDropdown
|
||||
<PageBarDropdown
|
||||
items={llmItems}
|
||||
value={props.chatLlmId} onChange={handleChatLLMChange}
|
||||
placeholder={props.placeholder || 'Models …'}
|
||||
|
||||
@@ -7,7 +7,7 @@ import CallIcon from '@mui/icons-material/Call';
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { GoodDropdown } from '~/common/components/GoodDropdown';
|
||||
import { PageBarDropdown } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
@@ -42,7 +42,7 @@ function AppBarPersonaDropdown(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<GoodDropdown
|
||||
<PageBarDropdown
|
||||
items={SystemPurposes} showSymbols={zenMode !== 'cleaner'}
|
||||
value={props.systemPurposeId} onChange={handleSystemPurposeChange}
|
||||
appendOption={appendOption}
|
||||
|
||||
@@ -170,7 +170,7 @@ export function CameraCaptureModal(props: {
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'space-between' }}>
|
||||
{/* Info */}
|
||||
<IconButton disabled={!info} variant='soft' color='neutral' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
|
||||
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
{/*<Button disabled={ocrProgress !== null} fullWidth variant='solid' size='lg' onClick={handleVideoOCRClicked} sx={{ flex: 1, maxWidth: 260 }}>*/}
|
||||
@@ -189,7 +189,7 @@ export function CameraCaptureModal(props: {
|
||||
</Button>
|
||||
|
||||
{/* Download */}
|
||||
<IconButton variant='soft' color='neutral' onClick={handleVideoDownloadClicked}>
|
||||
<IconButton size='lg' variant='soft' onClick={handleVideoDownloadClicked}>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -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';
|
||||
@@ -35,19 +36,23 @@ import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ActileItem, ActileProvider } from './actile/ActileProvider';
|
||||
import { providerCommands } from './actile/providerCommands';
|
||||
import { useActileManager } from './actile/useActileManager';
|
||||
|
||||
import type { AttachmentId } from './attachments/store-attachments';
|
||||
import { Attachments } from './attachments/Attachments';
|
||||
import { getTextBlockText, useLLMAttachments } from './attachments/useLLMAttachments';
|
||||
import { useAttachments } from './attachments/useAttachments';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './composer.types';
|
||||
import { ButtonAttachCameraMemo } from './ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from './ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './ButtonAttachFile';
|
||||
import { ButtonCall } from './ButtonCall';
|
||||
import { ButtonMicContinuationMemo } from './ButtonMicContinuation';
|
||||
import { ButtonMicMemo } from './ButtonMic';
|
||||
import { ButtonOptionsDraw } from './ButtonOptionsDraw';
|
||||
import { ButtonAttachCameraMemo } from './buttons/ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
|
||||
import { ButtonCall } from './buttons/ButtonCall';
|
||||
import { ButtonMicContinuationMemo } from './buttons/ButtonMicContinuation';
|
||||
import { ButtonMicMemo } from './buttons/ButtonMic';
|
||||
import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
|
||||
import { ChatModeMenu } from './ChatModeMenu';
|
||||
import { TokenBadgeMemo } from './TokenBadge';
|
||||
import { TokenProgressbarMemo } from './TokenProgressbar';
|
||||
@@ -89,7 +94,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 +103,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,24 +172,6 @@ export function Composer(props: {
|
||||
return enqueued;
|
||||
}, [clearAttachments, conversationId, llmAttachments, onAction, setComposeText]);
|
||||
|
||||
const handleTextareaKeyDown = React.useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
// Alt: append the message instead
|
||||
if (e.altKey) {
|
||||
handleSendAction('append-user', composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
// Shift: toggles the 'enter is newline'
|
||||
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
|
||||
if (!assistantTyping)
|
||||
handleSendAction(chatModeId, composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
}
|
||||
}, [assistantTyping, chatModeId, composeText, enterIsNewline, handleSendAction]);
|
||||
|
||||
const handleSendClicked = () => handleSendAction(chatModeId, composeText);
|
||||
|
||||
const handleStopClicked = () => props.conversationId && stopTyping(props.conversationId);
|
||||
@@ -194,7 +181,7 @@ export function Composer(props: {
|
||||
|
||||
const handleCallClicked = () => props.conversationId && systemPurposeId && launchAppCall(props.conversationId, systemPurposeId);
|
||||
|
||||
const handleDrawOptionsClicked = () => openPreferences(2);
|
||||
const handleDrawOptionsClicked = () => openPreferencesTab(2);
|
||||
|
||||
const handleTextImagineClicked = () => {
|
||||
if (!composeText || !props.conversationId)
|
||||
@@ -217,6 +204,67 @@ export function Composer(props: {
|
||||
};
|
||||
|
||||
|
||||
// Actiles
|
||||
|
||||
const onActileCommandSelect = React.useCallback((item: ActileItem) => {
|
||||
if (props.composerTextAreaRef.current) {
|
||||
const textArea = props.composerTextAreaRef.current;
|
||||
const currentText = textArea.value;
|
||||
const cursorPos = textArea.selectionStart;
|
||||
|
||||
// Find the position where the command starts
|
||||
const commandStart = currentText.lastIndexOf('/', cursorPos);
|
||||
|
||||
// Construct the new text with the autocompleted command
|
||||
const newText = currentText.substring(0, commandStart) + item.label + ' ' + currentText.substring(cursorPos);
|
||||
|
||||
// Update the text area with the new text
|
||||
setComposeText(newText);
|
||||
|
||||
// Move the cursor to the end of the autocompleted command
|
||||
const newCursorPos = commandStart + item.label.length + 1;
|
||||
textArea.setSelectionRange(newCursorPos, newCursorPos);
|
||||
}
|
||||
}, [props.composerTextAreaRef, setComposeText]);
|
||||
|
||||
const actileProviders: ActileProvider[] = React.useMemo(() => {
|
||||
return [providerCommands(onActileCommandSelect)];
|
||||
}, [onActileCommandSelect]);
|
||||
|
||||
const { actileComponent, actileInterceptKeydown } = useActileManager(actileProviders, props.composerTextAreaRef);
|
||||
|
||||
|
||||
// Text typing
|
||||
|
||||
const handleTextareaTextChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setComposeText(e.target.value);
|
||||
}, [setComposeText]);
|
||||
|
||||
const handleTextareaKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// disable keyboard handling if the actile is visible
|
||||
if (actileInterceptKeydown(e))
|
||||
return;
|
||||
|
||||
// Enter: primary action
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
// Alt: append the message instead
|
||||
if (e.altKey) {
|
||||
handleSendAction('append-user', composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
// Shift: toggles the 'enter is newline'
|
||||
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
|
||||
if (!assistantAbortible)
|
||||
handleSendAction(chatModeId, composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction]);
|
||||
|
||||
|
||||
// Mic typing & continuation mode
|
||||
|
||||
const onSpeechResultCallback = React.useCallback((result: SpeechResult) => {
|
||||
@@ -233,7 +281,7 @@ export function Composer(props: {
|
||||
nextText = nextText ? nextText + ' ' + transcript : transcript;
|
||||
|
||||
// auto-send (mic continuation mode) if requested
|
||||
const autoSend = micContinuation && nextText.length >= 1 && !!props.conversationId; //&& assistantTyping;
|
||||
const autoSend = micContinuation && nextText.length >= 1 && !!props.conversationId; //&& assistantAbortible;
|
||||
const notUserStop = result.doneReason !== 'manual';
|
||||
if (autoSend) {
|
||||
if (notUserStop)
|
||||
@@ -255,7 +303,7 @@ export function Composer(props: {
|
||||
useGlobalShortcut('m', true, false, false, toggleRecording);
|
||||
|
||||
const micIsRunning = !!speechInterimResult;
|
||||
const micContinuationTrigger = micContinuation && !micIsRunning && !assistantTyping;
|
||||
const micContinuationTrigger = micContinuation && !micIsRunning && !assistantAbortible;
|
||||
const micColor: ColorPaletteProp = isSpeechError ? 'danger' : isRecordingSpeech ? 'primary' : isRecordingAudio ? 'primary' : 'neutral';
|
||||
const micVariant: VariantProp = isRecordingSpeech ? 'solid' : isRecordingAudio ? 'soft' : 'soft'; //(isDesktop ? 'soft' : 'plain');
|
||||
|
||||
@@ -367,7 +415,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 +440,7 @@ export function Composer(props: {
|
||||
|
||||
{/* Vertical (insert) buttons */}
|
||||
{isMobile ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { md: 2 } }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
|
||||
{/* [mobile] Mic button */}
|
||||
{isSpeechEnabled && <ButtonMicMemo variant={micVariant} color={micColor} onClick={handleToggleMic} />}
|
||||
@@ -406,7 +456,7 @@ export function Composer(props: {
|
||||
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { md: 2 } }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
{/*<FormHelperText sx={{ mx: 'auto' }}>*/}
|
||||
{/* Attach*/}
|
||||
@@ -428,7 +478,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 +490,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 +515,7 @@ export function Composer(props: {
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
lineHeight: 1.75,
|
||||
lineHeight: lineHeightTextarea,
|
||||
}} />
|
||||
|
||||
{tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
|
||||
@@ -567,7 +619,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 +631,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 +650,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 +667,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>
|
||||
@@ -647,6 +703,9 @@ export function Composer(props: {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Actile */}
|
||||
{actileComponent}
|
||||
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, ListItem, ListItemButton, ListItemDecorator, Sheet, Typography } from '@mui/joy';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
|
||||
import type { ActileItem } from './ActileProvider';
|
||||
|
||||
|
||||
export function ActilePopup(props: {
|
||||
anchorEl: HTMLElement | null,
|
||||
onClose: () => void,
|
||||
title?: string,
|
||||
items: ActileItem[],
|
||||
activeItemIndex: number | undefined,
|
||||
activePrefixLength: number,
|
||||
onItemClick: (item: ActileItem) => void,
|
||||
children?: React.ReactNode
|
||||
}) {
|
||||
|
||||
const hasAnyIcon = props.items.some(item => !!item.Icon);
|
||||
|
||||
return (
|
||||
<CloseableMenu open anchorEl={props.anchorEl} onClose={props.onClose} noTopPadding>
|
||||
|
||||
{!!props.title && (
|
||||
<Sheet variant='soft' sx={{ p: 1, borderBottom: '1px solid', borderBottomColor: 'neutral.softActiveBg' }}>
|
||||
{/*<ListItemDecorator/>*/}
|
||||
<Typography level='title-md'>
|
||||
{props.title}
|
||||
</Typography>
|
||||
</Sheet>
|
||||
)}
|
||||
|
||||
{!props.items.length && (
|
||||
<ListItem variant='soft' color='primary'>
|
||||
<Typography level='body-md'>
|
||||
No matching command
|
||||
</Typography>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{props.items.map((item, idx) => {
|
||||
const labelBold = item.label.slice(0, props.activePrefixLength);
|
||||
const labelNormal = item.label.slice(props.activePrefixLength);
|
||||
return (
|
||||
<ListItemButton
|
||||
key={item.id}
|
||||
variant={idx === props.activeItemIndex ? 'soft' : undefined}
|
||||
onClick={() => props.onItemClick(item)}
|
||||
>
|
||||
{hasAnyIcon && (
|
||||
<ListItemDecorator>
|
||||
{item.Icon ? <item.Icon /> : null}
|
||||
</ListItemDecorator>
|
||||
)}
|
||||
<Box>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<Typography level='title-md' color='primary'>
|
||||
<span style={{ fontWeight: 600, textDecoration: 'underline' }}>{labelBold}</span>{labelNormal}
|
||||
</Typography>
|
||||
{item.argument && <Typography level='body-sm'>
|
||||
{item.argument}
|
||||
</Typography>}
|
||||
</Box>
|
||||
|
||||
{!!item.description && <Typography level='body-xs'>
|
||||
{item.description}
|
||||
</Typography>}
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
);
|
||||
},
|
||||
)}
|
||||
|
||||
{props.children}
|
||||
|
||||
</CloseableMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { FunctionComponent } from 'react';
|
||||
|
||||
export interface ActileItem {
|
||||
id: string;
|
||||
label: string;
|
||||
argument?: string;
|
||||
description?: string;
|
||||
Icon?: FunctionComponent;
|
||||
}
|
||||
|
||||
type ActileProviderIds = 'actile-commands' | 'actile-attach-reference';
|
||||
|
||||
export interface ActileProvider {
|
||||
id: ActileProviderIds;
|
||||
title: string;
|
||||
|
||||
checkTriggerText: (trailingText: string) => boolean;
|
||||
|
||||
fetchItems: () => Promise<ActileItem[]>;
|
||||
onItemSelect: (item: ActileItem) => void;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
|
||||
|
||||
export const providerAttachReference: ActileProvider = {
|
||||
id: 'actile-attach-reference',
|
||||
title: 'Attach Reference',
|
||||
|
||||
checkTriggerText: (trailingText: string) =>
|
||||
trailingText.endsWith(' @'),
|
||||
|
||||
fetchItems: async () => {
|
||||
return [{
|
||||
id: 'test-1',
|
||||
label: 'Attach This',
|
||||
description: 'Attach this to the message',
|
||||
Icon: undefined,
|
||||
}];
|
||||
},
|
||||
|
||||
onItemSelect: (item: ActileItem) => {
|
||||
console.log('Selected item:', item);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
import { findAllChatCommands } from '../../../commands/commands.registry';
|
||||
|
||||
|
||||
export const providerCommands = (onItemSelect: (item: ActileItem) => void): ActileProvider => ({
|
||||
id: 'actile-commands',
|
||||
title: 'Chat Commands',
|
||||
|
||||
checkTriggerText: (trailingText: string) =>
|
||||
trailingText.trim() === '/',
|
||||
|
||||
fetchItems: async () => {
|
||||
return findAllChatCommands().map((cmd) => ({
|
||||
id: cmd.primary,
|
||||
label: cmd.primary,
|
||||
argument: cmd.arguments?.join(' ') ?? undefined,
|
||||
description: cmd.description,
|
||||
Icon: cmd.Icon,
|
||||
}));
|
||||
},
|
||||
|
||||
onItemSelect,
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
import * as React from 'react';
|
||||
import { ActileItem, ActileProvider } from './ActileProvider';
|
||||
import { ActilePopup } from './ActilePopup';
|
||||
|
||||
|
||||
export const useActileManager = (providers: ActileProvider[], anchorRef: React.RefObject<HTMLElement>) => {
|
||||
|
||||
// state
|
||||
const [popupOpen, setPopupOpen] = React.useState(false);
|
||||
const [provider, setProvider] = React.useState<ActileProvider | null>(null);
|
||||
|
||||
const [items, setItems] = React.useState<ActileItem[]>([]);
|
||||
const [activeSearchString, setActiveSearchString] = React.useState<string>('');
|
||||
const [activeItemIndex, setActiveItemIndex] = React.useState<number>(0);
|
||||
|
||||
|
||||
// derived state
|
||||
const activeItems = React.useMemo(() => {
|
||||
const search = activeSearchString.trim().toLowerCase();
|
||||
return items.filter(item => item.label.toLowerCase().startsWith(search));
|
||||
}, [items, activeSearchString]);
|
||||
const activeItem = activeItemIndex >= 0 && activeItemIndex < activeItems.length ? activeItems[activeItemIndex] : null;
|
||||
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
setPopupOpen(false);
|
||||
setProvider(null);
|
||||
setItems([]);
|
||||
setActiveSearchString('');
|
||||
setActiveItemIndex(0);
|
||||
}, []);
|
||||
|
||||
const handlePopupItemClicked = React.useCallback((item: ActileItem) => {
|
||||
provider?.onItemSelect(item);
|
||||
handleClose();
|
||||
}, [handleClose, provider]);
|
||||
|
||||
const handleEnterKey = React.useCallback(() => {
|
||||
activeItem && handlePopupItemClicked(activeItem);
|
||||
}, [activeItem, handlePopupItemClicked]);
|
||||
|
||||
|
||||
const actileInterceptKeydown = React.useCallback((_event: React.KeyboardEvent<HTMLTextAreaElement>): boolean => {
|
||||
|
||||
// Popup open: Intercept
|
||||
|
||||
const { key, currentTarget, ctrlKey, metaKey } = _event;
|
||||
|
||||
if (popupOpen) {
|
||||
if (key === 'Escape' || key === 'ArrowLeft') {
|
||||
_event.preventDefault();
|
||||
handleClose();
|
||||
} else if (key === 'ArrowUp') {
|
||||
_event.preventDefault();
|
||||
setActiveItemIndex((prevIndex) => (prevIndex > 0 ? prevIndex - 1 : activeItems.length - 1));
|
||||
} else if (key === 'ArrowDown') {
|
||||
_event.preventDefault();
|
||||
setActiveItemIndex((prevIndex) => (prevIndex < activeItems.length - 1 ? prevIndex + 1 : 0));
|
||||
} else if (key === 'Enter' || key === 'ArrowRight' || key === 'Tab' || (key === ' ' && activeItems.length === 1)) {
|
||||
_event.preventDefault();
|
||||
handleEnterKey();
|
||||
} else if (key === 'Backspace') {
|
||||
handleClose();
|
||||
} else if (key.length === 1 && !ctrlKey && !metaKey) {
|
||||
setActiveSearchString((prev) => prev + key);
|
||||
setActiveItemIndex(0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Popup closed: Check for triggers
|
||||
|
||||
// optimization
|
||||
if (key !== '/' && key !== '@')
|
||||
return false;
|
||||
|
||||
const trailingText = (currentTarget.value || '') + key;
|
||||
|
||||
// check all rules to find one that triggers
|
||||
for (const provider of providers) {
|
||||
if (provider.checkTriggerText(trailingText)) {
|
||||
setProvider(provider);
|
||||
setPopupOpen(true);
|
||||
setActiveSearchString(key);
|
||||
provider
|
||||
.fetchItems()
|
||||
.then(items => setItems(items))
|
||||
.catch(error => {
|
||||
handleClose();
|
||||
console.error('Failed to fetch popup items:', error);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [activeItems.length, handleClose, handleEnterKey, popupOpen, providers]);
|
||||
|
||||
|
||||
const actileComponent = React.useMemo(() => {
|
||||
return !popupOpen ? null : (
|
||||
<ActilePopup
|
||||
anchorEl={anchorRef.current}
|
||||
onClose={handleClose}
|
||||
title={provider?.title}
|
||||
items={activeItems}
|
||||
activeItemIndex={activeItemIndex}
|
||||
activePrefixLength={activeSearchString.length}
|
||||
onItemClick={handlePopupItemClicked}
|
||||
/>
|
||||
);
|
||||
}, [activeItemIndex, activeItems, activeSearchString.length, anchorRef, handleClose, handlePopupItemClicked, popupOpen, provider?.title]);
|
||||
|
||||
return {
|
||||
actileComponent,
|
||||
actileInterceptKeydown,
|
||||
};
|
||||
};
|
||||
@@ -111,7 +111,7 @@ export function Attachments(props: {
|
||||
|
||||
{/* Overall Menu button */}
|
||||
<IconButton
|
||||
variant='plain' onClick={handleOverallMenuToggle}
|
||||
onClick={handleOverallMenuToggle}
|
||||
sx={{
|
||||
// borderRadius: 'sm',
|
||||
borderRadius: 0,
|
||||
|
||||
@@ -11,6 +11,16 @@ import type { ComposerOutputMultiPart } from '../composer.types';
|
||||
// extensions to treat as plain text
|
||||
const PLAIN_TEXT_EXTENSIONS: string[] = ['.ts', '.tsx'];
|
||||
|
||||
// mimetypes to treat as plain text
|
||||
const PLAIN_TEXT_MIMETYPES: string[] = [
|
||||
'text/plain',
|
||||
'text/html',
|
||||
'text/markdown',
|
||||
'text/csv',
|
||||
'text/css',
|
||||
'application/json',
|
||||
];
|
||||
|
||||
/**
|
||||
* Creates a new Attachment object.
|
||||
*/
|
||||
@@ -141,7 +151,7 @@ export function attachmentDefineConverters(sourceType: AttachmentSource['media']
|
||||
switch (true) {
|
||||
|
||||
// plain text types
|
||||
case ['text/plain', 'text/html', 'text/markdown', 'text/csv', 'application/json'].includes(input.mimeType):
|
||||
case PLAIN_TEXT_MIMETYPES.includes(input.mimeType):
|
||||
// handle a secondary layer of HTML 'text' origins: drop, paste, and clipboard-read
|
||||
const textOriginHtml = sourceType === 'text' && input.altMimeType === 'text/html' && !!input.altData;
|
||||
const isHtmlTable = !!input.altData?.startsWith('<table');
|
||||
|
||||
+2
-2
@@ -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 > {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>
|
||||
);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { Box, Typography } from '@mui/joy';
|
||||
|
||||
import { createConversationFromJsonV1 } from '~/modules/trade/trade.client';
|
||||
import { useHasChatLinkItems } from '~/modules/trade/store-module-trade';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
@@ -16,7 +15,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';
|
||||
|
||||
@@ -79,14 +78,14 @@ export function AppChatLink(props: { linkId: string }) {
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||
});
|
||||
const hasLinkItems = useHasChatLinkItems();
|
||||
// const hasLinkItems = useHasChatLinkItems();
|
||||
|
||||
|
||||
// pluggable UI
|
||||
|
||||
const drawerItems = React.useMemo(() => <AppChatLinkDrawerItems />, []);
|
||||
const drawerContent = React.useMemo(() => <AppChatLinkDrawerContent />, []);
|
||||
const menuItems = React.useMemo(() => <AppChatLinkMenuItems />, []);
|
||||
usePluggableOptimaLayout(hasLinkItems ? drawerItems : null, null, menuItems, 'AppChatLink');
|
||||
usePluggableOptimaLayout(drawerContent, null, menuItems, 'AppChatLink');
|
||||
|
||||
|
||||
const pageTitle = (data?.conversation && conversationTitle(data.conversation)) || 'Chat Link';
|
||||
|
||||
+21
-18
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { Box, ListDivider, ListItem, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
|
||||
import { Box, ListDivider, ListItem, ListItemButton, ListItemDecorator, Typography } from '@mui/joy';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
|
||||
import { useChatLinkItems } from '~/modules/trade/store-module-trade';
|
||||
@@ -9,44 +9,47 @@ 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}
|
||||
component={Link} href={ROUTE_INDEX} noLinkStyle
|
||||
>
|
||||
<ListItemDecorator><ArrowBackIcon /></ListItemDecorator>
|
||||
{Brand.Title.Base}
|
||||
</MenuItem>
|
||||
{notEmpty && (
|
||||
<ListItemButton
|
||||
onClick={closeDrawerOnMobile}
|
||||
component={Link} href={ROUTE_INDEX} noLinkStyle
|
||||
>
|
||||
<ListItemDecorator><ArrowBackIcon /></ListItemDecorator>
|
||||
{Brand.Title.Base}
|
||||
</ListItemButton>
|
||||
)}
|
||||
|
||||
{notEmpty && <ListDivider />}
|
||||
|
||||
{notEmpty && <ListItem>
|
||||
<ListItem>
|
||||
<Typography level='body-sm'>
|
||||
Links shared by you
|
||||
{notEmpty ? 'Links shared by you' : 'No prior shared links'}
|
||||
</Typography>
|
||||
</ListItem>}
|
||||
</ListItem>
|
||||
|
||||
{notEmpty && <Box sx={{ overflowY: 'auto' }}>
|
||||
{chatLinkItems.map(item => (
|
||||
|
||||
<MenuItem
|
||||
<ListItemButton
|
||||
key={'chat-link-' + item.objectId}
|
||||
component={Link} href={getChatLinkRelativePath(item.objectId)} noLinkStyle
|
||||
sx={{
|
||||
@@ -60,10 +63,10 @@ export function AppChatLinkDrawerItems() {
|
||||
<Typography level='body-xs'>
|
||||
<TimeAgo date={item.createdAt} />
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</ListItemButton>
|
||||
|
||||
))}
|
||||
</Box>}
|
||||
</>;
|
||||
</PageDrawerList>;
|
||||
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 = 12;
|
||||
|
||||
const B = (props: { href?: string, children: React.ReactNode }) => {
|
||||
const boldText = <Typography color={!!props.href ? 'primary' : 'neutral'} sx={{ fontWeight: 600 }}>{props.children}</Typography>;
|
||||
@@ -30,7 +30,7 @@ export const newsCallout =
|
||||
<Typography level='title-lg'>
|
||||
Open Roadmap
|
||||
</Typography>
|
||||
<Typography level='body-md'>
|
||||
<Typography level='body-sm'>
|
||||
Take a peek at our roadmap to see what's in the pipeline.
|
||||
Discover upcoming features and let us know what excites you the most!
|
||||
</Typography>
|
||||
@@ -59,10 +59,38 @@ export const newsCallout =
|
||||
// news and feature surfaces
|
||||
export const NewsItems: NewsItem[] = [
|
||||
// still unannounced: phone calls, split windows, ...
|
||||
{
|
||||
versionCode: '1.11.0',
|
||||
versionName: 'Singularity',
|
||||
versionMoji: '🌌🌠',
|
||||
versionDate: new Date('2024-01-16T06:30:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/329'}>Search</B> past conversations (@joriskalz) 🔍</>, issue: 329 },
|
||||
{ text: <>Quick <B href={RIssues + '/327'}>commands pane</B> (open with '/')</>, issue: 327 },
|
||||
{ text: <><B>Together AI</B> Inference platform support</>, issue: 346 },
|
||||
{ text: <>Persona creation: <B href={RIssues + '/301'}>history</B></>, issue: 301 },
|
||||
{ text: <>Persona creation: fix <B href={RIssues + '/328'}>API timeouts</B></>, issue: 328 },
|
||||
{ text: <>Support up to five <B href={RIssues + '/323'}>OpenAI-compatible</B> endpoints</>, issue: 323 },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.10.0',
|
||||
versionName: 'The Year of AGI',
|
||||
// versionMoji: '🎊✨',
|
||||
versionDate: new Date('2024-01-06T08:00:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/201'}>New UI</B> for desktop and mobile, enabling future expansions</>, issue: 201 },
|
||||
{ text: <><B href={RIssues + '/321'}>Folder categorization</B> for conversation management</>, issue: 321 },
|
||||
{ text: <><B>LM Studio</B> support and refined token management</> },
|
||||
{ text: <>Draggable panes in split screen mode</>, issue: 308 },
|
||||
{ text: <>Bug fixes and UI polish</> },
|
||||
{ text: <>Developers: document proxy settings on docker</>, issue: 318, dev: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.9.0',
|
||||
versionName: 'Creative Horizons',
|
||||
versionMoji: '🎨🌌',
|
||||
// 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 },
|
||||
|
||||
@@ -3,11 +3,33 @@ import * as React from 'react';
|
||||
import { Container, ListDivider, Sheet, Typography } from '@mui/joy';
|
||||
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { PersonaCreator } from './PersonaCreator';
|
||||
import { Creator } from './creator/Creator';
|
||||
import { CreatorDrawer } from './creator/CreatorDrawer';
|
||||
import { Viewer } from './creator/Viewer';
|
||||
|
||||
|
||||
export function AppPersonas() {
|
||||
|
||||
// state
|
||||
const [selectedSimplePersonaId, setSelectedSimplePersonaId] = React.useState<string | null>(null);
|
||||
|
||||
|
||||
// pluggable UI
|
||||
|
||||
const drawerContent = React.useMemo(() => {
|
||||
return (
|
||||
<CreatorDrawer
|
||||
selectedSimplePersonaId={selectedSimplePersonaId}
|
||||
setSelectedSimplePersonaId={setSelectedSimplePersonaId}
|
||||
/>
|
||||
);
|
||||
}, [selectedSimplePersonaId]);
|
||||
|
||||
usePluggableOptimaLayout(drawerContent, null, null, 'AppPersonas');
|
||||
|
||||
|
||||
return (
|
||||
<Sheet sx={{
|
||||
flexGrow: 1,
|
||||
@@ -24,7 +46,9 @@ export function AppPersonas() {
|
||||
|
||||
<ListDivider sx={{ my: 2 }} />
|
||||
|
||||
<PersonaCreator />
|
||||
{!!selectedSimplePersonaId && <Viewer selectedSimplePersonaId={selectedSimplePersonaId} />}
|
||||
|
||||
<Creator display={!selectedSimplePersonaId} />
|
||||
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -1,306 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, Input, LinearProgress, Tab, TabList, TabPanel, Tabs, Textarea, Typography } from '@mui/joy';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
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 { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
|
||||
|
||||
import { LLMChainStep, useLLMChain } from './useLLMChain';
|
||||
|
||||
|
||||
function extractVideoID(videoURL: string): string | null {
|
||||
const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^#&?]*).*/;
|
||||
const match = videoURL.match(regExp);
|
||||
return (match && match[1]?.length == 11) ? match[1] : null;
|
||||
}
|
||||
|
||||
|
||||
function useTranscriptFromVideo(videoID: string | null) {
|
||||
const { data, isFetching, isError, error } =
|
||||
apiQuery.ytpersona.getTranscript.useQuery({ videoId: videoID || '' }, {
|
||||
enabled: !!videoID,
|
||||
refetchOnWindowFocus: false,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
return {
|
||||
title: data?.videoTitle ?? null,
|
||||
thumbnailUrl: data?.thumbnailUrl ?? null,
|
||||
transcript: data?.transcript?.trim() ?? null,
|
||||
isFetching,
|
||||
isError, error,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const PersonaCreationSteps: LLMChainStep[] = [
|
||||
{
|
||||
name: 'Analyzing the transcript / text',
|
||||
setSystem: 'You are skilled in analyzing and embodying diverse characters. You meticulously study transcripts to capture key attributes, draft comprehensive character sheets, and refine them for authenticity. Feel free to make assumptions without hedging, be concise and be creative.',
|
||||
addUserInput: true,
|
||||
addUser: 'Conduct comprehensive research on the provided transcript. Identify key characteristics of the speaker, including age, professional field, distinct personality traits, style of communication, narrative context, and self-awareness. Additionally, consider any unique aspects such as their use of humor, their cultural background, core values, passions, fears, personal history, and social interactions. Your output for this stage is an in-depth written analysis that exhibits an understanding of both the superficial and more profound aspects of the speaker\'s persona.',
|
||||
},
|
||||
{
|
||||
name: 'Defining the character',
|
||||
addPrevAssistant: true,
|
||||
addUser: 'Craft your documented analysis into a draft of the \'You are a...\' character sheet. It should encapsulate all crucial personality dimensions, along with the motivations and aspirations of the persona. Keep in mind to balance succinctness and depth of detail for each dimension. The deliverable here is a comprehensive draft of the character sheet that captures the speaker\'s unique essence.',
|
||||
},
|
||||
{
|
||||
name: 'Crossing the t\'s',
|
||||
addPrevAssistant: true,
|
||||
addUser: 'Compare the draft character sheet with the original transcript, validating its content and ensuring it captures both the speaker’s overt characteristics and the subtler undertones. Omit unknown information, fine-tune any areas that require clarity, have been overlooked, or require more authenticity. Use clear and illustrative examples from the transcript to refine your sheet and offer meaningful, tangible reference points. Your output is a coherent, comprehensive, and nuanced instruction that begins with \'You are a...\' and serves as a go-to guide for an actor recreating the persona.',
|
||||
},
|
||||
// {
|
||||
// name: 'Shrink',
|
||||
// addPrevAssistant: true,
|
||||
// addUser: 'Now remove all the uncertain information, omit unknown information, Your output is a coherent, comprehensive, and nuanced instruction that begins with \'You are a...\' and serves as a go-to guide for a recreating the persona.',
|
||||
// },
|
||||
];
|
||||
|
||||
|
||||
export function PersonaCreator() {
|
||||
// state
|
||||
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');
|
||||
|
||||
// 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]);
|
||||
|
||||
// Reset the relevant state when the selected tab changes
|
||||
React.useEffect(() => {
|
||||
// reset state
|
||||
setVideoURL('');
|
||||
setVideoID('');
|
||||
setPersonaTranscript(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);
|
||||
|
||||
const handleVideoIdChange = (e: React.ChangeEvent<HTMLInputElement>) => setVideoURL(e.target.value);
|
||||
|
||||
const handleFetchTranscript = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault(); // stop the form submit
|
||||
const videoId = extractVideoID(videoURL);
|
||||
if (!videoId) {
|
||||
setVideoURL('Invalid');
|
||||
} else {
|
||||
setPersonaTranscript(null);
|
||||
setVideoID(videoId);
|
||||
}
|
||||
};
|
||||
|
||||
// New handler for persona text change
|
||||
const handlePersonaTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setPersonaText(e.target.value);
|
||||
};
|
||||
|
||||
return <>
|
||||
|
||||
<Typography level='title-sm' mb={3}>
|
||||
Create the <em>System Prompt</em> of an AI Persona from YouTube or Text.
|
||||
</Typography>
|
||||
|
||||
<Tabs defaultValue={0} variant='outlined'
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => setSelectedTab(newValue as number)}>
|
||||
<TabList sx={{ minHeight: 48 }}>
|
||||
<Tab>From YouTube Video</Tab>
|
||||
<Tab>From Text</Tab>
|
||||
</TabList>
|
||||
|
||||
{/* YouTube URL inputs */}
|
||||
<TabPanel value={0} sx={{ p: 3 }}>
|
||||
|
||||
<Typography level='title-md' startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />} sx={{ mb: 3 }}>
|
||||
YouTube -> Persona
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleFetchTranscript}>
|
||||
<Input
|
||||
required
|
||||
type='url'
|
||||
fullWidth
|
||||
variant='outlined'
|
||||
placeholder='YouTube Video URL'
|
||||
value={videoURL}
|
||||
onChange={handleVideoIdChange}
|
||||
sx={{ mb: 1.5 }}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Button type='submit' variant='solid' disabled={isFetching || isTransforming || !videoURL} loading={isFetching} sx={{ minWidth: 140 }}>
|
||||
Create
|
||||
</Button>
|
||||
<GoodTooltip title='This example comes from the popular Fireship YouTube channel, which presents technical topics with irreverent humor.'>
|
||||
<Button variant='outlined' color='neutral' onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}>
|
||||
Example
|
||||
</Button>
|
||||
</GoodTooltip>
|
||||
</Box>
|
||||
</form>
|
||||
</TabPanel>
|
||||
|
||||
{/* Text area for users to paste copied text */}
|
||||
<TabPanel value={1} sx={{ p: 3 }}>
|
||||
|
||||
<Typography level='title-md' startDecorator={<TextFieldsIcon />} sx={{ mb: 3 }}>
|
||||
<b>Text</b> -> Persona
|
||||
</Typography>
|
||||
|
||||
<Textarea
|
||||
variant='outlined'
|
||||
minRows={4} maxRows={8}
|
||||
placeholder='Paste your text here...'
|
||||
value={personaText}
|
||||
onChange={handlePersonaTextChange}
|
||||
sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
lineHeight: 1.75,
|
||||
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 }}>
|
||||
Create
|
||||
</Button>
|
||||
{!!personaText?.length && <Typography level='body-sm'>{personaText.length.toLocaleString()}</Typography>}
|
||||
</Box>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
{/* LLM selector (chat vs fast) */}
|
||||
{!isTransforming && !isFinished && <Box sx={{ mt: 3 }}>{llmComponent}</Box>}
|
||||
|
||||
{/* Errors */}
|
||||
{isError && (
|
||||
<Alert color='warning' sx={{ mt: 1 }}>
|
||||
<Typography component='div'>{transcriptError?.message || 'Unknown error'}</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
{!!chainError && (
|
||||
<Alert color='warning' sx={{ mt: 1 }}>
|
||||
<Typography component='div'>{chainError}</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Persona! */}
|
||||
{chainOutput && <>
|
||||
<Card sx={{ boxShadow: 'md', mt: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography level='title-lg' color='success' startDecorator={<SettingsAccessibilityIcon color='success' />}>
|
||||
Persona Prompt
|
||||
</Typography>
|
||||
<GoodTooltip title='Copy system prompt'>
|
||||
<Button color='success' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')} endDecorator={<ContentCopyIcon />} sx={{ minWidth: 120 }}>
|
||||
Copy
|
||||
</Button>
|
||||
</GoodTooltip>
|
||||
</Box>
|
||||
<CardContent>
|
||||
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
|
||||
You may now copy the text below and use it as Custom prompt!
|
||||
</Alert>
|
||||
<Typography level='title-sm' sx={{ lineHeight: 1.75 }}>
|
||||
{chainOutput}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>}
|
||||
|
||||
{/* Input: Transcript*/}
|
||||
{personaTranscript && <>
|
||||
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
|
||||
Input Data
|
||||
</Typography>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
{title || 'Transcript / Text'}
|
||||
</Typography>
|
||||
<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)}...
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>}
|
||||
|
||||
{/* Intermediate outputs rendered as cards in a grid */}
|
||||
{chainIntermediates && chainIntermediates.length > 0 && <>
|
||||
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
|
||||
{isTransforming ? 'Working...' : 'Intermediate Work'}
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{chainIntermediates.map((intermediate, i) =>
|
||||
<Grid xs={12} sm={6} md={4} key={i}>
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography level='title-sm' sx={{ mb: 1 }}>
|
||||
{i + 1}. {PersonaCreationSteps[i].name}
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{intermediate?.slice(0, 140)}...
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>,
|
||||
)}
|
||||
</Grid>
|
||||
</>}
|
||||
|
||||
|
||||
{/* Dialog: Embodiment Progress */}
|
||||
{isTransforming && <GoodModal open>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 2 }}>
|
||||
<CircularProgress color='primary' value={Math.max(10, 100 * chainProgress)} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography color='success' level='title-lg'>
|
||||
Embodying Persona ...
|
||||
</Typography>
|
||||
<Typography level='title-sm' sx={{ mt: 1 }}>
|
||||
Using: {personaLlm?.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography color='success' level='title-sm' sx={{ fontWeight: 600 }}>
|
||||
{chainStepName}
|
||||
</Typography>
|
||||
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1.5 }} />
|
||||
</Box>
|
||||
<Typography level='title-sm'>
|
||||
This may take 1-2 minutes. Do not close this window or the progress will be lost.
|
||||
While larger models will produce higher quality prompts,
|
||||
if you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
|
||||
please try again with faster/smaller models.
|
||||
</Typography>
|
||||
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 3 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
</GoodModal>}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Alert, Box, Button, Card, CardContent, CircularProgress, Divider, FormLabel, Grid, IconButton, LinearProgress, Tab, TabList, TabPanel, Tabs, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
|
||||
|
||||
import { RenderMarkdown } from '../../chat/components/message/RenderMarkdown';
|
||||
|
||||
import { LLMChainStep, useLLMChain } from '~/modules/aifn/useLLMChain';
|
||||
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { useFormEditTextArray } from '~/common/components/forms/useFormEditTextArray';
|
||||
import { useLLMSelect } from '~/common/components/forms/useLLMSelect';
|
||||
import { useToggleableBoolean } from '~/common/util/useToggleableBoolean';
|
||||
|
||||
import { FromText } from './FromText';
|
||||
import { FromYouTube } from './FromYouTube';
|
||||
import { prependSimplePersona, SimplePersonaProvenance } from '../store-app-personas';
|
||||
|
||||
|
||||
// delay to start a new chain after the previous one finishes
|
||||
const CONTINUE_DELAY: number | false = false;
|
||||
|
||||
|
||||
const Prompts: string[] = [
|
||||
'You are skilled in analyzing and embodying diverse characters. You meticulously study transcripts to capture key attributes, draft comprehensive character sheets, and refine them for authenticity. Feel free to make assumptions without hedging, be concise and be creative.',
|
||||
'Conduct comprehensive research on the provided transcript. Identify key characteristics of the speaker, including age, professional field, distinct personality traits, style of communication, narrative context, and self-awareness. Additionally, consider any unique aspects such as their use of humor, their cultural background, core values, passions, fears, personal history, and social interactions. Your output for this stage is an in-depth written analysis that exhibits an understanding of both the superficial and more profound aspects of the speaker\'s persona.',
|
||||
'Craft your documented analysis into a draft of the \'You are a...\' character sheet. It should encapsulate all crucial personality dimensions, along with the motivations and aspirations of the persona. Keep in mind to balance succinctness and depth of detail for each dimension. The deliverable here is a comprehensive draft of the character sheet that captures the speaker\'s unique essence.',
|
||||
'Compare the draft character sheet with the original transcript, validating its content and ensuring it captures both the speaker’s overt characteristics and the subtler undertones. Omit unknown information, fine-tune any areas that require clarity, have been overlooked, or require more authenticity. Use clear and illustrative examples from the transcript to refine your sheet and offer meaningful, tangible reference points. Your output is a coherent, comprehensive, and nuanced instruction that begins with \'You are a...\' and serves as a go-to guide for an actor recreating the persona.',
|
||||
];
|
||||
|
||||
const PromptTitles: string[] = [
|
||||
'Common: Creator System Prompt',
|
||||
'Analyze the transcript',
|
||||
'Define the character',
|
||||
'Cross the t\'s',
|
||||
];
|
||||
|
||||
// chain to convert a text input string (e.g. youtube transcript) into a persona prompt
|
||||
function createChain(instructions: string[], titles: string[]): LLMChainStep[] {
|
||||
return [
|
||||
{
|
||||
name: titles[1],
|
||||
setSystem: instructions[0],
|
||||
addUserInput: true,
|
||||
addUser: instructions[1],
|
||||
},
|
||||
{
|
||||
name: titles[2],
|
||||
addPrevAssistant: true,
|
||||
addUser: instructions[2],
|
||||
},
|
||||
{
|
||||
name: titles[3],
|
||||
addPrevAssistant: true,
|
||||
addUser: instructions[3],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
export const PersonaPromptCard = (props: { content: string }) =>
|
||||
<Card sx={{ boxShadow: 'md', mt: 3 }}>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography level='title-lg' color='success' startDecorator={<SettingsAccessibilityIcon color='success' />}>
|
||||
Persona Prompt
|
||||
</Typography>
|
||||
<GoodTooltip title='Copy system prompt'>
|
||||
<Button color='success' onClick={() => copyToClipboard(props.content, 'Persona prompt')} endDecorator={<ContentCopyIcon />} sx={{ minWidth: 120 }}>
|
||||
Copy
|
||||
</Button>
|
||||
</GoodTooltip>
|
||||
</Box>
|
||||
|
||||
<CardContent>
|
||||
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
|
||||
You may now copy the text below and use it as Custom prompt!
|
||||
</Alert>
|
||||
<RenderMarkdown textBlock={{ type: 'text', content: props.content }} />
|
||||
</CardContent>
|
||||
</Card>;
|
||||
|
||||
|
||||
export function Creator(props: { display: boolean }) {
|
||||
|
||||
// state
|
||||
const advanced = useToggleableBoolean();
|
||||
const [selectedTab, setSelectedTab] = React.useState(0);
|
||||
const [chainInputText, setChainInputText] = React.useState<string | null>(null);
|
||||
const [inputProvenance, setInputProvenance] = React.useState<SimplePersonaProvenance | null>(null);
|
||||
const [showIntermediates, setShowIntermediates] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const [personaLlm, llmComponent] = useLLMSelect(true, 'Persona Creation Model');
|
||||
|
||||
|
||||
// editable prompts
|
||||
const {
|
||||
strings: editedInstructions, stringEditors: instructionEditors,
|
||||
} = useFormEditTextArray(Prompts, PromptTitles);
|
||||
|
||||
const creationChainSteps = React.useMemo(() => {
|
||||
return createChain(editedInstructions, PromptTitles);
|
||||
}, [editedInstructions]);
|
||||
|
||||
const llmLabel = personaLlm?.label || undefined;
|
||||
const savePersona = React.useCallback((personaPrompt: string, inputText: string) => {
|
||||
prependSimplePersona(personaPrompt, inputText, inputProvenance ?? undefined, llmLabel);
|
||||
}, [inputProvenance, llmLabel]);
|
||||
|
||||
const {
|
||||
// isFinished,
|
||||
isTransforming,
|
||||
chainProgress,
|
||||
chainIntermediates,
|
||||
chainStepName,
|
||||
chainStepInterimChars,
|
||||
chainOutput,
|
||||
chainError,
|
||||
userCancelChain,
|
||||
restartChain,
|
||||
} = useLLMChain(creationChainSteps, personaLlm?.id, chainInputText ?? undefined, savePersona);
|
||||
|
||||
|
||||
// Reset the relevant state when the selected tab changes
|
||||
React.useEffect(() => {
|
||||
setChainInputText(null);
|
||||
}, [selectedTab]);
|
||||
|
||||
|
||||
// [debug] Restart the chain when complete after a delay
|
||||
const debugRestart = !!CONTINUE_DELAY && !isTransforming && (chainProgress === 1 || !!chainError);
|
||||
React.useEffect(() => {
|
||||
if (debugRestart) {
|
||||
const timeout = setTimeout(restartChain, CONTINUE_DELAY);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [debugRestart, restartChain]);
|
||||
|
||||
|
||||
const handleCreate = React.useCallback((text: string, provenance: SimplePersonaProvenance) => {
|
||||
setChainInputText(text);
|
||||
setInputProvenance(provenance);
|
||||
}, []);
|
||||
|
||||
const handleCancel = React.useCallback(() => {
|
||||
setChainInputText(null);
|
||||
setInputProvenance(null);
|
||||
userCancelChain();
|
||||
}, [userCancelChain]);
|
||||
|
||||
|
||||
// Hide the GFX, but not the logic (hooks)
|
||||
if (!props.display)
|
||||
return null;
|
||||
|
||||
return <>
|
||||
|
||||
<Typography level='title-sm' mb={3}>
|
||||
Create the <em>System Prompt</em> of an AI Persona from YouTube or Text.
|
||||
</Typography>
|
||||
|
||||
|
||||
{/* Inputs */}
|
||||
<Tabs
|
||||
variant='outlined'
|
||||
defaultValue={0}
|
||||
value={selectedTab}
|
||||
onChange={(_event, newValue) => setSelectedTab(newValue as number)}
|
||||
sx={{
|
||||
// boxShadow: 'sm',
|
||||
borderRadius: 'md',
|
||||
// overflow: 'hidden',
|
||||
display: isTransforming ? 'none' : undefined,
|
||||
}}
|
||||
>
|
||||
<TabList sx={{ minHeight: '3rem' }}>
|
||||
<Tab>From YouTube Video</Tab>
|
||||
<Tab>From Text</Tab>
|
||||
</TabList>
|
||||
<TabPanel keepMounted value={0} sx={{ p: 3 }}>
|
||||
<FromYouTube isTransforming={isTransforming} onCreate={handleCreate} />
|
||||
</TabPanel>
|
||||
<TabPanel keepMounted value={1} sx={{ p: 3 }}>
|
||||
<FromText isCreating={isTransforming} onCreate={handleCreate} />
|
||||
</TabPanel>
|
||||
|
||||
<Divider orientation='horizontal' />
|
||||
|
||||
<Box sx={{ p: 3, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{llmComponent}
|
||||
|
||||
{advanced.on && (
|
||||
<Box sx={{ my: 1, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
{instructionEditors}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<FormLabel onClick={advanced.toggle} sx={{ textDecoration: 'underline', cursor: 'pointer' }}>
|
||||
{advanced.on ? 'Hide Advanced' : 'Advanced: Prompts'}
|
||||
</FormLabel>
|
||||
</Box>
|
||||
</Tabs>
|
||||
|
||||
|
||||
{/* Embodiment Progress */}
|
||||
{/* <GoodModal open> */}
|
||||
{isTransforming && <Card><CardContent sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 2 }}>
|
||||
<CircularProgress color='primary' value={Math.max(10, 100 * chainProgress)} />
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography color='success' level='title-lg'>
|
||||
Embodying Persona ...
|
||||
</Typography>
|
||||
<Typography level='title-sm' sx={{ mt: 1 }}>
|
||||
Using: {personaLlm?.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography color='success' level='title-sm' sx={{ fontWeight: 600 }}>
|
||||
{chainStepName}
|
||||
</Typography>
|
||||
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1.5 }} />
|
||||
<Typography level='body-sm' sx={{ mt: 1 }}>
|
||||
{chainStepInterimChars === null ? 'Loading ...' : `Generating (${chainStepInterimChars.toLocaleString()} bytes) ...`}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography level='title-sm'>
|
||||
This may take 1-2 minutes.
|
||||
While larger models will produce higher quality prompts,
|
||||
if you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
|
||||
please try again with faster/smaller models.
|
||||
</Typography>
|
||||
<Button variant='soft' color='neutral' onClick={handleCancel} sx={{ ml: 'auto', minWidth: 100, mt: 3 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
</CardContent></Card>}
|
||||
|
||||
|
||||
{/* Errors */}
|
||||
{!!chainError && (
|
||||
<Alert color='warning' sx={{ mt: 1 }}>
|
||||
<Typography component='div'>{chainError}</Typography>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* The Persona (Output) */}
|
||||
{chainOutput && <>
|
||||
<PersonaPromptCard content={chainOutput} />
|
||||
</>}
|
||||
|
||||
|
||||
{/* Input + Intermediate outputs (with expander) */}
|
||||
{(isTransforming || chainIntermediates?.length > 0) && <>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', mt: 3, mb: 0.5, mx: 1 }}>
|
||||
<Typography level='title-lg'>
|
||||
{isTransforming ? 'Working ...' : 'Intermediate Work'}
|
||||
</Typography>
|
||||
<IconButton size='sm' variant={showIntermediates ? 'solid' : 'outlined'} onClick={() => setShowIntermediates(s => !s)}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid xs={12} md={showIntermediates ? 12 : 6}>
|
||||
<Card sx={{ height: '100%', overflow: 'hidden' }}>
|
||||
<CardContent>
|
||||
<Typography color='success' level='title-sm' sx={{ mb: 1 }}>
|
||||
Input Text
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{showIntermediates ? chainInputText : (chainInputText?.slice(0, 280) + '...')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
{chainIntermediates.map((intermediate, i) =>
|
||||
<Grid xs={12} md={showIntermediates ? 12 : 6} key={i}>
|
||||
<Card sx={{ height: '100%', overflow: 'hidden' }}>
|
||||
<CardContent>
|
||||
<Typography color='success' level='title-sm' sx={{ mb: 1 }}>
|
||||
{i + 1}. {intermediate.name}
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{showIntermediates ? intermediate.output : (intermediate.output?.slice(0, 280) + '...')}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>,
|
||||
)}
|
||||
</Grid>
|
||||
</>}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, IconButton, ListItemButton, ListItemDecorator, Sheet, Tooltip, Typography } from '@mui/joy';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import Diversity2Icon from '@mui/icons-material/Diversity2';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
|
||||
import { PageDrawerHeader } from '~/common/layout/optima/components/PageDrawerHeader';
|
||||
import { PageDrawerList } from '~/common/layout/optima/components/PageDrawerList';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
|
||||
import { CreatorDrawerItem } from './CreatorDrawerItem';
|
||||
import { deleteSimplePersona, useSimplePersonas } from '../store-app-personas';
|
||||
|
||||
|
||||
export function CreatorDrawer(props: {
|
||||
selectedSimplePersonaId: string | null,
|
||||
setSelectedSimplePersonaId: (simplePersonaId: string | null) => void,
|
||||
}) {
|
||||
|
||||
// selection mode
|
||||
const [selectMode, setSelectMode] = React.useState(false);
|
||||
const [selectedIds, setSelectedIds] = React.useState<Set<string>>(new Set());
|
||||
|
||||
// external state
|
||||
const { closeDrawer } = useOptimaDrawers();
|
||||
const { simplePersonas } = useSimplePersonas();
|
||||
|
||||
|
||||
// derived state
|
||||
const hasPersonas = simplePersonas.length > 0;
|
||||
|
||||
|
||||
// Simple Persona Operations
|
||||
|
||||
const { setSelectedSimplePersonaId } = props;
|
||||
|
||||
const handleSimplePersonaUnselect = React.useCallback(() => {
|
||||
setSelectedSimplePersonaId(null);
|
||||
}, [setSelectedSimplePersonaId]);
|
||||
|
||||
const handleSimplePersonaDelete = React.useCallback((simplePersonaId: string) => {
|
||||
deleteSimplePersona(simplePersonaId);
|
||||
handleSimplePersonaUnselect();
|
||||
}, [handleSimplePersonaUnselect]);
|
||||
|
||||
|
||||
// Selection
|
||||
|
||||
const handleSelectionClose = React.useCallback(() => {
|
||||
setSelectMode(false);
|
||||
setSelectedIds(new Set());
|
||||
}, []);
|
||||
|
||||
const handleSelectionToggleId = React.useCallback((simplePersonaId: string) => {
|
||||
setSelectedIds(prevSelectedIds => {
|
||||
const newSelectedItems = new Set(prevSelectedIds);
|
||||
if (newSelectedItems.has(simplePersonaId))
|
||||
newSelectedItems.delete(simplePersonaId);
|
||||
else
|
||||
newSelectedItems.add(simplePersonaId);
|
||||
return newSelectedItems;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleSelectionInvert = React.useCallback(() => {
|
||||
setSelectedIds(prevSelectedIds => {
|
||||
const newSelectedIds = new Set(prevSelectedIds);
|
||||
simplePersonas.forEach(persona => {
|
||||
if (newSelectedIds.has(persona.id))
|
||||
newSelectedIds.delete(persona.id);
|
||||
else
|
||||
newSelectedIds.add(persona.id);
|
||||
});
|
||||
return newSelectedIds;
|
||||
});
|
||||
}, [simplePersonas]);
|
||||
|
||||
const handleSelectionDelete = React.useCallback(() => {
|
||||
selectedIds.forEach(simplePersonaId => {
|
||||
deleteSimplePersona(simplePersonaId);
|
||||
});
|
||||
// clear the selection after deletion
|
||||
setSelectedIds(new Set());
|
||||
}, [selectedIds]);
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
{/* Drawer Header */}
|
||||
<PageDrawerHeader
|
||||
title={selectMode ? 'Selection Mode' : 'Recent'}
|
||||
onClose={selectMode ? handleSelectionClose : closeDrawer}
|
||||
startButton={(!hasPersonas || selectMode) ? undefined :
|
||||
<Tooltip title={selectMode ? 'Done' : 'Select'}>
|
||||
<IconButton onClick={selectMode ? handleSelectionClose : () => setSelectMode(true)}>
|
||||
{selectMode ? <DoneIcon /> : <CheckBoxOutlineBlankIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<PageDrawerList
|
||||
variant='plain'
|
||||
noTopPadding noBottomPadding tallRows
|
||||
onClick={handleSimplePersonaUnselect}
|
||||
>
|
||||
|
||||
{selectMode ? (
|
||||
// Selection Header
|
||||
<Sheet variant='soft' color='warning' invertedColors>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', px: 1, minHeight: '3rem' }}>
|
||||
<Button
|
||||
variant='plain'
|
||||
color='warning'
|
||||
startDecorator={selectedIds.size === simplePersonas.length ? <CheckBoxOutlineBlankIcon /> : <CheckBoxIcon />}
|
||||
onClick={handleSelectionInvert}
|
||||
>
|
||||
{selectedIds.size === simplePersonas.length ? 'Select None' : selectedIds.size !== 0 ? 'Invert' : 'Select All'}
|
||||
</Button>
|
||||
<Button
|
||||
variant='solid'
|
||||
color='warning'
|
||||
startDecorator={<DeleteOutlineIcon />}
|
||||
onClick={handleSelectionDelete}
|
||||
disabled={selectedIds.size === 0}
|
||||
sx={{ ml: 'auto' }}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Box>
|
||||
</Sheet>
|
||||
) : (
|
||||
// Create Button
|
||||
<ListItemButton
|
||||
variant={props.selectedSimplePersonaId ? 'plain' : 'soft'}
|
||||
onClick={handleSimplePersonaUnselect}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<Diversity2Icon />
|
||||
</ListItemDecorator>
|
||||
<Typography level='title-sm' sx={!props.selectedSimplePersonaId ? { fontWeight: 600 } : undefined}>
|
||||
Create
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
)}
|
||||
|
||||
{/* Personas [] */}
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
{simplePersonas.map(item =>
|
||||
<CreatorDrawerItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
isActive={item.id === props.selectedSimplePersonaId}
|
||||
isSelected={selectedIds.has(item.id)}
|
||||
isSelection={selectMode}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
if (selectMode)
|
||||
handleSelectionToggleId(item.id);
|
||||
else
|
||||
props.setSelectedSimplePersonaId(item.id);
|
||||
}}
|
||||
onDelete={handleSimplePersonaDelete}
|
||||
/>,
|
||||
)}
|
||||
</Box>
|
||||
|
||||
</PageDrawerList>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { Box, Checkbox, IconButton, ListItemButton, ListItemDecorator, Typography } from '@mui/joy';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
import type { SimplePersona } from '../store-app-personas';
|
||||
|
||||
|
||||
export function CreatorDrawerItem(props: {
|
||||
item: SimplePersona,
|
||||
isActive: boolean,
|
||||
isSelected: boolean,
|
||||
isSelection: boolean,
|
||||
onClick: (event: React.MouseEvent) => void,
|
||||
onDelete: (simplePersonaId: string) => void,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [deleteArmed, setDeleteArmed] = React.useState(false);
|
||||
|
||||
|
||||
// derived
|
||||
|
||||
const { item, isActive } = props;
|
||||
|
||||
const thumbnailUrl = item.pictureUrl || ((item.inputProvenance?.type === 'youtube' && item.inputProvenance.thumbnailUrl) ? item.inputProvenance.thumbnailUrl : undefined);
|
||||
|
||||
const icon = thumbnailUrl
|
||||
? <picture style={{ lineHeight: 0 }}><img src={thumbnailUrl} alt='Simple Persona Thumbnail' width={20} height={20} /></picture>
|
||||
: item.inputProvenance?.type === 'text'
|
||||
? <TextFieldsIcon />
|
||||
: item.inputProvenance?.type === 'youtube'
|
||||
? <YouTubeIcon />
|
||||
: undefined;
|
||||
|
||||
|
||||
return (
|
||||
<ListItemButton
|
||||
variant={isActive ? 'soft' : undefined}
|
||||
onClick={props.onClick}
|
||||
sx={{
|
||||
'&:hover > button': { opacity: 1 },
|
||||
}}
|
||||
>
|
||||
{/* Symbol or Thumbnail picture */}
|
||||
<ListItemDecorator>
|
||||
{props.isSelection ? (
|
||||
<Checkbox checked={props.isSelected} />
|
||||
) : icon}
|
||||
</ListItemDecorator>
|
||||
|
||||
<Box sx={{ overflow: 'hidden' }}>
|
||||
|
||||
{/* Title or System prompt (ellipsized) */}
|
||||
<Typography level='title-sm' sx={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
|
||||
{item.name || (item.systemPrompt?.slice(0, 40) + '...')}
|
||||
</Typography>
|
||||
|
||||
{/* creation Model */}
|
||||
{/*{!!item.llmLabel && <Typography level='body-xs' sx={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>*/}
|
||||
{/* {item.llmLabel}*/}
|
||||
{/*</Typography>}*/}
|
||||
|
||||
{/* creation Date */}
|
||||
<Typography level='body-xs'>
|
||||
{!!item.creationDate && <TimeAgo date={item.creationDate} />}
|
||||
</Typography>
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Delete Arming */}
|
||||
{!props.isSelection && !deleteArmed && (
|
||||
<IconButton
|
||||
variant={isActive ? 'solid' : 'outlined'}
|
||||
size='sm'
|
||||
sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.2s' }}
|
||||
onClick={() => setDeleteArmed(on => !on)}
|
||||
>
|
||||
<DeleteOutlineIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{/* Delete / Cancel buttons */}
|
||||
{!props.isSelection && deleteArmed && <>
|
||||
<IconButton size='sm' variant='solid' color='danger' onClick={() => props.onDelete(item.id)}>
|
||||
<DeleteOutlineIcon />
|
||||
</IconButton>
|
||||
<IconButton size='sm' variant='solid' color='neutral' onClick={() => setDeleteArmed(false)}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>}
|
||||
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, Textarea, Typography } from '@mui/joy';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
|
||||
import { lineHeightTextarea } from '~/common/app.theme';
|
||||
|
||||
import type { SimplePersonaProvenance } from '../store-app-personas';
|
||||
|
||||
|
||||
// minimum number of characters required to create from text
|
||||
const MIN_CHARS = 100;
|
||||
|
||||
|
||||
export function FromText(props: {
|
||||
isCreating: boolean;
|
||||
onCreate: (text: string, provenance: SimplePersonaProvenance) => void;
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [text, setText] = React.useState('');
|
||||
|
||||
const handleCreateFromText = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault(); // stop the form submit
|
||||
props.onCreate(text, { type: 'text' });
|
||||
};
|
||||
|
||||
return <>
|
||||
|
||||
<Typography level='title-md' startDecorator={<TextFieldsIcon />} sx={{ mb: 3 }}>
|
||||
<b>Text</b> -> Persona
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleCreateFromText}>
|
||||
<Textarea
|
||||
required
|
||||
variant='outlined'
|
||||
minRows={4} maxRows={8}
|
||||
placeholder='Paste your text here...'
|
||||
value={text}
|
||||
onChange={event => setText(event.target.value)}
|
||||
sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
lineHeight: lineHeightTextarea,
|
||||
mb: 1.5,
|
||||
}}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Button
|
||||
type='submit' variant='solid'
|
||||
disabled={props.isCreating || text?.length < MIN_CHARS}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
|
||||
<Typography level='body-sm'>
|
||||
{text.length < MIN_CHARS ? `(${MIN_CHARS - text.length})` : text.length.toLocaleString()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</form>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, Card, IconButton, Input, Typography } from '@mui/joy';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
import { useYouTubeTranscript, YTVideoTranscript } from '~/modules/youtube/useYouTubeTranscript';
|
||||
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
|
||||
import type { SimplePersonaProvenance } from '../store-app-personas';
|
||||
|
||||
|
||||
function extractVideoID(videoURL: string): string | null {
|
||||
const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^#&?]*).*/;
|
||||
const match = videoURL.match(regExp);
|
||||
return (match && match[1]?.length == 11) ? match[1] : null;
|
||||
}
|
||||
|
||||
|
||||
function YouTubeVideoTranscriptCard(props: { transcript: YTVideoTranscript, onClose: () => void, sx?: SxProps }) {
|
||||
const { transcript } = props;
|
||||
return (
|
||||
<Card
|
||||
variant='soft'
|
||||
sx={{
|
||||
border: '1px dashed',
|
||||
borderColor: 'neutral.solidBg',
|
||||
p: 1,
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
{!!transcript.thumbnailUrl && (
|
||||
<picture style={{ lineHeight: 0 }}>
|
||||
<img
|
||||
src={transcript.thumbnailUrl}
|
||||
alt='YouTube Video Thumbnail'
|
||||
height={80}
|
||||
style={{ float: 'left', marginRight: 8 }}
|
||||
/>
|
||||
</picture>
|
||||
)}
|
||||
|
||||
{/*<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0.5 }}>*/}
|
||||
<Typography level='title-sm'>
|
||||
{transcript?.title}
|
||||
</Typography>
|
||||
<Typography level='body-xs' sx={{ mt: 0.75 }}>
|
||||
{transcript?.transcript.slice(0, 280)}...
|
||||
</Typography>
|
||||
{/*</Box>*/}
|
||||
|
||||
<IconButton
|
||||
size='sm'
|
||||
onClick={props.onClose}
|
||||
sx={{
|
||||
position: 'absolute', top: -8, right: -8,
|
||||
borderRadius: 'md',
|
||||
}}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function FromYouTube(props: {
|
||||
isTransforming: boolean;
|
||||
onCreate: (text: string, provenance: SimplePersonaProvenance) => void;
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [videoURL, setVideoURL] = React.useState('');
|
||||
const [videoID, setVideoID] = React.useState<string | null>(null);
|
||||
|
||||
// external state
|
||||
|
||||
const { onCreate } = props;
|
||||
const onNewTranscript = React.useCallback((transcript: YTVideoTranscript) => {
|
||||
// setVideoID(null); // reset the video ID, to cycle the refetch
|
||||
onCreate(
|
||||
transcript.transcript,
|
||||
{
|
||||
type: 'youtube',
|
||||
url: videoURL,
|
||||
title: transcript.title,
|
||||
thumbnailUrl: transcript.thumbnailUrl,
|
||||
},
|
||||
);
|
||||
}, [onCreate, videoURL]);
|
||||
|
||||
const {
|
||||
transcript,
|
||||
isFetching, isError, error,
|
||||
} = useYouTubeTranscript(videoID, onNewTranscript);
|
||||
|
||||
|
||||
const handleVideoURLChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setVideoID(null);
|
||||
setVideoURL(e.target.value);
|
||||
};
|
||||
|
||||
const handleCreateFromTranscript = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault(); // stop the form submit
|
||||
|
||||
const videoId = extractVideoID(videoURL) || null;
|
||||
if (!videoId)
|
||||
setVideoURL('Invalid');
|
||||
|
||||
// kick-start the transcript fetch
|
||||
setVideoID(videoId);
|
||||
};
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
<Typography level='title-md' startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />} sx={{ mb: 3 }}>
|
||||
YouTube -> Persona
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleCreateFromTranscript}>
|
||||
<Input
|
||||
required
|
||||
type='url'
|
||||
fullWidth
|
||||
disabled={isFetching || props.isTransforming}
|
||||
variant='outlined'
|
||||
placeholder='YouTube Video URL'
|
||||
value={videoURL}
|
||||
onChange={handleVideoURLChange}
|
||||
sx={{ mb: 1.5 }}
|
||||
/>
|
||||
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Button
|
||||
type='submit' variant='solid'
|
||||
disabled={isFetching || props.isTransforming || !videoURL}
|
||||
loading={isFetching}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
|
||||
<GoodTooltip title='This example comes from the popular Fireship YouTube channel, which presents technical topics with irreverent humor.'>
|
||||
<Button variant='outlined' color='neutral' onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}>
|
||||
Example
|
||||
</Button>
|
||||
</GoodTooltip>
|
||||
</Box>
|
||||
</form>
|
||||
|
||||
{isError && (
|
||||
<InlineError error={error} sx={{ mt: 3 }} />
|
||||
)}
|
||||
|
||||
{!!transcript && !!videoID && (
|
||||
<YouTubeVideoTranscriptCard transcript={transcript} onClose={() => setVideoID(null)} sx={{ mt: 3 }} />
|
||||
)}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { Typography } from '@mui/joy';
|
||||
|
||||
import { Link } from '~/common/components/Link';
|
||||
|
||||
import { PersonaPromptCard } from './Creator';
|
||||
import { useSimplePersona } from '../store-app-personas';
|
||||
|
||||
|
||||
export function Viewer(props: { selectedSimplePersonaId: string }) {
|
||||
|
||||
// external state
|
||||
const { simplePersona } = useSimplePersona(props.selectedSimplePersonaId);
|
||||
|
||||
if (!simplePersona)
|
||||
return <Typography level='body-sm'>Loading Persona...</Typography>;
|
||||
|
||||
return <>
|
||||
|
||||
<Typography level='title-sm'>
|
||||
This <em>System Prompt</em> was created <TimeAgo date={simplePersona.creationDate} />
|
||||
using the <strong>{simplePersona.llmLabel}</strong> model.
|
||||
</Typography>
|
||||
|
||||
<PersonaPromptCard content={simplePersona.systemPrompt || ''} />
|
||||
|
||||
{/* tell about the Provenances */}
|
||||
<Typography level='body-sm' sx={{ mt: 3 }}>
|
||||
{simplePersona.inputProvenance?.type === 'youtube' && <>The source was this YouTube video: <Link href={simplePersona.inputProvenance.url} target='_blank'>{simplePersona.inputProvenance.title}</Link>.</>}
|
||||
{simplePersona.inputProvenance?.type === 'text' && <>The source was a text snippet of {simplePersona.inputText?.length.toLocaleString()} characters.</>}
|
||||
</Typography>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { createBase36Uid } from '~/common/util/textUtils';
|
||||
|
||||
|
||||
/**
|
||||
* Very simple personas store for the "Persona Creator" - note that we shall
|
||||
* switch to a more complex personas store in the future, as for now we mainly
|
||||
* save system prompts so that we don't lose what was created.
|
||||
*/
|
||||
export interface SimplePersona {
|
||||
id: string;
|
||||
name?: string;
|
||||
systemPrompt: string; // The system prompt is very important and required
|
||||
creationDate: string; // ISO string format
|
||||
pictureUrl?: string; // Optional picture URL
|
||||
// source material
|
||||
inputProvenance?: SimplePersonaProvenance;
|
||||
inputText: string;
|
||||
// llm used
|
||||
llmLabel?: string;
|
||||
}
|
||||
|
||||
export type SimplePersonaProvenance = {
|
||||
type: 'youtube';
|
||||
url: string;
|
||||
title?: string;
|
||||
thumbnailUrl?: string;
|
||||
} | {
|
||||
type: 'text';
|
||||
};
|
||||
|
||||
|
||||
interface AppPersonasStore {
|
||||
|
||||
// state
|
||||
simplePersonas: SimplePersona[];
|
||||
|
||||
// actions
|
||||
prependSimplePersona: (systemPrompt: string, inputText: string, inputProvenance?: SimplePersonaProvenance, llmLabel?: string) => void;
|
||||
deleteSimplePersona: (id: string) => void;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* DO NOT USE outside of this application - this is a very simple store for Personas so that
|
||||
* they're not immediately lost.
|
||||
*/
|
||||
const useAppPersonasStore = create<AppPersonasStore>()(persist(
|
||||
(_set, _get) => ({
|
||||
|
||||
simplePersonas: [],
|
||||
|
||||
prependSimplePersona: (systemPrompt: string, inputText: string, inputProvenance?: SimplePersonaProvenance, llmLabel?: string) =>
|
||||
_set(state => ({
|
||||
simplePersonas: [
|
||||
{
|
||||
id: createBase36Uid(state.simplePersonas.map(persona => persona.id)),
|
||||
systemPrompt,
|
||||
creationDate: new Date().toISOString(),
|
||||
inputProvenance,
|
||||
inputText,
|
||||
llmLabel,
|
||||
},
|
||||
...state.simplePersonas,
|
||||
],
|
||||
})),
|
||||
|
||||
deleteSimplePersona: (simplePersonaId: string) =>
|
||||
_set(state => ({
|
||||
simplePersonas: state.simplePersonas.filter(persona => persona.id !== simplePersonaId),
|
||||
})),
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'app-app-personas',
|
||||
version: 1,
|
||||
},
|
||||
));
|
||||
|
||||
export function useSimplePersonas() {
|
||||
const simplePersonas = useAppPersonasStore(state => state.simplePersonas, shallow);
|
||||
return { simplePersonas };
|
||||
}
|
||||
|
||||
export function useSimplePersona(simplePersonaId: string) {
|
||||
const simplePersona = useAppPersonasStore(state => {
|
||||
return state.simplePersonas.find(persona => persona.id === simplePersonaId) ?? null;
|
||||
}, shallow);
|
||||
return { simplePersona };
|
||||
}
|
||||
|
||||
export function prependSimplePersona(systemPrompt: string, inputText: string, inputProvenance?: SimplePersonaProvenance, llmLabel?: string) {
|
||||
useAppPersonasStore.getState().prependSimplePersona(systemPrompt, inputText, inputProvenance, llmLabel);
|
||||
}
|
||||
|
||||
export function deleteSimplePersona(simplePersonaId: string) {
|
||||
useAppPersonasStore.getState().deleteSimplePersona(simplePersonaId);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
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',
|
||||
drawer: true,
|
||||
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);
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -33,6 +33,7 @@ export function CloseableMenu(props: {
|
||||
noBottomPadding?: boolean,
|
||||
sx?: SxProps,
|
||||
zIndex?: number,
|
||||
listRef?: React.Ref<HTMLUListElement>,
|
||||
children?: React.ReactNode,
|
||||
}) {
|
||||
|
||||
@@ -71,11 +72,12 @@ export function CloseableMenu(props: {
|
||||
>
|
||||
<ClickAwayListener onClickAway={handleClose}>
|
||||
<MenuList
|
||||
ref={props.listRef}
|
||||
// variant={props.variant} color={props.color}
|
||||
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',
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import * as React from 'react';
|
||||
import Input, { InputProps } from '@mui/joy/Input';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
type DebounceInputProps = Omit<InputProps, 'onChange'> & {
|
||||
onDebounce: (value: string) => void;
|
||||
debounceTimeout: number;
|
||||
};
|
||||
|
||||
const DebounceInput: React.FC<DebounceInputProps> = ({
|
||||
onDebounce,
|
||||
debounceTimeout,
|
||||
...rest
|
||||
}) => {
|
||||
const [inputValue, setInputValue] = React.useState('');
|
||||
const timerRef = React.useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = event.target.value;
|
||||
setInputValue(newValue); // Update internal state immediately for a responsive UI
|
||||
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
|
||||
timerRef.current = setTimeout(() => {
|
||||
onDebounce(newValue); // Call onDebounce after the debounce timeout
|
||||
}, debounceTimeout);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleClear = () => {
|
||||
setInputValue(''); // Clear internal state
|
||||
onDebounce(''); // Call onDebounce with empty string
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...rest}
|
||||
value={inputValue}
|
||||
onChange={handleChange}
|
||||
aria-label={rest['aria-label'] || 'Search'}
|
||||
startDecorator={<SearchIcon />}
|
||||
endDecorator={
|
||||
inputValue && (
|
||||
<ClearIcon
|
||||
onClick={handleClear}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
handleClear();
|
||||
}
|
||||
}}
|
||||
aria-label="Clear search"
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(DebounceInput);
|
||||
@@ -23,7 +23,7 @@ export function GoodModal(props: {
|
||||
const showBottomClose = !!props.onClose && props.hideBottomClose !== true;
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<ModalOverflow sx={{p:1}}>
|
||||
<ModalOverflow sx={{ p: 1 }}>
|
||||
<ModalDialog
|
||||
sx={{
|
||||
minWidth: { xs: 360, sm: 500, md: 600, lg: 700 },
|
||||
|
||||
@@ -19,7 +19,11 @@ export const GoodTooltip = (props: {
|
||||
placement={props.placement}
|
||||
variant={(props.isError || props.isWarning) ? 'soft' : undefined}
|
||||
color={props.isError ? 'danger' : props.isWarning ? 'warning' : undefined}
|
||||
sx={{ maxWidth: { sm: '50vw', md: '25vw' }, ...props.sx }}
|
||||
sx={{
|
||||
maxWidth: { sm: '50vw', md: '25vw' },
|
||||
whiteSpace: 'break-spaces',
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</Tooltip>;
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -9,13 +9,15 @@ import { FormLabelStart } from './FormLabelStart';
|
||||
* Text form field (e.g. enter a host)
|
||||
*/
|
||||
export function FormTextField(props: {
|
||||
title: string | React.JSX.Element, description?: string | React.JSX.Element,
|
||||
title: string | React.JSX.Element,
|
||||
description?: string | React.JSX.Element,
|
||||
tooltip?: string | React.JSX.Element,
|
||||
placeholder?: string, isError?: boolean, disabled?: boolean,
|
||||
value: string | undefined, onChange: (text: string) => void,
|
||||
}) {
|
||||
return (
|
||||
<FormControl orientation='horizontal' disabled={props.disabled} sx={{ flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title={props.title} description={props.description} />
|
||||
<FormLabelStart title={props.title} description={props.description} tooltip={props.tooltip} />
|
||||
<Input
|
||||
variant='outlined' placeholder={props.placeholder} error={props.isError}
|
||||
value={props.value} onChange={event => props.onChange(event.target.value)}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, FormControl, IconButton, Textarea, Tooltip } from '@mui/joy';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
|
||||
|
||||
/**
|
||||
* A simple UI component, string array (ant titles array) in -> edited string array out
|
||||
*/
|
||||
export function useFormEditTextArray(initialStrings: string[], titles: string[]) {
|
||||
|
||||
// state
|
||||
const [strings, setStrings] = React.useState<string[]>(initialStrings);
|
||||
|
||||
const editString = React.useCallback((i: number, text: string) => {
|
||||
setStrings(s => s.map((s, j) => j === i ? text : s));
|
||||
}, []);
|
||||
|
||||
const stringEditors = React.useMemo(() => strings.map((text, i) =>
|
||||
<FormControl key={i} orientation='vertical'>
|
||||
<FormLabelStart title={i > 0 ? `${i}. ${titles[i]}` : titles[i]} />
|
||||
<Box sx={{ display: 'flex', alignItems: 'start', gap: 1 }}>
|
||||
<Textarea
|
||||
value={text}
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
onChange={event => editString(i, event.target.value)}
|
||||
sx={{ flex: 1, backgroundColor: 'background.level1', boxShadow: 'none' }}
|
||||
/>
|
||||
<Tooltip title='Reset'>
|
||||
<IconButton size='sm' onClick={() => editString(i, initialStrings[i])}>
|
||||
<ReplayIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</FormControl>,
|
||||
), [editString, initialStrings, strings, titles]);
|
||||
|
||||
return { strings, stringEditors };
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, FormControl, ListDivider, ListItemDecorator, Option, Select } from '@mui/joy';
|
||||
|
||||
import { DLLM, DLLMId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { findVendorById } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { IModelVendor } from '~/modules/llms/vendors/IModelVendor';
|
||||
|
||||
|
||||
/**
|
||||
* Select the Model, synced with either Global (Chat) LLM state, or local
|
||||
*
|
||||
* @param localState if true, the state is local to the hook, otherwise the global chat model is changed
|
||||
* @param label label of the select, use '' to hide it
|
||||
* @param placeholder placeholder of the select
|
||||
*/
|
||||
export function useLLMSelect(localState: boolean = true, label: string = 'Model', placeholder: string = 'Models …'): [DLLM | null, React.JSX.Element | null] {
|
||||
|
||||
// state
|
||||
const localSwitch = React.useRef(localState);
|
||||
|
||||
// external state
|
||||
const { llms, globalChatLLMId, globalSetChatLLMId } = useModelsStore(state => ({
|
||||
llms: state.llms,
|
||||
globalChatLLMId: state.chatLLMId,
|
||||
globalSetChatLLMId: state.setChatLLMId,
|
||||
}), shallow);
|
||||
|
||||
// local state initially synced to the global state (may be used or not)
|
||||
const [localLLMId, setLocalLLMId] = React.useState<DLLMId | null>(globalChatLLMId);
|
||||
|
||||
// global/local (stable) switch - do not change at runtime
|
||||
const chatLLMId = localSwitch.current ? localLLMId : globalChatLLMId;
|
||||
const setChatLLMId = localSwitch.current ? setLocalLLMId : globalSetChatLLMId;
|
||||
|
||||
|
||||
// derived state
|
||||
const chatLLM = chatLLMId ? llms.find(llm => llm.id === chatLLMId) ?? null : null;
|
||||
|
||||
|
||||
const component = React.useMemo(() => {
|
||||
// hide invisible models, except the current model
|
||||
const filteredLLMs = llms.filter(llm => !llm.hidden || llm.id === chatLLMId);
|
||||
|
||||
// create the option items
|
||||
let formerVendor: IModelVendor | null = null;
|
||||
const options = filteredLLMs.map((llm) => {
|
||||
|
||||
const vendor = findVendorById(llm._source?.vId);
|
||||
const vendorChanged = vendor !== formerVendor;
|
||||
const addSeparator = vendorChanged && formerVendor !== null;
|
||||
if (vendorChanged)
|
||||
formerVendor = vendor;
|
||||
|
||||
return (
|
||||
<React.Fragment key={'llm-' + llm.id}>
|
||||
{addSeparator && <ListDivider />}
|
||||
<Option
|
||||
value={llm.id}
|
||||
sx={llm.id === chatLLMId ? { fontWeight: 500 } : undefined}
|
||||
>
|
||||
{!!vendor?.Icon && (
|
||||
<ListItemDecorator>
|
||||
<vendor.Icon />
|
||||
</ListItemDecorator>
|
||||
)}
|
||||
{/*<Tooltip title={llm.description}>*/}
|
||||
{llm.label}
|
||||
{/*</Tooltip>*/}
|
||||
{/*{llm.gen === 'sdxl' && <Chip size='sm' variant='outlined'>XL</Chip>} {llm.label}*/}
|
||||
</Option>
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
// create the component
|
||||
return (
|
||||
<FormControl>
|
||||
{!!label && <FormLabelStart title={label} />}
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Select
|
||||
variant='outlined'
|
||||
value={chatLLMId}
|
||||
onChange={(_event, value) => value && setChatLLMId(value)}
|
||||
placeholder={placeholder}
|
||||
slotProps={{
|
||||
listbox: {
|
||||
sx: {
|
||||
// larger list
|
||||
'--ListItem-paddingLeft': '1rem',
|
||||
'--ListItem-minHeight': '2.5rem',
|
||||
// minWidth: '100%',
|
||||
},
|
||||
},
|
||||
button: {
|
||||
sx: {
|
||||
// show the full name on the button
|
||||
whiteSpace: 'inherit',
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
flex: 1,
|
||||
// minWidth: '200',
|
||||
}}
|
||||
>
|
||||
{options}
|
||||
</Select>
|
||||
</Box>
|
||||
</FormControl>
|
||||
);
|
||||
}, [chatLLMId, label, llms, placeholder, setChatLLMId]);
|
||||
|
||||
|
||||
return [chatLLM, component];
|
||||
}
|
||||
+11
-6
@@ -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>;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user