mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3d39a35c03 | |||
| 5ca9475bb6 | |||
| f12386c614 | |||
| 485dd0d91f | |||
| fc137176bd | |||
| b34fe2f9f6 | |||
| 3b7916c536 | |||
| d11a2b59ee | |||
| 63d1ec4c30 | |||
| 4ed49be67e | |||
| 3a0749c5b2 | |||
| 63470adc0f | |||
| 0bbfad4b41 | |||
| f9cb97ca49 | |||
| b63636cf2f | |||
| 54b388c9ae | |||
| d233f0946f | |||
| 671ac36946 | |||
| e6ba217302 | |||
| b9a18a5442 | |||
| f8d0f25f72 | |||
| 2213c61760 | |||
| e7edffa237 | |||
| fd83aca7a4 | |||
| bdc2d07747 | |||
| 1953f7d31a | |||
| 054ed80bbe | |||
| 13b64e65c3 | |||
| ee9ee72505 | |||
| 1b631a91b3 | |||
| 118d2cb2ad | |||
| b6acfa9d49 | |||
| 4798ba3fd0 | |||
| 14608f97da | |||
| 901d590159 | |||
| 28e71d4ac7 | |||
| 7f958c9e66 | |||
| 910f0c5556 | |||
| 427ef8c108 | |||
| 2efdfca7e5 | |||
| bc113b08f7 | |||
| 262a6d2560 | |||
| f9224aa25d | |||
| 6d0f7949f8 | |||
| 1a679bcf90 | |||
| 5de34fe3af | |||
| 420b4565dd | |||
| 27eb9adb16 | |||
| c4277b9ef0 | |||
| ec39c58474 | |||
| 3ce2e86a66 | |||
| d62757d94a | |||
| 7ba315c796 | |||
| 75e909e0e7 | |||
| 285c6a3fac | |||
| 9bcdbf8db6 | |||
| ae9d85d2cd | |||
| ad3191fcaf | |||
| d6c98bd304 | |||
| 52c1be20d9 | |||
| 69fb879439 | |||
| 135153464a | |||
| 87e556d6c4 | |||
| 46866ac061 | |||
| 9f222caadf | |||
| f82ac7a476 | |||
| 4fa5d875e9 | |||
| e2b1c6aff0 | |||
| 16b25fcc1f | |||
| 17cd765d00 | |||
| 1ea8b42e5f | |||
| 6b5a207522 | |||
| 85d5fef3fb | |||
| e9a77abd83 | |||
| 9d2857d41e | |||
| 62e71307d0 | |||
| f517f12b7e | |||
| 510b1d178d | |||
| 890e8afd47 | |||
| c25ce6db9d | |||
| ec789de1d1 | |||
| e96ac16d85 | |||
| 9d6fe97b11 | |||
| 8e90552fec | |||
| 71c8d5527e | |||
| 9fef95303a | |||
| 8458da826e | |||
| df59f5eb6b | |||
| 7c0ec8677f | |||
| 2e23026690 | |||
| 7bc110820e | |||
| d3cddd5b60 | |||
| 24cff721dc | |||
| 054df44e05 | |||
| 2dc3af3761 | |||
| 3d9bf70c85 | |||
| 30f4f6e7b8 | |||
| c5c71859f9 | |||
| b1a12d88a1 | |||
| 78d06e79a5 | |||
| 7580f1526f | |||
| 198e76c291 | |||
| f47bb1484c | |||
| 91f5136e29 | |||
| da3be58eec | |||
| 94432b496b | |||
| eab2550b88 | |||
| 179a496737 | |||
| 8f62c2ab78 | |||
| 9eaee22e3b | |||
| 2bdfe8399d | |||
| 001570464c | |||
| 90e77010bb | |||
| 6b73294186 | |||
| 101237aa75 | |||
| 8d3377aeb3 | |||
| 3ad350b10b | |||
| ce00480d99 | |||
| 2e7f2b6004 | |||
| aad0eae1b2 | |||
| be3e64b1aa | |||
| c089ea7499 | |||
| 190010b3e3 | |||
| 4dcdc175ee | |||
| 35fe54c713 | |||
| fd22d55835 | |||
| c978d78bd4 | |||
| fb488596b8 | |||
| 9edfa48e23 | |||
| 25360c5fba | |||
| e8ed346f20 | |||
| 507a35a826 | |||
| e604cf97ae | |||
| 510753ae1c | |||
| 828dfb56a2 | |||
| 843a8dcd69 | |||
| 53255d5524 | |||
| 0f8a5149b5 | |||
| 442d7e5fb5 | |||
| 11011d5367 | |||
| b80afca458 | |||
| a93d9aab08 | |||
| 721d31d98d | |||
| 8d83cff966 | |||
| 7643ee7749 | |||
| 78b0d5eb96 | |||
| 517252240a | |||
| 173635cfd1 | |||
| 051a05435e | |||
| cb367596d1 | |||
| 37de238f92 | |||
| b977c0e31c | |||
| f58c4ec8d7 | |||
| 48b0815363 | |||
| 4f15c9f749 | |||
| 7dd5175063 | |||
| cb9c6739cb | |||
| e541430891 | |||
| 60057716ae | |||
| f684442cc0 | |||
| d4246d305e | |||
| d13fafb2da |
@@ -21,7 +21,19 @@ 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.12.0 · Jan 26, 2024 · AGI Hotline
|
||||
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
|
||||
|
||||
- **Side-by-Side Split Windows**: multitask with parallel conversations. [#208](https://github.com/enricoros/big-AGI/issues/208)
|
||||
- **Multi-Chat Mode**: message everyone, all at once. [#388](https://github.com/enricoros/big-AGI/issues/388)
|
||||
- **Export tables as CSV** - big thanks to @aj47. [#392](https://github.com/enricoros/big-AGI/pull/392)
|
||||
- **Adjustable Text Size**: enjoy denser chats. [#399](https://github.com/enricoros/big-AGI/issues/399)
|
||||
- Dev2 Persona Technology Preview
|
||||
- Better looking chats with improved spacing, fonts, and menus
|
||||
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-lmstudio.md), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/config-database.md) (thanks @ranfysvalle02), and speedups
|
||||
|
||||
### What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/95ceb03c-945d-4fdd-9a9f-3317beb54f3f
|
||||
|
||||
@@ -45,21 +57,11 @@ https://github.com/enricoros/big-AGI/assets/1590910/a6b8e172-0726-4b03-a5e5-10cf
|
||||
- Enable adding up to five custom OpenAI-compatible endpoints
|
||||
- Developer enhancements: new 'Actiles' framework
|
||||
|
||||
### What's New in 1.10.0 · Jan 6, 2024 · The Year of AGI
|
||||
|
||||
- **New UI**: for both desktop and mobile, sets the stage for future scale. [#201](https://github.com/enricoros/big-AGI/issues/201)
|
||||
- **Conversation Folders**: enhanced conversation organization. [#321](https://github.com/enricoros/big-AGI/issues/321)
|
||||
- **[LM Studio](https://lmstudio.ai/)** support and improved token management
|
||||
- Resizable panes in split-screen conversations.
|
||||
- Large performance optimizations
|
||||
- Developer enhancements: new UI framework, updated documentation for proxy settings on browserless/docker
|
||||
|
||||
For full details and former releases, check out the [changelog](docs/changelog.md).
|
||||
|
||||
## ✨ Key Features 👊
|
||||
|
||||

|
||||
[More](docs/pixels/big-AGI-compo2b.png), [screenshots](docs/pixels).
|
||||

|
||||
|
||||
- **AI Personas**: Tailor your AI interactions with customizable personas
|
||||
- **Sleek UI/UX**: A smooth, intuitive, and mobile-responsive interface
|
||||
|
||||
@@ -10,6 +10,18 @@ by release.
|
||||
- milestone: [1.13.0](https://github.com/enricoros/big-agi/milestone/13)
|
||||
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
|
||||
|
||||
## What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
|
||||
|
||||
- **Side-by-Side Split Windows**: multitask with parallel conversations. [#208](https://github.com/enricoros/big-AGI/issues/208)
|
||||
- **Multi-Chat Mode**: message everyone, all at once. [#388](https://github.com/enricoros/big-AGI/issues/388)
|
||||
- **Export tables as CSV** - big thanks to @aj47. [#392](https://github.com/enricoros/big-AGI/pull/392)
|
||||
- **Adjustable Text Size**: enjoy denser chats. [#399](https://github.com/enricoros/big-AGI/issues/399)
|
||||
- Dev2 Persona Technology Preview
|
||||
- Better looking chats with improved spacing, fonts, and menus
|
||||
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-lmstudio.md), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/config-database.md) (thanks @ranfysvalle02), and speedups
|
||||
|
||||
## What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/95ceb03c-945d-4fdd-9a9f-3317beb54f3f
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
**Connecting Your Database for Enhanced Features:**
|
||||
|
||||
This guide outlines the database options and setup steps for enabling features like Chat Link Sharing in your application.
|
||||
|
||||
### Choose Your Database:
|
||||
|
||||
**1. Serverless Postgres (default):**
|
||||
|
||||
- Available on Vercel, Neon, and other platforms.
|
||||
- Less feature-rich but a suitable option depending on your needs.
|
||||
- **Connection String:** Replace placeholders with your Postgres credentials.
|
||||
- `postgres://USER:PASS@SOMEHOST.postgres.vercel-storage.com/SOMEDB?pgbouncer=true&connect_timeout=15`
|
||||
|
||||
**2. MongoDB Atlas (alternative):**
|
||||
|
||||
- **Highly Recommended:** More than a database, it's a data platform. MongoDB Atlas is a robust cloud-based platform that offers scalability, security, and a suite of developer tools. No need for a separate vector database, you can query your vector embeddings right within your operational database!
|
||||
- **Additional Features:** MongoDB Atlas is packed with unique features designed to streamline the development process such as: Atlas App Services, Atlas search (with vector search), Atlas charts, Data Federation, and more.
|
||||
- **Connection String:** Replace placeholders with your Atlas credentials.
|
||||
- `mongodb://USER:PASS@CLUSTER-NAME.mongodb.net/DATABASE-NAME?retryWrites=true&w=majority`
|
||||
|
||||
### Environment Variables:
|
||||
|
||||
#### Postgres:
|
||||
| Variable | |
|
||||
|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `POSTGRES_PRISMA_URL` | `postgres://USER:PASS@SOMEHOST.postgres.vercel-storage.com/SOMEDB?pgbouncer=true&connect_timeout=15` |
|
||||
| `POSTGRES_URL_NON_POOLING` (optional) | URL for the Postgres database without pooling (specific use cases) |
|
||||
|
||||
|
||||
#### MongoDB:
|
||||
| Variable | |
|
||||
|--------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `MDB_URI` | `mongodb://USER:PASS@CLUSTER-NAME.mongodb.net/DATABASE-NAME?retryWrites=true&w=majority` |
|
||||
|
||||
### MongoDB Atlas + Prisma
|
||||
When using MongoDB Atlas, you'll need to make the below changes to the file `prisma.schema`
|
||||
|
||||
```
|
||||
...
|
||||
datasource db {
|
||||
provider = "mongodb"
|
||||
url = env("MDB_URI")
|
||||
}
|
||||
|
||||
//
|
||||
// Storage of Linked Data
|
||||
//
|
||||
model LinkStorage {
|
||||
id String @id @default(uuid()) @map("_id")
|
||||
|
||||
// ...rest of file
|
||||
```
|
||||
|
||||
### Initial Setup Steps:
|
||||
|
||||
1. **Run `npx prisma db:push`:** Create or update the database schema (run once after connecting).
|
||||
|
||||
|
||||
### Additional Resources:
|
||||
|
||||
- Prisma documentation: [https://www.prisma.io/docs/](https://www.prisma.io/docs/)
|
||||
- MongoDB Atlas: [https://www.mongodb.com/atlas/database](https://www.mongodb.com/atlas/database)
|
||||
- Atlas App Services: [https://www.mongodb.com/docs/atlas/app-services/](https://www.mongodb.com/docs/atlas/app-services/)
|
||||
- Atlas vector search: [https://www.mongodb.com/products/platform/atlas-vector-search/](https://www.mongodb.com/products/platform/atlas-vector-search)
|
||||
- Atlas Data Federation: [https://www.mongodb.com/products/platform/atlas-data-federation](https://www.mongodb.com/products/platform/atlas-data-federation)
|
||||
@@ -0,0 +1,51 @@
|
||||
# Integrating LM Studio with big-AGI
|
||||
|
||||
Quickly set up LM Studio with big-AGI to run local and open LLMs on your computer for enhanced privacy and control over AI interactions.
|
||||
|
||||
## Video Tutorial
|
||||
|
||||
For a visual step-by-step guide, watch our [YouTube tutorial](https://www.youtube.com/watch?v=MqXzxVokMDk).
|
||||
|
||||
[](http://www.youtube.com/watch?v=MqXzxVokMDk "Running big-AGI locally with LM Studio")
|
||||
|
||||
|
||||
## Quick Setup Guide
|
||||
|
||||
### Installing big-AGI
|
||||
|
||||
Clone and set up big-AGI:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/enricoros/big-agi.git && cd big-agi
|
||||
npm install # Or: yarn install
|
||||
npm run dev # Or: yarn dev
|
||||
# If missing dependencies:
|
||||
npm install @mui/material # Or: yarn add @mui/material
|
||||
```
|
||||
|
||||
### Configuring LM Studio
|
||||
|
||||
Ensure LM Studio is running (default: [http://localhost:1234](http://localhost:1234)).
|
||||
Check the URL and modify if different.
|
||||
1. Download local models in LM Studio
|
||||
2. Start the LM Studio server
|
||||
3. Optionally. Check the logs
|
||||
|
||||
### Integration in big-AGI
|
||||
|
||||
1. In big-AGI, navigate to **Models** > **Add** > **LM Studio**
|
||||
2. Enter the API URL: `http://localhost:1234` (modify if different)
|
||||
3. Refresh by clicking on the `Models` button to load models from LM Studio
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Missing @mui/material**: Execute `npm install @mui/material` or `yarn add @mui/material`
|
||||
- **Connection Issues**: Check LM Studio's URL and ensure it's operational
|
||||
|
||||
|
||||
## Further Assistance
|
||||
|
||||
Advanced configurations and more:
|
||||
|
||||
- big-AGI Community: [Discord](https://discord.gg/MkH4qj2Jp9)
|
||||
- LM Studio: [LM Studio home page](https://lmstudio.ai/)
|
||||
@@ -12,10 +12,13 @@ Environment variables can be set by creating a `.env` file in the root directory
|
||||
The following is an example `.env` for copy-paste convenience:
|
||||
|
||||
```bash
|
||||
# Database
|
||||
# Database (Postgres)
|
||||
POSTGRES_PRISMA_URL=
|
||||
POSTGRES_URL_NON_POOLING=
|
||||
|
||||
# Database (MongoDB)
|
||||
MDB_URI=
|
||||
|
||||
# LLMs
|
||||
OPENAI_API_KEY=
|
||||
OPENAI_API_HOST=
|
||||
@@ -57,16 +60,9 @@ HTTP_BASIC_AUTH_PASSWORD=
|
||||
|
||||
### Database
|
||||
|
||||
To enable features such as Chat Link Shring, you need to connect the backend to a database. We require
|
||||
serverless Postgres, which is available on Vercel, Neon and more.
|
||||
For Database configuration see [config-database.md](config-database.md).
|
||||
|
||||
Also make sure that you run `npx prisma db:push` to create the initial schema on the database for the
|
||||
first time (or update it on a later stage).
|
||||
|
||||
| Variable | Description |
|
||||
|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `POSTGRES_PRISMA_URL` | The URL of the Postgres database used by Prisma - example: `postgres://USER:PASS@SOMEHOST.postgres.vercel-storage.com/SOMEDB?pgbouncer=true&connect_timeout=15` |
|
||||
| `POSTGRES_URL_NON_POOLING` | The URL of the Postgres database without pooling |
|
||||
To enable features such as Chat Link Sharing, you need to connect the backend to a database. We currently support Postgres and MongoDB.
|
||||
|
||||
### LLMs
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 303 KiB |
Generated
+410
-304
File diff suppressed because it is too large
Load Diff
+15
-12
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.12.0",
|
||||
"version": "1.13.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -17,10 +17,10 @@
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.6",
|
||||
"@mui/joy": "^5.0.0-beta.24",
|
||||
"@mui/icons-material": "^5.15.8",
|
||||
"@mui/joy": "5.0.0-beta.25",
|
||||
"@next/bundle-analyzer": "^14.1.0",
|
||||
"@prisma/client": "^5.8.1",
|
||||
"@prisma/client": "^5.9.1",
|
||||
"@sanity/diff-match-patch": "^3.1.1",
|
||||
"@t3-oss/env-nextjs": "^0.8.0",
|
||||
"@tanstack/react-query": "~4.36.1",
|
||||
@@ -28,8 +28,8 @@
|
||||
"@trpc/next": "10.44.1",
|
||||
"@trpc/react-query": "10.44.1",
|
||||
"@trpc/server": "10.44.1",
|
||||
"@vercel/analytics": "^1.1.2",
|
||||
"@vercel/speed-insights": "^1.0.8",
|
||||
"@vercel/analytics": "^1.1.3",
|
||||
"@vercel/speed-insights": "^1.0.9",
|
||||
"browser-fs-access": "^0.35.0",
|
||||
"eventsource-parser": "^1.1.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
@@ -40,35 +40,38 @@
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^1.0.9",
|
||||
"react-player": "^2.14.1",
|
||||
"react-resizable-panels": "^2.0.3",
|
||||
"react-timeago": "^7.2.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"superjson": "^2.2.1",
|
||||
"tesseract.js": "^5.0.4",
|
||||
"tiktoken": "^1.0.11",
|
||||
"tiktoken": "^1.0.13",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zustand": "^4.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/puppeteer": "^0.0.5",
|
||||
"@types/node": "^20.11.7",
|
||||
"@types/node": "^20.11.16",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-csv": "^1.1.10",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.1.0",
|
||||
"prettier": "^3.2.4",
|
||||
"prisma": "^5.8.1",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.9.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ export default function MyDocument({ emotionStyleTags }: MyDocumentProps) {
|
||||
<link rel='shortcut icon' href='/favicon.ico' />
|
||||
<link rel='icon' type='image/png' sizes='32x32' href='/icons/favicon-32x32.png' />
|
||||
<link rel='icon' type='image/png' sizes='16x16' href='/icons/favicon-16x16.png' />
|
||||
<link rel='apple-touch-icon' sizes='180x180' href='/icons/apple-touch-icon.png' />
|
||||
<link rel='apple-touch-icon' sizes='180x180' href='/apple-touch-icon.png' />
|
||||
<link rel='manifest' href='/manifest.json' />
|
||||
<meta name='apple-mobile-web-app-capable' content='yes' />
|
||||
<meta name='apple-mobile-web-app-status-bar-style' content='black' />
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box } from '@mui/joy';
|
||||
|
||||
// import { AppWorkspace } from '../src/apps/personas/AppWorkspace';
|
||||
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function PersonasPage() {
|
||||
return withLayout({ type: 'optima' }, <Box />);
|
||||
}
|
||||
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 7.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 254 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
+68
-40
@@ -9,7 +9,9 @@ import MicIcon from '@mui/icons-material/Mic';
|
||||
import MicNoneIcon from '@mui/icons-material/MicNone';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
|
||||
import { useChatLLMDropdown } from '../chat/components/applayout/useLLMDropdown';
|
||||
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '../chat/components/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { useChatLLMDropdown } from '../chat/components/useLLMDropdown';
|
||||
|
||||
import { EXPERIMENTAL_speakTextStream } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
import { SystemPurposeId, SystemPurposes } from '../../data';
|
||||
@@ -313,52 +315,74 @@ export function Telephone(props: {
|
||||
|
||||
{/* Live Transcript, w/ streaming messages, audio indication, etc. */}
|
||||
{(isConnected || isEnded) && (
|
||||
<Card variant='soft' sx={{
|
||||
<Card variant='outlined' sx={{
|
||||
flexGrow: 1,
|
||||
maxHeight: '24%',
|
||||
minHeight: '15%',
|
||||
maxHeight: '28%',
|
||||
minHeight: '20%',
|
||||
width: '100%',
|
||||
|
||||
// style
|
||||
backgroundColor: 'background.surface',
|
||||
// backgroundColor: 'background.surface',
|
||||
borderRadius: 'lg',
|
||||
boxShadow: 'sm',
|
||||
// boxShadow: 'sm',
|
||||
|
||||
// children
|
||||
display: 'flex', flexDirection: 'column-reverse',
|
||||
overflow: 'auto',
|
||||
padding: 0, // move this to the ScrollToBottom component
|
||||
}}>
|
||||
|
||||
{/* Messages in reverse order, for auto-scroll from the bottom */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column-reverse', gap: 1 }}>
|
||||
<ScrollToBottom
|
||||
// bootToBottom
|
||||
stickToBottom
|
||||
sx={{
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
|
||||
{/* Listening... */}
|
||||
{isRecording && (
|
||||
<CallMessage
|
||||
text={<>{speechInterim?.transcript ? speechInterim.transcript + ' ' : ''}<i>{speechInterim?.interimTranscript}</i></>}
|
||||
variant={isRecordingSpeech ? 'solid' : 'outlined'}
|
||||
role='user'
|
||||
/>
|
||||
)}
|
||||
// content
|
||||
display: 'grid',
|
||||
padding: 1,
|
||||
}}
|
||||
>
|
||||
|
||||
{/* Persona streaming text... */}
|
||||
{!!personaTextInterim && (
|
||||
<CallMessage
|
||||
text={personaTextInterim}
|
||||
variant='solid' color='neutral'
|
||||
role='assistant'
|
||||
/>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
{/* Messages (last 6 messages, in reverse order) */}
|
||||
{callMessages.slice(-6).reverse().map((message) =>
|
||||
<CallMessage
|
||||
key={message.id}
|
||||
text={message.text}
|
||||
variant={message.role === 'assistant' ? 'solid' : 'soft'} color='neutral'
|
||||
role={message.role} />,
|
||||
)}
|
||||
</Box>
|
||||
{/* Call Messages [] */}
|
||||
{callMessages.map((message) =>
|
||||
<CallMessage
|
||||
key={message.id}
|
||||
text={message.text}
|
||||
variant={message.role === 'assistant' ? 'solid' : 'soft'}
|
||||
color={message.role === 'assistant' ? 'neutral' : 'primary'}
|
||||
role={message.role} />,
|
||||
)}
|
||||
|
||||
{/* Persona streaming text... */}
|
||||
{!!personaTextInterim && (
|
||||
<CallMessage
|
||||
text={personaTextInterim}
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
role='assistant'
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Listening... */}
|
||||
{isRecording && (
|
||||
<CallMessage
|
||||
text={<>{speechInterim?.transcript.trim() || null}{speechInterim?.interimTranscript.trim() ? <i> {speechInterim.interimTranscript}</i> : null}</>}
|
||||
variant={(isRecordingSpeech || !!speechInterim?.transcript) ? 'soft' : 'outlined'}
|
||||
color='primary'
|
||||
role='user'
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Visibility and actions are handled via Context */}
|
||||
<ScrollToBottomButton />
|
||||
|
||||
</ScrollToBottom>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
@@ -371,11 +395,15 @@ export function Telephone(props: {
|
||||
|
||||
{/* [Calling] Hang / PTT (mute not enabled yet) */}
|
||||
{isConnected && <CallButton Icon={CallEndIcon} text='Hang up' color='danger' variant='soft' onClick={handleCallStop} />}
|
||||
{isConnected && (pushToTalk
|
||||
? <CallButton Icon={MicIcon} onClick={toggleRecording}
|
||||
text={isRecordingSpeech ? 'Listening...' : isRecording ? 'Listening' : 'Push To Talk'}
|
||||
variant={isRecordingSpeech ? 'solid' : isRecording ? 'soft' : 'outlined'} sx={!isRecording ? { backgroundColor: 'background.surface' } : undefined} />
|
||||
: null
|
||||
{isConnected && (pushToTalk ? (
|
||||
<CallButton
|
||||
Icon={MicIcon} onClick={toggleRecording}
|
||||
text={isRecordingSpeech ? 'Listening...' : isRecording ? 'Listening' : 'Push To Talk'}
|
||||
variant={isRecordingSpeech ? 'solid' : isRecording ? 'soft' : 'outlined'}
|
||||
color='primary'
|
||||
sx={!isRecording ? { backgroundColor: 'background.surface' } : undefined}
|
||||
/>
|
||||
) : null
|
||||
// <CallButton disabled={true} Icon={MicOffIcon} onClick={() => setMicMuted(muted => !muted)}
|
||||
// text={micMuted ? 'Muted' : 'Mute'}
|
||||
// color={micMuted ? 'warning' : undefined} variant={micMuted ? 'solid' : 'outlined'} />
|
||||
|
||||
@@ -19,9 +19,9 @@ export function CallMessage(props: {
|
||||
alignSelf: props.role === 'user' ? 'end' : 'start',
|
||||
whiteSpace: 'break-spaces',
|
||||
borderRadius: 'lg',
|
||||
mt: 'auto',
|
||||
// boxShadow: 'md',
|
||||
py: 1,
|
||||
px: 1.5,
|
||||
...(props.sx || {}),
|
||||
}}
|
||||
>
|
||||
|
||||
+129
-97
@@ -1,6 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import { useTheme } from '@mui/joy';
|
||||
|
||||
@@ -15,18 +14,18 @@ import { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { GoodPanelResizeHandler } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useConversation } from '~/common/state/store-chats';
|
||||
import { themeBgAppChatComposer } from '~/common/app.theme';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
|
||||
import { ChatDrawerMemo } from './components/applayout/ChatDrawer';
|
||||
import { ChatDropdowns } from './components/applayout/ChatDropdowns';
|
||||
import { ChatMenuItems } from './components/applayout/ChatMenuItems';
|
||||
import { ChatDrawerMemo } from './components/ChatDrawer';
|
||||
import { ChatDropdowns } from './components/ChatDropdowns';
|
||||
import { ChatPageMenuItems } from './components/ChatPageMenuItems';
|
||||
import { ChatMessageList } from './components/ChatMessageList';
|
||||
import { Composer } from './components/composer/Composer';
|
||||
import { Ephemerals } from './components/Ephemerals';
|
||||
@@ -55,19 +54,22 @@ const SPECIAL_ID_WIPE_ALL: DConversationId = 'wipe-chats';
|
||||
export function AppChat() {
|
||||
|
||||
// state
|
||||
const [isComposerMulticast, setIsComposerMulticast] = React.useState(false);
|
||||
const [isMessageSelectionMode, setIsMessageSelectionMode] = React.useState(false);
|
||||
const [diagramConfig, setDiagramConfig] = React.useState<DiagramConfig | null>(null);
|
||||
const [tradeConfig, setTradeConfig] = React.useState<TradeConfig | null>(null);
|
||||
const [clearConversationId, setClearConversationId] = React.useState<DConversationId | null>(null);
|
||||
const [deleteConversationId, setDeleteConversationId] = React.useState<DConversationId | null>(null);
|
||||
const [flattenConversationId, setFlattenConversationId] = React.useState<DConversationId | null>(null);
|
||||
const showNextTitle = React.useRef(false);
|
||||
const showNextTitleChange = React.useRef(false);
|
||||
const composerTextAreaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
const [_activeFolderId, setActiveFolderId] = React.useState<string | null>(null);
|
||||
|
||||
// external state
|
||||
const theme = useTheme();
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const { openLlmOptions } = useOptimaLayout();
|
||||
|
||||
const { chatLLM } = useChatLLM();
|
||||
@@ -78,8 +80,7 @@ export function AppChat() {
|
||||
navigateHistoryInFocusedPane,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
paneIndex,
|
||||
duplicatePane,
|
||||
focusedPaneIndex,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
} = usePanesManager();
|
||||
@@ -87,6 +88,7 @@ export function AppChat() {
|
||||
const {
|
||||
title: focusedChatTitle,
|
||||
chatIdx: focusedChatNumber,
|
||||
isNoChat: isNoChat,
|
||||
isChatEmpty: isFocusedChatEmpty,
|
||||
areChatsEmpty,
|
||||
newConversationId,
|
||||
@@ -112,8 +114,10 @@ export function AppChat() {
|
||||
|
||||
// Window actions
|
||||
|
||||
const panesConversationIDs = chatPanes.length > 0 ? chatPanes.map((pane) => pane.conversationId) : [null];
|
||||
const isSplitPane = chatPanes.length > 1;
|
||||
const isMultiPane = chatPanes.length >= 2;
|
||||
const isMultiAddable = chatPanes.length < 4;
|
||||
const isMultiConversationId = isMultiPane && new Set(chatPanes.map((pane) => pane.conversationId)).size >= 2;
|
||||
const willMulticast = isComposerMulticast && isMultiConversationId;
|
||||
|
||||
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
|
||||
conversationId && openConversationInFocusedPane(conversationId);
|
||||
@@ -123,21 +127,14 @@ 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;
|
||||
showNextTitleChange.current = true;
|
||||
}, [navigateHistoryInFocusedPane]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (showNextTitle.current) {
|
||||
showNextTitle.current = false;
|
||||
if (showNextTitleChange.current) {
|
||||
showNextTitleChange.current = false;
|
||||
const title = (focusedChatNumber >= 0 ? `#${focusedChatNumber + 1} · ` : '') + (focusedChatTitle || 'New Chat');
|
||||
const id = addSnackbar({ key: 'focused-title', message: title, type: 'title' });
|
||||
return () => removeSnackbar(id);
|
||||
@@ -169,6 +166,13 @@ export function AppChat() {
|
||||
return await runReActUpdatingState(conversationId, chatCommand.params!, chatLLMId);
|
||||
|
||||
case 'chat-alter':
|
||||
if (chatCommand.command === '/clear') {
|
||||
if (chatCommand.params === 'all')
|
||||
return setMessages(conversationId, []);
|
||||
const helpMessage = createDMessage('assistant', 'This command requires the \'all\' parameter to confirm the operation.');
|
||||
helpMessage.originLLM = Brand.Title.Base;
|
||||
return setMessages(conversationId, [...history, helpMessage]);
|
||||
}
|
||||
Object.assign(lastMessage, {
|
||||
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
|
||||
sender: 'Bot',
|
||||
@@ -234,17 +238,25 @@ export function AppChat() {
|
||||
}
|
||||
const userText = multiPartMessage[0].text;
|
||||
|
||||
// find conversation
|
||||
const conversation = getConversation(conversationId);
|
||||
if (!conversation)
|
||||
return false;
|
||||
// multicast: send the message to all the panes
|
||||
const uniqueIds = new Set([conversationId]);
|
||||
if (willMulticast)
|
||||
chatPanes.forEach(pane => pane.conversationId && uniqueIds.add(pane.conversationId));
|
||||
|
||||
// start execution (async)
|
||||
void _handleExecute(chatModeId, conversationId, [
|
||||
...conversation.messages,
|
||||
createDMessage('user', userText),
|
||||
]);
|
||||
return true;
|
||||
// we loop to handle both the normal and multicast modes
|
||||
let enqueued = false;
|
||||
for (const _cId of uniqueIds) {
|
||||
const _conversation = getConversation(_cId);
|
||||
if (_conversation) {
|
||||
// start execution fire/forget
|
||||
void _handleExecute(chatModeId, _cId, [
|
||||
..._conversation.messages,
|
||||
createDMessage('user', userText),
|
||||
]);
|
||||
enqueued = true;
|
||||
}
|
||||
}
|
||||
return enqueued;
|
||||
};
|
||||
|
||||
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[]): Promise<void> => {
|
||||
@@ -281,10 +293,10 @@ export function AppChat() {
|
||||
|
||||
// Chat actions
|
||||
|
||||
const handleConversationNew = React.useCallback(() => {
|
||||
const handleConversationNew = React.useCallback((forceNoRecycle?: boolean) => {
|
||||
|
||||
// activate an existing new conversation if present, or create another
|
||||
const conversationId = newConversationId
|
||||
const conversationId = (newConversationId && !forceNoRecycle)
|
||||
? newConversationId
|
||||
: prependNewConversation(focusedSystemPurposeId ?? undefined);
|
||||
setFocusedConversationId(conversationId);
|
||||
@@ -300,29 +312,29 @@ export function AppChat() {
|
||||
|
||||
const handleConversationImportDialog = React.useCallback(() => setTradeConfig({ dir: 'import' }), []);
|
||||
|
||||
const handleConversationExport = React.useCallback((conversationId: DConversationId | null) => setTradeConfig({ dir: 'export', conversationId }), []);
|
||||
const handleConversationExport = React.useCallback((conversationId: DConversationId | null, exportAll: boolean) => {
|
||||
setTradeConfig({ dir: 'export', conversationId, exportAll });
|
||||
}, []);
|
||||
|
||||
const handleConversationBranch = React.useCallback((conversationId: DConversationId, messageId: string | null): DConversationId | null => {
|
||||
showNextTitle.current = true;
|
||||
const branchedConversationId = branchConversation(conversationId, messageId);
|
||||
addSnackbar({
|
||||
key: 'branch-conversation',
|
||||
message: 'Branch started.',
|
||||
type: 'success',
|
||||
overrides: {
|
||||
autoHideDuration: 3000,
|
||||
startDecorator: <ForkRightIcon />,
|
||||
},
|
||||
});
|
||||
const branchInAltPanel = useUXLabsStore.getState().labsSplitBranching;
|
||||
if (branchInAltPanel)
|
||||
const handleConversationBranch = React.useCallback((srcConversationId: DConversationId, messageId: string | null): DConversationId | null => {
|
||||
// clone data
|
||||
const branchedConversationId = branchConversation(srcConversationId, messageId);
|
||||
|
||||
// if a folder is active, add the new conversation to the folder
|
||||
if (activeFolderId && branchedConversationId)
|
||||
useFolderStore.getState().addConversationToFolder(activeFolderId, branchedConversationId);
|
||||
|
||||
// replace/open a new pane with this
|
||||
showNextTitleChange.current = true;
|
||||
if (isMultiAddable)
|
||||
openSplitConversationId(branchedConversationId);
|
||||
else
|
||||
setFocusedConversationId(branchedConversationId);
|
||||
return branchedConversationId;
|
||||
}, [branchConversation, openSplitConversationId, setFocusedConversationId]);
|
||||
|
||||
const handleConversationFlatten = (conversationId: DConversationId) => setFlattenConversationId(conversationId);
|
||||
return branchedConversationId;
|
||||
}, [activeFolderId, branchConversation, isMultiAddable, openSplitConversationId, setFocusedConversationId]);
|
||||
|
||||
const handleConversationFlatten = React.useCallback((conversationId: DConversationId) => setFlattenConversationId(conversationId), []);
|
||||
|
||||
const handleConfirmedClearConversation = React.useCallback(() => {
|
||||
if (clearConversationId) {
|
||||
@@ -333,27 +345,24 @@ export function AppChat() {
|
||||
|
||||
const handleConversationClear = React.useCallback((conversationId: DConversationId) => setClearConversationId(conversationId), []);
|
||||
|
||||
const handleConfirmedDeleteConversation = () => {
|
||||
if (deleteConversationId) {
|
||||
let nextConversationId: DConversationId | null;
|
||||
if (deleteConversationId === SPECIAL_ID_WIPE_ALL)
|
||||
nextConversationId = wipeAllConversations(focusedSystemPurposeId ?? undefined, activeFolderId);
|
||||
else
|
||||
nextConversationId = deleteConversation(deleteConversationId);
|
||||
setFocusedConversationId(nextConversationId);
|
||||
setDeleteConversationId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConversationsDeleteAll = React.useCallback(() => 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) => {
|
||||
// show dialog if not bypassed
|
||||
if (!bypassConfirmation)
|
||||
return setDeleteConversationId(conversationId);
|
||||
|
||||
const nextConversationId = conversationId === SPECIAL_ID_WIPE_ALL
|
||||
? wipeAllConversations(activeFolderId /* restricted to this folder (or null for all) */, /*focusedSystemPurposeId ??*/ undefined)
|
||||
: deleteConversation(conversationId, /*focusedSystemPurposeId ??*/ undefined);
|
||||
setFocusedConversationId(nextConversationId);
|
||||
|
||||
setDeleteConversationId(null);
|
||||
}, [activeFolderId, deleteConversation, setFocusedConversationId, wipeAllConversations]);
|
||||
|
||||
const handleConfirmedDeleteConversation = React.useCallback(() => {
|
||||
deleteConversationId && handleConversationDelete(deleteConversationId, true);
|
||||
}, [deleteConversationId, handleConversationDelete]);
|
||||
|
||||
// Shortcuts
|
||||
|
||||
@@ -380,17 +389,16 @@ export function AppChat() {
|
||||
const centerItems = React.useMemo(() =>
|
||||
<ChatDropdowns
|
||||
conversationId={focusedConversationId}
|
||||
isSplitPanes={isSplitPane}
|
||||
onToggleSplitPanes={toggleSplitPane}
|
||||
/>,
|
||||
[focusedConversationId, isSplitPane, toggleSplitPane],
|
||||
[focusedConversationId],
|
||||
);
|
||||
|
||||
const drawerContent = React.useMemo(() =>
|
||||
<ChatDrawerMemo
|
||||
activeConversationId={focusedConversationId}
|
||||
activeFolderId={activeFolderId}
|
||||
disableNewButton={isFocusedChatEmpty}
|
||||
chatPanesConversationIds={chatPanes.map(pane => pane.conversationId).filter(Boolean) as DConversationId[]}
|
||||
disableNewButton={isFocusedChatEmpty && !isNoChat}
|
||||
onConversationActivate={setFocusedConversationId}
|
||||
onConversationDelete={handleConversationDelete}
|
||||
onConversationExportDialog={handleConversationExport}
|
||||
@@ -399,21 +407,23 @@ export function AppChat() {
|
||||
onConversationsDeleteAll={handleConversationsDeleteAll}
|
||||
setActiveFolderId={setActiveFolderId}
|
||||
/>,
|
||||
[activeFolderId, focusedConversationId, handleConversationDelete, handleConversationExport, handleConversationImportDialog, handleConversationNew, handleConversationsDeleteAll, isFocusedChatEmpty, setFocusedConversationId],
|
||||
[activeFolderId, chatPanes, focusedConversationId, handleConversationDelete, handleConversationExport, handleConversationImportDialog, handleConversationNew, handleConversationsDeleteAll, isFocusedChatEmpty, isNoChat, setFocusedConversationId],
|
||||
);
|
||||
|
||||
const menuItems = React.useMemo(() =>
|
||||
<ChatMenuItems
|
||||
<ChatPageMenuItems
|
||||
isMobile={isMobile}
|
||||
conversationId={focusedConversationId}
|
||||
disableItems={!focusedConversationId || isFocusedChatEmpty}
|
||||
hasConversations={!areChatsEmpty}
|
||||
isConversationEmpty={isFocusedChatEmpty}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationClear={handleConversationClear}
|
||||
onConversationFlatten={handleConversationFlatten}
|
||||
// onConversationNew={handleConversationNew}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
/>,
|
||||
[areChatsEmpty, focusedConversationId, handleConversationBranch, handleConversationClear, isFocusedChatEmpty, isMessageSelectionMode],
|
||||
[areChatsEmpty, focusedConversationId, handleConversationBranch, handleConversationClear, handleConversationFlatten, /*handleConversationNew,*/ isFocusedChatEmpty, isMessageSelectionMode, isMobile],
|
||||
);
|
||||
|
||||
usePluggableOptimaLayout(drawerContent, centerItems, menuItems, 'AppChat');
|
||||
@@ -421,32 +431,45 @@ export function AppChat() {
|
||||
return <>
|
||||
|
||||
<PanelGroup
|
||||
direction='horizontal'
|
||||
direction={isMobile ? 'vertical' : 'horizontal'}
|
||||
id='app-chat-panels'
|
||||
>
|
||||
|
||||
{panesConversationIDs.map((_conversationId, idx, panels) =>
|
||||
<React.Fragment key={`chat-pane-${idx}-${panels.length}-${_conversationId}`}>
|
||||
{chatPanes.map((pane, idx) => {
|
||||
const _paneConversationId = pane.conversationId;
|
||||
const _panesCount = chatPanes.length;
|
||||
const _keyAndId = `chat-pane-${idx}-${_paneConversationId}`;
|
||||
const _sepId = `sep-pane-${idx}-${_paneConversationId}`;
|
||||
return <React.Fragment key={_keyAndId}>
|
||||
|
||||
<Panel
|
||||
id={'chat-pane-' + _conversationId}
|
||||
id={_keyAndId}
|
||||
order={idx}
|
||||
collapsible
|
||||
defaultSize={panels.length > 0 ? Math.round(100 / panels.length) : undefined}
|
||||
collapsible={chatPanes.length === 2}
|
||||
defaultSize={(_panesCount === 3 && idx === 1) ? 34 : Math.round(100 / _panesCount)}
|
||||
minSize={20}
|
||||
onClick={(event) => {
|
||||
const setFocus = chatPanes.length < 2 || !event.altKey;
|
||||
setFocusedPane(setFocus ? idx : -1);
|
||||
}}
|
||||
onCollapse={() => setTimeout(() => removePane(idx), 50)}
|
||||
onCollapse={() => {
|
||||
// NOTE: despite the delay to try to let the draggin settle, there seems to be an issue with the Pane locking the screen
|
||||
// setTimeout(() => removePane(idx), 50);
|
||||
// more than 2 will result in an assertion from the framework
|
||||
if (chatPanes.length === 2) removePane(idx);
|
||||
}}
|
||||
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}` }),
|
||||
...(isMultiPane ? {
|
||||
borderRadius: '0.375rem',
|
||||
border: `2px solid ${idx === focusedPaneIndex
|
||||
? ((willMulticast || !isMultiConversationId) ? theme.palette.primary.solidBg : theme.palette.primary.solidBg)
|
||||
: ((willMulticast || !isMultiConversationId) ? theme.palette.warning.softActiveBg : theme.palette.background.level1)}`,
|
||||
filter: (!willMulticast && idx !== focusedPaneIndex)
|
||||
? (!isMultiConversationId ? 'grayscale(66.67%)' /* clone of the same */ : 'grayscale(66.67%)')
|
||||
: undefined,
|
||||
} : {}),
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -462,10 +485,11 @@ export function AppChat() {
|
||||
>
|
||||
|
||||
<ChatMessageList
|
||||
conversationId={_conversationId}
|
||||
conversationId={_paneConversationId}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
isMobile={isMobile}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
@@ -478,7 +502,7 @@ export function AppChat() {
|
||||
/>
|
||||
|
||||
<Ephemerals
|
||||
conversationId={_conversationId}
|
||||
conversationId={_paneConversationId}
|
||||
sx={{
|
||||
// TODO: Fixme post panels?
|
||||
// flexGrow: 0.1,
|
||||
@@ -494,20 +518,28 @@ export function AppChat() {
|
||||
</Panel>
|
||||
|
||||
{/* Panel Separators & Resizers */}
|
||||
{idx < panels.length - 1 && <GoodPanelResizeHandler />}
|
||||
{idx < _panesCount - 1 && (
|
||||
<PanelResizeHandle id={_sepId}>
|
||||
<PanelResizeInset />
|
||||
</PanelResizeHandle>
|
||||
)}
|
||||
|
||||
</React.Fragment>)}
|
||||
</React.Fragment>;
|
||||
})}
|
||||
|
||||
</PanelGroup>
|
||||
|
||||
<Composer
|
||||
isMobile={isMobile}
|
||||
chatLLM={chatLLM}
|
||||
composerTextAreaRef={composerTextAreaRef}
|
||||
conversationId={focusedConversationId}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
isMulticast={!isMultiConversationId ? null : isComposerMulticast}
|
||||
isDeveloperMode={focusedSystemPurposeId === 'Developer'}
|
||||
onAction={handleComposerAction}
|
||||
onTextImagine={handleTextImagine}
|
||||
setIsMulticast={setIsComposerMulticast}
|
||||
sx={{
|
||||
zIndex: 21, // position: 'sticky', bottom: 0,
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsAlter: ICommandsProvider = {
|
||||
id: 'chat-alter',
|
||||
rank: 20,
|
||||
rank: 25,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/assistant',
|
||||
@@ -14,6 +16,11 @@ export const CommandsAlter: ICommandsProvider = {
|
||||
alternatives: ['/s'],
|
||||
arguments: ['text'],
|
||||
description: 'Injects system message',
|
||||
}, {
|
||||
primary: '/clear',
|
||||
arguments: ['all'],
|
||||
description: 'Clears the chat (removes all messages)',
|
||||
Icon: ClearIcon,
|
||||
}],
|
||||
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
export const CommandsBrowse: ICommandsProvider = {
|
||||
id: 'ass-browse',
|
||||
rank: 25,
|
||||
rank: 20,
|
||||
|
||||
getCommands: () => [{
|
||||
primary: '/browse',
|
||||
|
||||
+40
-20
@@ -3,9 +3,10 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, IconButton, ListDivider, ListItem, ListItemButton, ListItemDecorator, Tooltip } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import FileDownloadIcon from '@mui/icons-material/FileDownload';
|
||||
import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import FolderOpenOutlinedIcon from '@mui/icons-material/FolderOpenOutlined';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
@@ -16,12 +17,13 @@ import { DFolder, 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 { themeZIndexOverMobileDrawer } from '~/common/app.theme';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { ChatDrawerItemMemo, ChatNavigationItemData, FolderChangeRequest } from './ChatDrawerItem';
|
||||
import { ChatFolderList } from './folder/ChatFolderList';
|
||||
import { ClearFolderText } from './folder/useFolderDropdown';
|
||||
import { ChatFolderList } from './folders/ChatFolderList';
|
||||
import { ClearFolderText } from './folders/useFolderDropdown';
|
||||
|
||||
|
||||
// this is here to make shallow comparisons work on the next hook
|
||||
@@ -46,11 +48,24 @@ export const useFolders = (activeFolderId: string | null) => useFolderStore(({ e
|
||||
}, shallow);
|
||||
|
||||
|
||||
/*
|
||||
* Returns a string with the pane indices where the conversation is also open, or false if it's not
|
||||
*/
|
||||
function findOpenInViewNumbers(chatPanesConversationIds: DConversationId[], ourId: DConversationId): string | false {
|
||||
if (chatPanesConversationIds.length <= 1) return false;
|
||||
return chatPanesConversationIds.reduce((acc: string[], id, idx) => {
|
||||
if (id === ourId)
|
||||
acc.push((idx + 1).toString());
|
||||
return acc;
|
||||
}, []).join(', ') || false;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* 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 useChatNavigationItemsData = (activeFolder: DFolder | null, allFolders: DFolder[], activeConversationId: DConversationId | null): ChatNavigationItemData[] =>
|
||||
export const useChatNavigationItemsData = (activeFolder: DFolder | null, allFolders: DFolder[], activeConversationId: DConversationId | null, chatPanesConversationIds: DConversationId[]): ChatNavigationItemData[] =>
|
||||
useChatStore(({ conversations }) => {
|
||||
|
||||
const activeConversations = activeFolder
|
||||
@@ -60,6 +75,7 @@ export const useChatNavigationItemsData = (activeFolder: DFolder | null, allFold
|
||||
return activeConversations.map((_c): ChatNavigationItemData => ({
|
||||
conversationId: _c.id,
|
||||
isActive: _c.id === activeConversationId,
|
||||
isAlsoOpen: findOpenInViewNumbers(chatPanesConversationIds, _c.id),
|
||||
isEmpty: !_c.messages.length && !_c.userTitle,
|
||||
title: conversationTitle(_c),
|
||||
folder: !allFolders.length
|
||||
@@ -83,12 +99,13 @@ export const ChatDrawerMemo = React.memo(ChatDrawer);
|
||||
function ChatDrawer(props: {
|
||||
activeConversationId: DConversationId | null,
|
||||
activeFolderId: string | null,
|
||||
chatPanesConversationIds: DConversationId[],
|
||||
disableNewButton: boolean,
|
||||
onConversationActivate: (conversationId: DConversationId) => void,
|
||||
onConversationDelete: (conversationId: DConversationId, bypassConfirmation: boolean) => void,
|
||||
onConversationExportDialog: (conversationId: DConversationId | null) => void,
|
||||
onConversationExportDialog: (conversationId: DConversationId | null, exportAll: boolean) => void,
|
||||
onConversationImportDialog: () => void,
|
||||
onConversationNew: () => void,
|
||||
onConversationNew: (forceNoRecycle: boolean) => void,
|
||||
onConversationsDeleteAll: () => void,
|
||||
setActiveFolderId: (folderId: string | null) => void,
|
||||
}) {
|
||||
@@ -102,20 +119,20 @@ function ChatDrawer(props: {
|
||||
// external state
|
||||
const { closeDrawer, closeDrawerOnMobile } = useOptimaDrawers();
|
||||
const { activeFolder, allFolders, enableFolders, toggleEnableFolders } = useFolders(props.activeFolderId);
|
||||
const chatNavItems = useChatNavigationItemsData(activeFolder, allFolders, props.activeConversationId);
|
||||
const chatNavItems = useChatNavigationItemsData(activeFolder, allFolders, props.activeConversationId, props.chatPanesConversationIds);
|
||||
const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner');
|
||||
|
||||
// derived state
|
||||
const selectConversationsCount = chatNavItems.length;
|
||||
const nonEmptyChats = selectConversationsCount > 1 || (selectConversationsCount === 1 && !chatNavItems[0].isEmpty);
|
||||
const singleChat = selectConversationsCount === 1;
|
||||
const softMaxReached = selectConversationsCount >= 10;
|
||||
const softMaxReached = selectConversationsCount >= 40 && showSymbols;
|
||||
|
||||
|
||||
const isMultiPane = props.chatPanesConversationIds.length >= 2;
|
||||
const handleButtonNew = React.useCallback(() => {
|
||||
onConversationNew();
|
||||
onConversationNew(isMultiPane);
|
||||
closeDrawerOnMobile();
|
||||
}, [closeDrawerOnMobile, onConversationNew]);
|
||||
}, [closeDrawerOnMobile, isMultiPane, onConversationNew]);
|
||||
|
||||
|
||||
const handleConversationActivate = React.useCallback((conversationId: DConversationId, closeMenu: boolean) => {
|
||||
@@ -126,8 +143,8 @@ function ChatDrawer(props: {
|
||||
|
||||
|
||||
const handleConversationDelete = React.useCallback((conversationId: DConversationId) => {
|
||||
!singleChat && conversationId && onConversationDelete(conversationId, true);
|
||||
}, [onConversationDelete, singleChat]);
|
||||
conversationId && onConversationDelete(conversationId, true);
|
||||
}, [onConversationDelete]);
|
||||
|
||||
|
||||
// Folder change request
|
||||
@@ -245,7 +262,7 @@ function ChatDrawer(props: {
|
||||
/>
|
||||
|
||||
<ListItem sx={{ '--ListItem-minHeight': '2.75rem' }}>
|
||||
<ListItemButton disabled={props.disableNewButton} onClick={handleButtonNew} sx={PageDrawerTallItemSx}>
|
||||
<ListItemButton disabled={props.disableNewButton && !isMultiPane} onClick={handleButtonNew} sx={PageDrawerTallItemSx}>
|
||||
<ListItemDecorator><AddIcon /></ListItemDecorator>
|
||||
<Box sx={{
|
||||
// style
|
||||
@@ -284,7 +301,6 @@ function ChatDrawer(props: {
|
||||
<ChatDrawerItemMemo
|
||||
key={'nav-' + item.conversationId}
|
||||
item={item}
|
||||
isLonely={singleChat}
|
||||
showSymbols={showSymbols}
|
||||
bottomBarBasis={(softMaxReached || debouncedSearchQuery) ? bottomBarBasis : 0}
|
||||
onConversationActivate={handleConversationActivate}
|
||||
@@ -299,15 +315,15 @@ function ChatDrawer(props: {
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<ListItemButton onClick={props.onConversationImportDialog} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator>
|
||||
<FileUploadIcon />
|
||||
<FileUploadOutlinedIcon />
|
||||
</ListItemDecorator>
|
||||
Import
|
||||
{/*<OpenAIIcon sx={{ ml: 'auto' }} />*/}
|
||||
</ListItemButton>
|
||||
|
||||
<ListItemButton disabled={!nonEmptyChats} onClick={() => props.onConversationExportDialog(props.activeConversationId)} sx={{ flex: 1 }}>
|
||||
<ListItemButton disabled={!nonEmptyChats} onClick={() => props.onConversationExportDialog(props.activeConversationId, true)} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator>
|
||||
<FileDownloadIcon />
|
||||
<FileDownloadOutlinedIcon />
|
||||
</ListItemDecorator>
|
||||
Export
|
||||
</ListItemButton>
|
||||
@@ -326,9 +342,10 @@ function ChatDrawer(props: {
|
||||
{/* [Menu] Chat Item Folder Change */}
|
||||
{!!folderChangeRequest?.anchorEl && (
|
||||
<CloseableMenu
|
||||
bigIcons
|
||||
open anchorEl={folderChangeRequest.anchorEl} onClose={handleConversationFolderCancel}
|
||||
placement='bottom-start'
|
||||
zIndex={1301 /* need to be on top of the Modal on Mobile */}
|
||||
zIndex={themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
|
||||
@@ -355,6 +372,9 @@ function ChatDrawer(props: {
|
||||
{!!folderChangeRequest.currentFolder && (
|
||||
<ListItem onClick={() => handleConversationFolderSet(folderChangeRequest.conversationId, null)}>
|
||||
<ListItemButton>
|
||||
<ListItemDecorator>
|
||||
<ClearIcon />
|
||||
</ListItemDecorator>
|
||||
{ClearFolderText}
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
+100
-75
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Avatar, Box, Divider, IconButton, ListItem, ListItemButton, ListItemDecorator, Sheet, styled, Tooltip, Typography } from '@mui/joy';
|
||||
import { Avatar, Box, IconButton, ListItem, ListItemButton, ListItemDecorator, Sheet, styled, Tooltip, Typography } from '@mui/joy';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import DeleteForeverIcon from '@mui/icons-material/DeleteForever';
|
||||
@@ -10,13 +10,14 @@ import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import FolderOutlinedIcon from '@mui/icons-material/FolderOutlined';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../data';
|
||||
|
||||
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
|
||||
import type { DFolder } from '~/common/state/store-folders';
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { isDeepEqual } from '~/common/util/jsUtils';
|
||||
|
||||
|
||||
// set to true to display the conversation IDs
|
||||
@@ -24,17 +25,27 @@ import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
|
||||
|
||||
export const FadeInButton = styled(IconButton)({
|
||||
opacity: 0.5,
|
||||
opacity: 0.667,
|
||||
transition: 'opacity 0.2s',
|
||||
'&:hover': { opacity: 1 },
|
||||
});
|
||||
|
||||
|
||||
export const ChatDrawerItemMemo = React.memo(ChatDrawerItem);
|
||||
export const ChatDrawerItemMemo = React.memo(ChatDrawerItem, (prev, next) =>
|
||||
// usign a custom function because `ChatNavigationItemData` is a complex object and memo won't work
|
||||
isDeepEqual(prev.item, next.item) &&
|
||||
prev.showSymbols === next.showSymbols &&
|
||||
prev.bottomBarBasis === next.bottomBarBasis &&
|
||||
prev.onConversationActivate === next.onConversationActivate &&
|
||||
prev.onConversationDelete === next.onConversationDelete &&
|
||||
prev.onConversationExport === next.onConversationExport &&
|
||||
prev.onConversationFolderChange === next.onConversationFolderChange,
|
||||
);
|
||||
|
||||
export interface ChatNavigationItemData {
|
||||
conversationId: DConversationId;
|
||||
isActive: boolean;
|
||||
isAlsoOpen: string | false;
|
||||
isEmpty: boolean;
|
||||
title: string;
|
||||
folder: DFolder | null | undefined; // null: 'All', undefined: do not show folder select
|
||||
@@ -51,13 +62,13 @@ export interface FolderChangeRequest {
|
||||
}
|
||||
|
||||
function ChatDrawerItem(props: {
|
||||
// NOTE: always update the Memo comparison if you add or remove props
|
||||
item: ChatNavigationItemData,
|
||||
isLonely: boolean,
|
||||
showSymbols: boolean,
|
||||
bottomBarBasis: number,
|
||||
onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void,
|
||||
onConversationDelete: (conversationId: DConversationId) => void,
|
||||
onConversationExport: (conversationId: DConversationId) => void,
|
||||
onConversationExport: (conversationId: DConversationId, exportAll: boolean) => void,
|
||||
onConversationFolderChange: (folderChangeRequest: FolderChangeRequest) => void,
|
||||
}) {
|
||||
|
||||
@@ -67,7 +78,7 @@ function ChatDrawerItem(props: {
|
||||
|
||||
// derived state
|
||||
const { onConversationExport, onConversationFolderChange } = props;
|
||||
const { conversationId, isActive, title, folder, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
|
||||
const { conversationId, isActive, isAlsoOpen, title, folder, messageCount, assistantTyping, systemPurposeId, searchFrequency } = props.item;
|
||||
const isNew = messageCount === 0;
|
||||
|
||||
|
||||
@@ -88,7 +99,7 @@ function ChatDrawerItem(props: {
|
||||
|
||||
const handleConversationExport = React.useCallback((event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
conversationId && onConversationExport(conversationId);
|
||||
conversationId && onConversationExport(conversationId, false);
|
||||
}, [conversationId, onConversationExport]);
|
||||
|
||||
|
||||
@@ -158,8 +169,9 @@ function ChatDrawerItem(props: {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography>
|
||||
{isNew ? '' : textSymbol}
|
||||
<Typography sx={isNew ? { opacity: 0.4, filter: 'grayscale(0.75)' } : undefined}>
|
||||
{/*{isNew ? '' : textSymbol}*/}
|
||||
{textSymbol}
|
||||
</Typography>
|
||||
)}
|
||||
</ListItemDecorator>}
|
||||
@@ -209,20 +221,28 @@ function ChatDrawerItem(props: {
|
||||
}} />
|
||||
), [progress]);
|
||||
|
||||
return (isActive || isAlsoOpen) ? (
|
||||
|
||||
return isActive ? (
|
||||
|
||||
// Active Conversation
|
||||
// Active or Also Open
|
||||
<Sheet
|
||||
variant={isActive ? 'solid' : 'plain'}
|
||||
variant={isActive ? 'solid' : 'outlined'}
|
||||
invertedColors={isActive}
|
||||
onClick={!isActive ? handleConversationActivate : undefined}
|
||||
sx={{
|
||||
// common
|
||||
// position: 'relative', // for the progress bar (now disabled)
|
||||
'--ListItem-minHeight': '2.75rem',
|
||||
position: 'relative', // for the progress bar
|
||||
// '--variant-borderWidth': '0.125rem',
|
||||
border: 'none', // there's a default border of 1px and invisible.. hmm
|
||||
|
||||
// differences between primary and secondary variants
|
||||
...(isActive ? {
|
||||
border: 'none', // there's a default border of 1px and invisible.. hmm
|
||||
} : {
|
||||
// '--variant-borderWidth': '0.125rem',
|
||||
cursor: 'pointer',
|
||||
}),
|
||||
|
||||
// style
|
||||
backgroundColor: isActive ? 'neutral.solidActiveBg' : 'neutral.softBg',
|
||||
borderRadius: 'md',
|
||||
mx: '0.25rem',
|
||||
'&:hover > button': {
|
||||
@@ -235,82 +255,87 @@ function ChatDrawerItem(props: {
|
||||
|
||||
{/* Title row */}
|
||||
<Box sx={{ display: 'flex', gap: 'var(--ListItem-gap)', minHeight: '2.25rem', alignItems: 'center' }}>
|
||||
|
||||
{titleRowComponent}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* buttons row */}
|
||||
<Box sx={{ display: 'flex', gap: 1, minHeight: '2.25rem', alignItems: 'center' }}>
|
||||
{isActive && (
|
||||
<Box sx={{ display: 'flex', gap: 1, minHeight: '2.25rem', alignItems: 'center' }}>
|
||||
<ListItemDecorator />
|
||||
|
||||
<ListItemDecorator />
|
||||
{/* Current Folder color, and change initiator */}
|
||||
{(folder !== undefined) && <>
|
||||
<Tooltip disableInteractive title={folder ? `Change Folder (${folder.title})` : 'Add to Folder'}>
|
||||
{folder ? (
|
||||
<IconButton size='sm' onClick={handleFolderChangeBegin}>
|
||||
<FolderIcon style={{ color: folder.color || 'inherit' }} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<FadeInButton size='sm' onClick={handleFolderChangeBegin}>
|
||||
<FolderOutlinedIcon />
|
||||
</FadeInButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
|
||||
{/* Current Folder color, and change initiator */}
|
||||
{(folder !== undefined) && <>
|
||||
<Tooltip disableInteractive title={folder ? `Change Folder (${folder.title})` : 'Add to Folder'}>
|
||||
{folder ? (
|
||||
<IconButton size='sm' onClick={handleFolderChangeBegin}>
|
||||
<FolderIcon style={{ color: folder.color || 'inherit' }} />
|
||||
</IconButton>
|
||||
) : (
|
||||
<FadeInButton size='sm' onClick={handleFolderChangeBegin}>
|
||||
<FolderOutlinedIcon />
|
||||
</FadeInButton>
|
||||
)}
|
||||
</Tooltip>
|
||||
{/*<Divider orientation='vertical' sx={{ my: 1, opacity: 0.5 }} />*/}
|
||||
</>}
|
||||
|
||||
<Divider orientation='vertical' sx={{ my: 1, opacity: 0.5 }} />
|
||||
</>}
|
||||
|
||||
<Tooltip disableInteractive title='Rename'>
|
||||
<FadeInButton size='sm' disabled={isEditingTitle} onClick={handleTitleEditBegin}>
|
||||
<EditIcon />
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
|
||||
{!isNew && <>
|
||||
<Tooltip disableInteractive title='Auto-Title'>
|
||||
<FadeInButton size='sm' disabled={isEditingTitle} onClick={handleTitleEditAuto}>
|
||||
<AutoFixHighIcon />
|
||||
<Tooltip disableInteractive title='Rename'>
|
||||
<FadeInButton size='sm' disabled={isEditingTitle} onClick={handleTitleEditBegin}>
|
||||
<EditIcon />
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
|
||||
<Divider orientation='vertical' sx={{ my: 1, opacity: 0.5 }} />
|
||||
|
||||
<Tooltip disableInteractive title='Export'>
|
||||
<FadeInButton size='sm' onClick={handleConversationExport}>
|
||||
<FileDownloadOutlinedIcon />
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
</>}
|
||||
|
||||
|
||||
{/* --> */}
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
{/* Delete [armed, arming] buttons */}
|
||||
{!props.isLonely && !searchFrequency && <>
|
||||
{deleteArmed && (
|
||||
<Tooltip disableInteractive title='Confirm Deletion'>
|
||||
<FadeInButton key='btn-del' variant='solid' color='success' size='sm' onClick={handleConversationDelete} sx={{ opacity: 1 }}>
|
||||
<DeleteForeverIcon sx={{ color: 'danger.solidBg' }} />
|
||||
{!isNew && <>
|
||||
<Tooltip disableInteractive title='Auto-Title'>
|
||||
<FadeInButton size='sm' disabled={isEditingTitle} onClick={handleTitleEditAuto}>
|
||||
<AutoFixHighIcon />
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip disableInteractive title={deleteArmed ? 'Cancel Delete' : 'Delete'}>
|
||||
<FadeInButton key='btn-arm' size='sm' onClick={deleteArmed ? handleDeleteButtonHide : handleDeleteButtonShow} sx={deleteArmed ? { opacity: 1 } : {}}>
|
||||
{deleteArmed ? <CloseIcon /> : <DeleteOutlineIcon />}
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
</>}
|
||||
{/*<Divider orientation='vertical' sx={{ my: 1, opacity: 0.5 }} />*/}
|
||||
|
||||
</Box>
|
||||
<Tooltip disableInteractive title='Export Chat'>
|
||||
<FadeInButton size='sm' onClick={handleConversationExport}>
|
||||
<FileDownloadOutlinedIcon />
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
</>}
|
||||
|
||||
{/* --> */}
|
||||
<Box sx={{ flex: 1 }} />
|
||||
|
||||
{/* Delete [armed, arming] buttons */}
|
||||
{!searchFrequency && <>
|
||||
{deleteArmed && (
|
||||
<Tooltip disableInteractive title='Confirm Deletion'>
|
||||
<FadeInButton key='btn-del' variant='solid' color='success' size='sm' onClick={handleConversationDelete} sx={{ opacity: 1 }}>
|
||||
<DeleteForeverIcon sx={{ color: 'danger.solidBg' }} />
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
<Tooltip disableInteractive title={deleteArmed ? 'Cancel Delete' : 'Delete'}>
|
||||
<FadeInButton key='btn-arm' size='sm' onClick={deleteArmed ? handleDeleteButtonHide : handleDeleteButtonShow} sx={deleteArmed ? { opacity: 1 } : {}}>
|
||||
{deleteArmed ? <CloseIcon /> : <DeleteOutlineIcon />}
|
||||
</FadeInButton>
|
||||
</Tooltip>
|
||||
</>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* View places row */}
|
||||
{isAlsoOpen && (
|
||||
<Typography level='body-xs' sx={{ mx: 'auto' }}>
|
||||
<em>In view {isAlsoOpen}</em>
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
</ListItem>
|
||||
|
||||
{/* Optional progress bar, underlay */}
|
||||
{progressBarFixedComponent}
|
||||
{/* NOTE: disabled on 20240204: quite distracting on the active chat sheet */}
|
||||
{/*{progressBarFixedComponent}*/}
|
||||
|
||||
</Sheet>
|
||||
|
||||
+1
-21
@@ -1,20 +1,14 @@
|
||||
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';
|
||||
import { useFolderDropdown } from './folders/useFolderDropdown';
|
||||
|
||||
|
||||
export function ChatDropdowns(props: {
|
||||
conversationId: DConversationId | null
|
||||
isSplitPanes: boolean
|
||||
onToggleSplitPanes: () => void
|
||||
}) {
|
||||
|
||||
// state
|
||||
@@ -22,9 +16,6 @@ export function ChatDropdowns(props: {
|
||||
const { personaDropdown } = usePersonaIdDropdown(props.conversationId);
|
||||
const { folderDropdown } = useFolderDropdown(props.conversationId);
|
||||
|
||||
// external state
|
||||
const labsSplitBranching = useUXLabsStore(state => state.labsSplitBranching);
|
||||
|
||||
return <>
|
||||
|
||||
{/* Persona selector */}
|
||||
@@ -36,16 +27,5 @@ export function ChatDropdowns(props: {
|
||||
{/* Folder selector */}
|
||||
{folderDropdown}
|
||||
|
||||
{/* Split Panes button */}
|
||||
{labsSplitBranching && <IconButton
|
||||
variant={props.isSplitPanes ? 'solid' : undefined}
|
||||
onClick={props.onToggleSplitPanes}
|
||||
// sx={{
|
||||
// ml: 'auto',
|
||||
// }}
|
||||
>
|
||||
<VerticalSplitIcon />
|
||||
</IconButton>}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -26,12 +26,14 @@ export function ChatMessageList(props: {
|
||||
conversationId: DConversationId | null,
|
||||
capabilityHasT2I: boolean,
|
||||
chatLLMContextTokens: number | null,
|
||||
isMessageSelectionMode: boolean, setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
|
||||
isMessageSelectionMode: boolean,
|
||||
isMobile: boolean,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
|
||||
onConversationExecuteHistory: (conversationId: DConversationId, history: DMessage[]) => Promise<void>,
|
||||
onTextDiagram: (diagramConfig: DiagramConfig | null) => void,
|
||||
onTextImagine: (conversationId: DConversationId, selectedText: string) => Promise<void>,
|
||||
onTextSpeak: (selectedText: string) => Promise<void>,
|
||||
setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
|
||||
sx?: SxProps,
|
||||
}) {
|
||||
|
||||
@@ -148,17 +150,17 @@ export function ChatMessageList(props: {
|
||||
});
|
||||
|
||||
|
||||
// text-diff functionality, find the messages to diff with
|
||||
// text-diff functionality: only diff the last message and when it's complete (not typing), and they're similar in size
|
||||
|
||||
const { diffMessage, diffText } = React.useMemo(() => {
|
||||
const { diffTargetMessage, diffPrevText } = React.useMemo(() => {
|
||||
const [msgB, msgA] = conversationMessages.filter(m => m.role === 'assistant').reverse();
|
||||
if (msgB?.text && msgA?.text && !msgB?.typing) {
|
||||
const textA = msgA.text, textB = msgB.text;
|
||||
const lenA = textA.length, lenB = textB.length;
|
||||
if (lenA > 80 && lenB > 80 && lenA > lenB / 3 && lenB > lenA / 3)
|
||||
return { diffMessage: msgB, diffText: textA };
|
||||
return { diffTargetMessage: msgB, diffPrevText: textA };
|
||||
}
|
||||
return { diffMessage: undefined, diffText: undefined };
|
||||
return { diffTargetMessage: undefined, diffPrevText: undefined };
|
||||
}, [conversationMessages]);
|
||||
|
||||
|
||||
@@ -219,9 +221,11 @@ export function ChatMessageList(props: {
|
||||
<ChatMessageMemo
|
||||
key={'msg-' + message.id}
|
||||
message={message}
|
||||
diffPreviousText={message === diffMessage ? diffText : undefined}
|
||||
diffPreviousText={message === diffTargetMessage ? diffPrevText : undefined}
|
||||
isBottom={idx === count - 1}
|
||||
isImagining={isImagining} isSpeaking={isSpeaking}
|
||||
isImagining={isImagining}
|
||||
isMobile={props.isMobile}
|
||||
isSpeaking={isSpeaking}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationRestartFrom={handleConversationRestartFrom}
|
||||
onConversationTruncate={handleConversationTruncate}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, IconButton, ListDivider, ListItemDecorator, MenuItem, Switch, Tooltip } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import CheckBoxOutlineBlankOutlinedIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined';
|
||||
import CheckBoxOutlinedIcon from '@mui/icons-material/CheckBoxOutlined';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import CompressIcon from '@mui/icons-material/Compress';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import HorizontalSplitIcon from '@mui/icons-material/HorizontalSplit';
|
||||
import HorizontalSplitOutlinedIcon from '@mui/icons-material/HorizontalSplitOutlined';
|
||||
import SettingsSuggestOutlinedIcon from '@mui/icons-material/SettingsSuggestOutlined';
|
||||
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
||||
import VerticalSplitOutlinedIcon from '@mui/icons-material/VerticalSplitOutlined';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
|
||||
import { useChatShowSystemMessages } from '../store-app-chat';
|
||||
import { usePaneDuplicateOrClose } from './panes/usePanesManager';
|
||||
|
||||
|
||||
export function ChatPageMenuItems(props: {
|
||||
isMobile: boolean,
|
||||
conversationId: DConversationId | null,
|
||||
disableItems: boolean,
|
||||
hasConversations: boolean,
|
||||
isMessageSelectionMode: boolean,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string | null) => void,
|
||||
onConversationClear: (conversationId: DConversationId) => void,
|
||||
onConversationFlatten: (conversationId: DConversationId) => void,
|
||||
// onConversationNew: (forceNoRecycle: boolean) => void,
|
||||
setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { closePageMenu } = useOptimaDrawers();
|
||||
const { canAddPane, isMultiPane, duplicateFocusedPane, removeOtherPanes } = usePaneDuplicateOrClose();
|
||||
const [showSystemMessages, setShowSystemMessages] = useChatShowSystemMessages();
|
||||
|
||||
|
||||
const handleIncreaseMultiPane = React.useCallback((event?: React.MouseEvent) => {
|
||||
event?.stopPropagation();
|
||||
|
||||
// create a new pane with the current conversation
|
||||
duplicateFocusedPane();
|
||||
|
||||
// load a brand new conversation inside
|
||||
// FIXME: still testing this
|
||||
// props.onConversationNew(true);
|
||||
}, [duplicateFocusedPane]);
|
||||
|
||||
const handleToggleMultiPane = React.useCallback((_event: React.MouseEvent) => {
|
||||
if (isMultiPane)
|
||||
removeOtherPanes();
|
||||
else
|
||||
handleIncreaseMultiPane(undefined);
|
||||
}, [handleIncreaseMultiPane, isMultiPane, removeOtherPanes]);
|
||||
|
||||
|
||||
const closeMenu = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
closePageMenu();
|
||||
};
|
||||
|
||||
const handleConversationClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
closeMenu(event);
|
||||
props.conversationId && props.onConversationClear(props.conversationId);
|
||||
};
|
||||
|
||||
const handleConversationBranch = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
closeMenu(event);
|
||||
props.conversationId && props.onConversationBranch(props.conversationId, null);
|
||||
};
|
||||
|
||||
const handleConversationFlatten = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
closeMenu(event);
|
||||
props.conversationId && props.onConversationFlatten(props.conversationId);
|
||||
};
|
||||
|
||||
const handleToggleMessageSelectionMode = (event: React.MouseEvent) => {
|
||||
closeMenu(event);
|
||||
props.setIsMessageSelectionMode(!props.isMessageSelectionMode);
|
||||
};
|
||||
|
||||
const handleToggleSystemMessages = () => setShowSystemMessages(!showSystemMessages);
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
{/* System Message(s) */}
|
||||
<MenuItem onClick={handleToggleSystemMessages}>
|
||||
<ListItemDecorator><SettingsSuggestOutlinedIcon /></ListItemDecorator>
|
||||
System messages
|
||||
<Switch checked={showSystemMessages} onChange={handleToggleSystemMessages} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
|
||||
{/* Un /Split */}
|
||||
<MenuItem onClick={handleToggleMultiPane}>
|
||||
<ListItemDecorator>{props.isMobile
|
||||
? (isMultiPane ? <HorizontalSplitIcon /> : <HorizontalSplitOutlinedIcon />)
|
||||
: (isMultiPane ? <VerticalSplitIcon /> : <VerticalSplitOutlinedIcon />)
|
||||
}</ListItemDecorator>
|
||||
{/* Unsplit / Split text*/}
|
||||
{isMultiPane ? 'Unsplit' : props.isMobile ? 'Split Down' : 'Split Right'}
|
||||
{/* '+' */}
|
||||
{isMultiPane && (
|
||||
<Tooltip title='Add Another Split'>
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
disabled={!canAddPane}
|
||||
onClick={handleIncreaseMultiPane}
|
||||
sx={{ ml: 'auto', /*mr: '2px',*/ my: '-0.25rem' /* absorb the menuItem padding */ }}
|
||||
>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem disabled={props.disableItems} onClick={handleConversationBranch}>
|
||||
<ListItemDecorator><ForkRightIcon /></ListItemDecorator>
|
||||
Branch
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider />
|
||||
|
||||
<MenuItem disabled={props.disableItems} onClick={handleToggleMessageSelectionMode}>
|
||||
<ListItemDecorator>{props.isMessageSelectionMode ? <CheckBoxOutlinedIcon /> : <CheckBoxOutlineBlankOutlinedIcon />}</ListItemDecorator>
|
||||
<span style={props.isMessageSelectionMode ? { fontWeight: 800 } : {}}>
|
||||
Cleanup ...
|
||||
</span>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem disabled={props.disableItems} onClick={handleConversationFlatten}>
|
||||
<ListItemDecorator><CompressIcon color='success' /></ListItemDecorator>
|
||||
Compress ...
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider />
|
||||
|
||||
<MenuItem disabled={props.disableItems} onClick={handleConversationClear}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Reset Chat
|
||||
{!props.disableItems && <KeyStroke combo='Ctrl + Alt + X' />}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, ListDivider, ListItemDecorator, MenuItem, Switch } from '@mui/joy';
|
||||
import CheckBoxOutlineBlankOutlinedIcon from '@mui/icons-material/CheckBoxOutlineBlankOutlined';
|
||||
import CheckBoxOutlinedIcon from '@mui/icons-material/CheckBoxOutlined';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import CompressIcon from '@mui/icons-material/Compress';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { useOptimaDrawers } from '~/common/layout/optima/useOptimaDrawers';
|
||||
|
||||
import { useChatShowSystemMessages } from '../../store-app-chat';
|
||||
|
||||
|
||||
export function ChatMenuItems(props: {
|
||||
conversationId: DConversationId | null,
|
||||
hasConversations: boolean,
|
||||
isConversationEmpty: boolean,
|
||||
isMessageSelectionMode: boolean,
|
||||
setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string | null) => void,
|
||||
onConversationClear: (conversationId: DConversationId) => void,
|
||||
onConversationFlatten: (conversationId: DConversationId) => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { closePageMenu } = useOptimaDrawers();
|
||||
const [showSystemMessages, setShowSystemMessages] = useChatShowSystemMessages();
|
||||
|
||||
// derived state
|
||||
const disabled = !props.conversationId || props.isConversationEmpty;
|
||||
|
||||
|
||||
const closeMenu = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
closePageMenu();
|
||||
};
|
||||
|
||||
const handleConversationClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
closeMenu(event);
|
||||
props.conversationId && props.onConversationClear(props.conversationId);
|
||||
};
|
||||
|
||||
const handleConversationBranch = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
closeMenu(event);
|
||||
props.conversationId && props.onConversationBranch(props.conversationId, null);
|
||||
};
|
||||
|
||||
const handleConversationFlatten = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
closeMenu(event);
|
||||
props.conversationId && props.onConversationFlatten(props.conversationId);
|
||||
};
|
||||
|
||||
const handleToggleMessageSelectionMode = (event: React.MouseEvent) => {
|
||||
closeMenu(event);
|
||||
props.setIsMessageSelectionMode(!props.isMessageSelectionMode);
|
||||
};
|
||||
|
||||
const handleToggleSystemMessages = () => setShowSystemMessages(!showSystemMessages);
|
||||
|
||||
|
||||
return <>
|
||||
|
||||
{/*<ListItem>*/}
|
||||
{/* <Typography level='body-sm'>*/}
|
||||
{/* Conversation*/}
|
||||
{/* </Typography>*/}
|
||||
{/*</ListItem>*/}
|
||||
|
||||
<MenuItem onClick={handleToggleSystemMessages}>
|
||||
<ListItemDecorator><SettingsSuggestIcon /></ListItemDecorator>
|
||||
System message
|
||||
<Switch checked={showSystemMessages} onChange={handleToggleSystemMessages} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider inset='startContent' />
|
||||
|
||||
<MenuItem disabled={disabled} onClick={handleConversationBranch}>
|
||||
<ListItemDecorator><ForkRightIcon /></ListItemDecorator>
|
||||
Branch
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem disabled={disabled} onClick={handleConversationFlatten}>
|
||||
<ListItemDecorator><CompressIcon color='success' /></ListItemDecorator>
|
||||
Flatten
|
||||
</MenuItem>
|
||||
|
||||
<ListDivider inset='startContent' />
|
||||
|
||||
<MenuItem disabled={disabled} onClick={handleToggleMessageSelectionMode}>
|
||||
<ListItemDecorator>{props.isMessageSelectionMode ? <CheckBoxOutlinedIcon /> : <CheckBoxOutlineBlankOutlinedIcon />}</ListItemDecorator>
|
||||
<span style={props.isMessageSelectionMode ? { fontWeight: 800 } : {}}>
|
||||
Cleanup ...
|
||||
</span>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem disabled={disabled} onClick={handleConversationClear}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Reset Chat
|
||||
{!disabled && <KeyStroke combo='Ctrl + Alt + X' />}
|
||||
</Box>
|
||||
</MenuItem>
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, ListItemButton, ListItemDecorator } from '@mui/joy';
|
||||
import BuildCircleIcon from '@mui/icons-material/BuildCircle';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { PageBarDropdown, DropdownItems } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
|
||||
function AppBarLLMDropdown(props: {
|
||||
llms: DLLM[],
|
||||
chatLlmId: DLLMId | null,
|
||||
setChatLlmId: (llmId: DLLMId | null) => void,
|
||||
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;
|
||||
for (const llm of props.llms) {
|
||||
|
||||
// filter-out hidden models
|
||||
if (!(!llm.hidden || llm.id === props.chatLlmId))
|
||||
continue;
|
||||
|
||||
// add separators when changing sources
|
||||
if (!prevSourceId || llm.sId !== prevSourceId) {
|
||||
if (prevSourceId)
|
||||
llmItems[`sep-${llm.id}`] = {
|
||||
type: 'separator',
|
||||
title: llm.sId,
|
||||
};
|
||||
prevSourceId = llm.sId;
|
||||
}
|
||||
|
||||
// add the model item
|
||||
llmItems[llm.id] = {
|
||||
title: llm.label,
|
||||
// icon: llm.id.startsWith('some vendor') ? <VendorIcon /> : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
const handleChatLLMChange = (_event: any, value: DLLMId | null) => value && props.setChatLlmId(value);
|
||||
|
||||
const handleOpenLLMOptions = () => props.chatLlmId && openLlmOptions(props.chatLlmId);
|
||||
|
||||
|
||||
return (
|
||||
<PageBarDropdown
|
||||
items={llmItems}
|
||||
value={props.chatLlmId} onChange={handleChatLLMChange}
|
||||
placeholder={props.placeholder || 'Models …'}
|
||||
appendOption={<>
|
||||
|
||||
{props.chatLlmId && (
|
||||
<ListItemButton key='menu-opt' onClick={handleOpenLLMOptions}>
|
||||
<ListItemDecorator><SettingsIcon color='success' /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Options
|
||||
<KeyStroke combo='Ctrl + Shift + O' />
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
)}
|
||||
|
||||
<ListItemButton key='menu-llms' onClick={openModelsSetup}>
|
||||
<ListItemDecorator><BuildCircleIcon color='success' /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Models
|
||||
<KeyStroke combo='Ctrl + Shift + M' />
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
|
||||
</>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useChatLLMDropdown() {
|
||||
// external state
|
||||
const { llms, chatLLMId, setChatLLMId } = useModelsStore(state => ({
|
||||
llms: state.llms,
|
||||
chatLLMId: state.chatLLMId,
|
||||
setChatLLMId: state.setChatLLMId,
|
||||
}), shallow);
|
||||
|
||||
const chatLLMDropdown = React.useMemo(
|
||||
() => <AppBarLLMDropdown llms={llms} chatLlmId={chatLLMId} setChatLlmId={setChatLLMId} />,
|
||||
[llms, chatLLMId, setChatLLMId],
|
||||
);
|
||||
|
||||
return { chatLLMId, chatLLMDropdown };
|
||||
}
|
||||
|
||||
/*export function useTempLLMDropdown(props: { initialLlmId: DLLMId | null }) {
|
||||
// local state
|
||||
const [llmId, setLlmId] = React.useState<DLLMId | null>(props.initialLlmId);
|
||||
|
||||
// external state
|
||||
const llms = useModelsStore(state => state.llms, shallow);
|
||||
|
||||
const chatLLMDropdown = React.useMemo(
|
||||
() => <AppBarLLMDropdown llms={llms} llmId={llmId} setLlmId={setLlmId} />,
|
||||
[llms, llmId, setLlmId],
|
||||
);
|
||||
|
||||
return { llmId, chatLLMDropdown };
|
||||
}*/
|
||||
@@ -1,60 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { PageBarDropdown } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
function AppBarPersonaDropdown(props: {
|
||||
systemPurposeId: SystemPurposeId | null,
|
||||
setSystemPurposeId: (systemPurposeId: SystemPurposeId | null) => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { zenMode } = useUIPreferencesStore(state => ({
|
||||
zenMode: state.zenMode,
|
||||
}), shallow);
|
||||
|
||||
const handleSystemPurposeChange = (_event: any, value: SystemPurposeId | null) => props.setSystemPurposeId(value);
|
||||
|
||||
|
||||
// options
|
||||
|
||||
// let appendOption: React.JSX.Element | undefined = undefined;
|
||||
|
||||
return (
|
||||
<PageBarDropdown
|
||||
items={SystemPurposes} showSymbols={zenMode !== 'cleaner'}
|
||||
value={props.systemPurposeId} onChange={handleSystemPurposeChange}
|
||||
// appendOption={appendOption}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export function usePersonaIdDropdown(conversationId: DConversationId | null) {
|
||||
|
||||
// external state
|
||||
const { systemPurposeId } = useChatStore(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === conversationId);
|
||||
return {
|
||||
systemPurposeId: conversation?.systemPurposeId ?? null,
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
const personaDropdown = React.useMemo(() => systemPurposeId
|
||||
? <AppBarPersonaDropdown
|
||||
systemPurposeId={systemPurposeId}
|
||||
setSystemPurposeId={(systemPurposeId) => {
|
||||
if (conversationId && systemPurposeId)
|
||||
useChatStore.getState().setSystemPurposeId(conversationId, systemPurposeId);
|
||||
}}
|
||||
/> : null,
|
||||
[conversationId, systemPurposeId],
|
||||
);
|
||||
|
||||
return { personaDropdown };
|
||||
}
|
||||
@@ -7,61 +7,21 @@ import InfoIcon from '@mui/icons-material/Info';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { downloadVideoFrameAsPNG, renderVideoFrameAsPNGFile } from '~/common/util/videoUtils';
|
||||
import { useCameraCapture } from '~/common/components/useCameraCapture';
|
||||
|
||||
|
||||
function prettyFileName(renderedFrame: HTMLCanvasElement) {
|
||||
const prettyDate = new Date().toISOString().replace(/[:-]/g, '').replace('T', '-').replace('Z', '');
|
||||
const prettyResolution = `${renderedFrame.width}x${renderedFrame.height}`;
|
||||
return `camera-${prettyDate}-${prettyResolution}.png`;
|
||||
}
|
||||
|
||||
function renderVideoFrameToCanvas(videoElement: HTMLVideoElement): HTMLCanvasElement {
|
||||
// paint the video on a canvas, to save it
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = videoElement.videoWidth || 640;
|
||||
canvas.height = videoElement.videoHeight || 480;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx?.drawImage(videoElement, 0, 0);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
function renderVideoFrameToFile(videoElement: HTMLVideoElement, callback: (file: File) => void) {
|
||||
// video to canvas
|
||||
const renderedFrame = renderVideoFrameToCanvas(videoElement);
|
||||
|
||||
// canvas to blob to file to callback
|
||||
renderedFrame.toBlob((blob) => {
|
||||
if (blob) {
|
||||
const file = new File([blob], prettyFileName(renderedFrame), { type: blob.type });
|
||||
callback(file);
|
||||
}
|
||||
}, 'image/png');
|
||||
}
|
||||
|
||||
function downloadVideoFrameAsPNG(videoElement: HTMLVideoElement) {
|
||||
// video to canvas to png
|
||||
const renderedFrame = renderVideoFrameToCanvas(videoElement);
|
||||
const imageDataURL = renderedFrame.toDataURL('image/png');
|
||||
|
||||
// auto-download
|
||||
const link = document.createElement('a');
|
||||
link.download = prettyFileName(renderedFrame);
|
||||
link.href = imageDataURL;
|
||||
link.click();
|
||||
}
|
||||
|
||||
|
||||
export function CameraCaptureModal(props: {
|
||||
onCloseModal: () => void,
|
||||
onAttachImage: (file: File) => void
|
||||
// onOCR: (ocrText: string) => void }
|
||||
}) {
|
||||
// state
|
||||
// const [ocrProgress/*, setOCRProgress*/] = React.useState<number | null>(null);
|
||||
const [showInfo, setShowInfo] = React.useState(false);
|
||||
|
||||
// camera operations
|
||||
// state
|
||||
const [showInfo, setShowInfo] = React.useState(false);
|
||||
// const [ocrProgress/*, setOCRProgress*/] = React.useState<number | null>(null);
|
||||
|
||||
// external state
|
||||
const {
|
||||
videoRef,
|
||||
cameras, cameraIdx, setCameraIdx,
|
||||
@@ -70,10 +30,14 @@ export function CameraCaptureModal(props: {
|
||||
} = useCameraCapture();
|
||||
|
||||
|
||||
const stopAndClose = () => {
|
||||
// derived state
|
||||
const { onCloseModal, onAttachImage } = props;
|
||||
|
||||
|
||||
const stopAndClose = React.useCallback(() => {
|
||||
resetVideo();
|
||||
props.onCloseModal();
|
||||
};
|
||||
onCloseModal();
|
||||
}, [onCloseModal, resetVideo]);
|
||||
|
||||
/*const handleVideoOCRClicked = async () => {
|
||||
if (!videoRef.current) return;
|
||||
@@ -94,18 +58,21 @@ export function CameraCaptureModal(props: {
|
||||
props.onOCR(result.data.text);
|
||||
};*/
|
||||
|
||||
const handleVideoSnapClicked = () => {
|
||||
const handleVideoSnapClicked = React.useCallback(async () => {
|
||||
if (!videoRef.current) return;
|
||||
renderVideoFrameToFile(videoRef.current, (file) => {
|
||||
props.onAttachImage(file);
|
||||
try {
|
||||
const file = await renderVideoFrameAsPNGFile(videoRef.current, 'camera');
|
||||
onAttachImage(file);
|
||||
stopAndClose();
|
||||
});
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error capturing video frame:', error);
|
||||
}
|
||||
}, [onAttachImage, stopAndClose, videoRef]);
|
||||
|
||||
const handleVideoDownloadClicked = () => {
|
||||
const handleVideoDownloadClicked = React.useCallback(() => {
|
||||
if (!videoRef.current) return;
|
||||
downloadVideoFrameAsPNG(videoRef.current);
|
||||
};
|
||||
downloadVideoFrameAsPNG(videoRef.current, 'camera');
|
||||
}, [videoRef]);
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -53,32 +53,35 @@ export function ChatModeMenu(props: {
|
||||
// external state
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
|
||||
return <CloseableMenu
|
||||
placement='top-end' sx={{ minWidth: 320 }}
|
||||
open anchorEl={props.anchorEl} onClose={props.onClose}
|
||||
>
|
||||
return (
|
||||
<CloseableMenu
|
||||
placement='top-end'
|
||||
open anchorEl={props.anchorEl} onClose={props.onClose}
|
||||
sx={{ minWidth: 320 }}
|
||||
>
|
||||
|
||||
{/*<MenuItem color='neutral' selected>*/}
|
||||
{/* Conversation Mode*/}
|
||||
{/*</MenuItem>*/}
|
||||
{/**/}
|
||||
{/*<ListDivider />*/}
|
||||
{/*<MenuItem color='neutral' selected>*/}
|
||||
{/* Conversation Mode*/}
|
||||
{/*</MenuItem>*/}
|
||||
{/**/}
|
||||
{/*<ListDivider />*/}
|
||||
|
||||
{/* ChatMode items */}
|
||||
{Object.entries(ChatModeItems)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Radio checked={key === props.chatModeId} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography>{data.label}</Typography>
|
||||
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
|
||||
{/* ChatMode items */}
|
||||
{Object.entries(ChatModeItems)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Radio checked={key === props.chatModeId} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography>{data.label}</Typography>
|
||||
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
|
||||
</Box>
|
||||
{(key === props.chatModeId || !!data.shortcut) && (
|
||||
<KeyStroke combo={fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline)} />
|
||||
)}
|
||||
</Box>
|
||||
{(key === props.chatModeId || !!data.shortcut) && (
|
||||
<KeyStroke combo={fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline)} />
|
||||
)}
|
||||
</Box>
|
||||
</MenuItem>)}
|
||||
</MenuItem>)}
|
||||
|
||||
</CloseableMenu>;
|
||||
</CloseableMenu>
|
||||
);
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { shallow } from 'zustand/shallow';
|
||||
import { fileOpen, FileWithHandle } from 'browser-fs-access';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Stack, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
|
||||
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
@@ -31,10 +31,10 @@ import { launchAppCall } from '~/common/app.routes';
|
||||
import { lineHeightTextarea } from '~/common/app.theme';
|
||||
import { playSoundUrl } from '~/common/util/audioUtils';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
import { useDebouncer } from '~/common/components/useDebouncer';
|
||||
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ActileItem, ActileProvider } from './actile/ActileProvider';
|
||||
@@ -50,9 +50,11 @@ import type { ComposerOutputMultiPart } from './composer.types';
|
||||
import { ButtonAttachCameraMemo, useCameraCaptureModal } from './buttons/ButtonAttachCamera';
|
||||
import { ButtonAttachClipboardMemo } from './buttons/ButtonAttachClipboard';
|
||||
import { ButtonAttachFileMemo } from './buttons/ButtonAttachFile';
|
||||
import { ButtonAttachScreenCaptureMemo } from './buttons/ButtonAttachScreenCapture';
|
||||
import { ButtonCall } from './buttons/ButtonCall';
|
||||
import { ButtonMicContinuationMemo } from './buttons/ButtonMicContinuation';
|
||||
import { ButtonMicMemo } from './buttons/ButtonMic';
|
||||
import { ButtonMultiChat } from './buttons/ButtonMultiChat';
|
||||
import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
|
||||
import { ChatModeMenu } from './ChatModeMenu';
|
||||
import { TokenBadgeMemo } from './TokenBadge';
|
||||
@@ -76,13 +78,16 @@ export const animationStopEnter = keyframes`
|
||||
* A React component for composing messages, with attachments and different modes.
|
||||
*/
|
||||
export function Composer(props: {
|
||||
isMobile?: boolean;
|
||||
chatLLM: DLLM | null;
|
||||
composerTextAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
conversationId: DConversationId | null;
|
||||
capabilityHasT2I: boolean;
|
||||
isMulticast: boolean | null;
|
||||
isDeveloperMode: boolean;
|
||||
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
|
||||
onTextImagine: (conversationId: DConversationId, text: string) => void;
|
||||
setIsMulticast: (on: boolean) => void;
|
||||
sx?: SxProps;
|
||||
}) {
|
||||
|
||||
@@ -94,11 +99,12 @@ export function Composer(props: {
|
||||
const [chatModeMenuAnchor, setChatModeMenuAnchor] = React.useState<HTMLAnchorElement | null>(null);
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const { openPreferencesTab /*, setIsFocusedMode*/ } = useOptimaLayout();
|
||||
const { labsCameraDesktop } = useUXLabsStore(state => ({
|
||||
const { labsAttachScreenCapture, labsCameraDesktop } = useUXLabsStore(state => ({
|
||||
labsAttachScreenCapture: state.labsAttachScreenCapture,
|
||||
labsCameraDesktop: state.labsCameraDesktop,
|
||||
}), shallow);
|
||||
const { novel: explainShiftEnter, touch: touchShiftEnter } = useUICounter('composer-shift-enter');
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
|
||||
const [startupText, setStartupText] = useComposerStartupText();
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
@@ -116,9 +122,11 @@ export function Composer(props: {
|
||||
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
|
||||
useAttachments(browsingInComposer && !composeText.startsWith('/'));
|
||||
|
||||
|
||||
// derived state
|
||||
|
||||
const isDesktop = !isMobile;
|
||||
const isMobile = !!props.isMobile;
|
||||
const isDesktop = !props.isMobile;
|
||||
const chatLLMId = props.chatLLM?.id || null;
|
||||
|
||||
// attachments derived state
|
||||
@@ -256,6 +264,8 @@ export function Composer(props: {
|
||||
}
|
||||
|
||||
// Shift: toggles the 'enter is newline'
|
||||
if (e.shiftKey)
|
||||
touchShiftEnter();
|
||||
if (enterIsNewline ? e.shiftKey : !e.shiftKey) {
|
||||
if (!assistantAbortible)
|
||||
handleSendAction(chatModeId, composeText);
|
||||
@@ -263,7 +273,7 @@ export function Composer(props: {
|
||||
}
|
||||
}
|
||||
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction]);
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchShiftEnter]);
|
||||
|
||||
|
||||
// Focus mode
|
||||
@@ -341,6 +351,10 @@ export function Composer(props: {
|
||||
void attachAppendFile('camera', file);
|
||||
}, [attachAppendFile]);
|
||||
|
||||
const handleAttachScreenCapture = React.useCallback((file: File) => {
|
||||
void attachAppendFile('screencapture', file);
|
||||
}, [attachAppendFile]);
|
||||
|
||||
const { openCamera, cameraCaptureComponent } = useCameraCaptureModal(handleAttachCameraImage);
|
||||
|
||||
const handleAttachFilePicker = React.useCallback(async () => {
|
||||
@@ -429,32 +443,36 @@ export function Composer(props: {
|
||||
? 'warning'
|
||||
: isReAct ? 'success' : isDraw ? 'warning' : 'primary';
|
||||
|
||||
const textPlaceholder: string =
|
||||
let textPlaceholder: string =
|
||||
isDraw
|
||||
? 'Describe an idea or a drawing...'
|
||||
: isReAct
|
||||
? 'Multi-step reasoning question...'
|
||||
: props.isDeveloperMode
|
||||
? 'Chat with me · drop source files · attach code...'
|
||||
? 'Chat with me' + (isDesktop ? ' · drop source' : '') + ' · attach code...'
|
||||
: props.capabilityHasT2I
|
||||
? 'Chat · /react · /draw · drop files...'
|
||||
: 'Chat · /react · drop files...';
|
||||
|
||||
if (isDesktop && explainShiftEnter)
|
||||
textPlaceholder += !enterIsNewline ? '\nShift+Enter to add a new line' : '\nShift+Enter to send';
|
||||
|
||||
return (
|
||||
<Box aria-label='User Message' component='section' sx={props.sx}>
|
||||
<Grid container spacing={{ xs: 1, md: 2 }}>
|
||||
|
||||
{/* Button column and composer Text (mobile: top, desktop: left and center) */}
|
||||
<Grid xs={12} md={9}><Stack direction='row' spacing={{ xs: 1, md: 2 }}>
|
||||
<Grid xs={12} md={9}><Box sx={{ display: 'flex', gap: { xs: 1, md: 2 }, alignItems: 'flex-start' }}>
|
||||
|
||||
{/* Vertical (insert) buttons */}
|
||||
{isMobile ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{/* Start buttons column */}
|
||||
<Box sx={{
|
||||
flexGrow: 0,
|
||||
display: 'grid', gap: 1,
|
||||
}}>
|
||||
{isMobile ? <>
|
||||
|
||||
{/* [mobile] Mic button */}
|
||||
{isSpeechEnabled && <ButtonMicMemo variant={micVariant} color={micColor} onClick={handleToggleMic} />}
|
||||
|
||||
{/* [mobile] [+] button */}
|
||||
<Dropdown>
|
||||
<MenuButton slots={{ root: IconButton }}>
|
||||
<AddCircleOutlineIcon />
|
||||
@@ -477,9 +495,10 @@ export function Composer(props: {
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{/* [Mobile] MultiChat button */}
|
||||
{props.isMulticast !== null && <ButtonMultiChat isMobile multiChat={props.isMulticast} onSetMultiChat={props.setIsMulticast} />}
|
||||
|
||||
</> : <>
|
||||
|
||||
{/*<FormHelperText sx={{ mx: 'auto' }}>*/}
|
||||
{/* Attach*/}
|
||||
@@ -491,20 +510,23 @@ export function Composer(props: {
|
||||
{/* Responsive Paste button */}
|
||||
{supportsClipboardRead && <ButtonAttachClipboardMemo onClick={attachAppendClipboardItems} />}
|
||||
|
||||
{/* Responsive Screen Capture button */}
|
||||
{labsAttachScreenCapture && supportsScreenCapture && <ButtonAttachScreenCaptureMemo onAttachScreenCapture={handleAttachScreenCapture} />}
|
||||
|
||||
{/* Responsive Camera OCR button */}
|
||||
{labsCameraDesktop && <ButtonAttachCameraMemo onOpenCamera={openCamera} />}
|
||||
|
||||
</Box>
|
||||
)}
|
||||
</>}
|
||||
</Box>
|
||||
|
||||
{/* Vertically stacked [ Edit box + Overlays + Mic | Attachments ] */}
|
||||
{/* [ Textarea + Overlays + Mic | Attachments ] */}
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex', flexDirection: 'column', gap: 1,
|
||||
minWidth: 200, // enable X-scrolling (resetting any possible minWidth due to the attachments)
|
||||
flexGrow: 1,
|
||||
display: 'grid', gap: 1,
|
||||
}}>
|
||||
|
||||
{/* Edit box + Overlays + Mic buttons */}
|
||||
{/* Textarea + Mic buttons + Mic/Drag overlay */}
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
|
||||
{/* Edit box with inner Token Progress bar */}
|
||||
@@ -628,13 +650,13 @@ export function Composer(props: {
|
||||
|
||||
</Box>
|
||||
|
||||
</Stack></Grid>
|
||||
</Box></Grid>
|
||||
|
||||
|
||||
{/* Send pane (mobile: bottom, desktop: right) */}
|
||||
<Grid xs={12} md={3}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, height: '100%' }}>
|
||||
|
||||
{/* Send/Stop (and mobile corner buttons) */}
|
||||
{/* This row is here only for the [mobile] bottom-start corner item */}
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
|
||||
{/* [mobile] bottom-corner secondary button */}
|
||||
@@ -701,9 +723,11 @@ export function Composer(props: {
|
||||
|
||||
</Box>
|
||||
|
||||
{/* [desktop] Multicast switch (under the Chat button) */}
|
||||
{isDesktop && props.isMulticast !== null && <ButtonMultiChat multiChat={props.isMulticast} onSetMultiChat={props.setIsMulticast} />}
|
||||
|
||||
{/* [desktop] secondary buttons (aligned to bottom for now, and mutually exclusive) */}
|
||||
{isDesktop && <Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: 1, justifyContent: 'flex-end' }}>
|
||||
{isDesktop && <Box sx={{ mt: 'auto', display: 'grid', gap: 1 }}>
|
||||
|
||||
{/* [desktop] Call secondary button */}
|
||||
{isChat && <ButtonCall disabled={!props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
|
||||
@@ -716,23 +740,23 @@ export function Composer(props: {
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
|
||||
{/* Mode selector */}
|
||||
{!!chatModeMenuAnchor && (
|
||||
<ChatModeMenu
|
||||
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
|
||||
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
|
||||
capabilityHasTTI={props.capabilityHasT2I}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Camera */}
|
||||
{cameraCaptureComponent}
|
||||
|
||||
{/* Actile */}
|
||||
{actileComponent}
|
||||
|
||||
</Grid>
|
||||
|
||||
{/* Mode selector */}
|
||||
{!!chatModeMenuAnchor && (
|
||||
<ChatModeMenu
|
||||
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
|
||||
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
|
||||
capabilityHasTTI={props.capabilityHasT2I}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Camera */}
|
||||
{cameraCaptureComponent}
|
||||
|
||||
{/* Actile */}
|
||||
{actileComponent}
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -21,7 +21,11 @@ export function ActilePopup(props: {
|
||||
const hasAnyIcon = props.items.some(item => !!item.Icon);
|
||||
|
||||
return (
|
||||
<CloseableMenu open anchorEl={props.anchorEl} onClose={props.onClose} noTopPadding noBottomPadding sx={{ minWidth: 320 }}>
|
||||
<CloseableMenu
|
||||
noTopPadding noBottomPadding
|
||||
open anchorEl={props.anchorEl} onClose={props.onClose}
|
||||
sx={{ minWidth: 320 }}
|
||||
>
|
||||
|
||||
{!!props.title && (
|
||||
<Sheet variant='soft' sx={{ p: 1, borderBottom: '1px solid', borderBottomColor: 'neutral.softActiveBg' }}>
|
||||
@@ -50,7 +54,7 @@ export function ActilePopup(props: {
|
||||
color={isActive ? 'primary' : undefined}
|
||||
onClick={() => props.onItemClick(item)}
|
||||
>
|
||||
<ListItemButton>
|
||||
<ListItemButton color='primary'>
|
||||
{hasAnyIcon && (
|
||||
<ListItemDecorator>
|
||||
{item.Icon ? <item.Icon /> : null}
|
||||
|
||||
@@ -96,9 +96,9 @@ export function AttachmentMenu(props: {
|
||||
|
||||
return (
|
||||
<CloseableMenu
|
||||
dense placement='top' sx={{ minWidth: 200 }}
|
||||
dense placement='top'
|
||||
open anchorEl={props.menuAnchor} onClose={props.onClose}
|
||||
noTopPadding noBottomPadding
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
|
||||
{/* Move Arrows */}
|
||||
|
||||
@@ -141,9 +141,8 @@ export function Attachments(props: {
|
||||
{/* Overall Menu */}
|
||||
{!!overallMenuAnchor && (
|
||||
<CloseableMenu
|
||||
placement='top-start'
|
||||
dense placement='top-start'
|
||||
open anchorEl={overallMenuAnchor} onClose={handleOverallMenuHide}
|
||||
noTopPadding noBottomPadding
|
||||
>
|
||||
<MenuItem onClick={handleAttachmentsInlineText} disabled={!isOutputTextInlineable}>
|
||||
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
|
||||
|
||||
@@ -2,13 +2,13 @@ import { create } from 'zustand';
|
||||
import type { FileWithHandle } from 'browser-fs-access';
|
||||
|
||||
import type { ComposerOutputMultiPart } from '../composer.types';
|
||||
import { attachmentPerformConversion, attachmentCreate, attachmentDefineConverters, attachmentLoadInputAsync } from './pipeline';
|
||||
import { attachmentCreate, attachmentDefineConverters, attachmentLoadInputAsync, attachmentPerformConversion } from './pipeline';
|
||||
|
||||
|
||||
// Attachment Types
|
||||
|
||||
export type AttachmentSourceOriginDTO = 'drop' | 'paste';
|
||||
export type AttachmentSourceOriginFile = 'camera' | 'file-open' | 'clipboard-read' | AttachmentSourceOriginDTO;
|
||||
export type AttachmentSourceOriginFile = 'camera' | 'screencapture' | 'file-open' | 'clipboard-read' | AttachmentSourceOriginDTO;
|
||||
|
||||
export type AttachmentSource = {
|
||||
media: 'url';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, IconButton, Tooltip } from '@mui/joy';
|
||||
import ScreenshotMonitorIcon from '@mui/icons-material/ScreenshotMonitor';
|
||||
|
||||
import { takeScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
|
||||
|
||||
export const ButtonAttachScreenCaptureMemo = React.memo(ButtonAttachScreenCapture);
|
||||
|
||||
function ButtonAttachScreenCapture(props: { isMobile?: boolean, onAttachScreenCapture: (file: File) => void }) {
|
||||
|
||||
// state
|
||||
const [capturing, setCapturing] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
|
||||
// derived state
|
||||
const { onAttachScreenCapture } = props;
|
||||
|
||||
|
||||
const handleTakeScreenCapture = React.useCallback(async () => {
|
||||
setError(null);
|
||||
setCapturing(true);
|
||||
try {
|
||||
const file = await takeScreenCapture();
|
||||
file && onAttachScreenCapture(file);
|
||||
} catch (error: any) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setError(`Screen capture issue: ${message}`);
|
||||
}
|
||||
setCapturing(false);
|
||||
}, [onAttachScreenCapture]);
|
||||
|
||||
|
||||
return props.isMobile ? (
|
||||
<IconButton onClick={handleTakeScreenCapture}>
|
||||
<ScreenshotMonitorIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tooltip
|
||||
arrow disableInteractive variant='solid' placement='top-start'
|
||||
title={
|
||||
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
|
||||
<b>Attach screen capture</b><br />
|
||||
{error || 'Attach the image of a window, a browser tab, or a screen'}
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
variant={capturing ? 'solid' : 'plain'}
|
||||
color={!!error ? 'danger' : 'neutral'}
|
||||
onClick={handleTakeScreenCapture}
|
||||
loading={capturing}
|
||||
startDecorator={<ScreenshotMonitorIcon />}
|
||||
sx={{ justifyContent: 'flex-start' }}
|
||||
>
|
||||
Screen
|
||||
</Button>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, FormControl, FormLabel, IconButton, Switch } from '@mui/joy';
|
||||
|
||||
import { ChatMulticastOnIcon } from '~/common/components/icons/ChatMulticastOnIcon';
|
||||
import { ChatMulticastOffIcon } from '~/common/components/icons/ChatMulticastOffIcon';
|
||||
|
||||
|
||||
export function ButtonMultiChat(props: { isMobile?: boolean, multiChat: boolean, onSetMultiChat: (multiChat: boolean) => void }) {
|
||||
const { multiChat } = props;
|
||||
return props.isMobile ? (
|
||||
<IconButton
|
||||
variant={multiChat ? 'solid' : 'outlined'}
|
||||
color={multiChat ? 'warning' : undefined}
|
||||
onClick={() => props.onSetMultiChat(!multiChat)}
|
||||
>
|
||||
{multiChat ? <ChatMulticastOnIcon /> : <ChatMulticastOffIcon />}
|
||||
</IconButton>
|
||||
) : (
|
||||
<FormControl orientation='horizontal' sx={{ minHeight: '2.25rem', justifyContent: 'space-between' }}>
|
||||
<FormLabel sx={{ gap: 1, flexFlow: 'row nowrap' }}>
|
||||
<Box sx={{ display: { xs: 'none', lg: 'inline-block' } }}>
|
||||
{multiChat ? <ChatMulticastOnIcon sx={{ color: 'warning.solidBg' }} /> : <ChatMulticastOffIcon />}
|
||||
</Box>
|
||||
{multiChat ? 'Multichat · On' : 'Multichat'}
|
||||
</FormLabel>
|
||||
<Switch color={multiChat ? 'primary' : undefined} checked={multiChat} onChange={(e) => props.onSetMultiChat(e.target.checked)} />
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
+5
-4
@@ -12,6 +12,7 @@ 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';
|
||||
import { themeZIndexOverMobileDrawer } from '~/common/app.theme';
|
||||
|
||||
|
||||
export function FolderListItem(props: {
|
||||
@@ -199,9 +200,9 @@ export function FolderListItem(props: {
|
||||
|
||||
{!!menuAnchorEl && (
|
||||
<CloseableMenu
|
||||
dense placement='top'
|
||||
open anchorEl={menuAnchorEl} onClose={handleMenuClose}
|
||||
placement='top'
|
||||
zIndex={1301 /* need to be on top of the Modal on Mobile */}
|
||||
zIndex={themeZIndexOverMobileDrawer /* need to be on top of the Modal on Mobile */}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
|
||||
@@ -254,10 +255,10 @@ export function FolderListItem(props: {
|
||||
id='folder-color'
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
fontWeight: 'xl',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'xl',
|
||||
letterSpacing: '0.1em',
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
Color
|
||||
+12
-7
@@ -1,13 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { DropdownItems, PageBarDropdown } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { DropdownItems, PageBarDropdownMemo } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
|
||||
|
||||
export const ClearFolderText = 'Clear Folder';
|
||||
export const ClearFolderText = 'No Folder';
|
||||
const SPECIAL_ID_CLEAR_FOLDER = '_REMOVE_';
|
||||
|
||||
|
||||
@@ -18,7 +19,10 @@ export function useFolderDropdown(conversationId: DConversationId | null) {
|
||||
|
||||
|
||||
// Prepare items for the dropdown
|
||||
const folderItems: DropdownItems = React.useMemo(() => {
|
||||
const folderItems: DropdownItems | null = React.useMemo(() => {
|
||||
if (!folders.length)
|
||||
return null;
|
||||
|
||||
// add one item per folder
|
||||
const items = folders.reduce((items, folder) => {
|
||||
items[folder.id] = {
|
||||
@@ -31,6 +35,7 @@ export function useFolderDropdown(conversationId: DConversationId | null) {
|
||||
// add one item representing no folder
|
||||
items[SPECIAL_ID_CLEAR_FOLDER] = {
|
||||
title: ClearFolderText,
|
||||
icon: <ClearIcon />,
|
||||
};
|
||||
|
||||
return items;
|
||||
@@ -38,7 +43,7 @@ export function useFolderDropdown(conversationId: DConversationId | null) {
|
||||
|
||||
|
||||
// Handle dropdown folder change
|
||||
const handleFolderChange = React.useCallback((_event: any, folderId: string | null) => {
|
||||
const handleFolderChange = React.useCallback((folderId: string | null) => {
|
||||
if (conversationId && folderId) {
|
||||
// Remove conversation from all folders
|
||||
folders.forEach(folder => {
|
||||
@@ -59,15 +64,15 @@ export function useFolderDropdown(conversationId: DConversationId | null) {
|
||||
const folderDropdown = React.useMemo(() => {
|
||||
|
||||
// don't show the dropdown if folders are not enabled
|
||||
if (!enableFolders)
|
||||
if (!enableFolders || !folderItems)
|
||||
return null;
|
||||
|
||||
return (
|
||||
<PageBarDropdown
|
||||
<PageBarDropdownMemo
|
||||
items={folderItems}
|
||||
value={currentFolderId}
|
||||
onChange={handleFolderChange}
|
||||
placeholder='Select a folder'
|
||||
placeholder='Assign to folder'
|
||||
showSymbols
|
||||
/>
|
||||
);
|
||||
@@ -1,10 +1,7 @@
|
||||
import * as React from 'react';
|
||||
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, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import { Avatar, Box, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
@@ -23,30 +20,19 @@ import VerticalAlignBottomIcon from '@mui/icons-material/VerticalAlignBottom';
|
||||
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { DMessage } from '~/common/state/store-chats';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
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, lineHeightChatText } from '~/common/app.theme';
|
||||
import { cssRainbowColorKeyframes } from '~/common/app.theme';
|
||||
import { prettyBaseModel } from '~/common/util/modelUtils';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { BlocksRenderer, editBlocksSx } from './blocks/BlocksRenderer';
|
||||
import { useChatShowTextDiff } from '../../store-app-chat';
|
||||
import { useSanityTextDiffs } from './blocks/RenderTextDiff';
|
||||
|
||||
import { RenderCode } from './RenderCode';
|
||||
import { RenderHtml } from './RenderHtml';
|
||||
import { RenderImage } from './RenderImage';
|
||||
import { RenderLatex } from './RenderLatex';
|
||||
import { RenderMarkdown } from './RenderMarkdown';
|
||||
import { RenderText } from './RenderText';
|
||||
import { RenderTextDiff } from './RenderTextDiff';
|
||||
import { parseBlocks } from './blocks';
|
||||
|
||||
|
||||
// How long is the user collapsed message
|
||||
const USER_COLLAPSED_LINES: number = 8;
|
||||
|
||||
// Enable the menu on text selection
|
||||
const ENABLE_SELECTION_RIGHT_CLICK_MENU: boolean = true;
|
||||
@@ -180,21 +166,6 @@ function explainErrorInMessage(text: string, isAssistant: boolean, modelId?: str
|
||||
return { errorMessage, isAssistantError };
|
||||
}
|
||||
|
||||
function useSanityTextDiffs(text: string, diffText: string | undefined, enabled: boolean) {
|
||||
const [diffs, setDiffs] = React.useState<TextDiff[] | null>(null);
|
||||
React.useEffect(() => {
|
||||
if (!diffText || !enabled)
|
||||
return setDiffs(null);
|
||||
setDiffs(
|
||||
cleanupEfficiency(makeDiff(diffText, text, {
|
||||
timeout: 1,
|
||||
checkLines: true,
|
||||
}), 4),
|
||||
);
|
||||
}, [text, diffText, enabled]);
|
||||
return diffs;
|
||||
}
|
||||
|
||||
|
||||
export const ChatMessageMemo = React.memo(ChatMessage);
|
||||
|
||||
@@ -206,13 +177,14 @@ export const ChatMessageMemo = React.memo(ChatMessage);
|
||||
* or collapsing long user messages.
|
||||
*
|
||||
*/
|
||||
export function ChatMessage(props: {
|
||||
function ChatMessage(props: {
|
||||
message: DMessage,
|
||||
showDate?: boolean, diffPreviousText?: string,
|
||||
hideAvatars?: boolean, codeBackground?: string,
|
||||
noMarkdown?: boolean, diagramMode?: boolean,
|
||||
isBottom?: boolean, noBottomBorder?: boolean,
|
||||
isImagining?: boolean, isSpeaking?: boolean,
|
||||
diffPreviousText?: string,
|
||||
isBottom?: boolean,
|
||||
isMobile?: boolean,
|
||||
isImagining?: boolean,
|
||||
isSpeaking?: boolean,
|
||||
blocksShowDate?: boolean,
|
||||
onConversationBranch?: (messageId: string) => void,
|
||||
onConversationRestartFrom?: (messageId: string, offset: number) => Promise<void>,
|
||||
onConversationTruncate?: (messageId: string) => void,
|
||||
@@ -221,11 +193,9 @@ export function ChatMessage(props: {
|
||||
onTextDiagram?: (messageId: string, text: string) => Promise<void>
|
||||
onTextImagine?: (text: string) => Promise<void>
|
||||
onTextSpeak?: (text: string) => Promise<void>
|
||||
sx?: SxProps,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [forceUserExpanded, setForceUserExpanded] = React.useState(false);
|
||||
const [isHovering, setIsHovering] = React.useState(false);
|
||||
const [opsMenuAnchor, setOpsMenuAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [selMenuAnchor, setSelMenuAnchor] = React.useState<HTMLElement | null>(null);
|
||||
@@ -233,10 +203,11 @@ export function ChatMessage(props: {
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const { cleanerLooks, renderMarkdown, doubleClickToEdit } = useUIPreferencesStore(state => ({
|
||||
const { cleanerLooks, doubleClickToEdit, messageTextSize, renderMarkdown } = useUIPreferencesStore(state => ({
|
||||
cleanerLooks: state.zenMode === 'cleaner',
|
||||
renderMarkdown: state.renderMarkdown,
|
||||
doubleClickToEdit: state.doubleClickToEdit,
|
||||
messageTextSize: state.messageTextSize,
|
||||
renderMarkdown: state.renderMarkdown,
|
||||
}), shallow);
|
||||
const [showDiff, setShowDiff] = useChatShowTextDiff();
|
||||
const textDiffs = useSanityTextDiffs(props.message.text, props.diffPreviousText, showDiff);
|
||||
@@ -257,10 +228,9 @@ export function ChatMessage(props: {
|
||||
|
||||
const fromAssistant = messageRole === 'assistant';
|
||||
const fromSystem = messageRole === 'system';
|
||||
const fromUser = messageRole === 'user';
|
||||
const wasEdited = !!messageUpdated;
|
||||
|
||||
const showAvatars = props.hideAvatars !== true && !cleanerLooks;
|
||||
const showAvatars = !cleanerLooks;
|
||||
|
||||
const textSel = selMenuText ? selMenuText : messageText;
|
||||
const isSpecialT2I = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/draw ') || textSel.startsWith('/imagine ') || textSel.startsWith('/img ');
|
||||
@@ -275,36 +245,35 @@ export function ChatMessage(props: {
|
||||
props.onMessageEdit(messageId, editedText);
|
||||
};
|
||||
|
||||
const handleUncollapse = () => setForceUserExpanded(true);
|
||||
|
||||
|
||||
// Operations Menu
|
||||
|
||||
const closeOperationsMenu = () => setOpsMenuAnchor(null);
|
||||
const closeOpsMenu = () => setOpsMenuAnchor(null);
|
||||
|
||||
const handleOpsCopy = (e: React.MouseEvent) => {
|
||||
copyToClipboard(textSel, 'Text');
|
||||
e.preventDefault();
|
||||
closeOperationsMenu();
|
||||
closeOpsMenu();
|
||||
closeSelectionMenu();
|
||||
};
|
||||
|
||||
const handleOpsEdit = (e: React.MouseEvent) => {
|
||||
const handleOpsEdit = React.useCallback((e: React.MouseEvent) => {
|
||||
if (messageTyping && !isEditing) return; // don't allow editing while typing
|
||||
setIsEditing(!isEditing);
|
||||
e.preventDefault();
|
||||
closeOperationsMenu();
|
||||
};
|
||||
closeOpsMenu();
|
||||
}, [isEditing, messageTyping]);
|
||||
|
||||
const handleOpsConversationBranch = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // to try to not steal the focus from the banched conversation
|
||||
props.onConversationBranch && props.onConversationBranch(messageId);
|
||||
closeOperationsMenu();
|
||||
closeOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsConversationRestartFrom = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
closeOperationsMenu();
|
||||
closeOpsMenu();
|
||||
props.onConversationRestartFrom && await props.onConversationRestartFrom(messageId, fromAssistant ? -1 : 0);
|
||||
};
|
||||
|
||||
@@ -314,7 +283,7 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextDiagram) {
|
||||
await props.onTextDiagram(messageId, textSel);
|
||||
closeOperationsMenu();
|
||||
closeOpsMenu();
|
||||
closeSelectionMenu();
|
||||
}
|
||||
};
|
||||
@@ -323,7 +292,7 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextImagine) {
|
||||
await props.onTextImagine(textSel);
|
||||
closeOperationsMenu();
|
||||
closeOpsMenu();
|
||||
closeSelectionMenu();
|
||||
}
|
||||
};
|
||||
@@ -332,14 +301,14 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextSpeak) {
|
||||
await props.onTextSpeak(textSel);
|
||||
closeOperationsMenu();
|
||||
closeOpsMenu();
|
||||
closeSelectionMenu();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpsTruncate = (_e: React.MouseEvent) => {
|
||||
props.onConversationTruncate && props.onConversationTruncate(messageId);
|
||||
closeOperationsMenu();
|
||||
closeOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsDelete = (_e: React.MouseEvent) => {
|
||||
@@ -395,6 +364,17 @@ export function ChatMessage(props: {
|
||||
}, [openSelectionMenu]);
|
||||
|
||||
|
||||
// Blocks renderer
|
||||
|
||||
const handleBlocksContextMenu = React.useCallback((event: React.MouseEvent) => {
|
||||
handleMouseUp(event.nativeEvent);
|
||||
}, [handleMouseUp]);
|
||||
|
||||
const handleBlocksDoubleClick = React.useCallback((event: React.MouseEvent) => {
|
||||
doubleClickToEdit && props.onMessageEdit && handleOpsEdit(event);
|
||||
}, [doubleClickToEdit, handleOpsEdit, props.onMessageEdit]);
|
||||
|
||||
|
||||
// prettier upstream errors
|
||||
const { isAssistantError, errorMessage } = React.useMemo(
|
||||
() => explainErrorInMessage(messageText, fromAssistant, messageOriginLLM),
|
||||
@@ -410,50 +390,19 @@ export function ChatMessage(props: {
|
||||
[messageAvatar, messageOriginLLM, messagePurposeId, messageRole, messageSender, messageTyping, showAvatars],
|
||||
);
|
||||
|
||||
// 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: '0.875rem',
|
||||
fontVariantLigatures: 'none',
|
||||
lineHeight: lineHeightChatText,
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
};
|
||||
|
||||
// user message truncation
|
||||
let collapsedText = messageText;
|
||||
let isCollapsed = false;
|
||||
if (fromUser && !forceUserExpanded) {
|
||||
const lines = messageText.split('\n');
|
||||
if (lines.length > USER_COLLAPSED_LINES) {
|
||||
collapsedText = lines.slice(0, USER_COLLAPSED_LINES).join('\n');
|
||||
isCollapsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
display: 'flex', flexDirection: !fromAssistant ? 'row-reverse' : 'row', alignItems: 'flex-start',
|
||||
gap: { xs: 0, md: 1 }, px: { xs: 1, md: 2 }, py: 2,
|
||||
gap: { xs: 0, md: 1 },
|
||||
px: { xs: 1, md: 2 },
|
||||
py: 2,
|
||||
backgroundColor,
|
||||
...(props.noBottomBorder !== true && {
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
}),
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
...(ENABLE_COPY_MESSAGE_OVERLAY && { position: 'relative' }),
|
||||
'&:hover > button': { opacity: 1 },
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -499,73 +448,26 @@ export function ChatMessage(props: {
|
||||
|
||||
<InlineTextarea
|
||||
initialText={messageText} onEdit={handleTextEdited}
|
||||
sx={{
|
||||
...blockSx,
|
||||
flexGrow: 1,
|
||||
}} />
|
||||
sx={editBlocksSx}
|
||||
/>
|
||||
|
||||
) : (
|
||||
|
||||
<Box
|
||||
onContextMenu={(ENABLE_SELECTION_RIGHT_CLICK_MENU && props.onMessageEdit) ? event => handleMouseUp(event.nativeEvent) : undefined}
|
||||
onDoubleClick={event => (doubleClickToEdit && props.onMessageEdit) ? handleOpsEdit(event) : null}
|
||||
sx={{
|
||||
...blockSx,
|
||||
flexGrow: 0,
|
||||
overflowX: 'auto',
|
||||
...(!!props.diagramMode && {
|
||||
// width: '100%',
|
||||
boxShadow: 'md',
|
||||
}),
|
||||
}}>
|
||||
<BlocksRenderer
|
||||
text={messageText}
|
||||
fromRole={messageRole}
|
||||
renderTextAsMarkdown={renderMarkdown}
|
||||
messageTextSize={messageTextSize}
|
||||
errorMessage={errorMessage}
|
||||
isBottom={props.isBottom}
|
||||
isMobile={props.isMobile}
|
||||
showDate={props.blocksShowDate === true ? messageUpdated || messageCreated || undefined : undefined}
|
||||
renderTextDiff={textDiffs || undefined}
|
||||
wasUserEdited={wasEdited}
|
||||
onContextMenu={(props.onMessageEdit && ENABLE_SELECTION_RIGHT_CLICK_MENU) ? handleBlocksContextMenu : undefined}
|
||||
onDoubleClick={(props.onMessageEdit && doubleClickToEdit) ? handleBlocksDoubleClick : undefined}
|
||||
/>
|
||||
|
||||
{props.showDate === true && (
|
||||
<Typography level='body-sm' sx={{ mx: 1.5, textAlign: fromAssistant ? 'left' : 'right' }}>
|
||||
<TimeAgo date={messageUpdated || messageCreated} />
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Warn about user-edited system message */}
|
||||
{fromSystem && wasEdited && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1, mx: 1.5 }}>modified by user - auto-update disabled</Typography>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<Tooltip title={<Typography sx={{ maxWidth: 800 }}>{collapsedText}</Typography>} variant='soft'>
|
||||
<InlineError error={errorMessage} />
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* sequence of render components, for each Block */}
|
||||
{!errorMessage && parseBlocks(collapsedText, fromSystem, textDiffs)
|
||||
.filter((block, _, blocks) => !props.diagramMode || block.type === 'code' || blocks.length === 1)
|
||||
.map(
|
||||
(block, index) =>
|
||||
block.type === 'html'
|
||||
? <RenderHtml key={'html-' + index} htmlBlock={block} sx={codeSx} />
|
||||
: block.type === 'code'
|
||||
? <RenderCode key={'code-' + index} codeBlock={block} sx={codeSx} noCopyButton={props.diagramMode} />
|
||||
: block.type === 'image'
|
||||
? <RenderImage key={'image-' + index} imageBlock={block} isFirst={!index} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
|
||||
: block.type === 'latex'
|
||||
? <RenderLatex key={'latex-' + index} latexBlock={block} sx={typographySx} />
|
||||
: block.type === 'diff'
|
||||
? <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} sx={typographySx} />)}
|
||||
|
||||
{isCollapsed && (
|
||||
<Button variant='plain' color='neutral' onClick={handleUncollapse}>... expand ...</Button>
|
||||
)}
|
||||
|
||||
{/* import VisibilityIcon from '@mui/icons-material/Visibility'; */}
|
||||
{/*<br />*/}
|
||||
{/*<Chip variant='outlined' color='warning' sx={{ mt: 1, fontSize: '0.75em' }} startDecorator={<VisibilityIcon />}>*/}
|
||||
{/* BlockAction*/}
|
||||
{/*</Chip>*/}
|
||||
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
@@ -587,9 +489,11 @@ export function ChatMessage(props: {
|
||||
{/* Operations Menu (3 dots) */}
|
||||
{!!opsMenuAnchor && (
|
||||
<CloseableMenu
|
||||
dense placement='bottom-end' sx={{ minWidth: 280 }}
|
||||
open anchorEl={opsMenuAnchor} onClose={closeOperationsMenu}
|
||||
dense placement='bottom-end'
|
||||
open anchorEl={opsMenuAnchor} onClose={closeOpsMenu}
|
||||
sx={{ minWidth: 280 }}
|
||||
>
|
||||
{/* Edit / Copy */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
{!!props.onMessageEdit && (
|
||||
<MenuItem variant='plain' disabled={messageTyping} onClick={handleOpsEdit} sx={{ flex: 1 }}>
|
||||
@@ -603,6 +507,32 @@ export function ChatMessage(props: {
|
||||
Copy
|
||||
</MenuItem>
|
||||
</Box>
|
||||
{/* Delete / Branch / Truncate */}
|
||||
{!!props.onMessageDelete && <ListDivider />}
|
||||
{!!props.onMessageDelete && (
|
||||
<MenuItem onClick={handleOpsDelete} disabled={false /*fromSystem*/}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Delete
|
||||
<span style={{ opacity: 0.5 }}>message</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onConversationBranch && (
|
||||
<MenuItem onClick={handleOpsConversationBranch} disabled={fromSystem}>
|
||||
<ListItemDecorator>
|
||||
<ForkRightIcon />
|
||||
</ListItemDecorator>
|
||||
Branch
|
||||
{!props.isBottom && <span style={{ opacity: 0.5 }}>from here</span>}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onConversationTruncate && (
|
||||
<MenuItem onClick={handleOpsTruncate} disabled={props.isBottom}>
|
||||
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
|
||||
Truncate
|
||||
<span style={{ opacity: 0.5 }}>after this</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Diff Viewer */}
|
||||
{!!props.diffPreviousText && <ListDivider />}
|
||||
{!!props.diffPreviousText && (
|
||||
<MenuItem onClick={handleOpsToggleShowDiff}>
|
||||
@@ -611,10 +541,31 @@ export function ChatMessage(props: {
|
||||
<Switch checked={showDiff} onChange={handleOpsToggleShowDiff} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
)}
|
||||
<ListDivider />
|
||||
{/* Diagram / Draw / Speak */}
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && (
|
||||
<MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
|
||||
<ListItemDecorator><AccountTreeIcon color='success' /></ListItemDecorator>
|
||||
Diagram ...
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextImagine && (
|
||||
<MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
|
||||
Draw ...
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextSpeak && (
|
||||
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Restart/try */}
|
||||
{!!props.onConversationRestartFrom && <ListDivider />}
|
||||
{!!props.onConversationRestartFrom && (
|
||||
<MenuItem onClick={handleOpsConversationRestartFrom}>
|
||||
<ListItemDecorator>{fromAssistant ? <ReplayIcon /> : <TelegramIcon />}</ListItemDecorator>
|
||||
<ListItemDecorator>{fromAssistant ? <ReplayIcon color='primary' /> : <TelegramIcon color='primary' />}</ListItemDecorator>
|
||||
{!fromAssistant
|
||||
? <>Restart <span style={{ opacity: 0.5 }}>from here</span></>
|
||||
: !props.isBottom
|
||||
@@ -622,42 +573,7 @@ export function ChatMessage(props: {
|
||||
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Retry
|
||||
<KeyStroke combo='Ctrl + Shift + R' />
|
||||
</Box>
|
||||
}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onConversationBranch && (
|
||||
<MenuItem onClick={handleOpsConversationBranch} disabled={fromSystem}>
|
||||
<ListItemDecorator>
|
||||
<ForkRightIcon />
|
||||
</ListItemDecorator>
|
||||
Branch {!props.isBottom && <span style={{ opacity: 0.5 }}>from here</span>}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onConversationBranch && <ListDivider />}
|
||||
{!!props.onTextDiagram && <MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
|
||||
<ListItemDecorator><AccountTreeIcon color='success' /></ListItemDecorator>
|
||||
Diagram ...
|
||||
</MenuItem>}
|
||||
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
|
||||
Draw ...
|
||||
</MenuItem>}
|
||||
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>}
|
||||
{!!props.onConversationRestartFrom && <ListDivider />}
|
||||
{!!props.onConversationTruncate && (
|
||||
<MenuItem onClick={handleOpsTruncate} disabled={props.isBottom}>
|
||||
<ListItemDecorator><VerticalAlignBottomIcon /></ListItemDecorator>
|
||||
Truncate <span style={{ opacity: 0.5 }}>after</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onMessageDelete && (
|
||||
<MenuItem onClick={handleOpsDelete} disabled={false /*fromSystem*/}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Delete <span style={{ opacity: 0.5 }}>message</span>
|
||||
</Box>}
|
||||
</MenuItem>
|
||||
)}
|
||||
</CloseableMenu>
|
||||
@@ -666,8 +582,9 @@ export function ChatMessage(props: {
|
||||
{/* Selection (Contextual) Menu */}
|
||||
{!!selMenuAnchor && (
|
||||
<CloseableMenu
|
||||
dense placement='bottom-start' sx={{ minWidth: 220 }}
|
||||
dense placement='bottom-start'
|
||||
open anchorEl={selMenuAnchor} onClose={closeSelectionMenu}
|
||||
sx={{ minWidth: 220 }}
|
||||
>
|
||||
<MenuItem onClick={handleOpsCopy} sx={{ flex: 1 }}>
|
||||
<ListItemDecorator><ContentCopyIcon /></ListItemDecorator>
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, styled } from '@mui/joy';
|
||||
|
||||
import { lineHeightChatText } from '~/common/app.theme';
|
||||
|
||||
import type { TextBlock } from './blocks';
|
||||
|
||||
|
||||
/*
|
||||
* For performance reasons, we style this component here and copy the equivalent of 'props.sx' (the lineHeight) locally.
|
||||
*/
|
||||
const RenderMarkdownBox = styled(Box)({
|
||||
// same look as the other RenderComponents
|
||||
marginInline: '0.75rem !important', // margin: 1.5 like other blocks
|
||||
lineHeight: lineHeightChatText,
|
||||
|
||||
// patch the CSS
|
||||
// fontFamily: `inherit !important`, // (not needed anymore, as CSS is under our control) use the default font family
|
||||
// '--color-canvas-default': 'transparent !important', // (not needed anymore) remove the default background color
|
||||
'& table': { width: 'inherit !important' }, // un-break auto-width (tables have 'max-content', which overflows)
|
||||
});
|
||||
|
||||
|
||||
// Dynamically import ReactMarkdown using React.lazy
|
||||
const DynamicReactGFM = React.lazy(async () => {
|
||||
const [markdownModule, remarkGfmModule] = await Promise.all([
|
||||
import('react-markdown'),
|
||||
import('remark-gfm'),
|
||||
]);
|
||||
|
||||
// NOTE: extracted here instead of inline as a large performance optimization
|
||||
const remarkPlugins = [remarkGfmModule.default];
|
||||
|
||||
// Pass the dynamically imported remarkGfm as children
|
||||
const ReactMarkdownWithRemarkGfm = (props: any) =>
|
||||
<markdownModule.default remarkPlugins={remarkPlugins} {...props} />;
|
||||
|
||||
return { default: ReactMarkdownWithRemarkGfm };
|
||||
});
|
||||
|
||||
|
||||
export const RenderMarkdown = (props: { textBlock: TextBlock }) => {
|
||||
return (
|
||||
<RenderMarkdownBox className='markdown-body' /* NODE: see GithubMarkdown.css for the dark/light switch, synced with Joy's */ >
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<DynamicReactGFM>
|
||||
{props.textBlock.content}
|
||||
</DynamicReactGFM>
|
||||
</React.Suspense>
|
||||
</RenderMarkdownBox>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
import * as React from 'react';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import type { Diff as TextDiff } from '@sanity/diff-match-patch';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, Tooltip, Typography } from '@mui/joy';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
import type { UIMessageTextSize } from '~/common/state/store-ui';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { lineHeightChatCode, lineHeightChatText } from '~/common/app.theme';
|
||||
|
||||
import { RenderCodeMemo } from './code/RenderCode';
|
||||
import { RenderHtml } from './RenderHtml';
|
||||
import { RenderImage } from './RenderImage';
|
||||
import { RenderLatex } from './RenderLatex';
|
||||
import { RenderMarkdownMemo } from './RenderMarkdown';
|
||||
import { RenderText } from './RenderText';
|
||||
import { RenderTextDiff } from './RenderTextDiff';
|
||||
import { areBlocksEqual, Block, parseMessageBlocks } from './blocks';
|
||||
|
||||
|
||||
// How long is the user collapsed message
|
||||
const USER_COLLAPSED_LINES: number = 8;
|
||||
|
||||
|
||||
const blocksSx: SxProps = {
|
||||
my: 'auto',
|
||||
lineHeight: lineHeightChatText,
|
||||
} as const;
|
||||
|
||||
export const editBlocksSx: SxProps = {
|
||||
...blocksSx,
|
||||
flexGrow: 1,
|
||||
} as const;
|
||||
|
||||
const renderBlocksSx: SxProps = {
|
||||
...blocksSx,
|
||||
flexGrow: 0,
|
||||
overflowX: 'auto',
|
||||
} as const;
|
||||
|
||||
|
||||
export function BlocksRenderer(props: {
|
||||
|
||||
// required
|
||||
text: string;
|
||||
fromRole: DMessage['role'];
|
||||
messageTextSize?: UIMessageTextSize;
|
||||
renderTextAsMarkdown: boolean;
|
||||
renderTextDiff?: TextDiff[];
|
||||
|
||||
errorMessage?: React.ReactNode;
|
||||
isBottom?: boolean;
|
||||
isMobile?: boolean;
|
||||
showDate?: number;
|
||||
wasUserEdited?: boolean;
|
||||
|
||||
specialDiagramMode?: boolean;
|
||||
|
||||
onContextMenu?: (event: React.MouseEvent) => void;
|
||||
onDoubleClick?: (event: React.MouseEvent) => void;
|
||||
onImageRegenerate?: () => void;
|
||||
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [forceUserExpanded, setForceUserExpanded] = React.useState(false);
|
||||
const prevBlocksRef = React.useRef<Block[]>([]);
|
||||
|
||||
// derived state
|
||||
const { text: _text, errorMessage, renderTextDiff, wasUserEdited = false } = props;
|
||||
const fromAssistant = props.fromRole === 'assistant';
|
||||
const fromSystem = props.fromRole === 'system';
|
||||
const fromUser = props.fromRole === 'user';
|
||||
|
||||
|
||||
const handleTextUncollapse = React.useCallback(() => {
|
||||
setForceUserExpanded(true);
|
||||
}, []);
|
||||
|
||||
|
||||
// Memo text, which could be 'collapsed' to a few lines in case of user messages
|
||||
|
||||
const { text, isTextCollapsed } = React.useMemo(() => {
|
||||
if (fromUser && !forceUserExpanded) {
|
||||
const textLines = _text.split('\n');
|
||||
if (textLines.length > USER_COLLAPSED_LINES)
|
||||
return { text: textLines.slice(0, USER_COLLAPSED_LINES).join('\n'), isTextCollapsed: true };
|
||||
}
|
||||
return { text: _text, isTextCollapsed: false };
|
||||
}, [forceUserExpanded, fromUser, _text]);
|
||||
|
||||
// Memo the code style, to minimize re-renders
|
||||
|
||||
const scaledCodeSx: SxProps = React.useMemo(() => (
|
||||
{
|
||||
backgroundColor: props.specialDiagramMode ? 'background.surface' : fromAssistant ? 'neutral.plainHoverBg' : 'primary.plainActiveBg',
|
||||
boxShadow: props.specialDiagramMode ? 'md' : 'xs',
|
||||
fontFamily: 'code',
|
||||
fontSize: props.messageTextSize === 'xs' ? '0.75rem' : props.messageTextSize === 'sm' ? '0.75rem' : '0.875rem',
|
||||
fontVariantLigatures: 'none',
|
||||
lineHeight: lineHeightChatCode,
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
}
|
||||
), [fromAssistant, props.messageTextSize, props.specialDiagramMode]);
|
||||
|
||||
const scaledTypographySx: SxProps = React.useMemo(() => (
|
||||
{
|
||||
lineHeight: lineHeightChatText,
|
||||
fontSize: (!props.messageTextSize || props.messageTextSize === 'md') ? undefined : props.messageTextSize,
|
||||
}
|
||||
), [props.messageTextSize]);
|
||||
|
||||
|
||||
// Block splitter, with memoand special recycle of former blocks, to help React minimize render work
|
||||
|
||||
const blocks = React.useMemo(() => {
|
||||
// split the complete input text into blocks
|
||||
const newBlocks = errorMessage ? [] : parseMessageBlocks(text, fromSystem, renderTextDiff);
|
||||
|
||||
// recycle the previous blocks if they are the same, for stable references to React
|
||||
const recycledBlocks: Block[] = [];
|
||||
for (let i = 0; i < newBlocks.length; i++) {
|
||||
const newBlock = newBlocks[i];
|
||||
const prevBlock = prevBlocksRef.current[i];
|
||||
|
||||
// Check if the new block can be replaced by the previous block to maintain reference stability
|
||||
if (prevBlock && areBlocksEqual(prevBlock, newBlock)) {
|
||||
recycledBlocks.push(prevBlock);
|
||||
} else {
|
||||
// Once a block doesn't match, we use the new blocks from this point forward.
|
||||
recycledBlocks.push(...newBlocks.slice(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update prevBlocksRef with the current blocks for the next render
|
||||
prevBlocksRef.current = recycledBlocks;
|
||||
|
||||
// Apply specialDiagramMode filter if applicable
|
||||
return props.specialDiagramMode
|
||||
? recycledBlocks.filter(block => block.type === 'code' || recycledBlocks.length === 1)
|
||||
: recycledBlocks;
|
||||
}, [errorMessage, fromSystem, props.specialDiagramMode, renderTextDiff, text]);
|
||||
|
||||
|
||||
return (
|
||||
<Box
|
||||
onContextMenu={props.onContextMenu}
|
||||
onDoubleClick={props.onDoubleClick}
|
||||
sx={renderBlocksSx}
|
||||
>
|
||||
|
||||
{!!props.showDate && (
|
||||
<Typography level='body-sm' sx={{ mx: 1.5, textAlign: fromAssistant ? 'left' : 'right' }}>
|
||||
<TimeAgo date={props.showDate} />
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Warn about user-edited system message */}
|
||||
{fromSystem && wasUserEdited && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1, mx: 1.5 }}>modified by user - auto-update disabled</Typography>
|
||||
)}
|
||||
|
||||
{errorMessage ? (
|
||||
|
||||
<Tooltip title={<Typography sx={{ maxWidth: 800 }}>{text}</Typography>} variant='soft'>
|
||||
<InlineError error={errorMessage} />
|
||||
</Tooltip>
|
||||
|
||||
) : (
|
||||
|
||||
// sequence of render components, for each Block
|
||||
blocks.map(
|
||||
(block, index) =>
|
||||
block.type === 'html'
|
||||
? <RenderHtml key={'html-' + index} htmlBlock={block} sx={scaledCodeSx} />
|
||||
: block.type === 'code'
|
||||
? <RenderCodeMemo key={'code-' + index} codeBlock={block} isMobile={props.isMobile} noCopyButton={props.specialDiagramMode} sx={scaledCodeSx} />
|
||||
: block.type === 'image'
|
||||
? <RenderImage key={'image-' + index} imageBlock={block} isFirst={!index} allowRunAgain={props.isBottom === true} onRunAgain={props.onImageRegenerate} />
|
||||
: block.type === 'latex'
|
||||
? <RenderLatex key={'latex-' + index} latexBlock={block} sx={scaledTypographySx} />
|
||||
: block.type === 'diff'
|
||||
? <RenderTextDiff key={'latex-' + index} diffBlock={block} sx={scaledTypographySx} />
|
||||
: (props.renderTextAsMarkdown && !fromSystem && !(fromUser && block.content.startsWith('/')))
|
||||
? <RenderMarkdownMemo key={'text-md-' + index} textBlock={block} sx={scaledTypographySx} />
|
||||
: <RenderText key={'text-' + index} textBlock={block} sx={scaledTypographySx} />)
|
||||
|
||||
)}
|
||||
|
||||
{isTextCollapsed && <Button variant='plain' color='neutral' onClick={handleTextUncollapse}>... expand ...</Button>}
|
||||
|
||||
{/* import VisibilityIcon from '@mui/icons-material/Visibility'; */}
|
||||
{/*<br />*/}
|
||||
{/*<Chip variant='outlined' color='warning' sx={{ mt: 1, fontSize: '0.75em' }} startDecorator={<VisibilityIcon />}>*/}
|
||||
{/* BlockAction*/}
|
||||
{/*</Chip>*/}
|
||||
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, IconButton, Tooltip, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import WebIcon from '@mui/icons-material/Web';
|
||||
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
|
||||
import { HtmlBlock } from './blocks';
|
||||
import { overlayButtonsSx } from './RenderCode';
|
||||
import type { HtmlBlock } from './blocks';
|
||||
import { overlayButtonsSx } from './code/RenderCode';
|
||||
|
||||
|
||||
// this is used by the blocks parser (for full text detection) and by the Code component (for inline rendering)
|
||||
+2
-2
@@ -6,8 +6,8 @@ import ReplayIcon from '@mui/icons-material/Replay';
|
||||
|
||||
import { Link } from '~/common/components/Link';
|
||||
|
||||
import { ImageBlock } from './blocks';
|
||||
import { overlayButtonsSx } from './RenderCode';
|
||||
import type { ImageBlock } from './blocks';
|
||||
import { overlayButtonsSx } from './code/RenderCode';
|
||||
|
||||
|
||||
const mdImageReferenceRegex = /^!\[([^\]]*)]\(([^)]+)\)$/;
|
||||
+7
-5
@@ -3,7 +3,7 @@ import * as React from 'react';
|
||||
import { Box } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import { LatexBlock } from './blocks';
|
||||
import type { LatexBlock } from './blocks';
|
||||
|
||||
|
||||
// Dynamically import the Katex functions
|
||||
@@ -14,13 +14,15 @@ const RenderLatexDynamic = React.lazy(async () => {
|
||||
};
|
||||
});
|
||||
|
||||
export const RenderLatex = ({ latexBlock, sx }: { latexBlock: LatexBlock; sx?: SxProps; }) =>
|
||||
export const RenderLatex = (props: { latexBlock: LatexBlock; sx?: SxProps; }) =>
|
||||
<Box
|
||||
sx={{
|
||||
mx: 1.5,
|
||||
...(sx || {}),
|
||||
my: '0.5em',
|
||||
textAlign: 'center',
|
||||
...props.sx,
|
||||
}}>
|
||||
<React.Suspense fallback={<div/>}>
|
||||
<RenderLatexDynamic latex={latexBlock.latex} />
|
||||
<React.Suspense fallback={<div />}>
|
||||
<RenderLatexDynamic latex={props.latexBlock.latex} />
|
||||
</React.Suspense>
|
||||
</Box>;
|
||||
@@ -0,0 +1,134 @@
|
||||
import * as React from 'react';
|
||||
import { CSVLink } from 'react-csv';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, styled } from '@mui/joy';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
|
||||
import { lineHeightChatText } from '~/common/app.theme';
|
||||
|
||||
import type { TextBlock } from './blocks';
|
||||
|
||||
|
||||
/*
|
||||
* For performance reasons, we style this component here and copy the equivalent of 'props.sx' (the lineHeight) locally.
|
||||
*/
|
||||
const RenderMarkdownBox = styled(Box)({
|
||||
// same look as the other RenderComponents
|
||||
marginInline: '0.75rem !important', // margin: 1.5 like other blocks
|
||||
lineHeight: lineHeightChatText,
|
||||
|
||||
// patch the CSS
|
||||
// fontFamily: `inherit !important`, // (not needed anymore, as CSS is under our control) use the default font family
|
||||
// '--color-canvas-default': 'transparent !important', // (not needed anymore) remove the default background color
|
||||
'& table': { width: 'inherit !important' }, // un-break auto-width (tables have 'max-content', which overflows)
|
||||
});
|
||||
|
||||
|
||||
// Dynamically import ReactMarkdown using React.lazy
|
||||
const DynamicReactGFM = React.lazy(async () => {
|
||||
const [markdownModule, remarkGfmModule] = await Promise.all([
|
||||
import('react-markdown'),
|
||||
import('remark-gfm'),
|
||||
]);
|
||||
|
||||
// NOTE: extracted here instead of inline as a large performance optimization
|
||||
const remarkPlugins = [remarkGfmModule.default];
|
||||
|
||||
//Extracts table data from jsx element in table renderer
|
||||
const extractTableData = (children: React.JSX.Element) => {
|
||||
// Function to extract text from a React element or component
|
||||
const extractText = (element: any): String => {
|
||||
// Base case: if the element is a string, return it
|
||||
if (typeof element === 'string') {
|
||||
return element;
|
||||
}
|
||||
// If the element has children, recursively extract text from them
|
||||
if (element.props && element.props.children) {
|
||||
if (Array.isArray(element.props.children)) {
|
||||
return element.props.children.map(extractText).join('');
|
||||
}
|
||||
return extractText(element.props.children);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
// Function to traverse and extract data from table rows and cells
|
||||
const traverseAndExtract = (elements: any, tableData: any[] = []) => {
|
||||
React.Children.forEach(elements, (element) => {
|
||||
if (element.type === 'tr') {
|
||||
const rowData = React.Children.map(element.props.children, (cell) => {
|
||||
// Extract and return the text content of each cell
|
||||
return extractText(cell);
|
||||
});
|
||||
tableData.push(rowData);
|
||||
} else if (element.props && element.props.children) {
|
||||
traverseAndExtract(element.props.children, tableData);
|
||||
}
|
||||
});
|
||||
return tableData;
|
||||
};
|
||||
|
||||
return traverseAndExtract(children);
|
||||
};
|
||||
|
||||
interface TableRendererProps {
|
||||
children: React.JSX.Element;
|
||||
node?: any; // an optional field we want to not pass to the <table/> element
|
||||
}
|
||||
|
||||
// Define a custom table renderer
|
||||
const TableRenderer = ({ children, node, ...props }: TableRendererProps) => {
|
||||
// Apply custom styles or modifications here
|
||||
const tableData = extractTableData(children);
|
||||
|
||||
return (
|
||||
<>
|
||||
<table style={{ borderCollapse: 'collapse', width: '100%', marginBottom: '0.5rem' }} {...props}>
|
||||
{children}
|
||||
</table>
|
||||
<CSVLink filename='big-agi-export' data={tableData}>
|
||||
<Button variant='outlined' color='neutral' size='md' endDecorator={<DownloadIcon />} sx={{
|
||||
mb: '1rem',
|
||||
backgroundColor: 'background.popup', // make this button 'pop' a bit from the page
|
||||
}}>
|
||||
Download table as .csv
|
||||
</Button>
|
||||
</CSVLink>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// Use the custom renderer for tables
|
||||
const components = {
|
||||
table: TableRenderer,
|
||||
// Add custom renderers for other elements if needed
|
||||
};
|
||||
|
||||
// Pass the dynamically imported remarkGfm as children
|
||||
const ReactMarkdownWithRemarkGfm = (props: any) =>
|
||||
<markdownModule.default
|
||||
remarkPlugins={remarkPlugins}
|
||||
{...props}
|
||||
components={components}
|
||||
/>;
|
||||
|
||||
return { default: ReactMarkdownWithRemarkGfm };
|
||||
});
|
||||
|
||||
function RenderMarkdown(props: { textBlock: TextBlock; sx?: SxProps; }) {
|
||||
return (
|
||||
<RenderMarkdownBox
|
||||
className='markdown-body' /* NODE: see GithubMarkdown.css for the dark/light switch, synced with Joy's */
|
||||
sx={props.sx}
|
||||
>
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<DynamicReactGFM>
|
||||
{props.textBlock.content}
|
||||
</DynamicReactGFM>
|
||||
</React.Suspense>
|
||||
</RenderMarkdownBox>
|
||||
);
|
||||
}
|
||||
|
||||
export const RenderMarkdownMemo = React.memo(RenderMarkdown);
|
||||
+4
-2
@@ -3,13 +3,15 @@ import * as React from 'react';
|
||||
import { Chip, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import { extractChatCommand } from '../../commands/commands.registry';
|
||||
import { extractChatCommand } from '../../../commands/commands.registry';
|
||||
|
||||
import type { TextBlock } from './blocks';
|
||||
|
||||
|
||||
export const RenderText = (props: { textBlock: TextBlock; sx?: SxProps; }) => {
|
||||
|
||||
const elements = extractChatCommand(props.textBlock.content);
|
||||
|
||||
return (
|
||||
<Typography
|
||||
sx={{
|
||||
@@ -18,7 +20,7 @@ export const RenderText = (props: { textBlock: TextBlock; sx?: SxProps; }) => {
|
||||
alignItems: 'baseline',
|
||||
overflowWrap: 'anywhere',
|
||||
whiteSpace: 'break-spaces',
|
||||
...(props.sx || {}),
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{elements.map((element, index) =>
|
||||
+36
-6
@@ -1,19 +1,49 @@
|
||||
import * as React from 'react';
|
||||
import { Diff as TextDiff, DIFF_DELETE, DIFF_INSERT } from '@sanity/diff-match-patch';
|
||||
import { cleanupEfficiency, Diff as TextDiff, DIFF_DELETE, DIFF_INSERT, makeDiff } from '@sanity/diff-match-patch';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Typography, useTheme } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import { DiffBlock } from './blocks';
|
||||
import type { DiffBlock } from './blocks';
|
||||
|
||||
|
||||
export const RenderTextDiff = ({ diffBlock, sx }: { diffBlock: DiffBlock; sx?: SxProps; }) => {
|
||||
export function useSanityTextDiffs(_text: string, _diffText: string | undefined, enabled: boolean) {
|
||||
// state
|
||||
const [diffs, setDiffs] = React.useState<TextDiff[] | null>(null);
|
||||
|
||||
const inputText = enabled ? _text : null;
|
||||
const inputPrevText = enabled ? _diffText : null;
|
||||
|
||||
// async processing of diffs
|
||||
React.useEffect(() => {
|
||||
if (!inputText || !inputPrevText)
|
||||
return setDiffs(null);
|
||||
|
||||
const callback = () => {
|
||||
setDiffs(
|
||||
cleanupEfficiency(makeDiff(inputPrevText, inputText, {
|
||||
timeout: 1,
|
||||
checkLines: true,
|
||||
}), 4),
|
||||
);
|
||||
};
|
||||
|
||||
// slight delay to cancel the previous operation if too close to this
|
||||
const timeout = setTimeout(callback, 200);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [inputPrevText, inputText]);
|
||||
|
||||
return diffs;
|
||||
}
|
||||
|
||||
|
||||
export const RenderTextDiff = (props: { diffBlock: DiffBlock; sx?: SxProps; }) => {
|
||||
|
||||
// external state
|
||||
const theme = useTheme();
|
||||
|
||||
// derived state
|
||||
const textDiffs: TextDiff[] = diffBlock.textDiffs;
|
||||
const textDiffs: TextDiff[] = props.diffBlock.textDiffs;
|
||||
|
||||
// text added
|
||||
const styleAdd = {
|
||||
@@ -44,7 +74,7 @@ export const RenderTextDiff = ({ diffBlock, sx }: { diffBlock: DiffBlock; sx?: S
|
||||
whiteSpace: 'break-spaces',
|
||||
display: 'block',
|
||||
zIndex: 200,
|
||||
...(sx || {}),
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{textDiffs.map(([op, text], index) =>
|
||||
+31
-8
@@ -1,9 +1,10 @@
|
||||
import type { Diff as TextDiff } from '@sanity/diff-match-patch';
|
||||
|
||||
import { heuristicIsHtml } from './RenderHtml';
|
||||
import { heuristicMarkdownImageReferenceBlocks, heuristicLegacyImageBlocks } from './RenderImage';
|
||||
import { heuristicLegacyImageBlocks, heuristicMarkdownImageReferenceBlocks } from './RenderImage';
|
||||
|
||||
type Block = CodeBlock | DiffBlock | HtmlBlock | ImageBlock | LatexBlock | TextBlock;
|
||||
// Block types
|
||||
export 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; };
|
||||
@@ -12,11 +13,33 @@ export type LatexBlock = { type: 'latex'; latex: string; };
|
||||
export type TextBlock = { type: 'text'; content: string; }; // for Text or Markdown
|
||||
|
||||
|
||||
export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDiff[] | null): Block[] {
|
||||
if (forceText)
|
||||
export function areBlocksEqual(a: Block, b: Block): boolean {
|
||||
if (a.type !== b.type)
|
||||
return false;
|
||||
|
||||
switch (a.type) {
|
||||
case 'code':
|
||||
return a.blockTitle === (b as CodeBlock).blockTitle && a.blockCode === (b as CodeBlock).blockCode && a.complete === (b as CodeBlock).complete;
|
||||
case 'diff':
|
||||
return false; // diff blocks are never equal
|
||||
case 'html':
|
||||
return a.html === (b as HtmlBlock).html;
|
||||
case 'image':
|
||||
return a.url === (b as ImageBlock).url && a.alt === (b as ImageBlock).alt;
|
||||
case 'latex':
|
||||
return a.latex === (b as LatexBlock).latex;
|
||||
case 'text':
|
||||
return a.content === (b as TextBlock).content;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function parseMessageBlocks(text: string, disableParsing: boolean, forceTextDiffs?: TextDiff[]): Block[] {
|
||||
if (disableParsing)
|
||||
return [{ type: 'text', content: text }];
|
||||
if (textDiffs && textDiffs.length > 0)
|
||||
return [{ type: 'diff', textDiffs }];
|
||||
|
||||
if (forceTextDiffs && forceTextDiffs.length >= 1)
|
||||
return [{ type: 'diff', textDiffs: forceTextDiffs }];
|
||||
|
||||
// special case: this could be generated by a proxy that returns an HTML page instead of the API response
|
||||
if (heuristicIsHtml(text))
|
||||
@@ -33,8 +56,8 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
|
||||
return legacyImageBlocks;
|
||||
|
||||
const regexPatterns = {
|
||||
codeBlock: /`{3,}([\w\\.+-_]+)?\n([\s\S]*?)(`{3,}\n?|$)/g,
|
||||
latexBlock: /\$\$([\s\S]*?)\$\$/g,
|
||||
codeBlock: /`{3,}([\w\x20\\.+-_]+)?\n([\s\S]*?)(`{3,}\n?|$)/g,
|
||||
latexBlock: /\$\$([\s\S]*?)\$\$\n?/g,
|
||||
// latexBlockOrInline: /\$\$([\s\S]*?)\$\$|\$([^$]*?)\$/g,
|
||||
};
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ interface CodeBlockProps {
|
||||
};
|
||||
}
|
||||
|
||||
export function OpenInCodepen({ codeBlock }: CodeBlockProps): React.JSX.Element {
|
||||
export function ButtonCodepen({ codeBlock }: CodeBlockProps): React.JSX.Element {
|
||||
const { code, language } = codeBlock;
|
||||
const hasCSS = language === 'css';
|
||||
const hasJS = ['javascript', 'json', 'typescript'].includes(language || '');
|
||||
+1
-1
@@ -9,7 +9,7 @@ interface CodeBlockProps {
|
||||
};
|
||||
}
|
||||
|
||||
export function OpenInReplit({ codeBlock }: CodeBlockProps): React.JSX.Element {
|
||||
export function ButtonReplit({ codeBlock }: CodeBlockProps): React.JSX.Element {
|
||||
const { language } = codeBlock;
|
||||
|
||||
const replitLanguageMap: Record<string, string> = {
|
||||
+95
-67
@@ -1,36 +1,73 @@
|
||||
import * as React from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, IconButton, Sheet, Tooltip, Typography } from '@mui/joy';
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import FitScreenIcon from '@mui/icons-material/FitScreen';
|
||||
import HtmlIcon from '@mui/icons-material/Html';
|
||||
import SchemaIcon from '@mui/icons-material/Schema';
|
||||
import ShapeLineOutlinedIcon from '@mui/icons-material/ShapeLineOutlined';
|
||||
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
|
||||
import { CodeBlock } from './blocks';
|
||||
import { OpenInCodepen } from './OpenInCodepen';
|
||||
import { OpenInReplit } from './OpenInReplit';
|
||||
import { RenderCodeMermaid } from './RenderCodeMermaid';
|
||||
import { heuristicIsHtml, IFrameComponent } from './RenderHtml';
|
||||
import type { CodeBlock } from '../blocks';
|
||||
import { ButtonCodepen } from './ButtonCodepen';
|
||||
import { ButtonReplit } from './ButtonReplit';
|
||||
import { heuristicIsHtml, IFrameComponent } from '../RenderHtml';
|
||||
import { patchSvgString, RenderCodeMermaid } from './RenderCodeMermaid';
|
||||
|
||||
|
||||
async function fetchPlantUmlSvg(plantUmlCode: string): Promise<string | null> {
|
||||
// fetch the PlantUML SVG
|
||||
let text: string = '';
|
||||
try {
|
||||
// Dynamically import the PlantUML encoder - it's a large library that slows down app loading
|
||||
const { encode: plantUmlEncode } = await import('plantuml-encoder');
|
||||
|
||||
// retrieve and manually adapt the SVG, to remove the background
|
||||
const encodedPlantUML: string = plantUmlEncode(plantUmlCode);
|
||||
const response = await fetch(`https://www.plantuml.com/plantuml/svg/${encodedPlantUML}`);
|
||||
text = await response.text();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
// validate/extract the SVG
|
||||
const start = text.indexOf('<svg ');
|
||||
const end = text.indexOf('</svg>');
|
||||
if (start < 0 || end <= start)
|
||||
throw new Error('Could not render PlantUML');
|
||||
const svg = text
|
||||
.slice(start, end + 6) // <svg ... </svg>
|
||||
.replace('background:#FFFFFF;', ''); // transparent background
|
||||
|
||||
// check for syntax errors
|
||||
if (svg.includes('>Syntax Error?</text>'))
|
||||
throw new Error('syntax issue (it happens!). Please regenerate or change generator model.');
|
||||
|
||||
return svg;
|
||||
}
|
||||
|
||||
|
||||
export const overlayButtonsSx: SxProps = {
|
||||
position: 'absolute', top: 0, right: 0, zIndex: 10,
|
||||
display: 'flex', flexDirection: 'row', gap: 1,
|
||||
opacity: 0, transition: 'opacity 0.2s',
|
||||
'& > button': { backdropFilter: 'blur(12px)' },
|
||||
// '& > button': {
|
||||
// backgroundColor: 'background.level2',
|
||||
// backdropFilter: 'blur(12px)',
|
||||
// },
|
||||
};
|
||||
|
||||
function RenderCodeImpl(props: {
|
||||
codeBlock: CodeBlock, noCopyButton?: boolean, sx?: SxProps,
|
||||
highlightCode: (inferredCodeLanguage: string | null, blockCode: string) => string,
|
||||
inferCodeLanguage: (blockTitle: string, code: string) => string | null,
|
||||
isMobile?: boolean,
|
||||
}) {
|
||||
|
||||
// state
|
||||
const [fitScreen, setFitScreen] = React.useState(!!props.isMobile);
|
||||
const [showHTML, setShowHTML] = React.useState(false);
|
||||
const [showMermaid, setShowMermaid] = React.useState(true);
|
||||
const [showPlantUML, setShowPlantUML] = React.useState(true);
|
||||
@@ -43,12 +80,11 @@ function RenderCodeImpl(props: {
|
||||
} = props;
|
||||
|
||||
// heuristic for language, and syntax highlight
|
||||
const { highlightedCode, inferredCodeLanguage } = React.useMemo(
|
||||
() => {
|
||||
const inferredCodeLanguage = inferCodeLanguage(blockTitle, blockCode);
|
||||
const highlightedCode = highlightCode(inferredCodeLanguage, blockCode);
|
||||
return { highlightedCode, inferredCodeLanguage };
|
||||
}, [inferCodeLanguage, blockTitle, blockCode, highlightCode]);
|
||||
const { highlightedCode, inferredCodeLanguage } = React.useMemo(() => {
|
||||
const inferredCodeLanguage = inferCodeLanguage(blockTitle, blockCode);
|
||||
const highlightedCode = highlightCode(inferredCodeLanguage, blockCode);
|
||||
return { highlightedCode, inferredCodeLanguage };
|
||||
}, [inferCodeLanguage, blockTitle, blockCode, highlightCode]);
|
||||
|
||||
|
||||
// heuristics for specialized rendering
|
||||
@@ -70,41 +106,14 @@ function RenderCodeImpl(props: {
|
||||
const { data: plantUmlHtmlData, error: plantUmlError } = useQuery({
|
||||
enabled: renderPlantUML,
|
||||
queryKey: ['plantuml', blockCode],
|
||||
queryFn: async () => {
|
||||
// fetch the PlantUML SVG
|
||||
let text: string = '';
|
||||
try {
|
||||
// Dynamically import the PlantUML encoder - it's a large library that slows down app loading
|
||||
const { encode: plantUmlEncode } = await import('plantuml-encoder');
|
||||
|
||||
// retrieve and manually adapt the SVG, to remove the background
|
||||
const encodedPlantUML: string = plantUmlEncode(blockCode);
|
||||
const response = await fetch(`https://www.plantuml.com/plantuml/svg/${encodedPlantUML}`);
|
||||
text = await response.text();
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
// validate/extract the SVG
|
||||
const start = text.indexOf('<svg ');
|
||||
const end = text.indexOf('</svg>');
|
||||
if (start < 0 || end <= start)
|
||||
throw new Error('Could not render PlantUML');
|
||||
const svg = text
|
||||
.slice(start, end + 6) // <svg ... </svg>
|
||||
.replace('background:#FFFFFF;', ''); // transparent background
|
||||
|
||||
// check for syntax errors
|
||||
if (svg.includes('>Syntax Error?</text>'))
|
||||
throw new Error('syntax issue (it happens!). Please regenerate or change generator model.');
|
||||
|
||||
return svg;
|
||||
},
|
||||
queryFn: () => fetchPlantUmlSvg(blockCode),
|
||||
staleTime: 24 * 60 * 60 * 1000, // 1 day
|
||||
});
|
||||
renderPlantUML = renderPlantUML && (!!plantUmlHtmlData || !!plantUmlError);
|
||||
|
||||
const isSVG = blockCode.startsWith('<svg') && blockCode.endsWith('</svg>');
|
||||
const renderSVG = isSVG && showSVG;
|
||||
const canScaleSVG = renderSVG && blockCode.includes('viewBox="');
|
||||
|
||||
|
||||
const languagesCodepen = ['html', 'css', 'javascript', 'json', 'typescript'];
|
||||
@@ -119,7 +128,11 @@ function RenderCodeImpl(props: {
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ position: 'relative' /* for overlay buttons to stick properly */ }}>
|
||||
<Box sx={{
|
||||
position: 'relative', /* for overlay buttons to stick properly */
|
||||
}}>
|
||||
|
||||
{/* Code render */}
|
||||
<Box
|
||||
component='code'
|
||||
className={`language-${inferredCodeLanguage || 'unknown'}`}
|
||||
@@ -128,6 +141,7 @@ function RenderCodeImpl(props: {
|
||||
mx: 0, p: 1.5, // this block gets a thicker border
|
||||
display: 'block',
|
||||
overflowX: 'auto',
|
||||
minWidth: 160,
|
||||
'&:hover > .overlay-buttons': { opacity: 1 },
|
||||
...(props.sx || {}),
|
||||
}}>
|
||||
@@ -146,14 +160,14 @@ function RenderCodeImpl(props: {
|
||||
{renderHTML
|
||||
? <IFrameComponent htmlString={blockCode} />
|
||||
: renderMermaid
|
||||
? <RenderCodeMermaid mermaidCode={blockCode} />
|
||||
? <RenderCodeMermaid mermaidCode={blockCode} fitScreen={fitScreen} />
|
||||
: <Box component='div'
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
renderSVG
|
||||
? blockCode
|
||||
? (patchSvgString(fitScreen, blockCode) || 'No SVG code')
|
||||
: renderPlantUML
|
||||
? (plantUmlHtmlData || (plantUmlError as string) || 'No PlantUML rendering.')
|
||||
? (patchSvgString(fitScreen, plantUmlHtmlData) || (plantUmlError as string) || 'No PlantUML rendering.')
|
||||
: highlightedCode,
|
||||
}}
|
||||
sx={{
|
||||
@@ -162,43 +176,52 @@ function RenderCodeImpl(props: {
|
||||
}}
|
||||
/>}
|
||||
|
||||
{/* Code Buttons */}
|
||||
{/* Buttons */}
|
||||
<Box className='overlay-buttons' sx={{ ...overlayButtonsSx, p: 0.5 }}>
|
||||
{isHTML && (
|
||||
<Tooltip title={renderHTML ? 'Hide' : 'Show Web Page'} variant='solid'>
|
||||
<IconButton variant={renderHTML ? 'solid' : 'outlined'} color='danger' onClick={() => setShowHTML(!showHTML)}>
|
||||
<Tooltip title={renderHTML ? 'Hide' : 'Show Web Page'}>
|
||||
<IconButton variant={renderHTML ? 'solid' : 'soft'} color='danger' onClick={() => setShowHTML(!showHTML)}>
|
||||
<HtmlIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isMermaid && (
|
||||
<Tooltip title={renderMermaid ? 'Show Code' : 'Render Mermaid'} variant='solid'>
|
||||
<IconButton variant={renderMermaid ? 'solid' : 'outlined'} onClick={() => setShowMermaid(!showMermaid)}>
|
||||
<Tooltip title={renderMermaid ? 'Show Code' : 'Render Mermaid'}>
|
||||
<IconButton variant={renderMermaid ? 'solid' : 'soft'} onClick={() => setShowMermaid(!showMermaid)}>
|
||||
<SchemaIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isPlantUML && (
|
||||
<Tooltip title={renderPlantUML ? 'Show Code' : 'Render PlantUML'} variant='solid'>
|
||||
<IconButton variant={renderPlantUML ? 'solid' : 'outlined'} onClick={() => setShowPlantUML(!showPlantUML)}>
|
||||
<Tooltip title={renderPlantUML ? 'Show Code' : 'Render PlantUML'}>
|
||||
<IconButton variant={renderPlantUML ? 'solid' : 'soft'} onClick={() => setShowPlantUML(!showPlantUML)}>
|
||||
<SchemaIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isSVG && (
|
||||
<Tooltip title={renderSVG ? 'Show Code' : 'Render SVG'} variant='solid'>
|
||||
<IconButton variant={renderSVG ? 'solid' : 'outlined'} onClick={() => setShowSVG(!showSVG)}>
|
||||
<Tooltip title={renderSVG ? 'Show Code' : 'Render SVG'}>
|
||||
<IconButton variant={renderSVG ? 'solid' : 'soft'} onClick={() => setShowSVG(!showSVG)}>
|
||||
<ShapeLineOutlinedIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{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' onClick={handleCopyToClipboard}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{((isMermaid && showMermaid) || (isPlantUML && showPlantUML) || (isSVG && showSVG && canScaleSVG)) && (
|
||||
<Tooltip title={fitScreen ? 'Original Size' : 'Fit Screen'}>
|
||||
<IconButton variant={fitScreen ? 'solid' : 'soft'} onClick={() => setFitScreen(on => !on)}>
|
||||
<FitScreenIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{canCodepen && <ButtonCodepen codeBlock={{ code: blockCode, language: inferredCodeLanguage || undefined }} />}
|
||||
{canReplit && <ButtonReplit codeBlock={{ code: blockCode, language: inferredCodeLanguage || undefined }} />}
|
||||
{props.noCopyButton !== true && (
|
||||
<Tooltip title='Copy Code'>
|
||||
<IconButton variant='soft' onClick={handleCopyToClipboard}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
@@ -213,12 +236,17 @@ const RenderCodeDynamic = React.lazy(async () => {
|
||||
const { highlightCode, inferCodeLanguage } = await import('./codePrism');
|
||||
|
||||
return {
|
||||
default: (props: { codeBlock: CodeBlock, noCopyButton?: boolean, sx?: SxProps }) =>
|
||||
default: (props: { codeBlock: CodeBlock, isMobile?: boolean, noCopyButton?: boolean, sx?: SxProps }) =>
|
||||
<RenderCodeImpl highlightCode={highlightCode} inferCodeLanguage={inferCodeLanguage} {...props} />,
|
||||
};
|
||||
});
|
||||
|
||||
export const RenderCode = (props: { codeBlock: CodeBlock, noCopyButton?: boolean, sx?: SxProps }) =>
|
||||
<React.Suspense fallback={<Box component='code' sx={{ p: 1.5, display: 'block', ...(props.sx || {}) }} />}>
|
||||
<RenderCodeDynamic {...props} />
|
||||
</React.Suspense>;
|
||||
function RenderCode(props: { codeBlock: CodeBlock, isMobile?: boolean, noCopyButton?: boolean, sx?: SxProps }) {
|
||||
return (
|
||||
<React.Suspense fallback={<Box component='code' sx={{ p: 1.5, display: 'block', ...props.sx }} />}>
|
||||
<RenderCodeDynamic {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
export const RenderCodeMemo = React.memo(RenderCode);
|
||||
+7
-3
@@ -107,10 +107,10 @@ function useMermaidLoader() {
|
||||
}
|
||||
|
||||
|
||||
export function RenderCodeMermaid(props: { mermaidCode: string }) {
|
||||
export function RenderCodeMermaid(props: { mermaidCode: string, fitScreen: boolean }) {
|
||||
|
||||
// state
|
||||
const [svgCode, setSvgCode] = React.useState<string | null>(null);
|
||||
const [_svgCode, setSvgCode] = React.useState<string | null>(null);
|
||||
const hasUnmounted = React.useRef(false);
|
||||
const mermaidContainerRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -157,8 +157,12 @@ export function RenderCodeMermaid(props: { mermaidCode: string }) {
|
||||
<Box
|
||||
component='div'
|
||||
ref={mermaidContainerRef}
|
||||
dangerouslySetInnerHTML={{ __html: svgCode || 'Loading Diagram...' }}
|
||||
dangerouslySetInnerHTML={{ __html: patchSvgString(props.fitScreen, _svgCode) || 'Loading Diagram...' }}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export function patchSvgString(fitScreen: boolean, svgCode?: string | null): string | null {
|
||||
return fitScreen ? svgCode?.replace('<svg ', `<svg style="width: 100%; height: 100%; object-fit: contain" `) || null : svgCode || null;
|
||||
}
|
||||
@@ -27,13 +27,13 @@ interface AppChatPanesStore {
|
||||
// state
|
||||
chatPanes: ChatPane[];
|
||||
chatPaneFocusIndex: number | null;
|
||||
chatPaneInputMode: 'focused' | 'broadcast';
|
||||
|
||||
// actions
|
||||
openConversationInFocusedPane: (conversationId: DConversationId) => void;
|
||||
openConversationInSplitPane: (conversationId: DConversationId) => void;
|
||||
navigateHistoryInFocusedPane: (direction: 'back' | 'forward') => boolean;
|
||||
duplicatePane: (paneIndex: number) => void;
|
||||
duplicateFocusedPane: (/*paneIndex: number*/) => void;
|
||||
removeOtherPanes: () => void;
|
||||
removePane: (paneIndex: number) => void;
|
||||
setFocusedPane: (paneIndex: number) => void;
|
||||
onConversationsChanged: (conversationIds: DConversationId[]) => void;
|
||||
@@ -54,7 +54,6 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
// Initial state: no panes
|
||||
chatPanes: [] as ChatPane[],
|
||||
chatPaneFocusIndex: null as number | null,
|
||||
chatPaneInputMode: 'focused' as 'focused' | 'broadcast',
|
||||
|
||||
openConversationInFocusedPane: (conversationId: DConversationId) => {
|
||||
_set((state) => {
|
||||
@@ -160,18 +159,18 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
return true;
|
||||
},
|
||||
|
||||
duplicatePane: (paneIndex: number) =>
|
||||
duplicateFocusedPane: (/*paneIndex: number*/) =>
|
||||
_set(state => {
|
||||
const { chatPanes } = state;
|
||||
const { chatPanes, chatPaneFocusIndex: _srcIndex } = state;
|
||||
|
||||
// Validate index
|
||||
if (paneIndex < 0 || paneIndex >= chatPanes.length) {
|
||||
console.warn('Attempted to duplicate a pane with an out-of-range index:', paneIndex);
|
||||
if (_srcIndex === null || _srcIndex < 0 || _srcIndex >= chatPanes.length) {
|
||||
console.warn('Attempted to duplicate a pane with an out-of-range index:', _srcIndex);
|
||||
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 paneToDuplicate = chatPanes[_srcIndex];
|
||||
const duplicatedPane = {
|
||||
...paneToDuplicate,
|
||||
history: [...paneToDuplicate.history], // Deep copy of the history array
|
||||
@@ -179,14 +178,27 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
|
||||
// Insert the duplicated pane into the array, right after the original pane
|
||||
const newPanes = [
|
||||
...chatPanes.slice(0, paneIndex + 1),
|
||||
...chatPanes.slice(0, _srcIndex + 1),
|
||||
duplicatedPane,
|
||||
...chatPanes.slice(paneIndex + 1),
|
||||
...chatPanes.slice(_srcIndex + 1),
|
||||
];
|
||||
|
||||
return {
|
||||
chatPanes: newPanes,
|
||||
chatPaneFocusIndex: paneIndex + 1,
|
||||
chatPaneFocusIndex: _srcIndex + 1,
|
||||
};
|
||||
}),
|
||||
|
||||
removeOtherPanes: () =>
|
||||
_set(state => {
|
||||
const { chatPanes, chatPaneFocusIndex } = state;
|
||||
if (chatPanes.length < 2)
|
||||
return state;
|
||||
|
||||
const newPanes = [chatPanes[chatPaneFocusIndex ?? 0]];
|
||||
return {
|
||||
chatPanes: newPanes,
|
||||
chatPaneFocusIndex: 0,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -267,7 +279,7 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
// play it safe, and make sure a pane exists, and is focused
|
||||
return {
|
||||
chatPanes: newPanes.length ? newPanes : [createPane(conversationIds[0] ?? null)],
|
||||
chatPaneFocusIndex: (newPanes.length && chatPaneFocusIndex !== null && chatPaneFocusIndex < newPanes.length) ? state.chatPaneFocusIndex : 0,
|
||||
chatPaneFocusIndex: (newPanes.length && chatPaneFocusIndex !== null && chatPaneFocusIndex < newPanes.length) ? chatPaneFocusIndex : 0,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -287,7 +299,6 @@ export function usePanesManager() {
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
duplicatePane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
} = state;
|
||||
@@ -299,8 +310,7 @@ export function usePanesManager() {
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
paneIndex: chatPaneFocusIndex,
|
||||
duplicatePane,
|
||||
focusedPaneIndex: chatPaneFocusIndex,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
};
|
||||
@@ -319,4 +329,13 @@ export function usePanesManager() {
|
||||
return {
|
||||
...panesFunctions,
|
||||
};
|
||||
}
|
||||
|
||||
export function usePaneDuplicateOrClose() {
|
||||
return useAppChatPanesStore(state => ({
|
||||
canAddPane: state.chatPanes.length < 4,
|
||||
isMultiPane: state.chatPanes.length > 1,
|
||||
duplicateFocusedPane: state.duplicateFocusedPane,
|
||||
removeOtherPanes: state.removeOtherPanes,
|
||||
}), shallow);
|
||||
}
|
||||
@@ -1,56 +1,117 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, Button, Checkbox, Grid, IconButton, Input, Stack, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Avatar, Box, Button, Card, CardContent, Checkbox, IconButton, Input, List, ListItem, ListItemButton, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
import { useChatLLM } from '~/modules/llms/store-llms';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
import { lineHeightTextarea } from '~/common/app.theme';
|
||||
import { navigateToPersonas } from '~/common/app.routes';
|
||||
import { useChipBoolean } from '~/common/components/useChipBoolean';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
import { SystemPurposeData, 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
|
||||
//
|
||||
// Absolutely dislike this workaround, but it's the only way I found to make it work
|
||||
|
||||
const bpTileSize = { xs: 116, md: 125, xl: 130 };
|
||||
const tileCols = [3, 4, 6];
|
||||
const tileSpacing = 1;
|
||||
const bpMaxWidth = Object.entries(bpTileSize).reduce((acc, [key, value], index) => {
|
||||
acc[key] = tileCols[index] * (value + 8 * tileSpacing) - 8 * tileSpacing;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
const bpTileGap = { xs: 0.5, md: 1 };
|
||||
// defined looks
|
||||
const tileSize = 7.5; // rem
|
||||
const tileGap = 0.5; // rem
|
||||
|
||||
|
||||
// Add this utility function to get a random array element
|
||||
const getRandomElement = <T, >(array: T[]): T | undefined =>
|
||||
array.length > 0 ? array[Math.floor(Math.random() * array.length)] : undefined;
|
||||
function Tile(props: {
|
||||
text?: string,
|
||||
imageUrl?: string,
|
||||
symbol?: string,
|
||||
isActive: boolean,
|
||||
isEditMode: boolean,
|
||||
isHidden?: boolean,
|
||||
isHighlighted?: boolean,
|
||||
onClick: () => void,
|
||||
sx?: SxProps,
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
variant={(!props.isEditMode && props.isActive) ? 'solid' : props.isHighlighted ? 'soft' : 'soft'}
|
||||
color={(!props.isEditMode && props.isActive) ? 'primary' : props.isHighlighted ? 'primary' : 'neutral'}
|
||||
onClick={props.onClick}
|
||||
sx={{
|
||||
aspectRatio: 1,
|
||||
height: `${tileSize}rem`,
|
||||
fontWeight: 500,
|
||||
...((props.isEditMode || !props.isActive) ? {
|
||||
boxShadow: props.isHighlighted ? '0 2px 8px -2px rgb(var(--joy-palette-primary-mainChannel) / 50%)' : 'sm',
|
||||
backgroundColor: props.isHighlighted ? undefined : 'background.surface',
|
||||
...(props.imageUrl && {
|
||||
backgroundImage: `linear-gradient(rgba(255 255 255 /0.85), rgba(255 255 255 /1)), url(${props.imageUrl})`,
|
||||
backgroundPosition: 'center',
|
||||
backgroundSize: 'cover',
|
||||
}),
|
||||
} : {}),
|
||||
flexDirection: 'column', gap: 1,
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
{/* [Edit mode checkbox] */}
|
||||
{props.isEditMode && (
|
||||
<Checkbox
|
||||
variant='soft' color='neutral'
|
||||
checked={!props.isHidden}
|
||||
// label={<Typography level='body-xs'>show</Typography>}
|
||||
sx={{ position: 'absolute', left: `${tileGap}rem`, top: `${tileGap}rem` }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Icon and Text */}
|
||||
{/*<Box sx={{ fontSize: '2rem' }}>*/}
|
||||
{/* {props.symbol}*/}
|
||||
{/*</Box>*/}
|
||||
<Avatar
|
||||
variant='plain'
|
||||
src={props.imageUrl}
|
||||
sx={{
|
||||
'--Avatar-size': '3rem',
|
||||
fontSize: '2rem',
|
||||
borderRadius: props.imageUrl ? 'sm' : 0,
|
||||
boxShadow: (props.imageUrl && !props.isActive) ? 'sm' : undefined,
|
||||
}}
|
||||
>
|
||||
{props.symbol}
|
||||
</Avatar>
|
||||
<div>
|
||||
{props.text}
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Purpose selector for the current chat. Clicking on any item activates it for the current chat.
|
||||
*/
|
||||
export function PersonaSelector(props: { conversationId: DConversationId, runExample: (example: string) => void }) {
|
||||
|
||||
// state
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [filteredIDs, setFilteredIDs] = React.useState<SystemPurposeId[] | null>(null);
|
||||
const [editMode, setEditMode] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const showFinder = useUIPreferencesStore(state => state.showPurposeFinder);
|
||||
const showFinder = useUIPreferencesStore(state => state.showPersonaFinder);
|
||||
const [showExamples, showExamplescomponent] = useChipBoolean('Examples', false);
|
||||
const [showPrompt, showPromptComponent] = useChipBoolean('Prompt', false);
|
||||
const { systemPurposeId, setSystemPurposeId } = useChatStore(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
|
||||
return {
|
||||
@@ -59,228 +120,266 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
};
|
||||
}, shallow);
|
||||
const { hiddenPurposeIDs, toggleHiddenPurposeId } = usePurposeStore(state => ({ hiddenPurposeIDs: state.hiddenPurposeIDs, toggleHiddenPurposeId: state.toggleHiddenPurposeId }), shallow);
|
||||
|
||||
// safety check - shouldn't happen
|
||||
if (!systemPurposeId || !setSystemPurposeId)
|
||||
return null;
|
||||
const { chatLLM } = useChatLLM();
|
||||
|
||||
|
||||
const handleSearchClear = () => {
|
||||
setSearchQuery('');
|
||||
setFilteredIDs(null);
|
||||
};
|
||||
// derived state
|
||||
|
||||
const handleSearchOnChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const query = e.target.value;
|
||||
if (!query)
|
||||
return handleSearchClear();
|
||||
setSearchQuery(query);
|
||||
|
||||
// Filter results based on search term
|
||||
const ids = Object.keys(SystemPurposes)
|
||||
.filter(key => SystemPurposes.hasOwnProperty(key))
|
||||
.filter(key => {
|
||||
const purpose = SystemPurposes[key as SystemPurposeId];
|
||||
return purpose.title.toLowerCase().includes(query.toLowerCase())
|
||||
|| (typeof purpose.description === 'string' && purpose.description.toLowerCase().includes(query.toLowerCase()));
|
||||
});
|
||||
setFilteredIDs(ids as SystemPurposeId[]);
|
||||
|
||||
// If there's a search term, activate the first item
|
||||
if (ids.length && !ids.includes(systemPurposeId))
|
||||
handlePurposeChanged(ids[0] as SystemPurposeId);
|
||||
};
|
||||
|
||||
const handleSearchOnKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key == 'Escape')
|
||||
handleSearchClear();
|
||||
};
|
||||
const { selectedPurpose, fourExamples } = React.useMemo(() => {
|
||||
const selectedPurpose: SystemPurposeData | null = systemPurposeId ? (SystemPurposes[systemPurposeId] ?? null) : null;
|
||||
// const selectedExample = selectedPurpose?.examples?.length
|
||||
// ? selectedPurpose.examples[Math.floor(Math.random() * selectedPurpose.examples.length)]
|
||||
// : null;
|
||||
const fourExamples = selectedPurpose?.examples?.slice(0, 4) ?? null;
|
||||
return { selectedPurpose, fourExamples };
|
||||
}, [systemPurposeId]);
|
||||
|
||||
|
||||
const toggleEditMode = () => setEditMode(!editMode);
|
||||
const unfilteredPurposeIDs = (filteredIDs && showFinder) ? filteredIDs : Object.keys(SystemPurposes) as SystemPurposeId[];
|
||||
const visiblePurposeIDs = editMode ? unfilteredPurposeIDs : unfilteredPurposeIDs.filter(id => !hiddenPurposeIDs.includes(id));
|
||||
const hidePersonaCreator = hiddenPurposeIDs.includes(PURPOSE_ID_PERSONA_CREATOR);
|
||||
|
||||
|
||||
const handlePurposeChanged = (purposeId: SystemPurposeId | null) => {
|
||||
if (purposeId)
|
||||
// Handlers
|
||||
|
||||
const handlePurposeChanged = React.useCallback((purposeId: SystemPurposeId | null) => {
|
||||
if (purposeId && setSystemPurposeId)
|
||||
setSystemPurposeId(props.conversationId, purposeId);
|
||||
};
|
||||
}, [props.conversationId, setSystemPurposeId]);
|
||||
|
||||
const handleCustomSystemMessageChange = (v: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
const handleCustomSystemMessageChange = React.useCallback((v: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
// TODO: persist this change? Right now it's reset every time.
|
||||
// maybe we shall have a "save" button just save on a state to persist between sessions
|
||||
SystemPurposes['Custom'].systemMessage = v.target.value;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const toggleEditMode = React.useCallback(() => setEditMode(on => !on), []);
|
||||
|
||||
|
||||
// we show them all if the filter is clear (null)
|
||||
const unfilteredPurposeIDs = (filteredIDs && showFinder) ? filteredIDs : Object.keys(SystemPurposes);
|
||||
const purposeIDs = editMode ? unfilteredPurposeIDs : unfilteredPurposeIDs.filter(id => !hiddenPurposeIDs.includes(id));
|
||||
// Search (filtering)
|
||||
|
||||
const hidePersonaCreator = hiddenPurposeIDs.includes(PURPOSE_ID_PERSONA_CREATOR);
|
||||
const handleSearchClear = React.useCallback(() => {
|
||||
setSearchQuery('');
|
||||
setFilteredIDs(null);
|
||||
}, []);
|
||||
|
||||
const selectedPurpose = purposeIDs.length ? (SystemPurposes[systemPurposeId] ?? null) : null;
|
||||
const selectedExample = selectedPurpose?.examples && getRandomElement(selectedPurpose.examples) || null;
|
||||
const handleSearchOnChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const query = e.target.value;
|
||||
if (!query)
|
||||
return handleSearchClear();
|
||||
|
||||
return <>
|
||||
// Filter results based on search term (title and description)
|
||||
const lcQuery = query.toLowerCase();
|
||||
const ids = (Object.keys(SystemPurposes) as SystemPurposeId[])
|
||||
.filter(key => SystemPurposes.hasOwnProperty(key))
|
||||
.filter(key => {
|
||||
const purpose = SystemPurposes[key as SystemPurposeId];
|
||||
return purpose.title.toLowerCase().includes(lcQuery)
|
||||
|| (typeof purpose.description === 'string' && purpose.description.toLowerCase().includes(lcQuery));
|
||||
});
|
||||
|
||||
{showFinder && <Box sx={{ p: 2 * tileSpacing }}>
|
||||
<Input
|
||||
fullWidth
|
||||
variant='outlined' color='neutral'
|
||||
value={searchQuery} onChange={handleSearchOnChange}
|
||||
onKeyDown={handleSearchOnKeyDown}
|
||||
placeholder='Search for purpose…'
|
||||
startDecorator={<SearchIcon />}
|
||||
endDecorator={searchQuery && (
|
||||
<IconButton onClick={handleSearchClear}>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
sx={{
|
||||
boxShadow: 'sm',
|
||||
}}
|
||||
/>
|
||||
</Box>}
|
||||
setSearchQuery(query);
|
||||
setFilteredIDs(ids);
|
||||
|
||||
<Stack direction='column' sx={{ minHeight: '60vh', justifyContent: 'center', alignItems: 'center' }}>
|
||||
// If there's a search term, activate the first item
|
||||
// if (ids.length && systemPurposeId && !ids.includes(systemPurposeId))
|
||||
// handlePurposeChanged(ids[0] as SystemPurposeId);
|
||||
}, [handleSearchClear]);
|
||||
|
||||
<Box sx={{ maxWidth: bpMaxWidth }}>
|
||||
const handleSearchOnKeyDown = React.useCallback((e: React.KeyboardEvent<HTMLInputElement>): void => {
|
||||
if (e.key == 'Escape')
|
||||
handleSearchClear();
|
||||
}, [handleSearchClear]);
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 2, mb: 1 }}>
|
||||
|
||||
// safety check - shouldn't happen - this is set to null when the conversation is not found
|
||||
if (!setSystemPurposeId)
|
||||
return null;
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
maxWidth: 'md',
|
||||
minWidth: `${2 + 1 + tileSize * 2}rem`, // accomodate at least 2 columns (scroll-x in case)
|
||||
mx: 'auto',
|
||||
minHeight: '60svh',
|
||||
display: 'grid',
|
||||
px: { xs: 0.5, sm: 1, md: 2 },
|
||||
py: 2,
|
||||
}}>
|
||||
|
||||
{showFinder && <Box>
|
||||
<Input
|
||||
fullWidth
|
||||
variant='outlined' color='neutral'
|
||||
value={searchQuery} onChange={handleSearchOnChange}
|
||||
onKeyDown={handleSearchOnKeyDown}
|
||||
placeholder='Search for purpose…'
|
||||
startDecorator={<SearchIcon />}
|
||||
endDecorator={searchQuery && (
|
||||
<IconButton onClick={handleSearchClear}>
|
||||
<ClearIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
sx={{
|
||||
boxShadow: 'sm',
|
||||
}}
|
||||
/>
|
||||
</Box>}
|
||||
|
||||
|
||||
<Box sx={{
|
||||
my: 'auto',
|
||||
// layout
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${tileSize}rem, ${tileSize}rem))`,
|
||||
justifyContent: 'center', gap: `${tileGap}rem`,
|
||||
}}>
|
||||
|
||||
{/* [row 0] ... Edit mode [ ] */}
|
||||
<Box sx={{
|
||||
gridColumn: '1 / -1',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
}}>
|
||||
<Typography level='title-sm'>
|
||||
AI Persona
|
||||
</Typography>
|
||||
<Tooltip disableInteractive title={editMode ? 'Done Editing' : 'Edit Tiles'}>
|
||||
<IconButton size='sm' onClick={toggleEditMode}>
|
||||
<IconButton size='sm' onClick={toggleEditMode} sx={{ my: '-0.25rem' /* absorb the button padding */ }}>
|
||||
{editMode ? <DoneIcon /> : <EditIcon />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
<Grid container spacing={tileSpacing} sx={{ justifyContent: 'flex-start' }}>
|
||||
{purposeIDs.map((spId) => (
|
||||
<Grid key={spId}>
|
||||
<Button
|
||||
variant={(!editMode && systemPurposeId === spId) ? 'solid' : 'soft'}
|
||||
color={(!editMode && systemPurposeId === spId) ? 'primary' : SystemPurposes[spId as SystemPurposeId]?.highlighted ? 'warning' : 'neutral'}
|
||||
onClick={() => editMode
|
||||
? toggleHiddenPurposeId(spId)
|
||||
: handlePurposeChanged(spId as SystemPurposeId)
|
||||
}
|
||||
{/* Personas Tiles */}
|
||||
{visiblePurposeIDs.map((spId: SystemPurposeId) => {
|
||||
const isActive = systemPurposeId === spId;
|
||||
const systemPurpose = SystemPurposes[spId];
|
||||
return (
|
||||
<Tile
|
||||
key={'tile-' + spId}
|
||||
text={systemPurpose?.title}
|
||||
imageUrl={systemPurpose?.imageUri}
|
||||
symbol={systemPurpose?.symbol}
|
||||
isActive={isActive}
|
||||
isEditMode={editMode}
|
||||
isHidden={hiddenPurposeIDs.includes(spId)}
|
||||
isHighlighted={systemPurpose?.highlighted}
|
||||
onClick={() => editMode ? toggleHiddenPurposeId(spId) : handlePurposeChanged(spId)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Persona Creator Tile */}
|
||||
{(editMode || !hidePersonaCreator) && (
|
||||
<Tile
|
||||
text='Persona Creator'
|
||||
symbol='🎭'
|
||||
isActive={false}
|
||||
isEditMode={editMode}
|
||||
isHidden={hidePersonaCreator}
|
||||
onClick={() => editMode ? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR) : void navigateToPersonas()}
|
||||
sx={{
|
||||
boxShadow: 'xs',
|
||||
backgroundColor: 'neutral.softDisabledBg',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* [row -3] Description */}
|
||||
<Box sx={{ gridColumn: '1 / -1', mt: 3, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
|
||||
|
||||
{/* Description*/}
|
||||
<Typography level='body-sm' sx={{ color: 'text.primary' }}>
|
||||
{!selectedPurpose
|
||||
? 'Cannot find the former persona' + (systemPurposeId ? ` "${systemPurposeId}"` : '')
|
||||
: selectedPurpose?.description || 'No description available'}
|
||||
</Typography>
|
||||
{/* Examples Toggle */}
|
||||
{fourExamples && showExamplescomponent}
|
||||
{showPromptComponent}
|
||||
</Box>
|
||||
|
||||
{/* [row -3] Example incipits */}
|
||||
{systemPurposeId !== 'Custom' && (
|
||||
<ExpanderControlledBox expanded={showExamples || showPrompt} sx={{ gridColumn: '1 / -1', pt: 1 }}>
|
||||
{showExamples && (
|
||||
<List
|
||||
aria-label='Persona Conversation Starters'
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
fontWeight: 500,
|
||||
// paddingInline: 1,
|
||||
gap: bpTileGap,
|
||||
height: bpTileSize,
|
||||
width: bpTileSize,
|
||||
...((editMode || systemPurposeId !== spId) ? {
|
||||
boxShadow: 'md',
|
||||
...(SystemPurposes[spId as SystemPurposeId]?.highlighted ? {} : { backgroundColor: 'background.surface' }),
|
||||
} : {}),
|
||||
// example items 2-col layout
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(auto-fit, minmax(${tileSize * 2 + 1}rem, 1fr))`,
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
{editMode && (
|
||||
<Checkbox
|
||||
color='neutral'
|
||||
checked={!hiddenPurposeIDs.includes(spId)}
|
||||
// label={<Typography level='body-xs'>show</Typography>}
|
||||
sx={{ position: 'absolute', left: 8, top: 8 }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ fontSize: '2rem' }}>
|
||||
{SystemPurposes[spId as SystemPurposeId]?.symbol}
|
||||
</div>
|
||||
<div>
|
||||
{SystemPurposes[spId as SystemPurposeId]?.title}
|
||||
</div>
|
||||
</Button>
|
||||
</Grid>
|
||||
))}
|
||||
{/* Button to start the Persona Creator */}
|
||||
{(editMode || !hidePersonaCreator) && <Grid>
|
||||
<Button
|
||||
variant='soft' color='neutral'
|
||||
onClick={() => editMode
|
||||
? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR)
|
||||
: void navigateToPersonas()
|
||||
}
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
fontWeight: 500,
|
||||
// paddingInline: 1,
|
||||
gap: bpTileGap,
|
||||
height: bpTileSize,
|
||||
width: bpTileSize,
|
||||
// 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>
|
||||
<div style={{ fontSize: '2rem' }}>
|
||||
🎭
|
||||
</div>
|
||||
{/*<SettingsAccessibilityIcon style={{ opacity: 0.5 }} />*/}
|
||||
</div>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Persona Creator
|
||||
</div>
|
||||
</Button>
|
||||
</Grid>}
|
||||
</Grid>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
mt: selectedExample ? 1 : 3,
|
||||
display: 'flex', alignItems: 'center', gap: 1,
|
||||
// justifyContent: 'center',
|
||||
'&:hover > button': { opacity: 1 },
|
||||
}}>
|
||||
{!selectedPurpose
|
||||
? 'Oops! No AI persona found for your search.'
|
||||
: (selectedExample
|
||||
? <>
|
||||
Example: {selectedExample}
|
||||
<IconButton
|
||||
color='primary'
|
||||
onClick={() => props.runExample(selectedExample)}
|
||||
sx={{ opacity: 0, transition: 'opacity 0.3s' }}
|
||||
{fourExamples?.map((example, idx) => (
|
||||
<ListItem
|
||||
key={idx}
|
||||
variant='soft'
|
||||
sx={{
|
||||
borderRadius: 'md',
|
||||
// boxShadow: 'xs',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: 'background.surface',
|
||||
'& svg': { opacity: 0.1, transition: 'opacity 0.2s' },
|
||||
'&:hover svg': { opacity: 1 },
|
||||
}}
|
||||
>
|
||||
<TelegramIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
: selectedPurpose.description
|
||||
<ListItemButton onClick={() => props.runExample(example)} sx={{ justifyContent: 'space-between' }}>
|
||||
<Typography level='body-sm'>
|
||||
{example}
|
||||
</Typography>
|
||||
<TelegramIcon color='primary' sx={{}} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</Typography>
|
||||
{showPrompt && (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography level='title-sm'>
|
||||
System Prompt
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ whiteSpace: 'break-spaces' }}>
|
||||
{bareBonesPromptMixer(selectedPurpose?.systemMessage || 'No system message available', chatLLM?.id)}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</ExpanderControlledBox>
|
||||
)}
|
||||
|
||||
{/* [row -1] Custom Prompt box */}
|
||||
{systemPurposeId === 'Custom' && (
|
||||
<Textarea
|
||||
variant='outlined' autoFocus placeholder={'Craft your custom system message here…'}
|
||||
autoFocus
|
||||
variant='outlined'
|
||||
placeholder='Craft your custom system message here…'
|
||||
minRows={3}
|
||||
defaultValue={SystemPurposes['Custom']?.systemMessage} onChange={handleCustomSystemMessageChange}
|
||||
defaultValue={SystemPurposes['Custom']?.systemMessage}
|
||||
onChange={handleCustomSystemMessageChange}
|
||||
endDecorator={
|
||||
<Typography level='body-sm' sx={{ px: 0.75 }}>
|
||||
Just start chatting when done.
|
||||
</Typography>
|
||||
}
|
||||
sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
gridColumn: '1 / -1',
|
||||
backgroundColor: 'background.surface',
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
lineHeight: lineHeightTextarea,
|
||||
mt: 1,
|
||||
}} />
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
</Stack>
|
||||
|
||||
</>;
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, IconButton, ListItemButton, ListItemDecorator } from '@mui/joy';
|
||||
import BuildCircleIcon from '@mui/icons-material/BuildCircle';
|
||||
import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { findVendorById } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import { DropdownItems, PageBarDropdownMemo } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
|
||||
function LLMDropdown(props: {
|
||||
llms: DLLM[],
|
||||
chatLlmId: DLLMId | null,
|
||||
setChatLlmId: (llmId: DLLMId | null) => void,
|
||||
placeholder?: string,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { openLlmOptions, openModelsSetup } = useOptimaLayout();
|
||||
|
||||
// derived state
|
||||
const { chatLlmId, llms, setChatLlmId } = props;
|
||||
|
||||
|
||||
const handleChatLLMChange = React.useCallback((value: DLLMId | null) => {
|
||||
value && setChatLlmId(value);
|
||||
}, [setChatLlmId]);
|
||||
|
||||
const handleOpenLLMOptions = React.useCallback(() => {
|
||||
return chatLlmId && openLlmOptions(chatLlmId);
|
||||
}, [chatLlmId, openLlmOptions]);
|
||||
|
||||
|
||||
const llmDropdownItems: DropdownItems = React.useMemo(() => {
|
||||
const llmItems: DropdownItems = {};
|
||||
let prevSourceId: DModelSourceId | null = null;
|
||||
let sepCount = 0;
|
||||
for (const llm of llms) {
|
||||
|
||||
// filter-out hidden models from the dropdown
|
||||
if (!(!llm.hidden || llm.id === chatLlmId))
|
||||
continue;
|
||||
|
||||
// add separators when changing sources
|
||||
if (!prevSourceId || llm.sId !== prevSourceId) {
|
||||
const llmVendor = findVendorById(llm._source?.vId ?? undefined);
|
||||
const sourceName = llmVendor?.name || llm.sId;
|
||||
llmItems[`sep-${llm.id}`] = {
|
||||
type: 'separator',
|
||||
title: sourceName,
|
||||
icon: llmVendor?.Icon ? <llmVendor.Icon /> : undefined,
|
||||
};
|
||||
prevSourceId = llm.sId;
|
||||
sepCount++;
|
||||
}
|
||||
|
||||
// add the model item
|
||||
llmItems[llm.id] = {
|
||||
title: llm.label,
|
||||
// icon: llm.id.startsWith('some vendor') ? <VendorIcon /> : undefined,
|
||||
};
|
||||
}
|
||||
// if there's a single separator (i.e. only one source), remove it
|
||||
if (sepCount === 1) {
|
||||
for (const key in llmItems) {
|
||||
if (key.startsWith('sep-')) {
|
||||
delete llmItems[key];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return llmItems;
|
||||
}, [chatLlmId, llms]);
|
||||
|
||||
|
||||
// "Model Options" button (only on the active item)
|
||||
const llmDropdownButton = React.useMemo(() => (
|
||||
<GoodTooltip title={
|
||||
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
|
||||
Model Options
|
||||
<KeyStroke combo='Ctrl + Shift + O' sx={{ my: 0.5 }} />
|
||||
</Box>
|
||||
}>
|
||||
<IconButton
|
||||
variant='outlined' color='neutral'
|
||||
onClick={handleOpenLLMOptions}
|
||||
sx={{
|
||||
ml: 'auto',
|
||||
// mr: -0.5,
|
||||
my: '-0.25rem' /* absorb the menuItem padding */,
|
||||
backgroundColor: 'background.surface',
|
||||
boxShadow: 'xs',
|
||||
}}
|
||||
>
|
||||
<SettingsIcon sx={{ fontSize: 'xl' }} />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
), [handleOpenLLMOptions]);
|
||||
|
||||
|
||||
// "Models Setup" button
|
||||
const llmDropdownAppendOptions = React.useMemo(() => <>
|
||||
|
||||
{/*{chatLlmId && (*/}
|
||||
{/* <ListItemButton key='menu-opt' onClick={handleOpenLLMOptions}>*/}
|
||||
{/* <ListItemDecorator><SettingsIcon color='success' /></ListItemDecorator>*/}
|
||||
{/* <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>*/}
|
||||
{/* Options*/}
|
||||
{/* <KeyStroke combo='Ctrl + Shift + O' />*/}
|
||||
{/* </Box>*/}
|
||||
{/* </ListItemButton>*/}
|
||||
{/*)}*/}
|
||||
|
||||
<ListItemButton key='menu-llms' onClick={openModelsSetup}>
|
||||
<ListItemDecorator><BuildCircleIcon color='success' /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Models
|
||||
<KeyStroke combo='Ctrl + Shift + M' sx={{ ml: 2 }} />
|
||||
</Box>
|
||||
</ListItemButton>
|
||||
|
||||
</>, [openModelsSetup]);
|
||||
|
||||
|
||||
return (
|
||||
<PageBarDropdownMemo
|
||||
items={llmDropdownItems}
|
||||
value={chatLlmId}
|
||||
onChange={handleChatLLMChange}
|
||||
placeholder={props.placeholder || 'Models …'}
|
||||
appendOption={llmDropdownAppendOptions}
|
||||
activeEndDecorator={llmDropdownButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function useChatLLMDropdown() {
|
||||
// external state
|
||||
const { llms, chatLLMId, setChatLLMId } = useModelsStore(state => ({
|
||||
llms: state.llms, // NOTE: we don't need a deep comparison as we reference the same array
|
||||
chatLLMId: state.chatLLMId,
|
||||
setChatLLMId: state.setChatLLMId,
|
||||
}), shallow);
|
||||
|
||||
const chatLLMDropdown = React.useMemo(
|
||||
() => <LLMDropdown llms={llms} chatLlmId={chatLLMId} setChatLlmId={setChatLLMId} />,
|
||||
[llms, chatLLMId, setChatLLMId],
|
||||
);
|
||||
|
||||
return { chatLLMId, chatLLMDropdown };
|
||||
}
|
||||
|
||||
/*export function useTempLLMDropdown(props: { initialLlmId: DLLMId | null }) {
|
||||
// local state
|
||||
const [llmId, setLlmId] = React.useState<DLLMId | null>(props.initialLlmId);
|
||||
|
||||
// external state
|
||||
const llms = useModelsStore(state => state.llms, shallow);
|
||||
|
||||
const chatLLMDropdown = React.useMemo(
|
||||
() => <LLMDropdown llms={llms} llmId={llmId} setLlmId={setLlmId} />,
|
||||
[llms, llmId, setLlmId],
|
||||
);
|
||||
|
||||
return { llmId, chatLLMDropdown };
|
||||
}*/
|
||||
@@ -0,0 +1,69 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../data';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { PageBarDropdownMemo } from '~/common/layout/optima/components/PageBarDropdown';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
function PersonaDropdown(props: {
|
||||
systemPurposeId: SystemPurposeId | null,
|
||||
setSystemPurposeId: (systemPurposeId: SystemPurposeId | null) => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { zenMode } = useUIPreferencesStore(state => ({
|
||||
zenMode: state.zenMode,
|
||||
}), shallow);
|
||||
|
||||
|
||||
const { setSystemPurposeId } = props;
|
||||
|
||||
const handleSystemPurposeChange = React.useCallback((value: string | null) => {
|
||||
setSystemPurposeId(value as (SystemPurposeId | null));
|
||||
}, [setSystemPurposeId]);
|
||||
|
||||
|
||||
return (
|
||||
<PageBarDropdownMemo
|
||||
items={SystemPurposes}
|
||||
value={props.systemPurposeId}
|
||||
onChange={handleSystemPurposeChange}
|
||||
showSymbols={zenMode !== 'cleaner'}
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
export function usePersonaIdDropdown(conversationId: DConversationId | null) {
|
||||
|
||||
// external state
|
||||
const { systemPurposeId } = useChatStore(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === conversationId);
|
||||
return {
|
||||
systemPurposeId: conversation?.systemPurposeId ?? null,
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
|
||||
const handleSetSystemPurposeId = React.useCallback((systemPurposeId: SystemPurposeId | null) => {
|
||||
if (conversationId && systemPurposeId)
|
||||
useChatStore.getState().setSystemPurposeId(conversationId, systemPurposeId);
|
||||
}, [conversationId]);
|
||||
|
||||
const personaDropdown = React.useMemo(() => {
|
||||
if (!systemPurposeId) return null;
|
||||
return (
|
||||
<PersonaDropdown
|
||||
systemPurposeId={systemPurposeId}
|
||||
setSystemPurposeId={handleSetSystemPurposeId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[handleSetSystemPurposeId, systemPurposeId],
|
||||
);
|
||||
|
||||
return { personaDropdown };
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { DLLMId } from '~/modules/llms/store-llms';
|
||||
import { DLLMId, getKnowledgeMapCutoff } from '~/modules/llms/store-llms';
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../data';
|
||||
|
||||
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
|
||||
import { createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
|
||||
@@ -19,9 +21,7 @@ export function updatePurposeInHistory(conversationId: string, history: DMessage
|
||||
const systemMessage: DMessage = systemMessageIndex >= 0 ? history.splice(systemMessageIndex, 1)[0] : createDMessage('system', '');
|
||||
if (!systemMessage.updated && purposeId && SystemPurposes[purposeId]?.systemMessage) {
|
||||
systemMessage.purposeId = purposeId;
|
||||
systemMessage.text = SystemPurposes[purposeId].systemMessage
|
||||
.replaceAll('{{Cutoff}}', assistantLlmId.includes('1106') ? '2023-04' : '2021-09')
|
||||
.replaceAll('{{Today}}', new Date().toISOString().split('T')[0]);
|
||||
systemMessage.text = bareBonesPromptMixer(SystemPurposes[purposeId].systemMessage, assistantLlmId);
|
||||
|
||||
// HACK: this is a special case for the "Custom" persona, to set the message in stone (so it doesn't get updated when switching to another persona)
|
||||
if (purposeId === 'Custom')
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
export function Gallery() {
|
||||
return (
|
||||
|
||||
<AppPlaceholder text='Drawing App is under development. v1.12 or v1.13.' />
|
||||
<AppPlaceholder text='Drawing App is under development. v1.13 or v1.14.' />
|
||||
|
||||
);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import * as React from 'react';
|
||||
import { Box } from '@mui/joy';
|
||||
|
||||
import type { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
import { themeBgAppChatComposer } from '~/common/app.theme';
|
||||
|
||||
import { DesignerPrompt, PromptDesigner } from './components/PromptDesigner';
|
||||
import { ProviderConfigure } from './components/ProviderConfigure';
|
||||
@@ -70,6 +71,7 @@ export function TextToImage(props: {
|
||||
onDrawingStop={handleStopDrawing}
|
||||
onPromptEnqueue={handlePromptEnqueue}
|
||||
sx={{
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
borderTop: `1px solid`,
|
||||
borderTopColor: 'divider',
|
||||
p: { xs: 1, md: 2 },
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button, ButtonGroup, IconButton, Tooltip } from '@mui/joy';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import LightbulbOutlinedIcon from '@mui/icons-material/LightbulbOutlined';
|
||||
|
||||
// const desktopButtonLegend =
|
||||
// <Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
|
||||
// <b>From Idea</b><br />
|
||||
// From Idea
|
||||
// </Box>;
|
||||
|
||||
|
||||
export function ButtonPromptFromIdea(props: {
|
||||
isMobile?: boolean,
|
||||
disabled: boolean,
|
||||
onIdeaNext: () => void,
|
||||
onIdeaUse: () => void,
|
||||
}) {
|
||||
|
||||
const { onIdeaNext, onIdeaUse } = props;
|
||||
|
||||
const handleIdeaNext = React.useCallback((event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onIdeaNext();
|
||||
}, [onIdeaNext]);
|
||||
|
||||
return props.isMobile ? null : (
|
||||
<ButtonGroup
|
||||
variant='soft' color='neutral'
|
||||
disabled={props.disabled}
|
||||
sx={{
|
||||
// '--ButtonGroup-separatorSize': 0,
|
||||
minWidth: 160,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
fullWidth onClick={handleIdeaNext}
|
||||
startDecorator={<LightbulbOutlinedIcon />}
|
||||
sx={{
|
||||
// '--Button-gap': 'auto',
|
||||
// minWidth: 100,
|
||||
justifyContent: 'flex-start',
|
||||
transition: 'background-color 0.2s, color 0.2s',
|
||||
}}>
|
||||
Idea
|
||||
</Button>
|
||||
<Tooltip disableInteractive title='Use Idea'>
|
||||
<IconButton size='sm' onClick={onIdeaUse}>
|
||||
<ArrowForwardIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Button } from '@mui/joy';
|
||||
import InsertPhotoOutlinedIcon from '@mui/icons-material/InsertPhotoOutlined';
|
||||
import ChatOutlinedIcon from '@mui/icons-material/ChatOutlined';
|
||||
|
||||
export function ButtonPromptFromPlaceholder(props: { isMobile?: boolean, name: string, disabled?: boolean }) {
|
||||
return props.isMobile ? null : (
|
||||
<Button
|
||||
disabled={props.disabled}
|
||||
fullWidth variant='soft' color='neutral'
|
||||
startDecorator={props.name === 'Chats' ? <ChatOutlinedIcon /> : <InsertPhotoOutlinedIcon />}
|
||||
sx={{
|
||||
justifyContent: 'flex-start',
|
||||
transition: 'background-color 0.2s, color 0.2s',
|
||||
minWidth: 160,
|
||||
}}>
|
||||
{props.name}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,24 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, ButtonGroup, Grid, IconButton, Textarea, Tooltip } from '@mui/joy';
|
||||
import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward';
|
||||
import { Box, Button, ButtonGroup, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import KeyboardArrowLeftIcon from '@mui/icons-material/KeyboardArrowLeft';
|
||||
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
|
||||
import MoreTimeIcon from '@mui/icons-material/MoreTime';
|
||||
import RemoveIcon from '@mui/icons-material/Remove';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
|
||||
import { animationStopEnter } from '../../chat/components/composer/Composer';
|
||||
|
||||
import { lineHeightTextarea } from '~/common/app.theme';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { animationStopEnter } from '../../chat/components/composer/Composer';
|
||||
import { ButtonPromptFromIdea } from './ButtonPromptFromIdea';
|
||||
import { ButtonPromptFromPlaceholder } from './ButtonPromptFromPlaceholder';
|
||||
import { useDrawIdeas } from '../state/useDrawIdeas';
|
||||
|
||||
|
||||
@@ -39,6 +46,7 @@ export function PromptDesigner(props: {
|
||||
|
||||
// state
|
||||
const [nextPrompt, setNextPrompt] = React.useState<string>('');
|
||||
const [tempCount, setTempCount] = React.useState<number>(2);
|
||||
|
||||
// external state
|
||||
const { currentIdea, nextRandomIdea } = useDrawIdeas();
|
||||
@@ -89,16 +97,16 @@ export function PromptDesigner(props: {
|
||||
}, [enterIsNewline, handlePromptEnqueue, userHasText]);
|
||||
|
||||
|
||||
// Ideas
|
||||
|
||||
const handleIdeaUse = React.useCallback(() => {
|
||||
setNextPrompt(currentIdea.prompt);
|
||||
}, [currentIdea.prompt]);
|
||||
|
||||
// PromptFx
|
||||
|
||||
const textEnrichComponents = React.useMemo(() => {
|
||||
|
||||
const handleIdeaUse = (event: React.MouseEvent) => {
|
||||
event.preventDefault();
|
||||
setNextPrompt(currentIdea.prompt);
|
||||
// setUserHasChanged(false);
|
||||
};
|
||||
|
||||
const handleClickMissing = (_event: React.MouseEvent) => {
|
||||
alert('Not implemented yet');
|
||||
};
|
||||
@@ -121,16 +129,18 @@ export function PromptDesigner(props: {
|
||||
}}>
|
||||
|
||||
{/* Change / Use idea */}
|
||||
<ButtonGroup variant='soft' color='neutral' sx={{ borderRadius: 'sm' }}>
|
||||
<Button className={promptButtonClass} disabled={userHasText} onClick={nextRandomIdea}>
|
||||
Idea
|
||||
</Button>
|
||||
<Tooltip disableInteractive title='Use Idea'>
|
||||
<IconButton onClick={handleIdeaUse}>
|
||||
<ArrowDownwardIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
{/*{props.isMobile && (*/}
|
||||
{/* <ButtonGroup variant='soft' color='neutral' sx={{ borderRadius: 'sm' }}>*/}
|
||||
{/* <Button className={promptButtonClass} disabled={userHasText} onClick={handleIdeaNext}>*/}
|
||||
{/* Idea*/}
|
||||
{/* </Button>*/}
|
||||
{/* <Tooltip disableInteractive title='Use Idea'>*/}
|
||||
{/* <IconButton onClick={handleIdeaUse}>*/}
|
||||
{/* <ArrowDownwardIcon />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/* </Tooltip>*/}
|
||||
{/* </ButtonGroup>*/}
|
||||
{/*)}*/}
|
||||
|
||||
{/* PromptFx */}
|
||||
<Button
|
||||
@@ -141,19 +151,49 @@ export function PromptDesigner(props: {
|
||||
onClick={handleClickMissing}
|
||||
sx={{ borderRadius: 'sm' }}
|
||||
>
|
||||
Detail
|
||||
Enhance
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant='soft' color='success'
|
||||
disabled={!userHasText}
|
||||
className={promptButtonClass}
|
||||
endDecorator={<AutoFixHighIcon sx={{ fontSize: '20px' }} />}
|
||||
onClick={handleClickMissing}
|
||||
sx={{ borderRadius: 'sm' }}
|
||||
>
|
||||
Restyle
|
||||
</Button>
|
||||
{/*<Button*/}
|
||||
{/* variant='soft' color='success'*/}
|
||||
{/* disabled={!userHasText}*/}
|
||||
{/* className={promptButtonClass}*/}
|
||||
{/* endDecorator={<AutoFixHighIcon sx={{ fontSize: '20px' }} />}*/}
|
||||
{/* onClick={handleClickMissing}*/}
|
||||
{/* sx={{ borderRadius: 'sm' }}*/}
|
||||
{/*>*/}
|
||||
{/* Restyle*/}
|
||||
{/*</Button>*/}
|
||||
|
||||
<ButtonGroup sx={{ ml: 'auto' }}>
|
||||
{tempCount > 1 && <IconButton onClick={() => setTempCount(count => count - 1)}>
|
||||
<RemoveIcon />
|
||||
</IconButton>}
|
||||
{tempCount > 1 && <>
|
||||
<IconButton>
|
||||
<KeyboardArrowLeftIcon />
|
||||
</IconButton>
|
||||
<Button
|
||||
sx={{
|
||||
px: 0,
|
||||
minWidth: '3rem',
|
||||
pointerEvents: 'none',
|
||||
fontSize: 'xs',
|
||||
fontWeight: 600,
|
||||
}}>
|
||||
<Typography level='body-xs' color='danger' sx={{ fontWeight: 'lg' }}>
|
||||
{tempCount > 1 ? `1 / ${tempCount}` : '1'}
|
||||
</Typography>
|
||||
</Button>
|
||||
<IconButton>
|
||||
<KeyboardArrowRightIcon />
|
||||
</IconButton>
|
||||
</>}
|
||||
<IconButton onClick={() => setTempCount(count => count + 1)}>
|
||||
<AddIcon />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
|
||||
{/* Char counter */}
|
||||
{/*<Typography level='body-sm' sx={{ ml: 'auto', mr: 1 }}>*/}
|
||||
@@ -161,20 +201,58 @@ export function PromptDesigner(props: {
|
||||
{/*</Typography>*/}
|
||||
</Box>
|
||||
);
|
||||
}, [currentIdea.prompt, nextRandomIdea, userHasText]);
|
||||
}, [tempCount, userHasText]);
|
||||
|
||||
return (
|
||||
<Box aria-label='Drawing Prompt' component='section' sx={props.sx}>
|
||||
<Grid container spacing={{ xs: 1, md: 2 }}>
|
||||
|
||||
{/* Prompt (Text) Box */}
|
||||
<Grid xs={12} md={9}>
|
||||
<Grid xs={12} md={9}><Box sx={{ display: 'flex', gap: { xs: 1, md: 2 } }}>
|
||||
|
||||
{props.isMobile ? (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
<Dropdown>
|
||||
<MenuButton slots={{ root: IconButton }}>
|
||||
<ArrowForwardIcon />
|
||||
</MenuButton>
|
||||
<Menu placement='top'>
|
||||
{/* Add From History? */}
|
||||
{/*<MenuItem>*/}
|
||||
{/* <ButtonPromptFromPlaceholder name='History' disabled />*/}
|
||||
{/*</MenuItem>*/}
|
||||
<MenuItem>
|
||||
<ButtonPromptFromIdea disabled={userHasText} onIdeaNext={nextRandomIdea} onIdeaUse={handleIdeaUse} />
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ButtonPromptFromPlaceholder name='Image' disabled />
|
||||
</MenuItem>
|
||||
{/*<MenuItem>*/}
|
||||
{/* <ButtonPromptFromPlaceholder name='Chat' disabled />*/}
|
||||
{/*</MenuItem>*/}
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
<ButtonPromptFromIdea disabled={userHasText} onIdeaNext={nextRandomIdea} onIdeaUse={handleIdeaUse} />
|
||||
|
||||
<ButtonPromptFromPlaceholder name='Image' disabled />
|
||||
|
||||
{/*<ButtonPromptFromPlaceholder name='Chats' disabled />*/}
|
||||
|
||||
</Box>
|
||||
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
variant='outlined'
|
||||
// size='sm'
|
||||
autoFocus
|
||||
minRows={props.isMobile ? 4 : 3}
|
||||
minRows={props.isMobile ? 5 : 3}
|
||||
maxRows={props.isMobile ? 6 : 8}
|
||||
placeholder={currentIdea.prompt}
|
||||
value={nextPrompt}
|
||||
@@ -188,12 +266,14 @@ export function PromptDesigner(props: {
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
boxShadow: 'lg',
|
||||
'&:focus-within': { backgroundColor: 'background.popup' },
|
||||
lineHeight: lineHeightTextarea,
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
</Box></Grid>
|
||||
|
||||
{/* [Desktop: Right, Mobile: Bottom] Buttons */}
|
||||
<Grid xs={12} md={3} spacing={1}>
|
||||
@@ -212,7 +292,7 @@ export function PromptDesigner(props: {
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
Draw
|
||||
Draw {tempCount > 1 ? `(${tempCount})` : ''}
|
||||
</Button>
|
||||
) : <>
|
||||
<Button
|
||||
@@ -243,6 +323,22 @@ export function PromptDesigner(props: {
|
||||
Enqueue
|
||||
</Button>
|
||||
</>}
|
||||
|
||||
<ButtonGroup size='sm' variant='soft' sx={{ flex: 1, display: 'flex' }}>
|
||||
<Button sx={{ flex: 1 }}>
|
||||
1
|
||||
</Button>
|
||||
<Button sx={{ flex: 1 }}>
|
||||
x2
|
||||
</Button>
|
||||
<Button color='primary' sx={{ flex: 1 }}>
|
||||
x4
|
||||
</Button>
|
||||
<Button sx={{ flex: 1 }}>
|
||||
xN
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
</Box>
|
||||
</Grid>
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { FormControl, FormLabel, ListItemDecorator, Option, Select } from '@mui/
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
|
||||
import type { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
|
||||
import { OpenAIIcon } from '~/common/components/icons/vendors/OpenAIIcon';
|
||||
import { hideOnMobile } from '~/common/app.theme';
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import TimeAgo from 'react-timeago';
|
||||
import { Box, Button, Card, CardContent, List, ListItem, Tooltip, Typography } from '@mui/joy';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { ChatMessage } from '../chat/components/message/ChatMessage';
|
||||
import { ChatMessageMemo } from '../chat/components/message/ChatMessage';
|
||||
import { ScrollToBottom } from '../chat/components/scroll-to-bottom/ScrollToBottom';
|
||||
import { useChatShowSystemMessages } from '../chat/store-app-chat';
|
||||
|
||||
@@ -136,10 +136,11 @@ export function LinkChat(props: { conversation: DConversation, storedAt: Date, e
|
||||
</ListItem>
|
||||
|
||||
{filteredMessages.map((message, idx) =>
|
||||
<ChatMessage
|
||||
key={'msg-' + message.id} message={message}
|
||||
showDate={idx === 0 || idx === filteredMessages.length - 1}
|
||||
onMessageEdit={text => message.text = text}
|
||||
<ChatMessageMemo
|
||||
key={'msg-' + message.id}
|
||||
message={message}
|
||||
blocksShowDate={idx === 0 || idx === filteredMessages.length - 1 /* first and last message */}
|
||||
onMessageEdit={(_messageId, text: string) => message.text = text}
|
||||
/>,
|
||||
)}
|
||||
|
||||
|
||||
+84
-53
@@ -1,18 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import NextImage from 'next/image';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { Box, Button, Card, CardContent, Container, IconButton, Typography } from '@mui/joy';
|
||||
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, IconButton, Typography } from '@mui/joy';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
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 } from '~/common/app.theme';
|
||||
|
||||
import { newsCallout, NewsItems } from './news.data';
|
||||
import { NewsItems, newsRoadmapCallout } from './news.data';
|
||||
|
||||
// number of news items to show by default, before the expander
|
||||
const DEFAULT_NEWS_COUNT = 3;
|
||||
@@ -52,85 +52,116 @@ export function AppNews() {
|
||||
<Box sx={{
|
||||
my: 'auto',
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center',
|
||||
gap: 4,
|
||||
}}>
|
||||
|
||||
<Typography level='h1' sx={{ fontSize: '3rem' }}>
|
||||
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${cssColorKeyframes} 10s infinite` }}>{firstNews?.versionCode}</Box>!
|
||||
<Typography level='h1' sx={{ fontSize: '2.9rem', mb: 4 }}>
|
||||
Welcome to {Brand.Title.Base} <Box component='span' sx={{ animation: `${cssColorKeyframes} 10s infinite`, zIndex: 1 }}>{firstNews?.versionCode}</Box>!
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
<Typography sx={{ mb: 2 }} level='title-sm'>
|
||||
{capitalizeFirstLetter(Brand.Title.Base)} has been updated to version {firstNews?.versionCode}
|
||||
</Typography>
|
||||
|
||||
<Box>
|
||||
<Box sx={{ mb: 5 }}>
|
||||
<Button
|
||||
variant='solid' color='neutral' size='lg'
|
||||
variant='solid' color='primary' size='lg'
|
||||
component={Link} href={ROUTE_INDEX} noLinkStyle
|
||||
endDecorator='✨'
|
||||
sx={{ minWidth: 200 }}
|
||||
sx={{
|
||||
boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
|
||||
minWidth: 180,
|
||||
}}
|
||||
>
|
||||
Sweet
|
||||
Continue
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{!!newsCallout && <Container disableGutters maxWidth='sm'>{newsCallout}</Container>}
|
||||
{/*<Typography level='title-sm' sx={{ mb: 1, placeSelf: 'start', ml: 1 }}>*/}
|
||||
{/* Here is what's new:*/}
|
||||
{/*</Typography>*/}
|
||||
|
||||
{!!news && <Container disableGutters maxWidth='sm'>
|
||||
<Container disableGutters maxWidth='sm'>
|
||||
{news?.map((ni, idx) => {
|
||||
// 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: 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} · ` : `Version ${ni.versionCode}:`}
|
||||
<Box component='span' sx={!idx ? {
|
||||
animation: `${cssRainbowColorKeyframes} 5s infinite`,
|
||||
fontWeight: 600,
|
||||
} : {}}>
|
||||
return <React.Fragment key={idx}>
|
||||
|
||||
{/* News Item */}
|
||||
<Card key={'news-' + idx} sx={{ mb: 3, minHeight: 32, gap: 1 }}>
|
||||
<CardContent sx={{ position: 'relative', pr: addPadding ? 4 : 0 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Typography level='title-sm' component='div'>
|
||||
{ni.text ? ni.text : ni.versionName ? <><span style={{ fontWeight: 600 }}>{ni.versionCode}</span> · </> : `Version ${ni.versionCode}:`}
|
||||
<Box
|
||||
component='span'
|
||||
sx={idx ? {} : {
|
||||
animation: `${cssRainbowColorKeyframes} 5s infinite`,
|
||||
fontWeight: 600,
|
||||
zIndex: 1,
|
||||
}}
|
||||
>
|
||||
{ni.versionName}
|
||||
</Box>
|
||||
</Typography>
|
||||
</GoodTooltip>
|
||||
{/*!firstCard &&*/ (
|
||||
<Typography level='body-sm'>
|
||||
<Typography level='body-sm' sx={{ ml: 'auto' }}>
|
||||
{!!ni.versionDate && <TimeAgo date={ni.versionDate} />}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{!!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='solid'
|
||||
onClick={() => setLastNewsIdx(idx + 1)}
|
||||
sx={{
|
||||
position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1,
|
||||
// backgroundColor: 'background.surface',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{!!ni.versionCoverImage && (
|
||||
<CardOverflow sx={{
|
||||
m: '0 calc(var(--CardOverflow-offset) - 1px) calc(var(--CardOverflow-offset) - 1px)',
|
||||
}}>
|
||||
<AspectRatio ratio='2'>
|
||||
<NextImage
|
||||
src={ni.versionCoverImage}
|
||||
alt={`Cover image for ${ni.versionCode}`}
|
||||
// commented: we scale the images to 600px wide (>300 px tall)
|
||||
// sizes='(max-width: 1200px) 100vw, 50vw'
|
||||
priority={idx === 0}
|
||||
/>
|
||||
</AspectRatio>
|
||||
</CardOverflow>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Inject the roadmap item here*/}
|
||||
{idx === 0 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{newsRoadmapCallout}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!!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='outlined'
|
||||
onClick={() => setLastNewsIdx(idx + 1)}
|
||||
sx={{
|
||||
position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1,
|
||||
backgroundColor: 'background.surface',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
</CardContent>
|
||||
</Card>;
|
||||
</React.Fragment>;
|
||||
})}
|
||||
</Container>}
|
||||
</Container>
|
||||
|
||||
{/*<Typography sx={{ textAlign: 'center' }}>*/}
|
||||
{/* Enjoy!*/}
|
||||
|
||||
+93
-57
@@ -1,5 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { StaticImageData } from 'next/image';
|
||||
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, Card, CardContent, Chip, Grid, Typography } from '@mui/joy';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
|
||||
@@ -8,17 +10,38 @@ import { Link } from '~/common/components/Link';
|
||||
import { clientUtmSource } from '~/common/util/pwaUtils';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
|
||||
// Images
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is calling on a 3D origami old-school pink telephone and the camera is zooming on the telephone. Close up photography, bokeh, white background.
|
||||
import coverV113 from '../../../public/images/covers/release-cover-v1.13.0.png';
|
||||
import coverV112 from '../../../public/images/covers/release-cover-v1.12.0.png';
|
||||
|
||||
|
||||
// update this variable every time you want to broadcast a new version to clients
|
||||
export const incrementalVersion: number = 12.1;
|
||||
export const incrementalVersion: number = 13;
|
||||
|
||||
|
||||
const wowStyle: SxProps = {
|
||||
textDecoration: 'underline',
|
||||
textDecorationThickness: '0.4em',
|
||||
textDecorationColor: 'rgba(var(--joy-palette-primary-lightChannel) / 1)',
|
||||
// textDecorationColor: 'rgba(0 255 0 / 0.5)',
|
||||
textDecorationSkipInk: 'none',
|
||||
// textUnderlineOffset: '-0.5em',
|
||||
};
|
||||
|
||||
function B(props: {
|
||||
// one-of
|
||||
href?: string,
|
||||
issue?: number,
|
||||
code?: string,
|
||||
|
||||
wow?: boolean,
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const href = props.issue ? RIssues + '/' + props.issue : props.href;
|
||||
const href =
|
||||
props.issue ? `${Brand.URIs.OpenRepo}/issues/${props.issue}`
|
||||
: props.code ? `${Brand.URIs.OpenRepo}/blob/main/${props.code}`
|
||||
: props.href;
|
||||
const boldText = (
|
||||
<Typography component='span' color={!!href ? 'primary' : 'neutral'} sx={{ fontWeight: 600 }}>
|
||||
{props.children}
|
||||
@@ -27,20 +50,16 @@ function B(props: {
|
||||
if (!href)
|
||||
return boldText;
|
||||
return (
|
||||
<Link href={href + clientUtmSource()} target='_blank' sx={{ /*textDecoration: 'underline'*/ }}>
|
||||
<Link href={href + clientUtmSource()} target='_blank' sx={props.wow ? wowStyle : undefined}>
|
||||
{boldText} <LaunchIcon sx={{ mx: 0.5, fontSize: 16 }} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
const { OpenRepo, OpenProject } = Brand.URIs;
|
||||
const RCode = `${OpenRepo}/blob/main`;
|
||||
const RIssues = `${OpenRepo}/issues`;
|
||||
|
||||
// callout, for special occasions
|
||||
export const newsCallout =
|
||||
<Card>
|
||||
export const newsRoadmapCallout =
|
||||
<Card variant='solid' invertedColors>
|
||||
<CardContent sx={{ gap: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
Open Roadmap
|
||||
@@ -53,7 +72,7 @@ export const newsCallout =
|
||||
<Grid xs={12} sm={7}>
|
||||
<Button
|
||||
fullWidth variant='soft' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={OpenProject} noLinkStyle target='_blank'
|
||||
component={Link} href={Brand.URIs.OpenProject} noLinkStyle target='_blank'
|
||||
>
|
||||
Explore
|
||||
</Button>
|
||||
@@ -61,7 +80,7 @@ export const newsCallout =
|
||||
<Grid xs={12} sm={5} sx={{ display: 'flex', flexAlign: 'center', justifyContent: 'center' }}>
|
||||
<Button
|
||||
fullWidth variant='plain' color='primary' endDecorator={<LaunchIcon />}
|
||||
component={Link} href={RIssues + '/new?template=roadmap-request.md&title=%5BSuggestion%5D'} noLinkStyle target='_blank'
|
||||
component={Link} href={Brand.URIs.OpenRepo + '/issues/new?template=roadmap-request.md&title=%5BSuggestion%5D'} noLinkStyle target='_blank'
|
||||
>
|
||||
Suggest a Feature
|
||||
</Button>
|
||||
@@ -71,18 +90,49 @@ export const newsCallout =
|
||||
</Card>;
|
||||
|
||||
|
||||
interface NewsItem {
|
||||
versionCode: string;
|
||||
versionName?: string;
|
||||
versionMoji?: string;
|
||||
versionDate?: Date;
|
||||
versionCoverImage?: StaticImageData;
|
||||
text?: string | React.JSX.Element;
|
||||
items?: {
|
||||
text: string | React.JSX.Element;
|
||||
dev?: boolean;
|
||||
issue?: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
// news and feature surfaces
|
||||
export const NewsItems: NewsItem[] = [
|
||||
// still unannounced: phone calls, split windows, ...
|
||||
{// 🆕
|
||||
{
|
||||
versionCode: '1.13.0',
|
||||
versionName: 'Multi + Mind',
|
||||
versionMoji: '🧠🔀',
|
||||
versionDate: new Date('2024-02-08T07:47:00Z'),
|
||||
versionCoverImage: coverV113,
|
||||
items: [
|
||||
{ text: <>Side-by-Side <B issue={208}>split windows</B>: multitask with parallel conversations</>, issue: 208 },
|
||||
{ text: <><B issue={388} wow>Multi-Chat</B> mode: message all, all at once</>, issue: 388 },
|
||||
{ text: <>Adjustable <B>text size</B>: denser chats</>, issue: 399 },
|
||||
{ text: <>Export <B issue={392}>tables as CSV</B> files</>, issue: 392 },
|
||||
{ text: <><B>Dev2</B> persona technology preview</> },
|
||||
{ text: <>Better looking chats, spacing, fonts, menus</> },
|
||||
{ text: <>More: video player, LM Studio tutorial, speedups, MongoDB (docs)</> },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.12.0',
|
||||
versionName: 'AGI Hotline',
|
||||
versionMoji: '✨🗣️',
|
||||
versionDate: new Date('2024-01-26T12:30:00Z'),
|
||||
versionCoverImage: coverV112,
|
||||
items: [
|
||||
{ text: <><B issue={354}>Voice Call Personas</B>: save time, recap conversations</>, issue: 354 },
|
||||
{ text: <><B issue={354} wow>Voice Call Personas</B>: save time, recap conversations</>, issue: 354 },
|
||||
{ text: <>Updated <B issue={364}>OpenAI Models</B> to the 0125 release</>, issue: 364 },
|
||||
{ text: <>Chats: Auto-<B issue={222}>Rename</B> and <B issue={360}>assign folders</B></>, issue: 222 },
|
||||
{ text: <>Chats: Auto-<B issue={222} wow>Rename</B> and <B issue={360}>assign folders</B></>, issue: 222 },
|
||||
{ text: <><B issue={356}>Link Sharing</B> makeover and control</>, issue: 356 },
|
||||
{ text: <><B issue={358}>Accessibility</B> for screen readers</>, issue: 358 },
|
||||
{ text: <>Export chats to <B>Markdown</B></>, issue: 337 },
|
||||
@@ -98,12 +148,12 @@ export const NewsItems: NewsItem[] = [
|
||||
versionMoji: '🌌🌠',
|
||||
versionDate: new Date('2024-01-16T06:30:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/329'}>Search</B> past conversations (@joriskalz) 🔍</>, issue: 329 },
|
||||
{ text: <>Quick <B href={RIssues + '/327'}>commands pane</B> (open with '/')</>, issue: 327 },
|
||||
{ text: <><B issue={329} wow>Search chats</B> (@joriskalz)</>, issue: 329 },
|
||||
{ text: <>Quick <B issue={327}>commands pane</B> (open with '/')</>, issue: 327 },
|
||||
{ text: <><B>Together AI</B> Inference platform support</>, issue: 346 },
|
||||
{ text: <>Persona creation: <B href={RIssues + '/301'}>history</B></>, issue: 301 },
|
||||
{ text: <>Persona creation: fix <B href={RIssues + '/328'}>API timeouts</B></>, issue: 328 },
|
||||
{ text: <>Support up to five <B href={RIssues + '/323'}>OpenAI-compatible</B> endpoints</>, issue: 323 },
|
||||
{ text: <>Persona creation: <B issue={301}>history</B></>, issue: 301 },
|
||||
{ text: <>Persona creation: fix <B issue={328}>API timeouts</B></>, issue: 328 },
|
||||
{ text: <>Support up to five <B issue={323}>OpenAI-compatible</B> endpoints</>, issue: 323 },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -112,8 +162,8 @@ export const NewsItems: NewsItem[] = [
|
||||
// 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 issue={201} wow>New UI</B> for desktop and mobile, enabling future expansions</>, issue: 201 },
|
||||
{ text: <><B issue={321} wow>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</> },
|
||||
@@ -126,9 +176,9 @@ export const NewsItems: NewsItem[] = [
|
||||
// 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: <><B issue={212} wow>DALL·E 3</B> support (/draw), with advanced control</>, issue: 212 },
|
||||
{ text: <><B issue={304} wow>Perfect scrolling</B> UX, on all devices</>, issue: 304 },
|
||||
{ text: <>Create personas <B issue={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 },
|
||||
@@ -141,10 +191,10 @@ export const NewsItems: NewsItem[] = [
|
||||
// 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 issue={275} wow>Google Gemini</B> models support</> },
|
||||
{ text: <><B issue={273}>Mistral Platform</B> support</> },
|
||||
{ text: <><B issue={270}>Ollama chats</B> perfection</> },
|
||||
{ text: <>Custom <B issue={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</> },
|
||||
@@ -158,11 +208,11 @@ export const NewsItems: NewsItem[] = [
|
||||
// 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)</> },
|
||||
{ text: <>Independent browsing with <B href={RCode + '/docs/config-browse.md'}>Browserless</B> support</> },
|
||||
{ text: <><B href={RIssues + '/256'}>Overheat</B> LLMs with higher temperature limits</> },
|
||||
{ text: <>Enhanced security via <B href={RCode + '/docs/deploy-authentication.md'}>password protection</B></> },
|
||||
{ text: <>New <B issue={251} wow>attachments system</B>: drag, paste, link, snap, images, text, pdfs</> },
|
||||
{ text: <>Desktop <B issue={253}>webcam access</B> for direct image capture (Labs option)</> },
|
||||
{ text: <>Independent browsing with <B code='/docs/config-browse.md'>Browserless</B> support</> },
|
||||
{ text: <><B issue={256}>Overheat</B> LLMs with higher temperature limits</> },
|
||||
{ text: <>Enhanced security via <B code='/docs/deploy-authentication.md'>password protection</B></> },
|
||||
{ text: <>{platformAwareKeystrokes('Ctrl+Shift+O')}: quick access to model options</> },
|
||||
{ text: <>Optimized voice input and performance</> },
|
||||
{ text: <>Latest Ollama and Oobabooga models</> },
|
||||
@@ -173,10 +223,10 @@ export const NewsItems: NewsItem[] = [
|
||||
versionName: 'Surf\'s Up',
|
||||
versionDate: new Date('2023-11-28T21:00:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/237'}>Web Browsing</B> support, see the <B href={RCode + '/docs/config-browse.md'}>browsing user guide</B></> },
|
||||
{ text: <><B href={RIssues + '/235'}>Branching Discussions</B> at any message</> },
|
||||
{ text: <><B href={RIssues + '/207'}>Keyboard Navigation</B>: use {platformAwareKeystrokes('Ctrl+Shift+Left/Right')} to navigate chats</> },
|
||||
{ text: <><B href={RIssues + '/236'}>UI fixes</B> (thanks to the first sponsor)</> },
|
||||
{ text: <><B issue={237} wow>Web Browsing</B> support, see the <B code='/docs/config-browse.md'>browsing user guide</B></> },
|
||||
{ text: <><B issue={235}>Branching Discussions</B> at any message</> },
|
||||
{ text: <><B issue={207}>Keyboard Navigation</B>: use {platformAwareKeystrokes('Ctrl+Shift+Left/Right')} to navigate chats</> },
|
||||
{ text: <><B issue={236}>UI fixes</B> (thanks to the first sponsor)</> },
|
||||
{ text: <>Added support for Anthropic Claude 2.1</> },
|
||||
{ text: <>Large rendering performance optimization</> },
|
||||
{ text: <>More: <Chip>/help</Chip>, import ChatGPT from source, new Flattener</> },
|
||||
@@ -188,10 +238,10 @@ export const NewsItems: NewsItem[] = [
|
||||
versionName: 'Loaded!',
|
||||
versionDate: new Date('2023-11-19T21:00:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/190'}>Continued Voice</B> for hands-free interaction</> },
|
||||
{ text: <><B href={RIssues + '/192'}>Visualization</B> Tool for data representations</> },
|
||||
{ text: <><B href={RCode + '/docs/config-ollama.md'}>Ollama (guide)</B> local models support</> },
|
||||
{ text: <><B href={RIssues + '/194'}>Text Tools</B> including highlight differences</> },
|
||||
{ text: <><B issue={190} wow>Continued Voice</B> for hands-free interaction</> },
|
||||
{ text: <><B issue={192}>Visualization</B> Tool for data representations</> },
|
||||
{ text: <><B code='/docs/config-ollama.md'>Ollama (guide)</B> local models support</> },
|
||||
{ text: <><B issue={194}>Text Tools</B> including highlight differences</> },
|
||||
{ text: <><B href='https://mermaid.js.org/'>Mermaid</B> Diagramming Rendering</> },
|
||||
{ text: <><B>OpenAI 1106</B> Chat Models</> },
|
||||
{ text: <><B>SDXL</B> support with Prodia</> },
|
||||
@@ -203,7 +253,7 @@ export const NewsItems: NewsItem[] = [
|
||||
versionCode: '1.4.0',
|
||||
items: [
|
||||
{ text: <><B>Share and clone</B> conversations, with public links</> },
|
||||
{ text: <><B href={RCode + '/docs/config-azure-openai.md'}>Azure</B> models, incl. gpt-4-32k</> },
|
||||
{ text: <><B code='/docs/config-azure-openai.md'>Azure</B> models, incl. gpt-4-32k</> },
|
||||
{ text: <><B>OpenRouter</B> models full support, incl. gpt-4-32k</> },
|
||||
{ text: <>Latex Rendering</> },
|
||||
{ text: <>Augmented Chat modes (Labs)</> },
|
||||
@@ -226,7 +276,7 @@ export const NewsItems: NewsItem[] = [
|
||||
{ text: <><B>Flattener</B> - 4-mode conversations summarizer</> },
|
||||
{ text: <><B>Forking</B> - branch your conversations</> },
|
||||
{ text: <><B>/s</B> and <B>/a</B> to append a <i>system</i> or <i>assistant</i> message</> },
|
||||
{ text: <>Local LLMs with <Link href={RCode + '/docs/config-local-oobabooga.md'} target='_blank'>Oobabooga server</Link></> },
|
||||
{ text: <>Local LLMs with <B code='/docs/config-local-oobabooga.md'>Oobabooga server</B></> },
|
||||
{ text: 'NextJS STOP bug.. squashed, with Vercel!' },
|
||||
],
|
||||
},
|
||||
@@ -240,17 +290,3 @@ 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;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import AddIcon from '@mui/icons-material/Add';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
|
||||
|
||||
import { RenderMarkdown } from '../../chat/components/message/RenderMarkdown';
|
||||
import { RenderMarkdownMemo } from '../../chat/components/message/blocks/RenderMarkdown';
|
||||
|
||||
import { LLMChainStep, useLLMChain } from '~/modules/aifn/useLLMChain';
|
||||
|
||||
@@ -79,7 +79,7 @@ export const PersonaPromptCard = (props: { content: string }) =>
|
||||
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
|
||||
You may now copy the text below and use it as Custom prompt!
|
||||
</Alert>
|
||||
<RenderMarkdown textBlock={{ type: 'text', content: props.content }} />
|
||||
<RenderMarkdownMemo textBlock={{ type: 'text', content: props.content }} />
|
||||
</CardContent>
|
||||
</Card>;
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export function CreatorDrawerItem(props: {
|
||||
<Box sx={{ overflow: 'hidden' }}>
|
||||
|
||||
{/* Title or System prompt (ellipsized) */}
|
||||
<Typography level='title-sm' sx={{ overflow: 'hidden', whiteSpace: 'nowrap', textOverflow: 'ellipsis' }}>
|
||||
<Typography level='title-sm' className='agi-ellipsize'>
|
||||
{item.name || (item.systemPrompt?.slice(0, 40) + '...')}
|
||||
</Typography>
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { PreferencesTab } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
|
||||
import { AppChatSettingsAI } from './AppChatSettingsAI';
|
||||
import { AppChatSettingsUI } from './AppChatSettingsUI';
|
||||
import { AppChatSettingsUI } from './settings-ui/AppChatSettingsUI';
|
||||
import { UxLabsSettings } from './UxLabsSettings';
|
||||
import { VoiceSettings } from './VoiceSettings';
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { ChatMessage } from '../chat/components/message/ChatMessage';
|
||||
import { BlocksRenderer } from '../chat/components/message/blocks/BlocksRenderer';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { createDMessage } from '~/common/state/store-chats';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
|
||||
|
||||
const shortcutsMd = `
|
||||
const shortcutsMd = platformAwareKeystrokes(`
|
||||
|
||||
| Shortcut | Description |
|
||||
|---------------------|-------------------------------------------------|
|
||||
@@ -30,15 +29,13 @@ const shortcutsMd = `
|
||||
| Ctrl + Shift + O | Options (current Chat Model) |
|
||||
| Ctrl + Shift + ? | Shortcuts |
|
||||
|
||||
`.trim();
|
||||
|
||||
const shortcutsMessage = createDMessage('assistant', platformAwareKeystrokes(shortcutsMd));
|
||||
`).trim();
|
||||
|
||||
|
||||
export function ShortcutsModal(props: { onClose: () => void }) {
|
||||
return (
|
||||
<GoodModal open title='Desktop Shortcuts' onClose={props.onClose}>
|
||||
<ChatMessage message={shortcutsMessage} hideAvatars noBottomBorder sx={{ p: 0, m: 0 }} />
|
||||
<BlocksRenderer text={shortcutsMd} fromRole='assistant' renderTextAsMarkdown />
|
||||
</GoodModal>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { FormControl, Typography } from '@mui/joy';
|
||||
import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
|
||||
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
||||
import ScreenshotMonitorIcon from '@mui/icons-material/ScreenshotMonitor';
|
||||
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
|
||||
@@ -16,31 +16,31 @@ export function UxLabsSettings() {
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const {
|
||||
labsCameraDesktop, labsSplitBranching, //labsDrawing,
|
||||
setLabsCameraDesktop, setLabsSplitBranching, //setLabsDrawing,
|
||||
labsAttachScreenCapture, setLabsAttachScreenCapture,
|
||||
labsCameraDesktop, setLabsCameraDesktop,
|
||||
} = useUXLabsStore();
|
||||
|
||||
return <>
|
||||
|
||||
{!isMobile && <FormSwitchControl
|
||||
title={<><AddAPhotoIcon color={labsCameraDesktop ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Webcam</>} description={labsCameraDesktop ? 'Enabled' : 'Disabled'}
|
||||
title={<><ScreenshotMonitorIcon color={labsAttachScreenCapture ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Screen Capture</>} description={'v1.13 · ' + (labsAttachScreenCapture ? 'Enabled' : 'Disabled')}
|
||||
checked={labsAttachScreenCapture} onChange={setLabsAttachScreenCapture}
|
||||
/>}
|
||||
|
||||
{!isMobile && <FormSwitchControl
|
||||
title={<><AddAPhotoIcon color={labsCameraDesktop ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Webcam</>} description={/*'v1.8 · ' +*/ (labsCameraDesktop ? 'Enabled' : 'Disabled')}
|
||||
checked={labsCameraDesktop} onChange={setLabsCameraDesktop}
|
||||
/>}
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><VerticalSplitIcon color={labsSplitBranching ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Split Branching</>} description={labsSplitBranching ? 'Enabled' : 'Disabled'}
|
||||
checked={labsSplitBranching} onChange={setLabsSplitBranching}
|
||||
/>
|
||||
|
||||
{/*<FormSwitchControl*/}
|
||||
{/* title={<><AddAPhotoIcon color={labsDrawing ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Drawing</>} description={labsDrawing ? 'Enabled' : 'Disabled'}*/}
|
||||
{/* checked={labsDrawing} onChange={setLabsDrawing}*/}
|
||||
{/*/>*/}
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title='Graduated' />
|
||||
<FormLabelStart title='Graduated' description='Ex-labs' />
|
||||
<Typography level='body-xs'>
|
||||
<Link href='https://github.com/enricoros/big-AGI/issues/354' target='_blank'>Call AGI</Link> · <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
|
||||
<Link href='https://github.com/enricoros/big-AGI/issues/208' target='_blank'>Split Chats</Link>
|
||||
{' · '}<Link href='https://github.com/enricoros/big-AGI/issues/359' target='_blank'>Draw App</Link>
|
||||
{' · '}<Link href='https://github.com/enricoros/big-AGI/issues/354' target='_blank'>Call AGI</Link>
|
||||
{' · '}<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
-6
@@ -13,6 +13,8 @@ import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { SettingTextSize } from './SettingTextSize';
|
||||
|
||||
|
||||
// configuration
|
||||
const SHOW_PURPOSE_FINDER = false;
|
||||
@@ -44,15 +46,17 @@ export function AppChatSettingsUI() {
|
||||
centerMode, setCenterMode,
|
||||
doubleClickToEdit, setDoubleClickToEdit,
|
||||
enterIsNewline, setEnterIsNewline,
|
||||
messageTextSize, setMessageTextSize,
|
||||
renderMarkdown, setRenderMarkdown,
|
||||
showPurposeFinder, setShowPurposeFinder,
|
||||
showPersonaFinder, setShowPersonaFinder,
|
||||
zenMode, setZenMode,
|
||||
} = useUIPreferencesStore(state => ({
|
||||
centerMode: state.centerMode, setCenterMode: state.setCenterMode,
|
||||
doubleClickToEdit: state.doubleClickToEdit, setDoubleClickToEdit: state.setDoubleClickToEdit,
|
||||
enterIsNewline: state.enterIsNewline, setEnterIsNewline: state.setEnterIsNewline,
|
||||
messageTextSize: state.messageTextSize, setMessageTextSize: state.setMessageTextSize,
|
||||
renderMarkdown: state.renderMarkdown, setRenderMarkdown: state.setRenderMarkdown,
|
||||
showPurposeFinder: state.showPurposeFinder, setShowPurposeFinder: state.setShowPurposeFinder,
|
||||
showPersonaFinder: state.showPersonaFinder, setShowPersonaFinder: state.setShowPersonaFinder,
|
||||
zenMode: state.zenMode, setZenMode: state.setZenMode,
|
||||
}), shallow);
|
||||
|
||||
@@ -62,7 +66,7 @@ export function AppChatSettingsUI() {
|
||||
|
||||
const handleRenderMarkdownChange = (event: React.ChangeEvent<HTMLInputElement>) => setRenderMarkdown(event.target.checked);
|
||||
|
||||
const handleShowSearchBarChange = (event: React.ChangeEvent<HTMLInputElement>) => setShowPurposeFinder(event.target.checked);
|
||||
const handleShowSearchBarChange = (event: React.ChangeEvent<HTMLInputElement>) => setShowPersonaFinder(event.target.checked);
|
||||
|
||||
return <>
|
||||
|
||||
@@ -98,9 +102,9 @@ export function AppChatSettingsUI() {
|
||||
|
||||
{SHOW_PURPOSE_FINDER && <FormControl orientation='horizontal' sx={{ justifyContent: 'space-between' }}>
|
||||
<FormLabelStart title='Purpose finder'
|
||||
description={showPurposeFinder ? 'Show search bar' : 'Hide search bar'} />
|
||||
<Switch checked={showPurposeFinder} onChange={handleShowSearchBarChange}
|
||||
endDecorator={showPurposeFinder ? 'On' : 'Off'}
|
||||
description={showPersonaFinder ? 'Show search bar' : 'Hide search bar'} />
|
||||
<Switch checked={showPersonaFinder} onChange={handleShowSearchBarChange}
|
||||
endDecorator={showPersonaFinder ? 'On' : 'Off'}
|
||||
slotProps={{ endDecorator: { sx: { minWidth: 26 } } }} />
|
||||
</FormControl>}
|
||||
|
||||
@@ -113,6 +117,8 @@ export function AppChatSettingsUI() {
|
||||
]}
|
||||
value={zenMode} onChange={setZenMode} />
|
||||
|
||||
<SettingTextSize textSize={messageTextSize} onChangeTextSize={setMessageTextSize} />
|
||||
|
||||
{!isPwa() && !isMobile && (
|
||||
<FormRadioControl
|
||||
title='Page Size'
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { FormControl, IconButton, Step, Stepper } from '@mui/joy';
|
||||
|
||||
import type { UIMessageTextSize } from '~/common/state/store-ui';
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
|
||||
|
||||
export function SettingTextSize({ textSize, onChangeTextSize }: {
|
||||
textSize: UIMessageTextSize,
|
||||
onChangeTextSize: (size: UIMessageTextSize) => void,
|
||||
}) {
|
||||
return (
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between' }}>
|
||||
<FormLabelStart title='Text Size'
|
||||
description={textSize === 'xs' ? 'Extra Small' : textSize === 'sm' ? 'Small' : 'Default'} />
|
||||
<Stepper sx={{
|
||||
maxWidth: 160,
|
||||
width: '100%',
|
||||
fontWeight: 'initial',
|
||||
'--Step-connectorThickness': '2px',
|
||||
'--StepIndicator-size': '2rem',
|
||||
}}>
|
||||
{(['xs', 'sm', 'md'] as UIMessageTextSize[]).map(sizeKey => {
|
||||
const isActive = sizeKey === textSize;
|
||||
return (
|
||||
<Step
|
||||
key={sizeKey}
|
||||
onClick={() => onChangeTextSize(sizeKey)}
|
||||
indicator={
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant={isActive ? 'outlined' : 'soft'}
|
||||
// color={isActive ? 'primary' : 'neutral'}
|
||||
sx={{
|
||||
// style
|
||||
fontSize: sizeKey,
|
||||
// borderRadius: !isActive ? '50%' : undefined,
|
||||
borderRadius: '50%',
|
||||
width: '1rem',
|
||||
height: '1rem',
|
||||
// style when active
|
||||
'--variant-borderWidth': '2px',
|
||||
borderColor: 'primary.solidBg',
|
||||
}}
|
||||
>
|
||||
{'Aa' /* Nothing says 'font' more than this */ }
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stepper>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { hasNoChatLinkItems } from '~/modules/trade/link/store-link';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
|
||||
// enable to show all items, for layout development
|
||||
@@ -115,7 +114,7 @@ export const navItems: {
|
||||
route: '/draw',
|
||||
// hideOnMobile: true,
|
||||
hideDrawer: true,
|
||||
hideIcon: () => !useUXLabsStore.getState().labsDrawing,
|
||||
_delete: true,
|
||||
},
|
||||
{
|
||||
name: 'Cortex',
|
||||
@@ -139,6 +138,8 @@ export const navItems: {
|
||||
iconActive: WorkspacesIcon,
|
||||
type: 'app',
|
||||
route: '/workspace',
|
||||
hideDrawer: true,
|
||||
hideOnMobile: true,
|
||||
_delete: true,
|
||||
},
|
||||
// <-- divider here -->
|
||||
|
||||
+16
-5
@@ -9,7 +9,7 @@ export const hideOnMobile = { display: { xs: 'none', md: 'flex' } };
|
||||
// export const hideOnDesktop = { display: { xs: 'flex', md: 'none' } };
|
||||
|
||||
// Dimensions
|
||||
export const formLabelStartWidth = 150;
|
||||
export const formLabelStartWidth = 140;
|
||||
|
||||
|
||||
// Theme & Fonts
|
||||
@@ -38,7 +38,7 @@ export const appTheme = extendTheme({
|
||||
palette: {
|
||||
neutral: {
|
||||
plainColor: 'var(--joy-palette-neutral-800)', // [700 -> 800] Dropdown menu: increase text contrast a bit
|
||||
solidBg: 'var(--joy-palette-neutral-700)', // [500 -> 700] AppBar background & Button[solid]
|
||||
solidBg: 'var(--joy-palette-neutral-700)', // [500 -> 700] PageBar background & Button[solid]
|
||||
solidHoverBg: 'var(--joy-palette-neutral-800)', // [600 -> 800] Buttons[solid]:hover
|
||||
},
|
||||
// primary [800] > secondary [700 -> 800] > tertiary [600] > icon [500 -> 700]
|
||||
@@ -103,6 +103,15 @@ export const appTheme = extendTheme({
|
||||
},
|
||||
},
|
||||
|
||||
// JoyModal: {
|
||||
// styleOverrides: {
|
||||
// backdrop: {
|
||||
// // backdropFilter: 'blur(2px)',
|
||||
// backdropFilter: 'none',
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
|
||||
/**
|
||||
* Switch: increase the size of the thumb, to a default iconButton
|
||||
* NOTE: do not use anything else than 'md' size
|
||||
@@ -111,9 +120,9 @@ export const appTheme = extendTheme({
|
||||
styleOverrides: {
|
||||
root: ({ ownerState }) => ({
|
||||
...(ownerState.size === 'md' && {
|
||||
'--Switch-trackWidth': '40px',
|
||||
'--Switch-trackHeight': '24px',
|
||||
'--Switch-thumbSize': '18px',
|
||||
'--Switch-trackWidth': '36px',
|
||||
'--Switch-trackHeight': '22px',
|
||||
'--Switch-thumbSize': '17px',
|
||||
}),
|
||||
}),
|
||||
},
|
||||
@@ -126,11 +135,13 @@ export const themeBgAppDarker = 'background.level2';
|
||||
export const themeBgAppChatComposer = 'background.surface';
|
||||
|
||||
export const lineHeightChatText = 1.75;
|
||||
export const lineHeightChatCode = 1.75;
|
||||
export const lineHeightTextarea = 1.75;
|
||||
|
||||
export const themeZIndexPageBar = 25;
|
||||
export const themeZIndexDesktopDrawer = 26;
|
||||
export const themeZIndexDesktopNav = 27;
|
||||
export const themeZIndexOverMobileDrawer = 1301;
|
||||
|
||||
export const themeBreakpoints = appTheme.breakpoints.values;
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ const Popup = styled(Popper)({
|
||||
export function CloseableMenu(props: {
|
||||
open: boolean, anchorEl: HTMLElement | null, onClose: () => void,
|
||||
dense?: boolean,
|
||||
bigIcons?: boolean,
|
||||
// variant?: VariantProp,
|
||||
// color?: ColorPaletteProp,
|
||||
// size?: 'sm' | 'md' | 'lg',
|
||||
@@ -76,9 +77,13 @@ export function CloseableMenu(props: {
|
||||
// variant={props.variant} color={props.color}
|
||||
onKeyDown={handleListKeyDown}
|
||||
sx={{
|
||||
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
|
||||
'--ListItem-minHeight': props.dense ? '2.25rem' : '3rem',
|
||||
'--ListItemDecorator-size': '2.75rem', // icon width
|
||||
'--ListItem-minHeight': props.dense
|
||||
? '2.25rem' /* 2.25 is the default */
|
||||
: '2.5rem', /* we enlarge the default */
|
||||
...(props.bigIcons && {
|
||||
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
|
||||
// '--ListItemDecorator-size': '2.75rem',
|
||||
}),
|
||||
backgroundColor: 'background.popup',
|
||||
boxShadow: 'md',
|
||||
...(props.maxHeightGapPx !== undefined ? { maxHeight: `calc(100dvh - ${props.maxHeightGapPx}px)`, overflowY: 'auto' } : {}),
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Accordion, AccordionDetails, AccordionGroup, AccordionSummary, accordionSummaryClasses, Box } from '@mui/joy';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
|
||||
|
||||
export function ExpanderAccordion(props: { title?: React.ReactNode, icon?: React.ReactNode, startCollapsed?: boolean, children?: React.JSX.Element }) {
|
||||
|
||||
// state
|
||||
const [expanded, setExpanded] = React.useState(props.startCollapsed !== true);
|
||||
|
||||
return (
|
||||
<AccordionGroup>
|
||||
<Accordion
|
||||
// variant={expanded ? 'solid' : 'soft'}
|
||||
expanded={expanded}
|
||||
onChange={(_event, expanded) => setExpanded(expanded)}
|
||||
>
|
||||
<AccordionSummary
|
||||
variant='soft'
|
||||
indicator={<KeyboardArrowDownIcon />}
|
||||
slotProps={{
|
||||
indicator: {
|
||||
sx: {
|
||||
transition: 'transform 0.2s',
|
||||
},
|
||||
},
|
||||
}}
|
||||
sx={{
|
||||
[`&.${accordionSummaryClasses.indicator}[aria-expanded='true']`]: {
|
||||
transform: 'rotate(-180deg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.icon} {props.title}
|
||||
</AccordionSummary>
|
||||
|
||||
<AccordionDetails variant='solid'>
|
||||
<Box sx={{ display: 'grid' }}>
|
||||
{expanded && props.children}
|
||||
</Box>
|
||||
</AccordionDetails>
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, styled } from '@mui/joy';
|
||||
|
||||
|
||||
const BoxCollapser = styled(Box)({
|
||||
display: 'grid',
|
||||
transition: 'grid-template-rows 0.2s cubic-bezier(.17,.84,.44,1)',
|
||||
gridTemplateRows: '0fr',
|
||||
'&[aria-expanded="true"]': {
|
||||
gridTemplateRows: '1fr',
|
||||
},
|
||||
});
|
||||
|
||||
const BoxCollapsee = styled(Box)({
|
||||
overflow: 'hidden',
|
||||
});
|
||||
|
||||
|
||||
export function ExpanderControlledBox(props: { expanded: boolean, children: React.ReactNode, sx?: SxProps }) {
|
||||
return (
|
||||
<BoxCollapser aria-expanded={props.expanded} sx={props.sx}>
|
||||
<BoxCollapsee>
|
||||
{props.children}
|
||||
</BoxCollapsee>
|
||||
</BoxCollapser>
|
||||
);
|
||||
}
|
||||
@@ -22,13 +22,17 @@ export function GoodModal(props: {
|
||||
}) {
|
||||
const showBottomClose = !!props.onClose && props.hideBottomClose !== true;
|
||||
return (
|
||||
<Modal open={props.open} onClose={props.onClose}>
|
||||
<Modal
|
||||
open={props.open}
|
||||
onClose={props.onClose}
|
||||
// slotProps={{ backdrop: { sx: { backdropFilter: 'blur(6px)' } } }}
|
||||
>
|
||||
<ModalOverflow sx={{ p: 1 }}>
|
||||
<ModalDialog
|
||||
sx={{
|
||||
minWidth: { xs: 360, sm: 500, md: 600, lg: 700 },
|
||||
maxWidth: 700,
|
||||
display: 'flex', flexDirection: 'column', gap: 'var(--Card-padding)',
|
||||
display: 'grid', gap: 'var(--Card-padding)',
|
||||
...props.sx,
|
||||
}}>
|
||||
|
||||
@@ -41,7 +45,9 @@ export function GoodModal(props: {
|
||||
|
||||
{props.dividers === true && <Divider />}
|
||||
|
||||
{/*<Box sx={{ maxHeight: '80lvh', overflowY: 'auto', display: 'grid', gap: 'var(--Card-padding)' }}>*/}
|
||||
{props.children}
|
||||
{/*</Box>*/}
|
||||
|
||||
{props.dividers === true && <Divider />}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { ReactPlayerProps } from 'react-player';
|
||||
|
||||
|
||||
type VideoPlayerProps = ReactPlayerProps & {
|
||||
// make the player responsive
|
||||
responsive?: boolean;
|
||||
// set this to not set the full URL
|
||||
youTubeVideoId?: string;
|
||||
};
|
||||
|
||||
const VideoPlayerDynamic = React.lazy(async () => {
|
||||
|
||||
// dynamically import react-player (saves 7kb but still..)
|
||||
const { default: ReactPlayer } = await import('react-player');
|
||||
|
||||
return {
|
||||
default: (props: ReactPlayerProps) => {
|
||||
|
||||
const { responsive, youTubeVideoId, ...baseProps } = props;
|
||||
|
||||
// responsive patch
|
||||
if (responsive) {
|
||||
baseProps.width = '100%';
|
||||
baseProps.height = '100%';
|
||||
}
|
||||
|
||||
// fill in the URL if we have a YouTube video ID
|
||||
if (youTubeVideoId) {
|
||||
baseProps.url = `https://www.youtube.com/watch?v=${youTubeVideoId}`;
|
||||
}
|
||||
|
||||
return <ReactPlayer {...baseProps} />;
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
export function VideoPlayer(props: VideoPlayerProps) {
|
||||
return (
|
||||
<React.Suspense fallback={<div>Loading...</div>}>
|
||||
<VideoPlayerDynamic {...props} />
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { SvgIcon } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: the PodcastsIcon from '@mui/icons-material/Podcasts';
|
||||
*/
|
||||
export function ChatMulticastOffIcon(props: { sx?: SxProps }) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 24 24' width='24' height='24' {...props}>
|
||||
<path d='M14 12c0 .74-.4 1.38-1 1.72V22h-2v-8.28c-.6-.35-1-.98-1-1.72 0-1.1.9-2 2-2s2 .9 2 2'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { SvgIcon } from '@mui/joy';
|
||||
|
||||
/*
|
||||
* Source: the PodcastsIcon from '@mui/icons-material/Podcasts';
|
||||
*/
|
||||
export function ChatMulticastOnIcon(props: { sx?: SxProps }) {
|
||||
return (
|
||||
<SvgIcon viewBox='0 0 24 24' width='24' height='24' {...props}>
|
||||
<path d='M14 12c0 .74-.4 1.38-1 1.72V22h-2v-8.28c-.6-.35-1-.98-1-1.72 0-1.1.9-2 2-2s2 .9 2 2m-2-6c-3.31 0-6 2.69-6 6 0 1.74.75 3.31 1.94 4.4l1.42-1.42C8.53 14.25 8 13.19 8 12c0-2.21 1.79-4 4-4s4 1.79 4 4c0 1.19-.53 2.25-1.36 2.98l1.42 1.42C17.25 15.31 18 13.74 18 12c0-3.31-2.69-6-6-6m0-4C6.48 2 2 6.48 2 12c0 2.85 1.2 5.41 3.11 7.24l1.42-1.42C4.98 16.36 4 14.29 4 12c0-4.41 3.59-8 8-8s8 3.59 8 8c0 2.29-.98 4.36-2.53 5.82l1.42 1.42C20.8 17.41 22 14.85 22 12c0-5.52-4.48-10-10-10'></path>
|
||||
</SvgIcon>
|
||||
);
|
||||
}
|
||||
Vendored
Vendored
Vendored
Vendored
Vendored
Vendored
@@ -1,22 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import { Box } from '@mui/joy';
|
||||
import { Box, styled } from '@mui/joy';
|
||||
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
|
||||
|
||||
export function GoodPanelResizeHandler() {
|
||||
return (
|
||||
<PanelResizeHandle>
|
||||
<Box sx={{
|
||||
backgroundColor: themeBgApp,
|
||||
height: '100%',
|
||||
width: '4px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.softActiveBg',
|
||||
},
|
||||
}} />
|
||||
</PanelResizeHandle>
|
||||
);
|
||||
}
|
||||
export const PanelResizeInset = styled(Box)({
|
||||
backgroundColor: themeBgApp,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minWidth: '0.25rem',
|
||||
minHeight: '0.25rem',
|
||||
transition: 'background-color 0.2s',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-primary-solidBg)',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Chip, chipClasses } from '@mui/joy';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
|
||||
|
||||
function ChipExpander(props: {
|
||||
text: React.ReactNode,
|
||||
expand: boolean,
|
||||
handleToggleExpand: () => void
|
||||
}) {
|
||||
return (
|
||||
<Chip
|
||||
variant={props.expand ? 'solid' : 'outlined'}
|
||||
size='md'
|
||||
onClick={props.handleToggleExpand}
|
||||
endDecorator={<KeyboardArrowDownIcon />}
|
||||
aria-expanded={props.expand}
|
||||
sx={{
|
||||
px: 1.5,
|
||||
[`& .${chipClasses.endDecorator}`]: {
|
||||
transition: 'transform 0.2s',
|
||||
},
|
||||
[`&[aria-expanded='true'] .${chipClasses.endDecorator}`]: {
|
||||
transform: 'rotate(-180deg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{props.text}
|
||||
</Chip>
|
||||
);
|
||||
}
|
||||
|
||||
export function useChipBoolean(text: React.ReactNode, initialExpand = false): [boolean, React.JSX.Element] {
|
||||
|
||||
// state
|
||||
const [expanded, setExpanded] = React.useState(initialExpand);
|
||||
|
||||
const component = React.useMemo(() => (
|
||||
<ChipExpander text={text} expand={expanded} handleToggleExpand={() => setExpanded(on => !on)} />
|
||||
), [expanded, text]);
|
||||
|
||||
return [expanded, component];
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import * as React from 'react';
|
||||
*/
|
||||
class AloneDetector {
|
||||
private readonly clientId: string;
|
||||
private readonly broadcastChannel: BroadcastChannel;
|
||||
private readonly bChannel: BroadcastChannel;
|
||||
|
||||
private aloneCallback: ((isAlone: boolean) => void) | null;
|
||||
private aloneTimerId: number | undefined;
|
||||
@@ -17,15 +17,15 @@ class AloneDetector {
|
||||
this.clientId = Math.random().toString(36).substring(2, 10);
|
||||
this.aloneCallback = onAlone;
|
||||
|
||||
this.broadcastChannel = new BroadcastChannel(channelName);
|
||||
this.broadcastChannel.onmessage = this.handleIncomingMessage;
|
||||
this.bChannel = new BroadcastChannel(channelName);
|
||||
this.bChannel.onmessage = this.handleIncomingMessage;
|
||||
|
||||
}
|
||||
|
||||
public onUnmount(): void {
|
||||
// close channel
|
||||
this.broadcastChannel.onmessage = null;
|
||||
this.broadcastChannel.close();
|
||||
this.bChannel.onmessage = null;
|
||||
this.bChannel.close();
|
||||
|
||||
// clear timeout
|
||||
if (this.aloneTimerId)
|
||||
@@ -38,7 +38,7 @@ class AloneDetector {
|
||||
public checkIfAlone(): void {
|
||||
|
||||
// triggers other clients
|
||||
this.broadcastChannel.postMessage({ type: 'CHECK', sender: this.clientId });
|
||||
this.bChannel.postMessage({ type: 'CHECK', sender: this.clientId });
|
||||
|
||||
// if no response within 500ms, assume this client is alone
|
||||
this.aloneTimerId = window.setTimeout(() => {
|
||||
@@ -56,7 +56,7 @@ class AloneDetector {
|
||||
switch (event.data.type) {
|
||||
|
||||
case 'CHECK':
|
||||
this.broadcastChannel.postMessage({ type: 'ALIVE', sender: this.clientId });
|
||||
this.bChannel.postMessage({ type: 'ALIVE', sender: this.clientId });
|
||||
break;
|
||||
|
||||
case 'ALIVE':
|
||||
|
||||
@@ -10,7 +10,6 @@ import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { AgiSquircleIcon } from '~/common/components/icons/AgiSquircleIcon';
|
||||
import { checkDivider, checkVisibileIcon, NavItemApp, navItems } from '~/common/app.nav';
|
||||
import { themeZIndexDesktopNav } from '~/common/app.theme';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { BringTheLove } from './components/BringTheLove';
|
||||
import { DesktopNavGroupBox, DesktopNavIcon, navItemClasses } from './components/DesktopNavIcon';
|
||||
@@ -35,8 +34,6 @@ export function DesktopNav(props: { component: React.ElementType, currentApp?: N
|
||||
showModelsSetup, openModelsSetup,
|
||||
} = useOptimaLayout();
|
||||
const noLLMs = useModelsStore(state => !state.llms.length);
|
||||
// ignore the return value, this just makes sure that the nav is refreshed when UX Labs change - while "drawing" is in there
|
||||
const labsDrawing = useUXLabsStore(state => state.labsDrawing);
|
||||
|
||||
|
||||
// show/hide the pane when clicking on the logo
|
||||
@@ -73,7 +70,7 @@ export function DesktopNav(props: { component: React.ElementType, currentApp?: N
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
}, [props.currentApp, isDrawerOpen, labsDrawing, toggleDrawer]);
|
||||
}, [isDrawerOpen, props.currentApp, toggleDrawer]);
|
||||
|
||||
|
||||
// External link items
|
||||
@@ -134,7 +131,7 @@ export function DesktopNav(props: { component: React.ElementType, currentApp?: N
|
||||
|
||||
<InvertedBarCornerItem>
|
||||
<Tooltip title={isDrawerOpen ? 'Close Drawer' /* for Aria reasons */ : 'Open Drawer'}>
|
||||
<DesktopNavIcon disabled={!logoButtonTogglesPane} onClick={handleLogoButtonClick}>
|
||||
<DesktopNavIcon disabled={!logoButtonTogglesPane} onClick={handleLogoButtonClick} className={navItemClasses.typeMenu}>
|
||||
{logoButtonTogglesPane ? <MenuIcon /> : <AgiSquircleIcon inverted sx={{ color: 'white' }} />}
|
||||
</DesktopNavIcon>
|
||||
</Tooltip>
|
||||
|
||||
@@ -19,7 +19,7 @@ import { PageWrapper } from './PageWrapper';
|
||||
*
|
||||
* Main functions:
|
||||
* - modern responsive layout
|
||||
* - core layout of the application, with the Nav, Panes, Appbar, etc.
|
||||
* - core layout of the application, with the Nav, Panes, PageBar, etc.
|
||||
* - the child(ren) of this layout are placed in the main content area
|
||||
* - allows for pluggable components of children applications, via usePluggableOptimaLayout
|
||||
* - overlays and displays various modals
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user