mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
313 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4a8a2b9c5d | |||
| 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 | |||
| 677facb867 | |||
| 494086765b | |||
| 59ca03e17d | |||
| e0e56d70c9 | |||
| b408267e6e | |||
| 6385d7aa84 | |||
| fa811c951c | |||
| 7085c3a7aa | |||
| 2333318cb4 | |||
| 3aebcb360c | |||
| bf60d699e3 | |||
| d775d47623 | |||
| 2eb3397394 | |||
| e27c35373d | |||
| 5e1966af5f | |||
| 7cbcf01ca9 | |||
| 6898fa6cc1 | |||
| 1e796299a2 | |||
| 7026024da5 | |||
| 3ed52fa92f | |||
| a3e04f5973 | |||
| 8bf90e3622 | |||
| cdc2de5018 | |||
| b26370a85a | |||
| adf0197a9e | |||
| c00c41a160 | |||
| 09c74e6cf4 | |||
| 304e66b098 | |||
| 64b6b08652 | |||
| cbea304a97 | |||
| c3e73fa9c8 | |||
| 4c978020d9 | |||
| 481b85bdad | |||
| b80fd0494a | |||
| c7dea43d1a | |||
| 726053ffcd | |||
| ee4e2c265b | |||
| a5332d2c82 | |||
| 2f45ce48fa | |||
| 104922dc20 | |||
| d68ccd9dfb | |||
| 676bcadd17 | |||
| c08e83c618 | |||
| 7a69b32506 | |||
| a9e1a968e8 | |||
| dc30a7a55a | |||
| f570627b09 | |||
| e601302db8 | |||
| f9e207ff7c | |||
| 8100c5cfd1 | |||
| 0b0c3891bb | |||
| b4cdd5546d | |||
| 8444b32db2 | |||
| 69098273bf | |||
| 5cd5702b83 | |||
| 605d288da6 | |||
| 499840cae3 | |||
| 4529fc325b | |||
| 4769e9b900 | |||
| 64d13a0d52 | |||
| 7df1517b23 | |||
| 56c372455d | |||
| 2e649ea12b | |||
| 2a67315504 | |||
| b53ceb70c4 | |||
| 3c9d06aac7 | |||
| 77e7c1d467 | |||
| eb38e119b8 | |||
| 06402cc5c1 | |||
| ddf631cdfc | |||
| f7e89ae65c | |||
| 07e1e1c580 | |||
| f6eb2aecee | |||
| f416b1df97 | |||
| 29d17795b8 | |||
| 3b30f649c6 | |||
| ba9a9714a7 | |||
| c304ab5f3b | |||
| cd4d5042e9 | |||
| 6c4d177bfc | |||
| 5d1620b5c1 | |||
| bd78808950 | |||
| 6aee6aeac1 | |||
| 5ae970a526 | |||
| 87718d73d2 | |||
| 7c8498573e | |||
| f6e82d0c0c | |||
| f7f827660d | |||
| 664b221e67 | |||
| f184a4bf97 | |||
| e442816c15 | |||
| aaa3b65cd8 | |||
| c6441662b0 | |||
| b902a7bce8 | |||
| 87a916ba09 | |||
| 35a85ed2fa | |||
| 75d56bfb56 | |||
| d0a125fad5 | |||
| 2af8437f6d | |||
| 0c3e65575c | |||
| 1c15057fca | |||
| 44da928489 | |||
| 85027d3e3a | |||
| 0fc83cf6f5 | |||
| 2949feccd5 | |||
| d6f1c2da81 | |||
| fabb433fde | |||
| b57445eb14 | |||
| 5f8f4aba78 | |||
| d693cdaeba | |||
| 39fbcfd97b | |||
| 7694bc3d52 | |||
| 7f21b2ac3d | |||
| fdb66da1a7 | |||
| 6b62a6733b | |||
| 5d62056807 | |||
| efff7126af | |||
| 45046c70ed | |||
| 7b5b852793 | |||
| 9952b757b8 | |||
| b08ecc9012 | |||
| bc5a38fa89 | |||
| bee49a4b1c | |||
| 0ece1ce58c | |||
| fd897b55b2 | |||
| dd41a402d0 | |||
| 3f9defd18c | |||
| 49c77f5a10 | |||
| 6b2bfa6060 | |||
| 8e3f247bfb | |||
| 201e3a7252 | |||
| 044ed4df79 | |||
| 0df7297cca | |||
| 453a3e5751 | |||
| 34c1c425b9 | |||
| e0a010189f | |||
| 7a07f10ed1 | |||
| 33cb2b84b2 | |||
| 3adec85e1f | |||
| 18cfe5e296 | |||
| 566ba366b4 | |||
| 7ed653b315 | |||
| cb333c33d7 | |||
| 22ba37074b | |||
| 84d7b7644a | |||
| 71445dafc8 | |||
| 66a5ad7f00 | |||
| 09f80adfaa | |||
| 9febd97065 | |||
| 5219f9928d | |||
| aec9f4665f | |||
| db48465204 | |||
| c2c858730a | |||
| 402bde9a81 | |||
| ba1c0ba0d9 | |||
| 084d77cd78 | |||
| 30c17a9b73 | |||
| 2442463da3 | |||
| 84a3e8cfdb |
@@ -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.'
|
||||
@@ -9,6 +9,8 @@ assignees: enricoros
|
||||
|
||||
## Release checklist:
|
||||
|
||||
- [x] Create a new [Release Issue](https://github.com/enricoros/big-AGI/issues/new?assignees=enricoros&projects=enricoros/4&template=maintainers-release.md&title=Release+1.2.3)
|
||||
- [ ] Replace 1.1.0 with the _former_ release, and _1.2.3_ with THIS
|
||||
- [ ] Update the [Roadmap](https://github.com/users/enricoros/projects/4/views/2) calling out shipped features
|
||||
- [ ] Create and update a [Milestone](https://github.com/enricoros/big-agi/milestones) for the release
|
||||
- [ ] Assign this task
|
||||
@@ -21,38 +23,55 @@ assignees: enricoros
|
||||
- [ ] Update the release version in package.json, and `npm i`
|
||||
- [ ] Update in-app News [src/apps/news/news.data.tsx](/src/apps/news/news.data.tsx)
|
||||
- [ ] Update the in-app News version number
|
||||
- [ ] Update the readme with the new release
|
||||
- [ ] Update the README.md with the new release
|
||||
- [ ] Copy the highlights to the [docs/changelog.md](/docs/changelog.md)
|
||||
- Release:
|
||||
- [ ] merge onto main
|
||||
- [ ] merge onto main `git checkout main && git merge --no-ff release-1.2.3`
|
||||
- [ ] re-tag `git tag -f v1.2.3 && git push opensource --tags -f'
|
||||
- [ ] verify deployment on Vercel
|
||||
- [ ] verify container on GitHub Packages
|
||||
- create a GitHub release
|
||||
- [ ] name it 'vX.Y.Z'
|
||||
- [ ] copy the release notes and link appropriate artifacts
|
||||
- [ ] update the GitHub release
|
||||
- [ ] push as stable `git push opensource main:main-stable`
|
||||
- Announce:
|
||||
- [ ] Discord announcement
|
||||
- [ ] Twitter announcement
|
||||
|
||||
### Links
|
||||
|
||||
## Links
|
||||
Milestone:
|
||||
Former release task:
|
||||
GitHub release:
|
||||
|
||||
- Milestone: https://github.com/enricoros/big-AGI/milestone/X
|
||||
- GitHub release: https://github.com/enricoros/big-AGI/releases/tag/vX.Y.Z
|
||||
- Former release task: https://github.com/enricoros/big-AGI/issues/XXX
|
||||
|
||||
## Artifacts Generation
|
||||
|
||||
1) The following is my opensource application
|
||||
- paste README.md
|
||||
2) I am announcing a new version, 1.7.0. The following were the announcements for 1.6.0. Discord announcement, GitHub Release, in-app news.data.tsx, changelog.md.
|
||||
- paste the former: `discord announcement`, `GitHub release`, `news.data.tsx`, `changelog.md`
|
||||
3) The following is the new data I have for 1.7.0
|
||||
- paste the link to the milestone (closed) and each individual issue (content will be downloaded)
|
||||
- paste the git changelog `git log v1.6.0..v1.7.0 | clip`
|
||||
|
||||
```markdown
|
||||
You help me generate the following collateral for the new release of my opensource application
|
||||
called big-AGI. The new release is 1.2.3.
|
||||
To familiarize yourself with the application, the following are the Website and the GitHub README.md.
|
||||
```
|
||||
|
||||
### news.data.TSX
|
||||
- paste the URL: https://big-agi.com
|
||||
- drag & drop: [README.md](https://raw.githubusercontent.com/enricoros/big-AGI/main/README.md)
|
||||
|
||||
```markdown
|
||||
I am announcing a new version, 1.2.3.
|
||||
For reference, the following was the collateral for 1.1.0 (Discord announcement,
|
||||
GitHub Release, in-app-news file news.data.tsx, changelog.md).
|
||||
```
|
||||
|
||||
- paste the former: `discord announcement`,
|
||||
- `GitHub release`,
|
||||
- `news.data.tsx`,
|
||||
- `changelog.md`
|
||||
|
||||
```markdown
|
||||
The following are the new developments for 1.2.3:
|
||||
```
|
||||
|
||||
- paste the link to the milestone (closed) and each individual issue (content will be downloaded)
|
||||
- paste the git changelog `git log v1.1.0..v1.2.3 | clip`
|
||||
|
||||
### news.data.tsx
|
||||
|
||||
```markdown
|
||||
I need the following from you:
|
||||
@@ -63,13 +82,27 @@ 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
|
||||
|
||||
Now paste the former release (or 1.5.0 which was accurate and great), including the new contributors and
|
||||
```markdown
|
||||
Please create the 1.2.3 Release Notes for GitHub.
|
||||
Use a truthful and honest tone, understanding that people's time and attention span is short.
|
||||
Today is 2024-1-1.
|
||||
```
|
||||
|
||||
Now paste-attachment the former release notes (or 1.5.0 which was accurate and great), including the new contributors and
|
||||
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,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Enrico Ros
|
||||
Copyright (c) 2023-2024 Enrico Ros
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -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 7 vendors and
|
||||
simplicity, and speed. Powered by the latest models from 8 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.
|
||||
|
||||
@@ -11,7 +11,7 @@ Pros use big-AGI. 🚀 Developers love big-AGI. 🤖
|
||||
|
||||
Or fork & run on Vercel
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-agi&env=OPENAI_API_KEY,OPENAI_API_HOST&envDescription=OpenAI%20KEY%20for%20your%20deployment.%20Set%20HOST%20only%20if%20non-default.)
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-agi&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-agi)
|
||||
|
||||
## 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2)
|
||||
|
||||
@@ -21,44 +21,41 @@ 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.7.3 · Dec 13, 2023 · Attachment Theory 🌟
|
||||
### What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI
|
||||
|
||||
- **Attachments System Overhaul**: Drag, paste, link, snap, text, images, PDFs and more. [#251](https://github.com/enricoros/big-agi/issues/251)
|
||||
- **Desktop Webcam Capture**: Image capture now available as Labs feature. [#253](https://github.com/enricoros/big-agi/issues/253)
|
||||
- **Independent Browsing**: Full browsing support with Browserless. [Learn More](https://github.com/enricoros/big-agi/blob/main/docs/config-browse.md)
|
||||
- **Overheat LLMs**: Push the creativity with higher LLM temperatures. [#256](https://github.com/enricoros/big-agi/issues/256)
|
||||
- **Model Options Shortcut**: Quick adjust with `Ctrl+Shift+O`
|
||||
- Optimized Voice Input and Performance
|
||||
- Latest Ollama and Oobabooga models
|
||||
- For developers: **Password Protection**: HTTP Basic Auth. [Learn How](https://github.com/enricoros/big-agi/blob/main/docs/deploy-authentication.md)
|
||||
- [1.7.1]: Improved Ollama chats. [#270](https://github.com/enricoros/big-agi/issues/270)
|
||||
- [1.7.2]: OpenRouter login & free models 🎁
|
||||
- [1.7.3]: Mistral Platform support. [#273](https://github.com/enricoros/big-agi/issues/273)
|
||||
- **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.6.0 - Nov 28, 2023
|
||||
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
|
||||
|
||||
- **Web Browsing**: Download web pages within chats - [browsing guide](https://github.com/enricoros/big-agi/blob/main/docs/config-browse.md)
|
||||
- **Branching Discussions**: Create new conversations from any message
|
||||
- **Keyboard Navigation**: Swift chat navigation with new shortcuts (e.g. ctrl+alt+left/right)
|
||||
- **Performance Boost**: Faster rendering for a smoother experience
|
||||
- **UI Enhancements**: Refined interface based on user feedback
|
||||
- **New Features**: Anthropic Claude 2.1, `/help` command, and Flattener tool
|
||||
- **For Developers**: Code quality upgrades and snackbar notifications
|
||||
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
|
||||
- **Perfect scrolling mechanics** across devices. [#304](https://github.com/enricoros/big-AGI/issues/304)
|
||||
- Persona creation now supports **text input**. [#287](https://github.com/enricoros/big-AGI/pull/287)
|
||||
- Openrouter updates for better model management and rate limit handling
|
||||
- Image drawing UX improvements
|
||||
- Layout fix for Firefox users
|
||||
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
|
||||
|
||||
### What's New in 1.5.0 - Nov 19, 2023
|
||||
### What's New in 1.8.0 · Dec 20, 2023
|
||||
|
||||
- **Continued Voice**: Engage with hands-free interaction for a seamless experience
|
||||
- **Visualization Tool**: Create data representations with our new visualization capabilities
|
||||
- **Ollama Local Models**: Leverage local models support with our comprehensive guide
|
||||
- **Text Tools**: Enjoy tools including highlight differences to refine your content
|
||||
- **Mermaid Diagramming**: Render complex diagrams with our Mermaid language support
|
||||
- **OpenAI 1106 Chat Models**: Experience the cutting-edge capabilities of the latest OpenAI models
|
||||
- **SDXL Support**: Enhance your image generation with SDXL support for Prodia
|
||||
- **Cloudflare OpenAI API Gateway**: Integrate with Cloudflare for a robust API gateway
|
||||
- **Helicone for Anthropic**: Utilize Helicone's tools for Anthropic models
|
||||
- **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
|
||||
|
||||
Check out the [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), or
|
||||
the [past releases changelog](docs/changelog.md).
|
||||
### 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)
|
||||
|
||||
## ✨ Key Features 👊
|
||||
|
||||
@@ -148,7 +145,7 @@ Please refer to the [Cloudflare deployment documentation](docs/deploy-cloudflare
|
||||
|
||||
Create your GitHub fork, create a Vercel project over that fork, and deploy it. Or press the button below for convenience.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-agi&env=OPENAI_API_KEY,OPENAI_API_HOST&envDescription=OpenAI%20KEY%20for%20your%20deployment.%20Set%20HOST%20only%20if%20non-default.)
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-agi&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-agi)
|
||||
|
||||
## Integrations:
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export const runtime = 'edge';
|
||||
export { openaiStreamingRelayHandler as POST } from '~/modules/llms/transports/server/openai/openai.streaming';
|
||||
export { llmStreamingRelayHandler as POST } from '~/modules/llms/server/llm.server.streaming';
|
||||
+1
-1
@@ -6,7 +6,7 @@ version: '3.9'
|
||||
|
||||
services:
|
||||
big-agi:
|
||||
image: ghcr.io/enricoros/big-agi:main
|
||||
image: ghcr.io/enricoros/big-agi:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file:
|
||||
|
||||
+34
-6
@@ -5,12 +5,43 @@ by release.
|
||||
|
||||
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
|
||||
|
||||
### 1.8.0 - Dec 2023
|
||||
### 1.11.0 - Jan 2024
|
||||
|
||||
- milestone: [1.11.0](https://github.com/enricoros/big-agi/milestone/11)
|
||||
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
|
||||
- milestone: [1.8.0](https://github.com/enricoros/big-agi/milestone/8)
|
||||
|
||||
### What's New in 1.7.3 · Dec 13, 2023 · Attachment Theory 🌟
|
||||
### What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI
|
||||
|
||||
- **New UI**: for both desktop and mobile, sets the stage for future scale. [#201](https://github.com/enricoros/big-AGI/issues/201)
|
||||
- **Conversation Folders**: enhanced conversation organization. [#321](https://github.com/enricoros/big-AGI/issues/321)
|
||||
- **[LM Studio](https://lmstudio.ai/)** support and improved token management
|
||||
- Resizable panes in split-screen conversations.
|
||||
- Large performance optimizations
|
||||
- Developer enhancements: new UI framework, updated documentation for proxy settings on browserless/docker
|
||||
|
||||
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
|
||||
|
||||
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
|
||||
- **Perfect scrolling mechanics** across devices. [#304](https://github.com/enricoros/big-AGI/issues/304)
|
||||
- Persona creation now supports **text input**. [#287](https://github.com/enricoros/big-AGI/pull/287)
|
||||
- Openrouter updates for better model management and rate limit handling
|
||||
- Image drawing UX improvements
|
||||
- Layout fix for Firefox users
|
||||
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
|
||||
|
||||
### What's New in 1.8.0 · Dec 20, 2023 · To The Moon And Back
|
||||
|
||||
- **Google Gemini Support**: Use the newest Google models. [#275](https://github.com/enricoros/big-agi/issues/275)
|
||||
- **Mistral Platform**: Mixtral and future models support. [#273](https://github.com/enricoros/big-agi/issues/273)
|
||||
- **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 1.7.0 · Dec 11, 2023 · Attachment Theory
|
||||
|
||||
- **Attachments System Overhaul**: Drag, paste, link, snap, text, images, PDFs and more. [#251](https://github.com/enricoros/big-agi/issues/251)
|
||||
- **Desktop Webcam Capture**: Image capture now available as Labs feature. [#253](https://github.com/enricoros/big-agi/issues/253)
|
||||
@@ -20,9 +51,6 @@ by release.
|
||||
- Optimized Voice Input and Performance
|
||||
- Latest Ollama and Oobabooga models
|
||||
- For developers: **Password Protection**: HTTP Basic Auth. [Learn How](https://github.com/enricoros/big-agi/blob/main/docs/deploy-authentication.md)
|
||||
- [1.7.1]: Improved Ollama chats. [#270](https://github.com/enricoros/big-agi/issues/270)
|
||||
- [1.7.2]: OpenRouter login & free models 🎁
|
||||
- [1.7.3]: Mistral Platform support. [#273](https://github.com/enricoros/big-agi/issues/273)
|
||||
|
||||
### What's New in 1.6.0 - Nov 28, 2023 · Surf's Up
|
||||
|
||||
|
||||
+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!
|
||||
|
||||
@@ -30,5 +30,5 @@ For instance with [Use luna-ai-llama2 with docker compose](https://localai.io/ba
|
||||
|
||||
> NOTE: LocalAI does not list details about the mdoels. Every model is assumed to be
|
||||
> capable of chatting, and with a context window of 4096 tokens.
|
||||
> Please update the [src/modules/llms/transports/server/openai/models.data.ts](../src/modules/llms/transports/server/openai/models.data.ts)
|
||||
> Please update the [src/modules/llms/transports/server/openai/models.data.ts](../src/modules/llms/server/openai/models.data.ts)
|
||||
> file with the mapping information between LocalAI model IDs and names/descriptions/tokens, etc.
|
||||
|
||||
+24
-12
@@ -5,13 +5,15 @@ This guide helps you connect [Ollama](https://ollama.ai) [models](https://ollama
|
||||
experience. The integration brings the popular big-AGI features to Ollama, including: voice chats,
|
||||
editing tools, models switching, personas, and more.
|
||||
|
||||
_Last updated Dec 11, 2023_
|
||||
_Last updated Dec 16, 2023_
|
||||
|
||||

|
||||
|
||||
## Quick Integration Guide
|
||||
|
||||
1. **Ensure Ollama API Server is Running**: Follow the official instructions to get Ollama up and running on your machine
|
||||
- For detailed instructions on setting up the Ollama API server, please refer to the
|
||||
[Ollama download page](https://ollama.ai/download) and [instructions for linux](https://github.com/jmorganca/ollama/blob/main/docs/linux.md).
|
||||
2. **Add Ollama as a Model Source**: In `big-AGI`, navigate to the **Models** section, select **Add a model source**, and choose **Ollama**
|
||||
3. **Enter Ollama Host URL**: Provide the Ollama Host URL where the API server is accessible (e.g., `http://localhost:11434`)
|
||||
4. **Refresh Model List**: Once connected, refresh the list of available models to include the Ollama models
|
||||
@@ -20,21 +22,29 @@ _Last updated Dec 11, 2023_
|
||||
you'll have to press the 'Pull' button again, until a green message appears.
|
||||
5. **Chat with Ollama models**: select an Ollama model and begin chatting with AI personas
|
||||
|
||||
### Ollama: installation and Setup
|
||||
**Visual Configuration Guide**:
|
||||
|
||||
For detailed instructions on setting up the Ollama API server, please refer to the
|
||||
[Ollama download page](https://ollama.ai/download) and [instructions for linux](https://github.com/jmorganca/ollama/blob/main/docs/linux.md).
|
||||
* After adding the `Ollama` model vendor, entering the IP address of an Ollama server, and refreshing models:<br/>
|
||||
<img src="pixels/config-ollama-1-models.png" alt="config-local-ollama-1-models.png" width="320">
|
||||
|
||||
### Visual Guide
|
||||
* The `Ollama` admin panel, with the `Pull` button highlighted, after pulling the "Yi" model:<br/>
|
||||
<img src="pixels/config-ollama-2-admin-pull.png" alt="config-local-ollama-2-admin-pull.png" width="320">
|
||||
|
||||
* After adding the `Ollama` model vendor, entering the IP address of an Ollama server, and refreshing models:
|
||||
<img src="pixels/config-ollama-1-models.png" alt="config-local-ollama-1-models.png" style="max-width: 320px;">
|
||||
* You can now switch model/persona dynamically and text/voice chat with the models:<br/>
|
||||
<img src="pixels/config-ollama-3-chat.png" alt="config-local-ollama-3-chat.png" width="320">
|
||||
|
||||
* The `Ollama` admin panel, with the `Pull` button highlighted, after pulling the "Yi" model:
|
||||
<img src="pixels/config-ollama-2-admin-pull.png" alt="config-local-ollama-2-admin-pull.png" style="max-width: 320px;">
|
||||
<br/>
|
||||
|
||||
* You can now switch model/persona dynamically and text/voice chat with the models:
|
||||
<img src="pixels/config-ollama-3-chat.png" alt="config-local-ollama-3-chat.png" style="max-width: 320px;">
|
||||
### ⚠️ Network Troubleshooting
|
||||
|
||||
If you get errors about the server having trouble connecting with Ollama, please see
|
||||
[this message](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483) on Issue #276.
|
||||
|
||||
And in brief, make sure the Ollama endpoint is accessible from the servers where you run big-AGI (which could
|
||||
be localhost or cloud servers).
|
||||

|
||||
|
||||
<br/>
|
||||
|
||||
### Advanced: Model parameters
|
||||
|
||||
@@ -73,6 +83,8 @@ Then, edit the nginx configuration file `/etc/nginx/sites-enabled/default` and a
|
||||
|
||||
Reach out to our community if you need help with this.
|
||||
|
||||
<br/>
|
||||
|
||||
### Community and Support
|
||||
|
||||
Join our community to share your experiences, get help, and discuss best practices:
|
||||
@@ -83,4 +95,4 @@ Join our community to share your experiences, get help, and discuss best practic
|
||||
---
|
||||
|
||||
`big-AGI` is committed to providing a powerful, intuitive, and privacy-respecting AI experience.
|
||||
We are excited for you to explore the possibilities with Ollama models. Happy creating!
|
||||
We are excited for you to explore the possibilities with Ollama models. Happy creating!
|
||||
|
||||
+37
-20
@@ -21,33 +21,23 @@ Docker ensures faster development cycles, easier collaboration, and seamless env
|
||||
```
|
||||
4. Browse to [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
## Documentation
|
||||
<br/>
|
||||
|
||||
The big-AGI repository includes a Dockerfile and a GitHub Actions workflow for building and publishing a
|
||||
Docker image of the application.
|
||||
## Run Official Containers 📦
|
||||
|
||||
### Dockerfile
|
||||
`big-AGI` is pre-built from source code and published as a Docker image on the GitHub Container Registry (ghcr).
|
||||
The build process is transparent, and happens via GitHub Actions, as described in the
|
||||
file.
|
||||
|
||||
The [`Dockerfile`](../Dockerfile) describes how to create a Docker image. It establishes a Node.js environment,
|
||||
installs dependencies, and creates a production-ready version of the application as a local container.
|
||||
### Official Images: [ghcr.io/enricoros/big-agi](https://github.com/enricoros/big-agi/pkgs/container/big-agi)
|
||||
|
||||
### Official container images
|
||||
#### Run using *docker* 🚀
|
||||
|
||||
The [`.github/workflows/docker-image.yml`](../.github/workflows/docker-image.yml) file automates the
|
||||
building and publishing of the Docker images to the GitHub Container Registry (ghcr) when changes are
|
||||
pushed to the `main` branch.
|
||||
|
||||
Official pre-built containers: [ghcr.io/enricoros/big-agi](https://github.com/enricoros/big-agi/pkgs/container/big-agi)
|
||||
|
||||
Run official pre-built containers:
|
||||
```bash
|
||||
docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi
|
||||
docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi:latest
|
||||
```
|
||||
|
||||
### Run official containers
|
||||
|
||||
In addition, the repository also includes a `docker-compose.yaml` file, configured to run the pre-built
|
||||
'ghcr image'. This file is used to define the `big-agi` service, the ports to expose, and the command to run.
|
||||
#### Run using *docker-compose* 🚀
|
||||
|
||||
If you have Docker Compose installed, you can run the Docker container with `docker-compose up`
|
||||
to pull the Docker image (if it hasn't been pulled already) and start a Docker container. If you want to
|
||||
@@ -57,4 +47,31 @@ update the image to the latest version, you can run `docker-compose pull` before
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
Leverage Docker's capabilities for a reliable and efficient big-AGI deployment.
|
||||
### Make Local Services Visible to Docker 🌐
|
||||
|
||||
To make local services running on your host machine accessible to a Docker container, such as a
|
||||
[Browseless](./config-browse.md) service or a local API, you can follow this simplified guide:
|
||||
|
||||
| Operating System | Steps to Make Local Services Visible to Docker |
|
||||
|:------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| Windows and macOS | Use the special DNS name `host.docker.internal` to refer to the host machine from within the Docker container. No additional network configuration is required. Access local services using `host.docker.internal:<PORT>`. |
|
||||
| Linux | Two options: *A*. Use <ins>--network="host"</ins> (`docker run --network="host" -d big-agi`) when running the Docker container to merge the container within the host network stack; however, this reduces container isolation. Alternatively: *B*. Connect to local services <ins>using the host's IP address</ins> directly, as host.docker.internal is not available by default on Linux. |
|
||||
|
||||
<br/>
|
||||
|
||||
### More Information
|
||||
|
||||
The [`Dockerfile`](../Dockerfile) describes how to create a Docker image. It establishes a Node.js environment,
|
||||
installs dependencies, and creates a production-ready version of the application as a local container.
|
||||
|
||||
The [`docker-compose.yaml`](../docker-compose.yaml) file is configured to run the
|
||||
official image (big-agi:latest). This file is used to define the `big-agi` service, to expose
|
||||
port 3000 on the host, and launch big-AGI within the container (startup command).
|
||||
|
||||
The [`.github/workflows/docker-image.yml`](../.github/workflows/docker-image.yml) file is used
|
||||
to build the Official Docker images and publish them to the GitHub Container Registry (ghcr).
|
||||
The build process is transparent and happens via GitHub Actions.
|
||||
|
||||
<br/>
|
||||
|
||||
Leverage Docker's capabilities for a reliable and efficient big-AGI deployment!
|
||||
@@ -12,7 +12,7 @@ version: '3.9'
|
||||
|
||||
services:
|
||||
big-agi:
|
||||
image: ghcr.io/enricoros/big-agi:main
|
||||
image: ghcr.io/enricoros/big-agi:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
env_file:
|
||||
|
||||
@@ -24,6 +24,7 @@ AZURE_OPENAI_API_ENDPOINT=
|
||||
AZURE_OPENAI_API_KEY=
|
||||
ANTHROPIC_API_KEY=
|
||||
ANTHROPIC_API_HOST=
|
||||
GEMINI_API_KEY=
|
||||
MISTRAL_API_KEY=
|
||||
OLLAMA_API_HOST=
|
||||
OPENROUTER_API_KEY=
|
||||
@@ -46,7 +47,7 @@ PUPPETEER_WSS_ENDPOINT=
|
||||
# Backend Analytics
|
||||
BACKEND_ANALYTICS=
|
||||
|
||||
# Backend HTTP Basic Authentication
|
||||
# Backend HTTP Basic Authentication (see `deploy-authentication.md` for turning on authentication)
|
||||
HTTP_BASIC_AUTH_USERNAME=
|
||||
HTTP_BASIC_AUTH_PASSWORD=
|
||||
```
|
||||
@@ -80,6 +81,7 @@ requiring the user to enter an API key
|
||||
| `AZURE_OPENAI_API_KEY` | Azure OpenAI API key, see [config-azure-openai.md](config-azure-openai.md) | Optional, but if set `AZURE_OPENAI_API_ENDPOINT` must also be set |
|
||||
| `ANTHROPIC_API_KEY` | The API key for Anthropic | Optional |
|
||||
| `ANTHROPIC_API_HOST` | Changes the backend host for the Anthropic vendor, to enable platforms such as [config-aws-bedrock.md](config-aws-bedrock.md) | Optional |
|
||||
| `GEMINI_API_KEY` | The API key for Google AI's Gemini | Optional |
|
||||
| `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 |
|
||||
@@ -115,10 +117,7 @@ Enable the app to Talk, Draw, and Google things up.
|
||||
| `PUPPETEER_WSS_ENDPOINT` | Puppeteer WebSocket endpoint - used for browsing, etc. |
|
||||
| **Backend** | |
|
||||
| `BACKEND_ANALYTICS` | Semicolon-separated list of analytics flags (see backend.analytics.ts). Flags: `domain` logs the responding domain. |
|
||||
| `HTTP_BASIC_AUTH_USERNAME` | Username for HTTP Basic Authentication. See the [Authentication](deploy-authentication.md) guide. |
|
||||
| `HTTP_BASIC_AUTH_USERNAME` | See the [Authentication](deploy-authentication.md) guide. Username for HTTP Basic Authentication. |
|
||||
| `HTTP_BASIC_AUTH_PASSWORD` | Password for HTTP Basic Authentication. |
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Generated
+366
-190
@@ -1,64 +1,70 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.7.3",
|
||||
"version": "1.10.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "big-agi",
|
||||
"version": "1.7.3",
|
||||
"version": "1.10.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.7",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.19",
|
||||
"@mui/joy": "^5.0.0-beta.17",
|
||||
"@mui/icons-material": "^5.15.2",
|
||||
"@mui/joy": "^5.0.0-beta.20",
|
||||
"@next/bundle-analyzer": "^14.0.4",
|
||||
"@prisma/client": "^5.7.0",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@sanity/diff-match-patch": "^3.1.1",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@trpc/client": "^10.44.1",
|
||||
"@trpc/next": "^10.44.1",
|
||||
"@trpc/react-query": "^10.44.1",
|
||||
"@trpc/server": "^10.44.1",
|
||||
"@tanstack/react-query": "~4.36.1",
|
||||
"@trpc/client": "10.44.1",
|
||||
"@trpc/next": "10.44.1",
|
||||
"@trpc/react-query": "10.44.1",
|
||||
"@trpc/server": "10.44.1",
|
||||
"@vercel/analytics": "^1.1.1",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"browser-fs-access": "^0.35.0",
|
||||
"eventsource-parser": "^1.1.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"next": "^14.0.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "4.0.269",
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^1.0.5",
|
||||
"react-timeago": "^7.2.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"superjson": "^2.2.1",
|
||||
"tesseract.js": "^5.0.3",
|
||||
"tesseract.js": "^5.0.4",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "~4.3.9"
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/puppeteer": "^0.0.5",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.6",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
"prisma": "^5.7.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -255,9 +261,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.23.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.6.tgz",
|
||||
"integrity": "sha512-zHd0eUrf5GZoOWVCXp6koAKQTfZV07eit6bGPmJgnZdnSAvvZee6zniW2XMF7Cmc4ISOOnPy3QaSiIJGJkVEDQ==",
|
||||
"version": "7.23.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.7.tgz",
|
||||
"integrity": "sha512-w06OXVOFso7LcbzMiDGt+3X7Rh7Ho8MmgPoWU3rarH+8upf+wSU/grlGbWzQyr3DkdN6ZeuMFjpdwW0Q+HxobA==",
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@@ -349,14 +355,14 @@
|
||||
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA=="
|
||||
},
|
||||
"node_modules/@emotion/react": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.1.tgz",
|
||||
"integrity": "sha512-5mlW1DquU5HaxjLkfkGN1GA/fvVGdyHURRiX/0FHl2cfIfRxSOfmxEH5YS43edp0OldZrZ+dkBKbngxcNCdZvA==",
|
||||
"version": "11.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.3.tgz",
|
||||
"integrity": "sha512-Cnn0kuq4DoONOMcnoVsTOR8E+AdnKFf//6kUWc4LCdnxj31pZWn7rIULd6Y7/Js1PiPHzn7SKCM9vB/jBni8eA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.18.3",
|
||||
"@emotion/babel-plugin": "^11.11.0",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/serialize": "^1.1.2",
|
||||
"@emotion/serialize": "^1.1.3",
|
||||
"@emotion/use-insertion-effect-with-fallbacks": "^1.0.1",
|
||||
"@emotion/utils": "^1.2.1",
|
||||
"@emotion/weak-memoize": "^0.3.1",
|
||||
@@ -372,9 +378,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emotion/serialize": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz",
|
||||
"integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.3.tgz",
|
||||
"integrity": "sha512-iD4D6QVZFDhcbH0RAG1uVu1CwVLMWUkCvAqqlewO/rxf8+87yIBAlt4+AxMiiKPLs5hFc0owNk/sLLAOROw3cA==",
|
||||
"dependencies": {
|
||||
"@emotion/hash": "^0.9.1",
|
||||
"@emotion/memoize": "^0.8.1",
|
||||
@@ -500,9 +506,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
|
||||
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
|
||||
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -596,14 +602,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/base": {
|
||||
"version": "5.0.0-beta.26",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.26.tgz",
|
||||
"integrity": "sha512-gPMRKC84VRw+tjqYoyBzyrBUqHQucMXdlBpYazHa5rCXrb91fYEQk5SqQ2U5kjxx9QxZxTBvWAmZ6DblIgaGhQ==",
|
||||
"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.4",
|
||||
"@babel/runtime": "^7.23.6",
|
||||
"@floating-ui/react-dom": "^2.0.4",
|
||||
"@mui/types": "^7.2.10",
|
||||
"@mui/utils": "^5.14.20",
|
||||
"@mui/types": "^7.2.11",
|
||||
"@mui/utils": "^5.15.2",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -627,20 +633,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "5.14.20",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.14.20.tgz",
|
||||
"integrity": "sha512-fXoGe8VOrIYajqALysFuyal1q1YmBARqJ3tmnWYDVl0scu8f6h6tZQbS2K8BY28QwkWNGyv4WRfuUkzN5HR3Ow==",
|
||||
"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.14.19",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.14.19.tgz",
|
||||
"integrity": "sha512-yjP8nluXxZGe3Y7pS+yxBV+hWZSsSBampCxkZwaw+1l+feL+rfP74vbEFbMrX/Kil9I/Y1tWfy5bs/eNvwNpWw==",
|
||||
"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.4"
|
||||
"@babel/runtime": "^7.23.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
@@ -661,16 +667,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/joy": {
|
||||
"version": "5.0.0-beta.17",
|
||||
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.17.tgz",
|
||||
"integrity": "sha512-KQMfQe7P98jRYWcjTxLRnjAlWre0YGvZstpE+xNJyOn6aTnMomnAskMIG0s2+k5PcluyxTEZZKZZ0Usl3M5D6g==",
|
||||
"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.4",
|
||||
"@mui/base": "5.0.0-beta.26",
|
||||
"@mui/core-downloads-tracker": "^5.14.20",
|
||||
"@mui/system": "^5.14.20",
|
||||
"@mui/types": "^7.2.10",
|
||||
"@mui/utils": "^5.14.20",
|
||||
"@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.2",
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
@@ -701,18 +707,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "5.14.20",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.14.20.tgz",
|
||||
"integrity": "sha512-SUcPZnN6e0h1AtrDktEl76Dsyo/7pyEUQ+SAVe9XhHg/iliA0b4Vo+Eg4HbNkELsMbpDsUF4WHp7rgflPG7qYQ==",
|
||||
"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.4",
|
||||
"@mui/base": "5.0.0-beta.26",
|
||||
"@mui/core-downloads-tracker": "^5.14.20",
|
||||
"@mui/system": "^5.14.20",
|
||||
"@mui/types": "^7.2.10",
|
||||
"@mui/utils": "^5.14.20",
|
||||
"@types/react-transition-group": "^4.4.9",
|
||||
"@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.2",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"clsx": "^2.0.0",
|
||||
"csstype": "^3.1.2",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -746,12 +752,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "5.14.20",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.14.20.tgz",
|
||||
"integrity": "sha512-WV560e1vhs2IHCh0pgUaWHznrcrVoW9+cDCahU1VTkuwPokWVvb71ccWQ1f8Y3tRBPPcNkU2dChkkRJChLmQlQ==",
|
||||
"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.4",
|
||||
"@mui/utils": "^5.14.20",
|
||||
"@babel/runtime": "^7.23.6",
|
||||
"@mui/utils": "^5.15.2",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -772,11 +778,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"version": "5.14.20",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.14.20.tgz",
|
||||
"integrity": "sha512-Vs4nGptd9wRslo9zeRkuWcZeIEp+oYbODy+fiZKqqr4CH1Gfi9fdP0Q1tGYk8OiJ2EPB/tZSAyOy62Hyp/iP7g==",
|
||||
"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.4",
|
||||
"@babel/runtime": "^7.23.6",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"csstype": "^3.1.2",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -803,15 +809,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/system": {
|
||||
"version": "5.14.20",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.14.20.tgz",
|
||||
"integrity": "sha512-jKOGtK4VfYZG5kdaryUHss4X6hzcfh0AihT8gmnkfqRtWP7xjY+vPaUhhuSeibE5sqA5wCtdY75z6ep9pxFnIg==",
|
||||
"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.4",
|
||||
"@mui/private-theming": "^5.14.20",
|
||||
"@mui/styled-engine": "^5.14.19",
|
||||
"@mui/types": "^7.2.10",
|
||||
"@mui/utils": "^5.14.20",
|
||||
"@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.2",
|
||||
"clsx": "^2.0.0",
|
||||
"csstype": "^3.1.2",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -842,9 +848,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/types": {
|
||||
"version": "7.2.10",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.10.tgz",
|
||||
"integrity": "sha512-wX1vbDC+lzF7FlhT6A3ffRZgEoKWPF8VqRoTu4lZwouFX2t90KyCMsgepMw5DxLak1BSp/KP86CmtZttikb/gQ==",
|
||||
"version": "7.2.11",
|
||||
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.11.tgz",
|
||||
"integrity": "sha512-KWe/QTEsFFlFSH+qRYf3zoFEj3z67s+qAuSnMMg+gFwbxG7P96Hm6g300inQL1Wy///gSRb8juX7Wafvp93m3w==",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.0 || ^18.0.0"
|
||||
},
|
||||
@@ -855,11 +861,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "5.14.20",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.14.20.tgz",
|
||||
"integrity": "sha512-Y6yL5MoFmtQml20DZnaaK1znrCEwG6/vRSzW8PKOTrzhyqKIql0FazZRUR7sA5EPASgiyKZfq0FPwISRXm5NdA==",
|
||||
"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.4",
|
||||
"@babel/runtime": "^7.23.6",
|
||||
"@types/prop-types": "^15.7.11",
|
||||
"prop-types": "^15.8.1",
|
||||
"react-is": "^18.2.0"
|
||||
@@ -1088,9 +1094,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.0.tgz",
|
||||
"integrity": "sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.1.tgz",
|
||||
"integrity": "sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
@@ -1105,54 +1111,54 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.0.tgz",
|
||||
"integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.1.tgz",
|
||||
"integrity": "sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.0.tgz",
|
||||
"integrity": "sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.1.tgz",
|
||||
"integrity": "sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.0",
|
||||
"@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
|
||||
"@prisma/fetch-engine": "5.7.0",
|
||||
"@prisma/get-platform": "5.7.0"
|
||||
"@prisma/debug": "5.7.1",
|
||||
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"@prisma/fetch-engine": "5.7.1",
|
||||
"@prisma/get-platform": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz",
|
||||
"integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==",
|
||||
"version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5.tgz",
|
||||
"integrity": "sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz",
|
||||
"integrity": "sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.1.tgz",
|
||||
"integrity": "sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.0",
|
||||
"@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
|
||||
"@prisma/get-platform": "5.7.0"
|
||||
"@prisma/debug": "5.7.1",
|
||||
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"@prisma/get-platform": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.0.tgz",
|
||||
"integrity": "sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.1.tgz",
|
||||
"integrity": "sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.0"
|
||||
"@prisma/debug": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rushstack/eslint-patch": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz",
|
||||
"integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz",
|
||||
"integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sanity/diff-match-patch": {
|
||||
@@ -1323,6 +1329,15 @@
|
||||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz",
|
||||
"integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -1343,14 +1358,20 @@
|
||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
|
||||
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
|
||||
"version": "20.10.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz",
|
||||
"integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nprogress": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz",
|
||||
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||
@@ -1377,19 +1398,28 @@
|
||||
"integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng=="
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.43",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.43.tgz",
|
||||
"integrity": "sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==",
|
||||
"version": "18.2.46",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.46.tgz",
|
||||
"integrity": "sha512-nNCvVBcZlvX4NU1nRRNV/mFl1nNRuTuslAJglQsq+8ldXe5Xv0Wd2f7WTE3jOxhLH2BFfiZGC6GCp+kHQbgG+w==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-beautiful-dnd": {
|
||||
"version": "13.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz",
|
||||
"integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz",
|
||||
"integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==",
|
||||
"version": "18.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz",
|
||||
"integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
@@ -1404,10 +1434,21 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-redux": {
|
||||
"version": "7.1.33",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz",
|
||||
"integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.0",
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-timeago": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.6.tgz",
|
||||
"integrity": "sha512-BFUH7FEple9m0K8dNvOqhba3+iCyrgsLcRtAnaZ6HlXvG8AJfC/7NCDcLaXfB1jvpAezwDRi/BflzdaLI4+Fow==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.7.tgz",
|
||||
"integrity": "sha512-ogD4Ror/hDG+pQggCX+TgPgJ8W2jeeUxsgNU485Qpm0Ma+E2TND2EJuKwK5+sxlkDXDEgsHradO0zWBkTgLzNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
@@ -1439,15 +1480,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz",
|
||||
"integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.16.0.tgz",
|
||||
"integrity": "sha512-H2GM3eUo12HpKZU9njig3DF5zJ58ja6ahj1GoHEHOgQvYxzoFJJEvC1MQ7T2l9Ha+69ZSOn7RTxOdpC/y3ikMw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.14.0",
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/typescript-estree": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0",
|
||||
"@typescript-eslint/scope-manager": "6.16.0",
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"@typescript-eslint/typescript-estree": "6.16.0",
|
||||
"@typescript-eslint/visitor-keys": "6.16.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1467,13 +1508,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz",
|
||||
"integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.16.0.tgz",
|
||||
"integrity": "sha512-0N7Y9DSPdaBQ3sqSCwlrm9zJwkpOuc6HYm7LpzLAPqBL7dmzAUimr4M29dMkOP/tEwvOCC/Cxo//yOfJD3HUiw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0"
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"@typescript-eslint/visitor-keys": "6.16.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1484,9 +1525,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz",
|
||||
"integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.16.0.tgz",
|
||||
"integrity": "sha512-hvDFpLEvTJoHutVl87+MG/c5C8I6LOgEx05zExTSJDEVU7hhR3jhV8M5zuggbdFCw98+HhZWPHZeKS97kS3JoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1497,16 +1538,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz",
|
||||
"integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==",
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.16.0.tgz",
|
||||
"integrity": "sha512-VTWZuixh/vr7nih6CfrdpmFNLEnoVBF1skfjdyGnNwXOH1SLeHItGdZDHhhAIzd3ACazyY2Fg76zuzOVTaknGA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0",
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"@typescript-eslint/visitor-keys": "6.16.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "9.0.3",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
},
|
||||
@@ -1523,13 +1565,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz",
|
||||
"integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==",
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
|
||||
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"balanced-match": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.16.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.16.0.tgz",
|
||||
"integrity": "sha512-QSFQLruk7fhs91a/Ep/LqRdbJCZ1Rq03rqBdKT5Ky17Sz8zRLUksqIe9DW0pKtg/Z35/ztbLQ6qpOCN6rOC11A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.16.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1553,6 +1619,12 @@
|
||||
"server-only": "^0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vercel/speed-insights": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-1.0.2.tgz",
|
||||
"integrity": "sha512-y5HWeB6RmlyVYxJAMrjiDEz8qAIy2cit0fhBq+MD78WaUwQvuBnQlX4+5MuwVTWi46bV3klaRMq83u9zUy1KOg==",
|
||||
"hasInstallScript": true
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@@ -1560,9 +1632,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.11.2",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
|
||||
"integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
|
||||
"version": "8.11.3",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
|
||||
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1970,9 +2042,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001568",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz",
|
||||
"integrity": "sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A==",
|
||||
"version": "1.0.30001572",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz",
|
||||
"integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -2078,9 +2150,9 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz",
|
||||
"integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz",
|
||||
"integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@@ -2192,6 +2264,14 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-box-model": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||
"dependencies": {
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -2563,15 +2643,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
|
||||
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
|
||||
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@eslint/js": "8.55.0",
|
||||
"@eslint/js": "8.56.0",
|
||||
"@humanwhocodes/config-array": "^0.11.13",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
@@ -2715,9 +2795,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import": {
|
||||
"version": "2.29.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz",
|
||||
"integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==",
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
|
||||
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.7",
|
||||
@@ -2736,7 +2816,7 @@
|
||||
"object.groupby": "^1.0.1",
|
||||
"object.values": "^1.1.7",
|
||||
"semver": "^6.3.1",
|
||||
"tsconfig-paths": "^3.14.2"
|
||||
"tsconfig-paths": "^3.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -3050,9 +3130,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
|
||||
"integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -4551,6 +4631,11 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
|
||||
@@ -5329,6 +5414,11 @@
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nprogress": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
||||
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5705,13 +5795,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.0.tgz",
|
||||
"integrity": "sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.1.tgz",
|
||||
"integrity": "sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.7.0"
|
||||
"@prisma/engines": "5.7.1"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
@@ -5796,6 +5886,11 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/raf-schd": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
|
||||
@@ -5807,6 +5902,24 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-beautiful-dnd": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
|
||||
"integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"css-box-model": "^1.2.0",
|
||||
"memoize-one": "^5.1.1",
|
||||
"raf-schd": "^4.0.2",
|
||||
"react-redux": "^7.2.0",
|
||||
"redux": "^4.0.4",
|
||||
"use-memo-one": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
@@ -5861,6 +5974,44 @@
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "7.2.9",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@types/react-redux": "^7.1.20",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.3 || ^17 || ^18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-1.0.5.tgz",
|
||||
"integrity": "sha512-OP0whNQCko+f4BgoptGaeIc7StBRyeMeJ+8r/7rXACBDf9W5EcMWuM32hfqPDMenS2HFy/eZVi/r8XqK+ZIEag==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-ssr-prepass": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
|
||||
@@ -5904,6 +6055,14 @@
|
||||
"string_decoder": "~0.10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz",
|
||||
@@ -5925,9 +6084,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.1",
|
||||
@@ -6577,9 +6736,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tesseract.js": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.0.3.tgz",
|
||||
"integrity": "sha512-UcEaIRQ+KSjxl57SS2WQgnac8hON0uJgqR6418MYLBeFPGqrAooNwcKVIoSVCzflO3CxTKPg5Ic7z7JuW7wOGA==",
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-5.0.4.tgz",
|
||||
"integrity": "sha512-GCIoSQMZlvTP2AaHrjUOH29/oyO7ZyHVe+BhTexEcO7/nDClRVDRjl2sYJLOWSSNbTDrm5q2m1+gfaf3lUrZ5Q==",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"bmp-js": "^0.1.0",
|
||||
@@ -6624,6 +6783,11 @@
|
||||
"xtend": "~2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.1.tgz",
|
||||
"integrity": "sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw=="
|
||||
},
|
||||
"node_modules/to-fast-properties": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
|
||||
@@ -6688,9 +6852,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||
"integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
"integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json5": "^0.0.29",
|
||||
@@ -6946,6 +7110,14 @@
|
||||
"integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/use-memo-one": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
|
||||
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz",
|
||||
@@ -7263,9 +7435,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "4.3.9",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.3.9.tgz",
|
||||
"integrity": "sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==",
|
||||
"version": "4.4.7",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.7.tgz",
|
||||
"integrity": "sha512-QFJWJMdlETcI69paJwhSMJz7PPWjVP8Sjhclxmxmxv/RYI7ZOvR5BHX+ktH0we9gTWQMxcne8q1OY8xxz604gw==",
|
||||
"dependencies": {
|
||||
"use-sync-external-store": "1.2.0"
|
||||
},
|
||||
@@ -7273,10 +7445,14 @@
|
||||
"node": ">=12.7.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=16.8",
|
||||
"immer": ">=9.0",
|
||||
"react": ">=16.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
|
||||
+24
-18
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.7.3",
|
||||
"version": "1.10.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -15,54 +15,60 @@
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.7",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.14.19",
|
||||
"@mui/joy": "^5.0.0-beta.17",
|
||||
"@mui/icons-material": "^5.15.2",
|
||||
"@mui/joy": "^5.0.0-beta.20",
|
||||
"@next/bundle-analyzer": "^14.0.4",
|
||||
"@prisma/client": "^5.7.0",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@sanity/diff-match-patch": "^3.1.1",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tanstack/react-query": "^4.36.1",
|
||||
"@trpc/client": "^10.44.1",
|
||||
"@trpc/next": "^10.44.1",
|
||||
"@trpc/react-query": "^10.44.1",
|
||||
"@trpc/server": "^10.44.1",
|
||||
"@tanstack/react-query": "~4.36.1",
|
||||
"@trpc/client": "10.44.1",
|
||||
"@trpc/next": "10.44.1",
|
||||
"@trpc/react-query": "10.44.1",
|
||||
"@trpc/server": "10.44.1",
|
||||
"@vercel/analytics": "^1.1.1",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"browser-fs-access": "^0.35.0",
|
||||
"eventsource-parser": "^1.1.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"next": "^14.0.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "4.0.269",
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^1.0.5",
|
||||
"react-timeago": "^7.2.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"superjson": "^2.2.1",
|
||||
"tesseract.js": "^5.0.3",
|
||||
"tesseract.js": "^5.0.4",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "~4.3.9"
|
||||
"zustand": "^4.4.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/puppeteer": "^0.0.5",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/node": "^20.10.6",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react": "^18.2.46",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.6",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
"prisma": "^5.7.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+22
-11
@@ -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,11 +11,15 @@ import { apiQuery } from '~/common/util/trpc.client';
|
||||
import 'katex/dist/katex.min.css';
|
||||
import '~/common/styles/CodePrism.css';
|
||||
import '~/common/styles/GithubMarkdown.css';
|
||||
import '~/common/styles/NProgress.css';
|
||||
import '~/common/styles/app.styles.css';
|
||||
|
||||
import { ProviderBackend } from '~/common/state/ProviderBackend';
|
||||
import { ProviderSnacks } from '~/common/state/ProviderSnacks';
|
||||
import { ProviderTRPCQueryClient } from '~/common/state/ProviderTRPCQueryClient';
|
||||
import { ProviderTheming } from '~/common/state/ProviderTheming';
|
||||
import { ProviderBackendAndNoSSR } from '~/common/providers/ProviderBackendAndNoSSR';
|
||||
import { ProviderBootstrapLogic } from '~/common/providers/ProviderBootstrapLogic';
|
||||
import { ProviderSingleTab } from '~/common/providers/ProviderSingleTab';
|
||||
import { ProviderSnacks } from '~/common/providers/ProviderSnacks';
|
||||
import { ProviderTRPCQueryClient } from '~/common/providers/ProviderTRPCQueryClient';
|
||||
import { ProviderTheming } from '~/common/providers/ProviderTheming';
|
||||
|
||||
|
||||
const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
|
||||
@@ -25,16 +31,21 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
|
||||
</Head>
|
||||
|
||||
<ProviderTheming emotionCache={emotionCache}>
|
||||
<ProviderTRPCQueryClient>
|
||||
<ProviderSnacks>
|
||||
<ProviderBackend>
|
||||
<Component {...pageProps} />
|
||||
</ProviderBackend>
|
||||
</ProviderSnacks>
|
||||
</ProviderTRPCQueryClient>
|
||||
<ProviderSingleTab>
|
||||
<ProviderBootstrapLogic>
|
||||
<ProviderTRPCQueryClient>
|
||||
<ProviderSnacks>
|
||||
<ProviderBackendAndNoSSR>
|
||||
<Component {...pageProps} />
|
||||
</ProviderBackendAndNoSSR>
|
||||
</ProviderSnacks>
|
||||
</ProviderTRPCQueryClient>
|
||||
</ProviderBootstrapLogic>
|
||||
</ProviderSingleTab>
|
||||
</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>
|
||||
);
|
||||
|
||||
+2
-6
@@ -2,13 +2,9 @@ import * as React from 'react';
|
||||
|
||||
import { AppCall } from '../src/apps/call/AppCall';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function CallPage() {
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppCall />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'optima' }, <AppCall />);
|
||||
}
|
||||
+9
-10
@@ -1,18 +1,17 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppChat } from '../src/apps/chat/AppChat';
|
||||
import { useShowNewsOnUpdate } from '../src/apps/news/news.hooks';
|
||||
import { useRedirectToNewsOnUpdates } from '../src/apps/news/news.hooks';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function ChatPage() {
|
||||
// show the News page on updates
|
||||
useShowNewsOnUpdate();
|
||||
export default function IndexPage() {
|
||||
// show the News page if there are unseen updates
|
||||
useRedirectToNewsOnUpdates();
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppChat />
|
||||
</AppLayout>
|
||||
);
|
||||
// TODO: This Index page will point to the Dashboard (or a landing page) soon
|
||||
// For now it offers the chat experience, but this will change. #299
|
||||
|
||||
return withLayout({ type: 'optima' }, <AppChat />);
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { Box, Typography } from '@mui/joy';
|
||||
|
||||
import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { apiQuery } from '~/common/util/trpc.client';
|
||||
import { navigateToIndex } from '~/common/app.routes';
|
||||
import { openLayoutModelsSetup } from '~/common/layout/store-applayout';
|
||||
import { navigateToIndex, useRouterQuery } from '~/common/app.routes';
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
|
||||
@@ -36,14 +35,14 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
|
||||
useModelsStore.getState().setOpenRoutersKey(openRouterKey);
|
||||
|
||||
// 2. Navigate to the chat app
|
||||
navigateToIndex(true).then(() => openLayoutModelsSetup());
|
||||
void navigateToIndex(true); //.then(openModelsSetup);
|
||||
|
||||
}, [isSuccess, openRouterKey]);
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level1',
|
||||
backgroundColor: themeBgApp,
|
||||
overflowY: 'auto',
|
||||
display: 'flex', justifyContent: 'center',
|
||||
p: { xs: 3, md: 6 },
|
||||
@@ -84,15 +83,10 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
|
||||
* Docs: https://openrouter.ai/docs#oauth
|
||||
* Example URL: https://localhost:3000/link/callback_openrouter?code=SomeCode
|
||||
*/
|
||||
export default function Page() {
|
||||
export default function CallbackPage() {
|
||||
|
||||
// get the 'code=...' from the URL
|
||||
const { query } = useRouter();
|
||||
const { code: openRouterCode } = query;
|
||||
// external state - get the 'code=...' from the URL
|
||||
const { code } = useRouterQuery<{ code: string | undefined }>();
|
||||
|
||||
return (
|
||||
<AppLayout suspendAutoModelsSetup>
|
||||
<CallbackOpenRouterPage openRouterCode={openRouterCode as (string | undefined)} />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'plain' }, <CallbackOpenRouterPage openRouterCode={code} />);
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { AppChatLink } from '../../../src/apps/link/AppChatLink';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { useRouterQuery } from '~/common/app.routes';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function ChatLinkPage() {
|
||||
const { query } = useRouter();
|
||||
const chatLinkId = query?.chatLinkId as string ?? '';
|
||||
|
||||
return (
|
||||
<AppLayout suspendAutoModelsSetup>
|
||||
<AppChatLink linkId={chatLinkId} />
|
||||
</AppLayout>
|
||||
);
|
||||
// external state
|
||||
const { chatLinkId } = useRouterQuery<{ chatLinkId: string | undefined }>();
|
||||
|
||||
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppChatLink linkId={chatLinkId || ''} />);
|
||||
}
|
||||
+14
-16
@@ -1,5 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { Alert, Box, Button, Typography } from '@mui/joy';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
@@ -8,10 +7,11 @@ import { setComposerStartupText } from '../../src/apps/chat/components/composer/
|
||||
|
||||
import { callBrowseFetchPage } from '~/modules/browse/browse.client';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { LogoProgress } from '~/common/components/LogoProgress';
|
||||
import { asValidURL } from '~/common/util/urlUtils';
|
||||
import { navigateToIndex } from '~/common/app.routes';
|
||||
import { navigateToIndex, useRouterQuery } from '~/common/app.routes';
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
/**
|
||||
@@ -31,8 +31,10 @@ function AppShareTarget() {
|
||||
const [isDownloading, setIsDownloading] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const { query } = useRouter();
|
||||
|
||||
const { url: queryUrl, text: queryText } = useRouterQuery<{
|
||||
url: string | string[] | undefined,
|
||||
text: string | string[] | undefined,
|
||||
}>();
|
||||
|
||||
const queueComposerTextAndLaunchApp = React.useCallback((text: string) => {
|
||||
setComposerStartupText(text);
|
||||
@@ -43,11 +45,11 @@ function AppShareTarget() {
|
||||
// Detect the share Intent from the query
|
||||
React.useEffect(() => {
|
||||
// skip when query is not parsed yet
|
||||
if (!Object.keys(query).length)
|
||||
let queryTextItem = queryUrl || queryText || null;
|
||||
if (!queryTextItem)
|
||||
return;
|
||||
|
||||
// single item from the query
|
||||
let queryTextItem: string[] | string | null = query.url || query.text || null;
|
||||
if (Array.isArray(queryTextItem))
|
||||
queryTextItem = queryTextItem[0];
|
||||
|
||||
@@ -58,9 +60,9 @@ function AppShareTarget() {
|
||||
else if (queryTextItem)
|
||||
setIntentText(queryTextItem);
|
||||
else
|
||||
setErrorMessage('No text or url. Received: ' + JSON.stringify(query));
|
||||
setErrorMessage('No text or url. Received: ' + JSON.stringify({ queryText, queryUrl }));
|
||||
|
||||
}, [query.url, query.text, query]);
|
||||
}, [queryText, queryUrl]);
|
||||
|
||||
|
||||
// Text -> Composer
|
||||
@@ -90,7 +92,7 @@ function AppShareTarget() {
|
||||
return (
|
||||
|
||||
<Box sx={{
|
||||
backgroundColor: 'background.level2',
|
||||
backgroundColor: themeBgApp,
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
@@ -132,10 +134,6 @@ function AppShareTarget() {
|
||||
* This page will be invoked on mobile when sharing Text/URLs/Files from other APPs
|
||||
* Example URL: https://localhost:3000/link/share_target?title=This+Title&text=https%3A%2F%2Fexample.com%2Fapp%2Fpath
|
||||
*/
|
||||
export default function LaunchPage() {
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppShareTarget />
|
||||
</AppLayout>
|
||||
);
|
||||
export default function ShareTargetPage() {
|
||||
return withLayout({ type: 'plain' }, <AppShareTarget />);
|
||||
}
|
||||
+3
-7
@@ -3,16 +3,12 @@ import * as React from 'react';
|
||||
import { AppNews } from '../src/apps/news/AppNews';
|
||||
import { useMarkNewsAsSeen } from '../src/apps/news/news.hooks';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function NewsPage() {
|
||||
// update the last seen news version
|
||||
// 'touch' the last seen news version
|
||||
useMarkNewsAsSeen();
|
||||
|
||||
return (
|
||||
<AppLayout suspendAutoModelsSetup>
|
||||
<AppNews />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppNews />);
|
||||
}
|
||||
+2
-6
@@ -2,13 +2,9 @@ import * as React from 'react';
|
||||
|
||||
import { AppPersonas } from '../src/apps/personas/AppPersonas';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function PersonasPage() {
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppPersonas />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'optima' }, <AppPersonas />);
|
||||
}
|
||||
@@ -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';
|
||||
@@ -15,20 +14,20 @@ import { useChatLLMDropdown } from '../chat/components/applayout/useLLMDropdown'
|
||||
|
||||
import { EXPERIMENTAL_speakTextStream } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { SystemPurposeId, SystemPurposes } from '../../data';
|
||||
import { VChatMessageIn } from '~/modules/llms/transports/chatGenerate';
|
||||
import { streamChat } from '~/modules/llms/transports/streamChat';
|
||||
import { llmStreamingChatGenerate, VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
import { useElevenLabsVoiceDropdown } from '~/modules/elevenlabs/useElevenLabsVoiceDropdown';
|
||||
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { conversationTitle, createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { playSoundUrl, usePlaySoundUrl } from '~/common/util/audioUtils';
|
||||
import { useLayoutPluggable } from '~/common/layout/store-applayout';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { CallAvatar } from './components/CallAvatar';
|
||||
import { CallButton } from './components/CallButton';
|
||||
import { CallMessage } from './components/CallMessage';
|
||||
import { CallStatus } from './components/CallStatus';
|
||||
import { launchAppChat, ROUTE_APP_CHAT } from '~/common/app.routes';
|
||||
|
||||
|
||||
function CallMenuItems(props: {
|
||||
@@ -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('/');
|
||||
}, 2000);
|
||||
setTimeout(launchAppChat, 2000);
|
||||
return;
|
||||
// command: regenerate answer
|
||||
case 'Retry.':
|
||||
@@ -216,7 +212,7 @@ export function CallUI(props: {
|
||||
responseAbortController.current = new AbortController();
|
||||
let finalText = '';
|
||||
let error: any | null = null;
|
||||
streamChat(chatLLMId, callPrompt, responseAbortController.current.signal, (updatedMessage: Partial<DMessage>) => {
|
||||
llmStreamingChatGenerate(chatLLMId, callPrompt, null, null, responseAbortController.current.signal, (updatedMessage: Partial<DMessage>) => {
|
||||
const text = updatedMessage.text?.trim();
|
||||
if (text) {
|
||||
finalText = text;
|
||||
@@ -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;
|
||||
@@ -273,7 +269,7 @@ export function CallUI(props: {
|
||||
, [overridePersonaVoice, pushToTalk],
|
||||
);
|
||||
|
||||
useLayoutPluggable(chatLLMDropdown, null, menuItems);
|
||||
usePluggableOptimaLayout(null, chatLLMDropdown, menuItems, 'CallUI');
|
||||
|
||||
|
||||
return <>
|
||||
@@ -367,7 +363,7 @@ export function CallUI(props: {
|
||||
)}
|
||||
|
||||
{/* [ended] Back / Call Again */}
|
||||
{(isEnded || isDeclined) && <Link noLinkStyle href='/'><CallButton Icon={ArrowBackIcon} text='Back' variant='soft' /></Link>}
|
||||
{(isEnded || isDeclined) && <Link noLinkStyle href={ROUTE_APP_CHAT}><CallButton Icon={ArrowBackIcon} text='Back' variant='soft' /></Link>}
|
||||
{(isEnded || isDeclined) && <CallButton Icon={CallIcon} text='Call Again' color='success' variant='soft' onClick={() => setStage('connected')} />}
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -11,9 +11,9 @@ import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
|
||||
import { navigateBack } from '~/common/app.routes';
|
||||
import { openLayoutPreferences } from '~/common/layout/store-applayout';
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUICounter } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
|
||||
const [recognitionOverride, setRecognitionOverride] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const { openPreferencesTab } = useOptimaLayout();
|
||||
const recognition = useCapabilityBrowserSpeechRecognition();
|
||||
const synthesis = useCapabilityElevenLabs();
|
||||
const chatIsEmpty = useChatStore(state => {
|
||||
@@ -103,7 +104,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
|
||||
const handleOverrideRecognition = () => setRecognitionOverride(true);
|
||||
|
||||
const handleConfigureElevenLabs = () => {
|
||||
openLayoutPreferences(3);
|
||||
openPreferencesTab(3);
|
||||
};
|
||||
|
||||
const handleFinishButton = () => {
|
||||
@@ -199,7 +200,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
|
||||
|
||||
<IconButton
|
||||
size='lg' variant={allGood ? 'soft' : 'solid'} color={allGood ? 'success' : 'danger'}
|
||||
onClick={handleFinishButton} sx={{ borderRadius: '50px' }}
|
||||
onClick={handleFinishButton} sx={{ borderRadius: '50px', mr: 0.5 }}
|
||||
>
|
||||
{allGood ? <ArrowForwardIcon sx={{ fontSize: '1.5em' }} /> : <CloseIcon sx={{ fontSize: '1.5em' }} />}
|
||||
</IconButton>
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { Chip, ColorPaletteProp, VariantProp } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import { VChatMessageIn } from '~/modules/llms/transports/chatGenerate';
|
||||
import type { VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
|
||||
|
||||
export function CallMessage(props: {
|
||||
|
||||
+240
-152
@@ -1,46 +1,53 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box } from '@mui/joy';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
|
||||
import { CmdRunBrowse } from '~/modules/browse/browse.client';
|
||||
import { CmdRunProdia } from '~/modules/prodia/prodia.client';
|
||||
import { CmdRunReact } from '~/modules/aifn/react/react';
|
||||
import { useTheme } from '@mui/joy';
|
||||
|
||||
import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
|
||||
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
|
||||
import { imaginePromptFromText } from '~/modules/aifn/imagine/imaginePromptFromText';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { useBrowseStore } from '~/modules/browse/store-module-browsing';
|
||||
import { useChatLLM, useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { GoodPanelResizeHandler } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useConversation } from '~/common/state/store-chats';
|
||||
import { openLayoutLLMOptions, useLayoutPluggable } from '~/common/layout/store-applayout';
|
||||
import { themeBgApp, themeBgAppChatComposer } from '~/common/app.theme';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
|
||||
import { ChatDrawerItemsMemo } from './components/applayout/ChatDrawerItems';
|
||||
import { ChatDrawerContentMemo } from './components/applayout/ChatDrawerItems';
|
||||
import { ChatDropdowns } from './components/applayout/ChatDropdowns';
|
||||
import { ChatMenuItems } from './components/applayout/ChatMenuItems';
|
||||
import { ChatMessageList } from './components/ChatMessageList';
|
||||
import { CmdAddRoleMessage, CmdHelp, createCommandsHelpMessage, extractCommands } from './editors/commands';
|
||||
import { Composer } from './components/composer/Composer';
|
||||
import { Ephemerals } from './components/Ephemerals';
|
||||
import { usePanesManager } from './components/usePanesManager';
|
||||
import { ScrollToBottom } from './components/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from './components/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { usePanesManager } from './components/panes/usePanesManager';
|
||||
|
||||
import { extractChatCommand, findAllChatCommands } from './commands/commands.registry';
|
||||
import { runAssistantUpdatingState } from './editors/chat-stream';
|
||||
import { runBrowseUpdatingState } from './editors/browse-load';
|
||||
import { runImageGenerationUpdatingState } from './editors/image-generate';
|
||||
import { runReActUpdatingState } from './editors/react-tangent';
|
||||
|
||||
|
||||
/**
|
||||
* Mode: how to treat the input from the Composer
|
||||
*/
|
||||
export type ChatModeId = 'immediate' | 'write-user' | 'react' | 'draw-imagine' | 'draw-imagine-plus';
|
||||
export type ChatModeId =
|
||||
| 'generate-text'
|
||||
| 'append-user'
|
||||
| 'generate-image'
|
||||
| 'generate-react';
|
||||
|
||||
|
||||
const SPECIAL_ID_WIPE_ALL: DConversationId = 'wipe-chats';
|
||||
@@ -56,8 +63,13 @@ export function AppChat() {
|
||||
const [flattenConversationId, setFlattenConversationId] = React.useState<DConversationId | null>(null);
|
||||
const showNextTitle = React.useRef(false);
|
||||
const composerTextAreaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
const [_selectedFolderId, setSelectedFolderId] = React.useState<string | null>(null);
|
||||
|
||||
// external state
|
||||
const theme = useTheme();
|
||||
|
||||
const { openLlmOptions } = useOptimaLayout();
|
||||
|
||||
const { chatLLM } = useChatLLM();
|
||||
|
||||
const {
|
||||
@@ -66,7 +78,10 @@ export function AppChat() {
|
||||
navigateHistoryInFocusedPane,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
setFocusedPaneIndex,
|
||||
paneIndex,
|
||||
duplicatePane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
} = usePanesManager();
|
||||
|
||||
const {
|
||||
@@ -75,6 +90,7 @@ export function AppChat() {
|
||||
isChatEmpty: isFocusedChatEmpty,
|
||||
areChatsEmpty,
|
||||
newConversationId,
|
||||
conversationsLength,
|
||||
_remove_systemPurposeId: focusedSystemPurposeId,
|
||||
prependNewConversation,
|
||||
branchConversation,
|
||||
@@ -83,14 +99,22 @@ export function AppChat() {
|
||||
setMessages,
|
||||
} = useConversation(focusedConversationId);
|
||||
|
||||
const { mayWork: capabilityHasT2I } = useCapabilityTextToImage();
|
||||
|
||||
const { folderConversationsCount, selectedFolderId } = useFolderStore(state => {
|
||||
const selectedFolderId = state.useFolders ? _selectedFolderId : null;
|
||||
return {
|
||||
folderConversationsCount: selectedFolderId
|
||||
? state.folders.find(folder => folder.id === selectedFolderId)?.conversationIds.length || 0
|
||||
: conversationsLength,
|
||||
selectedFolderId,
|
||||
};
|
||||
});
|
||||
|
||||
// Window actions
|
||||
|
||||
const chatPaneIDs = chatPanes.length > 0 ? chatPanes.map(pane => pane.conversationId) : [null];
|
||||
|
||||
const setActivePaneIndex = React.useCallback((idx: number) => {
|
||||
setFocusedPaneIndex(idx);
|
||||
}, [setFocusedPaneIndex]);
|
||||
const panesConversationIDs = chatPanes.length > 0 ? chatPanes.map((pane) => pane.conversationId) : [null];
|
||||
const isSplitPane = chatPanes.length > 1;
|
||||
|
||||
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
|
||||
conversationId && openConversationInFocusedPane(conversationId);
|
||||
@@ -100,6 +124,13 @@ export function AppChat() {
|
||||
conversationId && openConversationInSplitPane(conversationId);
|
||||
}, [openConversationInSplitPane]);
|
||||
|
||||
const toggleSplitPane = React.useCallback(() => {
|
||||
if (isSplitPane)
|
||||
removePane(paneIndex ?? chatPanes.length - 1);
|
||||
else
|
||||
duplicatePane(paneIndex ?? chatPanes.length - 1);
|
||||
}, [chatPanes.length, duplicatePane, isSplitPane, paneIndex, removePane]);
|
||||
|
||||
const handleNavigateHistory = React.useCallback((direction: 'back' | 'forward') => {
|
||||
if (navigateHistoryInFocusedPane(direction))
|
||||
showNextTitle.current = true;
|
||||
@@ -114,7 +145,6 @@ export function AppChat() {
|
||||
}
|
||||
}, [focusedChatNumber, focusedChatTitle]);
|
||||
|
||||
|
||||
// Execution
|
||||
|
||||
const _handleExecute = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) => {
|
||||
@@ -124,29 +154,36 @@ export function AppChat() {
|
||||
// "/command ...": overrides the chat mode
|
||||
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
|
||||
if (lastMessage?.role === 'user') {
|
||||
const pieces = extractCommands(lastMessage.text);
|
||||
if (pieces.length == 2 && pieces[0].type === 'cmd' && pieces[1].type === 'text') {
|
||||
const [command, prompt] = [pieces[0].value, pieces[1].value];
|
||||
if (CmdRunProdia.includes(command)) {
|
||||
setMessages(conversationId, history);
|
||||
return await runImageGenerationUpdatingState(conversationId, prompt);
|
||||
}
|
||||
if (CmdRunReact.includes(command) && chatLLMId) {
|
||||
setMessages(conversationId, history);
|
||||
return await runReActUpdatingState(conversationId, prompt, chatLLMId);
|
||||
}
|
||||
if (CmdRunBrowse.includes(command) && prompt?.trim() && useBrowseStore.getState().enableCommandBrowse) {
|
||||
setMessages(conversationId, history);
|
||||
return await runBrowseUpdatingState(conversationId, prompt);
|
||||
}
|
||||
if (CmdAddRoleMessage.includes(command)) {
|
||||
lastMessage.role = command.startsWith('/s') ? 'system' : command.startsWith('/a') ? 'assistant' : 'user';
|
||||
lastMessage.sender = 'Bot';
|
||||
lastMessage.text = prompt;
|
||||
return setMessages(conversationId, history);
|
||||
}
|
||||
if (CmdHelp.includes(command)) {
|
||||
return setMessages(conversationId, [...history, createCommandsHelpMessage()]);
|
||||
const chatCommand = extractChatCommand(lastMessage.text)[0];
|
||||
if (chatCommand && chatCommand.type === 'cmd') {
|
||||
switch (chatCommand.providerId) {
|
||||
case 'ass-browse':
|
||||
setMessages(conversationId, history);
|
||||
return await runBrowseUpdatingState(conversationId, chatCommand.params!);
|
||||
|
||||
case 'ass-t2i':
|
||||
setMessages(conversationId, history);
|
||||
return await runImageGenerationUpdatingState(conversationId, chatCommand.params!);
|
||||
|
||||
case 'ass-react':
|
||||
setMessages(conversationId, history);
|
||||
return await runReActUpdatingState(conversationId, chatCommand.params!, chatLLMId);
|
||||
|
||||
case 'chat-alter':
|
||||
Object.assign(lastMessage, {
|
||||
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
|
||||
sender: 'Bot',
|
||||
text: chatCommand.params || '',
|
||||
} satisfies Partial<DMessage>);
|
||||
return setMessages(conversationId, history);
|
||||
|
||||
case 'cmd-help':
|
||||
const chatCommandsText = findAllChatCommands()
|
||||
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
|
||||
.join('\n');
|
||||
const helpMessage = createDMessage('assistant', 'Available Chat Commands:\n' + chatCommandsText);
|
||||
helpMessage.originLLM = Brand.Title.Base;
|
||||
return setMessages(conversationId, [...history, helpMessage]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,27 +191,27 @@ export function AppChat() {
|
||||
// synchronous long-duration tasks, which update the state as they go
|
||||
if (chatLLMId && focusedSystemPurposeId) {
|
||||
switch (chatModeId) {
|
||||
case 'immediate':
|
||||
case 'generate-text':
|
||||
return await runAssistantUpdatingState(conversationId, history, chatLLMId, focusedSystemPurposeId);
|
||||
case 'write-user':
|
||||
|
||||
case 'append-user':
|
||||
return setMessages(conversationId, history);
|
||||
case 'react':
|
||||
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
// also add a 'fake' user message with the '/draw' command
|
||||
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
|
||||
...message,
|
||||
text: `/draw ${lastMessage.text}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(conversationId, lastMessage.text);
|
||||
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
setMessages(conversationId, history);
|
||||
return await runReActUpdatingState(conversationId, lastMessage.text, chatLLMId);
|
||||
case 'draw-imagine':
|
||||
case 'draw-imagine-plus':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
const imagePrompt = chatModeId == 'draw-imagine-plus'
|
||||
? await imaginePromptFromText(lastMessage.text) || 'An error sign.'
|
||||
: lastMessage.text;
|
||||
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
|
||||
...message,
|
||||
text: `${CmdRunProdia[0]} ${imagePrompt}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(conversationId, imagePrompt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +221,6 @@ export function AppChat() {
|
||||
}, [focusedSystemPurposeId, setMessages]);
|
||||
|
||||
const handleComposerAction = (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart): boolean => {
|
||||
|
||||
// validate inputs
|
||||
if (multiPartMessage.length !== 1 || multiPartMessage[0].type !== 'text-block') {
|
||||
addSnackbar({
|
||||
@@ -213,13 +249,13 @@ export function AppChat() {
|
||||
};
|
||||
|
||||
const handleConversationExecuteHistory = async (conversationId: DConversationId, history: DMessage[]) =>
|
||||
await _handleExecute('immediate', conversationId, history);
|
||||
await _handleExecute('generate-text', conversationId, history);
|
||||
|
||||
const handleMessageRegenerateLast = React.useCallback(async () => {
|
||||
const focusedConversation = getConversation(focusedConversationId);
|
||||
if (focusedConversation?.messages?.length) {
|
||||
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
|
||||
return await _handleExecute('immediate', focusedConversation.id, lastMessage.role === 'assistant'
|
||||
return await _handleExecute('generate-text', focusedConversation.id, lastMessage.role === 'assistant'
|
||||
? focusedConversation.messages.slice(0, -1)
|
||||
: [...focusedConversation.messages],
|
||||
);
|
||||
@@ -228,30 +264,39 @@ export function AppChat() {
|
||||
|
||||
const handleTextDiagram = async (diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig);
|
||||
|
||||
const handleTextImaginePlus = async (conversationId: DConversationId, messageText: string) => {
|
||||
const handleTextImagine = async (conversationId: DConversationId, messageText: string) => {
|
||||
const conversation = getConversation(conversationId);
|
||||
if (conversation)
|
||||
return await _handleExecute('draw-imagine-plus', conversationId, [
|
||||
...conversation.messages,
|
||||
createDMessage('user', messageText),
|
||||
]);
|
||||
if (!conversation)
|
||||
return;
|
||||
const imaginedPrompt = await imaginePromptFromText(messageText) || 'An error sign.';
|
||||
return await _handleExecute('generate-image', conversationId, [
|
||||
...conversation.messages,
|
||||
createDMessage('user', imaginedPrompt),
|
||||
]);
|
||||
};
|
||||
|
||||
const handleTextSpeak = async (text: string) => {
|
||||
await speakText(text);
|
||||
};
|
||||
|
||||
|
||||
// Chat actions
|
||||
|
||||
const handleConversationNew = React.useCallback(() => {
|
||||
|
||||
// activate an existing new conversation if present, or create another
|
||||
setFocusedConversationId(newConversationId
|
||||
const conversationId = newConversationId
|
||||
? newConversationId
|
||||
: prependNewConversation(focusedSystemPurposeId ?? undefined),
|
||||
);
|
||||
: prependNewConversation(focusedSystemPurposeId ?? undefined);
|
||||
setFocusedConversationId(conversationId);
|
||||
|
||||
// if a folder is selected, add the new conversation to the folder
|
||||
if (selectedFolderId && conversationId)
|
||||
useFolderStore.getState().addConversationToFolder(selectedFolderId, conversationId);
|
||||
|
||||
// focus the composer
|
||||
composerTextAreaRef.current?.focus();
|
||||
}, [focusedSystemPurposeId, newConversationId, prependNewConversation, setFocusedConversationId]);
|
||||
|
||||
}, [focusedSystemPurposeId, newConversationId, prependNewConversation, selectedFolderId, setFocusedConversationId]);
|
||||
|
||||
const handleConversationImportDialog = () => setTradeConfig({ dir: 'import' });
|
||||
|
||||
@@ -279,7 +324,6 @@ export function AppChat() {
|
||||
|
||||
const handleConversationFlatten = (conversationId: DConversationId) => setFlattenConversationId(conversationId);
|
||||
|
||||
|
||||
const handleConfirmedClearConversation = React.useCallback(() => {
|
||||
if (clearConversationId) {
|
||||
setMessages(clearConversationId, []);
|
||||
@@ -289,12 +333,11 @@ export function AppChat() {
|
||||
|
||||
const handleConversationClear = (conversationId: DConversationId) => setClearConversationId(conversationId);
|
||||
|
||||
|
||||
const handleConfirmedDeleteConversation = () => {
|
||||
if (deleteConversationId) {
|
||||
let nextConversationId: DConversationId | null;
|
||||
if (deleteConversationId === SPECIAL_ID_WIPE_ALL)
|
||||
nextConversationId = wipeAllConversations(focusedSystemPurposeId ?? undefined);
|
||||
nextConversationId = wipeAllConversations(focusedSystemPurposeId ?? undefined, selectedFolderId);
|
||||
else
|
||||
nextConversationId = deleteConversation(deleteConversationId);
|
||||
setFocusedConversationId(nextConversationId);
|
||||
@@ -304,53 +347,59 @@ export function AppChat() {
|
||||
|
||||
const handleConversationsDeleteAll = () => setDeleteConversationId(SPECIAL_ID_WIPE_ALL);
|
||||
|
||||
const handleConversationDelete = React.useCallback((conversationId: DConversationId, bypassConfirmation: boolean) => {
|
||||
if (bypassConfirmation)
|
||||
setFocusedConversationId(deleteConversation(conversationId));
|
||||
else
|
||||
setDeleteConversationId(conversationId);
|
||||
}, [deleteConversation, setFocusedConversationId]);
|
||||
|
||||
const handleConversationDelete = React.useCallback(
|
||||
(conversationId: DConversationId, bypassConfirmation: boolean) => {
|
||||
if (bypassConfirmation) setFocusedConversationId(deleteConversation(conversationId));
|
||||
else setDeleteConversationId(conversationId);
|
||||
},
|
||||
[deleteConversation, setFocusedConversationId],
|
||||
);
|
||||
|
||||
// Shortcuts
|
||||
|
||||
const handleOpenChatLlmOptions = React.useCallback(() => {
|
||||
const { chatLLMId } = useModelsStore.getState();
|
||||
if (!chatLLMId) return;
|
||||
openLayoutLLMOptions(chatLLMId);
|
||||
}, []);
|
||||
openLlmOptions(chatLLMId);
|
||||
}, [openLlmOptions]);
|
||||
|
||||
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
|
||||
['o', true, true, false, handleOpenChatLlmOptions],
|
||||
['r', true, true, false, handleMessageRegenerateLast],
|
||||
['n', true, false, true, handleConversationNew],
|
||||
['b', true, false, true, () => isFocusedChatEmpty || focusedConversationId && handleConversationBranch(focusedConversationId, null)],
|
||||
['x', true, false, true, () => isFocusedChatEmpty || focusedConversationId && handleConversationClear(focusedConversationId)],
|
||||
['b', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationBranch(focusedConversationId, null))],
|
||||
['x', true, false, true, () => isFocusedChatEmpty || (focusedConversationId && handleConversationClear(focusedConversationId))],
|
||||
['d', true, false, true, () => focusedConversationId && handleConversationDelete(focusedConversationId, false)],
|
||||
[ShortcutKeyName.Left, true, false, true, () => handleNavigateHistory('back')],
|
||||
[ShortcutKeyName.Right, true, false, true, () => handleNavigateHistory('forward')],
|
||||
], [focusedConversationId, handleConversationBranch, handleConversationDelete, handleConversationNew, handleMessageRegenerateLast, handleNavigateHistory, handleOpenChatLlmOptions, isFocusedChatEmpty]);
|
||||
useGlobalShortcuts(shortcuts);
|
||||
|
||||
|
||||
// Pluggable ApplicationBar components
|
||||
|
||||
const centerItems = React.useMemo(() =>
|
||||
<ChatDropdowns conversationId={focusedConversationId} />,
|
||||
[focusedConversationId],
|
||||
<ChatDropdowns
|
||||
conversationId={focusedConversationId}
|
||||
isSplitPanes={isSplitPane}
|
||||
onToggleSplitPanes={toggleSplitPane}
|
||||
/>,
|
||||
[focusedConversationId, isSplitPane, toggleSplitPane],
|
||||
);
|
||||
|
||||
const drawerItems = React.useMemo(() =>
|
||||
<ChatDrawerItemsMemo
|
||||
const drawerContent = React.useMemo(() =>
|
||||
<ChatDrawerContentMemo
|
||||
activeConversationId={focusedConversationId}
|
||||
disableNewButton={isFocusedChatEmpty}
|
||||
onConversationActivate={setFocusedConversationId}
|
||||
onConversationDelete={handleConversationDelete}
|
||||
onConversationExportDialog={handleConversationExport}
|
||||
onConversationImportDialog={handleConversationImportDialog}
|
||||
onConversationNew={handleConversationNew}
|
||||
onConversationsDeleteAll={handleConversationsDeleteAll}
|
||||
selectedFolderId={selectedFolderId}
|
||||
setSelectedFolderId={setSelectedFolderId}
|
||||
/>,
|
||||
[focusedConversationId, handleConversationDelete, handleConversationNew, isFocusedChatEmpty, setFocusedConversationId],
|
||||
[focusedConversationId, handleConversationDelete, handleConversationNew, isFocusedChatEmpty, selectedFolderId, setFocusedConversationId],
|
||||
);
|
||||
|
||||
const menuItems = React.useMemo(() =>
|
||||
@@ -368,77 +417,107 @@ export function AppChat() {
|
||||
[areChatsEmpty, focusedConversationId, handleConversationBranch, isFocusedChatEmpty, isMessageSelectionMode],
|
||||
);
|
||||
|
||||
useLayoutPluggable(centerItems, drawerItems, menuItems);
|
||||
usePluggableOptimaLayout(drawerContent, centerItems, menuItems, 'AppChat');
|
||||
|
||||
return <>
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex', flexDirection: { xs: 'column', md: 'row' },
|
||||
overflow: 'clip',
|
||||
}}>
|
||||
<PanelGroup
|
||||
direction='horizontal'
|
||||
id='app-chat-panels'
|
||||
>
|
||||
|
||||
{chatPaneIDs.map((_conversationId, idx) => (
|
||||
<Box key={'chat-pane-' + idx} onClick={() => setActivePaneIndex(idx)} sx={{
|
||||
flexGrow: 1, flexBasis: 1,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
overflow: 'clip',
|
||||
}}>
|
||||
|
||||
<ChatMessageList
|
||||
conversationId={_conversationId}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImaginePlus}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level1',
|
||||
overflowY: 'auto',
|
||||
minHeight: 96,
|
||||
// outline the current focused pane
|
||||
...(chatPaneIDs.length < 2 ? {}
|
||||
: (_conversationId === focusedConversationId)
|
||||
? {
|
||||
border: '2px solid',
|
||||
borderColor: 'primary.solidBg',
|
||||
} : {
|
||||
padding: '2px',
|
||||
}),
|
||||
{panesConversationIDs.map((_conversationId, idx, panels) =>
|
||||
<React.Fragment key={`chat-pane-${idx}-${panels.length}-${_conversationId}`}>
|
||||
<Panel
|
||||
id={'chat-pane-' + _conversationId}
|
||||
order={idx}
|
||||
collapsible
|
||||
defaultSize={panels.length > 0 ? Math.round(100 / panels.length) : undefined}
|
||||
minSize={20}
|
||||
onClick={(event) => {
|
||||
const setFocus = chatPanes.length < 2 || !event.altKey;
|
||||
setFocusedPane(setFocus ? idx : -1);
|
||||
}}
|
||||
/>
|
||||
onCollapse={() => setTimeout(() => removePane(idx), 50)}
|
||||
style={{
|
||||
// for anchoring the scroll button in place
|
||||
position: 'relative',
|
||||
// border only for active pane (if two or more panes)
|
||||
...(panesConversationIDs.length < 2
|
||||
? {}
|
||||
: (_conversationId === focusedConversationId)
|
||||
? { border: `2px solid ${theme.palette.primary.solidBg}` }
|
||||
: { border: `2px solid ${theme.palette.background.level1}` }),
|
||||
}}
|
||||
>
|
||||
|
||||
<Ephemerals
|
||||
conversationId={_conversationId}
|
||||
sx={{
|
||||
// flexGrow: 0.1,
|
||||
flexShrink: 0.5,
|
||||
overflowY: 'auto',
|
||||
minHeight: 64,
|
||||
}} />
|
||||
<ScrollToBottom
|
||||
bootToBottom
|
||||
stickToBottom
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
<ChatMessageList
|
||||
conversationId={_conversationId}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
sx={{
|
||||
backgroundColor: themeBgApp,
|
||||
minHeight: '100%', // ensures filling of the blank space on newer chats
|
||||
}}
|
||||
/>
|
||||
|
||||
<Ephemerals
|
||||
conversationId={_conversationId}
|
||||
sx={{
|
||||
// TODO: Fixme post panels?
|
||||
// flexGrow: 0.1,
|
||||
flexShrink: 0.5,
|
||||
overflowY: 'auto',
|
||||
minHeight: 64,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Visibility and actions are handled via Context */}
|
||||
<ScrollToBottomButton />
|
||||
</ScrollToBottom>
|
||||
</Panel>
|
||||
|
||||
{/* Panel Separators & Resizers */}
|
||||
{idx < panels.length - 1 && <GoodPanelResizeHandler />}
|
||||
|
||||
</React.Fragment>)}
|
||||
|
||||
</PanelGroup>
|
||||
|
||||
<Composer
|
||||
chatLLM={chatLLM}
|
||||
composerTextAreaRef={composerTextAreaRef}
|
||||
conversationId={focusedConversationId}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
isDeveloperMode={focusedSystemPurposeId === 'Developer'}
|
||||
onAction={handleComposerAction}
|
||||
onTextImagine={handleTextImagine}
|
||||
sx={{
|
||||
zIndex: 21, // position: 'sticky', bottom: 0,
|
||||
backgroundColor: 'background.surface',
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
borderTop: `1px solid`,
|
||||
borderTopColor: 'divider',
|
||||
p: { xs: 1, md: 2 },
|
||||
}} />
|
||||
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Diagrams */}
|
||||
{!!diagramConfig && <DiagramsModal config={diagramConfig} onClose={() => setDiagramConfig(null)} />}
|
||||
@@ -453,25 +532,34 @@ export function AppChat() {
|
||||
)}
|
||||
|
||||
{/* Import / Export */}
|
||||
{!!tradeConfig && <TradeModal config={tradeConfig} onConversationActivate={setFocusedConversationId} onClose={() => setTradeConfig(null)} />}
|
||||
|
||||
{!!tradeConfig && (
|
||||
<TradeModal
|
||||
config={tradeConfig}
|
||||
onConversationActivate={setFocusedConversationId}
|
||||
onClose={() => setTradeConfig(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* [confirmation] Reset Conversation */}
|
||||
{!!clearConversationId && <ConfirmationModal
|
||||
open onClose={() => setClearConversationId(null)} onPositive={handleConfirmedClearConversation}
|
||||
confirmationText={'Are you sure you want to discard all messages?'} positiveActionText={'Clear conversation'}
|
||||
/>}
|
||||
{!!clearConversationId && (
|
||||
<ConfirmationModal
|
||||
open
|
||||
onClose={() => setClearConversationId(null)}
|
||||
onPositive={handleConfirmedClearConversation}
|
||||
confirmationText='Are you sure you want to discard all messages?'
|
||||
positiveActionText='Clear conversation'
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* [confirmation] Delete All */}
|
||||
{!!deleteConversationId && <ConfirmationModal
|
||||
open onClose={() => setDeleteConversationId(null)} onPositive={handleConfirmedDeleteConversation}
|
||||
confirmationText={deleteConversationId === SPECIAL_ID_WIPE_ALL
|
||||
? 'Are you absolutely sure you want to delete ALL conversations? This action cannot be undone.'
|
||||
? `Are you absolutely sure you want to delete ${selectedFolderId ? 'ALL conversations in this folder' : 'ALL conversations'}? This action cannot be undone.`
|
||||
: 'Are you sure you want to delete this conversation?'}
|
||||
positiveActionText={deleteConversationId === SPECIAL_ID_WIPE_ALL
|
||||
? 'Yes, delete all'
|
||||
? `Yes, delete all ${folderConversationsCount} conversations`
|
||||
: 'Delete conversation'}
|
||||
/>}
|
||||
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsAlter: ICommandsProvider = {
|
||||
id: 'chat-alter',
|
||||
rank: 20,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/assistant',
|
||||
alternatives: ['/a'],
|
||||
description: 'Injects assistant response',
|
||||
}, {
|
||||
primary: '/system',
|
||||
alternatives: ['/s'],
|
||||
description: 'Injects system message',
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import LanguageIcon from '@mui/icons-material/Language';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsBrowse: ICommandsProvider = {
|
||||
id: 'ass-browse',
|
||||
rank: 25,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/browse',
|
||||
description: 'Assistant will download the web page',
|
||||
Icon: LanguageIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsDraw: ICommandsProvider = {
|
||||
id: 'ass-t2i',
|
||||
rank: 10,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/draw',
|
||||
alternatives: ['/imagine', '/img'],
|
||||
description: 'Generate an image from text',
|
||||
Icon: FormatPaintIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsHelp: ICommandsProvider = {
|
||||
id: 'cmd-help',
|
||||
rank: 99,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/help',
|
||||
alternatives: ['/?'],
|
||||
noArgs: true,
|
||||
description: 'Display this list of commands',
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsReact: ICommandsProvider = {
|
||||
id: 'ass-react',
|
||||
rank: 15,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/react',
|
||||
description: 'Use the AI ReAct strategy to answer your query (as sidebar)',
|
||||
Icon: PsychologyIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { FunctionComponent } from 'react';
|
||||
import type { CommandsProviderId } from './commands.registry';
|
||||
|
||||
|
||||
export interface ChatCommand {
|
||||
primary: string; // The primary command
|
||||
alternatives?: string[]; // Alternative commands
|
||||
noArgs?: boolean; // Whether the command requires arguments
|
||||
description: string; // Description of what the command does
|
||||
// usage?: string; // Example of how to use the command
|
||||
Icon?: FunctionComponent; // Icon to display next to the command
|
||||
}
|
||||
|
||||
|
||||
export interface ICommandsProvider {
|
||||
id: CommandsProviderId; // Unique identifier for the command provider
|
||||
rank: number; // Rank of the provider, used to sort the providers in the UI
|
||||
|
||||
// Function to get commands with their alternatives and details
|
||||
getCommands: () => ChatCommand[];
|
||||
|
||||
// Function to execute a command with optional parameters
|
||||
// executeCommand: (command: string, params?: string[]) => Promise<boolean>;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { ChatCommand, ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
import { CommandsAlter } from './CommandsAlter';
|
||||
import { CommandsBrowse } from './CommandsBrowse';
|
||||
import { CommandsDraw } from './CommandsDraw';
|
||||
import { CommandsHelp } from './CommandsHelp';
|
||||
import { CommandsReact } from './CommandsReact';
|
||||
|
||||
|
||||
export type CommandsProviderId = 'ass-browse' | 'ass-t2i' | 'ass-react' | 'chat-alter' | 'cmd-help';
|
||||
|
||||
type TextCommandPiece =
|
||||
| { type: 'text'; value: string; }
|
||||
| { type: 'cmd'; providerId: CommandsProviderId, command: string; params?: string, isError?: boolean };
|
||||
|
||||
|
||||
const ChatCommandsProviders: Record<CommandsProviderId, ICommandsProvider> = {
|
||||
'ass-browse': CommandsBrowse,
|
||||
'ass-react': CommandsReact,
|
||||
'ass-t2i': CommandsDraw,
|
||||
'chat-alter': CommandsAlter,
|
||||
'cmd-help': CommandsHelp,
|
||||
};
|
||||
|
||||
export function findAllChatCommands(): ChatCommand[] {
|
||||
return Object.values(ChatCommandsProviders)
|
||||
.sort((a, b) => a.rank - b.rank)
|
||||
.map(p => p.getCommands())
|
||||
.flat();
|
||||
}
|
||||
|
||||
export function extractChatCommand(input: string): TextCommandPiece[] {
|
||||
const inputTrimmed = input.trim();
|
||||
|
||||
// quick exit: command does not start with '/'
|
||||
if (!inputTrimmed.startsWith('/'))
|
||||
return [{ type: 'text', value: input }];
|
||||
|
||||
// Find the first space to separate the command from its parameters (if any)
|
||||
const firstSpaceIndex = inputTrimmed.indexOf(' ');
|
||||
const potentialCommand = inputTrimmed.substring(0, firstSpaceIndex >= 0 ? firstSpaceIndex : inputTrimmed.length);
|
||||
|
||||
// Check if the potential command is an actual command
|
||||
for (const provider of Object.values(ChatCommandsProviders)) {
|
||||
for (const cmd of provider.getCommands()) {
|
||||
if (cmd.primary === potentialCommand || cmd.alternatives?.includes(potentialCommand)) {
|
||||
|
||||
// command needs arguments: take the rest of the input as parameters
|
||||
if (cmd.noArgs !== true) {
|
||||
const params = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
return [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: params || undefined, isError: !params || undefined }];
|
||||
}
|
||||
|
||||
// command without arguments, treat any text after as a separate text piece
|
||||
const pieces: TextCommandPiece[] = [{ type: 'cmd', providerId: provider.id, command: potentialCommand, params: undefined }];
|
||||
const textAfterCommand = firstSpaceIndex >= 0 ? inputTrimmed.substring(firstSpaceIndex + 1) : '';
|
||||
if (textAfterCommand)
|
||||
pieces.push({ type: 'text', value: textAfterCommand });
|
||||
return pieces;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No command found, return the entire input as text
|
||||
return [{ type: 'text', value: input }];
|
||||
}
|
||||
@@ -9,13 +9,14 @@ import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { openLayoutPreferences } from '~/common/layout/store-applayout';
|
||||
import { useCapabilityElevenLabs, useCapabilityProdia } from '~/common/components/useCapabilities';
|
||||
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { ChatMessageMemo } from './message/ChatMessage';
|
||||
import { CleanerMessage, MessagesSelectionHeader } from './message/CleanerMessage';
|
||||
import { PersonaSelector } from './persona-selector/PersonaSelector';
|
||||
import { useChatShowSystemMessages } from '../store-app-chat';
|
||||
import { useScrollToBottom } from './scroll-to-bottom/useScrollToBottom';
|
||||
|
||||
|
||||
/**
|
||||
@@ -23,7 +24,8 @@ import { useChatShowSystemMessages } from '../store-app-chat';
|
||||
*/
|
||||
export function ChatMessageList(props: {
|
||||
conversationId: DConversationId | null,
|
||||
chatLLMContextTokens?: number,
|
||||
capabilityHasT2I: boolean,
|
||||
chatLLMContextTokens: number | null,
|
||||
isMessageSelectionMode: boolean, setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
|
||||
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[]) => void,
|
||||
@@ -39,6 +41,8 @@ export function ChatMessageList(props: {
|
||||
const [selectedMessages, setSelectedMessages] = React.useState<Set<string>>(new Set());
|
||||
|
||||
// external state
|
||||
const { notifyBooting } = useScrollToBottom();
|
||||
const { openPreferencesTab } = useOptimaLayout();
|
||||
const [showSystemMessages] = useChatShowSystemMessages();
|
||||
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
|
||||
@@ -50,11 +54,10 @@ export function ChatMessageList(props: {
|
||||
setMessages: state.setMessages,
|
||||
};
|
||||
}, shallow);
|
||||
const { mayWork: isImaginable } = useCapabilityProdia();
|
||||
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
|
||||
|
||||
// derived state
|
||||
const { conversationId, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
|
||||
const { conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
|
||||
|
||||
|
||||
// text actions
|
||||
@@ -98,22 +101,22 @@ export function ChatMessageList(props: {
|
||||
}, [conversationId, onTextDiagram]);
|
||||
|
||||
const handleTextImagine = React.useCallback(async (text: string) => {
|
||||
if (!isImaginable)
|
||||
return openLayoutPreferences(2);
|
||||
if (!capabilityHasT2I)
|
||||
return openPreferencesTab(2);
|
||||
if (conversationId) {
|
||||
setIsImagining(true);
|
||||
await onTextImagine(conversationId, text);
|
||||
setIsImagining(false);
|
||||
}
|
||||
}, [conversationId, isImaginable, onTextImagine]);
|
||||
}, [capabilityHasT2I, conversationId, onTextImagine, openPreferencesTab]);
|
||||
|
||||
const handleTextSpeak = React.useCallback(async (text: string) => {
|
||||
if (!isSpeakable)
|
||||
return openLayoutPreferences(3);
|
||||
return openPreferencesTab(3);
|
||||
setIsSpeaking(true);
|
||||
await onTextSpeak(text);
|
||||
setIsSpeaking(false);
|
||||
}, [isSpeakable, onTextSpeak]);
|
||||
}, [isSpeakable, onTextSpeak, openPreferencesTab]);
|
||||
|
||||
|
||||
// operate on the local selection set
|
||||
@@ -157,11 +160,19 @@ export function ChatMessageList(props: {
|
||||
return { diffMessage: undefined, diffText: undefined };
|
||||
}, [conversationMessages]);
|
||||
|
||||
|
||||
// scroll to the very bottom of a new chat
|
||||
React.useEffect(() => {
|
||||
if (conversationId)
|
||||
notifyBooting();
|
||||
}, [conversationId, notifyBooting]);
|
||||
|
||||
|
||||
// no content: show the persona selector
|
||||
|
||||
const filteredMessages = conversationMessages
|
||||
.filter(m => m.role !== 'system' || showSystemMessages) // hide the System message if the user choses to
|
||||
.reverse(); // 'reverse' is because flexDirection: 'column-reverse' to auto-snap-to-bottom
|
||||
.filter(m => m.role !== 'system' || showSystemMessages); // hide the System message if the user choses to
|
||||
|
||||
|
||||
if (!filteredMessages.length)
|
||||
return (
|
||||
@@ -176,18 +187,29 @@ export function ChatMessageList(props: {
|
||||
<List sx={{
|
||||
p: 0, ...(props.sx || {}),
|
||||
// this makes sure that the the window is scrolled to the bottom (column-reverse)
|
||||
display: 'flex', flexDirection: 'column-reverse',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// fix for the double-border on the last message (one by the composer, one to the bottom of the message)
|
||||
// marginBottom: '-1px',
|
||||
}}>
|
||||
|
||||
{filteredMessages.map((message, idx) =>
|
||||
{props.isMessageSelectionMode && (
|
||||
<MessagesSelectionHeader
|
||||
hasSelected={selectedMessages.size > 0}
|
||||
sumTokens={historyTokenCount}
|
||||
onClose={() => props.setIsMessageSelectionMode(false)}
|
||||
onSelectAll={handleSelectAll}
|
||||
onDeleteMessages={handleSelectionDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filteredMessages.map((message, idx, { length: count }) =>
|
||||
props.isMessageSelectionMode ? (
|
||||
|
||||
<CleanerMessage
|
||||
key={'sel-' + message.id}
|
||||
message={message}
|
||||
isBottom={idx === 0} remainingTokens={(props.chatLLMContextTokens || 0) - historyTokenCount}
|
||||
remainingTokens={props.chatLLMContextTokens ? (props.chatLLMContextTokens - historyTokenCount) : undefined}
|
||||
selected={selectedMessages.has(message.id)} onToggleSelected={handleSelectMessage}
|
||||
/>
|
||||
|
||||
@@ -197,7 +219,7 @@ export function ChatMessageList(props: {
|
||||
key={'msg-' + message.id}
|
||||
message={message}
|
||||
diffPreviousText={message === diffMessage ? diffText : undefined}
|
||||
isBottom={idx === 0}
|
||||
isBottom={idx === count - 1}
|
||||
isImagining={isImagining} isSpeaking={isSpeaking}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationRestartFrom={handleConversationRestartFrom}
|
||||
@@ -212,18 +234,6 @@ export function ChatMessageList(props: {
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Header at the bottom because of 'row-reverse' */}
|
||||
{props.isMessageSelectionMode && (
|
||||
<MessagesSelectionHeader
|
||||
hasSelected={selectedMessages.size > 0}
|
||||
isBottom={filteredMessages.length === 0}
|
||||
sumTokens={historyTokenCount}
|
||||
onClose={() => props.setIsMessageSelectionMode(false)}
|
||||
onSelectAll={handleSelectAll}
|
||||
onDeleteMessages={handleSelectionDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
</List>
|
||||
);
|
||||
}
|
||||
@@ -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,61 +1,117 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, ListDivider, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
|
||||
import { Box, IconButton, ListDivider, ListItemDecorator, MenuItem, Tooltip } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import FolderOpenOutlinedIcon from '@mui/icons-material/FolderOpenOutlined';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
|
||||
import { closeLayoutDrawer } from '~/common/layout/store-applayout';
|
||||
import { DFolder, useFoldersToggle, useFolderStore } from '~/common/state/store-folders';
|
||||
import { PageDrawerHeader } from '~/common/layout/optima/components/PageDrawerHeader';
|
||||
import { PageDrawerList, PageDrawerTallItemSx } from '~/common/layout/optima/components/PageDrawerList';
|
||||
import { conversationTitle, DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { ChatNavigationItemMemo } from './ChatNavigationItem';
|
||||
|
||||
import { ChatFolderList } from './folder/ChatFolderList';
|
||||
import { ChatDrawerItemMemo, ChatNavigationItemData } from './ChatNavigationItem';
|
||||
|
||||
// type ListGrouping = 'off' | 'persona';
|
||||
|
||||
export const ChatDrawerItemsMemo = React.memo(ChatDrawerItems);
|
||||
|
||||
/*
|
||||
* Optimization: return a reduced version of the DConversation object for 'Drawer Items' purposes,
|
||||
* to avoid unnecessary re-renders on each new character typed by the assistant
|
||||
*/
|
||||
export const useChatNavigationItems = (activeConversationId: DConversationId | null, folderId: string | null): {
|
||||
chatNavItems: ChatNavigationItemData[],
|
||||
folders: DFolder[],
|
||||
} => {
|
||||
|
||||
// monitor folder changes
|
||||
// NOTE: we're not checking for state.useFolders, as we strongly assume folderId to be null when folders are disabled
|
||||
const { currentFolder, folders } = useFolderStore(state => {
|
||||
const currentFolder = folderId ? state.folders.find(_f => _f.id === folderId) ?? null : null;
|
||||
return {
|
||||
folders: state.folders,
|
||||
currentFolder,
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
// transform (folder) selected conversation into optimized 'navigation item' data
|
||||
const chatNavItems: ChatNavigationItemData[] = useChatStore(state => {
|
||||
|
||||
const selectConversations = currentFolder
|
||||
? state.conversations.filter(_c => currentFolder.conversationIds.includes(_c.id))
|
||||
: state.conversations;
|
||||
|
||||
return selectConversations.map(_c => ({
|
||||
conversationId: _c.id,
|
||||
isActive: _c.id === activeConversationId,
|
||||
isEmpty: !_c.messages.length && !_c.userTitle,
|
||||
title: conversationTitle(_c, 'New Title'),
|
||||
messageCount: _c.messages.length,
|
||||
assistantTyping: !!_c.abortController,
|
||||
systemPurposeId: _c.systemPurposeId,
|
||||
}));
|
||||
|
||||
}, (a: ChatNavigationItemData[], b: ChatNavigationItemData[]) => {
|
||||
// custom equality function to avoid unnecessary re-renders
|
||||
return a.length === b.length && a.every((_a, i) => shallow(_a, b[i]));
|
||||
});
|
||||
|
||||
return { chatNavItems, folders };
|
||||
};
|
||||
|
||||
|
||||
export const ChatDrawerContentMemo = React.memo(ChatDrawerItems);
|
||||
|
||||
function ChatDrawerItems(props: {
|
||||
activeConversationId: DConversationId | null,
|
||||
disableNewButton: boolean,
|
||||
onConversationActivate: (conversationId: DConversationId) => void,
|
||||
onConversationDelete: (conversationId: DConversationId, bypassConfirmation: boolean) => void,
|
||||
onConversationExportDialog: (conversationId: DConversationId | null) => void,
|
||||
onConversationImportDialog: () => void,
|
||||
onConversationNew: () => void,
|
||||
onConversationsDeleteAll: () => void,
|
||||
selectedFolderId: string | null,
|
||||
setSelectedFolderId: (folderId: string | null) => void,
|
||||
}) {
|
||||
|
||||
// local state
|
||||
const { onConversationDelete, onConversationNew, onConversationActivate } = props;
|
||||
// const [grouping] = React.useState<ListGrouping>('off');
|
||||
const { onConversationDelete, onConversationNew, onConversationActivate } = props;
|
||||
|
||||
// external state
|
||||
const conversations = useChatStore(state => state.conversations, shallow);
|
||||
const { closeDrawer, closeDrawerOnMobile } = useOptimaDrawers();
|
||||
const { useFolders, toggleUseFolders } = useFoldersToggle();
|
||||
const { chatNavItems, folders } = useChatNavigationItems(props.activeConversationId, props.selectedFolderId);
|
||||
const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner');
|
||||
const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI);
|
||||
|
||||
// derived state
|
||||
const maxChatMessages = conversations.reduce((longest, _c) => Math.max(longest, _c.messages.length), 1);
|
||||
const totalConversations = conversations.length;
|
||||
const hasChats = totalConversations > 0;
|
||||
const singleChat = totalConversations === 1;
|
||||
const softMaxReached = totalConversations >= 50;
|
||||
const maxChatMessages = chatNavItems.reduce((longest, _c) => Math.max(longest, _c.messageCount), 1);
|
||||
const selectConversationsCount = chatNavItems.length;
|
||||
const nonEmptyChats = selectConversationsCount > 1 || (selectConversationsCount === 1 && !chatNavItems[0].isEmpty);
|
||||
const singleChat = selectConversationsCount === 1;
|
||||
const softMaxReached = selectConversationsCount >= 50;
|
||||
|
||||
|
||||
const handleButtonNew = React.useCallback(() => {
|
||||
onConversationNew();
|
||||
closeLayoutDrawer();
|
||||
}, [onConversationNew]);
|
||||
closeDrawerOnMobile();
|
||||
}, [closeDrawerOnMobile, onConversationNew]);
|
||||
|
||||
const handleConversationActivate = React.useCallback((conversationId: DConversationId, closeMenu: boolean) => {
|
||||
onConversationActivate(conversationId);
|
||||
if (closeMenu)
|
||||
closeLayoutDrawer();
|
||||
}, [onConversationActivate]);
|
||||
closeDrawerOnMobile();
|
||||
}, [closeDrawerOnMobile, onConversationActivate]);
|
||||
|
||||
const handleConversationDelete = React.useCallback((conversationId: DConversationId) => {
|
||||
!singleChat && conversationId && onConversationDelete(conversationId, true);
|
||||
@@ -84,66 +140,105 @@ function ChatDrawerItems(props: {
|
||||
|
||||
return <>
|
||||
|
||||
{/*<ListItem>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Active chats*/}
|
||||
{/* </Typography>*/}
|
||||
{/*</ListItem>*/}
|
||||
{/* Drawer Header */}
|
||||
<PageDrawerHeader
|
||||
title='Chats'
|
||||
onClose={closeDrawer}
|
||||
startButton={
|
||||
<Tooltip title={useFolders ? 'Hide Folders' : 'Use Folders'}>
|
||||
<IconButton onClick={toggleUseFolders}>
|
||||
{useFolders ? <FolderOpenOutlinedIcon /> : <FolderOutlinedIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<MenuItem disabled={props.disableNewButton} onClick={handleButtonNew}>
|
||||
<ListItemDecorator><AddIcon /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
New
|
||||
{/*<KeyStroke combo='Ctrl + Alt + N' />*/}
|
||||
{/* Folders List */}
|
||||
{useFolders && (
|
||||
<ChatFolderList
|
||||
folders={folders}
|
||||
selectedFolderId={props.selectedFolderId}
|
||||
onFolderSelect={props.setSelectedFolderId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Chats List */}
|
||||
<PageDrawerList variant='plain' noTopPadding noBottomPadding tallRows>
|
||||
|
||||
{useFolders && <ListDivider sx={{ mb: 0 }} />}
|
||||
|
||||
<MenuItem disabled={props.disableNewButton} onClick={handleButtonNew} sx={PageDrawerTallItemSx}>
|
||||
<ListItemDecorator><AddIcon /></ListItemDecorator>
|
||||
<Box sx={{
|
||||
// style
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'lg',
|
||||
// content
|
||||
flexGrow: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 1,
|
||||
}}>
|
||||
New chat
|
||||
{/*<KeyStroke combo='Ctrl + Alt + N' sx={props.disableNewButton ? { opacity: 0.5 } : undefined} />*/}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
{/*<ListDivider sx={{ mt: 0 }} />*/}
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
{/*<ListItem sticky sx={{ justifyContent: 'space-between', boxShadow: 'sm' }}>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Conversations*/}
|
||||
{/* </Typography>*/}
|
||||
{/* <ToggleButtonGroup variant='soft' size='sm' value={grouping} onChange={(_event, newValue) => newValue && setGrouping(newValue)}>*/}
|
||||
{/* <IconButton value='off'>*/}
|
||||
{/* <AccessTimeIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* <IconButton value='persona'>*/}
|
||||
{/* <PersonIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </ToggleButtonGroup>*/}
|
||||
{/*</ListItem>*/}
|
||||
|
||||
{chatNavItems.map(item =>
|
||||
<ChatDrawerItemMemo
|
||||
key={'nav-' + item.conversationId}
|
||||
item={item}
|
||||
isLonely={singleChat}
|
||||
maxChatMessages={(labsEnhancedUI || softMaxReached) ? maxChatMessages : 0}
|
||||
showSymbols={showSymbols}
|
||||
onConversationActivate={handleConversationActivate}
|
||||
onConversationDelete={handleConversationDelete}
|
||||
/>)}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider sx={{ mb: 0 }} />
|
||||
<ListDivider sx={{ mt: 0 }} />
|
||||
|
||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||
{/*<ListItem sticky sx={{ justifyContent: 'space-between', boxShadow: 'sm' }}>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Conversations*/}
|
||||
{/* </Typography>*/}
|
||||
{/* <ToggleButtonGroup variant='soft' size='sm' value={grouping} onChange={(_event, newValue) => newValue && setGrouping(newValue)}>*/}
|
||||
{/* <IconButton value='off'>*/}
|
||||
{/* <AccessTimeIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* <IconButton value='persona'>*/}
|
||||
{/* <PersonIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </ToggleButtonGroup>*/}
|
||||
{/*</ListItem>*/}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
|
||||
{conversations.map(conversation =>
|
||||
<ChatNavigationItemMemo
|
||||
key={'nav-' + conversation.id}
|
||||
conversation={conversation}
|
||||
isActive={conversation.id === props.activeConversationId}
|
||||
isLonely={singleChat}
|
||||
maxChatMessages={(labsEnhancedUI || softMaxReached) ? maxChatMessages : 0}
|
||||
showSymbols={showSymbols}
|
||||
onConversationActivate={handleConversationActivate}
|
||||
onConversationDelete={handleConversationDelete}
|
||||
/>)}
|
||||
</Box>
|
||||
<MenuItem onClick={props.onConversationImportDialog} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator>
|
||||
<FileUploadIcon />
|
||||
</ListItemDecorator>
|
||||
Import
|
||||
{/*<OpenAIIcon sx={{ ml: 'auto' }} />*/}
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider sx={{ mt: 0 }} />
|
||||
<MenuItem disabled={!nonEmptyChats} onClick={() => props.onConversationExportDialog(props.activeConversationId)} sx={{ flex: 1, display: 'flex', justifyContent: 'flex-end', gap: 2.5 }}>
|
||||
Export
|
||||
<FileDownloadIcon />
|
||||
</MenuItem>
|
||||
</Box>
|
||||
|
||||
<MenuItem onClick={props.onConversationImportDialog}>
|
||||
<ListItemDecorator>
|
||||
<FileUploadIcon />
|
||||
</ListItemDecorator>
|
||||
Import chats
|
||||
<OpenAIIcon sx={{ fontSize: 'xl', ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
<MenuItem disabled={!nonEmptyChats} onClick={props.onConversationsDeleteAll}>
|
||||
<ListItemDecorator>
|
||||
<DeleteOutlineIcon />
|
||||
</ListItemDecorator>
|
||||
Delete {selectConversationsCount >= 2 ? `all ${selectConversationsCount} chats` : 'chat'}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem disabled={!hasChats} onClick={props.onConversationsDeleteAll}>
|
||||
<ListItemDecorator><DeleteOutlineIcon /></ListItemDecorator>
|
||||
<Typography>
|
||||
Delete {totalConversations >= 2 ? `all ${totalConversations} chats` : 'chat'}
|
||||
</Typography>
|
||||
</MenuItem>
|
||||
</PageDrawerList>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -1,26 +1,51 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { IconButton } from '@mui/joy';
|
||||
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { useChatLLMDropdown } from './useLLMDropdown';
|
||||
import { usePersonaIdDropdown } from './usePersonaDropdown';
|
||||
import { useFolderDropdown } from './folder/useFolderDropdown';
|
||||
|
||||
|
||||
export function ChatDropdowns(props: {
|
||||
conversationId: DConversationId | null
|
||||
isSplitPanes: boolean
|
||||
onToggleSplitPanes: () => void
|
||||
}) {
|
||||
|
||||
// state
|
||||
const { chatLLMDropdown } = useChatLLMDropdown();
|
||||
const { personaDropdown } = usePersonaIdDropdown(props.conversationId);
|
||||
const { folderDropdown } = useFolderDropdown(props.conversationId);
|
||||
|
||||
// external state
|
||||
const labsSplitBranching = useUXLabsStore(state => state.labsSplitBranching);
|
||||
|
||||
return <>
|
||||
|
||||
{/* Model selector */}
|
||||
{chatLLMDropdown}
|
||||
|
||||
{/* Persona selector */}
|
||||
{personaDropdown}
|
||||
|
||||
{/* Model selector */}
|
||||
{chatLLMDropdown}
|
||||
|
||||
{/* Folder selector */}
|
||||
{folderDropdown}
|
||||
|
||||
{/* Split Panes button */}
|
||||
{labsSplitBranching && <IconButton
|
||||
variant={props.isSplitPanes ? 'solid' : undefined}
|
||||
onClick={props.onToggleSplitPanes}
|
||||
// sx={{
|
||||
// ml: 'auto',
|
||||
// }}
|
||||
>
|
||||
<VerticalSplitIcon />
|
||||
</IconButton>}
|
||||
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { closeLayoutMenu } from '~/common/layout/store-applayout';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
import { useUICounter } from '~/common/state/store-ui';
|
||||
|
||||
import { useChatShowSystemMessages } from '../../store-app-chat';
|
||||
@@ -30,6 +30,7 @@ export function ChatMenuItems(props: {
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { closePageMenu } = useOptimaDrawers();
|
||||
const { touch: shareTouch } = useUICounter('export-share');
|
||||
const [showSystemMessages, setShowSystemMessages] = useChatShowSystemMessages();
|
||||
|
||||
@@ -39,7 +40,7 @@ export function ChatMenuItems(props: {
|
||||
|
||||
const closeMenu = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
closeLayoutMenu();
|
||||
closePageMenu();
|
||||
};
|
||||
|
||||
const handleConversationClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
@@ -116,7 +117,7 @@ export function ChatMenuItems(props: {
|
||||
<MenuItem disabled={disabled} onClick={handleConversationClear}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Reset
|
||||
Reset Chat
|
||||
{!disabled && <KeyStroke combo='Ctrl + Alt + X' />}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Avatar, Box, IconButton, ListItemDecorator, MenuItem, Typography } from '@mui/joy';
|
||||
import { Avatar, Box, IconButton, ListItemButton, ListItemDecorator, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
|
||||
import { SystemPurposes } from '../../../../data';
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { conversationTitle, DConversation, DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
const DEBUG_CONVERSATION_IDs = false;
|
||||
|
||||
|
||||
export const ChatNavigationItemMemo = React.memo(ChatNavigationItem);
|
||||
export const ChatDrawerItemMemo = React.memo(ChatNavigationItem);
|
||||
|
||||
export interface ChatNavigationItemData {
|
||||
conversationId: DConversationId;
|
||||
isActive: boolean;
|
||||
isEmpty: boolean;
|
||||
title: string;
|
||||
messageCount: number;
|
||||
assistantTyping: boolean;
|
||||
systemPurposeId: SystemPurposeId;
|
||||
}
|
||||
|
||||
function ChatNavigationItem(props: {
|
||||
conversation: DConversation,
|
||||
isActive: boolean,
|
||||
item: ChatNavigationItemData,
|
||||
isLonely: boolean,
|
||||
maxChatMessages: number,
|
||||
showSymbols: boolean,
|
||||
@@ -27,8 +36,6 @@ function ChatNavigationItem(props: {
|
||||
onConversationDelete: (conversationId: DConversationId) => void,
|
||||
}) {
|
||||
|
||||
const { conversation, isActive } = props;
|
||||
|
||||
// state
|
||||
const [isEditingTitle, setIsEditingTitle] = React.useState(false);
|
||||
const [deleteArmed, setDeleteArmed] = React.useState(false);
|
||||
@@ -37,13 +44,8 @@ function ChatNavigationItem(props: {
|
||||
const doubleClickToEdit = useUIPreferencesStore(state => state.doubleClickToEdit);
|
||||
|
||||
// derived state
|
||||
const { id: conversationId } = conversation;
|
||||
const isNew = conversation.messages.length === 0;
|
||||
const messageCount = conversation.messages.length;
|
||||
const assistantTyping = !!conversation.abortController;
|
||||
const systemPurposeId = conversation.systemPurposeId;
|
||||
const title = conversationTitle(conversation, 'new conversation');
|
||||
// const setUserTitle = state.setUserTitle;
|
||||
const { conversationId, isActive, title, messageCount, assistantTyping, systemPurposeId } = props.item;
|
||||
const isNew = messageCount === 0;
|
||||
|
||||
// auto-close the arming menu when clicking away
|
||||
// NOTE: there currently is a bug (race condition) where the menu closes on a new item right after opening
|
||||
@@ -88,16 +90,15 @@ function ChatNavigationItem(props: {
|
||||
const progress = props.maxChatMessages ? 100 * messageCount / props.maxChatMessages : 0;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
variant={isActive ? 'solid' : 'plain'} color='neutral'
|
||||
selected={isActive}
|
||||
<ListItemButton
|
||||
variant={isActive ? 'soft' : 'plain'} color='neutral'
|
||||
onClick={handleConversationActivate}
|
||||
sx={{
|
||||
// py: 0,
|
||||
position: 'relative',
|
||||
border: 'none', // note, there's a default border of 1px and invisible.. hmm
|
||||
cursor: 'pointer',
|
||||
'&:hover > button': { opacity: 1 },
|
||||
...(isActive ? { bgcolor: 'red' } : {}),
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -145,7 +146,6 @@ function ChatNavigationItem(props: {
|
||||
{/* // TODO: Commented code */}
|
||||
{/* Edit */}
|
||||
{/*<IconButton*/}
|
||||
{/* variant='plain' color='neutral'*/}
|
||||
{/* onClick={() => props.onEditTitle(props.conversationId)}*/}
|
||||
{/* sx={{*/}
|
||||
{/* opacity: 0, transition: 'opacity 0.3s', ml: 'auto',*/}
|
||||
@@ -156,9 +156,11 @@ function ChatNavigationItem(props: {
|
||||
{/* Delete Arming */}
|
||||
{!props.isLonely && !deleteArmed && (
|
||||
<IconButton
|
||||
variant={isActive ? 'solid' : 'outlined'} color='neutral'
|
||||
size='sm' sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.3s', ...buttonSx }}
|
||||
onClick={handleDeleteButtonShow}>
|
||||
variant={isActive ? 'solid' : 'outlined'}
|
||||
size='sm'
|
||||
sx={{ opacity: { xs: 1, sm: 0 }, transition: 'opacity 0.2s', ...buttonSx }}
|
||||
onClick={handleDeleteButtonShow}
|
||||
>
|
||||
<DeleteOutlineIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -172,7 +174,7 @@ function ChatNavigationItem(props: {
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>}
|
||||
</MenuItem>
|
||||
|
||||
</ListItemButton>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, ListItem, ListItemDecorator } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { getRotatingFolderColor, useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
|
||||
export function AddFolderButton() {
|
||||
|
||||
// state
|
||||
const [isAddingFolder, setIsAddingFolder] = React.useState(false);
|
||||
const [newFolderColor, setNewFolderColor] = React.useState<string | null>(null);
|
||||
|
||||
|
||||
const handleAddFolder = () => {
|
||||
setNewFolderColor(getRotatingFolderColor());
|
||||
setIsAddingFolder(true);
|
||||
};
|
||||
|
||||
const handleCreateFolder = (name: string) => {
|
||||
if (name.trim())
|
||||
useFolderStore.getState().createFolder(name.trim(), newFolderColor || undefined);
|
||||
setIsAddingFolder(false);
|
||||
};
|
||||
|
||||
const handleCancelAddFolder = () => {
|
||||
setIsAddingFolder(false);
|
||||
};
|
||||
|
||||
return isAddingFolder ? (
|
||||
<ListItem sx={{
|
||||
'--ListItem-paddingLeft': '0.75rem',
|
||||
'--ListItem-minHeight': '3rem', // --Folder-ListItem-height
|
||||
display: 'flex', alignItems: 'center', gap: 1,
|
||||
}}>
|
||||
<ListItemDecorator>
|
||||
<FolderIcon style={{ color: newFolderColor || 'inherit' }} />
|
||||
</ListItemDecorator>
|
||||
<InlineTextarea
|
||||
initialText='' placeholder='Folder Name'
|
||||
onEdit={handleCreateFolder}
|
||||
onCancel={handleCancelAddFolder}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
}} />
|
||||
{/*<IconButton color='danger' onClick={handleCancelAddFolder}>*/}
|
||||
{/* <CloseIcon />*/}
|
||||
{/*</IconButton>*/}
|
||||
</ListItem>
|
||||
) : (
|
||||
<Button
|
||||
color='neutral'
|
||||
variant='plain'
|
||||
startDecorator={<AddIcon />}
|
||||
onClick={handleAddFolder}
|
||||
sx={{
|
||||
// display: 'flex', alignItems: 'center', justifyContent: 'flex-start',
|
||||
// minHeight: '3rem', // --Folder-ListItem-height
|
||||
// match the forder elements
|
||||
paddingInline: '1.2rem',
|
||||
gap: '0.75rem',
|
||||
// fontWeight: 400,
|
||||
}}
|
||||
>
|
||||
New folder
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import * as React from 'react';
|
||||
import { DragDropContext, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import { List, ListItem, ListItemButton, ListItemContent, ListItemDecorator, MenuList, Sheet, Typography } from '@mui/joy';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import { DFolder, useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
import { AddFolderButton } from './AddFolderButton';
|
||||
import { FolderListItem } from './FolderListItem';
|
||||
import { StrictModeDroppable } from './StrictModeDroppable';
|
||||
|
||||
|
||||
export function ChatFolderList(props: {
|
||||
folders: DFolder[];
|
||||
onFolderSelect: (folderId: string | null) => void;
|
||||
selectedFolderId: string | null;
|
||||
}) {
|
||||
|
||||
// derived props
|
||||
const { folders, onFolderSelect, selectedFolderId } = props;
|
||||
|
||||
// handlers
|
||||
|
||||
const onDragEnd = (result: DropResult) => {
|
||||
if (!result.destination) return;
|
||||
useFolderStore.getState().moveFolder(result.source.index, result.destination.index);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Sheet variant='soft' sx={{ p: 2 }}>
|
||||
<MenuList
|
||||
variant='plain'
|
||||
sx={(theme) => ({
|
||||
'& ul': {
|
||||
'--List-gap': '0px',
|
||||
bgcolor: 'background.surface',
|
||||
'& > li:first-of-type > [role="button"]': {
|
||||
borderTopRightRadius: 'var(--List-radius)',
|
||||
borderTopLeftRadius: 'var(--List-radius)',
|
||||
},
|
||||
'& > li:last-child > [role="button"]': {
|
||||
borderBottomRightRadius: 'var(--List-radius)',
|
||||
borderBottomLeftRadius: 'var(--List-radius)',
|
||||
},
|
||||
},
|
||||
// copied from the former PageDrawerList as this was contained
|
||||
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
|
||||
'--ListItemDecorator-size': '2.75rem',
|
||||
'--ListItem-minHeight': '3rem', // --Folder-ListItem-height
|
||||
|
||||
'--List-radius': '8px',
|
||||
'--List-gap': '1rem',
|
||||
'--ListDivider-gap': '0px',
|
||||
// '--ListItem-paddingY': '0.5rem',
|
||||
// override global variant tokens
|
||||
'--joy-palette-neutral-plainHoverBg': 'rgba(0 0 0 / 0.08)',
|
||||
'--joy-palette-neutral-plainActiveBg': 'rgba(0 0 0 / 0.12)',
|
||||
[theme.getColorSchemeSelector('light')]: {
|
||||
'--joy-palette-divider': 'rgba(0 0 0 / 0.08)',
|
||||
},
|
||||
[theme.getColorSchemeSelector('dark')]: {
|
||||
'--joy-palette-neutral-plainHoverBg': 'rgba(255 255 255 / 0.1)',
|
||||
'--joy-palette-neutral-plainActiveBg': 'rgba(255 255 255 / 0.16)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<ListItem nested>
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<StrictModeDroppable
|
||||
droppableId='folder'
|
||||
renderClone={(provided, snapshot, rubric) => (
|
||||
<FolderListItem
|
||||
folder={folders[rubric.source.index]}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
onFolderSelect={onFolderSelect}
|
||||
selectedFolderId={selectedFolderId}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
{(provided) => (
|
||||
<List ref={provided.innerRef} {...provided.droppableProps}>
|
||||
|
||||
{/* First item is the 'All' button */}
|
||||
<ListItem>
|
||||
<ListItemButton
|
||||
// handle folder select
|
||||
onClick={(event) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
onFolderSelect(null);
|
||||
}}
|
||||
selected={selectedFolderId === null}
|
||||
sx={{
|
||||
border: 0,
|
||||
justifyContent: 'space-between',
|
||||
'&:hover .menu-icon': {
|
||||
visibility: 'visible', // Hide delete icon for default folder
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<FolderIcon />
|
||||
</ListItemDecorator>
|
||||
|
||||
<ListItemContent>
|
||||
<Typography>All</Typography>
|
||||
</ListItemContent>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
|
||||
{folders.map((folder, index) => (
|
||||
<Draggable key={folder.id} draggableId={folder.id} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<FolderListItem
|
||||
folder={folder}
|
||||
provided={provided}
|
||||
snapshot={snapshot}
|
||||
onFolderSelect={onFolderSelect}
|
||||
selectedFolderId={selectedFolderId}
|
||||
/>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</List>
|
||||
)}
|
||||
</StrictModeDroppable>
|
||||
</DragDropContext>
|
||||
</ListItem>
|
||||
</MenuList>
|
||||
|
||||
<AddFolderButton />
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
import React, { useState } from 'react';
|
||||
import { DraggableProvided, DraggableStateSnapshot, DraggingStyle, NotDraggingStyle } from 'react-beautiful-dnd';
|
||||
|
||||
import { FormLabel, IconButton, ListItem, ListItemButton, ListItemContent, ListItemDecorator, MenuItem, Radio, radioClasses, RadioGroup, Sheet, Typography } from '@mui/joy';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import Done from '@mui/icons-material/Done';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { DFolder, FOLDERS_COLOR_PALETTE, useFolderStore } from '~/common/state/store-folders';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
|
||||
|
||||
// Define the type for your props if you're using TypeScript
|
||||
type RenderItemProps = {
|
||||
folder: DFolder;
|
||||
provided: DraggableProvided;
|
||||
snapshot: DraggableStateSnapshot;
|
||||
onFolderSelect: (folderId: string | null) => void;
|
||||
selectedFolderId: string | null;
|
||||
// Include any other props that RenderItem needs
|
||||
};
|
||||
|
||||
export const FolderListItem: React.FC<RenderItemProps> = ({ folder, provided, snapshot, onFolderSelect, selectedFolderId }) => {
|
||||
|
||||
// internal state
|
||||
const [deleteArmed, setDeleteArmed] = useState(false);
|
||||
const [editingFolderId, setEditingFolderId] = useState<string | null>(null);
|
||||
|
||||
// State to control the open state of the Menu
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLAnchorElement>(null);
|
||||
|
||||
|
||||
// Menu
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
setDeleteArmed(false); // Reset delete armed state
|
||||
};
|
||||
|
||||
const handleMenuClose = () => {
|
||||
setMenuAnchorEl(null);
|
||||
};
|
||||
|
||||
|
||||
// Edit Title
|
||||
|
||||
const handleEditTitle = (event: React.MouseEvent<HTMLElement, MouseEvent>, folderId: string) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
setEditingFolderId(folderId);
|
||||
};
|
||||
|
||||
const handleCancelEditTitle = () => {
|
||||
setEditingFolderId(null);
|
||||
};
|
||||
|
||||
const handleSetTitle = (newTitle: string, folderId: string) => {
|
||||
if (newTitle.trim())
|
||||
useFolderStore.getState().setFolderName(folderId, newTitle.trim());
|
||||
setEditingFolderId(null); // Exit edit mode
|
||||
// Blur the input element if it's currently focused
|
||||
if (document.activeElement instanceof HTMLElement) {
|
||||
document.activeElement.blur();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Deletion
|
||||
|
||||
const handleDeleteButtonShow = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setDeleteArmed(true);
|
||||
};
|
||||
|
||||
const handleDeleteConfirmed = (event: React.MouseEvent) => {
|
||||
if (deleteArmed) {
|
||||
setDeleteArmed(false);
|
||||
event.stopPropagation();
|
||||
useFolderStore.getState().deleteFolder(folder.id);
|
||||
handleMenuClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteCanceled = (event: React.MouseEvent) => {
|
||||
if (deleteArmed) {
|
||||
setDeleteArmed(false);
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Color
|
||||
|
||||
const handleColorChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
useFolderStore.getState().setFolderColor(folder.id, event.target.value);
|
||||
handleMenuClose();
|
||||
};
|
||||
|
||||
|
||||
const getItemStyle = (isDragging: boolean, draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
userSelect: 'none',
|
||||
borderRadius: '8px',
|
||||
backgroundColor: isDragging ? 'rgba(0, 80, 80, 0.18)' : 'transparent',
|
||||
|
||||
...draggableStyle,
|
||||
|
||||
// Any additional styles you want to apply during dragging
|
||||
...(isDragging &&
|
||||
{
|
||||
// Apply any drag-specific styles here
|
||||
// marginLeft: '12px',
|
||||
}),
|
||||
});
|
||||
|
||||
const getListItemContentStyle = (isDragging: boolean, _draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
...(isDragging && {
|
||||
// Apply any drag-specific styles here
|
||||
marginLeft: '20px',
|
||||
}),
|
||||
});
|
||||
|
||||
const getListItemDecoratorStyle = (isDragging: boolean, _draggableStyle: DraggingStyle | NotDraggingStyle | undefined) => ({
|
||||
...(isDragging && {
|
||||
// Apply any drag-specific styles here
|
||||
marginLeft: '12px',
|
||||
}),
|
||||
});
|
||||
|
||||
const handleFolderSelect = (folderId: string | null) => {
|
||||
onFolderSelect(folderId);
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
style={{
|
||||
...getItemStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<ListItemButton
|
||||
// handle folder select
|
||||
onClick={(event) => {
|
||||
event.stopPropagation(); // Prevent the ListItemButton's onClick from firing
|
||||
handleFolderSelect(folder.id);
|
||||
}}
|
||||
selected={folder.id === selectedFolderId}
|
||||
sx={{
|
||||
border: 0,
|
||||
justifyContent: 'space-between',
|
||||
'&:hover .menu-icon': {
|
||||
visibility: 'visible', // Hide delete icon for default folder
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator
|
||||
style={{
|
||||
...getListItemDecoratorStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<FolderIcon style={{ color: folder.color || 'inherit' }} />
|
||||
</ListItemDecorator>
|
||||
|
||||
{editingFolderId === folder.id ? (
|
||||
<InlineTextarea
|
||||
initialText={folder.title}
|
||||
onEdit={newTitle => handleSetTitle(newTitle, folder.id)}
|
||||
onCancel={handleCancelEditTitle}
|
||||
sx={{ ml: -1.5, mr: -0.5, flexGrow: 1 }}
|
||||
/>
|
||||
) : (
|
||||
<ListItemContent
|
||||
onDoubleClick={event => handleEditTitle(event, folder.id)}
|
||||
style={{
|
||||
...getListItemContentStyle(snapshot.isDragging, provided.draggableProps.style),
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<Typography>{folder.title}</Typography>
|
||||
</ListItemContent>
|
||||
)}
|
||||
|
||||
{/* Icon to show the Popup menu */}
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
className='menu-icon'
|
||||
onClick={handleMenuOpen}
|
||||
sx={{
|
||||
visibility: 'hidden',
|
||||
}}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
|
||||
{!!menuAnchorEl && (
|
||||
<CloseableMenu
|
||||
open anchorEl={menuAnchorEl} onClose={handleMenuClose}
|
||||
placement='top' zIndex={1301 /* need to be on top of the Modal on Mobile */}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
handleEditTitle(event, folder.id);
|
||||
handleMenuClose();
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<EditIcon />
|
||||
</ListItemDecorator>
|
||||
Edit
|
||||
</MenuItem>
|
||||
|
||||
{!deleteArmed ? (
|
||||
<MenuItem onClick={handleDeleteButtonShow}>
|
||||
<ListItemDecorator>
|
||||
<DeleteOutlineIcon />
|
||||
</ListItemDecorator>
|
||||
Delete
|
||||
</MenuItem>
|
||||
) : (
|
||||
<>
|
||||
<MenuItem onClick={handleDeleteCanceled}>
|
||||
<ListItemDecorator>
|
||||
<CloseIcon />
|
||||
</ListItemDecorator>
|
||||
Cancel
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDeleteConfirmed} color='danger' sx={{ color: 'danger' }}>
|
||||
<ListItemDecorator>
|
||||
<DeleteOutlineIcon />
|
||||
</ListItemDecorator>
|
||||
Confirm Deletion
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
p: 2,
|
||||
minWidth: 200,
|
||||
}}
|
||||
>
|
||||
<FormLabel
|
||||
id='folder-color'
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
fontWeight: 'xl',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 'xs',
|
||||
letterSpacing: '0.1em',
|
||||
}}
|
||||
>
|
||||
Color
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-labelledby='product-color-attribute'
|
||||
defaultValue={folder.color || 'warning'}
|
||||
onChange={handleColorChange}
|
||||
sx={{ gap: 2, flexWrap: 'wrap', flexDirection: 'row', maxWidth: 240 }}
|
||||
>
|
||||
{FOLDERS_COLOR_PALETTE.map((color, index) => (
|
||||
<Sheet
|
||||
key={index}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: 20,
|
||||
height: 20,
|
||||
flexShrink: 0,
|
||||
bgcolor: `${color}`,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
overlay
|
||||
variant='solid'
|
||||
checkedIcon={<Done />}
|
||||
value={color}
|
||||
color='neutral'
|
||||
slotProps={{
|
||||
input: { 'aria-label': color },
|
||||
radio: {
|
||||
sx: {
|
||||
display: 'contents',
|
||||
'--variant-borderWidth': '2px',
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
'--joy-focus-outlineOffset': '4px',
|
||||
'--joy-palette-focusVisible': color,
|
||||
[`& .${radioClasses.action}.${radioClasses.focusVisible}`]: {
|
||||
outlineWidth: '2px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Sheet>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</MenuItem>
|
||||
|
||||
</CloseableMenu>
|
||||
)}
|
||||
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Droppable, DroppableProps } from "react-beautiful-dnd";
|
||||
|
||||
export const StrictModeDroppable = ({ children, ...props }: DroppableProps) => {
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const animation = requestAnimationFrame(() => setEnabled(true));
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(animation);
|
||||
setEnabled(false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <Droppable {...props}>{children}</Droppable>;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { DropdownItems, GoodDropdown } from '~/common/components/GoodDropdown';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
|
||||
const SPECIAL_ID_REMOVE = '_REMOVE_';
|
||||
|
||||
|
||||
export function useFolderDropdown(conversationId: DConversationId | null) {
|
||||
|
||||
// external state
|
||||
const { folders, useFolders } = useFolderStore();
|
||||
|
||||
|
||||
// Prepare items for the dropdown
|
||||
const folderItems: DropdownItems = React.useMemo(() => {
|
||||
// add one item per folder
|
||||
const items = folders.reduce((items, folder) => {
|
||||
items[folder.id] = {
|
||||
title: folder.title,
|
||||
icon: <FolderIcon sx={{ color: folder.color }} />,
|
||||
};
|
||||
return items;
|
||||
}, {} as DropdownItems);
|
||||
|
||||
// add one item representing no folder
|
||||
items[SPECIAL_ID_REMOVE] = {
|
||||
title: 'No Folder',
|
||||
};
|
||||
|
||||
return items;
|
||||
}, [folders]);
|
||||
|
||||
|
||||
// Handle dropdown folder change
|
||||
const handleFolderChange = React.useCallback((_event: any, folderId: string | null) => {
|
||||
if (conversationId && folderId) {
|
||||
// Remove conversation from all folders
|
||||
folders.forEach(folder => {
|
||||
if (folder.conversationIds.includes(conversationId)) {
|
||||
useFolderStore.getState().removeConversationFromFolder(folder.id, conversationId);
|
||||
}
|
||||
});
|
||||
// Add conversation to the selected folder
|
||||
useFolderStore.getState().addConversationToFolder(folderId, conversationId);
|
||||
}
|
||||
}, [conversationId, folders]);
|
||||
|
||||
// find the folder ID for the active Conversation
|
||||
const currentFolderId = folders.find(folder => folder.conversationIds.includes(conversationId || ''))?.id || null;
|
||||
|
||||
// Create the dropdown component
|
||||
const folderDropdown = React.useMemo(() => {
|
||||
|
||||
// don't show the dropdown if folders are not enabled
|
||||
if (!useFolders)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<GoodDropdown
|
||||
items={folderItems}
|
||||
value={currentFolderId}
|
||||
onChange={handleFolderChange}
|
||||
placeholder='Select a folder'
|
||||
showSymbols
|
||||
/>
|
||||
);
|
||||
}, [currentFolderId, folderItems, handleFolderChange, useFolders]);
|
||||
|
||||
return { folderDropdown };
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { AppBarDropdown, DropdownItems } from '~/common/layout/AppBarDropdown';
|
||||
import { GoodDropdown, DropdownItems } from '~/common/components/GoodDropdown';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { openLayoutLLMOptions, openLayoutModelsSetup } from '~/common/layout/store-applayout';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
|
||||
function AppBarLLMDropdown(props: {
|
||||
@@ -19,6 +19,9 @@ function AppBarLLMDropdown(props: {
|
||||
placeholder?: string,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { openLlmOptions, openModelsSetup } = useOptimaLayout();
|
||||
|
||||
// build model menu items, filtering-out hidden models, and add Source separators
|
||||
const llmItems: DropdownItems = {};
|
||||
let prevSourceId: DModelSourceId | null = null;
|
||||
@@ -47,11 +50,11 @@ function AppBarLLMDropdown(props: {
|
||||
|
||||
const handleChatLLMChange = (_event: any, value: DLLMId | null) => value && props.setChatLlmId(value);
|
||||
|
||||
const handleOpenLLMOptions = () => props.chatLlmId && openLayoutLLMOptions(props.chatLlmId);
|
||||
const handleOpenLLMOptions = () => props.chatLlmId && openLlmOptions(props.chatLlmId);
|
||||
|
||||
|
||||
return (
|
||||
<AppBarDropdown
|
||||
<GoodDropdown
|
||||
items={llmItems}
|
||||
value={props.chatLlmId} onChange={handleChatLLMChange}
|
||||
placeholder={props.placeholder || 'Models …'}
|
||||
@@ -67,7 +70,7 @@ function AppBarLLMDropdown(props: {
|
||||
</ListItemButton>
|
||||
)}
|
||||
|
||||
<ListItemButton key='menu-llms' onClick={openLayoutModelsSetup}>
|
||||
<ListItemButton key='menu-llms' onClick={openModelsSetup}>
|
||||
<ListItemDecorator><BuildCircleIcon color='success' /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Models
|
||||
|
||||
@@ -6,8 +6,8 @@ import CallIcon from '@mui/icons-material/Call';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
|
||||
import { AppBarDropdown } from '~/common/layout/AppBarDropdown';
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { GoodDropdown } from '~/common/components/GoodDropdown';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
@@ -42,7 +42,7 @@ function AppBarPersonaDropdown(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppBarDropdown
|
||||
<GoodDropdown
|
||||
items={SystemPurposes} showSymbols={zenMode !== 'cleaner'}
|
||||
value={props.systemPurposeId} onChange={handleSystemPurposeChange}
|
||||
appendOption={appendOption}
|
||||
|
||||
@@ -170,7 +170,7 @@ export function CameraCaptureModal(props: {
|
||||
|
||||
<Box sx={{ display: 'flex', gap: 1, justifyContent: 'space-between' }}>
|
||||
{/* Info */}
|
||||
<IconButton disabled={!info} variant='soft' color='neutral' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
|
||||
<IconButton size='lg' disabled={!info} variant='soft' onClick={() => setShowInfo(info => !info)} sx={{ zIndex: 30 }}>
|
||||
<InfoIcon />
|
||||
</IconButton>
|
||||
{/*<Button disabled={ocrProgress !== null} fullWidth variant='solid' size='lg' onClick={handleVideoOCRClicked} sx={{ flex: 1, maxWidth: 260 }}>*/}
|
||||
@@ -189,7 +189,7 @@ export function CameraCaptureModal(props: {
|
||||
</Button>
|
||||
|
||||
{/* Download */}
|
||||
<IconButton variant='soft' color='neutral' onClick={handleVideoDownloadClicked}>
|
||||
<IconButton size='lg' variant='soft' onClick={handleVideoDownloadClicked}>
|
||||
<DownloadIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Box, MenuItem, Radio, Typography } from '@mui/joy';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { ChatModeId } from '../../AppChat';
|
||||
|
||||
@@ -14,29 +13,25 @@ interface ChatModeDescription {
|
||||
label: string;
|
||||
description: string | React.JSX.Element;
|
||||
shortcut?: string;
|
||||
experimental?: boolean;
|
||||
requiresTTI?: boolean;
|
||||
}
|
||||
|
||||
const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
|
||||
'immediate': {
|
||||
'generate-text': {
|
||||
label: 'Chat',
|
||||
description: 'Persona replies',
|
||||
},
|
||||
'write-user': {
|
||||
'append-user': {
|
||||
label: 'Write',
|
||||
description: 'Appends a message',
|
||||
shortcut: 'Alt + Enter',
|
||||
},
|
||||
'draw-imagine': {
|
||||
'generate-image': {
|
||||
label: 'Draw',
|
||||
description: 'AI Image Generation',
|
||||
requiresTTI: true,
|
||||
},
|
||||
'draw-imagine-plus': {
|
||||
label: 'Assisted Draw',
|
||||
description: 'Assisted Image Generation',
|
||||
experimental: true,
|
||||
},
|
||||
'react': {
|
||||
'generate-react': {
|
||||
label: 'Reason + Act · α',
|
||||
description: 'Answers questions in multiple steps',
|
||||
},
|
||||
@@ -49,11 +44,14 @@ function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
|
||||
return shortcut;
|
||||
}
|
||||
|
||||
export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClose: () => void, chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void }) {
|
||||
export function ChatModeMenu(props: {
|
||||
anchorEl: HTMLAnchorElement | null, onClose: () => void,
|
||||
chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void
|
||||
capabilityHasTTI: boolean,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
const labsMagicDraw = useUXLabsStore(state => state.labsMagicDraw);
|
||||
|
||||
return <CloseableMenu
|
||||
placement='top-end' sx={{ minWidth: 320 }}
|
||||
@@ -68,14 +66,13 @@ export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClos
|
||||
|
||||
{/* ChatMode items */}
|
||||
{Object.entries(ChatModeItems)
|
||||
.filter(([, { experimental }]) => labsMagicDraw || !experimental)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Radio checked={key === props.chatModeId} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography>{data.label}</Typography>
|
||||
<Typography level='body-xs'>{data.description}</Typography>
|
||||
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
|
||||
</Box>
|
||||
{(key === props.chatModeId || !!data.shortcut) && (
|
||||
<KeyStroke combo={fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline)} />
|
||||
|
||||
@@ -3,11 +3,13 @@ import { shallow } from 'zustand/shallow';
|
||||
import { fileOpen, FileWithHandle } from 'browser-fs-access';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Typography } from '@mui/joy';
|
||||
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
@@ -24,12 +26,13 @@ import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { countModelTokens } from '~/common/util/token-counter';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
import { openLayoutPreferences } from '~/common/layout/store-applayout';
|
||||
import { lineHeightTextarea } from '~/common/app.theme';
|
||||
import { playSoundUrl } from '~/common/util/audioUtils';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { useDebouncer } from '~/common/components/useDebouncer';
|
||||
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
@@ -39,13 +42,13 @@ import { getTextBlockText, useLLMAttachments } from './attachments/useLLMAttachm
|
||||
import { useAttachments } from './attachments/useAttachments';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './composer.types';
|
||||
import { ButtonAttachCameraMemo } from './ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from './ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './ButtonAttachFile';
|
||||
import { ButtonCall } from './ButtonCall';
|
||||
import { ButtonMicContinuationMemo } from './ButtonMicContinuation';
|
||||
import { ButtonMicMemo } from './ButtonMic';
|
||||
import { ButtonOptionsDraw } from './ButtonOptionsDraw';
|
||||
import { ButtonAttachCameraMemo } from './buttons/ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
|
||||
import { ButtonCall } from './buttons/ButtonCall';
|
||||
import { ButtonMicContinuationMemo } from './buttons/ButtonMicContinuation';
|
||||
import { ButtonMicMemo } from './buttons/ButtonMic';
|
||||
import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
|
||||
import { ChatModeMenu } from './ChatModeMenu';
|
||||
import { TokenBadgeMemo } from './TokenBadge';
|
||||
import { TokenProgressbarMemo } from './TokenProgressbar';
|
||||
@@ -71,8 +74,10 @@ export function Composer(props: {
|
||||
chatLLM: DLLM | null;
|
||||
composerTextAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
conversationId: DConversationId | null;
|
||||
capabilityHasT2I: boolean;
|
||||
isDeveloperMode: boolean;
|
||||
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
|
||||
onTextImagine: (conversationId: DConversationId, text: string) => void;
|
||||
sx?: SxProps;
|
||||
}) {
|
||||
|
||||
@@ -85,18 +90,19 @@ export function Composer(props: {
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const { openPreferencesTab, setIsFocusedMode } = useOptimaLayout();
|
||||
const { labsCalling, labsCameraDesktop } = useUXLabsStore(state => ({
|
||||
labsCalling: state.labsCalling,
|
||||
labsCameraDesktop: state.labsCameraDesktop,
|
||||
}), shallow);
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('immediate');
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
|
||||
const [startupText, setStartupText] = useComposerStartupText();
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
|
||||
const { assistantTyping, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(state => {
|
||||
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(state => {
|
||||
const conversation = state.conversations.find(_c => _c.id === props.conversationId);
|
||||
return {
|
||||
assistantTyping: conversation ? !!conversation.abortController : false,
|
||||
assistantAbortible: conversation ? !!conversation.abortController : false,
|
||||
systemPurposeId: conversation?.systemPurposeId ?? null,
|
||||
tokenCount: conversation ? conversation.tokenCount : 0,
|
||||
stopTyping: state.stopTyping,
|
||||
@@ -162,24 +168,6 @@ export function Composer(props: {
|
||||
return enqueued;
|
||||
}, [clearAttachments, conversationId, llmAttachments, onAction, setComposeText]);
|
||||
|
||||
const handleTextareaKeyDown = React.useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
// Alt: append the message instead
|
||||
if (e.altKey) {
|
||||
handleSendAction('write-user', composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
// Shift: toggles the 'enter is newline'
|
||||
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
|
||||
if (!assistantTyping)
|
||||
handleSendAction(chatModeId, composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
}
|
||||
}, [assistantTyping, chatModeId, composeText, enterIsNewline, handleSendAction]);
|
||||
|
||||
const handleSendClicked = () => handleSendAction(chatModeId, composeText);
|
||||
|
||||
const handleStopClicked = () => props.conversationId && stopTyping(props.conversationId);
|
||||
@@ -189,7 +177,39 @@ export function Composer(props: {
|
||||
|
||||
const handleCallClicked = () => props.conversationId && systemPurposeId && launchAppCall(props.conversationId, systemPurposeId);
|
||||
|
||||
const handleDrawOptionsClicked = () => openLayoutPreferences(2);
|
||||
const handleDrawOptionsClicked = () => openPreferencesTab(2);
|
||||
|
||||
const handleTextImagineClicked = () => {
|
||||
if (!composeText || !props.conversationId)
|
||||
return;
|
||||
props.onTextImagine(props.conversationId, composeText);
|
||||
setComposeText('');
|
||||
};
|
||||
|
||||
|
||||
// Text actions
|
||||
|
||||
const handleTextAreaTextChange = React.useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setComposeText(e.target.value);
|
||||
}, [setComposeText]);
|
||||
|
||||
const handleTextareaKeyDown = React.useCallback((e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
}, [assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction]);
|
||||
|
||||
|
||||
// Mode menu
|
||||
@@ -221,7 +241,7 @@ export function Composer(props: {
|
||||
nextText = nextText ? nextText + ' ' + transcript : transcript;
|
||||
|
||||
// auto-send (mic continuation mode) if requested
|
||||
const autoSend = micContinuation && nextText.length >= 1 && !!props.conversationId; //&& assistantTyping;
|
||||
const autoSend = micContinuation && nextText.length >= 1 && !!props.conversationId; //&& assistantAbortible;
|
||||
const notUserStop = result.doneReason !== 'manual';
|
||||
if (autoSend) {
|
||||
if (notUserStop)
|
||||
@@ -243,7 +263,7 @@ export function Composer(props: {
|
||||
useGlobalShortcut('m', true, false, false, toggleRecording);
|
||||
|
||||
const micIsRunning = !!speechInterimResult;
|
||||
const micContinuationTrigger = micContinuation && !micIsRunning && !assistantTyping;
|
||||
const micContinuationTrigger = micContinuation && !micIsRunning && !assistantAbortible;
|
||||
const micColor: ColorPaletteProp = isSpeechError ? 'danger' : isRecordingSpeech ? 'primary' : isRecordingAudio ? 'primary' : 'neutral';
|
||||
const micVariant: VariantProp = isRecordingSpeech ? 'solid' : isRecordingAudio ? 'soft' : 'soft'; //(isDesktop ? 'soft' : 'plain');
|
||||
|
||||
@@ -350,24 +370,25 @@ export function Composer(props: {
|
||||
}, [attachAppendDataTransfer, eatDragEvent, setComposeText]);
|
||||
|
||||
|
||||
const isImmediate = chatModeId === 'immediate';
|
||||
const isWriteUser = chatModeId === 'write-user';
|
||||
const isChat = isImmediate || isWriteUser;
|
||||
const isReAct = chatModeId === 'react';
|
||||
const isDraw = chatModeId === 'draw-imagine';
|
||||
const isDrawPlus = chatModeId === 'draw-imagine-plus';
|
||||
const buttonColor: ColorPaletteProp = isReAct ? 'success' : (isDraw || isDrawPlus) ? 'warning' : 'primary';
|
||||
const isText = chatModeId === 'generate-text';
|
||||
const isAppend = chatModeId === 'append-user';
|
||||
const isChat = isText || isAppend;
|
||||
const isReAct = chatModeId === 'generate-react';
|
||||
const isDraw = chatModeId === 'generate-image';
|
||||
const buttonColor: ColorPaletteProp = assistantAbortible
|
||||
? 'warning'
|
||||
: isReAct ? 'success' : isDraw ? 'warning' : 'primary';
|
||||
|
||||
const textPlaceholder: string =
|
||||
isDrawPlus
|
||||
? 'Write a subject, and we\'ll add detail...'
|
||||
: isDraw
|
||||
? 'Describe an idea or a drawing...'
|
||||
: isReAct
|
||||
? 'Multi-step reasoning question...'
|
||||
: props.isDeveloperMode
|
||||
? 'Chat with me · drop source files · attach code...'
|
||||
: /*isProdiaConfigured ?*/ 'Chat · /react · /imagine · drop text files...' /*: 'Chat · /react · drop text files...'*/;
|
||||
isDraw
|
||||
? 'Describe an idea or a drawing...'
|
||||
: isReAct
|
||||
? 'Multi-step reasoning question...'
|
||||
: props.isDeveloperMode
|
||||
? 'Chat with me · drop source files · attach code...'
|
||||
: props.capabilityHasT2I
|
||||
? 'Chat · /react · /draw · drop text files...'
|
||||
: 'Chat · /react · drop text files...';
|
||||
|
||||
|
||||
return (
|
||||
@@ -379,7 +400,7 @@ export function Composer(props: {
|
||||
|
||||
{/* Vertical (insert) buttons */}
|
||||
{isMobile ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { md: 2 } }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
|
||||
{/* [mobile] Mic button */}
|
||||
{isSpeechEnabled && <ButtonMicMemo variant={micVariant} color={micColor} onClick={handleToggleMic} />}
|
||||
@@ -395,7 +416,7 @@ export function Composer(props: {
|
||||
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: { md: 2 } }}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
{/*<FormHelperText sx={{ mx: 'auto' }}>*/}
|
||||
{/* Attach*/}
|
||||
@@ -417,7 +438,7 @@ export function Composer(props: {
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex', flexDirection: 'column', gap: 1,
|
||||
overflowX: 'clip',
|
||||
minWidth: 200, // enable X-scrolling (resetting any possible minWidth due to the attachments)
|
||||
}}>
|
||||
|
||||
{/* Edit box + Overlays + Mic buttons */}
|
||||
@@ -427,16 +448,18 @@ export function Composer(props: {
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
|
||||
<Textarea
|
||||
variant='outlined' color={(isDraw || isDrawPlus) ? 'warning' : isReAct ? 'success' : 'neutral'}
|
||||
variant='outlined' color={isDraw ? 'warning' : isReAct ? 'success' : 'neutral'}
|
||||
autoFocus
|
||||
minRows={5} maxRows={10}
|
||||
minRows={isMobile ? 5 : 5} maxRows={10}
|
||||
placeholder={textPlaceholder}
|
||||
value={composeText}
|
||||
onChange={(event) => setComposeText(event.target.value)}
|
||||
onChange={handleTextAreaTextChange}
|
||||
onDragEnter={handleTextareaDragEnter}
|
||||
onDragStart={handleTextareaDragStart}
|
||||
onKeyDown={handleTextareaKeyDown}
|
||||
onPasteCapture={handleAttachCtrlV}
|
||||
onFocusCapture={() => setIsFocusedMode(true)}
|
||||
onBlurCapture={() => setIsFocusedMode(false)}
|
||||
slotProps={{
|
||||
textarea: {
|
||||
enterKeyHint: enterIsNewline ? 'enter' : 'send',
|
||||
@@ -452,8 +475,7 @@ export function Composer(props: {
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
// fontSize: '16px',
|
||||
lineHeight: 1.75,
|
||||
lineHeight: lineHeightTextarea,
|
||||
}} />
|
||||
|
||||
{tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
|
||||
@@ -555,34 +577,40 @@ export function Composer(props: {
|
||||
{/* [mobile] bottom-corner secondary button */}
|
||||
{isMobile && (isChat
|
||||
? <ButtonCall isMobile disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
: (isDraw || isDrawPlus)
|
||||
: isDraw
|
||||
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
: <IconButton disabled variant='plain' color='neutral' sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
: <IconButton disabled sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
)}
|
||||
|
||||
{/* Responsive Send/Stop buttons */}
|
||||
<ButtonGroup
|
||||
variant={isWriteUser ? 'outlined' : 'solid'}
|
||||
variant={isAppend ? 'outlined' : 'solid'}
|
||||
color={buttonColor}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
boxShadow: isMobile ? 'none' : `0 8px 24px -4px rgb(var(--joy-palette-${buttonColor}-mainChannel) / 20%)`,
|
||||
}}
|
||||
>
|
||||
{!assistantTyping ? (
|
||||
{!assistantAbortible ? (
|
||||
<Button
|
||||
key='composer-act'
|
||||
fullWidth disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
|
||||
onClick={handleSendClicked}
|
||||
endDecorator={micContinuation ? <AutoModeIcon /> : isWriteUser ? <SendIcon sx={{ fontSize: 18 }} /> : isReAct ? <PsychologyIcon /> : <TelegramIcon />}
|
||||
endDecorator={
|
||||
micContinuation ? <AutoModeIcon /> :
|
||||
isAppend ? <SendIcon sx={{ fontSize: 18 }} /> :
|
||||
isReAct ? <PsychologyIcon /> :
|
||||
isDraw ? <FormatPaintIcon />
|
||||
: <TelegramIcon />
|
||||
}
|
||||
>
|
||||
{micContinuation && 'Voice '}
|
||||
{isWriteUser ? 'Write' : isReAct ? 'ReAct' : isDraw ? 'Draw' : isDrawPlus ? 'Draw+' : 'Chat'}
|
||||
{isAppend ? 'Write' : isReAct ? 'ReAct' : isDraw ? 'Draw' : 'Chat'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
key='composer-stop'
|
||||
fullWidth variant='soft' color={isReAct ? 'success' : 'primary'} disabled={!props.conversationId}
|
||||
fullWidth variant='soft' disabled={!props.conversationId}
|
||||
onClick={handleStopClicked}
|
||||
endDecorator={<StopOutlinedIcon sx={{ fontSize: 18 }} />}
|
||||
sx={{ animation: `${animationStopEnter} 0.1s ease-out` }}
|
||||
@@ -590,7 +618,20 @@ export function Composer(props: {
|
||||
Stop
|
||||
</Button>
|
||||
)}
|
||||
<IconButton disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor} onClick={handleModeSelectorShow}>
|
||||
|
||||
{/* [Draw] Imagine */}
|
||||
{isDraw && !!composeText && <Tooltip title='Imagine a drawing prompt'>
|
||||
<IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleTextImagineClicked}>
|
||||
<AutoAwesomeIcon />
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
|
||||
{/* Mode expander */}
|
||||
<IconButton
|
||||
variant={assistantAbortible ? 'soft' : isDraw ? undefined : undefined}
|
||||
disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor}
|
||||
onClick={handleModeSelectorShow}
|
||||
>
|
||||
<ExpandLessIcon />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
@@ -605,7 +646,7 @@ export function Composer(props: {
|
||||
{isChat && <ButtonCall disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
|
||||
|
||||
{/* [desktop] Draw Options secondary button */}
|
||||
{(isDraw || isDrawPlus) && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
|
||||
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
|
||||
|
||||
</Box>}
|
||||
|
||||
@@ -618,6 +659,7 @@ export function Composer(props: {
|
||||
<ChatModeMenu
|
||||
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
|
||||
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
|
||||
capabilityHasTTI={props.capabilityHasT2I}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
+1
-1
@@ -11,7 +11,7 @@ export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => vo
|
||||
<FormatPaintIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button variant='soft' color='warning' onClick={props.onClick} endDecorator={<FormatPaintIcon />} sx={props.sx}>
|
||||
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
|
||||
Options
|
||||
</Button>
|
||||
);
|
||||
@@ -3,7 +3,7 @@ import TimeAgo from 'react-timeago';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { cleanupEfficiency, Diff as TextDiff, makeDiff } from '@sanity/diff-match-patch';
|
||||
|
||||
import { Avatar, Box, Button, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Stack, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import { Avatar, Box, Button, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
@@ -14,7 +14,6 @@ import Face6Icon from '@mui/icons-material/Face6';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import PaletteOutlinedIcon from '@mui/icons-material/PaletteOutlined';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
|
||||
@@ -30,7 +29,7 @@ import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { cssRainbowColorKeyframes } from '~/common/app.theme';
|
||||
import { cssRainbowColorKeyframes, lineHeightChatText } from '~/common/app.theme';
|
||||
import { prettyBaseModel } from '~/common/util/modelUtils';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
@@ -69,47 +68,54 @@ export function messageBackground(messageRole: DMessage['role'] | string, wasEdi
|
||||
}
|
||||
}
|
||||
|
||||
const avatarIconSx = { width: 36, height: 36 };
|
||||
|
||||
export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['role'] | string, messageOriginLLM: string | undefined, messagePurposeId: SystemPurposeId | undefined, messageSender: string, messageTyping: boolean, size: 'sm' | undefined = undefined): React.JSX.Element {
|
||||
if (typeof messageAvatar === 'string' && messageAvatar)
|
||||
return <Avatar alt={messageSender} src={messageAvatar} />;
|
||||
const iconSx = { width: 40, height: 40 };
|
||||
const mascotSx = size === 'sm' ? { width: 40, height: 40 } : { width: 64, height: 64 };
|
||||
const mascotSx = size === 'sm' ? avatarIconSx : { width: 64, height: 64 };
|
||||
switch (messageRole) {
|
||||
case 'system':
|
||||
return <SettingsSuggestIcon sx={iconSx} />; // https://em-content.zobj.net/thumbs/120/apple/325/robot_1f916.png
|
||||
return <SettingsSuggestIcon sx={avatarIconSx} />; // https://em-content.zobj.net/thumbs/120/apple/325/robot_1f916.png
|
||||
|
||||
case 'user':
|
||||
return <Face6Icon sx={avatarIconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
|
||||
|
||||
case 'assistant':
|
||||
// display a gif avatar when the assistant is typing (people seem to love this, so keeping it after april fools')
|
||||
// typing gif (people seem to love this, so keeping it after april fools')
|
||||
const isTextToImage = messageOriginLLM === 'DALL·E' || messageOriginLLM === 'Prodia';
|
||||
const isReact = messageOriginLLM?.startsWith('react-');
|
||||
if (messageTyping) {
|
||||
return <Avatar
|
||||
alt={messageSender} variant='plain'
|
||||
src={messageOriginLLM === 'prodia'
|
||||
? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
|
||||
: messageOriginLLM?.startsWith('react-')
|
||||
? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
|
||||
src={isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
|
||||
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
|
||||
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'}
|
||||
sx={{ ...mascotSx, borderRadius: 'var(--joy-radius-sm)' }}
|
||||
sx={{ ...mascotSx, borderRadius: 'sm' }}
|
||||
/>;
|
||||
}
|
||||
// display the purpose symbol
|
||||
if (messageOriginLLM === 'prodia')
|
||||
return <PaletteOutlinedIcon sx={iconSx} />;
|
||||
const symbol = SystemPurposes[messagePurposeId!]?.symbol;
|
||||
if (symbol)
|
||||
return <Box
|
||||
sx={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
width: '100%', minWidth: `${iconSx.width}px`, lineHeight: `${iconSx.height}px`,
|
||||
}}
|
||||
>
|
||||
{symbol}
|
||||
</Box>;
|
||||
// default assistant avatar
|
||||
return <SmartToyOutlinedIcon sx={iconSx} />; // https://mui.com/static/images/avatar/2.jpg
|
||||
|
||||
case 'user':
|
||||
return <Face6Icon sx={iconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
|
||||
// text-to-image: icon
|
||||
if (isTextToImage)
|
||||
return <FormatPaintIcon sx={{
|
||||
...avatarIconSx,
|
||||
animation: `${cssRainbowColorKeyframes} 1s linear 2.66`,
|
||||
}} />;
|
||||
|
||||
// purpose symbol (if present)
|
||||
const symbol = SystemPurposes[messagePurposeId!]?.symbol;
|
||||
if (symbol) return <Box sx={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
minWidth: `${avatarIconSx.width}px`,
|
||||
lineHeight: `${avatarIconSx.height}px`,
|
||||
}}>
|
||||
{symbol}
|
||||
</Box>;
|
||||
|
||||
// default assistant avatar
|
||||
return <SmartToyOutlinedIcon sx={avatarIconSx} />; // https://mui.com/static/images/avatar/2.jpg
|
||||
}
|
||||
return <Avatar alt={messageSender} />;
|
||||
}
|
||||
@@ -144,13 +150,14 @@ function explainErrorInMessage(text: string, isAssistant: boolean, modelId?: str
|
||||
</>;
|
||||
} else if (text.includes('"context_length_exceeded"')) {
|
||||
// TODO: propose to summarize or split the input?
|
||||
const pattern = /maximum context length is (\d+) tokens.+you requested (\d+) tokens/;
|
||||
const pattern = /maximum context length is (\d+) tokens.+resulted in (\d+) tokens/;
|
||||
const match = pattern.exec(text);
|
||||
const usedText = match ? <b>{parseInt(match[2] || '0').toLocaleString()} tokens > {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"}}
|
||||
@@ -167,6 +174,8 @@ function explainErrorInMessage(text: string, isAssistant: boolean, modelId?: str
|
||||
make sure the usage is under <Link noLinkStyle href='https://platform.openai.com/account/billing/limits' target='_blank'>the limits</Link>.
|
||||
</>;
|
||||
}
|
||||
// else
|
||||
// errorMessage = <>{text || 'Unknown error'}</>;
|
||||
|
||||
return { errorMessage, isAssistantError };
|
||||
}
|
||||
@@ -254,9 +263,9 @@ export function ChatMessage(props: {
|
||||
const showAvatars = props.hideAvatars !== true && !cleanerLooks;
|
||||
|
||||
const textSel = selMenuText ? selMenuText : messageText;
|
||||
const isSpecialProdia = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/imagine') || textSel.startsWith('/img');
|
||||
const couldDiagram = textSel?.length >= 100 && !isSpecialProdia;
|
||||
const couldImagine = textSel?.length >= 2 && !isSpecialProdia;
|
||||
const isSpecialT2I = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/draw ') || textSel.startsWith('/imagine ') || textSel.startsWith('/img ');
|
||||
const couldDiagram = textSel?.length >= 100 && !isSpecialT2I;
|
||||
const couldImagine = textSel?.length >= 2 && !isSpecialT2I;
|
||||
const couldSpeak = couldImagine;
|
||||
|
||||
|
||||
@@ -404,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)',
|
||||
};
|
||||
|
||||
@@ -439,46 +452,57 @@ export function ChatMessage(props: {
|
||||
borderBottomColor: 'divider',
|
||||
}),
|
||||
...(ENABLE_COPY_MESSAGE_OVERLAY && { position: 'relative' }),
|
||||
...(props.isBottom === true && { mb: 'auto' }),
|
||||
'&:hover > button': { opacity: 1 },
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Avatar */}
|
||||
{showAvatars && <Stack
|
||||
sx={{ alignItems: 'center', minWidth: { xs: 50, md: 64 }, maxWidth: 80, textAlign: 'center' }}
|
||||
onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}
|
||||
onClick={event => setOpsMenuAnchor(event.currentTarget)}>
|
||||
{showAvatars && (
|
||||
<Box
|
||||
onMouseEnter={() => setIsHovering(true)} onMouseLeave={() => setIsHovering(false)}
|
||||
onClick={event => setOpsMenuAnchor(event.currentTarget)}
|
||||
sx={{
|
||||
// flexBasis: 0, // this won't let the item grow
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
minWidth: { xs: 50, md: 64 }, maxWidth: 80,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
|
||||
{isHovering ? (
|
||||
<IconButton variant='soft' color={fromAssistant ? 'neutral' : 'primary'}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
avatarEl
|
||||
)}
|
||||
{isHovering ? (
|
||||
<IconButton variant='soft' color={fromAssistant ? 'neutral' : 'primary'} sx={avatarIconSx}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
avatarEl
|
||||
)}
|
||||
|
||||
{/* Assistant model name */}
|
||||
{fromAssistant && (
|
||||
<Tooltip title={messageOriginLLM || 'unk-model'} variant='solid'>
|
||||
<Typography level='body-sm' sx={{
|
||||
fontSize: { xs: 'xs', sm: 'sm' }, fontWeight: 500,
|
||||
overflowWrap: 'anywhere',
|
||||
...(messageTyping ? { animation: `${cssRainbowColorKeyframes} 5s linear infinite` } : {}),
|
||||
}}>
|
||||
{prettyBaseModel(messageOriginLLM)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
{/* Assistant model name */}
|
||||
{fromAssistant && (
|
||||
<Tooltip title={messageOriginLLM || 'unk-model'} variant='solid'>
|
||||
<Typography level='body-xs' sx={{
|
||||
overflowWrap: 'anywhere',
|
||||
...(messageTyping ? { animation: `${cssRainbowColorKeyframes} 5s linear infinite` } : {}),
|
||||
}}>
|
||||
{prettyBaseModel(messageOriginLLM)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
</Stack>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{/* Edit / Blocks */}
|
||||
{isEditing
|
||||
|
||||
? <InlineTextarea initialText={messageText} onEdit={handleTextEdited} sx={{ ...blockSx, lineHeight: 1.75, flexGrow: 1 }} />
|
||||
? <InlineTextarea
|
||||
initialText={messageText} onEdit={handleTextEdited}
|
||||
sx={{
|
||||
...blockSx,
|
||||
flexGrow: 1,
|
||||
}} />
|
||||
|
||||
: <Box
|
||||
onContextMenu={(ENABLE_SELECTION_RIGHT_CLICK_MENU && props.onMessageEdit) ? event => handleMouseUp(event.nativeEvent) : undefined}
|
||||
@@ -520,14 +544,14 @@ export function ChatMessage(props: {
|
||||
: block.type === 'code'
|
||||
? <RenderCode key={'code-' + index} codeBlock={block} sx={codeSx} noCopyButton={props.diagramMode} />
|
||||
: block.type === 'image'
|
||||
? <RenderImage key={'image-' + index} imageBlock={block} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
|
||||
? <RenderImage key={'image-' + index} imageBlock={block} isFirst={!index} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
|
||||
: block.type === 'latex'
|
||||
? <RenderLatex key={'latex-' + index} latexBlock={block} />
|
||||
? <RenderLatex key={'latex-' + index} latexBlock={block} sx={typographySx} />
|
||||
: block.type === 'diff'
|
||||
? <RenderTextDiff key={'latex-' + index} diffBlock={block} />
|
||||
? <RenderTextDiff key={'latex-' + index} diffBlock={block} sx={typographySx} />
|
||||
: (renderMarkdown && props.noMarkdown !== true && !fromSystem && !(fromUser && block.content.startsWith('/')))
|
||||
? <RenderMarkdown key={'text-md-' + index} textBlock={block} />
|
||||
: <RenderText key={'text-' + index} textBlock={block} />)}
|
||||
? <RenderMarkdown key={'text-md-' + index} textBlock={block} sx={typographySx} />
|
||||
: <RenderText key={'text-' + index} textBlock={block} sx={typographySx} />)}
|
||||
|
||||
{isCollapsed && (
|
||||
<Button variant='plain' color='neutral' onClick={handleUncollapse}>... expand ...</Button>
|
||||
@@ -547,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',
|
||||
@@ -615,7 +639,7 @@ export function ChatMessage(props: {
|
||||
</MenuItem>}
|
||||
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
|
||||
Imagine
|
||||
Draw ...
|
||||
</MenuItem>}
|
||||
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { makeAvatar, messageBackground } from './ChatMessage';
|
||||
/**
|
||||
* Header bar for controlling the operations during the Selection mode
|
||||
*/
|
||||
export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
|
||||
export const MessagesSelectionHeader = (props: { hasSelected: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
|
||||
<Sheet color='warning' variant='solid' invertedColors sx={{
|
||||
display: 'flex', flexDirection: 'row', alignItems: 'center',
|
||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101,
|
||||
@@ -28,7 +28,7 @@ export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom:
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<IconButton variant='plain' onClick={props.onClose}>
|
||||
<IconButton onClick={props.onClose}>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
</Sheet>;
|
||||
@@ -39,7 +39,7 @@ export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom:
|
||||
*
|
||||
* Shall look similarly to the main ChatMessage, for consistency, but just allow a simple checkbox selection
|
||||
*/
|
||||
export function CleanerMessage(props: { message: DMessage, isBottom: boolean, selected: boolean, remainingTokens?: number, onToggleSelected?: (messageId: string, selected: boolean) => void }) {
|
||||
export function CleanerMessage(props: { message: DMessage, selected: boolean, remainingTokens?: number, onToggleSelected?: (messageId: string, selected: boolean) => void }) {
|
||||
|
||||
// derived state
|
||||
const {
|
||||
@@ -77,7 +77,6 @@ export function CleanerMessage(props: { message: DMessage, isBottom: boolean, se
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
// position: 'relative',
|
||||
...(props.isBottom && { mb: 'auto' }),
|
||||
'&:hover > button': { opacity: 1 },
|
||||
}}>
|
||||
|
||||
|
||||
@@ -173,21 +173,21 @@ function RenderCodeImpl(props: {
|
||||
)}
|
||||
{isMermaid && (
|
||||
<Tooltip title={renderMermaid ? 'Show Code' : 'Render Mermaid'} variant='solid'>
|
||||
<IconButton variant={renderMermaid ? 'solid' : 'outlined'} color='neutral' onClick={() => setShowMermaid(!showMermaid)}>
|
||||
<IconButton variant={renderMermaid ? 'solid' : 'outlined'} onClick={() => setShowMermaid(!showMermaid)}>
|
||||
<SchemaIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isPlantUML && (
|
||||
<Tooltip title={renderPlantUML ? 'Show Code' : 'Render PlantUML'} variant='solid'>
|
||||
<IconButton variant={renderPlantUML ? 'solid' : 'outlined'} color='neutral' onClick={() => setShowPlantUML(!showPlantUML)}>
|
||||
<IconButton variant={renderPlantUML ? 'solid' : 'outlined'} onClick={() => setShowPlantUML(!showPlantUML)}>
|
||||
<SchemaIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isSVG && (
|
||||
<Tooltip title={renderSVG ? 'Show Code' : 'Render SVG'} variant='solid'>
|
||||
<IconButton variant={renderSVG ? 'solid' : 'outlined'} color='neutral' onClick={() => setShowSVG(!showSVG)}>
|
||||
<IconButton variant={renderSVG ? 'solid' : 'outlined'} onClick={() => setShowSVG(!showSVG)}>
|
||||
<ShapeLineOutlinedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -195,7 +195,7 @@ function RenderCodeImpl(props: {
|
||||
{canCodepen && <OpenInCodepen codeBlock={{ code: blockCode, language: inferredCodeLanguage || undefined }} />}
|
||||
{canReplit && <OpenInReplit codeBlock={{ code: blockCode, language: inferredCodeLanguage || undefined }} />}
|
||||
{props.noCopyButton !== true && <Tooltip title='Copy Code' variant='solid'>
|
||||
<IconButton variant='outlined' color='neutral' onClick={handleCopyToClipboard}>
|
||||
<IconButton variant='outlined' onClick={handleCopyToClipboard}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
|
||||
@@ -104,7 +104,7 @@ export function RenderHtml(props: { htmlBlock: HtmlBlock, sx?: SxProps }) {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title='Copy Code' variant='solid'>
|
||||
<IconButton variant='outlined' color='neutral' onClick={handleCopyToClipboard}>
|
||||
<IconButton variant='outlined' onClick={handleCopyToClipboard}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, IconButton, Tooltip } from '@mui/joy';
|
||||
import { Alert, Box, IconButton, Tooltip, Typography } from '@mui/joy';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
|
||||
|
||||
import { Link } from '~/common/components/Link';
|
||||
|
||||
@@ -10,39 +10,132 @@ import { ImageBlock } from './blocks';
|
||||
import { overlayButtonsSx } from './RenderCode';
|
||||
|
||||
|
||||
export const RenderImage = (props: { imageBlock: ImageBlock, allowRunAgain: boolean, onRunAgain?: (e: React.MouseEvent) => void }) => {
|
||||
const imageUrls = props.imageBlock.url.split('\n');
|
||||
const mdImageReferenceRegex = /^!\[([^\]]*)]\(([^)]+)\)$/;
|
||||
const imageExtensions = /\.(jpg|jpeg|png|gif|bmp|svg)/i;
|
||||
|
||||
return imageUrls.map((url, index) => (
|
||||
<Box
|
||||
|
||||
/**
|
||||
* Checks if the entire content consists solely of Markdown image references.
|
||||
* If so, returns an array of ImageBlock objects for each image reference.
|
||||
* If any non-image content is present or if there are no image references, returns null.
|
||||
*/
|
||||
export function heuristicMarkdownImageReferenceBlocks(fullText: string) {
|
||||
|
||||
// Check if all lines are valid Markdown image references with image URLs
|
||||
const imageBlocks: ImageBlock[] = [];
|
||||
for (const line of fullText.split('\n')) {
|
||||
if (line.trim() === '') continue; // skip empty lines
|
||||
const match = mdImageReferenceRegex.exec(line);
|
||||
if (match && imageExtensions.test(match[2])) {
|
||||
const alt = match[1];
|
||||
const url = match[2];
|
||||
imageBlocks.push({ type: 'image', url, alt });
|
||||
} else {
|
||||
// if there is any outlier line, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the image blocks if all lines are image references with valid image URLs
|
||||
return imageBlocks.length > 0 ? imageBlocks : null;
|
||||
}
|
||||
|
||||
const prodiaUrlRegex = /^(https?:\/\/images\.prodia\.\S+)$/i;
|
||||
|
||||
/**
|
||||
* Legacy heuristic for detecting images from "images.prodia." URLs.
|
||||
*/
|
||||
export function heuristicLegacyImageBlocks(fullText: string): ImageBlock[] | null {
|
||||
|
||||
// Check if all lines are URLs starting with "http://images.prodia." or "https://images.prodia."
|
||||
const imageBlocks: ImageBlock[] = [];
|
||||
for (const line of fullText.split('\n')) {
|
||||
const match = prodiaUrlRegex.exec(line);
|
||||
if (match) {
|
||||
const url = match[1];
|
||||
imageBlocks.push({ type: 'image', url });
|
||||
} else {
|
||||
// if there is any outlier line, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the image blocks if all lines are URLs from "images.prodia."
|
||||
return imageBlocks.length > 0 ? imageBlocks : null;
|
||||
}
|
||||
|
||||
|
||||
export const RenderImage = (props: { imageBlock: ImageBlock, isFirst: boolean, allowRunAgain: boolean, onRunAgain?: (e: React.MouseEvent) => void }) => {
|
||||
const { url, alt } = props.imageBlock;
|
||||
const imageUrls = url.split('\n');
|
||||
|
||||
return imageUrls.map((url, index) => {
|
||||
|
||||
// display a notice for temporary images DallE
|
||||
const isTempDalleUrl = url.startsWith('https://oaidalle');
|
||||
|
||||
return <Box
|
||||
key={'gen-img-' + index}
|
||||
sx={{
|
||||
display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', position: 'relative',
|
||||
mx: 1.5, mt: index > 0 ? 1.5 : 0,
|
||||
mx: 1.5, mb: 1.5, // mt: (index > 0 || !props.isFirst) ? 1.5 : 0,
|
||||
// p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1,
|
||||
minWidth: 64, minHeight: 64, boxShadow: 'lg',
|
||||
minWidth: 128, minHeight: 128,
|
||||
boxShadow: 'md',
|
||||
backgroundColor: 'neutral.solidBg',
|
||||
'& picture': { display: 'flex' },
|
||||
'& img': { maxWidth: '100%', maxHeight: '100%' },
|
||||
'&:hover > .overlay-buttons': { opacity: 1 },
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
|
||||
{/* External Image */}
|
||||
<picture><img src={url} alt='Generated Image' /></picture>
|
||||
{alt ? (
|
||||
<Tooltip
|
||||
variant='outlined' color='neutral'
|
||||
title={
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{isTempDalleUrl && <Alert variant='soft' color='warning' sx={{ flexDirection: 'column', alignItems: 'start' }}>
|
||||
<Typography level='title-sm'>⚠️ Temporary Image</Typography>
|
||||
<Typography level='body-sm'>
|
||||
This image will be deleted from the OpenAI servers in one hour. <b>Please save it to your device</b>.
|
||||
</Typography>
|
||||
{/*<Typography level='body-xs'>*/}
|
||||
{/* The following is the re-written DALL·E prompt that generated this image.*/}
|
||||
{/*</Typography>*/}
|
||||
</Alert>}
|
||||
<Typography level='title-sm' sx={{ p: 2 }}>
|
||||
{alt}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
placement='top-start'
|
||||
sx={{
|
||||
maxWidth: { sm: '90vw', md: '70vw' },
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
<picture><img src={url} alt={`Generated Image: ${alt}`} /></picture>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<picture><img src={url} alt='Generated Image' /></picture>
|
||||
)}
|
||||
|
||||
{/* Image Buttons */}
|
||||
<Box className='overlay-buttons' sx={{ ...overlayButtonsSx, pt: 0.5, px: 0.5, gap: 0.5 }}>
|
||||
{props.allowRunAgain && !!props.onRunAgain && (
|
||||
<Tooltip title='Draw again' variant='solid'>
|
||||
<IconButton variant='solid' color='neutral' onClick={props.onRunAgain}>
|
||||
<IconButton variant='solid' onClick={props.onRunAgain}>
|
||||
<ReplayIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton component={Link} href={url} target='_blank' variant='solid' color='neutral'>
|
||||
<ZoomOutMapIcon />
|
||||
</IconButton>
|
||||
<Tooltip title='Open in new tab'>
|
||||
<IconButton component={Link} href={url} download={alt || 'image'} target='_blank' variant='solid'>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
));
|
||||
</Box>;
|
||||
});
|
||||
};
|
||||
@@ -17,7 +17,6 @@ const RenderLatexDynamic = React.lazy(async () => {
|
||||
export const RenderLatex = ({ latexBlock, sx }: { latexBlock: LatexBlock; sx?: SxProps; }) =>
|
||||
<Box
|
||||
sx={{
|
||||
lineHeight: 1.75,
|
||||
mx: 1.5,
|
||||
...(sx || {}),
|
||||
}}>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, useTheme } from '@mui/joy';
|
||||
|
||||
import { TextBlock } from './blocks';
|
||||
import type { TextBlock } from './blocks';
|
||||
|
||||
|
||||
// Dynamically import ReactMarkdown using React.lazy
|
||||
const ReactMarkdown = React.lazy(async () => {
|
||||
const [markdownModule, remarkGfmModule] = await Promise.all([
|
||||
import('react-markdown'),
|
||||
import('remark-gfm')
|
||||
import('remark-gfm'),
|
||||
]);
|
||||
|
||||
// Pass the dynamically imported remarkGfm as children
|
||||
@@ -21,22 +22,26 @@ const ReactMarkdown = React.lazy(async () => {
|
||||
});
|
||||
|
||||
|
||||
export const RenderMarkdown = ({ textBlock }: { textBlock: TextBlock }) => {
|
||||
export const RenderMarkdown = (props: { textBlock: TextBlock, sx?: SxProps }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<Box
|
||||
className={`markdown-body ${theme.palette.mode === 'dark' ? 'markdown-body-dark' : 'markdown-body-light'}`}
|
||||
sx={{
|
||||
mx: '12px !important', // margin: 1.5 like other blocks
|
||||
'& table': { width: 'inherit !important' }, // un-break auto-width (tables have 'max-content', which overflows)
|
||||
mx: '0.75rem !important', // margin: 1.5 like other blocks
|
||||
'& table': {
|
||||
width: 'inherit !important', // un-break auto-width (tables have 'max-content', which overflows)
|
||||
},
|
||||
'--color-canvas-default': 'transparent !important', // remove the default background color
|
||||
fontFamily: `inherit !important`, // use the default font family
|
||||
lineHeight: '1.75 !important', // line-height: 1.75 like the text block
|
||||
// NOTE: the following are not needed because the CSS is under our control, and we
|
||||
// disabled the redefintions there
|
||||
// fontFamily: `inherit !important`, // use the default font family
|
||||
...(props.sx || {}),
|
||||
}}>
|
||||
|
||||
{/* Using React.Suspense / React.Lazy loading this */}
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<ReactMarkdown>{textBlock.content}</ReactMarkdown>
|
||||
<ReactMarkdown>{props.textBlock.content}</ReactMarkdown>
|
||||
</React.Suspense>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Diff as TextDiff } from '@sanity/diff-match-patch';
|
||||
|
||||
import { heuristicIsHtml } from './RenderHtml';
|
||||
import { heuristicMarkdownImageReferenceBlocks, heuristicLegacyImageBlocks } from './RenderImage';
|
||||
|
||||
type Block = CodeBlock | DiffBlock | HtmlBlock | ImageBlock | LatexBlock | TextBlock;
|
||||
export type CodeBlock = { type: 'code'; blockTitle: string; blockCode: string; complete: boolean; };
|
||||
export type DiffBlock = { type: 'diff'; textDiffs: TextDiff[] };
|
||||
export type HtmlBlock = { type: 'html'; html: string; };
|
||||
export type ImageBlock = { type: 'image'; url: string; };
|
||||
export type ImageBlock = { type: 'image'; url: string; alt?: string }; // Added optional alt property
|
||||
export type LatexBlock = { type: 'latex'; latex: string; };
|
||||
export type TextBlock = { type: 'text'; content: string; }; // for Text or Markdown
|
||||
|
||||
@@ -21,9 +22,18 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
|
||||
if (heuristicIsHtml(text))
|
||||
return [{ type: 'html', html: text }];
|
||||
|
||||
// special case: markdown image references (e.g. )
|
||||
const mdImageBlocks = heuristicMarkdownImageReferenceBlocks(text);
|
||||
if (mdImageBlocks)
|
||||
return mdImageBlocks;
|
||||
|
||||
// special case: legacy prodia images
|
||||
const legacyImageBlocks = heuristicLegacyImageBlocks(text);
|
||||
if (legacyImageBlocks)
|
||||
return legacyImageBlocks;
|
||||
|
||||
const regexPatterns = {
|
||||
codeBlock: /`{3,}([\w\\.+-_]+)?\n([\s\S]*?)(`{3,}\n?|$)/g,
|
||||
imageBlock: /(https:\/\/images\.prodia\.xyz\/.*?\.png)/g, // NOTE: only Prodia for now - but this shall be expanded to markdown images  or any png/jpeg
|
||||
latexBlock: /\$\$([\s\S]*?)\$\$/g,
|
||||
// latexBlockOrInline: /\$\$([\s\S]*?)\$\$|\$([^$]*?)\$/g,
|
||||
};
|
||||
@@ -61,11 +71,6 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
|
||||
blocks.push({ type: 'code', blockTitle, blockCode, complete: blockEnd.startsWith('```') });
|
||||
break;
|
||||
|
||||
case 'imageBlock':
|
||||
const url: string = match[1];
|
||||
blocks.push({ type: 'image', url });
|
||||
break;
|
||||
|
||||
case 'latexBlock':
|
||||
const latex: string = match[1];
|
||||
blocks.push({ type: 'latex', latex });
|
||||
|
||||
+56
-22
@@ -33,9 +33,9 @@ interface AppChatPanesStore {
|
||||
openConversationInFocusedPane: (conversationId: DConversationId) => void;
|
||||
openConversationInSplitPane: (conversationId: DConversationId) => void;
|
||||
navigateHistoryInFocusedPane: (direction: 'back' | 'forward') => boolean;
|
||||
setFocusedPaneIndex: (paneIndex: number) => void;
|
||||
splitChatPane: (numberOfPanes: number) => void;
|
||||
unsplitChatPane: (paneIndexToKeep: number) => void;
|
||||
duplicatePane: (paneIndex: number) => void;
|
||||
removePane: (paneIndex: number) => void;
|
||||
setFocusedPane: (paneIndex: number) => void;
|
||||
onConversationsChanged: (conversationIds: DConversationId[]) => void;
|
||||
|
||||
}
|
||||
@@ -160,7 +160,52 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
return true;
|
||||
},
|
||||
|
||||
setFocusedPaneIndex: (paneIndex: number) =>
|
||||
duplicatePane: (paneIndex: number) =>
|
||||
_set(state => {
|
||||
const { chatPanes } = state;
|
||||
|
||||
// Validate index
|
||||
if (paneIndex < 0 || paneIndex >= chatPanes.length) {
|
||||
console.warn('Attempted to duplicate a pane with an out-of-range index:', paneIndex);
|
||||
return state; // Return the existing state without changes
|
||||
}
|
||||
|
||||
// Clone the pane at the specified index, including a deep copy of the history array
|
||||
const paneToDuplicate = chatPanes[paneIndex];
|
||||
const duplicatedPane = {
|
||||
...paneToDuplicate,
|
||||
history: [...paneToDuplicate.history], // Deep copy of the history array
|
||||
};
|
||||
|
||||
// Insert the duplicated pane into the array, right after the original pane
|
||||
const newPanes = [
|
||||
...chatPanes.slice(0, paneIndex + 1),
|
||||
duplicatedPane,
|
||||
...chatPanes.slice(paneIndex + 1),
|
||||
];
|
||||
|
||||
return {
|
||||
chatPanes: newPanes,
|
||||
chatPaneFocusIndex: paneIndex + 1,
|
||||
};
|
||||
}),
|
||||
|
||||
removePane: (paneIndex: number) =>
|
||||
_set(state => {
|
||||
const { chatPanes } = state;
|
||||
if (paneIndex < 0 || paneIndex >= chatPanes.length)
|
||||
return state;
|
||||
|
||||
const newPanes = chatPanes.toSpliced(paneIndex, 1);
|
||||
|
||||
// when a pane is removed, focus the pane 0, or null if no panes remain
|
||||
return {
|
||||
chatPanes: newPanes,
|
||||
chatPaneFocusIndex: newPanes.length ? 0 : null,
|
||||
};
|
||||
}),
|
||||
|
||||
setFocusedPane: (paneIndex: number) =>
|
||||
_set(state => {
|
||||
if (state.chatPaneFocusIndex === paneIndex)
|
||||
return state;
|
||||
@@ -169,22 +214,6 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
};
|
||||
}),
|
||||
|
||||
splitChatPane: (numberOfPanes: number) => {
|
||||
const { chatPanes, chatPaneFocusIndex } = _get();
|
||||
const focusedPane = (chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex] : null) ?? createPane();
|
||||
|
||||
_set({
|
||||
chatPanes: Array.from({ length: numberOfPanes }, () => ({ ...focusedPane })),
|
||||
chatPaneFocusIndex: 0,
|
||||
});
|
||||
},
|
||||
|
||||
unsplitChatPane: (paneIndexToKeep: number) =>
|
||||
_set(state => ({
|
||||
chatPanes: [state.chatPanes[paneIndexToKeep] || createPane()],
|
||||
chatPaneFocusIndex: 0,
|
||||
})),
|
||||
|
||||
|
||||
/**
|
||||
* This function is vital, as is invoked when the conversationId[] changes in the global chats store.
|
||||
@@ -258,7 +287,9 @@ export function usePanesManager() {
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
setFocusedPaneIndex,
|
||||
duplicatePane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
} = state;
|
||||
const focusedConversationId = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex]?.conversationId ?? null : null;
|
||||
return {
|
||||
@@ -268,7 +299,10 @@ export function usePanesManager() {
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
setFocusedPaneIndex,
|
||||
paneIndex: chatPaneFocusIndex,
|
||||
duplicatePane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
@@ -3,19 +3,21 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, Button, Checkbox, Grid, IconButton, Input, Stack, Textarea, Typography } from '@mui/joy';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import ScienceIcon from '@mui/icons-material/Science';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { lineHeightTextarea } from '~/common/app.theme';
|
||||
import { navigateToPersonas } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
import { usePurposeStore } from './store-purposes';
|
||||
|
||||
|
||||
// 'special' purpose IDs, for tile hiding purposes
|
||||
const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__';
|
||||
|
||||
// Constants for tile sizes / grid width - breakpoints need to be computed here to work around
|
||||
// the "flex box cannot shrink over wrapped content" issue
|
||||
//
|
||||
@@ -47,7 +49,6 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
|
||||
// external state
|
||||
const showFinder = useUIPreferencesStore(state => state.showPurposeFinder);
|
||||
const labsPersonaYTCreator = useUXLabsStore(state => state.labsPersonaYTCreator);
|
||||
const { systemPurposeId, setSystemPurposeId } = useChatStore(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
|
||||
return {
|
||||
@@ -113,6 +114,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
const unfilteredPurposeIDs = (filteredIDs && showFinder) ? filteredIDs : Object.keys(SystemPurposes);
|
||||
const purposeIDs = editMode ? unfilteredPurposeIDs : unfilteredPurposeIDs.filter(id => !hiddenPurposeIDs.includes(id));
|
||||
|
||||
const hidePersonaCreator = hiddenPurposeIDs.includes(PURPOSE_ID_PERSONA_CREATOR);
|
||||
|
||||
const selectedPurpose = purposeIDs.length ? (SystemPurposes[systemPurposeId] ?? null) : null;
|
||||
const selectedExample = selectedPurpose?.examples && getRandomElement(selectedPurpose.examples) || null;
|
||||
|
||||
@@ -127,7 +130,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
placeholder='Search for purpose…'
|
||||
startDecorator={<SearchIcon />}
|
||||
endDecorator={searchQuery && (
|
||||
<IconButton variant='plain' color='neutral' onClick={handleSearchClear}>
|
||||
<IconButton onClick={handleSearchClear}>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
@@ -156,10 +159,14 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
<Button
|
||||
variant={(!editMode && systemPurposeId === spId) ? 'solid' : 'soft'}
|
||||
color={(!editMode && systemPurposeId === spId) ? 'primary' : SystemPurposes[spId as SystemPurposeId]?.highlighted ? 'warning' : 'neutral'}
|
||||
onClick={() => !editMode && handlePurposeChanged(spId as SystemPurposeId)}
|
||||
onClick={() => editMode
|
||||
? toggleHiddenPurposeId(spId)
|
||||
: handlePurposeChanged(spId as SystemPurposeId)
|
||||
}
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
fontWeight: 500,
|
||||
// paddingInline: 1,
|
||||
gap: bpTileGap,
|
||||
height: bpTileSize,
|
||||
width: bpTileSize,
|
||||
@@ -171,9 +178,10 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
>
|
||||
{editMode && (
|
||||
<Checkbox
|
||||
label={<Typography level='body-sm'>show</Typography>}
|
||||
checked={!hiddenPurposeIDs.includes(spId)} onChange={() => toggleHiddenPurposeId(spId)}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
color='neutral'
|
||||
checked={!hiddenPurposeIDs.includes(spId)}
|
||||
// label={<Typography level='body-xs'>show</Typography>}
|
||||
sx={{ position: 'absolute', left: 8, top: 8 }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ fontSize: '2rem' }}>
|
||||
@@ -185,28 +193,43 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
</Button>
|
||||
</Grid>
|
||||
))}
|
||||
{/* Button to start the YouTube persona creator */}
|
||||
{labsPersonaYTCreator && <Grid>
|
||||
{/* Button to start the Persona Creator */}
|
||||
{(editMode || !hidePersonaCreator) && <Grid>
|
||||
<Button
|
||||
variant='soft' color='neutral'
|
||||
component={Link} noLinkStyle href='/personas'
|
||||
onClick={() => editMode
|
||||
? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR)
|
||||
: void navigateToPersonas()
|
||||
}
|
||||
sx={{
|
||||
'--Icon-fontSize': '2rem',
|
||||
flexDirection: 'column',
|
||||
fontWeight: 500,
|
||||
// gap: bpTileGap,
|
||||
// paddingInline: 1,
|
||||
gap: bpTileGap,
|
||||
height: bpTileSize,
|
||||
width: bpTileSize,
|
||||
border: `1px dashed`,
|
||||
boxShadow: 'md',
|
||||
backgroundColor: 'background.surface',
|
||||
// border: `1px dashed`,
|
||||
// borderColor: 'neutral.softActiveBg',
|
||||
boxShadow: 'xs',
|
||||
backgroundColor: 'neutral.softDisabledBg',
|
||||
}}
|
||||
>
|
||||
{editMode && (
|
||||
<Checkbox
|
||||
color='neutral'
|
||||
checked={!hidePersonaCreator}
|
||||
// label={<Typography level='body-xs'>show</Typography>}
|
||||
sx={{ position: 'absolute', left: 8, top: 8 }}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<ScienceIcon />
|
||||
<div style={{ fontSize: '2rem' }}>
|
||||
🎭
|
||||
</div>
|
||||
{/*<SettingsAccessibilityIcon style={{ opacity: 0.5 }} />*/}
|
||||
</div>
|
||||
<div>
|
||||
YouTube persona creator
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Persona Creator
|
||||
</div>
|
||||
</Button>
|
||||
</Grid>}
|
||||
@@ -225,7 +248,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
? <>
|
||||
Example: {selectedExample}
|
||||
<IconButton
|
||||
variant='plain' color='primary' size='md'
|
||||
color='primary'
|
||||
onClick={() => props.runExample(selectedExample)}
|
||||
sx={{ opacity: 0, transition: 'opacity 0.3s' }}
|
||||
>
|
||||
@@ -246,7 +269,7 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
lineHeight: 1.75,
|
||||
lineHeight: lineHeightTextarea,
|
||||
mt: 1,
|
||||
}} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 Enrico Ros
|
||||
*
|
||||
* This subsystem is responsible for 'snap-to-bottom' and 'scroll-to-bottom' features,
|
||||
* with an animated, gradual scroll.
|
||||
*
|
||||
* See the `ScrollToBottomButton` component for the button that triggers the scroll.
|
||||
*
|
||||
* Example usage:
|
||||
* <ScrollToBottom bootToBottom stickToBottom sx={{ overflowY: 'auto', height: '100%' }}>
|
||||
* <LongMessagesList />
|
||||
* <ScrollToBottomButton />
|
||||
* </ScrollToBottom>
|
||||
*
|
||||
* Within the Context (children components), functions are made available by using:
|
||||
* const { notifyBooting, setStickToBottom } = useScrollToBottom();
|
||||
*
|
||||
*/
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box } from '@mui/joy';
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import { isBrowser } from '~/common/util/pwaUtils';
|
||||
|
||||
import { ScrollToBottomState, UseScrollToBottomProvider } from './useScrollToBottom';
|
||||
|
||||
|
||||
// set this to true to debug this component
|
||||
const DEBUG_SCROLL_TO_BOTTOM = false;
|
||||
|
||||
// NOTE: in Chrome a wheel scroll event is 100px
|
||||
// If you make this too small, the button may show when jumping lines on mobile
|
||||
// if you make it too large, the user would need a very large flick to unlock the view
|
||||
const USER_STICKY_MARGIN = 60;
|
||||
|
||||
// during the 'booting' timeout, scrolls happen instantly instead of smoothly
|
||||
const BOOTING_TIMEOUT = 400;
|
||||
|
||||
|
||||
function DebugBorderBox(props: { heightPx: number, color: string }) {
|
||||
return (
|
||||
<Box sx={{
|
||||
position: 'absolute', bottom: 0, right: 0, left: 0,
|
||||
height: `${props.heightPx}px`,
|
||||
border: `1px solid ${props.color}`,
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function ScrollToBottom(props: {
|
||||
bootToBottom?: boolean
|
||||
bootSmoothly?: boolean
|
||||
stickToBottom?: boolean
|
||||
sx?: SxProps
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
|
||||
// state
|
||||
|
||||
const [state, setState] = React.useState<ScrollToBottomState>({
|
||||
stickToBottom: props.stickToBottom || false,
|
||||
booting: props.bootToBottom || false,
|
||||
atBottom: undefined,
|
||||
});
|
||||
|
||||
// track scrollable (for events and to scroll it)
|
||||
const scrollableElementRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// track programmatic scrolls
|
||||
const isProgrammaticScroll = React.useRef(false);
|
||||
|
||||
|
||||
// derived state
|
||||
|
||||
const bootToBottom = props.bootToBottom || false;
|
||||
const scrollBehavior: ScrollBehavior = (state.booting && !props.bootSmoothly) ? 'auto' : 'smooth';
|
||||
|
||||
|
||||
// [Debugging]
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log('ScrollToBottom', { ...state });
|
||||
|
||||
|
||||
// main programmatic scroll to bottom function
|
||||
|
||||
const doScrollToBottom = React.useCallback(() => {
|
||||
const scrollable = scrollableElementRef.current;
|
||||
if (scrollable) {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log(' -> doScrollToBottom()', { scrollHeight: scrollable.scrollHeight, offsetHeight: scrollable.offsetHeight });
|
||||
|
||||
// eat the next scroll event
|
||||
isProgrammaticScroll.current = true;
|
||||
|
||||
// smooth scrolling only after booting
|
||||
scrollable.scrollTo({ top: scrollable.scrollHeight, behavior: scrollBehavior });
|
||||
}
|
||||
}, [scrollBehavior]);
|
||||
|
||||
|
||||
/**
|
||||
* Booting state reset (after BOOTING_TIMEOUT ms)
|
||||
* - the "Booting" window will scroll instantly instead of smoothly
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (!state.booting || !isBrowser) return;
|
||||
|
||||
const _clearBootingHandler = () => {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log(' -> booting done');
|
||||
|
||||
setState(state => ({ ...state, booting: false }));
|
||||
|
||||
if (bootToBottom)
|
||||
doScrollToBottom();
|
||||
};
|
||||
|
||||
// cancelable listener
|
||||
const timeout = window.setTimeout(_clearBootingHandler, BOOTING_TIMEOUT);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [bootToBottom, doScrollToBottom, state.booting]);
|
||||
|
||||
/**
|
||||
* Children elements resize event listener
|
||||
* - note that the 'scrollable' will likely have a fixed size, while its children are the ones who become scrollable
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
const scrollable = scrollableElementRef.current;
|
||||
if (!scrollable) return;
|
||||
|
||||
const _containerResizeObserver = new ResizeObserver(entries => {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log(' -> scrollable children resized', entries.length);
|
||||
|
||||
// Edge case: when the content is smaller, we need to reset the bottom state (#312)
|
||||
const atTop = scrollable.scrollTop == 0;
|
||||
const unScrollable = scrollable.scrollHeight <= scrollable.offsetHeight;
|
||||
if (unScrollable && atTop) {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log(' -> large enough window', entries.length);
|
||||
|
||||
// udpate state only if this changed
|
||||
setState(state => (state.atBottom !== true)
|
||||
? ({ ...state, atBottom: true })
|
||||
: state,
|
||||
);
|
||||
}
|
||||
|
||||
if (entries.length > 0 && state.stickToBottom)
|
||||
doScrollToBottom();
|
||||
});
|
||||
|
||||
// cancelable observer of resize of scrollable's children elements
|
||||
Array.from(scrollable.children).forEach(child => _containerResizeObserver.observe(child));
|
||||
return () => _containerResizeObserver.disconnect();
|
||||
|
||||
}, [state.stickToBottom, doScrollToBottom]);
|
||||
|
||||
/**
|
||||
* (User) Scroll events listener
|
||||
* - will cancel any state.stickToBottom, if the user dragged the scroll bar
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (state.booting) return;
|
||||
|
||||
const scrollable = scrollableElementRef.current;
|
||||
if (!scrollable) return;
|
||||
|
||||
const _scrollEventsListener = () => {
|
||||
// ignore scroll events during programmatic scrolls
|
||||
// NOTE: some will go through, but somewhat the framework is stable
|
||||
if (isProgrammaticScroll.current) {
|
||||
isProgrammaticScroll.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// compute intersections
|
||||
const atBottom = scrollable.scrollHeight - scrollable.scrollTop <= scrollable.offsetHeight + USER_STICKY_MARGIN;
|
||||
|
||||
// assume this is = to the user intention
|
||||
const stickToBottom = atBottom;
|
||||
|
||||
// update state only if anything changed
|
||||
setState(state => (state.stickToBottom !== stickToBottom || state.atBottom !== atBottom)
|
||||
? ({ ...state, stickToBottom, atBottom })
|
||||
: state,
|
||||
);
|
||||
};
|
||||
|
||||
// _scrollEventsListener(true);
|
||||
|
||||
// cancelable listener (user and programatic scroll events)
|
||||
scrollable.addEventListener('scroll', _scrollEventsListener);
|
||||
return () => scrollable.removeEventListener('scroll', _scrollEventsListener);
|
||||
|
||||
}, [state.booting]);
|
||||
|
||||
|
||||
// actions for this context
|
||||
|
||||
const notifyBooting = React.useCallback(() => {
|
||||
if (bootToBottom)
|
||||
setState(state => state.booting ? state : ({ ...state, booting: true }));
|
||||
}, [bootToBottom]);
|
||||
|
||||
/*const notifyContentUpdated = React.useCallback(() => {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log('-= notifyContentUpdated');
|
||||
|
||||
if (state.stickToBottom)
|
||||
doScrollToBottom();
|
||||
}, [doScrollToBottom, state.stickToBottom]);*/
|
||||
|
||||
const setStickToBottom = React.useCallback((stickToBottom: boolean) => {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log('-= setStickToBottom', stickToBottom);
|
||||
|
||||
setState(state => state.stickToBottom !== stickToBottom
|
||||
? ({ ...state, stickToBottom })
|
||||
: state,
|
||||
);
|
||||
|
||||
if (stickToBottom)
|
||||
doScrollToBottom();
|
||||
}, [doScrollToBottom]);
|
||||
|
||||
|
||||
return (
|
||||
<UseScrollToBottomProvider value={{
|
||||
...state,
|
||||
notifyBooting,
|
||||
setStickToBottom,
|
||||
}}>
|
||||
<Box ref={scrollableElementRef} sx={props.sx}>
|
||||
{props.children}
|
||||
{DEBUG_SCROLL_TO_BOTTOM && <DebugBorderBox heightPx={USER_STICKY_MARGIN} color='red' />}
|
||||
{DEBUG_SCROLL_TO_BOTTOM && <DebugBorderBox heightPx={100} color='blue' />}
|
||||
</Box>
|
||||
</UseScrollToBottomProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { IconButton } from '@mui/joy';
|
||||
import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown';
|
||||
|
||||
import { useScrollToBottom } from './useScrollToBottom';
|
||||
|
||||
|
||||
export function ScrollToBottomButton() {
|
||||
|
||||
// state
|
||||
const { atBottom, stickToBottom, setStickToBottom } = useScrollToBottom();
|
||||
|
||||
const handleStickToBottom = React.useCallback(() => {
|
||||
setStickToBottom(true);
|
||||
}, [setStickToBottom]);
|
||||
|
||||
// do not render the button at all if we're already snapping
|
||||
if (atBottom || stickToBottom)
|
||||
return null;
|
||||
|
||||
return (
|
||||
// <Tooltip title={
|
||||
// <Typography variant='solid' level='title-sm' sx={{ px: 1 }}>
|
||||
// Scroll to bottom
|
||||
// </Typography>
|
||||
// }>
|
||||
<IconButton
|
||||
variant='outlined'
|
||||
onClick={handleStickToBottom}
|
||||
sx={{
|
||||
// place this on the bottom-right corner (FAB-like)
|
||||
position: 'absolute',
|
||||
bottom: '2rem',
|
||||
right: {
|
||||
xs: '1rem',
|
||||
md: '2rem',
|
||||
},
|
||||
|
||||
// style it
|
||||
backgroundColor: 'background.surface',
|
||||
borderRadius: '50%',
|
||||
boxShadow: 'md',
|
||||
|
||||
// fade it in when hovering
|
||||
// transition: 'all 0.15s',
|
||||
// '&:hover': {
|
||||
// transform: 'scale(1.1)',
|
||||
// },
|
||||
}}
|
||||
>
|
||||
<KeyboardDoubleArrowDownIcon />
|
||||
</IconButton>
|
||||
// </Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* State is minimal - to keep state machinery stable and simple
|
||||
*/
|
||||
export interface ScrollToBottomState {
|
||||
// config
|
||||
stickToBottom: boolean;
|
||||
|
||||
// state
|
||||
booting: boolean;
|
||||
atBottom: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions are very simplified, for providing a minimal control surface from the outside
|
||||
*/
|
||||
export interface ScrollToBottomActions {
|
||||
notifyBooting: () => void;
|
||||
setStickToBottom: (stick: boolean) => void;
|
||||
}
|
||||
|
||||
type ScrollToBottomContext = ScrollToBottomState & ScrollToBottomActions;
|
||||
|
||||
const UseScrollToBottom = React.createContext<ScrollToBottomContext | undefined>(undefined);
|
||||
|
||||
export const UseScrollToBottomProvider = UseScrollToBottom.Provider;
|
||||
|
||||
export const useScrollToBottom = (): ScrollToBottomContext => {
|
||||
const context = React.useContext(UseScrollToBottom);
|
||||
if (!context)
|
||||
throw new Error('useScrollToBottom must be used within a ScrollToBottomProvider');
|
||||
return context;
|
||||
};
|
||||
@@ -2,8 +2,8 @@ import { DLLMId } from '~/modules/llms/store-llms';
|
||||
import { SystemPurposeId } from '../../../data';
|
||||
import { autoSuggestions } from '~/modules/aifn/autosuggestions/autoSuggestions';
|
||||
import { autoTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
import { llmStreamingChatGenerate } from '~/modules/llms/llm.client';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { streamChat } from '~/modules/llms/transports/streamChat';
|
||||
|
||||
import { DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
@@ -63,7 +63,7 @@ async function streamAssistantMessage(
|
||||
const messages = history.map(({ role, text }) => ({ role, content: text }));
|
||||
|
||||
try {
|
||||
await streamChat(llmId, messages, abortSignal,
|
||||
await llmStreamingChatGenerate(llmId, messages, null, null, abortSignal,
|
||||
(updatedMessage: Partial<DMessage>) => {
|
||||
// update the message in the store (and thus schedule a re-render)
|
||||
editMessage(updatedMessage);
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { CmdRunBrowse } from '~/modules/browse/browse.client';
|
||||
import { CmdRunProdia } from '~/modules/prodia/prodia.client';
|
||||
import { CmdRunReact } from '~/modules/aifn/react/react';
|
||||
import { CmdRunSearch } from '~/modules/google/search.client';
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { createDMessage, DMessage } from '~/common/state/store-chats';
|
||||
|
||||
|
||||
export const CmdAddRoleMessage: string[] = ['/assistant', '/a', '/system', '/s'];
|
||||
|
||||
export const CmdHelp: string[] = ['/help', '/h', '/?'];
|
||||
|
||||
export const commands = [...CmdRunBrowse, ...CmdRunProdia, ...CmdRunReact, ...CmdRunSearch, ...CmdAddRoleMessage, ...CmdHelp];
|
||||
|
||||
export interface SentencePiece {
|
||||
type: 'text' | 'cmd';
|
||||
value: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sentence to pieces (must have a leading slash) from the provided text
|
||||
* Used by rendering functions, as well as input processing functions.
|
||||
*/
|
||||
export function extractCommands(input: string): SentencePiece[] {
|
||||
// 'help' commands are the only without a space and text after
|
||||
if (CmdHelp.includes(input))
|
||||
return [{ type: 'cmd', value: input }, { type: 'text', value: '' }];
|
||||
const regexFromTags = commands.map(tag => `^\\${tag} `).join('\\b|') + '\\b';
|
||||
const pattern = new RegExp(regexFromTags, 'g');
|
||||
const result: SentencePiece[] = [];
|
||||
let lastIndex = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = pattern.exec(input)) !== null) {
|
||||
if (match.index !== lastIndex)
|
||||
result.push({ type: 'text', value: input.substring(lastIndex, match.index) });
|
||||
result.push({ type: 'cmd', value: match[0].trim() });
|
||||
lastIndex = pattern.lastIndex;
|
||||
|
||||
// Remove the space after the matched tag
|
||||
if (input[lastIndex] === ' ')
|
||||
lastIndex++;
|
||||
}
|
||||
|
||||
if (lastIndex !== input.length)
|
||||
result.push({ type: 'text', value: input.substring(lastIndex) });
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createCommandsHelpMessage(): DMessage {
|
||||
let text = 'Available Chat Commands:\n';
|
||||
text += commands.map(c => ` - ${c}`).join('\n');
|
||||
const helpMessage = createDMessage('assistant', text);
|
||||
helpMessage.originLLM = Brand.Title.Base;
|
||||
return helpMessage;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { SystemPurposeId, SystemPurposes } from '../../../data';
|
||||
import { createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
|
||||
export function createAssistantTypingMessage(conversationId: string, assistantLlmLabel: DLLMId | 'prodia' | 'react-...' | 'web', assistantPurposeId: SystemPurposeId | undefined, text: string): string {
|
||||
export function createAssistantTypingMessage(conversationId: string, assistantLlmLabel: DLLMId | string /* 'DALL·E' | 'Prodia' | 'react-...' | 'web' */, assistantPurposeId: SystemPurposeId | undefined, text: string): string {
|
||||
const assistantMessage: DMessage = createDMessage('assistant', text);
|
||||
assistantMessage.typing = true;
|
||||
assistantMessage.purposeId = assistantPurposeId;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { prodiaGenerateImage } from '~/modules/prodia/prodia.client';
|
||||
import { getActiveTextToImageProviderOrThrow, t2iGenerateImageOrThrow } from '~/modules/t2i/t2i.client';
|
||||
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createAssistantTypingMessage } from './editors';
|
||||
|
||||
|
||||
/**
|
||||
* The main 'image generation' function - for now specialized to the 'imagine' command.
|
||||
* Text to image, appended as an 'assistant' message
|
||||
*/
|
||||
export async function runImageGenerationUpdatingState(conversationId: string, imageText: string) {
|
||||
|
||||
@@ -17,21 +17,23 @@ export async function runImageGenerationUpdatingState(conversationId: string, im
|
||||
imageText = imageText.replace(/x(\d+)$|\[(\d+)]$/, '').trim(); // Remove the "xN" or "[N]" part from the imageText
|
||||
|
||||
// create a blank and 'typing' message for the assistant
|
||||
const assistantMessageId = createAssistantTypingMessage(conversationId, 'prodia', undefined,
|
||||
const assistantMessageId = createAssistantTypingMessage(conversationId, '', undefined,
|
||||
`Give me a few seconds while I draw ${imageText?.length > 20 ? 'that' : '"' + imageText + '"'}...`);
|
||||
|
||||
// reference the state editing functions
|
||||
const { editMessage } = useChatStore.getState();
|
||||
|
||||
try {
|
||||
const imageUrls = await prodiaGenerateImage(count, imageText);
|
||||
|
||||
// Concatenate all the resulting URLs and update the assistant message with these URLs
|
||||
const allImageUrls = imageUrls.join('\n');
|
||||
editMessage(conversationId, assistantMessageId, { text: allImageUrls, typing: false }, false);
|
||||
const t2iProvider = getActiveTextToImageProviderOrThrow();
|
||||
editMessage(conversationId, assistantMessageId, { originLLM: t2iProvider.painter }, false);
|
||||
|
||||
const imageUrls = await t2iGenerateImageOrThrow(t2iProvider, imageText, count);
|
||||
editMessage(conversationId, assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
|
||||
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || error?.toString() || 'Unknown error';
|
||||
editMessage(conversationId, assistantMessageId, { text: `Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
if (assistantMessageId)
|
||||
editMessage(conversationId, assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -13,9 +13,10 @@ import { LogoProgress } from '~/common/components/LogoProgress';
|
||||
import { apiAsyncNode } from '~/common/util/trpc.client';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { conversationTitle } from '~/common/state/store-chats';
|
||||
import { useLayoutPluggable } from '~/common/layout/store-applayout';
|
||||
import { themeBgAppDarker } from '~/common/app.theme';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { AppChatLinkDrawerItems } from './AppChatLinkDrawerItems';
|
||||
import { AppChatLinkDrawerContent } from './AppChatLinkDrawerContent';
|
||||
import { AppChatLinkMenuItems } from './AppChatLinkMenuItems';
|
||||
import { ViewChatLink } from './ViewChatLink';
|
||||
|
||||
@@ -30,7 +31,7 @@ const Centerer = (props: { backgroundColor: string, children?: React.ReactNode }
|
||||
</Box>;
|
||||
|
||||
const ShowLoading = () =>
|
||||
<Centerer backgroundColor='background.level3'>
|
||||
<Centerer backgroundColor={themeBgAppDarker}>
|
||||
<LogoProgress showProgress={true} />
|
||||
<Typography level='title-sm' sx={{ mt: 2 }}>
|
||||
Loading Chat...
|
||||
@@ -38,7 +39,7 @@ const ShowLoading = () =>
|
||||
</Centerer>;
|
||||
|
||||
const ShowError = (props: { error: any }) =>
|
||||
<Centerer backgroundColor='background.level2'>
|
||||
<Centerer backgroundColor={themeBgAppDarker}>
|
||||
<InlineError error={props.error} severity='warning' />
|
||||
</Centerer>;
|
||||
|
||||
@@ -83,9 +84,9 @@ export function AppChatLink(props: { linkId: string }) {
|
||||
|
||||
// pluggable UI
|
||||
|
||||
const drawerItems = React.useMemo(() => <AppChatLinkDrawerItems />, []);
|
||||
const drawerContent = React.useMemo(() => <AppChatLinkDrawerContent />, []);
|
||||
const menuItems = React.useMemo(() => <AppChatLinkMenuItems />, []);
|
||||
useLayoutPluggable(null, hasLinkItems ? drawerItems : null, menuItems);
|
||||
usePluggableOptimaLayout(hasLinkItems ? drawerContent : null, null, menuItems, 'AppChatLink');
|
||||
|
||||
|
||||
const pageTitle = (data?.conversation && conversationTitle(data.conversation)) || 'Chat Link';
|
||||
@@ -102,7 +103,7 @@ export function AppChatLink(props: { linkId: string }) {
|
||||
? <ShowError error={error} />
|
||||
: !!data?.conversation
|
||||
? <ViewChatLink conversation={data.conversation} storedAt={data.storedAt} expiresAt={data.expiresAt} />
|
||||
: <Centerer backgroundColor='background.level3' />}
|
||||
: <Centerer backgroundColor={themeBgAppDarker} />}
|
||||
|
||||
</>;
|
||||
}
|
||||
+7
-5
@@ -8,26 +8,28 @@ import { useChatLinkItems } from '~/modules/trade/store-module-trade';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { closeLayoutDrawer } from '~/common/layout/store-applayout';
|
||||
import { getChatLinkRelativePath, ROUTE_INDEX } from '~/common/app.routes';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
import { PageDrawerList } from '~/common/layout/optima/components/PageDrawerList';
|
||||
|
||||
|
||||
/**
|
||||
* Drawer Items are all the links already shared, for quick access.
|
||||
* This is stores in the Trade Store (local storage).
|
||||
*/
|
||||
export function AppChatLinkDrawerItems() {
|
||||
export function AppChatLinkDrawerContent() {
|
||||
|
||||
// external state
|
||||
const { closeDrawerOnMobile } = useOptimaDrawers();
|
||||
const chatLinkItems = useChatLinkItems()
|
||||
.slice()
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
const notEmpty = chatLinkItems.length > 0;
|
||||
|
||||
return <>
|
||||
return <PageDrawerList>
|
||||
|
||||
<MenuItem
|
||||
onClick={closeLayoutDrawer}
|
||||
onClick={closeDrawerOnMobile}
|
||||
component={Link} href={ROUTE_INDEX} noLinkStyle
|
||||
>
|
||||
<ListItemDecorator><ArrowBackIcon /></ListItemDecorator>
|
||||
@@ -63,6 +65,6 @@ export function AppChatLinkDrawerItems() {
|
||||
|
||||
))}
|
||||
</Box>}
|
||||
</>;
|
||||
</PageDrawerList>;
|
||||
|
||||
}
|
||||
@@ -5,11 +5,13 @@ import { Box, Button, Card, List, ListItem, Tooltip, Typography } from '@mui/joy
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { ChatMessage } from '../chat/components/message/ChatMessage';
|
||||
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
|
||||
import { useChatShowSystemMessages } from '../chat/store-app-chat';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { navigateToChat } from '~/common/app.routes';
|
||||
import { launchAppChat } from '~/common/app.routes';
|
||||
import { themeBgAppDarker } from '~/common/app.theme';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
@@ -58,7 +60,7 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
|
||||
const handleClone = async (canOverwrite: boolean) => {
|
||||
setCloning(true);
|
||||
const importedId = useChatStore.getState().importConversation({ ...props.conversation }, !canOverwrite);
|
||||
await navigateToChat(importedId);
|
||||
await launchAppChat(importedId);
|
||||
setCloning(false);
|
||||
};
|
||||
|
||||
@@ -67,7 +69,7 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level3',
|
||||
backgroundColor: themeBgAppDarker,
|
||||
display: 'flex', flexFlow: 'column nowrap', minHeight: 96, alignItems: 'center',
|
||||
gap: { xs: 4, md: 5, xl: 6 },
|
||||
px: { xs: 2 },
|
||||
@@ -96,46 +98,59 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
|
||||
<Card sx={{
|
||||
borderRadius: 'xl', boxShadow: 'md',
|
||||
maxWidth: '100%', // fixes the card growing out of bounds
|
||||
overflowY: 'auto',
|
||||
overflowY: 'hidden',
|
||||
p: 0,
|
||||
}}>
|
||||
|
||||
<List sx={{
|
||||
p: 0,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
<ScrollToBottom
|
||||
bootToBottom bootSmoothly
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
|
||||
<ListItem sx={{
|
||||
// backgroundColor: 'background.surface',
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
borderBottomStyle: 'dashed',
|
||||
// justifyContent: 'center',
|
||||
px: { xs: 2.5, md: 3.5 }, py: 2,
|
||||
<List sx={{
|
||||
p: 0,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
<Typography level='body-md'>
|
||||
Welcome to the chat! 👋
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
{filteredMessages.map((message, idx) =>
|
||||
<ChatMessage
|
||||
key={'msg-' + message.id} message={message}
|
||||
showDate={idx === 0 || idx === filteredMessages.length - 1}
|
||||
onMessageEdit={text => message.text = text}
|
||||
/>,
|
||||
)}
|
||||
<ListItem sx={{
|
||||
// backgroundColor: 'background.surface',
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
borderBottomStyle: 'dashed',
|
||||
// justifyContent: 'center',
|
||||
px: { xs: 2.5, md: 3.5 }, py: 2,
|
||||
}}>
|
||||
<Typography level='body-md'>
|
||||
Welcome to the chat! 👋
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
<ListItem sx={{
|
||||
px: { xs: 2.5, md: 3.5 }, py: 2,
|
||||
}}>
|
||||
<Typography level='body-sm' ref={listBottomRef}>
|
||||
Like the chat? Clone it and keep the talk going! 🚀
|
||||
</Typography>
|
||||
</ListItem>
|
||||
{filteredMessages.map((message, idx) =>
|
||||
<ChatMessage
|
||||
key={'msg-' + message.id} message={message}
|
||||
showDate={idx === 0 || idx === filteredMessages.length - 1}
|
||||
onMessageEdit={text => message.text = text}
|
||||
/>,
|
||||
)}
|
||||
|
||||
<ListItem sx={{
|
||||
px: { xs: 2.5, md: 3.5 }, py: 2,
|
||||
}}>
|
||||
<Typography level='body-sm' ref={listBottomRef}>
|
||||
Like the chat? Clone it and keep the talk going! 🚀
|
||||
</Typography>
|
||||
</ListItem>
|
||||
|
||||
</List>
|
||||
|
||||
</ScrollToBottom>
|
||||
|
||||
</List>
|
||||
</Card>
|
||||
|
||||
{/* Buttons */}
|
||||
|
||||
+29
-16
@@ -10,11 +10,12 @@ import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { ROUTE_INDEX } from '~/common/app.routes';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { cssRainbowColorKeyframes, themeBgApp } from '~/common/app.theme';
|
||||
|
||||
import { newsCallout, NewsItems } from './news.data';
|
||||
|
||||
// number of news items to show by default, before the expander
|
||||
const DEFAULT_NEWS_COUNT = 2;
|
||||
const DEFAULT_NEWS_COUNT = 3;
|
||||
|
||||
export const cssColorKeyframes = keyframes`
|
||||
0%, 100% {
|
||||
@@ -27,7 +28,7 @@ export const cssColorKeyframes = keyframes`
|
||||
color: #0B6BCB; /* Primary main color (500) */
|
||||
}
|
||||
75% {
|
||||
color: #97C3F0; /* Primary lighter shade (300) */
|
||||
color: #083e75; /* Primary lighter shade (300) */
|
||||
}`;
|
||||
|
||||
|
||||
@@ -43,7 +44,7 @@ export function AppNews() {
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level1',
|
||||
backgroundColor: themeBgApp,
|
||||
overflowY: 'auto',
|
||||
display: 'flex', justifyContent: 'center',
|
||||
p: { xs: 3, md: 6 },
|
||||
@@ -78,16 +79,22 @@ export function AppNews() {
|
||||
|
||||
{!!news && <Container disableGutters maxWidth='sm'>
|
||||
{news?.map((ni, idx) => {
|
||||
const firstCard = idx === 0;
|
||||
// const firstCard = idx === 0;
|
||||
const hasCardAfter = news.length < NewsItems.length;
|
||||
const showExpander = hasCardAfter && (idx === news.length - 1);
|
||||
const addPadding = false; //!firstCard; // || showExpander;
|
||||
return <Card key={'news-' + idx} sx={{ mb: 2, minHeight: 32 }}>
|
||||
<CardContent sx={{ position: 'relative', pr: addPadding ? 4 : 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 1 }}>
|
||||
<GoodTooltip title={ni.versionName || null} placement='top-start'>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 0 }}>
|
||||
<GoodTooltip title={ni.versionName ? `${ni.versionName} ${ni.versionMoji || ''}` : null} placement='top-start'>
|
||||
<Typography level='title-sm' component='div' sx={{ flexGrow: 1 }}>
|
||||
{ni.text ? ni.text : ni.versionName ? `${ni.versionCode} · ${ni.versionName}` : `Version ${ni.versionCode}:`}
|
||||
{ni.text ? ni.text : ni.versionName ? `${ni.versionCode} · ` : `Version ${ni.versionCode}:`}
|
||||
<Box component='span' sx={!idx ? {
|
||||
animation: `${cssRainbowColorKeyframes} 5s infinite`,
|
||||
fontWeight: 600,
|
||||
} : {}}>
|
||||
{ni.versionName}
|
||||
</Box>
|
||||
</Typography>
|
||||
</GoodTooltip>
|
||||
{/*!firstCard &&*/ (
|
||||
@@ -97,19 +104,25 @@ export function AppNews() {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!!ni.items && (ni.items.length > 0) && <ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: 24 }}>
|
||||
{ni.items.filter(item => item.dev !== true).map((item, idx) => <li key={idx}>
|
||||
<Typography component='div' level='body-sm'>
|
||||
{item.text}
|
||||
</Typography>
|
||||
</li>)}
|
||||
</ul>}
|
||||
{!!ni.items && (ni.items.length > 0) && (
|
||||
<ul style={{ marginTop: 8, marginBottom: 8, paddingInlineStart: '1.5rem' }}>
|
||||
{ni.items.filter(item => item.dev !== true).map((item, idx) => <li key={idx}>
|
||||
< Typography component='div' level='body-sm'>
|
||||
{item.text}
|
||||
</Typography>
|
||||
</li>)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{showExpander && (
|
||||
<IconButton
|
||||
variant='plain' size='sm'
|
||||
variant='outlined'
|
||||
onClick={() => setLastNewsIdx(idx + 1)}
|
||||
sx={{ position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1 }}
|
||||
sx={{
|
||||
position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1,
|
||||
backgroundColor: 'background.surface',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
|
||||
+59
-21
@@ -10,10 +10,10 @@ import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
|
||||
|
||||
// update this variable every time you want to broadcast a new version to clients
|
||||
export const incrementalVersion: number = 8;
|
||||
export const incrementalVersion: number = 11;
|
||||
|
||||
const B = (props: { href?: string, children: React.ReactNode }) => {
|
||||
const boldText = <Typography color={!!props.href ? 'primary' : 'warning'} sx={{ fontWeight: 600 }}>{props.children}</Typography>;
|
||||
const boldText = <Typography color={!!props.href ? 'primary' : 'neutral'} sx={{ fontWeight: 600 }}>{props.children}</Typography>;
|
||||
return props.href ?
|
||||
<Link href={props.href + clientUtmSource()} target='_blank' sx={{ /*textDecoration: 'underline'*/ }}>{boldText} <LaunchIcon sx={{ ml: 1 }} /></Link> :
|
||||
boldText;
|
||||
@@ -27,11 +27,12 @@ const RIssues = `${OpenRepo}/issues`;
|
||||
export const newsCallout =
|
||||
<Card>
|
||||
<CardContent sx={{ gap: 2 }}>
|
||||
<Typography level='h4'>
|
||||
<Typography level='title-lg'>
|
||||
Open Roadmap
|
||||
</Typography>
|
||||
<Typography>
|
||||
The roadmap is officially out. For the first time you get a look at what's brewing, up and coming, and get a chance to pick up cool features!
|
||||
<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>
|
||||
<Grid container spacing={1}>
|
||||
<Grid xs={12} sm={7}>
|
||||
@@ -39,7 +40,7 @@ export const newsCallout =
|
||||
fullWidth variant='soft' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={OpenProject} noLinkStyle target='_blank'
|
||||
>
|
||||
Explore the Roadmap
|
||||
Explore
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
|
||||
@@ -57,20 +58,58 @@ export const newsCallout =
|
||||
|
||||
// news and feature surfaces
|
||||
export const NewsItems: NewsItem[] = [
|
||||
/*{
|
||||
// https://github.com/enricoros/big-agi/milestone/7
|
||||
// https://github.com/users/enricoros/projects/4/views/2
|
||||
versionName: '1.7.0',
|
||||
items: [
|
||||
// multi-window support
|
||||
// phone calls
|
||||
],
|
||||
},*/
|
||||
// still unannounced: phone calls, split windows, ...
|
||||
{
|
||||
versionCode: '1.7.3',
|
||||
versionCode: '1.10.0',
|
||||
versionName: 'The Year of AGI',
|
||||
versionMoji: '🎊✨',
|
||||
versionDate: new Date('2024-01-06T08:00:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/201'}>New UI</B> for desktop and mobile, enabling future expansions</>, issue: 201 },
|
||||
{ text: <><B href={RIssues + '/321'}>Folder categorization</B> for conversation management</>, issue: 321 },
|
||||
{ text: <><B>LM Studio</B> support and refined token management</> },
|
||||
{ text: <>Draggable panes in split screen mode</>, issue: 308 },
|
||||
{ text: <>Bug fixes and UI polish</> },
|
||||
{ text: <>Developers: document proxy settings on docker</>, issue: 318, dev: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.9.0',
|
||||
versionName: 'Creative Horizons',
|
||||
// versionMoji: '🎨🌌',
|
||||
versionDate: new Date('2023-12-28T22:30:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/212'}>DALL·E 3</B> support (/draw), with advanced control</>, issue: 212 },
|
||||
{ text: <><B href={RIssues + '/304'}>Perfect scrolling</B> UX, on all devices</>, issue: 304 },
|
||||
{ text: <>Create personas <B href={RIssues + '/287'}>from text</B></>, issue: 287 },
|
||||
{ text: <>Openrouter: auto-detect models, support free-tiers and rates</>, issue: 291 },
|
||||
{ text: <>Image drawing: unified UX, including auto-prompting</> },
|
||||
{ text: <>Fix layout on Firefox</>, issue: 255 },
|
||||
{ text: <>Developers: new Text2Image subsystem, Optima layout subsystem, ScrollToBottom library, using new Panes library, improved Llms subsystem</>, dev: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.8.0',
|
||||
versionName: 'To The Moon And Back',
|
||||
// versionMoji: '🚀🌕🔙❤️',
|
||||
versionDate: new Date('2023-12-20T09:30:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/275'}>Google Gemini</B> models support</> },
|
||||
{ text: <><B href={RIssues + '/273'}>Mistral Platform</B> support</> },
|
||||
{ text: <><B href={RIssues + '/270'}>Ollama chats</B> perfection</> },
|
||||
{ text: <>Custom <B href={RIssues + '/280'}>diagrams instructions</B> (@joriskalz)</> },
|
||||
{ text: <><B>Single-Tab</B> mode, enhances data integrity and prevents DB corruption</> },
|
||||
{ text: <>Updated Ollama (v0.1.17) and OpenRouter models</> },
|
||||
{ text: <>More: fixed ⌘ shortcuts on Mac</> },
|
||||
{ text: <><Link href='https://big-agi.com'>Website</Link>: official downloads</> },
|
||||
{ text: <>Easier Vercel deployment, documented <Link href='https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483'>network troubleshooting</Link></>, dev: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.7.0',
|
||||
versionName: 'Attachment Theory',
|
||||
versionDate: new Date('2023-12-11T06:00:00Z'), // new Date().toISOString()
|
||||
// versionDate: new Date('2023-12-10T12:00:00Z'), // 1.7.0
|
||||
// versionDate: new Date('2023-12-11T06:00:00Z'), // 1.7.3
|
||||
versionDate: new Date('2023-12-10T12:00:00Z'), // 1.7.0
|
||||
items: [
|
||||
{ text: <>Redesigned <B href={RIssues + '/251'}>attachments system</B>: drag, paste, link, snap, images, text, pdfs</> },
|
||||
{ text: <>Desktop <B href={RIssues + '/253'}>webcam access</B> for direct image capture (Labs option)</> },
|
||||
@@ -80,9 +119,6 @@ export const NewsItems: NewsItem[] = [
|
||||
{ text: <>{platformAwareKeystrokes('Ctrl+Shift+O')}: quick access to model options</> },
|
||||
{ text: <>Optimized voice input and performance</> },
|
||||
{ text: <>Latest Ollama and Oobabooga models</> },
|
||||
{ text: <>1.7.1: Improved <B href={RIssues + '/270'}>Ollama chats</B></> },
|
||||
{ text: <>1.7.2: Updated OpenRouter models 🎁</> },
|
||||
{ text: <>1.7.3: <B href={RIssues + '/273'}>Mistral Platform</B> support</> },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -162,10 +198,12 @@ export const NewsItems: NewsItem[] = [
|
||||
interface NewsItem {
|
||||
versionCode: string;
|
||||
versionName?: string;
|
||||
versionMoji?: string;
|
||||
versionDate?: Date;
|
||||
text?: string | React.JSX.Element;
|
||||
items?: {
|
||||
text: string | React.JSX.Element;
|
||||
dev?: boolean;
|
||||
issue?: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -1,25 +1,20 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { navigateToNews } from '~/common/app.routes';
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
|
||||
import { incrementalVersion } from './news.data';
|
||||
|
||||
|
||||
export function useShowNewsOnUpdate() {
|
||||
const { push: routerPush } = useRouter();
|
||||
const { usageCount, lastSeenNewsVersion } = useAppStateStore(state => ({
|
||||
usageCount: state.usageCount,
|
||||
lastSeenNewsVersion: state.lastSeenNewsVersion,
|
||||
}), shallow);
|
||||
export function useRedirectToNewsOnUpdates() {
|
||||
React.useEffect(() => {
|
||||
const { usageCount, lastSeenNewsVersion } = useAppStateStore.getState();
|
||||
const isNewsOutdated = (lastSeenNewsVersion || 0) < incrementalVersion;
|
||||
if (isNewsOutdated && usageCount > 2) {
|
||||
// Disable for now
|
||||
void routerPush('/news');
|
||||
void navigateToNews();
|
||||
}
|
||||
}, [lastSeenNewsVersion, routerPush, usageCount]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useMarkNewsAsSeen() {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Container, ListDivider, Sheet, Typography } from '@mui/joy';
|
||||
import { Container, ListDivider, Sheet, Typography } from '@mui/joy';
|
||||
|
||||
import { YTPersonaCreator } from './YTPersonaCreator';
|
||||
import ScienceIcon from '@mui/icons-material/Science';
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
|
||||
import { PersonaCreator } from './PersonaCreator';
|
||||
|
||||
|
||||
export function AppPersonas() {
|
||||
@@ -11,26 +12,19 @@ export function AppPersonas() {
|
||||
<Sheet sx={{
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'background.level1',
|
||||
backgroundColor: themeBgApp,
|
||||
p: { xs: 3, md: 6 },
|
||||
}}>
|
||||
|
||||
<Container disableGutters maxWidth='md' sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
<Typography level='title-lg' sx={{ textAlign: 'center' }}>
|
||||
Advanced AI Personas
|
||||
AI Personas Creator
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
|
||||
<Typography>
|
||||
Experimental
|
||||
</Typography>
|
||||
<ScienceIcon color='primary' />
|
||||
</Box>
|
||||
|
||||
<ListDivider sx={{ my: 2 }} />
|
||||
|
||||
<YTPersonaCreator />
|
||||
<PersonaCreator />
|
||||
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, IconButton, Input, LinearProgress, Tooltip, Typography } from '@mui/joy';
|
||||
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 WhatshotIcon from '@mui/icons-material/Whatshot';
|
||||
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
import { RenderMarkdown } from '../chat/components/message/RenderMarkdown';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { apiQuery } from '~/common/util/trpc.client';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { lineHeightTextarea } from '~/common/app.theme';
|
||||
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
|
||||
|
||||
import { LLMChainStep, useLLMChain } from './useLLMChain';
|
||||
@@ -37,9 +42,9 @@ function useTranscriptFromVideo(videoID: string | null) {
|
||||
}
|
||||
|
||||
|
||||
const YouTubePersonaSteps: LLMChainStep[] = [
|
||||
const PersonaCreationSteps: LLMChainStep[] = [
|
||||
{
|
||||
name: 'Analyzing the transcript',
|
||||
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.',
|
||||
@@ -62,23 +67,44 @@ const YouTubePersonaSteps: LLMChainStep[] = [
|
||||
];
|
||||
|
||||
|
||||
export function YTPersonaCreator() {
|
||||
export function PersonaCreator() {
|
||||
// state
|
||||
const [selectedTab, setSelectedTab] = React.useState(0);
|
||||
const [inputText, setInputText] = React.useState<string | null>(null);
|
||||
const [videoURL, setVideoURL] = React.useState('');
|
||||
const [videoID, setVideoID] = React.useState('');
|
||||
const [personaTranscript, setPersonaTranscript] = React.useState<string | null>(null);
|
||||
const [personaText, setPersonaText] = React.useState('');
|
||||
|
||||
// external state
|
||||
const [diagramLlm, llmComponent] = useFormRadioLlmType();
|
||||
const [personaLlm, llmComponent] = useFormRadioLlmType('Persona Creation Model');
|
||||
|
||||
|
||||
// chain to convert a text input string (e.g. youtube transcript) into a persona prompt
|
||||
const savePersona = React.useCallback((personaPrompt: string) => {
|
||||
// TODO.. save the persona prompt here
|
||||
}, []);
|
||||
|
||||
const { isFinished, isTransforming, chainProgress, chainIntermediates, chainStepName, chainOutput, chainError, abortChain } =
|
||||
useLLMChain(PersonaCreationSteps, personaLlm?.id, inputText ?? undefined, savePersona);
|
||||
|
||||
|
||||
// fetch transcript when the Video ID is ready, then store it
|
||||
const { transcript, thumbnailUrl, title, isFetching, isError, error: transcriptError } =
|
||||
useTranscriptFromVideo(videoID);
|
||||
React.useEffect(() => setPersonaTranscript(transcript), [transcript]);
|
||||
React.useEffect(() => setInputText(transcript), [transcript]);
|
||||
|
||||
// use the transformation sequence to create a persona
|
||||
const { isFinished, isTransforming, chainProgress, chainIntermediates, chainStepName, chainOutput, chainError, abortChain } =
|
||||
useLLMChain(YouTubePersonaSteps, diagramLlm?.id, personaTranscript ?? undefined);
|
||||
|
||||
// Reset the relevant state when the selected tab changes
|
||||
React.useEffect(() => {
|
||||
// reset state
|
||||
setVideoURL('');
|
||||
setVideoID('');
|
||||
setInputText(null);
|
||||
setPersonaText('');
|
||||
}, [selectedTab]);
|
||||
|
||||
|
||||
// [Tab: 0] Video download
|
||||
|
||||
const handleVideoIdChange = (e: React.ChangeEvent<HTMLInputElement>) => setVideoURL(e.target.value);
|
||||
|
||||
@@ -88,66 +114,94 @@ export function YTPersonaCreator() {
|
||||
if (!videoId) {
|
||||
setVideoURL('Invalid');
|
||||
} else {
|
||||
setPersonaTranscript(null);
|
||||
setInputText(null);
|
||||
setVideoID(videoId);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// [Tab: 1] Text input
|
||||
|
||||
const handlePersonaTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => setPersonaText(e.target.value);
|
||||
|
||||
return <>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 1 }}>
|
||||
<YouTubeIcon sx={{ color: '#f00' }} />
|
||||
<Typography level='title-lg'>
|
||||
YouTube -> AI persona
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography level='title-sm' mb={3}>
|
||||
Create the <em>System Prompt</em> of an AI Persona from YouTube or Text.
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleFetchTranscript}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
<Input
|
||||
required
|
||||
type='url'
|
||||
fullWidth
|
||||
<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'
|
||||
placeholder='YouTube Video URL'
|
||||
value={videoURL} onChange={handleVideoIdChange}
|
||||
endDecorator={
|
||||
<IconButton
|
||||
variant='outlined' color='neutral'
|
||||
onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}
|
||||
>
|
||||
<WhatshotIcon />
|
||||
</IconButton>
|
||||
}
|
||||
minRows={4} maxRows={8}
|
||||
placeholder='Paste your text here...'
|
||||
value={personaText}
|
||||
onChange={handlePersonaTextChange}
|
||||
sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
lineHeight: lineHeightTextarea,
|
||||
mb: 1.5,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='solid' disabled={isFetching || isTransforming} loading={isFetching}
|
||||
sx={{ minWidth: 120 }}>
|
||||
Create
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Button variant='solid' disabled={isFetching || isTransforming || !personaText} onClick={() => setInputText(personaText)} sx={{ minWidth: 140 }}>
|
||||
Create
|
||||
</Button>
|
||||
{!!personaText?.length && <Typography level='body-sm'>{personaText.length.toLocaleString()}</Typography>}
|
||||
</Box>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
{/* LLM selector (chat vs fast) */}
|
||||
{!isTransforming && !isFinished && llmComponent}
|
||||
|
||||
{/* 1. Transcript*/}
|
||||
{personaTranscript && (
|
||||
<Card sx={{ mt: 2, boxShadow: 'md' }}>
|
||||
<CardContent>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
{title || 'Transcript'}
|
||||
</Typography>
|
||||
<Box>
|
||||
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Image' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
|
||||
<Typography level='body-sm'>
|
||||
{personaTranscript.slice(0, 280)}...
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{!isTransforming && !isFinished && <Box sx={{ mt: 3 }}>{llmComponent}</Box>}
|
||||
|
||||
{/* Errors */}
|
||||
{isError && (
|
||||
@@ -161,49 +215,62 @@ export function YTPersonaCreator() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
{/* Persona! */}
|
||||
{chainOutput && <Box sx={{ mt: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
YouTuber Persona System Prompt
|
||||
</Typography>
|
||||
<Card sx={{ boxShadow: 'md' }}>
|
||||
<CardContent sx={{
|
||||
position: 'relative',
|
||||
'&:hover > button': { opacity: 1 },
|
||||
}}>
|
||||
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
|
||||
You can now copy the following text and use it as Custom prompt!
|
||||
</Alert>
|
||||
<Tooltip title='Copy system prompt' variant='solid'>
|
||||
<IconButton
|
||||
variant='outlined' color='neutral' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')}
|
||||
sx={{
|
||||
position: 'absolute', right: 0, zIndex: 10,
|
||||
// opacity: 0, transition: 'opacity 0.3s',
|
||||
}}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography level='body-sm'>
|
||||
{chainOutput}
|
||||
{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>
|
||||
<RenderMarkdown textBlock={{ type: 'text', content: chainOutput }} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>}
|
||||
</>}
|
||||
|
||||
{/* Input: Transcript/Text */}
|
||||
{inputText && <>
|
||||
<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'>
|
||||
{inputText.slice(0, 280)}...
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>}
|
||||
|
||||
{/* Intermediate outputs rendered as cards in a grid */}
|
||||
{chainIntermediates && chainIntermediates.length > 0 && <Box sx={{ mt: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
{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>
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography level='title-sm' sx={{ mb: 1 }}>
|
||||
{i + 1}. {YouTubePersonaSteps[i].name}
|
||||
{i + 1}. {PersonaCreationSteps[i].name}
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{intermediate?.slice(0, 140)}...
|
||||
@@ -213,27 +280,35 @@ export function YTPersonaCreator() {
|
||||
</Grid>,
|
||||
)}
|
||||
</Grid>
|
||||
</Box>}
|
||||
</>}
|
||||
|
||||
|
||||
{/* Embodiment Progress */}
|
||||
{/* 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>
|
||||
<Typography color='success' level='title-lg' sx={{ mt: 1 }}>
|
||||
Embodying Persona ...
|
||||
</Typography>
|
||||
<Typography color='success' level='title-sm' sx={{ mt: 1, fontWeight: 600 }}>
|
||||
{chainStepName}
|
||||
</Typography>
|
||||
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1, mb: 2 }} />
|
||||
<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.
|
||||
If you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
|
||||
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: 5 }}>
|
||||
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 3 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
</GoodModal>}
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { DLLMId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { callChatGenerate, VChatMessageIn } from '~/modules/llms/transports/chatGenerate';
|
||||
import { llmChatGenerateOrThrow, VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
|
||||
|
||||
export interface LLMChainStep {
|
||||
@@ -16,7 +16,7 @@ export interface LLMChainStep {
|
||||
/**
|
||||
* React hook to manage a chain of LLM transformations.
|
||||
*/
|
||||
export function useLLMChain(steps: LLMChainStep[], llmId: DLLMId | undefined, chainInput: string | undefined) {
|
||||
export function useLLMChain(steps: LLMChainStep[], llmId: DLLMId | undefined, chainInput: string | undefined, onSuccess?: (output: string) => void) {
|
||||
const [chain, setChain] = React.useState<ChainState | null>(null);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
const chainAbortController = React.useRef(new AbortController());
|
||||
@@ -80,11 +80,15 @@ export function useLLMChain(steps: LLMChainStep[], llmId: DLLMId | undefined, ch
|
||||
_chainAbortController.signal.addEventListener('abort', globalToStepListener);
|
||||
|
||||
// LLM call
|
||||
callChatGenerate(llmId, llmChatInput, chain.overrideResponseTokens)
|
||||
llmChatGenerateOrThrow(llmId, llmChatInput, null, null, chain.overrideResponseTokens ?? undefined)
|
||||
.then(({ content }) => {
|
||||
stepDone = true;
|
||||
if (!stepAbortController.signal.aborted)
|
||||
setChain(updateChainState(chain, llmChatInput, stepIdx, content));
|
||||
if (stepAbortController.signal.aborted)
|
||||
return;
|
||||
const chainState = updateChainState(chain, llmChatInput, stepIdx, content);
|
||||
if (chainState.output && onSuccess)
|
||||
onSuccess(chainState.output);
|
||||
setChain(chainState);
|
||||
})
|
||||
.catch((err) => {
|
||||
stepDone = true;
|
||||
@@ -121,8 +125,8 @@ interface ChainState {
|
||||
steps: StepState[];
|
||||
chatHistory: VChatMessageIn[];
|
||||
progress: number;
|
||||
safeInputLength: number;
|
||||
overrideResponseTokens: number;
|
||||
safeInputLength: number | null;
|
||||
overrideResponseTokens: number | null;
|
||||
input: string;
|
||||
output: string | null;
|
||||
}
|
||||
@@ -142,8 +146,9 @@ function initChainState(llmId: DLLMId, input: string, steps: LLMChainStep[]): Ch
|
||||
throw new Error(`LLM ${llmId} not found`);
|
||||
|
||||
const overrideResponseTokens = llm.maxOutputTokens;
|
||||
const inputTokens = llm.contextTokens - overrideResponseTokens;
|
||||
const safeInputLength = Math.floor(inputTokens * 2); // it's deemed around 4
|
||||
const safeInputLength = (llm.contextTokens && overrideResponseTokens)
|
||||
? Math.floor((llm.contextTokens - overrideResponseTokens) * 2)
|
||||
: null;
|
||||
|
||||
return {
|
||||
steps: steps.map((step, i) => ({
|
||||
@@ -177,8 +182,8 @@ function updateChainState(chain: ChainState, history: VChatMessageIn[], stepIdx:
|
||||
};
|
||||
}
|
||||
|
||||
function implodeText(text: string, maxLength: number) {
|
||||
if (text.length <= maxLength) return text;
|
||||
function implodeText(text: string, maxLength: number | null) {
|
||||
if (!maxLength || text.length <= maxLength) return text;
|
||||
const halfLength = Math.floor(maxLength / 2);
|
||||
return `${text.substring(0, halfLength)}\n...\n${text.substring(text.length - halfLength)}`;
|
||||
}
|
||||
@@ -9,8 +9,8 @@ import WidthWideIcon from '@mui/icons-material/WidthWide';
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormRadioControl } from '~/common/components/forms/FormRadioControl';
|
||||
import { isPwa } from '~/common/util/pwaUtils';
|
||||
import { openLayoutModelsSetup } from '~/common/layout/store-applayout';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
@@ -18,10 +18,14 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
const SHOW_PURPOSE_FINDER = false;
|
||||
|
||||
|
||||
const ModelOptionsButton = () =>
|
||||
<Button
|
||||
const ModelsSetupButton = () => {
|
||||
|
||||
// external state
|
||||
const { openModelsSetup } = useOptimaLayout();
|
||||
|
||||
return <Button
|
||||
// variant='soft' color='success'
|
||||
onClick={openLayoutModelsSetup}
|
||||
onClick={openModelsSetup}
|
||||
startDecorator={<BuildCircleIcon />}
|
||||
sx={{
|
||||
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
|
||||
@@ -29,6 +33,7 @@ const ModelOptionsButton = () =>
|
||||
>
|
||||
Models
|
||||
</Button>;
|
||||
};
|
||||
|
||||
|
||||
export function AppChatSettingsUI() {
|
||||
@@ -64,7 +69,7 @@ export function AppChatSettingsUI() {
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title='AI Models'
|
||||
description='Setup' />
|
||||
<ModelOptionsButton />
|
||||
<ModelsSetupButton />
|
||||
</FormControl>
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between' }}>
|
||||
|
||||
@@ -6,12 +6,13 @@ import ScienceIcon from '@mui/icons-material/Science';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
import { BrowseSettings } from '~/modules/browse/BrowseSettings';
|
||||
import { DallESettings } from '~/modules/t2i/dalle/DallESettings';
|
||||
import { ElevenlabsSettings } from '~/modules/elevenlabs/ElevenlabsSettings';
|
||||
import { GoogleSearchSettings } from '~/modules/google/GoogleSearchSettings';
|
||||
import { ProdiaSettings } from '~/modules/prodia/ProdiaSettings';
|
||||
import { ProdiaSettings } from '~/modules/t2i/prodia/ProdiaSettings';
|
||||
import { T2ISettings } from '~/modules/t2i/T2ISettings';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { closeLayoutPreferences, openLayoutShortcuts, useLayoutPreferencesTab } from '~/common/layout/store-applayout';
|
||||
import { settingsGap } from '~/common/app.theme';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
|
||||
@@ -100,20 +101,24 @@ function Topic(props: { title?: string, icon?: string | React.ReactNode, startCo
|
||||
* Component that allows the User to modify the application settings,
|
||||
* persisted on the client via localStorage.
|
||||
*/
|
||||
export function SettingsModal() {
|
||||
export function SettingsModal(props: {
|
||||
open: boolean,
|
||||
tabIndex: number,
|
||||
onClose: () => void,
|
||||
onOpenShortcuts: () => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const settingsTabIndex = useLayoutPreferencesTab();
|
||||
|
||||
const tabFixSx = { fontFamily: 'body', flex: 1, p: 0, m: 0 };
|
||||
|
||||
return (
|
||||
<GoodModal
|
||||
title='Preferences' strongerTitle
|
||||
open={!!settingsTabIndex} onClose={closeLayoutPreferences}
|
||||
open={props.open} onClose={props.onClose}
|
||||
startButton={isMobile ? undefined : (
|
||||
<Button variant='soft' onClick={openLayoutShortcuts}>
|
||||
<Button variant='soft' onClick={props.onOpenShortcuts}>
|
||||
👉 See Shortcuts
|
||||
</Button>
|
||||
)}
|
||||
@@ -124,7 +129,7 @@ export function SettingsModal() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tabs aria-label='Settings tabbed menu' defaultValue={settingsTabIndex}>
|
||||
<Tabs aria-label='Settings tabbed menu' defaultValue={props.tabIndex}>
|
||||
<TabList
|
||||
variant='soft'
|
||||
disableUnderline
|
||||
@@ -151,7 +156,7 @@ export function SettingsModal() {
|
||||
<Tab disableIndicator value={4} sx={tabFixSx}>Tools</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value={1} sx={{ p: 'var(--Tabs-gap)' }}>
|
||||
<TabPanel value={1} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic>
|
||||
<AppChatSettingsUI />
|
||||
@@ -165,7 +170,7 @@ export function SettingsModal() {
|
||||
</Topics>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={3} sx={{ p: 'var(--Tabs-gap)' }}>
|
||||
<TabPanel value={3} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic icon='🎙️' title='Voice settings'>
|
||||
<VoiceSettings />
|
||||
@@ -176,15 +181,21 @@ export function SettingsModal() {
|
||||
</Topics>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={2} sx={{ p: 'var(--Tabs-gap)' }}>
|
||||
<TabPanel value={2} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic icon='🖍️️' title='Prodia API'>
|
||||
<Topic>
|
||||
<T2ISettings />
|
||||
</Topic>
|
||||
<Topic icon='🖍️️' title='OpenAI DALL·E' startCollapsed>
|
||||
<DallESettings />
|
||||
</Topic>
|
||||
<Topic icon='🖍️️' title='Prodia API' startCollapsed>
|
||||
<ProdiaSettings />
|
||||
</Topic>
|
||||
</Topics>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={4} sx={{ p: 'var(--Tabs-gap)' }}>
|
||||
<TabPanel value={4} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic icon={<SearchIcon />} title='Browsing' startCollapsed>
|
||||
<BrowseSettings />
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { ChatMessage } from '../chat/components/message/ChatMessage';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { closeLayoutShortcuts, useLayoutShortcuts } from '~/common/layout/store-applayout';
|
||||
import { createDMessage } from '~/common/state/store-chats';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
|
||||
@@ -36,17 +35,9 @@ const shortcutsMd = `
|
||||
const shortcutsMessage = createDMessage('assistant', platformAwareKeystrokes(shortcutsMd));
|
||||
|
||||
|
||||
export function ShortcutsModal() {
|
||||
|
||||
// external state
|
||||
const showShortcuts = useLayoutShortcuts();
|
||||
|
||||
export function ShortcutsModal(props: { onClose: () => void }) {
|
||||
return (
|
||||
<GoodModal
|
||||
open={showShortcuts}
|
||||
title='Desktop Shortcuts'
|
||||
onClose={closeLayoutShortcuts}
|
||||
>
|
||||
<GoodModal open title='Desktop Shortcuts' onClose={props.onClose}>
|
||||
<ChatMessage message={shortcutsMessage} hideAvatars noBottomBorder sx={{ p: 0, m: 0 }} />
|
||||
</GoodModal>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,7 @@ import * as React from 'react';
|
||||
import { FormControl, Typography } from '@mui/joy';
|
||||
import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
|
||||
@@ -19,22 +17,12 @@ export function UxLabsSettings() {
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const {
|
||||
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsMagicDraw, labsPersonaYTCreator, labsSplitBranching,
|
||||
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsMagicDraw, setLabsPersonaYTCreator, setLabsSplitBranching,
|
||||
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsSplitBranching,
|
||||
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsSplitBranching,
|
||||
} = useUXLabsStore();
|
||||
|
||||
return <>
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><YouTubeIcon color={labsPersonaYTCreator ? 'primary' : undefined} sx={{ mr: 0.25 }} /> YouTube Personas</>} description={labsPersonaYTCreator ? 'Creator Enabled' : 'Disabled'}
|
||||
checked={labsPersonaYTCreator} onChange={setLabsPersonaYTCreator}
|
||||
/>
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><FormatPaintIcon color={labsMagicDraw ? 'primary' : undefined} sx={{ mr: 0.25 }} />Assisted Draw</>} description={labsMagicDraw ? 'Enabled' : 'Disabled'}
|
||||
checked={labsMagicDraw} onChange={setLabsMagicDraw}
|
||||
/>
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><CallIcon color={labsCalling ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Voice Calls</>} description={labsCalling ? 'Call AGI' : 'Disabled'}
|
||||
checked={labsCalling} onChange={setLabsCalling}
|
||||
@@ -46,7 +34,7 @@ export function UxLabsSettings() {
|
||||
/>}
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><VerticalSplitIcon color={labsSplitBranching ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Split Branching</>} description={labsSplitBranching ? 'Enabled' : 'Disabled'} disabled
|
||||
title={<><VerticalSplitIcon color={labsSplitBranching ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Split Branching</>} description={labsSplitBranching ? 'Enabled' : 'Disabled'}
|
||||
checked={labsSplitBranching} onChange={setLabsSplitBranching}
|
||||
/>
|
||||
|
||||
@@ -58,7 +46,7 @@ export function UxLabsSettings() {
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title='Graduated' />
|
||||
<Typography level='body-xs'>
|
||||
<Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Relative chat size · Text Tools · LLM Overheat
|
||||
<Link href='https://github.com/enricoros/big-AGI/issues/282' target='_blank'>Persona Creator</Link> · <Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Imagine · Relative chat size · Text Tools · LLM Overheat
|
||||
</Typography>
|
||||
</FormControl>
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ export const Brand = {
|
||||
Common: (process.env.NODE_ENV === 'development' ? '[DEV] ' : '') + 'big-AGI',
|
||||
},
|
||||
Meta: {
|
||||
Description: 'Leading open-source AI web interface to help you learn, think, and do. AI personas, superior privacy, advanced features, and fun UX.',
|
||||
SiteName: 'big-AGI | Harnessing AI for You',
|
||||
Description: 'Launch big-AGI to unlock the full potential of AI, with precise control over your data and models. Voice interface, AI personas, advanced features, and fun UX.',
|
||||
SiteName: 'big-AGI | Precision AI for You',
|
||||
ThemeColor: '#32383E',
|
||||
TwitterSite: '@enricoros',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
import type { FunctionComponent } from 'react';
|
||||
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import BuildCircleIcon from '@mui/icons-material/BuildCircle';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
import Diversity2Icon from '@mui/icons-material/Diversity2';
|
||||
import EventNoteIcon from '@mui/icons-material/EventNote';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
import WorkspacesIcon from '@mui/icons-material/Workspaces';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { DiscordIcon } from '~/common/components/icons/DiscordIcon';
|
||||
|
||||
|
||||
// enable to show all items, for layout development
|
||||
const SHOW_ALL_APPS = false;
|
||||
|
||||
|
||||
// Nav items
|
||||
|
||||
interface ItemBase {
|
||||
name: string,
|
||||
icon: FunctionComponent,
|
||||
tooltip?: string,
|
||||
}
|
||||
|
||||
export interface NavItemApp extends ItemBase {
|
||||
type: 'app',
|
||||
route: string,
|
||||
drawer?: string | true, // true: can make use of the drawer, string: also set the title
|
||||
hideBar?: boolean, // set to true to hide the page bar
|
||||
hideNav?: boolean, // set to hide the Nav bar (note: must have a way to navigate back)
|
||||
automatic?: boolean, // only accessible by the machine
|
||||
fullWidth?: boolean, // set to true to override the user preference
|
||||
hide?: boolean, // delete from the UI
|
||||
}
|
||||
|
||||
export interface NavItemModal extends ItemBase {
|
||||
type: 'modal',
|
||||
overlayId: 'settings' | 'models',
|
||||
}
|
||||
|
||||
export interface NavItemExtLink extends ItemBase {
|
||||
type: 'extLink',
|
||||
href: string,
|
||||
}
|
||||
|
||||
// interface MenuItemAction extends ItemBase {
|
||||
// type: 'action',
|
||||
// action: () => void,
|
||||
// }
|
||||
|
||||
|
||||
export const navItems: {
|
||||
apps: NavItemApp[]
|
||||
modals: NavItemModal[]
|
||||
links: NavItemExtLink[],
|
||||
} = {
|
||||
|
||||
// User-chosen apps
|
||||
apps: [
|
||||
{
|
||||
name: 'Chat',
|
||||
icon: TelegramIcon,
|
||||
type: 'app',
|
||||
route: '/',
|
||||
drawer: true,
|
||||
},
|
||||
{
|
||||
name: 'Call',
|
||||
icon: CallIcon,
|
||||
type: 'app',
|
||||
route: '/call',
|
||||
drawer: 'Recent Calls',
|
||||
automatic: true,
|
||||
fullWidth: true,
|
||||
},
|
||||
{
|
||||
name: 'Draw',
|
||||
icon: FormatPaintIcon,
|
||||
type: 'app',
|
||||
route: '/draw',
|
||||
hide: true,
|
||||
},
|
||||
{
|
||||
name: 'Cortex',
|
||||
icon: AutoAwesomeIcon,
|
||||
type: 'app',
|
||||
route: '/cortex',
|
||||
automatic: true,
|
||||
hide: true,
|
||||
},
|
||||
{
|
||||
name: 'Patterns',
|
||||
icon: AccountTreeIcon,
|
||||
type: 'app',
|
||||
route: '/patterns',
|
||||
hide: true,
|
||||
},
|
||||
{
|
||||
name: 'Workspace',
|
||||
icon: WorkspacesIcon,
|
||||
type: 'app',
|
||||
route: '/workspace',
|
||||
hide: true,
|
||||
},
|
||||
{
|
||||
name: 'Personas',
|
||||
icon: Diversity2Icon,
|
||||
type: 'app',
|
||||
route: '/personas',
|
||||
hideBar: true,
|
||||
},
|
||||
{
|
||||
name: 'News',
|
||||
icon: EventNoteIcon,
|
||||
type: 'app',
|
||||
route: '/news',
|
||||
hideBar: true,
|
||||
},
|
||||
|
||||
// non-user-selectable ('automatic') Apps
|
||||
{
|
||||
name: 'Shared Chat',
|
||||
icon: IosShareIcon,
|
||||
type: 'app',
|
||||
route: '/link/chat/[chatLinkId]',
|
||||
drawer: 'Shared Chats',
|
||||
automatic: true,
|
||||
hideNav: true,
|
||||
},
|
||||
],
|
||||
|
||||
// Modals
|
||||
modals: [
|
||||
{
|
||||
name: 'Manage Models',
|
||||
icon: BuildCircleIcon,
|
||||
type: 'modal',
|
||||
overlayId: 'models',
|
||||
},
|
||||
{
|
||||
name: 'Preferences',
|
||||
icon: SettingsIcon,
|
||||
type: 'modal',
|
||||
overlayId: 'settings',
|
||||
},
|
||||
],
|
||||
|
||||
// External links
|
||||
links: [
|
||||
// {
|
||||
// type: 'extLink',
|
||||
// name: 'X',
|
||||
// icon: TwitterIcon,
|
||||
// href: 'https://twitter.com',
|
||||
// },
|
||||
{
|
||||
type: 'extLink',
|
||||
name: 'Discord',
|
||||
icon: DiscordIcon,
|
||||
href: Brand.URIs.SupportInvite,
|
||||
},
|
||||
{
|
||||
type: 'extLink',
|
||||
name: 'GitHub',
|
||||
icon: GitHubIcon,
|
||||
href: Brand.URIs.OpenRepo,
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
|
||||
// apply UI filtering right away - do it here, once, and for all
|
||||
navItems.apps = navItems.apps.filter(app => !app.hide || SHOW_ALL_APPS);
|
||||
+50
-21
@@ -4,18 +4,22 @@
|
||||
// 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';
|
||||
|
||||
|
||||
export const ROUTE_INDEX = '/';
|
||||
export const ROUTE_APP_CHAT = '/';
|
||||
export const ROUTE_APP_CALL = '/call';
|
||||
export const ROUTE_APP_LINK_CHAT = '/link/chat/:linkId';
|
||||
export const ROUTE_APP_NEWS = '/news';
|
||||
export const ROUTE_APP_PERSONAS = '/personas';
|
||||
const ROUTE_CALLBACK_OPENROUTER = '/link/callback_openrouter';
|
||||
|
||||
export const getIndexLink = () => ROUTE_INDEX;
|
||||
|
||||
// Get Paths
|
||||
|
||||
export const getCallbackUrl = (source: 'openrouter') => {
|
||||
const callbackUrl = new URL(window.location.href);
|
||||
@@ -31,29 +35,54 @@ export const getCallbackUrl = (source: 'openrouter') => {
|
||||
|
||||
export const getChatLinkRelativePath = (chatLinkId: string) => ROUTE_APP_LINK_CHAT.replace(':linkId', chatLinkId);
|
||||
|
||||
const navigateFn = (path: string) => (replace?: boolean): Promise<boolean> =>
|
||||
Router[replace ? 'replace' : 'push'](path);
|
||||
export function useRouterQuery<TQuery>(): TQuery {
|
||||
const { query } = useRouter();
|
||||
return query as TQuery;
|
||||
}
|
||||
|
||||
export function useRouterRoute(): string {
|
||||
const { route } = useRouter();
|
||||
return route;
|
||||
}
|
||||
|
||||
|
||||
/// Simple Navigation
|
||||
|
||||
export const navigateToIndex = navigateFn(ROUTE_INDEX);
|
||||
export const navigateToChat = async (conversationId?: DConversationId) => {
|
||||
if (conversationId) {
|
||||
await Router.push(
|
||||
{
|
||||
pathname: ROUTE_APP_CHAT,
|
||||
query: {
|
||||
conversationId,
|
||||
},
|
||||
},
|
||||
ROUTE_APP_CHAT,
|
||||
);
|
||||
} else {
|
||||
await Router.push(ROUTE_APP_CHAT, ROUTE_APP_CHAT);
|
||||
}
|
||||
};
|
||||
|
||||
export const navigateToNews = navigateFn(ROUTE_APP_NEWS);
|
||||
|
||||
export const navigateToPersonas = navigateFn(ROUTE_APP_PERSONAS);
|
||||
|
||||
export const navigateBack = Router.back;
|
||||
|
||||
export const reloadPage = () => isBrowser && window.location.reload();
|
||||
|
||||
function navigateFn(path: string) {
|
||||
return (replace?: boolean): Promise<boolean> => Router[replace ? 'replace' : 'push'](path);
|
||||
}
|
||||
|
||||
|
||||
/// Launch Apps
|
||||
|
||||
/* Note: not used yet
|
||||
export interface AppChatQueryParams {
|
||||
conversationId?: string;
|
||||
}*/
|
||||
|
||||
export const launchAppChat = async (conversationId?: DConversationId) => {
|
||||
await Router.push(
|
||||
{
|
||||
pathname: ROUTE_APP_CHAT,
|
||||
query: conversationId ? {
|
||||
conversationId,
|
||||
} /*satisfies AppChatQueryParams*/
|
||||
: undefined,
|
||||
},
|
||||
ROUTE_APP_CHAT,
|
||||
);
|
||||
};
|
||||
|
||||
export interface AppCallQueryParams {
|
||||
conversationId: string;
|
||||
personaId: string;
|
||||
@@ -62,12 +91,12 @@ export interface AppCallQueryParams {
|
||||
export function launchAppCall(conversationId: string, personaId: string) {
|
||||
void Router.push(
|
||||
{
|
||||
pathname: `/call`,
|
||||
pathname: ROUTE_APP_CALL,
|
||||
query: {
|
||||
conversationId,
|
||||
personaId,
|
||||
} satisfies AppCallQueryParams,
|
||||
},
|
||||
// '/call',
|
||||
// ROUTE_APP_CALL,
|
||||
).then();
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user