mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 677facb867 | |||
| 494086765b | |||
| 59ca03e17d | |||
| e0e56d70c9 | |||
| b408267e6e | |||
| 6385d7aa84 | |||
| fa811c951c | |||
| 7085c3a7aa | |||
| 2333318cb4 | |||
| 3aebcb360c | |||
| bf60d699e3 | |||
| d775d47623 | |||
| 2eb3397394 | |||
| e27c35373d | |||
| 5e1966af5f | |||
| 7cbcf01ca9 | |||
| 6898fa6cc1 | |||
| 1e796299a2 | |||
| 7026024da5 | |||
| 3ed52fa92f | |||
| a3e04f5973 | |||
| 8bf90e3622 | |||
| cdc2de5018 | |||
| b26370a85a | |||
| adf0197a9e | |||
| c00c41a160 | |||
| 09c74e6cf4 | |||
| 304e66b098 | |||
| 64b6b08652 | |||
| cbea304a97 | |||
| c3e73fa9c8 | |||
| 4c978020d9 | |||
| 481b85bdad | |||
| b80fd0494a | |||
| c7dea43d1a | |||
| 726053ffcd | |||
| ee4e2c265b | |||
| a5332d2c82 | |||
| 2f45ce48fa | |||
| 104922dc20 | |||
| d68ccd9dfb | |||
| 676bcadd17 | |||
| c08e83c618 | |||
| 7a69b32506 | |||
| a9e1a968e8 | |||
| dc30a7a55a | |||
| f570627b09 | |||
| e601302db8 | |||
| f9e207ff7c | |||
| 8100c5cfd1 | |||
| 0b0c3891bb | |||
| b4cdd5546d | |||
| 8444b32db2 | |||
| 69098273bf | |||
| 5cd5702b83 | |||
| 605d288da6 | |||
| 499840cae3 | |||
| 4529fc325b | |||
| 4769e9b900 | |||
| 64d13a0d52 | |||
| 7df1517b23 | |||
| 56c372455d | |||
| 2e649ea12b | |||
| 2a67315504 | |||
| b53ceb70c4 | |||
| 3c9d06aac7 | |||
| 77e7c1d467 | |||
| eb38e119b8 | |||
| 06402cc5c1 | |||
| ddf631cdfc | |||
| f7e89ae65c | |||
| 07e1e1c580 | |||
| f6eb2aecee | |||
| f416b1df97 | |||
| 29d17795b8 | |||
| 3b30f649c6 | |||
| ba9a9714a7 | |||
| c304ab5f3b | |||
| cd4d5042e9 | |||
| 6c4d177bfc | |||
| 5d1620b5c1 | |||
| bd78808950 | |||
| 6aee6aeac1 | |||
| 5ae970a526 | |||
| 87718d73d2 | |||
| 7c8498573e | |||
| f6e82d0c0c | |||
| f7f827660d | |||
| 664b221e67 | |||
| f184a4bf97 | |||
| e442816c15 | |||
| aaa3b65cd8 | |||
| c6441662b0 | |||
| b902a7bce8 | |||
| 87a916ba09 | |||
| 35a85ed2fa | |||
| 75d56bfb56 | |||
| d0a125fad5 | |||
| 2af8437f6d | |||
| 0c3e65575c | |||
| 1c15057fca | |||
| 44da928489 | |||
| 85027d3e3a |
@@ -9,6 +9,8 @@ assignees: enricoros
|
||||
|
||||
## Release checklist:
|
||||
|
||||
- [x] Create a new [Release Issue](https://github.com/enricoros/big-AGI/issues/new?assignees=enricoros&projects=enricoros/4&template=maintainers-release.md&title=Release+1.2.3)
|
||||
- [ ] Replace 1.1.0 with the _former_ release, and _1.2.3_ with THIS
|
||||
- [ ] Update the [Roadmap](https://github.com/users/enricoros/projects/4/views/2) calling out shipped features
|
||||
- [ ] Create and update a [Milestone](https://github.com/enricoros/big-agi/milestones) for the release
|
||||
- [ ] Assign this task
|
||||
@@ -34,23 +36,40 @@ assignees: enricoros
|
||||
- [ ] Discord announcement
|
||||
- [ ] Twitter announcement
|
||||
|
||||
### Links
|
||||
|
||||
## Links
|
||||
Milestone:
|
||||
Former release task:
|
||||
GitHub release:
|
||||
|
||||
- Milestone: https://github.com/enricoros/big-AGI/milestone/X
|
||||
- GitHub release: https://github.com/enricoros/big-AGI/releases/tag/vX.Y.Z
|
||||
- Former release task: https://github.com/enricoros/big-AGI/issues/XXX
|
||||
|
||||
## Artifacts Generation
|
||||
|
||||
1) The following is my opensource application
|
||||
- paste README.md
|
||||
2) I am announcing a new version, 1.7.0. The following were the announcements for 1.6.0. Discord announcement, GitHub Release, in-app news.data.tsx, changelog.md.
|
||||
- paste the former: `discord announcement`, `GitHub release`, `news.data.tsx`, `changelog.md`
|
||||
3) The following is the new data I have for 1.7.0
|
||||
- paste the link to the milestone (closed) and each individual issue (content will be downloaded)
|
||||
- paste the git changelog `git log v1.6.0..v1.7.0 | clip`
|
||||
|
||||
```markdown
|
||||
You help me generate the following collateral for the new release of my opensource application
|
||||
called big-AGI. The new release is 1.2.3.
|
||||
To familiarize yourself with the application, the following are the Website and the GitHub README.md.
|
||||
```
|
||||
|
||||
- paste the URL: https://big-agi.com
|
||||
- drag & drop: [README.md](https://raw.githubusercontent.com/enricoros/big-AGI/main/README.md)
|
||||
|
||||
```markdown
|
||||
I am announcing a new version, 1.2.3.
|
||||
For reference, the following was the collateral for 1.1.0 (Discord announcement,
|
||||
GitHub Release, in-app-news file news.data.tsx, changelog.md).
|
||||
```
|
||||
|
||||
- paste the former: `discord announcement`,
|
||||
- `GitHub release`,
|
||||
- `news.data.tsx`,
|
||||
- `changelog.md`
|
||||
|
||||
```markdown
|
||||
The following are the new developments for 1.2.3:
|
||||
```
|
||||
|
||||
- paste the link to the milestone (closed) and each individual issue (content will be downloaded)
|
||||
- paste the git changelog `git log v1.1.0..v1.2.3 | clip`
|
||||
|
||||
### news.data.TSX
|
||||
|
||||
@@ -66,7 +85,9 @@ I need the following from you:
|
||||
### GitHub release
|
||||
|
||||
```markdown
|
||||
Please create the 1.2.3 Release Notes for GitHub. The following were the Release Notes for 1.1.0. Use a truthful and honest tone, undestanding that people's time and attention span is short. Today is 2023-12-20.
|
||||
Please create the 1.2.3 Release Notes for GitHub.
|
||||
Use a truthful and honest tone, understanding that people's time and attention span is short.
|
||||
Today is 2024-1-1.
|
||||
```
|
||||
|
||||
Now paste-attachment the former release notes (or 1.5.0 which was accurate and great), including the new contributors and
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Enrico Ros
|
||||
Copyright (c) 2023-2024 Enrico Ros
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@@ -21,7 +21,17 @@ shows the current developments and future ideas.
|
||||
- Got a suggestion? [_Add your roadmap ideas_](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
|
||||
- Want to contribute? [_Pick up a task!_](https://github.com/users/enricoros/projects/4/views/4) - _easy_ to _pro_
|
||||
|
||||
### What's New in 1.8.0 · Dec 20, 2023 · To The Moon And Back · 🚀🌕🔙
|
||||
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
|
||||
|
||||
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
|
||||
- **Perfect scrolling mechanics** across devices. [#304](https://github.com/enricoros/big-AGI/issues/304)
|
||||
- Persona creation now supports **text input**. [#287](https://github.com/enricoros/big-AGI/pull/287)
|
||||
- Openrouter updates for better model management and rate limit handling
|
||||
- Image drawing UX improvements
|
||||
- Layout fix for Firefox users
|
||||
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
|
||||
|
||||
### What's New in 1.8.0 · Dec 20, 2023
|
||||
|
||||
- **Google Gemini Support**: Use the newest Google models. [#275](https://github.com/enricoros/big-agi/issues/275)
|
||||
- **Mistral Platform**: Mixtral and future models support. [#273](https://github.com/enricoros/big-agi/issues/273)
|
||||
@@ -33,41 +43,10 @@ shows the current developments and future ideas.
|
||||
- Official Downloads: Easy access to the latest big-AGI on [big-AGI.com](https://big-agi.com)
|
||||
- For developers: [troubleshot networking](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483), fixed Vercel deployment, cleaned up the LLMs/Streaming framework
|
||||
|
||||
### What's New in 1.7.0 · Dec 11, 2023
|
||||
### What's New in... ?
|
||||
|
||||
- **Attachments System Overhaul**: Drag, paste, link, snap, text, images, PDFs and more. [#251](https://github.com/enricoros/big-agi/issues/251)
|
||||
- **Desktop Webcam Capture**: Image capture now available as Labs feature. [#253](https://github.com/enricoros/big-agi/issues/253)
|
||||
- **Independent Browsing**: Full browsing support with Browserless. [Learn More](https://github.com/enricoros/big-agi/blob/main/docs/config-browse.md)
|
||||
- **Overheat LLMs**: Push the creativity with higher LLM temperatures. [#256](https://github.com/enricoros/big-agi/issues/256)
|
||||
- **Model Options Shortcut**: Quick adjust with `Ctrl+Shift+O`
|
||||
- Optimized Voice Input and Performance
|
||||
- Latest Ollama and Oobabooga models
|
||||
- For developers: **Password Protection**: HTTP Basic Auth. [Learn How](https://github.com/enricoros/big-agi/blob/main/docs/deploy-authentication.md)
|
||||
|
||||
### What's New in 1.6.0 - Nov 28, 2023
|
||||
|
||||
- **Web Browsing**: Download web pages within chats - [browsing guide](https://github.com/enricoros/big-agi/blob/main/docs/config-browse.md)
|
||||
- **Branching Discussions**: Create new conversations from any message
|
||||
- **Keyboard Navigation**: Swift chat navigation with new shortcuts (e.g. ctrl+alt+left/right)
|
||||
- **Performance Boost**: Faster rendering for a smoother experience
|
||||
- **UI Enhancements**: Refined interface based on user feedback
|
||||
- **New Features**: Anthropic Claude 2.1, `/help` command, and Flattener tool
|
||||
- **For Developers**: Code quality upgrades and snackbar notifications
|
||||
|
||||
### What's New in 1.5.0 - Nov 19, 2023
|
||||
|
||||
- **Continued Voice**: Engage with hands-free interaction for a seamless experience
|
||||
- **Visualization Tool**: Create data representations with our new visualization capabilities
|
||||
- **Ollama Local Models**: Leverage local models support with our comprehensive guide
|
||||
- **Text Tools**: Enjoy tools including highlight differences to refine your content
|
||||
- **Mermaid Diagramming**: Render complex diagrams with our Mermaid language support
|
||||
- **OpenAI 1106 Chat Models**: Experience the cutting-edge capabilities of the latest OpenAI models
|
||||
- **SDXL Support**: Enhance your image generation with SDXL support for Prodia
|
||||
- **Cloudflare OpenAI API Gateway**: Integrate with Cloudflare for a robust API gateway
|
||||
- **Helicone for Anthropic**: Utilize Helicone's tools for Anthropic models
|
||||
|
||||
Check out the [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), or
|
||||
the [past releases changelog](docs/changelog.md).
|
||||
> [To The Moon And Back, Attachment Theory, Surf's Up, Loaded, and more releases...](docs/changelog.md).
|
||||
> Check out the [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2)
|
||||
|
||||
## ✨ Key Features 👊
|
||||
|
||||
|
||||
+13
-3
@@ -5,12 +5,22 @@ by release.
|
||||
|
||||
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
|
||||
|
||||
### 1.9.0 - Dec 2023
|
||||
### 1.10.0 - Jan 2024
|
||||
|
||||
- milestone: [1.10.0](https://github.com/enricoros/big-agi/milestone/10)
|
||||
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
|
||||
- milestone: [1.9.0](https://github.com/enricoros/big-agi/milestone/9)
|
||||
|
||||
### What's New in 1.8.0 · Dec 20, 2023 · To The Moon And Back · 🚀🌕🔙
|
||||
### What's New in 1.9.0 · Dec 28, 2023 · Creative Horizons
|
||||
|
||||
- **DALL·E 3 integration** for enhanced image generation. [#212](https://github.com/enricoros/big-AGI/issues/212)
|
||||
- **Perfect scrolling mechanics** across devices. [#304](https://github.com/enricoros/big-AGI/issues/304)
|
||||
- Persona creation now supports **text input**. [#287](https://github.com/enricoros/big-AGI/pull/287)
|
||||
- Openrouter updates for better model management and rate limit handling
|
||||
- Image drawing UX improvements
|
||||
- Layout fix for Firefox users
|
||||
- Developer enhancements: Text2Image subsystem, Optima layout, ScrollToBottom library, Panes library, and Llms subsystem updates.
|
||||
|
||||
### What's New in 1.8.0 · Dec 20, 2023 · To The Moon And Back
|
||||
|
||||
- **Google Gemini Support**: Use the newest Google models. [#275](https://github.com/enricoros/big-agi/issues/275)
|
||||
- **Mistral Platform**: Mixtral and future models support. [#273](https://github.com/enricoros/big-agi/issues/273)
|
||||
|
||||
Generated
+163
-140
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "big-agi",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@dqbd/tiktoken": "^1.0.7",
|
||||
@@ -14,10 +14,10 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.0",
|
||||
"@mui/joy": "^5.0.0-beta.18",
|
||||
"@mui/icons-material": "^5.15.1",
|
||||
"@mui/joy": "^5.0.0-beta.19",
|
||||
"@next/bundle-analyzer": "^14.0.4",
|
||||
"@prisma/client": "^5.7.0",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@sanity/diff-match-patch": "^3.1.1",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tanstack/react-query": "~4.36.1",
|
||||
@@ -30,6 +30,7 @@
|
||||
"eventsource-parser": "^1.1.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"next": "^14.0.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "4.0.269",
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
@@ -37,6 +38,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^1.0.5",
|
||||
"react-timeago": "^7.2.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"superjson": "^2.2.1",
|
||||
@@ -47,18 +49,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/puppeteer": "^0.0.5",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.6",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
"prisma": "^5.7.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
@@ -500,9 +503,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
|
||||
"integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz",
|
||||
"integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
@@ -596,14 +599,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/base": {
|
||||
"version": "5.0.0-beta.27",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.27.tgz",
|
||||
"integrity": "sha512-duL37qxihT1N0pW/gyXVezP7SttLkF+cLAs/y6g6ubEFmVadjbnZ45SeF12/vAiKzqwf5M0uFH1cczIPXFZygA==",
|
||||
"version": "5.0.0-beta.28",
|
||||
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.28.tgz",
|
||||
"integrity": "sha512-KIoSc5sUFceeCaZTq5MQBapFzhHqMo4kj+4azWaCAjorduhcRQtN+BCgVHmo+gvEjix74bUfxwTqGifnu2fNTg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@floating-ui/react-dom": "^2.0.4",
|
||||
"@mui/types": "^7.2.11",
|
||||
"@mui/utils": "^5.15.0",
|
||||
"@mui/utils": "^5.15.1",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -627,18 +630,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/core-downloads-tracker": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.0.tgz",
|
||||
"integrity": "sha512-NpGtlHwuyLfJtdrlERXb8qRqd279O0VnuGaZAor1ehdNhUJOD1bSxHDeXKZkbqNpvi50hasFj7lsbTpluworTQ==",
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.15.1.tgz",
|
||||
"integrity": "sha512-y/nUEsWHyBzaKYp9zLtqJKrLod/zMNEWpMj488FuQY9QTmqBiyUhI2uh7PVaLqLewXRtdmG6JV0b6T5exyuYRw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/mui-org"
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/icons-material": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.0.tgz",
|
||||
"integrity": "sha512-zHY6fOkaK7VfhWeyxO8MjO3IAjEYpYMXuqUhX7TkUZJ9+TSH/9dn4ClG4K2j6hdgBU5Yrq2Z/89Bo6BHHp7AdQ==",
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.15.1.tgz",
|
||||
"integrity": "sha512-VPJdBSyap6uOxCb5BLbWbkvd6aeJCp1pQZm8DcZBITCH0NOSv8Mz9c8Zvo8xr4Od7+xyWHUAgvRSL4047pL2WQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5"
|
||||
},
|
||||
@@ -661,16 +664,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/joy": {
|
||||
"version": "5.0.0-beta.18",
|
||||
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.18.tgz",
|
||||
"integrity": "sha512-TxEo7kqEnbjB5S8cyFrytWjzhxW12UxkEJOT0QM8WpwaBN3Ie1okFuo2bnFW94vYFZperW97/H/08cqqS/2JPA==",
|
||||
"version": "5.0.0-beta.19",
|
||||
"resolved": "https://registry.npmjs.org/@mui/joy/-/joy-5.0.0-beta.19.tgz",
|
||||
"integrity": "sha512-L3tutkCsJ4pI20LCiQthnXKMIv6GCJqbjPmzCjLvIUwp8kNhgQzauWc4ToYHP7FzZSoDt4HjwY52mvKhVrtytw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@mui/base": "5.0.0-beta.27",
|
||||
"@mui/core-downloads-tracker": "^5.15.0",
|
||||
"@mui/system": "^5.15.0",
|
||||
"@mui/base": "5.0.0-beta.28",
|
||||
"@mui/core-downloads-tracker": "^5.15.1",
|
||||
"@mui/system": "^5.15.1",
|
||||
"@mui/types": "^7.2.11",
|
||||
"@mui/utils": "^5.15.0",
|
||||
"@mui/utils": "^5.15.1",
|
||||
"clsx": "^2.0.0",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
@@ -701,18 +704,18 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/material": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.0.tgz",
|
||||
"integrity": "sha512-60CDI/hQNwJv9a3vEZtFG7zz0USdQhVwpBd3fZqrzhuXSdiMdYMaZcCXeX/KMuNq0ZxQEAZd74Pv+gOb408QVA==",
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/material/-/material-5.15.1.tgz",
|
||||
"integrity": "sha512-WA5DVyvacxDakVyAhNqu/rRT28ppuuUFFw1bLpmRzrCJ4uw/zLTATcd4WB3YbB+7MdZNEGG/SJNWTDLEIyn3xQ==",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@mui/base": "5.0.0-beta.27",
|
||||
"@mui/core-downloads-tracker": "^5.15.0",
|
||||
"@mui/system": "^5.15.0",
|
||||
"@mui/base": "5.0.0-beta.28",
|
||||
"@mui/core-downloads-tracker": "^5.15.1",
|
||||
"@mui/system": "^5.15.1",
|
||||
"@mui/types": "^7.2.11",
|
||||
"@mui/utils": "^5.15.0",
|
||||
"@types/react-transition-group": "^4.4.9",
|
||||
"@mui/utils": "^5.15.1",
|
||||
"@types/react-transition-group": "^4.4.10",
|
||||
"clsx": "^2.0.0",
|
||||
"csstype": "^3.1.2",
|
||||
"prop-types": "^15.8.1",
|
||||
@@ -746,12 +749,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/private-theming": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.0.tgz",
|
||||
"integrity": "sha512-7WxtIhXxNek0JjtsYy+ut2LtFSLpsUW5JSDehQO+jF7itJ8ehy7Bd9bSt2yIllbwGjCFowLfYpPk2Ykgvqm1tA==",
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.15.1.tgz",
|
||||
"integrity": "sha512-wTbzuy5KjSvCPE9UVJktWHJ0b/tD5biavY9wvF+OpYDLPpdXK52vc1hTDxSbdkHIFMkJExzrwO9GvpVAHZBnFQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@mui/utils": "^5.15.0",
|
||||
"@mui/utils": "^5.15.1",
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -772,9 +775,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/styled-engine": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.0.tgz",
|
||||
"integrity": "sha512-6NysIsHkuUS2lF+Lzv1jiK3UjBJk854/vKVcJQVGKlPiqNEVZJNlwaSpsaU5xYXxWEZYfbVFSAomLOS/LV/ovQ==",
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.15.1.tgz",
|
||||
"integrity": "sha512-7WDZTJLqGexWDjqE9oAgjU8ak6hEtUw2yQU7SIYID5kLVO2Nj/Wi/KicbLsXnTsJNvSqePIlUIWTBSXwWJCPZw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@emotion/cache": "^11.11.0",
|
||||
@@ -803,15 +806,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/system": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.0.tgz",
|
||||
"integrity": "sha512-8TPjfTlYBNB7/zBJRL4QOD9kImwdZObbiYNh0+hxvhXr2koezGx8USwPXj8y/JynbzGCkIybkUztCdWlMZe6OQ==",
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/system/-/system-5.15.1.tgz",
|
||||
"integrity": "sha512-LAnP0ls69rqW9eBgI29phIx/lppv+WDGI7b3EJN7VZIqw0RezA0GD7NRpV12BgEYJABEii6z5Q9B5tg7dsX0Iw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@mui/private-theming": "^5.15.0",
|
||||
"@mui/styled-engine": "^5.15.0",
|
||||
"@mui/private-theming": "^5.15.1",
|
||||
"@mui/styled-engine": "^5.15.1",
|
||||
"@mui/types": "^7.2.11",
|
||||
"@mui/utils": "^5.15.0",
|
||||
"@mui/utils": "^5.15.1",
|
||||
"clsx": "^2.0.0",
|
||||
"csstype": "^3.1.2",
|
||||
"prop-types": "^15.8.1"
|
||||
@@ -855,9 +858,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mui/utils": {
|
||||
"version": "5.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.0.tgz",
|
||||
"integrity": "sha512-XSmTKStpKYamewxyJ256+srwEnsT3/6eNo6G7+WC1tj2Iq9GfUJ/6yUoB7YXjOD2jTZ3XobToZm4pVz1LBt6GA==",
|
||||
"version": "5.15.1",
|
||||
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.15.1.tgz",
|
||||
"integrity": "sha512-V1/d0E3Bju5YdB59HJf2G0tnHrFEvWLN+f8hAXp9+JSNy/LC2zKyqUfPPahflR6qsI681P8G9r4mEZte/SrrYA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.5",
|
||||
"@types/prop-types": "^15.7.11",
|
||||
@@ -1088,9 +1091,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/client": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.0.tgz",
|
||||
"integrity": "sha512-cZmglCrfNbYpzUtz7HscVHl38e9CrUs31nrVoGUK1nIPXGgt8hT4jj2s657UXcNdQ/jBUxDgGmHyu2Nyrq1txg==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.7.1.tgz",
|
||||
"integrity": "sha512-TUSa4nUcC4nf/e7X3jyO1pEd6XcI/TLRCA0KjkA46RDIpxUaRsBYEOqITwXRW2c0bMFyKcCRXrH4f7h4q9oOlg==",
|
||||
"hasInstallScript": true,
|
||||
"engines": {
|
||||
"node": ">=16.13"
|
||||
@@ -1105,54 +1108,54 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/debug": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.0.tgz",
|
||||
"integrity": "sha512-tZ+MOjWlVvz1kOEhNYMa4QUGURY+kgOUBqLHYIV8jmCsMuvA1tWcn7qtIMLzYWCbDcQT4ZS8xDgK0R2gl6/0wA==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.7.1.tgz",
|
||||
"integrity": "sha512-yrVSO/YZOxdeIxcBtZ5BaNqUfPrZkNsAKQIQg36cJKMxj/VYK3Vk5jMKkI+gQLl0KReo1YvX8GWKfV788SELjw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/engines": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.0.tgz",
|
||||
"integrity": "sha512-TkOMgMm60n5YgEKPn9erIvFX2/QuWnl3GBo6yTRyZKk5O5KQertXiNnrYgSLy0SpsKmhovEPQb+D4l0SzyE7XA==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.7.1.tgz",
|
||||
"integrity": "sha512-R+Pqbra8tpLP2cvyiUpx+SIKglav3nTCpA+rn6826CThviQ8yvbNG0s8jNpo51vS9FuZO3pOkARqG062vKX7uA==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.0",
|
||||
"@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
|
||||
"@prisma/fetch-engine": "5.7.0",
|
||||
"@prisma/get-platform": "5.7.0"
|
||||
"@prisma/debug": "5.7.1",
|
||||
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"@prisma/fetch-engine": "5.7.1",
|
||||
"@prisma/get-platform": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/engines-version": {
|
||||
"version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9.tgz",
|
||||
"integrity": "sha512-V6tgRVi62jRwTm0Hglky3Scwjr/AKFBFtS+MdbsBr7UOuiu1TKLPc6xfPiyEN1+bYqjEtjxwGsHgahcJsd1rNg==",
|
||||
"version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5.tgz",
|
||||
"integrity": "sha512-dIR5IQK/ZxEoWRBDOHF87r1Jy+m2ih3Joi4vzJRP+FOj5yxCwS2pS5SBR3TWoVnEK1zxtLI/3N7BjHyGF84fgw==",
|
||||
"devOptional": true
|
||||
},
|
||||
"node_modules/@prisma/fetch-engine": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.0.tgz",
|
||||
"integrity": "sha512-zIn/qmO+N/3FYe7/L9o+yZseIU8ivh4NdPKSkQRIHfg2QVTVMnbhGoTcecbxfVubeTp+DjcbjS0H9fCuM4W04w==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.7.1.tgz",
|
||||
"integrity": "sha512-9ELauIEBkIaEUpMIYPRlh5QELfoC6pyHolHVQgbNxglaINikZ9w9X7r1TIePAcm05pCNp2XPY1ObQIJW5nYfBQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.0",
|
||||
"@prisma/engines-version": "5.7.0-41.79fb5193cf0a8fdbef536e4b4a159cad677ab1b9",
|
||||
"@prisma/get-platform": "5.7.0"
|
||||
"@prisma/debug": "5.7.1",
|
||||
"@prisma/engines-version": "5.7.1-1.0ca5ccbcfa6bdc81c003cf549abe4269f59c41e5",
|
||||
"@prisma/get-platform": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@prisma/get-platform": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.0.tgz",
|
||||
"integrity": "sha512-ZeV/Op4bZsWXuw5Tg05WwRI8BlKiRFhsixPcAM+5BKYSiUZiMKIi713tfT3drBq8+T0E1arNZgYSA9QYcglWNA==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.7.1.tgz",
|
||||
"integrity": "sha512-eDlswr3a1m5z9D/55Iyt/nZqS5UpD+DZ9MooBB3hvrcPhDQrcf9m4Tl7buy4mvAtrubQ626ECtb8c6L/f7rGSQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@prisma/debug": "5.7.0"
|
||||
"@prisma/debug": "5.7.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rushstack/eslint-patch": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.0.tgz",
|
||||
"integrity": "sha512-2/U3GXA6YiPYQDLGwtGlnNgKYBSwCFIHf8Y9LUY5VATHdtbLlU0Y1R3QoBnT0aB4qv/BEiVVsj7LJXoQCgJ2vA==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.6.1.tgz",
|
||||
"integrity": "sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sanity/diff-match-patch": {
|
||||
@@ -1343,14 +1346,20 @@
|
||||
"integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g=="
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz",
|
||||
"integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==",
|
||||
"version": "20.10.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.5.tgz",
|
||||
"integrity": "sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/nprogress": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/nprogress/-/nprogress-0.2.3.tgz",
|
||||
"integrity": "sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
|
||||
@@ -1387,9 +1396,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz",
|
||||
"integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==",
|
||||
"version": "18.2.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.18.tgz",
|
||||
"integrity": "sha512-TJxDm6OfAX2KJWJdMEVTwWke5Sc/E/RlnPGvGfS0W7+6ocy2xhDVQVh/KvC2Uf7kACs+gDytdusDSdWfWkaNzw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
@@ -1405,9 +1414,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-timeago": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.6.tgz",
|
||||
"integrity": "sha512-BFUH7FEple9m0K8dNvOqhba3+iCyrgsLcRtAnaZ6HlXvG8AJfC/7NCDcLaXfB1jvpAezwDRi/BflzdaLI4+Fow==",
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-timeago/-/react-timeago-4.1.7.tgz",
|
||||
"integrity": "sha512-ogD4Ror/hDG+pQggCX+TgPgJ8W2jeeUxsgNU485Qpm0Ma+E2TND2EJuKwK5+sxlkDXDEgsHradO0zWBkTgLzNg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
@@ -1439,15 +1448,15 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz",
|
||||
"integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.15.0.tgz",
|
||||
"integrity": "sha512-MkgKNnsjC6QwcMdlNAel24jjkEO/0hQaMDLqP4S9zq5HBAUJNQB6y+3DwLjX7b3l2b37eNAxMPLwb3/kh8VKdA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "6.14.0",
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/typescript-estree": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0",
|
||||
"@typescript-eslint/scope-manager": "6.15.0",
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"@typescript-eslint/typescript-estree": "6.15.0",
|
||||
"@typescript-eslint/visitor-keys": "6.15.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1467,13 +1476,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz",
|
||||
"integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.15.0.tgz",
|
||||
"integrity": "sha512-+BdvxYBltqrmgCNu4Li+fGDIkW9n//NrruzG9X1vBzaNK+ExVXPoGB71kneaVw/Jp+4rH/vaMAGC6JfMbHstVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0"
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"@typescript-eslint/visitor-keys": "6.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1484,9 +1493,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz",
|
||||
"integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.15.0.tgz",
|
||||
"integrity": "sha512-yXjbt//E4T/ee8Ia1b5mGlbNj9fB9lJP4jqLbZualwpP2BCQ5is6BcWwxpIsY4XKAhmdv3hrW92GdtJbatC6dQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^16.0.0 || >=18.0.0"
|
||||
@@ -1497,13 +1506,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz",
|
||||
"integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.15.0.tgz",
|
||||
"integrity": "sha512-7mVZJN7Hd15OmGuWrp2T9UvqR2Ecg+1j/Bp1jXUEY2GZKV6FXlOIoqVDmLpBiEiq3katvj/2n2mR0SDwtloCew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/visitor-keys": "6.14.0",
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"@typescript-eslint/visitor-keys": "6.15.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
@@ -1524,12 +1533,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz",
|
||||
"integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==",
|
||||
"version": "6.15.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.15.0.tgz",
|
||||
"integrity": "sha512-1zvtdC1a9h5Tb5jU9x3ADNXO9yjP8rXlaoChu0DQX40vf5ACVpYIVIZhIMZ6d5sDXH7vq4dsZBT1fEGj8D2n2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "6.14.0",
|
||||
"@typescript-eslint/types": "6.15.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1970,9 +1979,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001568",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001568.tgz",
|
||||
"integrity": "sha512-vSUkH84HontZJ88MiNrOau1EBrCqEQYgkC5gIySiDlpsm8sGVrhU7Kx4V6h0tnqaHzIHZv08HlJIwPbL4XL9+A==",
|
||||
"version": "1.0.30001571",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001571.tgz",
|
||||
"integrity": "sha512-tYq/6MoXhdezDLFZuCO/TKboTzuQ/xR5cFdgXPfDtM7/kchBO3b4VWghE/OAi/DV7tTdhmLjZiZBZi1fA/GheQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -2563,15 +2572,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint": {
|
||||
"version": "8.55.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
|
||||
"integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
|
||||
"version": "8.56.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz",
|
||||
"integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.6.1",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"@eslint/js": "8.55.0",
|
||||
"@eslint/js": "8.56.0",
|
||||
"@humanwhocodes/config-array": "^0.11.13",
|
||||
"@humanwhocodes/module-importer": "^1.0.1",
|
||||
"@nodelib/fs.walk": "^1.2.8",
|
||||
@@ -2715,9 +2724,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import": {
|
||||
"version": "2.29.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.0.tgz",
|
||||
"integrity": "sha512-QPOO5NO6Odv5lpoTkddtutccQjysJuFxoPS7fAHO+9m9udNHvTCPSAMW9zGAYj8lAIdr40I8yPCdUYrncXtrwg==",
|
||||
"version": "2.29.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
|
||||
"integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.7",
|
||||
@@ -2736,7 +2745,7 @@
|
||||
"object.groupby": "^1.0.1",
|
||||
"object.values": "^1.1.7",
|
||||
"semver": "^6.3.1",
|
||||
"tsconfig-paths": "^3.14.2"
|
||||
"tsconfig-paths": "^3.15.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -3050,9 +3059,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz",
|
||||
"integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz",
|
||||
"integrity": "sha512-ifCoaXsDrsdkWTtiNJX5uzHDsrck5TzfKKDcuFFTIrrc/BS076qgEIfoIy1VeZqViznfKiysPYTh/QeHtnIsYA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -5329,6 +5338,11 @@
|
||||
"set-blocking": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nprogress": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz",
|
||||
"integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA=="
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5705,13 +5719,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prisma": {
|
||||
"version": "5.7.0",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.0.tgz",
|
||||
"integrity": "sha512-0rcfXO2ErmGAtxnuTNHQT9ztL0zZheQjOI/VNJzdq87C3TlGPQtMqtM+KCwU6XtmkoEr7vbCQqA7HF9IY0ST+Q==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.7.1.tgz",
|
||||
"integrity": "sha512-ekho7ziH0WEJvC4AxuJz+ewRTMskrebPcrKuBwcNzVDniYxx+dXOGcorNeIb9VEMO5vrKzwNYvhD271Ui2jnNw==",
|
||||
"devOptional": true,
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@prisma/engines": "5.7.0"
|
||||
"@prisma/engines": "5.7.1"
|
||||
},
|
||||
"bin": {
|
||||
"prisma": "build/index.js"
|
||||
@@ -5861,6 +5875,15 @@
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-resizable-panels": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-1.0.5.tgz",
|
||||
"integrity": "sha512-OP0whNQCko+f4BgoptGaeIc7StBRyeMeJ+8r/7rXACBDf9W5EcMWuM32hfqPDMenS2HFy/eZVi/r8XqK+ZIEag==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-ssr-prepass": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
|
||||
@@ -5925,9 +5948,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/regenerator-runtime": {
|
||||
"version": "0.14.0",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/regexp.prototype.flags": {
|
||||
"version": "1.5.1",
|
||||
@@ -6688,9 +6711,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tsconfig-paths": {
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
|
||||
"integrity": "sha512-o/9iXgCYc5L/JxCHPe3Hvh8Q/2xm5Z+p18PESBU6Ff33695QnCHBEjcytY2q19ua7Mbl/DavtBOLq+oG0RCL+g==",
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||
"integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/json5": "^0.0.29",
|
||||
|
||||
+12
-9
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.8.0",
|
||||
"version": "1.9.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -18,10 +18,10 @@
|
||||
"@emotion/react": "^11.11.1",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.0",
|
||||
"@mui/joy": "^5.0.0-beta.18",
|
||||
"@mui/icons-material": "^5.15.1",
|
||||
"@mui/joy": "^5.0.0-beta.19",
|
||||
"@next/bundle-analyzer": "^14.0.4",
|
||||
"@prisma/client": "^5.7.0",
|
||||
"@prisma/client": "^5.7.1",
|
||||
"@sanity/diff-match-patch": "^3.1.1",
|
||||
"@t3-oss/env-nextjs": "^0.7.1",
|
||||
"@tanstack/react-query": "~4.36.1",
|
||||
@@ -34,6 +34,7 @@
|
||||
"eventsource-parser": "^1.1.1",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"next": "^14.0.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "4.0.269",
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
@@ -41,6 +42,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-resizable-panels": "^1.0.5",
|
||||
"react-timeago": "^7.2.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"superjson": "^2.2.1",
|
||||
@@ -51,18 +53,19 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/puppeteer": "^0.0.5",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.45",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.6",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/uuid": "^9.0.7",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-next": "^14.0.4",
|
||||
"prettier": "^3.1.1",
|
||||
"prisma": "^5.7.0",
|
||||
"prisma": "^5.7.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"engines": {
|
||||
|
||||
+15
-12
@@ -10,11 +10,12 @@ import 'katex/dist/katex.min.css';
|
||||
import '~/common/styles/CodePrism.css';
|
||||
import '~/common/styles/GithubMarkdown.css';
|
||||
|
||||
import { ProviderBackend } from '~/common/state/ProviderBackend';
|
||||
import { ProviderSingleTab } from '~/common/state/ProviderSingleTab';
|
||||
import { ProviderSnacks } from '~/common/state/ProviderSnacks';
|
||||
import { ProviderTRPCQueryClient } from '~/common/state/ProviderTRPCQueryClient';
|
||||
import { ProviderTheming } from '~/common/state/ProviderTheming';
|
||||
import { ProviderBackendAndNoSSR } from '~/common/providers/ProviderBackendAndNoSSR';
|
||||
import { ProviderBootstrapLogic } from '~/common/providers/ProviderBootstrapLogic';
|
||||
import { ProviderSingleTab } from '~/common/providers/ProviderSingleTab';
|
||||
import { ProviderSnacks } from '~/common/providers/ProviderSnacks';
|
||||
import { ProviderTRPCQueryClient } from '~/common/providers/ProviderTRPCQueryClient';
|
||||
import { ProviderTheming } from '~/common/providers/ProviderTheming';
|
||||
|
||||
|
||||
const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
|
||||
@@ -27,13 +28,15 @@ const MyApp = ({ Component, emotionCache, pageProps }: MyAppProps) =>
|
||||
|
||||
<ProviderTheming emotionCache={emotionCache}>
|
||||
<ProviderSingleTab>
|
||||
<ProviderTRPCQueryClient>
|
||||
<ProviderSnacks>
|
||||
<ProviderBackend>
|
||||
<Component {...pageProps} />
|
||||
</ProviderBackend>
|
||||
</ProviderSnacks>
|
||||
</ProviderTRPCQueryClient>
|
||||
<ProviderBootstrapLogic>
|
||||
<ProviderTRPCQueryClient>
|
||||
<ProviderSnacks>
|
||||
<ProviderBackendAndNoSSR>
|
||||
<Component {...pageProps} />
|
||||
</ProviderBackendAndNoSSR>
|
||||
</ProviderSnacks>
|
||||
</ProviderTRPCQueryClient>
|
||||
</ProviderBootstrapLogic>
|
||||
</ProviderSingleTab>
|
||||
</ProviderTheming>
|
||||
|
||||
|
||||
+2
-6
@@ -2,13 +2,9 @@ import * as React from 'react';
|
||||
|
||||
import { AppCall } from '../src/apps/call/AppCall';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function CallPage() {
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppCall />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'optima' }, <AppCall />);
|
||||
}
|
||||
+5
-9
@@ -1,18 +1,14 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { AppChat } from '../src/apps/chat/AppChat';
|
||||
import { useShowNewsOnUpdate } from '../src/apps/news/news.hooks';
|
||||
import { useRedirectToNewsOnUpdates } from '../src/apps/news/news.hooks';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function ChatPage() {
|
||||
// show the News page on updates
|
||||
useShowNewsOnUpdate();
|
||||
// show the News page if there are unseen updates
|
||||
useRedirectToNewsOnUpdates();
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppChat />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'optima' }, <AppChat />);
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import { Box, Typography } from '@mui/joy';
|
||||
|
||||
import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { apiQuery } from '~/common/util/trpc.client';
|
||||
import { navigateToIndex } from '~/common/app.routes';
|
||||
import { openLayoutModelsSetup } from '~/common/layout/store-applayout';
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
|
||||
@@ -36,14 +36,14 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
|
||||
useModelsStore.getState().setOpenRoutersKey(openRouterKey);
|
||||
|
||||
// 2. Navigate to the chat app
|
||||
navigateToIndex(true).then(() => openLayoutModelsSetup());
|
||||
void navigateToIndex(true); //.then(openModelsSetup);
|
||||
|
||||
}, [isSuccess, openRouterKey]);
|
||||
|
||||
return (
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level1',
|
||||
backgroundColor: themeBgApp,
|
||||
overflowY: 'auto',
|
||||
display: 'flex', justifyContent: 'center',
|
||||
p: { xs: 3, md: 6 },
|
||||
@@ -84,15 +84,11 @@ function CallbackOpenRouterPage(props: { openRouterCode: string | undefined }) {
|
||||
* Docs: https://openrouter.ai/docs#oauth
|
||||
* Example URL: https://localhost:3000/link/callback_openrouter?code=SomeCode
|
||||
*/
|
||||
export default function Page() {
|
||||
export default function CallbackPage() {
|
||||
|
||||
// get the 'code=...' from the URL
|
||||
const { query } = useRouter();
|
||||
const { code: openRouterCode } = query;
|
||||
|
||||
return (
|
||||
<AppLayout suspendAutoModelsSetup>
|
||||
<CallbackOpenRouterPage openRouterCode={openRouterCode as (string | undefined)} />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'plain' }, <CallbackOpenRouterPage openRouterCode={openRouterCode as (string | undefined)} />);
|
||||
}
|
||||
@@ -3,16 +3,12 @@ import { useRouter } from 'next/router';
|
||||
|
||||
import { AppChatLink } from '../../../src/apps/link/AppChatLink';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function ChatLinkPage() {
|
||||
const { query } = useRouter();
|
||||
const chatLinkId = query?.chatLinkId as string ?? '';
|
||||
|
||||
return (
|
||||
<AppLayout suspendAutoModelsSetup>
|
||||
<AppChatLink linkId={chatLinkId} />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppChatLink linkId={chatLinkId} />);
|
||||
}
|
||||
@@ -8,10 +8,11 @@ import { setComposerStartupText } from '../../src/apps/chat/components/composer/
|
||||
|
||||
import { callBrowseFetchPage } from '~/modules/browse/browse.client';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { LogoProgress } from '~/common/components/LogoProgress';
|
||||
import { asValidURL } from '~/common/util/urlUtils';
|
||||
import { navigateToIndex } from '~/common/app.routes';
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
/**
|
||||
@@ -90,7 +91,7 @@ function AppShareTarget() {
|
||||
return (
|
||||
|
||||
<Box sx={{
|
||||
backgroundColor: 'background.level2',
|
||||
backgroundColor: themeBgApp,
|
||||
display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
@@ -132,10 +133,6 @@ function AppShareTarget() {
|
||||
* This page will be invoked on mobile when sharing Text/URLs/Files from other APPs
|
||||
* Example URL: https://localhost:3000/link/share_target?title=This+Title&text=https%3A%2F%2Fexample.com%2Fapp%2Fpath
|
||||
*/
|
||||
export default function LaunchPage() {
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppShareTarget />
|
||||
</AppLayout>
|
||||
);
|
||||
export default function ShareTargetPage() {
|
||||
return withLayout({ type: 'plain' }, <AppShareTarget />);
|
||||
}
|
||||
+3
-7
@@ -3,16 +3,12 @@ import * as React from 'react';
|
||||
import { AppNews } from '../src/apps/news/AppNews';
|
||||
import { useMarkNewsAsSeen } from '../src/apps/news/news.hooks';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function NewsPage() {
|
||||
// update the last seen news version
|
||||
// 'touch' the last seen news version
|
||||
useMarkNewsAsSeen();
|
||||
|
||||
return (
|
||||
<AppLayout suspendAutoModelsSetup>
|
||||
<AppNews />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'optima', suspendAutoModelsSetup: true }, <AppNews />);
|
||||
}
|
||||
+2
-6
@@ -2,13 +2,9 @@ import * as React from 'react';
|
||||
|
||||
import { AppPersonas } from '../src/apps/personas/AppPersonas';
|
||||
|
||||
import { AppLayout } from '~/common/layout/AppLayout';
|
||||
import { withLayout } from '~/common/layout/withLayout';
|
||||
|
||||
|
||||
export default function PersonasPage() {
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppPersonas />
|
||||
</AppLayout>
|
||||
);
|
||||
return withLayout({ type: 'optima' }, <AppPersonas />);
|
||||
}
|
||||
@@ -22,12 +22,13 @@ import { Link } from '~/common/components/Link';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { conversationTitle, createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { playSoundUrl, usePlaySoundUrl } from '~/common/util/audioUtils';
|
||||
import { useLayoutPluggable } from '~/common/layout/store-applayout';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { CallAvatar } from './components/CallAvatar';
|
||||
import { CallButton } from './components/CallButton';
|
||||
import { CallMessage } from './components/CallMessage';
|
||||
import { CallStatus } from './components/CallStatus';
|
||||
import { ROUTE_APP_CHAT } from '~/common/app.routes';
|
||||
|
||||
|
||||
function CallMenuItems(props: {
|
||||
@@ -178,7 +179,7 @@ export function CallUI(props: {
|
||||
case 'Goodbye.':
|
||||
setStage('ended');
|
||||
setTimeout(() => {
|
||||
void routerPush('/');
|
||||
void routerPush(ROUTE_APP_CHAT);
|
||||
}, 2000);
|
||||
return;
|
||||
// command: regenerate answer
|
||||
@@ -272,7 +273,7 @@ export function CallUI(props: {
|
||||
, [overridePersonaVoice, pushToTalk],
|
||||
);
|
||||
|
||||
useLayoutPluggable(chatLLMDropdown, null, menuItems);
|
||||
usePluggableOptimaLayout(null, chatLLMDropdown, menuItems, 'CallUI');
|
||||
|
||||
|
||||
return <>
|
||||
@@ -366,7 +367,7 @@ export function CallUI(props: {
|
||||
)}
|
||||
|
||||
{/* [ended] Back / Call Again */}
|
||||
{(isEnded || isDeclined) && <Link noLinkStyle href='/'><CallButton Icon={ArrowBackIcon} text='Back' variant='soft' /></Link>}
|
||||
{(isEnded || isDeclined) && <Link noLinkStyle href={ROUTE_APP_CHAT}><CallButton Icon={ArrowBackIcon} text='Back' variant='soft' /></Link>}
|
||||
{(isEnded || isDeclined) && <CallButton Icon={CallIcon} text='Call Again' color='success' variant='soft' onClick={() => setStage('connected')} />}
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -11,9 +11,9 @@ import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
|
||||
import { navigateBack } from '~/common/app.routes';
|
||||
import { openLayoutPreferences } from '~/common/layout/store-applayout';
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUICounter } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
|
||||
const [recognitionOverride, setRecognitionOverride] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const { openPreferences } = useOptimaLayout();
|
||||
const recognition = useCapabilityBrowserSpeechRecognition();
|
||||
const synthesis = useCapabilityElevenLabs();
|
||||
const chatIsEmpty = useChatStore(state => {
|
||||
@@ -103,7 +104,7 @@ export function CallWizard(props: { strict?: boolean, conversationId: string, ch
|
||||
const handleOverrideRecognition = () => setRecognitionOverride(true);
|
||||
|
||||
const handleConfigureElevenLabs = () => {
|
||||
openLayoutPreferences(3);
|
||||
openPreferences(3);
|
||||
};
|
||||
|
||||
const handleFinishButton = () => {
|
||||
|
||||
+147
-86
@@ -1,11 +1,12 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box } from '@mui/joy';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import { Box, useTheme } from '@mui/joy';
|
||||
|
||||
import { CmdRunBrowse } from '~/modules/browse/browse.client';
|
||||
import { CmdRunProdia } from '~/modules/prodia/prodia.client';
|
||||
import { CmdRunReact } from '~/modules/aifn/react/react';
|
||||
import { CmdRunT2I, useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
|
||||
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
|
||||
@@ -18,7 +19,8 @@ import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useConversation } from '~/common/state/store-chats';
|
||||
import { openLayoutLLMOptions, useLayoutPluggable } from '~/common/layout/store-applayout';
|
||||
import { themeBgApp, themeBgAppChatComposer } from '~/common/app.theme';
|
||||
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
|
||||
@@ -29,7 +31,9 @@ import { ChatMessageList } from './components/ChatMessageList';
|
||||
import { CmdAddRoleMessage, CmdHelp, createCommandsHelpMessage, extractCommands } from './editors/commands';
|
||||
import { Composer } from './components/composer/Composer';
|
||||
import { Ephemerals } from './components/Ephemerals';
|
||||
import { usePanesManager } from './components/usePanesManager';
|
||||
import { ScrollToBottom } from './components/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from './components/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { usePanesManager } from './components/panes/usePanesManager';
|
||||
|
||||
import { runAssistantUpdatingState } from './editors/chat-stream';
|
||||
import { runBrowseUpdatingState } from './editors/browse-load';
|
||||
@@ -40,7 +44,11 @@ import { runReActUpdatingState } from './editors/react-tangent';
|
||||
/**
|
||||
* Mode: how to treat the input from the Composer
|
||||
*/
|
||||
export type ChatModeId = 'immediate' | 'write-user' | 'react' | 'draw-imagine' | 'draw-imagine-plus';
|
||||
export type ChatModeId =
|
||||
| 'generate-text'
|
||||
| 'append-user'
|
||||
| 'generate-image'
|
||||
| 'generate-react';
|
||||
|
||||
|
||||
const SPECIAL_ID_WIPE_ALL: DConversationId = 'wipe-chats';
|
||||
@@ -58,6 +66,10 @@ export function AppChat() {
|
||||
const composerTextAreaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
// external state
|
||||
const theme = useTheme();
|
||||
|
||||
const { openLlmOptions } = useOptimaLayout();
|
||||
|
||||
const { chatLLM } = useChatLLM();
|
||||
|
||||
const {
|
||||
@@ -66,7 +78,10 @@ export function AppChat() {
|
||||
navigateHistoryInFocusedPane,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
setFocusedPaneIndex,
|
||||
paneIndex,
|
||||
duplicatePane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
} = usePanesManager();
|
||||
|
||||
const {
|
||||
@@ -83,14 +98,13 @@ export function AppChat() {
|
||||
setMessages,
|
||||
} = useConversation(focusedConversationId);
|
||||
|
||||
const { mayWork: capabilityHasT2I } = useCapabilityTextToImage();
|
||||
|
||||
|
||||
// Window actions
|
||||
|
||||
const chatPaneIDs = chatPanes.length > 0 ? chatPanes.map(pane => pane.conversationId) : [null];
|
||||
|
||||
const setActivePaneIndex = React.useCallback((idx: number) => {
|
||||
setFocusedPaneIndex(idx);
|
||||
}, [setFocusedPaneIndex]);
|
||||
const panesConversationIDs = chatPanes.length > 0 ? chatPanes.map(pane => pane.conversationId) : [null];
|
||||
const isSplitPane = chatPanes.length > 1;
|
||||
|
||||
const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => {
|
||||
conversationId && openConversationInFocusedPane(conversationId);
|
||||
@@ -100,6 +114,13 @@ export function AppChat() {
|
||||
conversationId && openConversationInSplitPane(conversationId);
|
||||
}, [openConversationInSplitPane]);
|
||||
|
||||
const toggleSplitPane = React.useCallback(() => {
|
||||
if (isSplitPane)
|
||||
removePane(paneIndex ?? chatPanes.length - 1);
|
||||
else
|
||||
duplicatePane(paneIndex ?? chatPanes.length - 1);
|
||||
}, [chatPanes.length, duplicatePane, isSplitPane, paneIndex, removePane]);
|
||||
|
||||
const handleNavigateHistory = React.useCallback((direction: 'back' | 'forward') => {
|
||||
if (navigateHistoryInFocusedPane(direction))
|
||||
showNextTitle.current = true;
|
||||
@@ -127,7 +148,7 @@ export function AppChat() {
|
||||
const pieces = extractCommands(lastMessage.text);
|
||||
if (pieces.length == 2 && pieces[0].type === 'cmd' && pieces[1].type === 'text') {
|
||||
const [command, prompt] = [pieces[0].value, pieces[1].value];
|
||||
if (CmdRunProdia.includes(command)) {
|
||||
if (CmdRunT2I.includes(command)) {
|
||||
setMessages(conversationId, history);
|
||||
return await runImageGenerationUpdatingState(conversationId, prompt);
|
||||
}
|
||||
@@ -154,27 +175,26 @@ export function AppChat() {
|
||||
// synchronous long-duration tasks, which update the state as they go
|
||||
if (chatLLMId && focusedSystemPurposeId) {
|
||||
switch (chatModeId) {
|
||||
case 'immediate':
|
||||
case 'generate-text':
|
||||
return await runAssistantUpdatingState(conversationId, history, chatLLMId, focusedSystemPurposeId);
|
||||
case 'write-user':
|
||||
|
||||
case 'append-user':
|
||||
return setMessages(conversationId, history);
|
||||
case 'react':
|
||||
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
|
||||
...message,
|
||||
text: `${CmdRunT2I[0]} ${lastMessage.text}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(conversationId, lastMessage.text);
|
||||
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
setMessages(conversationId, history);
|
||||
return await runReActUpdatingState(conversationId, lastMessage.text, chatLLMId);
|
||||
case 'draw-imagine':
|
||||
case 'draw-imagine-plus':
|
||||
if (!lastMessage?.text)
|
||||
break;
|
||||
const imagePrompt = chatModeId == 'draw-imagine-plus'
|
||||
? await imaginePromptFromText(lastMessage.text) || 'An error sign.'
|
||||
: lastMessage.text;
|
||||
setMessages(conversationId, history.map(message => message.id !== lastMessage.id ? message : {
|
||||
...message,
|
||||
text: `${CmdRunProdia[0]} ${imagePrompt}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(conversationId, imagePrompt);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,13 +233,13 @@ export function AppChat() {
|
||||
};
|
||||
|
||||
const handleConversationExecuteHistory = async (conversationId: DConversationId, history: DMessage[]) =>
|
||||
await _handleExecute('immediate', conversationId, history);
|
||||
await _handleExecute('generate-text', conversationId, history);
|
||||
|
||||
const handleMessageRegenerateLast = React.useCallback(async () => {
|
||||
const focusedConversation = getConversation(focusedConversationId);
|
||||
if (focusedConversation?.messages?.length) {
|
||||
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
|
||||
return await _handleExecute('immediate', focusedConversation.id, lastMessage.role === 'assistant'
|
||||
return await _handleExecute('generate-text', focusedConversation.id, lastMessage.role === 'assistant'
|
||||
? focusedConversation.messages.slice(0, -1)
|
||||
: [...focusedConversation.messages],
|
||||
);
|
||||
@@ -228,13 +248,15 @@ export function AppChat() {
|
||||
|
||||
const handleTextDiagram = async (diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig);
|
||||
|
||||
const handleTextImaginePlus = async (conversationId: DConversationId, messageText: string) => {
|
||||
const handleTextImagine = async (conversationId: DConversationId, messageText: string) => {
|
||||
const conversation = getConversation(conversationId);
|
||||
if (conversation)
|
||||
return await _handleExecute('draw-imagine-plus', conversationId, [
|
||||
...conversation.messages,
|
||||
createDMessage('user', messageText),
|
||||
]);
|
||||
if (!conversation)
|
||||
return;
|
||||
const imaginedPrompt = await imaginePromptFromText(messageText) || 'An error sign.';
|
||||
return await _handleExecute('generate-image', conversationId, [
|
||||
...conversation.messages,
|
||||
createDMessage('user', imaginedPrompt),
|
||||
]);
|
||||
};
|
||||
|
||||
const handleTextSpeak = async (text: string) => {
|
||||
@@ -317,8 +339,8 @@ export function AppChat() {
|
||||
const handleOpenChatLlmOptions = React.useCallback(() => {
|
||||
const { chatLLMId } = useModelsStore.getState();
|
||||
if (!chatLLMId) return;
|
||||
openLayoutLLMOptions(chatLLMId);
|
||||
}, []);
|
||||
openLlmOptions(chatLLMId);
|
||||
}, [openLlmOptions]);
|
||||
|
||||
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
|
||||
['o', true, true, false, handleOpenChatLlmOptions],
|
||||
@@ -336,8 +358,12 @@ export function AppChat() {
|
||||
// Pluggable ApplicationBar components
|
||||
|
||||
const centerItems = React.useMemo(() =>
|
||||
<ChatDropdowns conversationId={focusedConversationId} />,
|
||||
[focusedConversationId],
|
||||
<ChatDropdowns
|
||||
conversationId={focusedConversationId}
|
||||
isSplitPanes={isSplitPane}
|
||||
onToggleSplitPanes={toggleSplitPane}
|
||||
/>,
|
||||
[focusedConversationId, isSplitPane, toggleSplitPane],
|
||||
);
|
||||
|
||||
const drawerItems = React.useMemo(() =>
|
||||
@@ -368,72 +394,107 @@ export function AppChat() {
|
||||
[areChatsEmpty, focusedConversationId, handleConversationBranch, isFocusedChatEmpty, isMessageSelectionMode],
|
||||
);
|
||||
|
||||
useLayoutPluggable(centerItems, drawerItems, menuItems);
|
||||
usePluggableOptimaLayout(drawerItems, centerItems, menuItems, 'AppChat');
|
||||
|
||||
return <>
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex', flexDirection: { xs: 'column', md: 'row' },
|
||||
overflow: 'clip',
|
||||
}}>
|
||||
<PanelGroup direction='horizontal'>
|
||||
|
||||
{chatPaneIDs.map((_conversationId, idx) => (
|
||||
<Box key={'chat-pane-' + idx} onClick={() => setActivePaneIndex(idx)} sx={{
|
||||
flexGrow: 1, flexBasis: 1,
|
||||
display: 'flex', flexDirection: 'column',
|
||||
overflow: 'clip',
|
||||
}}>
|
||||
{panesConversationIDs.map((_conversationId, idx, panels) => <React.Fragment key={`chat-pane-${idx}-${panels.length}-${_conversationId}`}>
|
||||
|
||||
<ChatMessageList
|
||||
conversationId={_conversationId}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImaginePlus}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
<Panel
|
||||
id={'chat-pane-' + _conversationId}
|
||||
order={idx}
|
||||
collapsible
|
||||
defaultSize={panels.length > 0 ? Math.round(100 / panels.length) : undefined}
|
||||
minSize={20}
|
||||
onClick={() => setFocusedPane(idx)}
|
||||
onCollapse={() => setTimeout(() => removePane(idx), 50)}
|
||||
style={{
|
||||
// for anchoring the scroll button in place
|
||||
position: 'relative',
|
||||
// border only for active pane (if two or more panes)
|
||||
...(panesConversationIDs.length < 2 ? {}
|
||||
: (_conversationId === focusedConversationId)
|
||||
? { border: `2px solid ${theme.palette.primary.solidBg}` }
|
||||
: { border: `2px solid ${theme.palette.background.level1}` }),
|
||||
}}
|
||||
>
|
||||
|
||||
<ScrollToBottom
|
||||
bootToBottom
|
||||
stickToBottom
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level1',
|
||||
// allows the content to be scrolled (all browsers)
|
||||
overflowY: 'auto',
|
||||
minHeight: 96,
|
||||
// outline the current focused pane
|
||||
...(chatPaneIDs.length < 2 ? {}
|
||||
: (_conversationId === focusedConversationId)
|
||||
? {
|
||||
border: '2px solid',
|
||||
borderColor: 'primary.solidBg',
|
||||
} : {
|
||||
padding: '2px',
|
||||
}),
|
||||
// actually make sure this scrolls & fills
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
|
||||
<Ephemerals
|
||||
conversationId={_conversationId}
|
||||
sx={{
|
||||
// flexGrow: 0.1,
|
||||
flexShrink: 0.5,
|
||||
overflowY: 'auto',
|
||||
minHeight: 64,
|
||||
<ChatMessageList
|
||||
conversationId={_conversationId}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
sx={{
|
||||
backgroundColor: themeBgApp,
|
||||
minHeight: '100%', // ensures filling of the blank space on newer chats
|
||||
}}
|
||||
/>
|
||||
|
||||
<Ephemerals
|
||||
conversationId={_conversationId}
|
||||
sx={{
|
||||
// TODO: Fixme post panels?
|
||||
// flexGrow: 0.1,
|
||||
flexShrink: 0.5,
|
||||
overflowY: 'auto',
|
||||
minHeight: 64,
|
||||
}} />
|
||||
|
||||
{/* Visibility and actions are handled via Context */}
|
||||
<ScrollToBottomButton />
|
||||
|
||||
</ScrollToBottom>
|
||||
|
||||
</Panel>
|
||||
|
||||
{/* Panel Separators & Resizers */}
|
||||
{idx < panels.length - 1 && (
|
||||
<PanelResizeHandle>
|
||||
<Box sx={{
|
||||
backgroundColor: themeBgApp,
|
||||
height: '100%',
|
||||
width: '4px',
|
||||
'&:hover': {
|
||||
backgroundColor: 'primary.softActiveBg',
|
||||
},
|
||||
}} />
|
||||
</PanelResizeHandle>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</React.Fragment>)}
|
||||
|
||||
</PanelGroup>
|
||||
|
||||
<Composer
|
||||
chatLLM={chatLLM}
|
||||
composerTextAreaRef={composerTextAreaRef}
|
||||
conversationId={focusedConversationId}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
isDeveloperMode={focusedSystemPurposeId === 'Developer'}
|
||||
onAction={handleComposerAction}
|
||||
onTextImagine={handleTextImagine}
|
||||
sx={{
|
||||
zIndex: 21, // position: 'sticky', bottom: 0,
|
||||
backgroundColor: 'background.surface',
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
borderTop: `1px solid`,
|
||||
borderTopColor: 'divider',
|
||||
p: { xs: 1, md: 2 },
|
||||
|
||||
@@ -9,13 +9,14 @@ import type { DiagramConfig } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { ShortcutKeyName, useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { openLayoutPreferences } from '~/common/layout/store-applayout';
|
||||
import { useCapabilityElevenLabs, useCapabilityProdia } from '~/common/components/useCapabilities';
|
||||
import { useCapabilityElevenLabs } from '~/common/components/useCapabilities';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { ChatMessageMemo } from './message/ChatMessage';
|
||||
import { CleanerMessage, MessagesSelectionHeader } from './message/CleanerMessage';
|
||||
import { PersonaSelector } from './persona-selector/PersonaSelector';
|
||||
import { useChatShowSystemMessages } from '../store-app-chat';
|
||||
import { useScrollToBottom } from './scroll-to-bottom/useScrollToBottom';
|
||||
|
||||
|
||||
/**
|
||||
@@ -23,6 +24,7 @@ import { useChatShowSystemMessages } from '../store-app-chat';
|
||||
*/
|
||||
export function ChatMessageList(props: {
|
||||
conversationId: DConversationId | null,
|
||||
capabilityHasT2I: boolean,
|
||||
chatLLMContextTokens?: number,
|
||||
isMessageSelectionMode: boolean, setIsMessageSelectionMode: (isMessageSelectionMode: boolean) => void,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string) => void,
|
||||
@@ -39,6 +41,8 @@ export function ChatMessageList(props: {
|
||||
const [selectedMessages, setSelectedMessages] = React.useState<Set<string>>(new Set());
|
||||
|
||||
// external state
|
||||
const { notifyBooting } = useScrollToBottom();
|
||||
const { openPreferences } = useOptimaLayout();
|
||||
const [showSystemMessages] = useChatShowSystemMessages();
|
||||
const { conversationMessages, historyTokenCount, editMessage, deleteMessage, setMessages } = useChatStore(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
|
||||
@@ -50,11 +54,10 @@ export function ChatMessageList(props: {
|
||||
setMessages: state.setMessages,
|
||||
};
|
||||
}, shallow);
|
||||
const { mayWork: isImaginable } = useCapabilityProdia();
|
||||
const { mayWork: isSpeakable } = useCapabilityElevenLabs();
|
||||
|
||||
// derived state
|
||||
const { conversationId, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
|
||||
const { conversationId, capabilityHasT2I, onConversationBranch, onConversationExecuteHistory, onTextDiagram, onTextImagine, onTextSpeak } = props;
|
||||
|
||||
|
||||
// text actions
|
||||
@@ -98,22 +101,22 @@ export function ChatMessageList(props: {
|
||||
}, [conversationId, onTextDiagram]);
|
||||
|
||||
const handleTextImagine = React.useCallback(async (text: string) => {
|
||||
if (!isImaginable)
|
||||
return openLayoutPreferences(2);
|
||||
if (!capabilityHasT2I)
|
||||
return openPreferences(2);
|
||||
if (conversationId) {
|
||||
setIsImagining(true);
|
||||
await onTextImagine(conversationId, text);
|
||||
setIsImagining(false);
|
||||
}
|
||||
}, [conversationId, isImaginable, onTextImagine]);
|
||||
}, [capabilityHasT2I, conversationId, onTextImagine, openPreferences]);
|
||||
|
||||
const handleTextSpeak = React.useCallback(async (text: string) => {
|
||||
if (!isSpeakable)
|
||||
return openLayoutPreferences(3);
|
||||
return openPreferences(3);
|
||||
setIsSpeaking(true);
|
||||
await onTextSpeak(text);
|
||||
setIsSpeaking(false);
|
||||
}, [isSpeakable, onTextSpeak]);
|
||||
}, [isSpeakable, onTextSpeak, openPreferences]);
|
||||
|
||||
|
||||
// operate on the local selection set
|
||||
@@ -157,11 +160,19 @@ export function ChatMessageList(props: {
|
||||
return { diffMessage: undefined, diffText: undefined };
|
||||
}, [conversationMessages]);
|
||||
|
||||
|
||||
// scroll to the very bottom of a new chat
|
||||
React.useEffect(() => {
|
||||
if (conversationId)
|
||||
notifyBooting();
|
||||
}, [conversationId, notifyBooting]);
|
||||
|
||||
|
||||
// no content: show the persona selector
|
||||
|
||||
const filteredMessages = conversationMessages
|
||||
.filter(m => m.role !== 'system' || showSystemMessages) // hide the System message if the user choses to
|
||||
.reverse(); // 'reverse' is because flexDirection: 'column-reverse' to auto-snap-to-bottom
|
||||
.filter(m => m.role !== 'system' || showSystemMessages); // hide the System message if the user choses to
|
||||
|
||||
|
||||
if (!filteredMessages.length)
|
||||
return (
|
||||
@@ -176,18 +187,29 @@ export function ChatMessageList(props: {
|
||||
<List sx={{
|
||||
p: 0, ...(props.sx || {}),
|
||||
// this makes sure that the the window is scrolled to the bottom (column-reverse)
|
||||
display: 'flex', flexDirection: 'column-reverse',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
// fix for the double-border on the last message (one by the composer, one to the bottom of the message)
|
||||
// marginBottom: '-1px',
|
||||
}}>
|
||||
|
||||
{filteredMessages.map((message, idx) =>
|
||||
{props.isMessageSelectionMode && (
|
||||
<MessagesSelectionHeader
|
||||
hasSelected={selectedMessages.size > 0}
|
||||
sumTokens={historyTokenCount}
|
||||
onClose={() => props.setIsMessageSelectionMode(false)}
|
||||
onSelectAll={handleSelectAll}
|
||||
onDeleteMessages={handleSelectionDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{filteredMessages.map((message, idx, { length: count }) =>
|
||||
props.isMessageSelectionMode ? (
|
||||
|
||||
<CleanerMessage
|
||||
key={'sel-' + message.id}
|
||||
message={message}
|
||||
isBottom={idx === 0} remainingTokens={(props.chatLLMContextTokens || 0) - historyTokenCount}
|
||||
remainingTokens={(props.chatLLMContextTokens || 0) - historyTokenCount}
|
||||
selected={selectedMessages.has(message.id)} onToggleSelected={handleSelectMessage}
|
||||
/>
|
||||
|
||||
@@ -197,7 +219,7 @@ export function ChatMessageList(props: {
|
||||
key={'msg-' + message.id}
|
||||
message={message}
|
||||
diffPreviousText={message === diffMessage ? diffText : undefined}
|
||||
isBottom={idx === 0}
|
||||
isBottom={idx === count - 1}
|
||||
isImagining={isImagining} isSpeaking={isSpeaking}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationRestartFrom={handleConversationRestartFrom}
|
||||
@@ -212,18 +234,6 @@ export function ChatMessageList(props: {
|
||||
),
|
||||
)}
|
||||
|
||||
{/* Header at the bottom because of 'row-reverse' */}
|
||||
{props.isMessageSelectionMode && (
|
||||
<MessagesSelectionHeader
|
||||
hasSelected={selectedMessages.size > 0}
|
||||
isBottom={filteredMessages.length === 0}
|
||||
sumTokens={historyTokenCount}
|
||||
onClose={() => props.setIsMessageSelectionMode(false)}
|
||||
onSelectAll={handleSelectAll}
|
||||
onDeleteMessages={handleSelectionDelete}
|
||||
/>
|
||||
)}
|
||||
|
||||
</List>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import FileUploadIcon from '@mui/icons-material/FileUpload';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { OpenAIIcon } from '~/common/components/icons/OpenAIIcon';
|
||||
import { closeLayoutDrawer } from '~/common/layout/store-applayout';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
@@ -34,6 +34,7 @@ function ChatDrawerItems(props: {
|
||||
// const [grouping] = React.useState<ListGrouping>('off');
|
||||
|
||||
// external state
|
||||
const { closeAppDrawer } = useOptimaLayout();
|
||||
const conversations = useChatStore(state => state.conversations, shallow);
|
||||
const showSymbols = useUIPreferencesStore(state => state.zenMode !== 'cleaner');
|
||||
const labsEnhancedUI = useUXLabsStore(state => state.labsEnhancedUI);
|
||||
@@ -48,14 +49,14 @@ function ChatDrawerItems(props: {
|
||||
|
||||
const handleButtonNew = React.useCallback(() => {
|
||||
onConversationNew();
|
||||
closeLayoutDrawer();
|
||||
}, [onConversationNew]);
|
||||
closeAppDrawer();
|
||||
}, [closeAppDrawer, onConversationNew]);
|
||||
|
||||
const handleConversationActivate = React.useCallback((conversationId: DConversationId, closeMenu: boolean) => {
|
||||
onConversationActivate(conversationId);
|
||||
if (closeMenu)
|
||||
closeLayoutDrawer();
|
||||
}, [onConversationActivate]);
|
||||
closeAppDrawer();
|
||||
}, [closeAppDrawer, onConversationActivate]);
|
||||
|
||||
const handleConversationDelete = React.useCallback((conversationId: DConversationId) => {
|
||||
!singleChat && conversationId && onConversationDelete(conversationId, true);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
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';
|
||||
@@ -8,12 +12,17 @@ import { usePersonaIdDropdown } from './usePersonaDropdown';
|
||||
|
||||
export function ChatDropdowns(props: {
|
||||
conversationId: DConversationId | null
|
||||
isSplitPanes: boolean
|
||||
onToggleSplitPanes: () => void
|
||||
}) {
|
||||
|
||||
// state
|
||||
const { chatLLMDropdown } = useChatLLMDropdown();
|
||||
const { personaDropdown } = usePersonaIdDropdown(props.conversationId);
|
||||
|
||||
// external state
|
||||
const labsSplitBranching = useUXLabsStore(state => state.labsSplitBranching);
|
||||
|
||||
return <>
|
||||
|
||||
{/* Model selector */}
|
||||
@@ -22,5 +31,17 @@ export function ChatDropdowns(props: {
|
||||
{/* Persona selector */}
|
||||
{personaDropdown}
|
||||
|
||||
{/* Split Panes button */}
|
||||
{labsSplitBranching && <IconButton
|
||||
variant={props.isSplitPanes ? 'solid' : 'plain'}
|
||||
color='neutral'
|
||||
onClick={props.onToggleSplitPanes}
|
||||
sx={{
|
||||
ml: 'auto',
|
||||
}}
|
||||
>
|
||||
<VerticalSplitIcon />
|
||||
</IconButton>}
|
||||
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
|
||||
|
||||
import type { DConversationId } from '~/common/state/store-chats';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { closeLayoutMenu } from '~/common/layout/store-applayout';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUICounter } from '~/common/state/store-ui';
|
||||
|
||||
import { useChatShowSystemMessages } from '../../store-app-chat';
|
||||
@@ -30,6 +30,7 @@ export function ChatMenuItems(props: {
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { closeAppMenu } = useOptimaLayout();
|
||||
const { touch: shareTouch } = useUICounter('export-share');
|
||||
const [showSystemMessages, setShowSystemMessages] = useChatShowSystemMessages();
|
||||
|
||||
@@ -39,7 +40,7 @@ export function ChatMenuItems(props: {
|
||||
|
||||
const closeMenu = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
closeLayoutMenu();
|
||||
closeAppMenu();
|
||||
};
|
||||
|
||||
const handleConversationClear = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
|
||||
@@ -7,9 +7,9 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
|
||||
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { AppBarDropdown, DropdownItems } from '~/common/layout/AppBarDropdown';
|
||||
import { GoodDropdown, DropdownItems } from '~/common/components/GoodDropdown';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { openLayoutLLMOptions, openLayoutModelsSetup } from '~/common/layout/store-applayout';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
|
||||
function AppBarLLMDropdown(props: {
|
||||
@@ -19,6 +19,9 @@ function AppBarLLMDropdown(props: {
|
||||
placeholder?: string,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const { openLlmOptions, openModelsSetup } = useOptimaLayout();
|
||||
|
||||
// build model menu items, filtering-out hidden models, and add Source separators
|
||||
const llmItems: DropdownItems = {};
|
||||
let prevSourceId: DModelSourceId | null = null;
|
||||
@@ -47,11 +50,11 @@ function AppBarLLMDropdown(props: {
|
||||
|
||||
const handleChatLLMChange = (_event: any, value: DLLMId | null) => value && props.setChatLlmId(value);
|
||||
|
||||
const handleOpenLLMOptions = () => props.chatLlmId && openLayoutLLMOptions(props.chatLlmId);
|
||||
const handleOpenLLMOptions = () => props.chatLlmId && openLlmOptions(props.chatLlmId);
|
||||
|
||||
|
||||
return (
|
||||
<AppBarDropdown
|
||||
<GoodDropdown
|
||||
items={llmItems}
|
||||
value={props.chatLlmId} onChange={handleChatLLMChange}
|
||||
placeholder={props.placeholder || 'Models …'}
|
||||
@@ -67,7 +70,7 @@ function AppBarLLMDropdown(props: {
|
||||
</ListItemButton>
|
||||
)}
|
||||
|
||||
<ListItemButton key='menu-llms' onClick={openLayoutModelsSetup}>
|
||||
<ListItemButton key='menu-llms' onClick={openModelsSetup}>
|
||||
<ListItemDecorator><BuildCircleIcon color='success' /></ListItemDecorator>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>
|
||||
Models
|
||||
|
||||
@@ -6,8 +6,8 @@ import CallIcon from '@mui/icons-material/Call';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
|
||||
import { AppBarDropdown } from '~/common/layout/AppBarDropdown';
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { GoodDropdown } from '~/common/components/GoodDropdown';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
@@ -42,7 +42,7 @@ function AppBarPersonaDropdown(props: {
|
||||
}
|
||||
|
||||
return (
|
||||
<AppBarDropdown
|
||||
<GoodDropdown
|
||||
items={SystemPurposes} showSymbols={zenMode !== 'cleaner'}
|
||||
value={props.systemPurposeId} onChange={handleSystemPurposeChange}
|
||||
appendOption={appendOption}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function ButtonOptionsDraw(props: { isMobile?: boolean, onClick: () => vo
|
||||
<FormatPaintIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Button variant='soft' color='warning' onClick={props.onClick} endDecorator={<FormatPaintIcon />} sx={props.sx}>
|
||||
<Button variant='soft' color='warning' onClick={props.onClick} sx={props.sx}>
|
||||
Options
|
||||
</Button>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Box, MenuItem, Radio, Typography } from '@mui/joy';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { ChatModeId } from '../../AppChat';
|
||||
|
||||
@@ -14,29 +13,25 @@ interface ChatModeDescription {
|
||||
label: string;
|
||||
description: string | React.JSX.Element;
|
||||
shortcut?: string;
|
||||
experimental?: boolean;
|
||||
requiresTTI?: boolean;
|
||||
}
|
||||
|
||||
const ChatModeItems: { [key in ChatModeId]: ChatModeDescription } = {
|
||||
'immediate': {
|
||||
'generate-text': {
|
||||
label: 'Chat',
|
||||
description: 'Persona replies',
|
||||
},
|
||||
'write-user': {
|
||||
'append-user': {
|
||||
label: 'Write',
|
||||
description: 'Appends a message',
|
||||
shortcut: 'Alt + Enter',
|
||||
},
|
||||
'draw-imagine': {
|
||||
'generate-image': {
|
||||
label: 'Draw',
|
||||
description: 'AI Image Generation',
|
||||
requiresTTI: true,
|
||||
},
|
||||
'draw-imagine-plus': {
|
||||
label: 'Assisted Draw',
|
||||
description: 'Assisted Image Generation',
|
||||
experimental: true,
|
||||
},
|
||||
'react': {
|
||||
'generate-react': {
|
||||
label: 'Reason + Act · α',
|
||||
description: 'Answers questions in multiple steps',
|
||||
},
|
||||
@@ -49,11 +44,14 @@ function fixNewLineShortcut(shortcut: string, enterIsNewLine: boolean) {
|
||||
return shortcut;
|
||||
}
|
||||
|
||||
export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClose: () => void, chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void }) {
|
||||
export function ChatModeMenu(props: {
|
||||
anchorEl: HTMLAnchorElement | null, onClose: () => void,
|
||||
chatModeId: ChatModeId, onSetChatModeId: (chatMode: ChatModeId) => void
|
||||
capabilityHasTTI: boolean,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
const labsMagicDraw = useUXLabsStore(state => state.labsMagicDraw);
|
||||
|
||||
return <CloseableMenu
|
||||
placement='top-end' sx={{ minWidth: 320 }}
|
||||
@@ -68,14 +66,13 @@ export function ChatModeMenu(props: { anchorEl: HTMLAnchorElement | null, onClos
|
||||
|
||||
{/* ChatMode items */}
|
||||
{Object.entries(ChatModeItems)
|
||||
.filter(([, { experimental }]) => labsMagicDraw || !experimental)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
|
||||
<Box sx={{ flexGrow: 1, display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 2 }}>
|
||||
<Radio checked={key === props.chatModeId} />
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<Typography>{data.label}</Typography>
|
||||
<Typography level='body-xs'>{data.description}</Typography>
|
||||
<Typography level='body-xs'>{data.description}{(data.requiresTTI && !props.capabilityHasTTI) ? 'Unconfigured' : ''}</Typography>
|
||||
</Box>
|
||||
{(key === props.chatModeId || !!data.shortcut) && (
|
||||
<KeyStroke combo={fixNewLineShortcut((key === props.chatModeId) ? 'ENTER' : data.shortcut ? data.shortcut : 'ENTER', enterIsNewline)} />
|
||||
|
||||
@@ -3,11 +3,13 @@ import { shallow } from 'zustand/shallow';
|
||||
import { fileOpen, FileWithHandle } from 'browser-fs-access';
|
||||
import { keyframes } from '@emotion/react';
|
||||
|
||||
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Typography } from '@mui/joy';
|
||||
import { Box, Button, ButtonGroup, Card, Grid, IconButton, Stack, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
import { ColorPaletteProp, SxProps, VariantProp } from '@mui/joy/styles/types';
|
||||
import AttachFileIcon from '@mui/icons-material/AttachFile';
|
||||
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome';
|
||||
import AutoModeIcon from '@mui/icons-material/AutoMode';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import PsychologyIcon from '@mui/icons-material/Psychology';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
import StopOutlinedIcon from '@mui/icons-material/StopOutlined';
|
||||
@@ -24,12 +26,12 @@ import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { countModelTokens } from '~/common/util/token-counter';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
import { openLayoutPreferences } from '~/common/layout/store-applayout';
|
||||
import { playSoundUrl } from '~/common/util/audioUtils';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { useDebouncer } from '~/common/components/useDebouncer';
|
||||
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
@@ -71,8 +73,10 @@ export function Composer(props: {
|
||||
chatLLM: DLLM | null;
|
||||
composerTextAreaRef: React.RefObject<HTMLTextAreaElement>;
|
||||
conversationId: DConversationId | null;
|
||||
capabilityHasT2I: boolean;
|
||||
isDeveloperMode: boolean;
|
||||
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
|
||||
onTextImagine: (conversationId: DConversationId, text: string) => void;
|
||||
sx?: SxProps;
|
||||
}) {
|
||||
|
||||
@@ -85,11 +89,12 @@ export function Composer(props: {
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const { openPreferences } = useOptimaLayout();
|
||||
const { labsCalling, labsCameraDesktop } = useUXLabsStore(state => ({
|
||||
labsCalling: state.labsCalling,
|
||||
labsCameraDesktop: state.labsCameraDesktop,
|
||||
}), shallow);
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('immediate');
|
||||
const [chatModeId, setChatModeId] = React.useState<ChatModeId>('generate-text');
|
||||
const [startupText, setStartupText] = useComposerStartupText();
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
|
||||
@@ -167,7 +172,7 @@ export function Composer(props: {
|
||||
|
||||
// Alt: append the message instead
|
||||
if (e.altKey) {
|
||||
handleSendAction('write-user', composeText);
|
||||
handleSendAction('append-user', composeText);
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -189,7 +194,14 @@ export function Composer(props: {
|
||||
|
||||
const handleCallClicked = () => props.conversationId && systemPurposeId && launchAppCall(props.conversationId, systemPurposeId);
|
||||
|
||||
const handleDrawOptionsClicked = () => openLayoutPreferences(2);
|
||||
const handleDrawOptionsClicked = () => openPreferences(2);
|
||||
|
||||
const handleTextImagineClicked = () => {
|
||||
if (!composeText || !props.conversationId)
|
||||
return;
|
||||
props.onTextImagine(props.conversationId, composeText);
|
||||
setComposeText('');
|
||||
};
|
||||
|
||||
|
||||
// Mode menu
|
||||
@@ -350,24 +362,23 @@ export function Composer(props: {
|
||||
}, [attachAppendDataTransfer, eatDragEvent, setComposeText]);
|
||||
|
||||
|
||||
const isImmediate = chatModeId === 'immediate';
|
||||
const isWriteUser = chatModeId === 'write-user';
|
||||
const isChat = isImmediate || isWriteUser;
|
||||
const isReAct = chatModeId === 'react';
|
||||
const isDraw = chatModeId === 'draw-imagine';
|
||||
const isDrawPlus = chatModeId === 'draw-imagine-plus';
|
||||
const buttonColor: ColorPaletteProp = isReAct ? 'success' : (isDraw || isDrawPlus) ? 'warning' : 'primary';
|
||||
const isText = chatModeId === 'generate-text';
|
||||
const isAppend = chatModeId === 'append-user';
|
||||
const isChat = isText || isAppend;
|
||||
const isReAct = chatModeId === 'generate-react';
|
||||
const isDraw = chatModeId === 'generate-image';
|
||||
const buttonColor: ColorPaletteProp = isReAct ? 'success' : isDraw ? 'warning' : 'primary';
|
||||
|
||||
const textPlaceholder: string =
|
||||
isDrawPlus
|
||||
? 'Write a subject, and we\'ll add detail...'
|
||||
: isDraw
|
||||
? 'Describe an idea or a drawing...'
|
||||
: isReAct
|
||||
? 'Multi-step reasoning question...'
|
||||
: props.isDeveloperMode
|
||||
? 'Chat with me · drop source files · attach code...'
|
||||
: /*isProdiaConfigured ?*/ 'Chat · /react · /imagine · drop text files...' /*: 'Chat · /react · drop text files...'*/;
|
||||
isDraw
|
||||
? 'Describe an idea or a drawing...'
|
||||
: isReAct
|
||||
? 'Multi-step reasoning question...'
|
||||
: props.isDeveloperMode
|
||||
? 'Chat with me · drop source files · attach code...'
|
||||
: props.capabilityHasT2I
|
||||
? 'Chat · /react · /draw · drop text files...'
|
||||
: 'Chat · /react · drop text files...';
|
||||
|
||||
|
||||
return (
|
||||
@@ -417,7 +428,7 @@ export function Composer(props: {
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
display: 'flex', flexDirection: 'column', gap: 1,
|
||||
overflowX: 'clip',
|
||||
minWidth: 250, // enable X-scrolling (resetting any possible minWidth due to the attachments)
|
||||
}}>
|
||||
|
||||
{/* Edit box + Overlays + Mic buttons */}
|
||||
@@ -427,7 +438,7 @@ export function Composer(props: {
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
|
||||
<Textarea
|
||||
variant='outlined' color={(isDraw || isDrawPlus) ? 'warning' : isReAct ? 'success' : 'neutral'}
|
||||
variant='outlined' color={isDraw ? 'warning' : isReAct ? 'success' : 'neutral'}
|
||||
autoFocus
|
||||
minRows={5} maxRows={10}
|
||||
placeholder={textPlaceholder}
|
||||
@@ -452,7 +463,6 @@ export function Composer(props: {
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
// fontSize: '16px',
|
||||
lineHeight: 1.75,
|
||||
}} />
|
||||
|
||||
@@ -555,14 +565,14 @@ export function Composer(props: {
|
||||
{/* [mobile] bottom-corner secondary button */}
|
||||
{isMobile && (isChat
|
||||
? <ButtonCall isMobile disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
: (isDraw || isDrawPlus)
|
||||
: isDraw
|
||||
? <ButtonOptionsDraw isMobile onClick={handleDrawOptionsClicked} sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
: <IconButton disabled variant='plain' color='neutral' sx={{ mr: { xs: 1, md: 2 } }} />
|
||||
)}
|
||||
|
||||
{/* Responsive Send/Stop buttons */}
|
||||
<ButtonGroup
|
||||
variant={isWriteUser ? 'outlined' : 'solid'}
|
||||
variant={isAppend ? 'outlined' : 'solid'}
|
||||
color={buttonColor}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
@@ -574,10 +584,16 @@ export function Composer(props: {
|
||||
key='composer-act'
|
||||
fullWidth disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
|
||||
onClick={handleSendClicked}
|
||||
endDecorator={micContinuation ? <AutoModeIcon /> : isWriteUser ? <SendIcon sx={{ fontSize: 18 }} /> : isReAct ? <PsychologyIcon /> : <TelegramIcon />}
|
||||
endDecorator={
|
||||
micContinuation ? <AutoModeIcon /> :
|
||||
isAppend ? <SendIcon sx={{ fontSize: 18 }} /> :
|
||||
isReAct ? <PsychologyIcon /> :
|
||||
isDraw ? <FormatPaintIcon />
|
||||
: <TelegramIcon />
|
||||
}
|
||||
>
|
||||
{micContinuation && 'Voice '}
|
||||
{isWriteUser ? 'Write' : isReAct ? 'ReAct' : isDraw ? 'Draw' : isDrawPlus ? 'Draw+' : 'Chat'}
|
||||
{isAppend ? 'Write' : isReAct ? 'ReAct' : isDraw ? 'Draw' : 'Chat'}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@@ -590,7 +606,16 @@ export function Composer(props: {
|
||||
Stop
|
||||
</Button>
|
||||
)}
|
||||
<IconButton disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor} onClick={handleModeSelectorShow}>
|
||||
|
||||
{/* [Draw] Imagine */}
|
||||
{isDraw && !!composeText && <Tooltip title='Imagine a drawing prompt'>
|
||||
<IconButton variant='outlined' disabled={!props.conversationId || !chatLLMId} onClick={handleTextImagineClicked}>
|
||||
<AutoAwesomeIcon />
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
|
||||
{/* Mode expander */}
|
||||
<IconButton variant={isDraw ? undefined : undefined} disabled={!props.conversationId || !chatLLMId || !!chatModeMenuAnchor} onClick={handleModeSelectorShow}>
|
||||
<ExpandLessIcon />
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
@@ -605,7 +630,7 @@ export function Composer(props: {
|
||||
{isChat && <ButtonCall disabled={!labsCalling || !props.conversationId || !chatLLMId} onClick={handleCallClicked} />}
|
||||
|
||||
{/* [desktop] Draw Options secondary button */}
|
||||
{(isDraw || isDrawPlus) && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
|
||||
{isDraw && <ButtonOptionsDraw onClick={handleDrawOptionsClicked} />}
|
||||
|
||||
</Box>}
|
||||
|
||||
@@ -618,6 +643,7 @@ export function Composer(props: {
|
||||
<ChatModeMenu
|
||||
anchorEl={chatModeMenuAnchor} onClose={handleModeSelectorHide}
|
||||
chatModeId={chatModeId} onSetChatModeId={handleModeChange}
|
||||
capabilityHasTTI={props.capabilityHasT2I}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import Face6Icon from '@mui/icons-material/Face6';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import PaletteOutlinedIcon from '@mui/icons-material/PaletteOutlined';
|
||||
import RecordVoiceOverIcon from '@mui/icons-material/RecordVoiceOver';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
|
||||
@@ -78,38 +77,42 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
|
||||
case 'system':
|
||||
return <SettingsSuggestIcon sx={iconSx} />; // https://em-content.zobj.net/thumbs/120/apple/325/robot_1f916.png
|
||||
|
||||
case 'user':
|
||||
return <Face6Icon sx={iconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
|
||||
|
||||
case 'assistant':
|
||||
// display a gif avatar when the assistant is typing (people seem to love this, so keeping it after april fools')
|
||||
// typing gif (people seem to love this, so keeping it after april fools')
|
||||
const isTextToImage = messageOriginLLM === 'DALL·E' || messageOriginLLM === 'Prodia';
|
||||
const isReact = messageOriginLLM?.startsWith('react-');
|
||||
if (messageTyping) {
|
||||
return <Avatar
|
||||
alt={messageSender} variant='plain'
|
||||
src={messageOriginLLM === 'prodia'
|
||||
? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
|
||||
: messageOriginLLM?.startsWith('react-')
|
||||
? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
|
||||
src={isTextToImage ? 'https://i.giphy.com/media/5t9ujj9cMisyVjUZ0m/giphy.webp'
|
||||
: isReact ? 'https://i.giphy.com/media/l44QzsOLXxcrigdgI/giphy.webp'
|
||||
: 'https://i.giphy.com/media/jJxaUysjzO9ri/giphy.webp'}
|
||||
sx={{ ...mascotSx, borderRadius: 'var(--joy-radius-sm)' }}
|
||||
sx={{ ...mascotSx, borderRadius: 'sm' }}
|
||||
/>;
|
||||
}
|
||||
// display the purpose symbol
|
||||
if (messageOriginLLM === 'prodia')
|
||||
return <PaletteOutlinedIcon sx={iconSx} />;
|
||||
|
||||
// text-to-image: icon
|
||||
if (isTextToImage)
|
||||
return <FormatPaintIcon sx={{
|
||||
...iconSx,
|
||||
animation: `${cssRainbowColorKeyframes} 1s linear 2.66`,
|
||||
}} />;
|
||||
|
||||
// purpose symbol (if present)
|
||||
const symbol = SystemPurposes[messagePurposeId!]?.symbol;
|
||||
if (symbol)
|
||||
return <Box
|
||||
sx={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
width: '100%', minWidth: `${iconSx.width}px`, lineHeight: `${iconSx.height}px`,
|
||||
}}
|
||||
>
|
||||
{symbol}
|
||||
</Box>;
|
||||
if (symbol) return <Box sx={{
|
||||
fontSize: '24px',
|
||||
textAlign: 'center',
|
||||
width: '100%', minWidth: `${iconSx.width}px`, lineHeight: `${iconSx.height}px`,
|
||||
}}>
|
||||
{symbol}
|
||||
</Box>;
|
||||
|
||||
// default assistant avatar
|
||||
return <SmartToyOutlinedIcon sx={iconSx} />; // https://mui.com/static/images/avatar/2.jpg
|
||||
|
||||
case 'user':
|
||||
return <Face6Icon sx={iconSx} />; // https://www.svgrepo.com/show/306500/openai.svg
|
||||
}
|
||||
return <Avatar alt={messageSender} />;
|
||||
}
|
||||
@@ -256,9 +259,9 @@ export function ChatMessage(props: {
|
||||
const showAvatars = props.hideAvatars !== true && !cleanerLooks;
|
||||
|
||||
const textSel = selMenuText ? selMenuText : messageText;
|
||||
const isSpecialProdia = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/imagine') || textSel.startsWith('/img');
|
||||
const couldDiagram = textSel?.length >= 100 && !isSpecialProdia;
|
||||
const couldImagine = textSel?.length >= 2 && !isSpecialProdia;
|
||||
const isSpecialT2I = textSel.startsWith('https://images.prodia.xyz/') || textSel.startsWith('/draw ') || textSel.startsWith('/imagine ') || textSel.startsWith('/img ');
|
||||
const couldDiagram = textSel?.length >= 100 && !isSpecialT2I;
|
||||
const couldImagine = textSel?.length >= 2 && !isSpecialT2I;
|
||||
const couldSpeak = couldImagine;
|
||||
|
||||
|
||||
@@ -441,7 +444,6 @@ export function ChatMessage(props: {
|
||||
borderBottomColor: 'divider',
|
||||
}),
|
||||
...(ENABLE_COPY_MESSAGE_OVERLAY && { position: 'relative' }),
|
||||
...(props.isBottom === true && { mb: 'auto' }),
|
||||
'&:hover > button': { opacity: 1 },
|
||||
...props.sx,
|
||||
}}
|
||||
@@ -522,7 +524,7 @@ export function ChatMessage(props: {
|
||||
: block.type === 'code'
|
||||
? <RenderCode key={'code-' + index} codeBlock={block} sx={codeSx} noCopyButton={props.diagramMode} />
|
||||
: block.type === 'image'
|
||||
? <RenderImage key={'image-' + index} imageBlock={block} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
|
||||
? <RenderImage key={'image-' + index} imageBlock={block} isFirst={!index} allowRunAgain={props.isBottom === true} onRunAgain={handleOpsConversationRestartFrom} />
|
||||
: block.type === 'latex'
|
||||
? <RenderLatex key={'latex-' + index} latexBlock={block} />
|
||||
: block.type === 'diff'
|
||||
@@ -617,7 +619,7 @@ export function ChatMessage(props: {
|
||||
</MenuItem>}
|
||||
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintIcon color='success' />}</ListItemDecorator>
|
||||
Imagine
|
||||
Draw ...
|
||||
</MenuItem>}
|
||||
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverIcon color='success' />}</ListItemDecorator>
|
||||
|
||||
@@ -13,7 +13,7 @@ import { makeAvatar, messageBackground } from './ChatMessage';
|
||||
/**
|
||||
* Header bar for controlling the operations during the Selection mode
|
||||
*/
|
||||
export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
|
||||
export const MessagesSelectionHeader = (props: { hasSelected: boolean, sumTokens: number, onClose: () => void, onSelectAll: (selected: boolean) => void, onDeleteMessages: () => void }) =>
|
||||
<Sheet color='warning' variant='solid' invertedColors sx={{
|
||||
display: 'flex', flexDirection: 'row', alignItems: 'center',
|
||||
position: 'fixed', top: 0, left: 0, right: 0, zIndex: 101,
|
||||
@@ -39,7 +39,7 @@ export const MessagesSelectionHeader = (props: { hasSelected: boolean, isBottom:
|
||||
*
|
||||
* Shall look similarly to the main ChatMessage, for consistency, but just allow a simple checkbox selection
|
||||
*/
|
||||
export function CleanerMessage(props: { message: DMessage, isBottom: boolean, selected: boolean, remainingTokens?: number, onToggleSelected?: (messageId: string, selected: boolean) => void }) {
|
||||
export function CleanerMessage(props: { message: DMessage, selected: boolean, remainingTokens?: number, onToggleSelected?: (messageId: string, selected: boolean) => void }) {
|
||||
|
||||
// derived state
|
||||
const {
|
||||
@@ -77,7 +77,6 @@ export function CleanerMessage(props: { message: DMessage, isBottom: boolean, se
|
||||
borderBottom: '1px solid',
|
||||
borderBottomColor: 'divider',
|
||||
// position: 'relative',
|
||||
...(props.isBottom && { mb: 'auto' }),
|
||||
'&:hover > button': { opacity: 1 },
|
||||
}}>
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, IconButton, Tooltip } from '@mui/joy';
|
||||
import { Alert, Box, IconButton, Tooltip, Typography } from '@mui/joy';
|
||||
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import ZoomOutMapIcon from '@mui/icons-material/ZoomOutMap';
|
||||
|
||||
import { Link } from '~/common/components/Link';
|
||||
|
||||
@@ -10,25 +10,116 @@ import { ImageBlock } from './blocks';
|
||||
import { overlayButtonsSx } from './RenderCode';
|
||||
|
||||
|
||||
export const RenderImage = (props: { imageBlock: ImageBlock, allowRunAgain: boolean, onRunAgain?: (e: React.MouseEvent) => void }) => {
|
||||
const imageUrls = props.imageBlock.url.split('\n');
|
||||
const mdImageReferenceRegex = /^!\[([^\]]*)]\(([^)]+)\)$/;
|
||||
const imageExtensions = /\.(jpg|jpeg|png|gif|bmp|svg)/i;
|
||||
|
||||
return imageUrls.map((url, index) => (
|
||||
<Box
|
||||
|
||||
/**
|
||||
* Checks if the entire content consists solely of Markdown image references.
|
||||
* If so, returns an array of ImageBlock objects for each image reference.
|
||||
* If any non-image content is present or if there are no image references, returns null.
|
||||
*/
|
||||
export function heuristicMarkdownImageReferenceBlocks(fullText: string) {
|
||||
|
||||
// Check if all lines are valid Markdown image references with image URLs
|
||||
const imageBlocks: ImageBlock[] = [];
|
||||
for (const line of fullText.split('\n')) {
|
||||
if (line.trim() === '') continue; // skip empty lines
|
||||
const match = mdImageReferenceRegex.exec(line);
|
||||
if (match && imageExtensions.test(match[2])) {
|
||||
const alt = match[1];
|
||||
const url = match[2];
|
||||
imageBlocks.push({ type: 'image', url, alt });
|
||||
} else {
|
||||
// if there is any outlier line, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the image blocks if all lines are image references with valid image URLs
|
||||
return imageBlocks.length > 0 ? imageBlocks : null;
|
||||
}
|
||||
|
||||
const prodiaUrlRegex = /^(https?:\/\/images\.prodia\.\S+)$/i;
|
||||
|
||||
/**
|
||||
* Legacy heuristic for detecting images from "images.prodia." URLs.
|
||||
*/
|
||||
export function heuristicLegacyImageBlocks(fullText: string): ImageBlock[] | null {
|
||||
|
||||
// Check if all lines are URLs starting with "http://images.prodia." or "https://images.prodia."
|
||||
const imageBlocks: ImageBlock[] = [];
|
||||
for (const line of fullText.split('\n')) {
|
||||
const match = prodiaUrlRegex.exec(line);
|
||||
if (match) {
|
||||
const url = match[1];
|
||||
imageBlocks.push({ type: 'image', url });
|
||||
} else {
|
||||
// if there is any outlier line, return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Return the image blocks if all lines are URLs from "images.prodia."
|
||||
return imageBlocks.length > 0 ? imageBlocks : null;
|
||||
}
|
||||
|
||||
|
||||
export const RenderImage = (props: { imageBlock: ImageBlock, isFirst: boolean, allowRunAgain: boolean, onRunAgain?: (e: React.MouseEvent) => void }) => {
|
||||
const { url, alt } = props.imageBlock;
|
||||
const imageUrls = url.split('\n');
|
||||
|
||||
return imageUrls.map((url, index) => {
|
||||
|
||||
// display a notice for temporary images DallE
|
||||
const isTempDalleUrl = url.startsWith('https://oaidalle');
|
||||
|
||||
return <Box
|
||||
key={'gen-img-' + index}
|
||||
sx={{
|
||||
display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center', position: 'relative',
|
||||
mx: 1.5, mt: index > 0 ? 1.5 : 0,
|
||||
mx: 1.5, mb: 1.5, // mt: (index > 0 || !props.isFirst) ? 1.5 : 0,
|
||||
// p: 1, border: '1px solid', borderColor: 'divider', borderRadius: 1,
|
||||
minWidth: 64, minHeight: 64, boxShadow: 'lg',
|
||||
minWidth: 128, minHeight: 128,
|
||||
boxShadow: 'md',
|
||||
backgroundColor: 'neutral.solidBg',
|
||||
'& picture': { display: 'flex' },
|
||||
'& img': { maxWidth: '100%', maxHeight: '100%' },
|
||||
'&:hover > .overlay-buttons': { opacity: 1 },
|
||||
}}>
|
||||
}}
|
||||
>
|
||||
|
||||
{/* External Image */}
|
||||
<picture><img src={url} alt='Generated Image' /></picture>
|
||||
{alt ? (
|
||||
<Tooltip
|
||||
variant='outlined' color='neutral'
|
||||
title={
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{isTempDalleUrl && <Alert variant='soft' color='warning' sx={{ flexDirection: 'column', alignItems: 'start' }}>
|
||||
<Typography level='title-sm'>⚠️ Temporary Image</Typography>
|
||||
<Typography level='body-sm'>
|
||||
This image will be deleted from the OpenAI servers in one hour. <b>Please save it to your device</b>.
|
||||
</Typography>
|
||||
{/*<Typography level='body-xs'>*/}
|
||||
{/* The following is the re-written DALL·E prompt that generated this image.*/}
|
||||
{/*</Typography>*/}
|
||||
</Alert>}
|
||||
<Typography level='title-sm' sx={{ p: 2 }}>
|
||||
{alt}
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
placement='top-start'
|
||||
sx={{
|
||||
maxWidth: { sm: '90vw', md: '70vw' },
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
<picture><img src={url} alt={`Generated Image: ${alt}`} /></picture>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<picture><img src={url} alt='Generated Image' /></picture>
|
||||
)}
|
||||
|
||||
{/* Image Buttons */}
|
||||
<Box className='overlay-buttons' sx={{ ...overlayButtonsSx, pt: 0.5, px: 0.5, gap: 0.5 }}>
|
||||
@@ -39,10 +130,12 @@ export const RenderImage = (props: { imageBlock: ImageBlock, allowRunAgain: bool
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton component={Link} href={url} target='_blank' variant='solid' color='neutral'>
|
||||
<ZoomOutMapIcon />
|
||||
</IconButton>
|
||||
<Tooltip title='Open in new tab'>
|
||||
<IconButton component={Link} href={url} target='_blank' variant='solid' color='neutral'>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
));
|
||||
</Box>;
|
||||
});
|
||||
};
|
||||
@@ -1,12 +1,13 @@
|
||||
import type { Diff as TextDiff } from '@sanity/diff-match-patch';
|
||||
|
||||
import { heuristicIsHtml } from './RenderHtml';
|
||||
import { heuristicMarkdownImageReferenceBlocks, heuristicLegacyImageBlocks } from './RenderImage';
|
||||
|
||||
type Block = CodeBlock | DiffBlock | HtmlBlock | ImageBlock | LatexBlock | TextBlock;
|
||||
export type CodeBlock = { type: 'code'; blockTitle: string; blockCode: string; complete: boolean; };
|
||||
export type DiffBlock = { type: 'diff'; textDiffs: TextDiff[] };
|
||||
export type HtmlBlock = { type: 'html'; html: string; };
|
||||
export type ImageBlock = { type: 'image'; url: string; };
|
||||
export type ImageBlock = { type: 'image'; url: string; alt?: string }; // Added optional alt property
|
||||
export type LatexBlock = { type: 'latex'; latex: string; };
|
||||
export type TextBlock = { type: 'text'; content: string; }; // for Text or Markdown
|
||||
|
||||
@@ -21,9 +22,18 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
|
||||
if (heuristicIsHtml(text))
|
||||
return [{ type: 'html', html: text }];
|
||||
|
||||
// special case: markdown image references (e.g. )
|
||||
const mdImageBlocks = heuristicMarkdownImageReferenceBlocks(text);
|
||||
if (mdImageBlocks)
|
||||
return mdImageBlocks;
|
||||
|
||||
// special case: legacy prodia images
|
||||
const legacyImageBlocks = heuristicLegacyImageBlocks(text);
|
||||
if (legacyImageBlocks)
|
||||
return legacyImageBlocks;
|
||||
|
||||
const regexPatterns = {
|
||||
codeBlock: /`{3,}([\w\\.+-_]+)?\n([\s\S]*?)(`{3,}\n?|$)/g,
|
||||
imageBlock: /(https:\/\/images\.prodia\.xyz\/.*?\.png)/g, // NOTE: only Prodia for now - but this shall be expanded to markdown images  or any png/jpeg
|
||||
latexBlock: /\$\$([\s\S]*?)\$\$/g,
|
||||
// latexBlockOrInline: /\$\$([\s\S]*?)\$\$|\$([^$]*?)\$/g,
|
||||
};
|
||||
@@ -61,11 +71,6 @@ export function parseBlocks(text: string, forceText: boolean, textDiffs: TextDif
|
||||
blocks.push({ type: 'code', blockTitle, blockCode, complete: blockEnd.startsWith('```') });
|
||||
break;
|
||||
|
||||
case 'imageBlock':
|
||||
const url: string = match[1];
|
||||
blocks.push({ type: 'image', url });
|
||||
break;
|
||||
|
||||
case 'latexBlock':
|
||||
const latex: string = match[1];
|
||||
blocks.push({ type: 'latex', latex });
|
||||
|
||||
+56
-22
@@ -33,9 +33,9 @@ interface AppChatPanesStore {
|
||||
openConversationInFocusedPane: (conversationId: DConversationId) => void;
|
||||
openConversationInSplitPane: (conversationId: DConversationId) => void;
|
||||
navigateHistoryInFocusedPane: (direction: 'back' | 'forward') => boolean;
|
||||
setFocusedPaneIndex: (paneIndex: number) => void;
|
||||
splitChatPane: (numberOfPanes: number) => void;
|
||||
unsplitChatPane: (paneIndexToKeep: number) => void;
|
||||
duplicatePane: (paneIndex: number) => void;
|
||||
removePane: (paneIndex: number) => void;
|
||||
setFocusedPane: (paneIndex: number) => void;
|
||||
onConversationsChanged: (conversationIds: DConversationId[]) => void;
|
||||
|
||||
}
|
||||
@@ -160,7 +160,52 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
return true;
|
||||
},
|
||||
|
||||
setFocusedPaneIndex: (paneIndex: number) =>
|
||||
duplicatePane: (paneIndex: number) =>
|
||||
_set(state => {
|
||||
const { chatPanes } = state;
|
||||
|
||||
// Validate index
|
||||
if (paneIndex < 0 || paneIndex >= chatPanes.length) {
|
||||
console.warn('Attempted to duplicate a pane with an out-of-range index:', paneIndex);
|
||||
return state; // Return the existing state without changes
|
||||
}
|
||||
|
||||
// Clone the pane at the specified index, including a deep copy of the history array
|
||||
const paneToDuplicate = chatPanes[paneIndex];
|
||||
const duplicatedPane = {
|
||||
...paneToDuplicate,
|
||||
history: [...paneToDuplicate.history], // Deep copy of the history array
|
||||
};
|
||||
|
||||
// Insert the duplicated pane into the array, right after the original pane
|
||||
const newPanes = [
|
||||
...chatPanes.slice(0, paneIndex + 1),
|
||||
duplicatedPane,
|
||||
...chatPanes.slice(paneIndex + 1),
|
||||
];
|
||||
|
||||
return {
|
||||
chatPanes: newPanes,
|
||||
chatPaneFocusIndex: paneIndex + 1,
|
||||
};
|
||||
}),
|
||||
|
||||
removePane: (paneIndex: number) =>
|
||||
_set(state => {
|
||||
const { chatPanes } = state;
|
||||
if (paneIndex < 0 || paneIndex >= chatPanes.length)
|
||||
return state;
|
||||
|
||||
const newPanes = chatPanes.toSpliced(paneIndex, 1);
|
||||
|
||||
// when a pane is removed, focus the pane 0, or null if no panes remain
|
||||
return {
|
||||
chatPanes: newPanes,
|
||||
chatPaneFocusIndex: newPanes.length ? 0 : null,
|
||||
};
|
||||
}),
|
||||
|
||||
setFocusedPane: (paneIndex: number) =>
|
||||
_set(state => {
|
||||
if (state.chatPaneFocusIndex === paneIndex)
|
||||
return state;
|
||||
@@ -169,22 +214,6 @@ const useAppChatPanesStore = create<AppChatPanesStore>()(persist(
|
||||
};
|
||||
}),
|
||||
|
||||
splitChatPane: (numberOfPanes: number) => {
|
||||
const { chatPanes, chatPaneFocusIndex } = _get();
|
||||
const focusedPane = (chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex] : null) ?? createPane();
|
||||
|
||||
_set({
|
||||
chatPanes: Array.from({ length: numberOfPanes }, () => ({ ...focusedPane })),
|
||||
chatPaneFocusIndex: 0,
|
||||
});
|
||||
},
|
||||
|
||||
unsplitChatPane: (paneIndexToKeep: number) =>
|
||||
_set(state => ({
|
||||
chatPanes: [state.chatPanes[paneIndexToKeep] || createPane()],
|
||||
chatPaneFocusIndex: 0,
|
||||
})),
|
||||
|
||||
|
||||
/**
|
||||
* This function is vital, as is invoked when the conversationId[] changes in the global chats store.
|
||||
@@ -258,7 +287,9 @@ export function usePanesManager() {
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
setFocusedPaneIndex,
|
||||
duplicatePane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
} = state;
|
||||
const focusedConversationId = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex]?.conversationId ?? null : null;
|
||||
return {
|
||||
@@ -268,7 +299,10 @@ export function usePanesManager() {
|
||||
onConversationsChanged,
|
||||
openConversationInFocusedPane,
|
||||
openConversationInSplitPane,
|
||||
setFocusedPaneIndex,
|
||||
paneIndex: chatPaneFocusIndex,
|
||||
duplicatePane,
|
||||
removePane,
|
||||
setFocusedPane,
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
@@ -3,19 +3,20 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, Button, Checkbox, Grid, IconButton, Input, Stack, Textarea, Typography } from '@mui/joy';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import ScienceIcon from '@mui/icons-material/Science';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { navigateToPersonas } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
import { usePurposeStore } from './store-purposes';
|
||||
|
||||
|
||||
// 'special' purpose IDs, for tile hiding purposes
|
||||
const PURPOSE_ID_PERSONA_CREATOR = '__persona-creator__';
|
||||
|
||||
// Constants for tile sizes / grid width - breakpoints need to be computed here to work around
|
||||
// the "flex box cannot shrink over wrapped content" issue
|
||||
//
|
||||
@@ -47,7 +48,6 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
|
||||
// external state
|
||||
const showFinder = useUIPreferencesStore(state => state.showPurposeFinder);
|
||||
const labsPersonaYTCreator = useUXLabsStore(state => state.labsPersonaYTCreator);
|
||||
const { systemPurposeId, setSystemPurposeId } = useChatStore(state => {
|
||||
const conversation = state.conversations.find(conversation => conversation.id === props.conversationId);
|
||||
return {
|
||||
@@ -113,6 +113,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
const unfilteredPurposeIDs = (filteredIDs && showFinder) ? filteredIDs : Object.keys(SystemPurposes);
|
||||
const purposeIDs = editMode ? unfilteredPurposeIDs : unfilteredPurposeIDs.filter(id => !hiddenPurposeIDs.includes(id));
|
||||
|
||||
const hidePersonaCreator = hiddenPurposeIDs.includes(PURPOSE_ID_PERSONA_CREATOR);
|
||||
|
||||
const selectedPurpose = purposeIDs.length ? (SystemPurposes[systemPurposeId] ?? null) : null;
|
||||
const selectedExample = selectedPurpose?.examples && getRandomElement(selectedPurpose.examples) || null;
|
||||
|
||||
@@ -156,10 +158,14 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
<Button
|
||||
variant={(!editMode && systemPurposeId === spId) ? 'solid' : 'soft'}
|
||||
color={(!editMode && systemPurposeId === spId) ? 'primary' : SystemPurposes[spId as SystemPurposeId]?.highlighted ? 'warning' : 'neutral'}
|
||||
onClick={() => !editMode && handlePurposeChanged(spId as SystemPurposeId)}
|
||||
onClick={() => editMode
|
||||
? toggleHiddenPurposeId(spId)
|
||||
: handlePurposeChanged(spId as SystemPurposeId)
|
||||
}
|
||||
sx={{
|
||||
flexDirection: 'column',
|
||||
fontWeight: 500,
|
||||
// paddingInline: 1,
|
||||
gap: bpTileGap,
|
||||
height: bpTileSize,
|
||||
width: bpTileSize,
|
||||
@@ -171,9 +177,10 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
>
|
||||
{editMode && (
|
||||
<Checkbox
|
||||
label={<Typography level='body-sm'>show</Typography>}
|
||||
checked={!hiddenPurposeIDs.includes(spId)} onChange={() => toggleHiddenPurposeId(spId)}
|
||||
sx={{ alignSelf: 'flex-start' }}
|
||||
color='neutral'
|
||||
checked={!hiddenPurposeIDs.includes(spId)}
|
||||
// label={<Typography level='body-xs'>show</Typography>}
|
||||
sx={{ position: 'absolute', left: 8, top: 8 }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ fontSize: '2rem' }}>
|
||||
@@ -185,28 +192,43 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
</Button>
|
||||
</Grid>
|
||||
))}
|
||||
{/* Button to start the YouTube persona creator */}
|
||||
{labsPersonaYTCreator && <Grid>
|
||||
{/* Button to start the Persona Creator */}
|
||||
{(editMode || !hidePersonaCreator) && <Grid>
|
||||
<Button
|
||||
variant='soft' color='neutral'
|
||||
component={Link} noLinkStyle href='/personas'
|
||||
onClick={() => editMode
|
||||
? toggleHiddenPurposeId(PURPOSE_ID_PERSONA_CREATOR)
|
||||
: void navigateToPersonas()
|
||||
}
|
||||
sx={{
|
||||
'--Icon-fontSize': '2rem',
|
||||
flexDirection: 'column',
|
||||
fontWeight: 500,
|
||||
// gap: bpTileGap,
|
||||
// paddingInline: 1,
|
||||
gap: bpTileGap,
|
||||
height: bpTileSize,
|
||||
width: bpTileSize,
|
||||
border: `1px dashed`,
|
||||
boxShadow: 'md',
|
||||
backgroundColor: 'background.surface',
|
||||
// border: `1px dashed`,
|
||||
// borderColor: 'neutral.softActiveBg',
|
||||
boxShadow: 'xs',
|
||||
backgroundColor: 'neutral.softDisabledBg',
|
||||
}}
|
||||
>
|
||||
{editMode && (
|
||||
<Checkbox
|
||||
color='neutral'
|
||||
checked={!hidePersonaCreator}
|
||||
// label={<Typography level='body-xs'>show</Typography>}
|
||||
sx={{ position: 'absolute', left: 8, top: 8 }}
|
||||
/>
|
||||
)}
|
||||
<div>
|
||||
<ScienceIcon />
|
||||
<div style={{ fontSize: '2rem' }}>
|
||||
🎭
|
||||
</div>
|
||||
{/*<SettingsAccessibilityIcon style={{ opacity: 0.5 }} />*/}
|
||||
</div>
|
||||
<div>
|
||||
YouTube persona creator
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
Persona Creator
|
||||
</div>
|
||||
</Button>
|
||||
</Grid>}
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 Enrico Ros
|
||||
*
|
||||
* This subsystem is responsible for 'snap-to-bottom' and 'scroll-to-bottom' features,
|
||||
* with an animated, gradual scroll.
|
||||
*
|
||||
* See the `ScrollToBottomButton` component for the button that triggers the scroll.
|
||||
*
|
||||
* Example usage:
|
||||
* <ScrollToBottom bootToBottom stickToBottom sx={{ overflowY: 'auto', height: '100%' }}>
|
||||
* <LongMessagesList />
|
||||
* <ScrollToBottomButton />
|
||||
* </ScrollToBottom>
|
||||
*
|
||||
* Within the Context (children components), functions are made available by using:
|
||||
* const { notifyBooting, setStickToBottom } = useScrollToBottom();
|
||||
*
|
||||
*/
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box } from '@mui/joy';
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
|
||||
import { isBrowser } from '~/common/util/pwaUtils';
|
||||
|
||||
import { ScrollToBottomState, UseScrollToBottomProvider } from './useScrollToBottom';
|
||||
|
||||
|
||||
// set this to true to debug this component
|
||||
const DEBUG_SCROLL_TO_BOTTOM = false;
|
||||
|
||||
// NOTE: in Chrome a wheel scroll event is 100px
|
||||
const USER_STICKY_MARGIN = 60;
|
||||
|
||||
// during the 'booting' timeout, scrolls happen instantly instead of smoothly
|
||||
const BOOTING_TIMEOUT = 400;
|
||||
|
||||
|
||||
function DebugBorderBox(props: { heightPx: number, color: string }) {
|
||||
return (
|
||||
<Box sx={{
|
||||
position: 'absolute', bottom: 0, right: 0, left: 0,
|
||||
height: `${props.heightPx}px`,
|
||||
border: `1px solid ${props.color}`,
|
||||
pointerEvents: 'none',
|
||||
}} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export function ScrollToBottom(props: {
|
||||
bootToBottom?: boolean
|
||||
stickToBottom?: boolean
|
||||
sx?: SxProps
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
|
||||
// state
|
||||
|
||||
const [state, setState] = React.useState<ScrollToBottomState>({
|
||||
stickToBottom: props.stickToBottom || false,
|
||||
booting: props.bootToBottom || false,
|
||||
atBottom: undefined,
|
||||
});
|
||||
|
||||
// track scrollable (for events and to scroll it)
|
||||
const scrollableElementRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
// track programmatic scrolls
|
||||
const isProgrammaticScroll = React.useRef(false);
|
||||
|
||||
|
||||
// derived state
|
||||
|
||||
const bootToBottom = props.bootToBottom || false;
|
||||
const scrollBehavior: ScrollBehavior = state.booting ? 'auto' : 'smooth';
|
||||
|
||||
|
||||
// [Debugging]
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log('ScrollToBottom', { ...state });
|
||||
|
||||
|
||||
// main programmatic scroll to bottom function
|
||||
|
||||
const doScrollToBottom = React.useCallback(() => {
|
||||
const scrollable = scrollableElementRef.current;
|
||||
if (scrollable) {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log(' -> doScrollToBottom()', { scrollHeight: scrollable.scrollHeight, offsetHeight: scrollable.offsetHeight });
|
||||
|
||||
// eat the next scroll event
|
||||
isProgrammaticScroll.current = true;
|
||||
|
||||
// smooth scrolling only after booting
|
||||
scrollable.scrollTo({ top: scrollable.scrollHeight, behavior: scrollBehavior });
|
||||
}
|
||||
}, [scrollBehavior]);
|
||||
|
||||
|
||||
/**
|
||||
* Booting state reset (after BOOTING_TIMEOUT ms)
|
||||
* - the "Booting" window will scroll instantly instead of smoothly
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (!state.booting || !isBrowser) return;
|
||||
|
||||
const _clearBootingHandler = () => {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log(' -> booting done');
|
||||
|
||||
setState(state => ({ ...state, booting: false }));
|
||||
|
||||
if (bootToBottom)
|
||||
doScrollToBottom();
|
||||
};
|
||||
|
||||
// cancelable listener
|
||||
const timeout = window.setTimeout(_clearBootingHandler, BOOTING_TIMEOUT);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [bootToBottom, doScrollToBottom, state.booting]);
|
||||
|
||||
/**
|
||||
* Children elements resize event listener
|
||||
* - note that the 'scrollable' will likely have a fixed size, while its children are the ones who become scrollable
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
const scrollable = scrollableElementRef.current;
|
||||
if (!scrollable) return;
|
||||
|
||||
const _containerResizeObserver = new ResizeObserver(entries => {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log(' -> scrollable children resized', entries.length);
|
||||
|
||||
if (entries.length > 0 && state.stickToBottom)
|
||||
doScrollToBottom();
|
||||
});
|
||||
|
||||
|
||||
// cancelable observer of resize of scrollable's children elements
|
||||
Array.from(scrollable.children).forEach(child => _containerResizeObserver.observe(child));
|
||||
return () => _containerResizeObserver.disconnect();
|
||||
|
||||
}, [state.stickToBottom, doScrollToBottom]);
|
||||
|
||||
/**
|
||||
* (User) Scroll events listener
|
||||
* - will cancel any state.stickToBottom, if the user dragged the scroll bar
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
if (state.booting) return;
|
||||
|
||||
const scrollable = scrollableElementRef.current;
|
||||
if (!scrollable) return;
|
||||
|
||||
const _scrollEventsListener = () => {
|
||||
// ignore scroll events during programmatic scrolls
|
||||
// NOTE: some will go through, but somewhat the framework is stable
|
||||
if (isProgrammaticScroll.current) {
|
||||
isProgrammaticScroll.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// compute intersections
|
||||
const atBottom = scrollable.scrollHeight - scrollable.scrollTop <= scrollable.offsetHeight + USER_STICKY_MARGIN;
|
||||
|
||||
// assume this is = to the user intention
|
||||
const stickToBottom = atBottom;
|
||||
|
||||
// update state only if anything changed
|
||||
setState(state => (state.stickToBottom !== stickToBottom || state.atBottom !== atBottom)
|
||||
? ({ ...state, stickToBottom, atBottom })
|
||||
: state,
|
||||
);
|
||||
};
|
||||
|
||||
// _scrollEventsListener(true);
|
||||
|
||||
// cancelable listener (user and programatic scroll events)
|
||||
scrollable.addEventListener('scroll', _scrollEventsListener);
|
||||
return () => scrollable.removeEventListener('scroll', _scrollEventsListener);
|
||||
|
||||
}, [state.booting]);
|
||||
|
||||
|
||||
// actions for this context
|
||||
|
||||
const notifyBooting = React.useCallback(() => {
|
||||
if (bootToBottom)
|
||||
setState(state => state.booting ? state : ({ ...state, booting: true }));
|
||||
}, [bootToBottom]);
|
||||
|
||||
/*const notifyContentUpdated = React.useCallback(() => {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log('-= notifyContentUpdated');
|
||||
|
||||
if (state.stickToBottom)
|
||||
doScrollToBottom();
|
||||
}, [doScrollToBottom, state.stickToBottom]);*/
|
||||
|
||||
const setStickToBottom = React.useCallback((stickToBottom: boolean) => {
|
||||
if (DEBUG_SCROLL_TO_BOTTOM)
|
||||
console.log('-= setStickToBottom', stickToBottom);
|
||||
|
||||
setState(state => state.stickToBottom !== stickToBottom
|
||||
? ({ ...state, stickToBottom })
|
||||
: state,
|
||||
);
|
||||
|
||||
if (stickToBottom)
|
||||
doScrollToBottom();
|
||||
}, [doScrollToBottom]);
|
||||
|
||||
|
||||
return (
|
||||
<UseScrollToBottomProvider value={{
|
||||
...state,
|
||||
notifyBooting,
|
||||
setStickToBottom,
|
||||
}}>
|
||||
<Box ref={scrollableElementRef} sx={props.sx}>
|
||||
{props.children}
|
||||
{DEBUG_SCROLL_TO_BOTTOM && <DebugBorderBox heightPx={USER_STICKY_MARGIN} color='red' />}
|
||||
{DEBUG_SCROLL_TO_BOTTOM && <DebugBorderBox heightPx={100} color='blue' />}
|
||||
</Box>
|
||||
</UseScrollToBottomProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { IconButton } from '@mui/joy';
|
||||
import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown';
|
||||
|
||||
import { useScrollToBottom } from './useScrollToBottom';
|
||||
|
||||
|
||||
export function ScrollToBottomButton() {
|
||||
|
||||
// state
|
||||
const { atBottom, stickToBottom, setStickToBottom } = useScrollToBottom();
|
||||
|
||||
const handleStickToBottom = React.useCallback(() => {
|
||||
setStickToBottom(true);
|
||||
}, [setStickToBottom]);
|
||||
|
||||
// do not render the button at all if we're already snapping
|
||||
if (atBottom || stickToBottom)
|
||||
return null;
|
||||
|
||||
return (
|
||||
// <Tooltip title={
|
||||
// <Typography variant='solid' level='title-sm' sx={{ px: 1 }}>
|
||||
// Scroll to bottom
|
||||
// </Typography>
|
||||
// }>
|
||||
<IconButton
|
||||
variant='outlined' color='neutral' size='md'
|
||||
onClick={handleStickToBottom}
|
||||
sx={{
|
||||
// place this on the bottom-right corner (FAB-like)
|
||||
position: 'absolute',
|
||||
bottom: '2rem',
|
||||
right: {
|
||||
xs: '1rem',
|
||||
md: '2rem',
|
||||
},
|
||||
|
||||
// style it
|
||||
backgroundColor: 'background.surface',
|
||||
borderRadius: '50%',
|
||||
boxShadow: 'md',
|
||||
|
||||
// fade it in when hovering
|
||||
// transition: 'all 0.15s',
|
||||
// '&:hover': {
|
||||
// transform: 'scale(1.1)',
|
||||
// },
|
||||
}}
|
||||
>
|
||||
<KeyboardDoubleArrowDownIcon />
|
||||
</IconButton>
|
||||
// </Tooltip>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* State is minimal - to keep state machinery stable and simple
|
||||
*/
|
||||
export interface ScrollToBottomState {
|
||||
// config
|
||||
stickToBottom: boolean;
|
||||
|
||||
// state
|
||||
booting: boolean;
|
||||
atBottom: boolean | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actions are very simplified, for providing a minimal control surface from the outside
|
||||
*/
|
||||
export interface ScrollToBottomActions {
|
||||
notifyBooting: () => void;
|
||||
setStickToBottom: (stick: boolean) => void;
|
||||
}
|
||||
|
||||
type ScrollToBottomContext = ScrollToBottomState & ScrollToBottomActions;
|
||||
|
||||
const UseScrollToBottom = React.createContext<ScrollToBottomContext | undefined>(undefined);
|
||||
|
||||
export const UseScrollToBottomProvider = UseScrollToBottom.Provider;
|
||||
|
||||
export const useScrollToBottom = (): ScrollToBottomContext => {
|
||||
const context = React.useContext(UseScrollToBottom);
|
||||
if (!context)
|
||||
throw new Error('useScrollToBottom must be used within a ScrollToBottomProvider');
|
||||
return context;
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CmdRunBrowse } from '~/modules/browse/browse.client';
|
||||
import { CmdRunProdia } from '~/modules/prodia/prodia.client';
|
||||
import { CmdRunReact } from '~/modules/aifn/react/react';
|
||||
import { CmdRunSearch } from '~/modules/google/search.client';
|
||||
import { CmdRunT2I } from '~/modules/t2i/t2i.client';
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { createDMessage, DMessage } from '~/common/state/store-chats';
|
||||
|
||||
@@ -10,7 +10,7 @@ export const CmdAddRoleMessage: string[] = ['/assistant', '/a', '/system', '/s']
|
||||
|
||||
export const CmdHelp: string[] = ['/help', '/h', '/?'];
|
||||
|
||||
export const commands = [...CmdRunBrowse, ...CmdRunProdia, ...CmdRunReact, ...CmdRunSearch, ...CmdAddRoleMessage, ...CmdHelp];
|
||||
export const commands = [...CmdRunBrowse, ...CmdRunT2I, ...CmdRunReact, ...CmdRunSearch, ...CmdAddRoleMessage, ...CmdHelp];
|
||||
|
||||
export interface SentencePiece {
|
||||
type: 'text' | 'cmd';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SystemPurposeId, SystemPurposes } from '../../../data';
|
||||
import { createDMessage, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
|
||||
export function createAssistantTypingMessage(conversationId: string, assistantLlmLabel: DLLMId | 'prodia' | 'react-...' | 'web', assistantPurposeId: SystemPurposeId | undefined, text: string): string {
|
||||
export function createAssistantTypingMessage(conversationId: string, assistantLlmLabel: DLLMId | string /* 'DALL·E' | 'Prodia' | 'react-...' | 'web' */, assistantPurposeId: SystemPurposeId | undefined, text: string): string {
|
||||
const assistantMessage: DMessage = createDMessage('assistant', text);
|
||||
assistantMessage.typing = true;
|
||||
assistantMessage.purposeId = assistantPurposeId;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { prodiaGenerateImage } from '~/modules/prodia/prodia.client';
|
||||
import { getActiveTextToImageProviderOrThrow, t2iGenerateImageOrThrow } from '~/modules/t2i/t2i.client';
|
||||
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { createAssistantTypingMessage } from './editors';
|
||||
|
||||
|
||||
/**
|
||||
* The main 'image generation' function - for now specialized to the 'imagine' command.
|
||||
* Text to image, appended as an 'assistant' message
|
||||
*/
|
||||
export async function runImageGenerationUpdatingState(conversationId: string, imageText: string) {
|
||||
|
||||
@@ -17,21 +17,23 @@ export async function runImageGenerationUpdatingState(conversationId: string, im
|
||||
imageText = imageText.replace(/x(\d+)$|\[(\d+)]$/, '').trim(); // Remove the "xN" or "[N]" part from the imageText
|
||||
|
||||
// create a blank and 'typing' message for the assistant
|
||||
const assistantMessageId = createAssistantTypingMessage(conversationId, 'prodia', undefined,
|
||||
const assistantMessageId = createAssistantTypingMessage(conversationId, '', undefined,
|
||||
`Give me a few seconds while I draw ${imageText?.length > 20 ? 'that' : '"' + imageText + '"'}...`);
|
||||
|
||||
// reference the state editing functions
|
||||
const { editMessage } = useChatStore.getState();
|
||||
|
||||
try {
|
||||
const imageUrls = await prodiaGenerateImage(count, imageText);
|
||||
|
||||
// Concatenate all the resulting URLs and update the assistant message with these URLs
|
||||
const allImageUrls = imageUrls.join('\n');
|
||||
editMessage(conversationId, assistantMessageId, { text: allImageUrls, typing: false }, false);
|
||||
const t2iProvider = getActiveTextToImageProviderOrThrow();
|
||||
editMessage(conversationId, assistantMessageId, { originLLM: t2iProvider.painter }, false);
|
||||
|
||||
const imageUrls = await t2iGenerateImageOrThrow(t2iProvider, imageText, count);
|
||||
editMessage(conversationId, assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
|
||||
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || error?.toString() || 'Unknown error';
|
||||
editMessage(conversationId, assistantMessageId, { text: `Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
if (assistantMessageId)
|
||||
editMessage(conversationId, assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,8 @@ import { LogoProgress } from '~/common/components/LogoProgress';
|
||||
import { apiAsyncNode } from '~/common/util/trpc.client';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { conversationTitle } from '~/common/state/store-chats';
|
||||
import { useLayoutPluggable } from '~/common/layout/store-applayout';
|
||||
import { themeBgAppDarker } from '~/common/app.theme';
|
||||
import { usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { AppChatLinkDrawerItems } from './AppChatLinkDrawerItems';
|
||||
import { AppChatLinkMenuItems } from './AppChatLinkMenuItems';
|
||||
@@ -30,7 +31,7 @@ const Centerer = (props: { backgroundColor: string, children?: React.ReactNode }
|
||||
</Box>;
|
||||
|
||||
const ShowLoading = () =>
|
||||
<Centerer backgroundColor='background.level3'>
|
||||
<Centerer backgroundColor={themeBgAppDarker}>
|
||||
<LogoProgress showProgress={true} />
|
||||
<Typography level='title-sm' sx={{ mt: 2 }}>
|
||||
Loading Chat...
|
||||
@@ -38,7 +39,7 @@ const ShowLoading = () =>
|
||||
</Centerer>;
|
||||
|
||||
const ShowError = (props: { error: any }) =>
|
||||
<Centerer backgroundColor='background.level2'>
|
||||
<Centerer backgroundColor={themeBgAppDarker}>
|
||||
<InlineError error={props.error} severity='warning' />
|
||||
</Centerer>;
|
||||
|
||||
@@ -85,7 +86,7 @@ export function AppChatLink(props: { linkId: string }) {
|
||||
|
||||
const drawerItems = React.useMemo(() => <AppChatLinkDrawerItems />, []);
|
||||
const menuItems = React.useMemo(() => <AppChatLinkMenuItems />, []);
|
||||
useLayoutPluggable(null, hasLinkItems ? drawerItems : null, menuItems);
|
||||
usePluggableOptimaLayout(hasLinkItems ? drawerItems : null, null, menuItems, 'AppChatLink');
|
||||
|
||||
|
||||
const pageTitle = (data?.conversation && conversationTitle(data.conversation)) || 'Chat Link';
|
||||
@@ -102,7 +103,7 @@ export function AppChatLink(props: { linkId: string }) {
|
||||
? <ShowError error={error} />
|
||||
: !!data?.conversation
|
||||
? <ViewChatLink conversation={data.conversation} storedAt={data.storedAt} expiresAt={data.expiresAt} />
|
||||
: <Centerer backgroundColor='background.level3' />}
|
||||
: <Centerer backgroundColor={themeBgAppDarker} />}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -8,8 +8,8 @@ import { useChatLinkItems } from '~/modules/trade/store-module-trade';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { closeLayoutDrawer } from '~/common/layout/store-applayout';
|
||||
import { getChatLinkRelativePath, ROUTE_INDEX } from '~/common/app.routes';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
|
||||
/**
|
||||
@@ -19,6 +19,7 @@ import { getChatLinkRelativePath, ROUTE_INDEX } from '~/common/app.routes';
|
||||
export function AppChatLinkDrawerItems() {
|
||||
|
||||
// external state
|
||||
const { closeAppDrawer } = useOptimaLayout();
|
||||
const chatLinkItems = useChatLinkItems()
|
||||
.slice()
|
||||
.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
||||
@@ -27,7 +28,7 @@ export function AppChatLinkDrawerItems() {
|
||||
return <>
|
||||
|
||||
<MenuItem
|
||||
onClick={closeLayoutDrawer}
|
||||
onClick={closeAppDrawer}
|
||||
component={Link} href={ROUTE_INDEX} noLinkStyle
|
||||
>
|
||||
<ListItemDecorator><ArrowBackIcon /></ListItemDecorator>
|
||||
|
||||
@@ -9,7 +9,8 @@ import { useChatShowSystemMessages } from '../chat/store-app-chat';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { conversationTitle, DConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { navigateToChat } from '~/common/app.routes';
|
||||
import { launchAppChat } from '~/common/app.routes';
|
||||
import { themeBgAppDarker } from '~/common/app.theme';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
@@ -58,7 +59,7 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
|
||||
const handleClone = async (canOverwrite: boolean) => {
|
||||
setCloning(true);
|
||||
const importedId = useChatStore.getState().importConversation({ ...props.conversation }, !canOverwrite);
|
||||
await navigateToChat(importedId);
|
||||
await launchAppChat(importedId);
|
||||
setCloning(false);
|
||||
};
|
||||
|
||||
@@ -67,7 +68,7 @@ export function ViewChatLink(props: { conversation: DConversation, storedAt: Dat
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level3',
|
||||
backgroundColor: themeBgAppDarker,
|
||||
display: 'flex', flexFlow: 'column nowrap', minHeight: 96, alignItems: 'center',
|
||||
gap: { xs: 4, md: 5, xl: 6 },
|
||||
px: { xs: 2 },
|
||||
|
||||
@@ -10,6 +10,7 @@ import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { ROUTE_INDEX } from '~/common/app.routes';
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
|
||||
import { newsCallout, NewsItems } from './news.data';
|
||||
|
||||
@@ -43,7 +44,7 @@ export function AppNews() {
|
||||
|
||||
<Box sx={{
|
||||
flexGrow: 1,
|
||||
backgroundColor: 'background.level1',
|
||||
backgroundColor: themeBgApp,
|
||||
overflowY: 'auto',
|
||||
display: 'flex', justifyContent: 'center',
|
||||
p: { xs: 3, md: 6 },
|
||||
|
||||
@@ -10,7 +10,7 @@ import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
|
||||
|
||||
// update this variable every time you want to broadcast a new version to clients
|
||||
export const incrementalVersion: number = 9;
|
||||
export const incrementalVersion: number = 10;
|
||||
|
||||
const B = (props: { href?: string, children: React.ReactNode }) => {
|
||||
const boldText = <Typography color={!!props.href ? 'primary' : 'neutral'} sx={{ fontWeight: 600 }}>{props.children}</Typography>;
|
||||
@@ -58,19 +58,26 @@ export const newsCallout =
|
||||
|
||||
// news and feature surfaces
|
||||
export const NewsItems: NewsItem[] = [
|
||||
/*{
|
||||
// https://github.com/enricoros/big-agi/milestone/7
|
||||
// https://github.com/users/enricoros/projects/4/views/2
|
||||
versionName: '1.7.0',
|
||||
// still unannounced: phone calls, split windows, ...
|
||||
{
|
||||
versionCode: '1.9.0',
|
||||
versionName: 'Creative Horizons',
|
||||
versionMoji: '🎨🌌',
|
||||
versionDate: new Date('2023-12-28T22:30:00Z'),
|
||||
items: [
|
||||
// multi-window support
|
||||
// phone calls
|
||||
{ text: <><B href={RIssues + '/212'}>DALL·E 3</B> support (/draw), with advanced control</>, issue: 212 },
|
||||
{ text: <><B href={RIssues + '/304'}>Perfect scrolling</B> UX, on all devices</>, issue: 304 },
|
||||
{ text: <>Create personas <B href={RIssues + '/287'}>from text</B></>, issue: 287 },
|
||||
{ text: <>Openrouter: auto-detect models, support free-tiers and rates</>, issue: 291 },
|
||||
{ text: <>Image drawing: unified UX, including auto-prompting</> },
|
||||
{ text: <>Fix layout on Firefox</>, issue: 255 },
|
||||
{ text: <>Developers: new Text2Image subsystem, Optima layout subsystem, ScrollToBottom library, using new Panes library, improved Llms subsystem</>, dev: true },
|
||||
],
|
||||
},*/
|
||||
},
|
||||
{
|
||||
versionCode: '1.8.0',
|
||||
versionName: 'To The Moon And Back',
|
||||
versionMoji: '🚀🌕🔙❤️',
|
||||
// versionMoji: '🚀🌕🔙❤️',
|
||||
versionDate: new Date('2023-12-20T09:30:00Z'),
|
||||
items: [
|
||||
{ text: <><B href={RIssues + '/275'}>Google Gemini</B> models support</> },
|
||||
@@ -183,5 +190,6 @@ interface NewsItem {
|
||||
items?: {
|
||||
text: string | React.JSX.Element;
|
||||
dev?: boolean;
|
||||
issue?: number;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { navigateToNews } from '~/common/app.routes';
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
@@ -7,18 +6,15 @@ import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
import { incrementalVersion } from './news.data';
|
||||
|
||||
|
||||
export function useShowNewsOnUpdate() {
|
||||
const { usageCount, lastSeenNewsVersion } = useAppStateStore(state => ({
|
||||
usageCount: state.usageCount,
|
||||
lastSeenNewsVersion: state.lastSeenNewsVersion,
|
||||
}), shallow);
|
||||
export function useRedirectToNewsOnUpdates() {
|
||||
React.useEffect(() => {
|
||||
const { usageCount, lastSeenNewsVersion } = useAppStateStore.getState();
|
||||
const isNewsOutdated = (lastSeenNewsVersion || 0) < incrementalVersion;
|
||||
if (isNewsOutdated && usageCount > 2) {
|
||||
// Disable for now
|
||||
void navigateToNews();
|
||||
}
|
||||
}, [lastSeenNewsVersion, usageCount]);
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function useMarkNewsAsSeen() {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Container, ListDivider, Sheet, Typography } from '@mui/joy';
|
||||
import { Container, ListDivider, Sheet, Typography } from '@mui/joy';
|
||||
|
||||
import { YTPersonaCreator } from './YTPersonaCreator';
|
||||
import ScienceIcon from '@mui/icons-material/Science';
|
||||
import { themeBgApp } from '~/common/app.theme';
|
||||
|
||||
import { PersonaCreator } from './PersonaCreator';
|
||||
|
||||
|
||||
export function AppPersonas() {
|
||||
@@ -11,26 +12,19 @@ export function AppPersonas() {
|
||||
<Sheet sx={{
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto',
|
||||
backgroundColor: 'background.level1',
|
||||
backgroundColor: themeBgApp,
|
||||
p: { xs: 3, md: 6 },
|
||||
}}>
|
||||
|
||||
<Container disableGutters maxWidth='md' sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
|
||||
<Typography level='title-lg' sx={{ textAlign: 'center' }}>
|
||||
Advanced AI Personas
|
||||
AI Personas Creator
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 1 }}>
|
||||
<Typography>
|
||||
Experimental
|
||||
</Typography>
|
||||
<ScienceIcon color='primary' />
|
||||
</Box>
|
||||
|
||||
<ListDivider sx={{ my: 2 }} />
|
||||
|
||||
<YTPersonaCreator />
|
||||
<PersonaCreator />
|
||||
|
||||
</Container>
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, IconButton, Input, LinearProgress, Tooltip, Typography } from '@mui/joy';
|
||||
import { Alert, Box, Button, Card, CardContent, CircularProgress, Grid, Input, LinearProgress, Tab, TabList, TabPanel, Tabs, Textarea, Typography } from '@mui/joy';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import WhatshotIcon from '@mui/icons-material/Whatshot';
|
||||
import SettingsAccessibilityIcon from '@mui/icons-material/SettingsAccessibility';
|
||||
import TextFieldsIcon from '@mui/icons-material/TextFields';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { apiQuery } from '~/common/util/trpc.client';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
|
||||
@@ -37,9 +39,9 @@ function useTranscriptFromVideo(videoID: string | null) {
|
||||
}
|
||||
|
||||
|
||||
const YouTubePersonaSteps: LLMChainStep[] = [
|
||||
const PersonaCreationSteps: LLMChainStep[] = [
|
||||
{
|
||||
name: 'Analyzing the transcript',
|
||||
name: 'Analyzing the transcript / text',
|
||||
setSystem: 'You are skilled in analyzing and embodying diverse characters. You meticulously study transcripts to capture key attributes, draft comprehensive character sheets, and refine them for authenticity. Feel free to make assumptions without hedging, be concise and be creative.',
|
||||
addUserInput: true,
|
||||
addUser: 'Conduct comprehensive research on the provided transcript. Identify key characteristics of the speaker, including age, professional field, distinct personality traits, style of communication, narrative context, and self-awareness. Additionally, consider any unique aspects such as their use of humor, their cultural background, core values, passions, fears, personal history, and social interactions. Your output for this stage is an in-depth written analysis that exhibits an understanding of both the superficial and more profound aspects of the speaker\'s persona.',
|
||||
@@ -62,23 +64,34 @@ const YouTubePersonaSteps: LLMChainStep[] = [
|
||||
];
|
||||
|
||||
|
||||
export function YTPersonaCreator() {
|
||||
export function PersonaCreator() {
|
||||
// state
|
||||
const [videoURL, setVideoURL] = React.useState('');
|
||||
const [videoID, setVideoID] = React.useState('');
|
||||
const [personaTranscript, setPersonaTranscript] = React.useState<string | null>(null);
|
||||
const [personaText, setPersonaText] = React.useState('');
|
||||
const [selectedTab, setSelectedTab] = React.useState(0);
|
||||
|
||||
// external state
|
||||
const [diagramLlm, llmComponent] = useFormRadioLlmType();
|
||||
const [personaLlm, llmComponent] = useFormRadioLlmType('Persona Creation Model');
|
||||
|
||||
// fetch transcript when the Video ID is ready, then store it
|
||||
const { transcript, thumbnailUrl, title, isFetching, isError, error: transcriptError } =
|
||||
useTranscriptFromVideo(videoID);
|
||||
React.useEffect(() => setPersonaTranscript(transcript), [transcript]);
|
||||
|
||||
// Reset the relevant state when the selected tab changes
|
||||
React.useEffect(() => {
|
||||
// reset state
|
||||
setVideoURL('');
|
||||
setVideoID('');
|
||||
setPersonaTranscript(null);
|
||||
setPersonaText('');
|
||||
}, [selectedTab]);
|
||||
|
||||
// use the transformation sequence to create a persona
|
||||
const { isFinished, isTransforming, chainProgress, chainIntermediates, chainStepName, chainOutput, chainError, abortChain } =
|
||||
useLLMChain(YouTubePersonaSteps, diagramLlm?.id, personaTranscript ?? undefined);
|
||||
useLLMChain(PersonaCreationSteps, personaLlm?.id, personaTranscript ?? undefined);
|
||||
|
||||
const handleVideoIdChange = (e: React.ChangeEvent<HTMLInputElement>) => setVideoURL(e.target.value);
|
||||
|
||||
@@ -93,61 +106,89 @@ export function YTPersonaCreator() {
|
||||
}
|
||||
};
|
||||
|
||||
// New handler for persona text change
|
||||
const handlePersonaTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setPersonaText(e.target.value);
|
||||
};
|
||||
|
||||
return <>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 1 }}>
|
||||
<YouTubeIcon sx={{ color: '#f00' }} />
|
||||
<Typography level='title-lg'>
|
||||
YouTube -> AI persona
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography level='title-sm' mb={3}>
|
||||
Create the <em>System Prompt</em> of an AI Persona from YouTube or Text.
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleFetchTranscript}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', gap: 2 }}>
|
||||
<Input
|
||||
required
|
||||
type='url'
|
||||
fullWidth
|
||||
<Tabs defaultValue={0} variant='outlined'
|
||||
value={selectedTab}
|
||||
onChange={(event, newValue) => setSelectedTab(newValue as number)}>
|
||||
<TabList sx={{ minHeight: 48 }}>
|
||||
<Tab>From YouTube Video</Tab>
|
||||
<Tab>From Text</Tab>
|
||||
</TabList>
|
||||
|
||||
{/* YouTube URL inputs */}
|
||||
<TabPanel value={0} sx={{ p: 3 }}>
|
||||
|
||||
<Typography level='title-md' startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />} sx={{ mb: 3 }}>
|
||||
YouTube -> Persona
|
||||
</Typography>
|
||||
|
||||
<form onSubmit={handleFetchTranscript}>
|
||||
<Input
|
||||
required
|
||||
type='url'
|
||||
fullWidth
|
||||
variant='outlined'
|
||||
placeholder='YouTube Video URL'
|
||||
value={videoURL}
|
||||
onChange={handleVideoIdChange}
|
||||
sx={{ mb: 1.5 }}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Button type='submit' variant='solid' disabled={isFetching || isTransforming || !videoURL} loading={isFetching} sx={{ minWidth: 140 }}>
|
||||
Create
|
||||
</Button>
|
||||
<GoodTooltip title='This example comes from the popular Fireship YouTube channel, which presents technical topics with irreverent humor.'>
|
||||
<Button variant='outlined' color='neutral' onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}>
|
||||
Example
|
||||
</Button>
|
||||
</GoodTooltip>
|
||||
</Box>
|
||||
</form>
|
||||
</TabPanel>
|
||||
|
||||
{/* Text area for users to paste copied text */}
|
||||
<TabPanel value={1} sx={{ p: 3 }}>
|
||||
|
||||
<Typography level='title-md' startDecorator={<TextFieldsIcon />} sx={{ mb: 3 }}>
|
||||
<b>Text</b> -> Persona
|
||||
</Typography>
|
||||
|
||||
<Textarea
|
||||
variant='outlined'
|
||||
placeholder='YouTube Video URL'
|
||||
value={videoURL} onChange={handleVideoIdChange}
|
||||
endDecorator={
|
||||
<IconButton
|
||||
variant='outlined' color='neutral'
|
||||
onClick={() => setVideoURL('https://www.youtube.com/watch?v=M_wZpSEvOkc')}
|
||||
>
|
||||
<WhatshotIcon />
|
||||
</IconButton>
|
||||
}
|
||||
minRows={4} maxRows={8}
|
||||
placeholder='Paste your text here...'
|
||||
value={personaText}
|
||||
onChange={handlePersonaTextChange}
|
||||
sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
'&:focus-within': {
|
||||
backgroundColor: 'background.popup',
|
||||
},
|
||||
lineHeight: 1.75,
|
||||
mb: 1.5,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='solid' disabled={isFetching || isTransforming} loading={isFetching}
|
||||
sx={{ minWidth: 120 }}>
|
||||
Create
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Button variant='solid' disabled={isFetching || isTransforming || !personaText} onClick={() => setPersonaTranscript(personaText)} sx={{ minWidth: 140 }}>
|
||||
Create
|
||||
</Button>
|
||||
{!!personaText?.length && <Typography level='body-sm'>{personaText.length.toLocaleString()}</Typography>}
|
||||
</Box>
|
||||
</TabPanel>
|
||||
</Tabs>
|
||||
|
||||
{/* LLM selector (chat vs fast) */}
|
||||
{!isTransforming && !isFinished && llmComponent}
|
||||
|
||||
{/* 1. Transcript*/}
|
||||
{personaTranscript && (
|
||||
<Card sx={{ mt: 2, boxShadow: 'md' }}>
|
||||
<CardContent>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
{title || 'Transcript'}
|
||||
</Typography>
|
||||
<Box>
|
||||
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Image' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
|
||||
<Typography level='body-sm'>
|
||||
{personaTranscript.slice(0, 280)}...
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
{!isTransforming && !isFinished && <Box sx={{ mt: 3 }}>{llmComponent}</Box>}
|
||||
|
||||
{/* Errors */}
|
||||
{isError && (
|
||||
@@ -161,49 +202,64 @@ export function YTPersonaCreator() {
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
|
||||
{/* Persona! */}
|
||||
{chainOutput && <Box sx={{ mt: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
YouTuber Persona System Prompt
|
||||
</Typography>
|
||||
<Card sx={{ boxShadow: 'md' }}>
|
||||
<CardContent sx={{
|
||||
position: 'relative',
|
||||
'&:hover > button': { opacity: 1 },
|
||||
}}>
|
||||
{chainOutput && <>
|
||||
<Card sx={{ boxShadow: 'md', mt: 3 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Typography level='title-lg' color='success' startDecorator={<SettingsAccessibilityIcon color='success' />}>
|
||||
Persona Prompt
|
||||
</Typography>
|
||||
<GoodTooltip title='Copy system prompt'>
|
||||
<Button color='success' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')} endDecorator={<ContentCopyIcon />} sx={{ minWidth: 120 }}>
|
||||
Copy
|
||||
</Button>
|
||||
</GoodTooltip>
|
||||
</Box>
|
||||
<CardContent>
|
||||
<Alert variant='soft' color='success' sx={{ mb: 1 }}>
|
||||
You can now copy the following text and use it as Custom prompt!
|
||||
You may now copy the text below and use it as Custom prompt!
|
||||
</Alert>
|
||||
<Tooltip title='Copy system prompt' variant='solid'>
|
||||
<IconButton
|
||||
variant='outlined' color='neutral' onClick={() => copyToClipboard(chainOutput, 'Persona prompt')}
|
||||
sx={{
|
||||
position: 'absolute', right: 0, zIndex: 10,
|
||||
// opacity: 0, transition: 'opacity 0.3s',
|
||||
}}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography level='body-sm'>
|
||||
<Typography level='title-sm' sx={{ lineHeight: 1.75 }}>
|
||||
{chainOutput}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>}
|
||||
</>}
|
||||
|
||||
{/* Input: Transcript*/}
|
||||
{personaTranscript && <>
|
||||
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
|
||||
Input Data
|
||||
</Typography>
|
||||
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography level='title-md' sx={{ mb: 1 }}>
|
||||
{title || 'Transcript / Text'}
|
||||
</Typography>
|
||||
<Box>
|
||||
{!!thumbnailUrl && <picture><img src={thumbnailUrl} alt='YouTube Video Thumbnail' height={80} style={{ float: 'left', marginRight: 8 }} /></picture>}
|
||||
<Typography level='body-sm'>
|
||||
{personaTranscript.slice(0, 280)}...
|
||||
</Typography>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>}
|
||||
|
||||
{/* Intermediate outputs rendered as cards in a grid */}
|
||||
{chainIntermediates && chainIntermediates.length > 0 && <Box sx={{ mt: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
{chainIntermediates && chainIntermediates.length > 0 && <>
|
||||
<Typography level='title-lg' sx={{ mt: 3, mb: 0.5 }}>
|
||||
{isTransforming ? 'Working...' : 'Intermediate Work'}
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
{chainIntermediates.map((intermediate, i) =>
|
||||
<Grid xs={12} sm={6} md={4} key={i}>
|
||||
<Card>
|
||||
<Card sx={{ height: '100%' }}>
|
||||
<CardContent>
|
||||
<Typography level='title-sm' sx={{ mb: 1 }}>
|
||||
{i + 1}. {YouTubePersonaSteps[i].name}
|
||||
{i + 1}. {PersonaCreationSteps[i].name}
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
{intermediate?.slice(0, 140)}...
|
||||
@@ -213,27 +269,35 @@ export function YTPersonaCreator() {
|
||||
</Grid>,
|
||||
)}
|
||||
</Grid>
|
||||
</Box>}
|
||||
</>}
|
||||
|
||||
|
||||
{/* Embodiment Progress */}
|
||||
{/* Dialog: Embodiment Progress */}
|
||||
{isTransforming && <GoodModal open>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', my: 2 }}>
|
||||
<CircularProgress color='primary' value={Math.max(10, 100 * chainProgress)} />
|
||||
</Box>
|
||||
<Typography color='success' level='title-lg' sx={{ mt: 1 }}>
|
||||
Embodying Persona ...
|
||||
</Typography>
|
||||
<Typography color='success' level='title-sm' sx={{ mt: 1, fontWeight: 600 }}>
|
||||
{chainStepName}
|
||||
</Typography>
|
||||
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1, mb: 2 }} />
|
||||
<Box>
|
||||
<Typography color='success' level='title-lg'>
|
||||
Embodying Persona ...
|
||||
</Typography>
|
||||
<Typography level='title-sm' sx={{ mt: 1 }}>
|
||||
Using: {personaLlm?.label}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box>
|
||||
<Typography color='success' level='title-sm' sx={{ fontWeight: 600 }}>
|
||||
{chainStepName}
|
||||
</Typography>
|
||||
<LinearProgress color='success' determinate value={Math.max(10, 100 * chainProgress)} sx={{ mt: 1.5 }} />
|
||||
</Box>
|
||||
<Typography level='title-sm'>
|
||||
This may take 1-2 minutes. Do not close this window or the progress will be lost.
|
||||
If you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
|
||||
While larger models will produce higher quality prompts,
|
||||
if you experience any errors (e.g. LLM timeouts, or context overflows for larger videos)
|
||||
please try again with faster/smaller models.
|
||||
</Typography>
|
||||
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 5 }}>
|
||||
<Button variant='soft' color='neutral' onClick={abortChain} sx={{ ml: 'auto', minWidth: 100, mt: 3 }}>
|
||||
Cancel
|
||||
</Button>
|
||||
</GoodModal>}
|
||||
@@ -9,8 +9,8 @@ import WidthWideIcon from '@mui/icons-material/WidthWide';
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormRadioControl } from '~/common/components/forms/FormRadioControl';
|
||||
import { isPwa } from '~/common/util/pwaUtils';
|
||||
import { openLayoutModelsSetup } from '~/common/layout/store-applayout';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
|
||||
@@ -18,10 +18,14 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
const SHOW_PURPOSE_FINDER = false;
|
||||
|
||||
|
||||
const ModelOptionsButton = () =>
|
||||
<Button
|
||||
const ModelsSetupButton = () => {
|
||||
|
||||
// external state
|
||||
const { openModelsSetup } = useOptimaLayout();
|
||||
|
||||
return <Button
|
||||
// variant='soft' color='success'
|
||||
onClick={openLayoutModelsSetup}
|
||||
onClick={openModelsSetup}
|
||||
startDecorator={<BuildCircleIcon />}
|
||||
sx={{
|
||||
'--Icon-fontSize': 'var(--joy-fontSize-xl2)',
|
||||
@@ -29,6 +33,7 @@ const ModelOptionsButton = () =>
|
||||
>
|
||||
Models
|
||||
</Button>;
|
||||
};
|
||||
|
||||
|
||||
export function AppChatSettingsUI() {
|
||||
@@ -64,7 +69,7 @@ export function AppChatSettingsUI() {
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title='AI Models'
|
||||
description='Setup' />
|
||||
<ModelOptionsButton />
|
||||
<ModelsSetupButton />
|
||||
</FormControl>
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between' }}>
|
||||
|
||||
@@ -6,12 +6,13 @@ import ScienceIcon from '@mui/icons-material/Science';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
|
||||
import { BrowseSettings } from '~/modules/browse/BrowseSettings';
|
||||
import { DallESettings } from '~/modules/t2i/dalle/DallESettings';
|
||||
import { ElevenlabsSettings } from '~/modules/elevenlabs/ElevenlabsSettings';
|
||||
import { GoogleSearchSettings } from '~/modules/google/GoogleSearchSettings';
|
||||
import { ProdiaSettings } from '~/modules/prodia/ProdiaSettings';
|
||||
import { ProdiaSettings } from '~/modules/t2i/prodia/ProdiaSettings';
|
||||
import { T2ISettings } from '~/modules/t2i/T2ISettings';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { closeLayoutPreferences, openLayoutShortcuts, useLayoutPreferencesTab } from '~/common/layout/store-applayout';
|
||||
import { settingsGap } from '~/common/app.theme';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
|
||||
@@ -100,20 +101,24 @@ function Topic(props: { title?: string, icon?: string | React.ReactNode, startCo
|
||||
* Component that allows the User to modify the application settings,
|
||||
* persisted on the client via localStorage.
|
||||
*/
|
||||
export function SettingsModal() {
|
||||
export function SettingsModal(props: {
|
||||
open: boolean,
|
||||
tabIndex: number,
|
||||
onClose: () => void,
|
||||
onOpenShortcuts: () => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const settingsTabIndex = useLayoutPreferencesTab();
|
||||
|
||||
const tabFixSx = { fontFamily: 'body', flex: 1, p: 0, m: 0 };
|
||||
|
||||
return (
|
||||
<GoodModal
|
||||
title='Preferences' strongerTitle
|
||||
open={!!settingsTabIndex} onClose={closeLayoutPreferences}
|
||||
open={props.open} onClose={props.onClose}
|
||||
startButton={isMobile ? undefined : (
|
||||
<Button variant='soft' onClick={openLayoutShortcuts}>
|
||||
<Button variant='soft' onClick={props.onOpenShortcuts}>
|
||||
👉 See Shortcuts
|
||||
</Button>
|
||||
)}
|
||||
@@ -124,7 +129,7 @@ export function SettingsModal() {
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tabs aria-label='Settings tabbed menu' defaultValue={settingsTabIndex}>
|
||||
<Tabs aria-label='Settings tabbed menu' defaultValue={props.tabIndex}>
|
||||
<TabList
|
||||
variant='soft'
|
||||
disableUnderline
|
||||
@@ -151,7 +156,7 @@ export function SettingsModal() {
|
||||
<Tab disableIndicator value={4} sx={tabFixSx}>Tools</Tab>
|
||||
</TabList>
|
||||
|
||||
<TabPanel value={1} sx={{ p: 'var(--Tabs-gap)' }}>
|
||||
<TabPanel value={1} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic>
|
||||
<AppChatSettingsUI />
|
||||
@@ -165,7 +170,7 @@ export function SettingsModal() {
|
||||
</Topics>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={3} sx={{ p: 'var(--Tabs-gap)' }}>
|
||||
<TabPanel value={3} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic icon='🎙️' title='Voice settings'>
|
||||
<VoiceSettings />
|
||||
@@ -176,15 +181,21 @@ export function SettingsModal() {
|
||||
</Topics>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={2} sx={{ p: 'var(--Tabs-gap)' }}>
|
||||
<TabPanel value={2} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic icon='🖍️️' title='Prodia API'>
|
||||
<Topic>
|
||||
<T2ISettings />
|
||||
</Topic>
|
||||
<Topic icon='🖍️️' title='OpenAI DALL·E' startCollapsed>
|
||||
<DallESettings />
|
||||
</Topic>
|
||||
<Topic icon='🖍️️' title='Prodia API' startCollapsed>
|
||||
<ProdiaSettings />
|
||||
</Topic>
|
||||
</Topics>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={4} sx={{ p: 'var(--Tabs-gap)' }}>
|
||||
<TabPanel value={4} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic icon={<SearchIcon />} title='Browsing' startCollapsed>
|
||||
<BrowseSettings />
|
||||
|
||||
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import { ChatMessage } from '../chat/components/message/ChatMessage';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { closeLayoutShortcuts, useLayoutShortcuts } from '~/common/layout/store-applayout';
|
||||
import { createDMessage } from '~/common/state/store-chats';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
|
||||
@@ -36,17 +35,9 @@ const shortcutsMd = `
|
||||
const shortcutsMessage = createDMessage('assistant', platformAwareKeystrokes(shortcutsMd));
|
||||
|
||||
|
||||
export function ShortcutsModal() {
|
||||
|
||||
// external state
|
||||
const showShortcuts = useLayoutShortcuts();
|
||||
|
||||
export function ShortcutsModal(props: { onClose: () => void }) {
|
||||
return (
|
||||
<GoodModal
|
||||
open={showShortcuts}
|
||||
title='Desktop Shortcuts'
|
||||
onClose={closeLayoutShortcuts}
|
||||
>
|
||||
<GoodModal open title='Desktop Shortcuts' onClose={props.onClose}>
|
||||
<ChatMessage message={shortcutsMessage} hideAvatars noBottomBorder sx={{ p: 0, m: 0 }} />
|
||||
</GoodModal>
|
||||
);
|
||||
|
||||
@@ -3,9 +3,7 @@ import * as React from 'react';
|
||||
import { FormControl, Typography } from '@mui/joy';
|
||||
import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
|
||||
import CallIcon from '@mui/icons-material/Call';
|
||||
import FormatPaintIcon from '@mui/icons-material/FormatPaint';
|
||||
import VerticalSplitIcon from '@mui/icons-material/VerticalSplit';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
|
||||
@@ -19,22 +17,12 @@ export function UxLabsSettings() {
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const {
|
||||
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsMagicDraw, labsPersonaYTCreator, labsSplitBranching,
|
||||
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsMagicDraw, setLabsPersonaYTCreator, setLabsSplitBranching,
|
||||
labsCalling, labsCameraDesktop, /*labsEnhancedUI,*/ labsSplitBranching,
|
||||
setLabsCalling, setLabsCameraDesktop, /*setLabsEnhancedUI,*/ setLabsSplitBranching,
|
||||
} = useUXLabsStore();
|
||||
|
||||
return <>
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><YouTubeIcon color={labsPersonaYTCreator ? 'primary' : undefined} sx={{ mr: 0.25 }} /> YouTube Personas</>} description={labsPersonaYTCreator ? 'Creator Enabled' : 'Disabled'}
|
||||
checked={labsPersonaYTCreator} onChange={setLabsPersonaYTCreator}
|
||||
/>
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><FormatPaintIcon color={labsMagicDraw ? 'primary' : undefined} sx={{ mr: 0.25 }} />Assisted Draw</>} description={labsMagicDraw ? 'Enabled' : 'Disabled'}
|
||||
checked={labsMagicDraw} onChange={setLabsMagicDraw}
|
||||
/>
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><CallIcon color={labsCalling ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Voice Calls</>} description={labsCalling ? 'Call AGI' : 'Disabled'}
|
||||
checked={labsCalling} onChange={setLabsCalling}
|
||||
@@ -58,7 +46,7 @@ export function UxLabsSettings() {
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title='Graduated' />
|
||||
<Typography level='body-xs'>
|
||||
<Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Relative chat size · Text Tools · LLM Overheat
|
||||
<Link href='https://github.com/enricoros/big-AGI/issues/282' target='_blank'>Persona Creator</Link> · <Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link> · Imagine · Relative chat size · Text Tools · LLM Overheat
|
||||
</Typography>
|
||||
</FormControl>
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ export const Brand = {
|
||||
Common: (process.env.NODE_ENV === 'development' ? '[DEV] ' : '') + 'big-AGI',
|
||||
},
|
||||
Meta: {
|
||||
Description: 'Leading open-source AI web interface to help you learn, think, and do. AI personas, superior privacy, advanced features, and fun UX.',
|
||||
SiteName: 'big-AGI | Harnessing AI for You',
|
||||
Description: 'Launch big-AGI to unlock the full potential of AI, with precise control over your data and models. Voice interface, AI personas, advanced features, and fun UX.',
|
||||
SiteName: 'big-AGI | Precision AI for You',
|
||||
ThemeColor: '#32383E',
|
||||
TwitterSite: '@enricoros',
|
||||
},
|
||||
|
||||
+24
-17
@@ -12,8 +12,10 @@ import { isBrowser } from './util/pwaUtils';
|
||||
|
||||
export const ROUTE_INDEX = '/';
|
||||
export const ROUTE_APP_CHAT = '/';
|
||||
export const ROUTE_APP_CALL = '/call';
|
||||
export const ROUTE_APP_LINK_CHAT = '/link/chat/:linkId';
|
||||
export const ROUTE_APP_NEWS = '/news';
|
||||
export const ROUTE_APP_PERSONAS = '/personas';
|
||||
const ROUTE_CALLBACK_OPENROUTER = '/link/callback_openrouter';
|
||||
|
||||
|
||||
@@ -38,23 +40,10 @@ export const getChatLinkRelativePath = (chatLinkId: string) => ROUTE_APP_LINK_CH
|
||||
|
||||
export const navigateToIndex = navigateFn(ROUTE_INDEX);
|
||||
|
||||
export const navigateToChat = async (conversationId?: DConversationId) => {
|
||||
if (conversationId) {
|
||||
await Router.push(
|
||||
{
|
||||
pathname: ROUTE_APP_CHAT,
|
||||
query: {
|
||||
conversationId,
|
||||
},
|
||||
},
|
||||
ROUTE_APP_CHAT,
|
||||
);
|
||||
} else {
|
||||
await Router.push(ROUTE_APP_CHAT, ROUTE_APP_CHAT);
|
||||
}
|
||||
};
|
||||
export const navigateToNews = navigateFn(ROUTE_APP_NEWS);
|
||||
|
||||
export const navigateToPersonas = navigateFn(ROUTE_APP_PERSONAS);
|
||||
|
||||
export const navigateBack = Router.back;
|
||||
|
||||
export const reloadPage = () => isBrowser && window.location.reload();
|
||||
@@ -66,6 +55,24 @@ function navigateFn(path: string) {
|
||||
|
||||
/// Launch Apps
|
||||
|
||||
/* Note: not used yet
|
||||
export interface AppChatQueryParams {
|
||||
conversationId?: string;
|
||||
}*/
|
||||
|
||||
export const launchAppChat = async (conversationId?: DConversationId) => {
|
||||
await Router.push(
|
||||
{
|
||||
pathname: ROUTE_APP_CHAT,
|
||||
query: conversationId ? {
|
||||
conversationId,
|
||||
} /*satisfies AppChatQueryParams*/
|
||||
: undefined,
|
||||
},
|
||||
ROUTE_APP_CHAT,
|
||||
);
|
||||
};
|
||||
|
||||
export interface AppCallQueryParams {
|
||||
conversationId: string;
|
||||
personaId: string;
|
||||
@@ -74,12 +81,12 @@ export interface AppCallQueryParams {
|
||||
export function launchAppCall(conversationId: string, personaId: string) {
|
||||
void Router.push(
|
||||
{
|
||||
pathname: `/call`,
|
||||
pathname: ROUTE_APP_CALL,
|
||||
query: {
|
||||
conversationId,
|
||||
personaId,
|
||||
} satisfies AppCallQueryParams,
|
||||
},
|
||||
// '/call',
|
||||
// ROUTE_APP_CALL,
|
||||
).then();
|
||||
}
|
||||
+18
-6
@@ -48,9 +48,15 @@ export const appTheme = extendTheme({
|
||||
secondary: 'var(--joy-palette-neutral-800)', // increase contrast a bit
|
||||
// tertiary: 'var(--joy-palette-neutral-700)', // increase contrast a bit
|
||||
},
|
||||
// popup [white] > surface [50] > level1 [100] > level2 [200] > level3 [300] > body [white -> 400]
|
||||
// popup [white] > surface [50] > level1 [100] > level2 [200] > level3 [300 -> unused] > body [white -> 300]
|
||||
background: {
|
||||
body: 'var(--joy-palette-neutral-400, #9FA6AD)', // background to stand back after all levels
|
||||
// New
|
||||
surface: 'var(--joy-palette-neutral-50, #FBFCFE)',
|
||||
level1: 'var(--joy-palette-neutral-100, #F0F4F8)',
|
||||
level2: 'var(--joy-palette-neutral-200, #DDE7EE)',
|
||||
body: 'var(--joy-palette-neutral-300, #CDD7E1)',
|
||||
// Former
|
||||
// body: 'var(--joy-palette-neutral-400, #9FA6AD)',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -62,10 +68,12 @@ export const appTheme = extendTheme({
|
||||
// tertiary: 'var(--joy-palette-neutral-400, #9FA6AD)',
|
||||
},
|
||||
background: {
|
||||
surface: 'var(--joy-palette-neutral-900, #131318)',
|
||||
level1: 'var(--joy-palette-common-black, #09090D)',
|
||||
level2: 'var(--joy-palette-neutral-800, #25252D)',
|
||||
// popup: 'var(--joy-palette-common-black, #09090D)',
|
||||
// New
|
||||
surface: 'var(--joy-palette-neutral-800, #171A1C)',
|
||||
level1: 'var(--joy-palette-neutral-900, #0B0D0E)',
|
||||
level2: 'var(--joy-palette-neutral-800, #171A1C)',
|
||||
body: 'var(--joy-palette-common-black, #000)',
|
||||
// Former: surface [900] > level 1 [black], level 2 [800] > body [black]
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -134,6 +142,10 @@ export const appTheme = extendTheme({
|
||||
},
|
||||
});
|
||||
|
||||
export const themeBgApp = 'background.level1';
|
||||
export const themeBgAppDarker = 'background.level2';
|
||||
export const themeBgAppChatComposer = 'background.surface';
|
||||
|
||||
export const bodyFontClassName = inter.className;
|
||||
export const themeBreakpoints = appTheme.breakpoints.values;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as React from 'react';
|
||||
import { KeyboardEvent } from 'react';
|
||||
|
||||
import { ClickAwayListener, Popper, PopperPlacementType } from '@mui/base';
|
||||
import { MenuList, styled } from '@mui/joy';
|
||||
@@ -37,12 +36,12 @@ export function CloseableMenu(props: {
|
||||
children?: React.ReactNode,
|
||||
}) {
|
||||
|
||||
const handleClose = (event: MouseEvent | TouchEvent | KeyboardEvent) => {
|
||||
const handleClose = (event: MouseEvent | TouchEvent | React.KeyboardEvent) => {
|
||||
event.stopPropagation();
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const handleListKeyDown = (event: KeyboardEvent) => {
|
||||
const handleListKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Tab') {
|
||||
handleClose(event);
|
||||
} else if (event.key === 'Escape') {
|
||||
|
||||
@@ -16,7 +16,7 @@ export type DropdownItems = Record<string, {
|
||||
/**
|
||||
* A Select component that blends-in nicely (cleaner, easier to the eyes)
|
||||
*/
|
||||
export function AppBarDropdown<TValue extends string>(props: {
|
||||
export function GoodDropdown<TValue extends string>(props: {
|
||||
items: DropdownItems,
|
||||
prependOption?: React.JSX.Element,
|
||||
appendOption?: React.JSX.Element,
|
||||
@@ -1,20 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
/**
|
||||
* Prevents the children from being rendered on the server.
|
||||
*
|
||||
* This is vital for using localStorage, which is not available on the server, and which
|
||||
* state is loaded synchronously on the client.
|
||||
*
|
||||
* The discrepancy between server and client state can cause hydration errors for React,
|
||||
* and we avoid those by using this wrapper.
|
||||
*
|
||||
* Suggestion: use sparingly, to show you are aware of the root causes of hydration errors.
|
||||
*/
|
||||
export const NoSSR = ({ children }: { children: any }): React.JSX.Element | null => {
|
||||
const [isMounted, setIsMounted] = React.useState(false);
|
||||
|
||||
React.useEffect(() => setIsMounted(true), []);
|
||||
|
||||
return isMounted ? children : null;
|
||||
};
|
||||
@@ -19,7 +19,7 @@ export const FormRadioControl = <TValue extends string>(props: {
|
||||
tooltip?: string | React.JSX.Element,
|
||||
disabled?: boolean;
|
||||
options: FormRadioOption<TValue>[];
|
||||
value: TValue;
|
||||
value?: TValue;
|
||||
onChange: (value: TValue) => void;
|
||||
}) =>
|
||||
<FormControl orientation='horizontal' disabled={props.disabled} sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
@@ -28,6 +28,7 @@ export const FormRadioControl = <TValue extends string>(props: {
|
||||
orientation='horizontal'
|
||||
value={props.value}
|
||||
onChange={(event: React.ChangeEvent<HTMLInputElement>) => event.target.value && props.onChange(event.target.value as TValue)}
|
||||
sx={{ flexWrap: 'wrap' }}
|
||||
>
|
||||
{props.options.map((option) =>
|
||||
<Radio key={'opt-' + option.value} value={option.value} label={option.label} disabled={option.disabled || props.disabled} />,
|
||||
|
||||
@@ -11,7 +11,9 @@ import type { ToggleableBoolean } from '~/common/util/useToggleableBoolean';
|
||||
*/
|
||||
export function SetupFormRefetchButton(props: {
|
||||
refetch: () => void,
|
||||
disabled: boolean, error: boolean,
|
||||
disabled: boolean,
|
||||
loading: boolean,
|
||||
error: boolean,
|
||||
leftButton?: React.ReactNode,
|
||||
advanced?: ToggleableBoolean
|
||||
}) {
|
||||
@@ -29,6 +31,7 @@ export function SetupFormRefetchButton(props: {
|
||||
<Button
|
||||
color={props.error ? 'warning' : 'primary'}
|
||||
disabled={props.disabled}
|
||||
loading={props.loading}
|
||||
endDecorator={<SyncIcon />}
|
||||
onClick={props.refetch}
|
||||
sx={{ minWidth: 120, ml: 'auto' }}
|
||||
|
||||
@@ -33,13 +33,25 @@ export interface CapabilityElevenLabsSpeechSynthesis {
|
||||
export { useCapability as useCapabilityElevenLabs } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
|
||||
|
||||
/// Image Generation: Prodia
|
||||
/// Image Generation
|
||||
|
||||
export interface CapabilityProdiaImageGeneration {
|
||||
mayWork: boolean;
|
||||
export interface TextToImageProvider {
|
||||
id: string;
|
||||
label: string;
|
||||
painter: string;
|
||||
description: string;
|
||||
configured: boolean;
|
||||
vendor: 'openai' | 'prodia';
|
||||
}
|
||||
|
||||
export { useCapability as useCapabilityProdia } from '~/modules/prodia/prodia.client';
|
||||
export interface CapabilityTextToImage {
|
||||
mayWork: boolean;
|
||||
providers: TextToImageProvider[],
|
||||
activeProviderId: string | null;
|
||||
setActiveProviderId: (providerId: string | null) => void;
|
||||
}
|
||||
|
||||
export { useCapabilityTextToImage } from '~/modules/t2i/t2i.client';
|
||||
|
||||
|
||||
/// Browsing
|
||||
|
||||
@@ -4,8 +4,9 @@ import { themeBreakpoints } from '../app.theme';
|
||||
|
||||
import { isBrowser } from '~/common/util/pwaUtils';
|
||||
|
||||
export const isMobileQuery = () => `(max-width: ${themeBreakpoints.md - 1}px)`;
|
||||
|
||||
export const useIsMobile = (): boolean => useMatchMedia(`(max-width: ${themeBreakpoints.md - 1}px)`, false);
|
||||
export const useIsMobile = (): boolean => useMatchMedia(isMobileQuery(), false);
|
||||
|
||||
export function useMatchMedia(query: string, ssrValue: boolean): boolean {
|
||||
const [matches, setMatches] = React.useState(isBrowser ? window.matchMedia(query).matches : ssrValue);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, Container } from '@mui/joy';
|
||||
|
||||
import { ModelsModal } from '~/modules/llms/models-modal/ModelsModal';
|
||||
import { SettingsModal } from '../../apps/settings-modal/SettingsModal';
|
||||
import { ShortcutsModal } from '../../apps/settings-modal/ShortcutsModal';
|
||||
|
||||
import { isPwa } from '~/common/util/pwaUtils';
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { AppBar } from './AppBar';
|
||||
import { GlobalShortcutItem, useGlobalShortcuts } from '../components/useGlobalShortcut';
|
||||
import { NoSSR } from '../components/NoSSR';
|
||||
import { openLayoutModelsSetup, openLayoutPreferences, openLayoutShortcuts } from './store-applayout';
|
||||
|
||||
|
||||
export function AppLayout(props: {
|
||||
noAppBar?: boolean, suspendAutoModelsSetup?: boolean,
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
// external state
|
||||
const { centerMode } = useUIPreferencesStore(state => ({ centerMode: isPwa() ? 'full' : state.centerMode }), shallow);
|
||||
|
||||
// usage counter, for progressive disclosure of features
|
||||
useAppStateStore(state => state.usageCount);
|
||||
|
||||
// global shortcuts for modals
|
||||
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
|
||||
['m', true, true, false, openLayoutModelsSetup],
|
||||
['p', true, true, false, openLayoutPreferences],
|
||||
['?', true, true, false, openLayoutShortcuts],
|
||||
], []);
|
||||
useGlobalShortcuts(shortcuts);
|
||||
|
||||
return (
|
||||
// Global NoSSR wrapper: the overall Container could have hydration issues when using localStorage and non-default maxWidth
|
||||
<NoSSR>
|
||||
|
||||
<Container
|
||||
disableGutters
|
||||
maxWidth={centerMode === 'full' ? false : centerMode === 'narrow' ? 'md' : 'xl'}
|
||||
sx={{
|
||||
boxShadow: {
|
||||
xs: 'none',
|
||||
md: centerMode === 'narrow' ? 'md' : 'none',
|
||||
xl: centerMode !== 'full' ? 'lg' : 'none',
|
||||
},
|
||||
}}>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
height: '100dvh',
|
||||
}}>
|
||||
|
||||
{!props.noAppBar && <AppBar sx={{
|
||||
zIndex: 20, // position: 'sticky', top: 0,
|
||||
}} />}
|
||||
|
||||
{props.children}
|
||||
|
||||
</Box>
|
||||
|
||||
</Container>
|
||||
|
||||
{/* Overlay Settings */}
|
||||
<SettingsModal />
|
||||
|
||||
{/* Overlay Models (& Model Options )*/}
|
||||
<ModelsModal suspendAutoModelsSetup={props.suspendAutoModelsSetup} />
|
||||
|
||||
{/* Overlay Shortcuts */}
|
||||
<ShortcutsModal />
|
||||
|
||||
</NoSSR>
|
||||
);
|
||||
}
|
||||
@@ -9,19 +9,19 @@ import MenuIcon from '@mui/icons-material/Menu';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
|
||||
|
||||
import { Brand } from '../app.config';
|
||||
import { CloseableMenu } from '../components/CloseableMenu';
|
||||
import { Link } from '../components/Link';
|
||||
import { LogoSquircle } from '../components/LogoSquircle';
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { CloseableMenu } from '~/common/components/CloseableMenu';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { LogoSquircle } from '~/common/components/LogoSquircle';
|
||||
import { ROUTE_INDEX } from '~/common/app.routes';
|
||||
|
||||
// import { AppBarSupportItem } from './AppBarSupportItem';
|
||||
import { AppBarSwitcherItem } from './AppBarSwitcherItem';
|
||||
import { closeLayoutDrawer, closeLayoutMenu, openLayoutPreferences, setLayoutDrawerAnchor, setLayoutMenuAnchor, useLayoutComponents } from './store-applayout';
|
||||
import { useOptimaLayout } from './useOptimaLayout';
|
||||
|
||||
|
||||
function AppBarTitle() {
|
||||
return (
|
||||
<Link href='/'>
|
||||
<Link href={ROUTE_INDEX}>
|
||||
<LogoSquircle sx={{
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -40,12 +40,14 @@ function AppBarTitle() {
|
||||
|
||||
|
||||
function CommonMenuItems(props: { onClose: () => void }) {
|
||||
|
||||
// external state
|
||||
const { openPreferences } = useOptimaLayout();
|
||||
const { mode: colorMode, setMode: setColorMode } = useColorScheme();
|
||||
|
||||
const handleShowSettings = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
openLayoutPreferences();
|
||||
openPreferences();
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
@@ -93,10 +95,14 @@ export function AppBar(props: { sx?: SxProps }) {
|
||||
// const [value, setValue] = React.useState<ContainedAppType>('chat');
|
||||
|
||||
// external state
|
||||
const { centerItems, drawerAnchor, drawerItems, menuAnchor, menuItems } = useLayoutComponents();
|
||||
const {
|
||||
appBarItems, appDrawerAnchor, appPaneContent, appMenuAnchor, appMenuItems,
|
||||
closeAppMenu, closeAppDrawer,
|
||||
setAppDrawerAnchor, setAppMenuAnchor,
|
||||
} = useOptimaLayout();
|
||||
|
||||
const commonMenuItems = React.useMemo(() =>
|
||||
<CommonMenuItems onClose={closeLayoutMenu} />, []);
|
||||
<CommonMenuItems onClose={closeAppMenu} />, [closeAppMenu]);
|
||||
|
||||
return <>
|
||||
|
||||
@@ -110,47 +116,47 @@ export function AppBar(props: { sx?: SxProps }) {
|
||||
}}>
|
||||
|
||||
{/* Drawer Anchor */}
|
||||
{!drawerItems ? (
|
||||
<IconButton component={Link} href='/' noLinkStyle variant='plain'>
|
||||
{!appPaneContent ? (
|
||||
<IconButton component={Link} href={ROUTE_INDEX} noLinkStyle variant='plain'>
|
||||
<ArrowBackIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton disabled={!!drawerAnchor || !drawerItems} variant='plain' onClick={event => setLayoutDrawerAnchor(event.currentTarget)}>
|
||||
<IconButton disabled={!!appDrawerAnchor || !appPaneContent} variant='plain' onClick={event => setAppDrawerAnchor(event.currentTarget)}>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
|
||||
{/* Center Items */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', my: 'auto' }}>
|
||||
{!!centerItems ? centerItems : <AppBarTitle />}
|
||||
{!!appBarItems ? appBarItems : <AppBarTitle />}
|
||||
</Box>
|
||||
|
||||
{/* Menu Anchor */}
|
||||
<IconButton disabled={!!menuAnchor /*|| !menuItems*/} variant='plain' onClick={event => setLayoutMenuAnchor(event.currentTarget)}>
|
||||
<IconButton disabled={!!appMenuAnchor /*|| !appMenuItems*/} variant='plain' onClick={event => setAppMenuAnchor(event.currentTarget)}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Sheet>
|
||||
|
||||
|
||||
{/* Drawer Menu */}
|
||||
{!!drawerItems && <CloseableMenu
|
||||
{!!appPaneContent && <CloseableMenu
|
||||
maxHeightGapPx={56 + 24} sx={{ minWidth: 320 }}
|
||||
open={!!drawerAnchor} anchorEl={drawerAnchor} onClose={closeLayoutDrawer}
|
||||
open={!!appDrawerAnchor} anchorEl={appDrawerAnchor} onClose={closeAppDrawer}
|
||||
placement='bottom-start'
|
||||
>
|
||||
{drawerItems}
|
||||
{appPaneContent}
|
||||
</CloseableMenu>}
|
||||
|
||||
{/* Menu Menu */}
|
||||
<CloseableMenu
|
||||
maxHeightGapPx={56 + 24} noBottomPadding noTopPadding sx={{ minWidth: 320 }}
|
||||
open={!!menuAnchor} anchorEl={menuAnchor} onClose={closeLayoutMenu}
|
||||
open={!!appMenuAnchor} anchorEl={appMenuAnchor} onClose={closeAppMenu}
|
||||
placement='bottom-end'
|
||||
>
|
||||
{commonMenuItems}
|
||||
{!!menuItems && <ListDivider sx={{ mt: 0 }} />}
|
||||
{!!menuItems && <Box sx={{ overflowY: 'auto' }}>{menuItems}</Box>}
|
||||
{!!menuItems && <ListDivider sx={{ mb: 0 }} />}
|
||||
{!!appMenuItems && <ListDivider sx={{ mt: 0 }} />}
|
||||
{!!appMenuItems && <Box sx={{ overflowY: 'auto' }}>{appMenuItems}</Box>}
|
||||
{!!appMenuItems && <ListDivider sx={{ mb: 0 }} />}
|
||||
<AppBarSwitcherItem />
|
||||
{/*<AppBarSupportItem />*/}
|
||||
</CloseableMenu>
|
||||
+2
-2
@@ -5,8 +5,8 @@ import { SxProps } from '@mui/joy/styles/types';
|
||||
// import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
|
||||
// import { Brand } from '..//common/app.brand';
|
||||
import { Link } from '../components/Link';
|
||||
import { cssRainbowColorKeyframes } from '../app.theme';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { cssRainbowColorKeyframes } from '~/common/app.theme';
|
||||
|
||||
|
||||
// missing from MUI, using Tabler for Discord
|
||||
+8
-5
@@ -4,10 +4,11 @@ import { useRouter } from 'next/router';
|
||||
import { Box, Button, ButtonGroup, ListItem } from '@mui/joy';
|
||||
import GitHubIcon from '@mui/icons-material/GitHub';
|
||||
|
||||
import { Brand } from '../app.config';
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { ROUTE_APP_CHAT, ROUTE_APP_NEWS } from '~/common/app.routes';
|
||||
|
||||
import { BringTheLove, DiscordIcon } from './AppBarSupportItem';
|
||||
import { closeLayoutMenu } from './store-applayout';
|
||||
import { useOptimaLayout } from './useOptimaLayout';
|
||||
|
||||
|
||||
// routes for the quick switcher menu item
|
||||
@@ -19,7 +20,7 @@ const AppItems: ContainedAppType[] = ['chat', 'news'];
|
||||
const AppRouteMap: { [key in ContainedAppType]: { name: string, route: string } } = {
|
||||
'chat': {
|
||||
name: 'Chat',
|
||||
route: '/',
|
||||
route: ROUTE_APP_CHAT,
|
||||
},
|
||||
// 'data': {
|
||||
// name: 'Data',
|
||||
@@ -27,13 +28,15 @@ const AppRouteMap: { [key in ContainedAppType]: { name: string, route: string }
|
||||
// },
|
||||
'news': {
|
||||
name: 'News',
|
||||
route: '/news',
|
||||
route: ROUTE_APP_NEWS,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
export function AppBarSwitcherItem() {
|
||||
|
||||
// external state
|
||||
const { closeAppMenu } = useOptimaLayout();
|
||||
const { route, push: routerPush } = useRouter();
|
||||
|
||||
// find the current ContainedAppType or null
|
||||
@@ -42,7 +45,7 @@ export function AppBarSwitcherItem() {
|
||||
// switcher
|
||||
const switchApp = (app: ContainedAppType) => {
|
||||
if (currentApp !== app) {
|
||||
closeLayoutMenu();
|
||||
closeAppMenu();
|
||||
void routerPush(AppRouteMap[app].route);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
import * as React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { default as NProgress } from 'nprogress';
|
||||
|
||||
|
||||
import 'nprogress/nprogress.css';
|
||||
|
||||
|
||||
/**
|
||||
* Not show the bar for very fast loads (with a delay), and for the same route
|
||||
*/
|
||||
export function NextRouterProgress(props: { color: string, delay?: number }) {
|
||||
|
||||
// external state
|
||||
const router = useRouter();
|
||||
|
||||
// this fires both when the page is refreshed, and when the route changes
|
||||
React.useEffect(() => {
|
||||
|
||||
NProgress.configure({
|
||||
showSpinner: false,
|
||||
});
|
||||
|
||||
// timeout to not show the progress bar for very fast loads
|
||||
let timeout: number;
|
||||
const handleStop = () => {
|
||||
clearTimeout(timeout);
|
||||
NProgress.done();
|
||||
};
|
||||
const handleStart = (newRoute: string) => {
|
||||
handleStop();
|
||||
if (newRoute == router.route)
|
||||
return;
|
||||
timeout = window.setTimeout(
|
||||
() => NProgress.start(),
|
||||
props.delay === undefined ? 250 : props.delay,
|
||||
);
|
||||
};
|
||||
|
||||
router.events.on('routeChangeStart', handleStart);
|
||||
router.events.on('routeChangeComplete', handleStop);
|
||||
router.events.on('routeChangeError', handleStop);
|
||||
|
||||
return () => {
|
||||
handleStop();
|
||||
router.events.off('routeChangeStart', handleStart);
|
||||
router.events.off('routeChangeComplete', handleStop);
|
||||
router.events.off('routeChangeError', handleStop);
|
||||
};
|
||||
}, [props.delay, router]);
|
||||
|
||||
return (
|
||||
<style>
|
||||
{`
|
||||
#nprogress .bar {
|
||||
height: 4px;
|
||||
background: ${props.color};
|
||||
}
|
||||
#nprogress .peg {
|
||||
box-shadow: 0 0 10px ${props.color}, 0 0 5px ${props.color};
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Container } from '@mui/joy';
|
||||
|
||||
import { ModelsModal } from '~/modules/llms/models-modal/ModelsModal';
|
||||
import { SettingsModal } from '../../../apps/settings-modal/SettingsModal';
|
||||
import { ShortcutsModal } from '../../../apps/settings-modal/ShortcutsModal';
|
||||
|
||||
import { isPwa } from '~/common/util/pwaUtils';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { AppBar } from './AppBar';
|
||||
import { NextRouterProgress } from './NextLoadProgress';
|
||||
import { useOptimaLayout } from './useOptimaLayout';
|
||||
|
||||
|
||||
/*function ResponsiveNavigation() {
|
||||
return <>
|
||||
<Drawer
|
||||
open={false}
|
||||
variant='solid'
|
||||
anchor='left'
|
||||
onClose={() => {
|
||||
}}
|
||||
sx={{
|
||||
'& .MuiDrawer-paper': {
|
||||
width: 256,
|
||||
boxSizing: 'border-box',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: 256, height: '100%' }}>
|
||||
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Box sx={{ flexGrow: 1 }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Drawer>
|
||||
</>;
|
||||
}*/
|
||||
|
||||
|
||||
/**
|
||||
* Core layout of big-AGI, used by all the Primary applications therein.
|
||||
*
|
||||
* Main functions:
|
||||
* - modern responsive layout
|
||||
* - core layout of the application, with the Nav, Panes, Appbar, 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
|
||||
* - flicker free
|
||||
*/
|
||||
export function OptimaLayout(props: { suspendAutoModelsSetup?: boolean, children: React.ReactNode, }) {
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
let centerMode = useUIPreferencesStore(state => (isPwa() || isMobile) ? 'full' : state.centerMode);
|
||||
|
||||
const {
|
||||
closePreferences, closeShortcuts,
|
||||
openShortcuts,
|
||||
showPreferencesTab, showShortcuts,
|
||||
} = useOptimaLayout();
|
||||
|
||||
return <>
|
||||
|
||||
{/*<Box sx={{*/}
|
||||
{/* display: 'flex', flexDirection: 'row',*/}
|
||||
{/* maxWidth: '100%', flexWrap: 'nowrap',*/}
|
||||
{/* // overflowX: 'hidden',*/}
|
||||
{/* background: 'lime',*/}
|
||||
{/*}}>*/}
|
||||
|
||||
{/*<Box sx={{ background: 'rgba(100 0 0 / 0.5)' }}>a</Box>*/}
|
||||
|
||||
{/*<ResponsiveNavigation />*/}
|
||||
|
||||
<Container
|
||||
disableGutters
|
||||
maxWidth={centerMode === 'full' ? false : centerMode === 'narrow' ? 'md' : 'xl'}
|
||||
sx={{
|
||||
// minWidth: 0,
|
||||
boxShadow: {
|
||||
xs: 'none',
|
||||
md: centerMode === 'narrow' ? 'md' : 'none',
|
||||
xl: centerMode !== 'full' ? 'lg' : 'none',
|
||||
},
|
||||
}}>
|
||||
|
||||
<Box sx={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
height: '100dvh',
|
||||
}}>
|
||||
|
||||
<AppBar sx={{
|
||||
zIndex: 20,
|
||||
}} />
|
||||
|
||||
{/* Children must make the assumption they're in a flex-col layout */}
|
||||
{props.children}
|
||||
|
||||
</Box>
|
||||
|
||||
</Container>
|
||||
|
||||
{/*<Box sx={{ background: 'rgba(100 0 0 / 0.5)' }}>bb</Box>*/}
|
||||
|
||||
{/*</Box>*/}
|
||||
|
||||
|
||||
{/* Overlay Settings */}
|
||||
<SettingsModal open={!!showPreferencesTab} tabIndex={showPreferencesTab} onClose={closePreferences} onOpenShortcuts={openShortcuts} />
|
||||
|
||||
{/* Overlay Models + LLM Options */}
|
||||
<ModelsModal suspendAutoModelsSetup={props.suspendAutoModelsSetup} />
|
||||
|
||||
{/* Overlay Shortcuts */}
|
||||
{showShortcuts && <ShortcutsModal onClose={closeShortcuts} />}
|
||||
|
||||
|
||||
{/* Route loading progress overlay */}
|
||||
<NextRouterProgress color='var(--joy-palette-neutral-700, #32383E)' />
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
|
||||
import { GlobalShortcutItem, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
|
||||
|
||||
const DEBUG_OPTIMA_LAYOUT_PLUGGING = false;
|
||||
|
||||
|
||||
type PC = React.JSX.Element | null;
|
||||
|
||||
interface OptimaLayoutState {
|
||||
|
||||
// pluggable UI
|
||||
appPaneContent: PC;
|
||||
appBarItems: PC;
|
||||
appMenuItems: PC;
|
||||
|
||||
// anchors - for externally closeable menus
|
||||
appDrawerAnchor: HTMLElement | null;
|
||||
appMenuAnchor: HTMLElement | null;
|
||||
|
||||
// modals that can overlay anything
|
||||
showPreferencesTab: number;
|
||||
showModelsSetup: boolean;
|
||||
showLlmOptions: DLLMId | null;
|
||||
showShortcuts: boolean;
|
||||
|
||||
}
|
||||
|
||||
const initialState: OptimaLayoutState = {
|
||||
|
||||
appPaneContent: null,
|
||||
appBarItems: null,
|
||||
appMenuItems: null,
|
||||
|
||||
appDrawerAnchor: null,
|
||||
appMenuAnchor: null,
|
||||
|
||||
showPreferencesTab: 0, // 0 = closed, 1+ open tab n-1
|
||||
showModelsSetup: false,
|
||||
showLlmOptions: null,
|
||||
showShortcuts: false,
|
||||
|
||||
};
|
||||
|
||||
interface OptimaLayoutActions {
|
||||
setPluggableComponents: (
|
||||
appPaneContent: PC,
|
||||
appBarItems: PC,
|
||||
appMenuItems: PC,
|
||||
) => void;
|
||||
|
||||
setAppDrawerAnchor: (anchor: HTMLElement | null) => void;
|
||||
closeAppDrawer: () => void;
|
||||
|
||||
setAppMenuAnchor: (anchor: HTMLElement | null) => void;
|
||||
closeAppMenu: () => void;
|
||||
|
||||
openPreferences: (tab?: number) => void;
|
||||
closePreferences: () => void;
|
||||
|
||||
openModelsSetup: () => void;
|
||||
closeModelsSetup: () => void;
|
||||
|
||||
openLlmOptions: (id: DLLMId) => void;
|
||||
closeLlmOptions: () => void;
|
||||
|
||||
openShortcuts: () => void;
|
||||
closeShortcuts: () => void;
|
||||
}
|
||||
|
||||
|
||||
// React Context with ...state and ...actions
|
||||
const UseOptimaLayout = React.createContext<
|
||||
(OptimaLayoutState & OptimaLayoutActions) | undefined
|
||||
>(undefined);
|
||||
|
||||
|
||||
export function OptimaLayoutProvider(props: { children: React.ReactNode }) {
|
||||
|
||||
// optima state, only modified by the static actions
|
||||
const [state, setState] = React.useState<OptimaLayoutState>(initialState);
|
||||
|
||||
// actions
|
||||
const actions: OptimaLayoutActions = React.useMemo(() => ({
|
||||
|
||||
setPluggableComponents: (appPaneContent: PC, appBarItems: PC, appMenuItems: PC) =>
|
||||
setState(state => ({ ...state, appPaneContent, appBarItems, appMenuItems })),
|
||||
|
||||
setAppDrawerAnchor: (anchor: HTMLElement | null) => setState(state => ({ ...state, appDrawerAnchor: anchor })),
|
||||
closeAppDrawer: () => setState(state => ({ ...state, appDrawerAnchor: null })),
|
||||
|
||||
setAppMenuAnchor: (anchor: HTMLElement | null) => setState(state => ({ ...state, appMenuAnchor: anchor })),
|
||||
closeAppMenu: () => setState(state => ({ ...state, appMenuAnchor: null })),
|
||||
|
||||
openPreferences: (tab?: number) => setState(state => ({ ...state, showPreferencesTab: tab || 1 })),
|
||||
closePreferences: () => setState(state => ({ ...state, showPreferencesTab: 0 })),
|
||||
|
||||
openModelsSetup: () => setState(state => ({ ...state, showModelsSetup: true })),
|
||||
closeModelsSetup: () => setState(state => ({ ...state, showModelsSetup: false })),
|
||||
|
||||
openLlmOptions: (id: DLLMId) => setState(state => ({ ...state, showLlmOptions: id })),
|
||||
closeLlmOptions: () => setState(state => ({ ...state, showLlmOptions: null })),
|
||||
|
||||
openShortcuts: () => setState(state => ({ ...state, showShortcuts: true })),
|
||||
closeShortcuts: () => setState(state => ({ ...state, showShortcuts: false })),
|
||||
|
||||
}), []);
|
||||
|
||||
|
||||
// global shortcuts for Optima
|
||||
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
|
||||
['?', true, true, false, actions.openShortcuts],
|
||||
['m', true, true, false, actions.openModelsSetup],
|
||||
['p', true, true, false, actions.openPreferences],
|
||||
], [actions]);
|
||||
useGlobalShortcuts(shortcuts);
|
||||
|
||||
|
||||
return (
|
||||
<UseOptimaLayout.Provider value={{ ...state, ...actions }}>
|
||||
{props.children}
|
||||
</UseOptimaLayout.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Optima Layout accessor for getting state and actions
|
||||
*/
|
||||
export const useOptimaLayout = (): OptimaLayoutState & OptimaLayoutActions => {
|
||||
const context = React.useContext(UseOptimaLayout);
|
||||
if (!context)
|
||||
throw new Error('useOptimaLayout must be used within an OptimaLayoutProvider');
|
||||
return context;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* used by the active UI client to register its components (and unregister on cleanup)
|
||||
*/
|
||||
export const usePluggableOptimaLayout = (appPaneContent: PC, appBarItems: PC, appMenuItems: PC, debugCallerName: string) => {
|
||||
const { setPluggableComponents } = useOptimaLayout();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (DEBUG_OPTIMA_LAYOUT_PLUGGING)
|
||||
console.log(' +PLUG layout', debugCallerName);
|
||||
setPluggableComponents(appPaneContent, appBarItems, appMenuItems);
|
||||
|
||||
return () => {
|
||||
if (DEBUG_OPTIMA_LAYOUT_PLUGGING)
|
||||
console.log(' -UNplug layout', debugCallerName);
|
||||
setPluggableComponents(null, null, null);
|
||||
};
|
||||
}, [appBarItems, appMenuItems, appPaneContent, debugCallerName, setPluggableComponents]);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Container } from '@mui/joy';
|
||||
|
||||
|
||||
export function PlainLayout(props: { children?: React.ReactNode }) {
|
||||
return <>
|
||||
|
||||
{/* Headers as needed */}
|
||||
|
||||
<Container disableGutters>
|
||||
<Box sx={{
|
||||
display: 'flex', flexDirection: 'column',
|
||||
minHeight: '100dvh',
|
||||
}}>
|
||||
|
||||
{props.children}
|
||||
|
||||
</Box>
|
||||
</Container>
|
||||
|
||||
{/* Footers as needed */}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { create } from 'zustand';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
|
||||
interface AppLayoutStore {
|
||||
|
||||
// pluggable UI
|
||||
drawerItems: React.JSX.Element | null;
|
||||
centerItems: React.JSX.Element | null;
|
||||
menuItems: React.JSX.Element | null;
|
||||
|
||||
// anchors - for externally closeable menus
|
||||
drawerAnchor: HTMLElement | null;
|
||||
menuAnchor: HTMLElement | null;
|
||||
|
||||
// modals, which are on the AppLayout
|
||||
preferencesTab: number; // 0: closed, 1..N: tab index
|
||||
modelsSetupOpen: boolean;
|
||||
llmOptionsId: DLLMId | null;
|
||||
shortcutsOpen: boolean;
|
||||
|
||||
}
|
||||
|
||||
const useAppLayoutStore = create<AppLayoutStore>()(
|
||||
() => ({
|
||||
|
||||
drawerItems: null,
|
||||
centerItems: null,
|
||||
menuItems: null,
|
||||
|
||||
drawerAnchor: null,
|
||||
menuAnchor: null,
|
||||
|
||||
preferencesTab: 0,
|
||||
modelsSetupOpen: false,
|
||||
llmOptionsId: null,
|
||||
shortcutsOpen: false,
|
||||
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* used by the active UI client to register its components (and unregister on cleanup)
|
||||
*/
|
||||
export function useLayoutPluggable(centerItems: React.JSX.Element | null, drawerItems: React.JSX.Element | null, menuItems: React.JSX.Element | null) {
|
||||
React.useEffect(() => {
|
||||
useAppLayoutStore.setState({ centerItems, drawerItems, menuItems });
|
||||
return () => useAppLayoutStore.setState({ centerItems: null, drawerItems: null, menuItems: null });
|
||||
}, [centerItems, drawerItems, menuItems]);
|
||||
}
|
||||
|
||||
export function useLayoutComponents() {
|
||||
return useAppLayoutStore(state => ({
|
||||
drawerItems: state.drawerItems,
|
||||
centerItems: state.centerItems,
|
||||
menuItems: state.menuItems,
|
||||
drawerAnchor: state.drawerAnchor,
|
||||
menuAnchor: state.menuAnchor,
|
||||
}), shallow);
|
||||
}
|
||||
|
||||
export const setLayoutDrawerAnchor = (anchor: HTMLElement | null) => useAppLayoutStore.setState({ drawerAnchor: anchor });
|
||||
export const closeLayoutDrawer = () => useAppLayoutStore.setState({ drawerAnchor: null });
|
||||
|
||||
export const setLayoutMenuAnchor = (anchor: HTMLElement) => useAppLayoutStore.setState({ menuAnchor: anchor });
|
||||
export const closeLayoutMenu = () => useAppLayoutStore.setState({ menuAnchor: null });
|
||||
|
||||
export const useLayoutPreferencesTab = () => useAppLayoutStore(state => state.preferencesTab);
|
||||
export const openLayoutPreferences = (tab?: number) => useAppLayoutStore.setState({ preferencesTab: tab || 1 });
|
||||
export const closeLayoutPreferences = () => useAppLayoutStore.setState({ preferencesTab: 0 });
|
||||
|
||||
export const useLayoutModelsSetup = (): [open: boolean, llmId: DLLMId | null] => useAppLayoutStore(state => [state.modelsSetupOpen, state.llmOptionsId], shallow);
|
||||
export const openLayoutModelsSetup = () => useAppLayoutStore.setState({ modelsSetupOpen: true });
|
||||
export const closeLayoutModelsSetup = () => useAppLayoutStore.setState({ modelsSetupOpen: false });
|
||||
export const openLayoutLLMOptions = (llmId: DLLMId) => useAppLayoutStore.setState({ llmOptionsId: llmId });
|
||||
export const closeLayoutLLMOptions = () => useAppLayoutStore.setState({ llmOptionsId: null });
|
||||
export const useLayoutShortcuts = () => useAppLayoutStore(state => state.shortcutsOpen);
|
||||
export const openLayoutShortcuts = () => useAppLayoutStore.setState({ shortcutsOpen: true });
|
||||
export const closeLayoutShortcuts = () => useAppLayoutStore.setState({ shortcutsOpen: false });
|
||||
@@ -0,0 +1,31 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { OptimaLayout } from './optima/OptimaLayout';
|
||||
import { OptimaLayoutProvider } from './optima/useOptimaLayout';
|
||||
import { PlainLayout } from './plain/PlainLayout';
|
||||
|
||||
|
||||
type WithLayout = {
|
||||
type: 'optima';
|
||||
suspendAutoModelsSetup?: boolean;
|
||||
} | {
|
||||
type: 'plain';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Dynamic page-level layouting: a wrapper that adds the layout around the children.
|
||||
*/
|
||||
export function withLayout(layoutOptions: WithLayout, children: React.ReactNode): React.ReactElement {
|
||||
|
||||
// Optima layout: also wrap it in the OptimaLayoutProvider
|
||||
if (layoutOptions.type === 'optima')
|
||||
return <OptimaLayoutProvider><OptimaLayout {...layoutOptions}>{children}</OptimaLayout></OptimaLayoutProvider>;
|
||||
|
||||
else if (layoutOptions.type === 'plain')
|
||||
return <PlainLayout {...layoutOptions}>{children}</PlainLayout>;
|
||||
|
||||
// if no layout is specified, return the children as-is
|
||||
console.error('No layout specified for this top-level page');
|
||||
return <>{children}</>;
|
||||
}
|
||||
+5
-3
@@ -4,8 +4,11 @@ import { useBackendCapsLoader } from '~/modules/backend/state-backend';
|
||||
|
||||
import { apiQuery } from '~/common/util/trpc.client';
|
||||
|
||||
|
||||
export function ProviderBackend(props: { children: React.ReactNode }) {
|
||||
/**
|
||||
* Note: we used to have a NoSSR wrapper inside the AppLayout component (which was delaying rendering 1 cycle),
|
||||
* however this wrapper is now providing the same function, given the network roundtrip.
|
||||
*/
|
||||
export function ProviderBackendAndNoSSR(props: { children: React.ReactNode }) {
|
||||
|
||||
// external state
|
||||
const [loaded, setCapabilties] = useBackendCapsLoader();
|
||||
@@ -22,7 +25,6 @@ export function ProviderBackend(props: { children: React.ReactNode }) {
|
||||
setCapabilties(capabilities);
|
||||
}, [capabilities, setCapabilties]);
|
||||
|
||||
|
||||
// block rendering until the capabilities are loaded
|
||||
return !loaded ? null : props.children;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { isBrowser } from '~/common/util/pwaUtils';
|
||||
import { isMobileQuery } from '~/common/components/useMatchMedia';
|
||||
|
||||
|
||||
export function ProviderBootstrapLogic(props: { children: React.ReactNode }) {
|
||||
|
||||
// NOTE: just a pass-through for now. Will be used for the following:
|
||||
// - loading the latest news (see ChatPage -> useRedirectToNewsOnUpdates)
|
||||
// - loading the commander
|
||||
// - ...
|
||||
|
||||
// boot-up logic. this is not updated at route changes, but only at app startup
|
||||
React.useEffect(() => {
|
||||
const isMobile = isBrowser ? window.matchMedia(isMobileQuery()).matches : false;
|
||||
if (isMobile) {
|
||||
// TODO: the app booted in mobile mode
|
||||
}
|
||||
}, []);
|
||||
|
||||
return props.children;
|
||||
}
|
||||
@@ -21,12 +21,6 @@ interface UXLabsStore {
|
||||
labsEnhancedUI: boolean;
|
||||
setLabsEnhancedUI: (labsEnhancedUI: boolean) => void;
|
||||
|
||||
labsMagicDraw: boolean;
|
||||
setLabsMagicDraw: (labsMagicDraw: boolean) => void;
|
||||
|
||||
labsPersonaYTCreator: boolean;
|
||||
setLabsPersonaYTCreator: (labsPersonaYTCreator: boolean) => void;
|
||||
|
||||
labsSplitBranching: boolean;
|
||||
setLabsSplitBranching: (labsSplitBranching: boolean) => void;
|
||||
|
||||
@@ -45,12 +39,6 @@ export const useUXLabsStore = create<UXLabsStore>()(
|
||||
labsEnhancedUI: false,
|
||||
setLabsEnhancedUI: (labsEnhancedUI: boolean) => set({ labsEnhancedUI }),
|
||||
|
||||
labsMagicDraw: false,
|
||||
setLabsMagicDraw: (labsMagicDraw: boolean) => set({ labsMagicDraw }),
|
||||
|
||||
labsPersonaYTCreator: true, // NOTE: default to true, as it is a graduated experiment
|
||||
setLabsPersonaYTCreator: (labsPersonaYTCreator: boolean) => set({ labsPersonaYTCreator }),
|
||||
|
||||
labsSplitBranching: false,
|
||||
setLabsSplitBranching: (labsSplitBranching: boolean) => set({ labsSplitBranching }),
|
||||
|
||||
|
||||
Vendored
+3
-5
@@ -1,11 +1,9 @@
|
||||
import type { EmotionCache } from '@emotion/react';
|
||||
|
||||
|
||||
// export type GetLayout = (page: ReactElement) => ReactNode;
|
||||
|
||||
// Extend the NextPage type with an optional getLayout function
|
||||
// type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
// getLayout?: GetLayout;
|
||||
// export type NextPageWithLayout<P = {}, IP = P> = NextPage<P, IP> & {
|
||||
// // require .layoutOptions on the page component
|
||||
// layoutOptions: LayoutOptions;
|
||||
// };
|
||||
|
||||
// Extend the AppProps type with the custom page component type
|
||||
|
||||
@@ -34,4 +34,15 @@ export function asValidURL(textString: string | null): string | null {
|
||||
const trimmedTextString = textString.trim();
|
||||
const urlMatch = urlRegex.exec(trimmedTextString);
|
||||
return urlMatch ? urlMatch[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add https if missing, and remove trailing slash if present and the path starts with a slash.
|
||||
*/
|
||||
export function fixupHost(host: string, apiPath: string): string {
|
||||
if (!host.startsWith('http'))
|
||||
host = `https://${host}`;
|
||||
if (host.endsWith('/') && apiPath.startsWith('/'))
|
||||
host = host.slice(0, -1);
|
||||
return host;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, ButtonGroup, CircularProgress, Divider, Grid, IconButton, Input, FormControl, FormLabel } from '@mui/joy';
|
||||
import { Box, Button, ButtonGroup, CircularProgress, Divider, FormControl, FormLabel, Grid, IconButton, Input } from '@mui/joy';
|
||||
import AccountTreeIcon from '@mui/icons-material/AccountTree';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
@@ -152,11 +152,11 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
|
||||
{llmComponent}
|
||||
</Grid>
|
||||
<Grid xs={12} md={6}>
|
||||
<FormControl>
|
||||
<FormLabel>Custom Instruction</FormLabel>
|
||||
<Input title="Custom Instruction" placeholder='e.g. visualize as state' value={customInstruction} onChange={(e) => setCustomInstruction(e.target.value)} />
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<FormControl>
|
||||
<FormLabel>Custom Instruction</FormLabel>
|
||||
<Input title='Custom Instruction' placeholder='e.g. visualize as state' value={customInstruction} onChange={(e) => setCustomInstruction(e.target.value)} />
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
|
||||
@@ -188,7 +188,7 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
|
||||
codeBackground='background.surface'
|
||||
onMessageEdit={(text) => setMessage({ ...message, text })}
|
||||
sx={{
|
||||
backgroundColor: abortController ? 'background.level3' : 'background.level2',
|
||||
backgroundColor: 'background.level2',
|
||||
marginX: 'calc(-1 * var(--Card-padding))',
|
||||
minHeight: 96,
|
||||
}}
|
||||
|
||||
@@ -130,7 +130,7 @@ export class Agent {
|
||||
try {
|
||||
content = (await llmChatGenerateOrThrow(llmId, S.messages, null, null, 500)).content;
|
||||
} catch (error: any) {
|
||||
content = `Error in callChat: ${error}`;
|
||||
content = `Error in llmChatGenerateOrThrow: ${error}`;
|
||||
}
|
||||
// process response, strip out potential hallucinated response after PAUSE is detected
|
||||
content = this.truncateStringAfterPause(content);
|
||||
|
||||
@@ -45,6 +45,11 @@ export async function llmChatGenerateOrThrow<TSourceSetup = unknown, TAccess = u
|
||||
const partialSourceSetup = llm._source.setup;
|
||||
const access = vendor.getTransportAccess(partialSourceSetup);
|
||||
|
||||
// get any vendor-specific rate limit delay
|
||||
const delay = vendor.getRateLimitDelay?.(llm) ?? 0;
|
||||
if (delay > 0)
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
// execute via the vendor
|
||||
return await vendor.rpcChatGenerateOrThrow(access, options, messages, functions, forceFunctionName, maxTokens);
|
||||
}
|
||||
@@ -69,6 +74,11 @@ export async function llmStreamingChatGenerate<TSourceSetup = unknown, TAccess =
|
||||
const partialSourceSetup = llm._source.setup;
|
||||
const access = vendor.getTransportAccess(partialSourceSetup); // as ChatStreamInputSchema['access'];
|
||||
|
||||
// get any vendor-specific rate limit delay
|
||||
const delay = vendor.getRateLimitDelay?.(llm) ?? 0;
|
||||
if (delay > 0)
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
|
||||
// execute via the vendor
|
||||
return await vendor.streamingChatGenerateOrThrow(access, llmId, llmOptions, messages, functions, forceFunctionName, abortSignal, onUpdate);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { Box, Button, ButtonGroup, Divider, FormControl, Input, Switch, Typography } from '@mui/joy';
|
||||
import { Box, Button, ButtonGroup, Divider, FormControl, Input, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
@@ -11,7 +11,7 @@ import { findVendorById } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { closeLayoutLLMOptions } from '~/common/layout/store-applayout';
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { settingsGap } from '~/common/app.theme';
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ function VendorLLMOptions(props: { llmId: DLLMId }) {
|
||||
}
|
||||
|
||||
|
||||
export function LLMOptionsModal(props: { id: DLLMId }) {
|
||||
export function LLMOptionsModal(props: { id: DLLMId, onClose: () => void }) {
|
||||
|
||||
// state
|
||||
const [showDetails, setShowDetails] = React.useState(false);
|
||||
@@ -64,14 +64,14 @@ export function LLMOptionsModal(props: { id: DLLMId }) {
|
||||
|
||||
const handleLlmDelete = () => {
|
||||
removeLLM(llm.id);
|
||||
closeLayoutLLMOptions();
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<GoodModal
|
||||
title={<><b>{llm.label}</b> options</>}
|
||||
open={!!props.id} onClose={closeLayoutLLMOptions}
|
||||
open={!!props.id} onClose={props.onClose}
|
||||
startButton={
|
||||
<Button variant='plain' color='neutral' onClick={handleLlmDelete} startDecorator={<DeleteOutlineIcon />}>
|
||||
Delete
|
||||
@@ -93,18 +93,25 @@ export function LLMOptionsModal(props: { id: DLLMId }) {
|
||||
<FormControl orientation='horizontal' sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<FormLabelStart title='Defaults' sx={{ minWidth: 80 }} />
|
||||
<ButtonGroup orientation='horizontal' size='sm' variant='outlined'>
|
||||
<Button variant={isChatLLM ? 'solid' : undefined} onClick={() => setChatLLMId(isChatLLM ? null : props.id)}>Chat</Button>
|
||||
<Button variant={isFastLLM ? 'solid' : undefined} onClick={() => setFastLLMId(isFastLLM ? null : props.id)}>Fast</Button>
|
||||
<Button variant={isFuncLLM ? 'solid' : undefined} onClick={() => setFuncLLMId(isFuncLLM ? null : props.id)}>Func</Button>
|
||||
<GoodTooltip title='Is this model the currently selected Chat model'>
|
||||
<Button variant={isChatLLM ? 'solid' : undefined} onClick={() => setChatLLMId(isChatLLM ? null : props.id)}>Chat</Button>
|
||||
</GoodTooltip>
|
||||
<GoodTooltip title='Make this the model appointed for fast (e.g. auto-title, summarize) operations.'>
|
||||
<Button variant={isFastLLM ? 'solid' : undefined} onClick={() => setFastLLMId(isFastLLM ? null : props.id)}>Fast</Button>
|
||||
</GoodTooltip>
|
||||
<GoodTooltip title='Make this the model appointed for "function calling" and other structured features, such as Auto-Chart, Auto-Follow-ups, etc.'>
|
||||
<Button variant={isFuncLLM ? 'solid' : undefined} onClick={() => setFuncLLMId(isFuncLLM ? null : props.id)}>Func</Button>
|
||||
</GoodTooltip>
|
||||
</ButtonGroup>
|
||||
</FormControl>
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ flexWrap: 'wrap', alignItems: 'center' }}>
|
||||
<FormLabelStart title='Visible' sx={{ minWidth: 80 }} />
|
||||
<Switch checked={!llm.hidden} onChange={handleLlmVisibilityToggle}
|
||||
endDecorator={!llm.hidden ? <VisibilityIcon /> : <VisibilityOffIcon />}
|
||||
slotProps={{ endDecorator: { sx: { minWidth: 26 } } }}
|
||||
sx={{ ml: 0, mr: 'auto' }} />
|
||||
<Tooltip title={!llm.hidden ? 'Show this model in the list of Chat models' : 'Hide this model from the list of Chat models'}>
|
||||
<Switch checked={!llm.hidden} onChange={handleLlmVisibilityToggle}
|
||||
endDecorator={!llm.hidden ? <VisibilityIcon /> : <VisibilityOffIcon />}
|
||||
slotProps={{ endDecorator: { sx: { minWidth: 26 } } }} />
|
||||
</Tooltip>
|
||||
</FormControl>
|
||||
|
||||
{/*<FormControl orientation='horizontal' sx={{ flexWrap: 'wrap', alignItems: 'center' }}>*/}
|
||||
@@ -115,14 +122,24 @@ export function LLMOptionsModal(props: { id: DLLMId }) {
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ flexWrap: 'nowrap' }}>
|
||||
<FormLabelStart title='Details' sx={{ minWidth: 80 }} onClick={() => setShowDetails(!showDetails)} />
|
||||
{showDetails && <Typography level='body-sm' sx={{ display: 'block' }}>
|
||||
[{llm.id}]: {llm.options.llmRef && `${llm.options.llmRef} · `}
|
||||
{!!llm.contextTokens && `context tokens: ${llm.contextTokens.toLocaleString()} · `}
|
||||
{!!llm.maxOutputTokens && `max output tokens: ${llm.maxOutputTokens.toLocaleString()} · `}
|
||||
{!!llm.created && `created: ${(new Date(llm.created * 1000)).toLocaleString()} · `}
|
||||
description: {llm.description}
|
||||
{/*· tags: {llm.tags.join(', ')}*/}
|
||||
</Typography>}
|
||||
{showDetails && <Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<Typography level='body-md'>
|
||||
{llm.id}
|
||||
</Typography>
|
||||
{llm.isFree && <Typography level='body-xs'>
|
||||
🎁 Free model - note: refresh models to check for updates in pricing
|
||||
</Typography>}
|
||||
{!!llm.description && <Typography level='body-xs'>
|
||||
{llm.description}
|
||||
</Typography>}
|
||||
<Typography level='body-xs'>
|
||||
{!!llm.contextTokens && `context tokens: ${llm.contextTokens.toLocaleString()} · `}
|
||||
{!!llm.maxOutputTokens && `max output tokens: ${llm.maxOutputTokens.toLocaleString()}`}<br />
|
||||
{!!llm.created && `created: ${(new Date(llm.created * 1000)).toLocaleString()} · `}
|
||||
{/*· tags: {llm.tags.join(', ')}*/}
|
||||
{JSON.stringify(llm.options)}
|
||||
</Typography>
|
||||
</Box>}
|
||||
</FormControl>
|
||||
|
||||
</GoodModal>
|
||||
|
||||
@@ -5,15 +5,14 @@ import { Box, Chip, IconButton, List, ListItem, ListItemButton, Typography } fro
|
||||
import SettingsOutlinedIcon from '@mui/icons-material/SettingsOutlined';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
|
||||
import { DLLM, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { DLLM, DLLMId, DModelSourceId, useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { IModelVendor } from '~/modules/llms/vendors/IModelVendor';
|
||||
import { findVendorById } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import { GoodTooltip } from '~/common/components/GoodTooltip';
|
||||
import { openLayoutLLMOptions } from '~/common/layout/store-applayout';
|
||||
|
||||
|
||||
function ModelItem(props: { llm: DLLM, vendor: IModelVendor, chipChat: boolean, chipFast: boolean, chipFunc: boolean }) {
|
||||
function ModelItem(props: { llm: DLLM, vendor: IModelVendor, chipChat: boolean, chipFast: boolean, chipFunc: boolean, onClick: () => void }) {
|
||||
|
||||
// derived
|
||||
const llm = props.llm;
|
||||
@@ -31,7 +30,7 @@ function ModelItem(props: { llm: DLLM, vendor: IModelVendor, chipChat: boolean,
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemButton onClick={() => openLayoutLLMOptions(llm.id)} sx={{ alignItems: 'center', gap: 1 }}>
|
||||
<ListItemButton onClick={props.onClick} sx={{ alignItems: 'center', gap: 1 }}>
|
||||
|
||||
{/* Model Name */}
|
||||
<GoodTooltip title={tooltip}>
|
||||
@@ -65,7 +64,8 @@ function ModelItem(props: { llm: DLLM, vendor: IModelVendor, chipChat: boolean,
|
||||
}
|
||||
|
||||
export function ModelsList(props: {
|
||||
filterSourceId: DModelSourceId | null
|
||||
filterSourceId: DModelSourceId | null,
|
||||
onOpenLLMOptions: (id: DLLMId) => void,
|
||||
}) {
|
||||
|
||||
// external state
|
||||
@@ -101,7 +101,14 @@ export function ModelsList(props: {
|
||||
// for safety, ensure the vendor exists
|
||||
const vendor = findVendorById(llm._source.vId);
|
||||
!!vendor && items.push(
|
||||
<ModelItem key={'llm-' + llm.id} llm={llm} vendor={vendor} chipChat={llm.id === chatLLMId} chipFast={llm.id === fastLLMId} chipFunc={llm.id === funcLLMId} />,
|
||||
<ModelItem
|
||||
key={'llm-' + llm.id}
|
||||
llm={llm} vendor={vendor}
|
||||
chipChat={llm.id === chatLLMId}
|
||||
chipFast={llm.id === fastLLMId}
|
||||
chipFunc={llm.id === funcLLMId}
|
||||
onClick={() => props.onOpenLLMOptions(llm.id)}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import { DModelSource, DModelSourceId, useModelsStore } from '~/modules/llms/sto
|
||||
import { createModelSourceForDefaultVendor, findVendorById } from '~/modules/llms/vendors/vendors.registry';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { closeLayoutModelsSetup, openLayoutModelsSetup, useLayoutModelsSetup } from '~/common/layout/store-applayout';
|
||||
import { settingsGap } from '~/common/app.theme';
|
||||
import { useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
|
||||
import { LLMOptionsModal } from './LLMOptionsModal';
|
||||
import { ModelsList } from './ModelsList';
|
||||
@@ -30,7 +30,11 @@ export function ModelsModal(props: { suspendAutoModelsSetup?: boolean }) {
|
||||
const [showAllSources, setShowAllSources] = React.useState<boolean>(false);
|
||||
|
||||
// external state
|
||||
const [modelsSetupOpen, llmOptionsId] = useLayoutModelsSetup();
|
||||
const {
|
||||
closeLlmOptions, closeModelsSetup,
|
||||
openLlmOptions, openModelsSetup,
|
||||
showLlmOptions, showModelsSetup,
|
||||
} = useOptimaLayout();
|
||||
const { modelSources, llmCount } = useModelsStore(state => ({
|
||||
modelSources: state.sources,
|
||||
llmCount: state.llms.length,
|
||||
@@ -47,8 +51,8 @@ export function ModelsModal(props: { suspendAutoModelsSetup?: boolean }) {
|
||||
// if no sources at startup, open the modal
|
||||
React.useEffect(() => {
|
||||
if (!selectedSourceId && !props.suspendAutoModelsSetup)
|
||||
openLayoutModelsSetup();
|
||||
}, [selectedSourceId, props.suspendAutoModelsSetup]);
|
||||
openModelsSetup();
|
||||
}, [selectedSourceId, props.suspendAutoModelsSetup, openModelsSetup]);
|
||||
|
||||
// add the default source on cold - will require setup
|
||||
React.useEffect(() => {
|
||||
@@ -61,7 +65,7 @@ export function ModelsModal(props: { suspendAutoModelsSetup?: boolean }) {
|
||||
return <>
|
||||
|
||||
{/* Sources Setup */}
|
||||
{modelsSetupOpen && <GoodModal
|
||||
{showModelsSetup && <GoodModal
|
||||
title={<>Configure <b>AI Models</b></>}
|
||||
startButton={
|
||||
multiSource ? <Checkbox
|
||||
@@ -69,7 +73,7 @@ export function ModelsModal(props: { suspendAutoModelsSetup?: boolean }) {
|
||||
checked={showAllSources} onChange={() => setShowAllSources(all => !all)}
|
||||
/> : undefined
|
||||
}
|
||||
open onClose={closeLayoutModelsSetup}
|
||||
open onClose={closeModelsSetup}
|
||||
>
|
||||
|
||||
<ModelsSourceSelector selectedSourceId={selectedSourceId} setSelectedSourceId={setSelectedSourceId} />
|
||||
@@ -84,14 +88,14 @@ export function ModelsModal(props: { suspendAutoModelsSetup?: boolean }) {
|
||||
|
||||
{!!llmCount && <Divider />}
|
||||
|
||||
{!!llmCount && <ModelsList filterSourceId={showAllSources ? null : selectedSourceId} />}
|
||||
{!!llmCount && <ModelsList filterSourceId={showAllSources ? null : selectedSourceId} onOpenLLMOptions={openLlmOptions} />}
|
||||
|
||||
<Divider />
|
||||
|
||||
</GoodModal>}
|
||||
|
||||
{/* per-LLM options */}
|
||||
{!!llmOptionsId && <LLMOptionsModal id={llmOptionsId} />}
|
||||
{!!showLlmOptions && <LLMOptionsModal id={showLlmOptions} onClose={closeLlmOptions} />}
|
||||
|
||||
</>;
|
||||
}
|
||||
@@ -5,8 +5,10 @@ import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.serverutils';
|
||||
|
||||
import { fixupHost, openAIChatGenerateOutputSchema, OpenAIHistorySchema, openAIHistorySchema, OpenAIModelSchema, openAIModelSchema } from '../openai/openai.router';
|
||||
import { listModelsOutputSchema } from '../llm.server.types';
|
||||
import { fixupHost } from '~/common/util/urlUtils';
|
||||
|
||||
import { OpenAIHistorySchema, openAIHistorySchema, OpenAIModelSchema, openAIModelSchema } from '../openai/openai.router';
|
||||
import { llmsListModelsOutputSchema, llmsChatGenerateOutputSchema } from '../llm.server.types';
|
||||
|
||||
import { AnthropicWire } from './anthropic.wiretypes';
|
||||
import { hardcodedAnthropicModels } from './anthropic.models';
|
||||
@@ -106,13 +108,13 @@ export const llmAnthropicRouter = createTRPCRouter({
|
||||
*/
|
||||
listModels: publicProcedure
|
||||
.input(listModelsInputSchema)
|
||||
.output(listModelsOutputSchema)
|
||||
.output(llmsListModelsOutputSchema)
|
||||
.query(() => ({ models: hardcodedAnthropicModels })),
|
||||
|
||||
/* Anthropic: Chat generation */
|
||||
chatGenerate: publicProcedure
|
||||
.input(chatGenerateInputSchema)
|
||||
.output(openAIChatGenerateOutputSchema)
|
||||
.output(llmsChatGenerateOutputSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
|
||||
const { access, model, history } = input;
|
||||
|
||||
@@ -7,10 +7,12 @@ import packageJson from '../../../../../package.json';
|
||||
import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.serverutils';
|
||||
|
||||
import { LLM_IF_OAI_Chat, LLM_IF_OAI_Vision } from '../../store-llms';
|
||||
import { listModelsOutputSchema, ModelDescriptionSchema } from '../llm.server.types';
|
||||
import { fixupHost } from '~/common/util/urlUtils';
|
||||
|
||||
import { fixupHost, openAIChatGenerateOutputSchema, OpenAIHistorySchema, openAIHistorySchema, OpenAIModelSchema, openAIModelSchema } from '../openai/openai.router';
|
||||
import { LLM_IF_OAI_Chat, LLM_IF_OAI_Vision } from '../../store-llms';
|
||||
import { llmsListModelsOutputSchema, ModelDescriptionSchema, llmsChatGenerateOutputSchema } from '../llm.server.types';
|
||||
|
||||
import { OpenAIHistorySchema, openAIHistorySchema, OpenAIModelSchema, openAIModelSchema } from '../openai/openai.router';
|
||||
|
||||
import { GeminiBlockSafetyLevel, geminiBlockSafetyLevelSchema, GeminiContentSchema, GeminiGenerateContentRequest, geminiGeneratedContentResponseSchema, geminiModelsGenerateContentPath, geminiModelsListOutputSchema, geminiModelsListPath } from './gemini.wiretypes';
|
||||
|
||||
@@ -133,7 +135,7 @@ export const llmGeminiRouter = createTRPCRouter({
|
||||
/* [Gemini] models.list = /v1beta/models */
|
||||
listModels: publicProcedure
|
||||
.input(accessOnlySchema)
|
||||
.output(listModelsOutputSchema)
|
||||
.output(llmsListModelsOutputSchema)
|
||||
.query(async ({ input }) => {
|
||||
|
||||
// get the models
|
||||
@@ -185,7 +187,7 @@ export const llmGeminiRouter = createTRPCRouter({
|
||||
/* [Gemini] models.generateContent = /v1/{model=models/*}:generateContent */
|
||||
chatGenerate: publicProcedure
|
||||
.input(chatGenerateInputSchema)
|
||||
.output(openAIChatGenerateOutputSchema)
|
||||
.output(llmsChatGenerateOutputSchema)
|
||||
.mutation(async ({ input: { access, history, model } }) => {
|
||||
|
||||
// generate the content
|
||||
|
||||
@@ -30,6 +30,23 @@ const modelDescriptionSchema = z.object({
|
||||
// this is also used by the Client
|
||||
export type ModelDescriptionSchema = z.infer<typeof modelDescriptionSchema>;
|
||||
|
||||
export const listModelsOutputSchema = z.object({
|
||||
export const llmsListModelsOutputSchema = z.object({
|
||||
models: z.array(modelDescriptionSchema),
|
||||
});
|
||||
|
||||
|
||||
// (non-streaming) Chat Generation Output
|
||||
|
||||
export const llmsChatGenerateOutputSchema = z.object({
|
||||
role: z.enum(['assistant', 'system', 'user']),
|
||||
content: z.string(),
|
||||
finish_reason: z.union([z.enum(['stop', 'length']), z.null()]),
|
||||
});
|
||||
|
||||
export const llmsChatGenerateWithFunctionsOutputSchema = z.union([
|
||||
llmsChatGenerateOutputSchema,
|
||||
z.object({
|
||||
function_name: z.string(),
|
||||
function_arguments: z.record(z.any()),
|
||||
}),
|
||||
]);
|
||||
@@ -8,9 +8,10 @@ import { fetchJsonOrTRPCError, fetchTextOrTRPCError } from '~/server/api/trpc.se
|
||||
import { LLM_IF_OAI_Chat } from '../../store-llms';
|
||||
|
||||
import { capitalizeFirstLetter } from '~/common/util/textUtils';
|
||||
import { fixupHost } from '~/common/util/urlUtils';
|
||||
|
||||
import { fixupHost, openAIChatGenerateOutputSchema, OpenAIHistorySchema, openAIHistorySchema, OpenAIModelSchema, openAIModelSchema } from '../openai/openai.router';
|
||||
import { listModelsOutputSchema, ModelDescriptionSchema } from '../llm.server.types';
|
||||
import { OpenAIHistorySchema, openAIHistorySchema, OpenAIModelSchema, openAIModelSchema } from '../openai/openai.router';
|
||||
import { llmsListModelsOutputSchema, ModelDescriptionSchema, llmsChatGenerateOutputSchema } from '../llm.server.types';
|
||||
|
||||
import { OLLAMA_BASE_MODELS, OLLAMA_PREV_UPDATE } from './ollama.models';
|
||||
import { WireOllamaChatCompletionInput, wireOllamaChunkedOutputSchema } from './ollama.wiretypes';
|
||||
@@ -186,7 +187,7 @@ export const llmOllamaRouter = createTRPCRouter({
|
||||
/* Ollama: List the Models available */
|
||||
listModels: publicProcedure
|
||||
.input(accessOnlySchema)
|
||||
.output(listModelsOutputSchema)
|
||||
.output(llmsListModelsOutputSchema)
|
||||
.query(async ({ input }) => {
|
||||
|
||||
// get the models
|
||||
@@ -241,7 +242,7 @@ export const llmOllamaRouter = createTRPCRouter({
|
||||
/* Ollama: Chat generation */
|
||||
chatGenerate: publicProcedure
|
||||
.input(chatGenerateInputSchema)
|
||||
.output(openAIChatGenerateOutputSchema)
|
||||
.output(llmsChatGenerateOutputSchema)
|
||||
.mutation(async ({ input: { access, history, model } }) => {
|
||||
|
||||
const wireGeneration = await ollamaPOST(access, ollamaChatCompletionPayload(model, history, false), OLLAMA_PATH_CHAT);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { SERVER_DEBUG_WIRE } from '~/server/wire';
|
||||
|
||||
import { LLM_IF_OAI_Chat, LLM_IF_OAI_Complete, LLM_IF_OAI_Fn, LLM_IF_OAI_Vision } from '../../store-llms';
|
||||
|
||||
import type { ModelDescriptionSchema } from '../llm.server.types';
|
||||
import { wireMistralModelsListOutputSchema } from './mistral.wiretypes';
|
||||
import { wireOpenrouterModelsListOutputSchema } from './openrouter.wiretypes';
|
||||
|
||||
|
||||
// [Azure] / [OpenAI]
|
||||
@@ -294,94 +293,10 @@ export function oobaboogaModelToModelDescription(modelId: string, created: numbe
|
||||
|
||||
// [OpenRouter]
|
||||
|
||||
/**
|
||||
* Created to reflect the doc page: https://openrouter.ai/docs
|
||||
*
|
||||
* Update prompt (last updated 2023-12-12)
|
||||
* "Please update the following typescript object (do not change the definition, just values, and do not miss any rows), based on the information provided thereafter:"
|
||||
*
|
||||
* fields:
|
||||
* - cw: context window size (max tokens, total)
|
||||
* - cp: cost per 1k prompt tokens
|
||||
* - cc: cost per 1k completion tokens
|
||||
* - old: if true, this is an older model that has been superseded by a newer one
|
||||
*/
|
||||
const orModelMap: { [id: string]: { name: string; cw: number; cp?: number; cc?: number; old?: boolean; unfilt?: boolean; } } = {
|
||||
// 'openrouter/auto': { name: 'Auto (best for prompt)', cw: 128000, cp: undefined, cc: undefined, unfilt: undefined },
|
||||
'nousresearch/nous-capybara-7b': { name: 'Nous: Capybara 7B', cw: 4096, cp: 0, cc: 0, unfilt: true },
|
||||
'mistralai/mistral-7b-instruct': { name: 'Mistral 7B Instruct', cw: 8192, cp: 0, cc: 0, unfilt: true },
|
||||
'huggingfaceh4/zephyr-7b-beta': { name: 'Hugging Face: Zephyr 7B', cw: 4096, cp: 0, cc: 0, unfilt: true },
|
||||
'openchat/openchat-7b': { name: 'OpenChat 3.5', cw: 8192, cp: 0, cc: 0, unfilt: true },
|
||||
'gryphe/mythomist-7b': { name: 'MythoMist 7B', cw: 32768, cp: 0, cc: 0, unfilt: true },
|
||||
'openrouter/cinematika-7b': { name: 'Cinematika 7B (alpha)', cw: 32768, cp: 0, cc: 0, unfilt: true },
|
||||
'rwkv/rwkv-5-world-3b': { name: 'RWKV v5 World 3B (beta)', cw: 10000, cp: 0, cc: 0, unfilt: true },
|
||||
'recursal/rwkv-5-3b-ai-town': { name: 'RWKV v5 3B AI Town (beta)', cw: 10000, cp: 0, cc: 0, unfilt: true },
|
||||
'jebcarter/psyfighter-13b': { name: 'Psyfighter 13B', cw: 4096, cp: 0.0001, cc: 0.0001, unfilt: true },
|
||||
'koboldai/psyfighter-13b-2': { name: 'Psyfighter v2 13B', cw: 4096, cp: 0.0001, cc: 0.0001, unfilt: true },
|
||||
'nousresearch/nous-hermes-llama2-13b': { name: 'Nous: Hermes 13B', cw: 4096, cp: 0.000075, cc: 0.000075, unfilt: true },
|
||||
'meta-llama/codellama-34b-instruct': { name: 'Meta: CodeLlama 34B Instruct', cw: 8192, cp: 0.0002, cc: 0.0002, unfilt: true },
|
||||
'phind/phind-codellama-34b': { name: 'Phind: CodeLlama 34B v2', cw: 4096, cp: 0.0002, cc: 0.0002, unfilt: true },
|
||||
'intel/neural-chat-7b': { name: 'Neural Chat 7B v3.1', cw: 4096, cp: 0.0025, cc: 0.0025, unfilt: true },
|
||||
'mistralai/mixtral-8x7b-instruct': { name: 'Mistral: Mixtral 8x7B Instruct (beta)', cw: 32768, cp: 0.0003, cc: 0.0003, unfilt: true },
|
||||
'haotian-liu/llava-13b': { name: 'Llava 13B', cw: 2048, cp: 0.0025, cc: 0.0025, unfilt: true },
|
||||
'nousresearch/nous-hermes-2-vision-7b': { name: 'Nous: Hermes 2 Vision 7B (alpha)', cw: 4096, cp: 0.0025, cc: 0.0025, unfilt: true },
|
||||
'meta-llama/llama-2-13b-chat': { name: 'Meta: Llama v2 13B Chat', cw: 4096, cp: 0.000156755, cc: 0.000156755, unfilt: true },
|
||||
'openai/gpt-3.5-turbo': { name: 'OpenAI: GPT-3.5 Turbo', cw: 4095, cp: 0.001, cc: 0.002, unfilt: false },
|
||||
'openai/gpt-3.5-turbo-1106': { name: 'OpenAI: GPT-3.5 Turbo 16k (preview)', cw: 16385, cp: 0.001, cc: 0.002, unfilt: false },
|
||||
'openai/gpt-3.5-turbo-16k': { name: 'OpenAI: GPT-3.5 Turbo 16k', cw: 16385, cp: 0.003, cc: 0.004, unfilt: false },
|
||||
'openai/gpt-4-1106-preview': { name: 'OpenAI: GPT-4 Turbo (preview)', cw: 128000, cp: 0.01, cc: 0.03, unfilt: false },
|
||||
'openai/gpt-4': { name: 'OpenAI: GPT-4', cw: 8191, cp: 0.03, cc: 0.06, unfilt: false },
|
||||
'openai/gpt-4-32k': { name: 'OpenAI: GPT-4 32k', cw: 32767, cp: 0.06, cc: 0.12, unfilt: false },
|
||||
'openai/gpt-4-vision-preview': { name: 'OpenAI: GPT-4 Vision (preview)', cw: 128000, cp: 0.01, cc: 0.03, unfilt: false },
|
||||
'openai/gpt-3.5-turbo-instruct': { name: 'OpenAI: GPT-3.5 Turbo Instruct', cw: 4095, cp: 0.0015, cc: 0.002, unfilt: false },
|
||||
'google/palm-2-chat-bison': { name: 'Google: PaLM 2 Chat', cw: 36864, cp: 0.00025, cc: 0.0005, unfilt: true },
|
||||
'google/palm-2-codechat-bison': { name: 'Google: PaLM 2 Code Chat', cw: 28672, cp: 0.00025, cc: 0.0005, unfilt: true },
|
||||
'google/palm-2-chat-bison-32k': { name: 'Google: PaLM 2 Chat 32k', cw: 131072, cp: 0.00025, cc: 0.0005, unfilt: true },
|
||||
'google/palm-2-codechat-bison-32k': { name: 'Google: PaLM 2 Code Chat 32k', cw: 131072, cp: 0.00025, cc: 0.0005, unfilt: true },
|
||||
'google/gemini-pro': { name: 'Google: Gemini Pro (preview)', cw: 131040, cp: 0.00025, cc: 0.0005, unfilt: true },
|
||||
'google/gemini-pro-vision': { name: 'Google: Gemini Pro Vision (preview)', cw: 65536, cp: 0.00025, cc: 0.0005, unfilt: true },
|
||||
'perplexity/pplx-70b-online': { name: 'Perplexity: PPLX 70B Online', cw: 4096, cp: 0, cc: 0.0028, unfilt: true },
|
||||
'perplexity/pplx-7b-online': { name: 'Perplexity: PPLX 7B Online', cw: 4096, cp: 0, cc: 0.00028, unfilt: true },
|
||||
'perplexity/pplx-7b-chat': { name: 'Perplexity: PPLX 7B Chat', cw: 8192, cp: 0.00007, cc: 0.00028, unfilt: true },
|
||||
'perplexity/pplx-70b-chat': { name: 'Perplexity: PPLX 70B Chat', cw: 4096, cp: 0.0007, cc: 0.0028, unfilt: true },
|
||||
'meta-llama/llama-2-70b-chat': { name: 'Meta: Llama v2 70B Chat', cw: 4096, cp: 0.0007, cc: 0.00095, unfilt: true },
|
||||
'nousresearch/nous-hermes-llama2-70b': { name: 'Nous: Hermes 70B', cw: 4096, cp: 0.0009, cc: 0.0009, unfilt: true },
|
||||
'nousresearch/nous-capybara-34b': { name: 'Nous: Capybara 34B', cw: 32000, cp: 0.0007, cc: 0.0028, unfilt: true },
|
||||
'jondurbin/airoboros-l2-70b': { name: 'Airoboros 70B', cw: 4096, cp: 0.0007, cc: 0.00095, unfilt: true },
|
||||
'migtissera/synthia-70b': { name: 'Synthia 70B', cw: 8192, cp: 0.00375, cc: 0.00375, unfilt: true },
|
||||
'open-orca/mistral-7b-openorca': { name: 'Mistral OpenOrca 7B', cw: 8192, cp: 0.0001425006, cc: 0.0001425006, unfilt: true },
|
||||
'teknium/openhermes-2-mistral-7b': { name: 'OpenHermes 2 Mistral 7B', cw: 4096, cp: 0.0002, cc: 0.0002, unfilt: true },
|
||||
'teknium/openhermes-2.5-mistral-7b': { name: 'OpenHermes 2.5 Mistral 7B', cw: 4096, cp: 0.0002, cc: 0.0002, unfilt: true },
|
||||
'pygmalionai/mythalion-13b': { name: 'Pygmalion: Mythalion 13B', cw: 8192, cp: 0.001125, cc: 0.001125, unfilt: true },
|
||||
'undi95/remm-slerp-l2-13b': { name: 'ReMM SLERP 13B', cw: 6144, cp: 0.001125, cc: 0.001125, unfilt: true },
|
||||
'xwin-lm/xwin-lm-70b': { name: 'Xwin 70B', cw: 8192, cp: 0.00375, cc: 0.00375, unfilt: true },
|
||||
'gryphe/mythomax-l2-13b-8k': { name: 'MythoMax 13B 8k', cw: 8192, cp: 0.001125, cc: 0.001125, unfilt: true },
|
||||
'undi95/toppy-m-7b': { name: 'Toppy M 7B', cw: 32768, cp: 0.000375, cc: 0.000375, unfilt: true },
|
||||
'alpindale/goliath-120b': { name: 'Goliath 120B', cw: 6144, cp: 0.009375, cc: 0.009375, unfilt: true },
|
||||
'lizpreciatior/lzlv-70b-fp16-hf': { name: 'lzlv 70B', cw: 4096, cp: 0.0007, cc: 0.00095, unfilt: true },
|
||||
'neversleep/noromaid-20b': { name: 'Noromaid 20B', cw: 8192, cp: 0.00225, cc: 0.00225, unfilt: true },
|
||||
'01-ai/yi-34b-chat': { name: 'Yi 34B Chat', cw: 4096, cp: 0.0008, cc: 0.0008, unfilt: true },
|
||||
'01-ai/yi-34b': { name: 'Yi 34B (base)', cw: 4096, cp: 0.0008, cc: 0.0008, unfilt: true },
|
||||
'01-ai/yi-6b': { name: 'Yi 6B (base)', cw: 4096, cp: 0.00014, cc: 0.00014, unfilt: true },
|
||||
'togethercomputer/stripedhyena-nous-7b': { name: 'StripedHyena Nous 7B', cw: 32768, cp: 0.0002, cc: 0.0002, unfilt: true },
|
||||
'togethercomputer/stripedhyena-hessian-7b': { name: 'StripedHyena Hessian 7B (base)', cw: 32768, cp: 0.0002, cc: 0.0002, unfilt: true },
|
||||
'mistralai/mixtral-8x7b': { name: 'Mistral: Mixtral 8x7B (base) (beta)', cw: 32768, cp: 0.0006, cc: 0.0006, unfilt: true },
|
||||
'anthropic/claude-2': { name: 'Anthropic: Claude v2.1', cw: 200000, cp: 0.008, cc: 0.024, unfilt: false },
|
||||
'anthropic/claude-2.0': { name: 'Anthropic: Claude v2.0', cw: 100000, cp: 0.008, cc: 0.024, unfilt: false },
|
||||
'anthropic/claude-instant-v1': { name: 'Anthropic: Claude Instant v1', cw: 100000, cp: 0.00163, cc: 0.00551, unfilt: false },
|
||||
'mancer/weaver': { name: 'Mancer: Weaver (alpha)', cw: 8000, cp: 0.003375, cc: 0.003375, unfilt: true },
|
||||
'gryphe/mythomax-l2-13b': { name: 'MythoMax 13B', cw: 4096, cp: 0.0006, cc: 0.0006, unfilt: true },
|
||||
// Old models (maintained for reference)
|
||||
'openai/gpt-3.5-turbo-0301': { name: 'OpenAI: GPT-3.5 Turbo (older v0301)', cw: 4095, cp: 0.001, cc: 0.002, old: true },
|
||||
'openai/gpt-4-0314': { name: 'OpenAI: GPT-4 (older v0314)', cw: 8191, cp: 0.03, cc: 0.06, old: true },
|
||||
'openai/gpt-4-32k-0314': { name: 'OpenAI: GPT-4 32k (older v0314)', cw: 32767, cp: 0.06, cc: 0.12, old: true },
|
||||
'openai/text-davinci-002': { name: 'OpenAI: Davinci 2', cw: 4095, cp: 0.02, cc: 0.02, old: true },
|
||||
'anthropic/claude-v1': { name: 'Anthropic: Claude v1', cw: 9000, cp: 0.008, cc: 0.024, old: true },
|
||||
'anthropic/claude-1.2': { name: 'Anthropic: Claude (older v1)', cw: 9000, cp: 0.008, cc: 0.024, old: true },
|
||||
'anthropic/claude-instant-v1-100k': { name: 'Anthropic: Claude Instant 100k v1', cw: 100000, cp: 0.00163, cc: 0.00551, old: true },
|
||||
'anthropic/claude-v1-100k': { name: 'Anthropic: Claude 100k v1', cw: 100000, cp: 0.008, cc: 0.024, old: true },
|
||||
'anthropic/claude-instant-1.0': { name: 'Anthropic: Claude Instant (older v1)', cw: 9000, cp: 0.00163, cc: 0.00551, old: true },
|
||||
};
|
||||
const orOldModelIDs = [
|
||||
'openai/gpt-3.5-turbo-0301', 'openai/gpt-4-0314', 'openai/gpt-4-32k-0314', 'openai/text-davinci-002',
|
||||
'anthropic/claude-v1', 'anthropic/claude-1.2', 'anthropic/claude-instant-v1-100k', 'anthropic/claude-v1-100k', 'anthropic/claude-instant-1.0',
|
||||
];
|
||||
|
||||
const orModelFamilyOrder = [
|
||||
// great models (pickes by hand, they're free)
|
||||
@@ -402,28 +317,36 @@ export function openRouterModelFamilySortFn(a: { id: string }, b: { id: string }
|
||||
return aPrefixIndex !== -1 ? -1 : 1;
|
||||
}
|
||||
|
||||
export function openRouterModelToModelDescription(modelId: string, created: number, context_length?: number): ModelDescriptionSchema {
|
||||
export function openRouterModelToModelDescription(wireModel: object): ModelDescriptionSchema {
|
||||
|
||||
// label: use the known name if available, otherwise format the model id
|
||||
const orModel = orModelMap[modelId] ?? null;
|
||||
let label = orModel?.name || modelId.replace('/', ' · ');
|
||||
if (orModel?.cp === 0 && orModel?.cc === 0)
|
||||
// parse the model
|
||||
const model = wireOpenrouterModelsListOutputSchema.parse(wireModel);
|
||||
|
||||
// parse pricing
|
||||
const pricing = {
|
||||
cpmPrompt: parseFloat(model.pricing.prompt),
|
||||
cpmCompletion: parseFloat(model.pricing.completion),
|
||||
};
|
||||
const isFree = pricing.cpmPrompt === 0 && pricing.cpmCompletion === 0;
|
||||
|
||||
// openrouter provides the fields we need as part of the model object
|
||||
let label = model.name || model.id.replace('/', ' · ');
|
||||
if (isFree)
|
||||
label += ' · 🎁'; // Free? Discounted?
|
||||
|
||||
if (SERVER_DEBUG_WIRE && !orModel)
|
||||
console.log(' - openRouterModelToModelDescription: non-mapped model id:', modelId);
|
||||
|
||||
// context: use the known size if available, otherwise fallback to the (undocumneted) provided length or fallback again to 4096
|
||||
const contextWindow = orModel?.cw || context_length || 4096;
|
||||
|
||||
// hidden: hide by default older models or models not in known families
|
||||
const hidden = orModel?.old || !orModelFamilyOrder.some(prefix => modelId.startsWith(prefix));
|
||||
const hidden = orOldModelIDs.includes(model.id) || !orModelFamilyOrder.some(prefix => model.id.startsWith(prefix));
|
||||
|
||||
return fromManualMapping([], modelId, created, undefined, {
|
||||
idPrefix: modelId,
|
||||
return fromManualMapping([], model.id, undefined, undefined, {
|
||||
idPrefix: model.id,
|
||||
// latest: ...
|
||||
label,
|
||||
description: 'OpenRouter model',
|
||||
contextWindow,
|
||||
// created: ...
|
||||
// updated: ...
|
||||
description: model.description,
|
||||
contextWindow: model.context_length || 4096,
|
||||
maxCompletionTokens: model.top_provider.max_completion_tokens || undefined,
|
||||
pricing,
|
||||
interfaces: [LLM_IF_OAI_Chat],
|
||||
hidden,
|
||||
});
|
||||
@@ -452,6 +375,7 @@ function fromManualMapping(mappings: ManualMappings, id: string, created?: numbe
|
||||
description: known.description,
|
||||
contextWindow: known.contextWindow,
|
||||
...(!!known.maxCompletionTokens && { maxCompletionTokens: known.maxCompletionTokens }),
|
||||
...(!!known.pricing && { pricing: known.pricing }),
|
||||
interfaces: known.interfaces,
|
||||
...(!!known.hidden && { hidden: known.hidden }),
|
||||
};
|
||||
|
||||
@@ -5,15 +5,16 @@ import { createTRPCRouter, publicProcedure } from '~/server/api/trpc.server';
|
||||
import { env } from '~/server/env.mjs';
|
||||
import { fetchJsonOrTRPCError } from '~/server/api/trpc.serverutils';
|
||||
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { t2iCreateImagesOutputSchema } from '~/modules/t2i/t2i.server.types';
|
||||
|
||||
import type { OpenAIWire } from './openai.wiretypes';
|
||||
import { listModelsOutputSchema, ModelDescriptionSchema } from '../llm.server.types';
|
||||
import { Brand } from '~/common/app.config';
|
||||
import { fixupHost } from '~/common/util/urlUtils';
|
||||
|
||||
import { OpenAIWire, WireOpenAICreateImageOutput, wireOpenAICreateImageOutputSchema, WireOpenAICreateImageRequest } from './openai.wiretypes';
|
||||
import { llmsChatGenerateWithFunctionsOutputSchema, llmsListModelsOutputSchema, ModelDescriptionSchema } from '../llm.server.types';
|
||||
import { localAIModelToModelDescription, mistralModelsSort, mistralModelToModelDescription, oobaboogaModelToModelDescription, openAIModelToModelDescription, openRouterModelFamilySortFn, openRouterModelToModelDescription } from './models.data';
|
||||
|
||||
|
||||
// Input Schemas
|
||||
|
||||
const openAIDialects = z.enum(['azure', 'localai', 'mistral', 'oobabooga', 'openai', 'openrouter']);
|
||||
|
||||
export const openAIAccessSchema = z.object({
|
||||
@@ -52,8 +53,11 @@ export const openAIFunctionsSchema = z.array(z.object({
|
||||
required: z.array(z.string()).optional(),
|
||||
}).optional(),
|
||||
}));
|
||||
export type OpenAIFunctionsSchema = z.infer<typeof openAIFunctionsSchema>;
|
||||
|
||||
|
||||
// Router Input Schemas
|
||||
|
||||
const listModelsInputSchema = z.object({
|
||||
access: openAIAccessSchema,
|
||||
});
|
||||
@@ -64,35 +68,31 @@ const chatGenerateWithFunctionsInputSchema = z.object({
|
||||
functions: openAIFunctionsSchema.optional(), forceFunctionName: z.string().optional(),
|
||||
});
|
||||
|
||||
const createImagesInputSchema = z.object({
|
||||
access: openAIAccessSchema,
|
||||
request: z.object({
|
||||
prompt: z.string(),
|
||||
count: z.number().min(1),
|
||||
model: z.enum(['dall-e-2', 'dall-e-3']),
|
||||
quality: z.enum(['standard', 'hd']),
|
||||
asUrl: z.boolean(), // if false, returns a base64 encoded data Url
|
||||
size: z.enum(['256x256', '512x512', '1024x1024', '1792x1024', '1024x1792']),
|
||||
style: z.enum(['natural', 'vivid']),
|
||||
}),
|
||||
});
|
||||
|
||||
const moderationInputSchema = z.object({
|
||||
access: openAIAccessSchema,
|
||||
text: z.string(),
|
||||
});
|
||||
|
||||
|
||||
// Output Schemas
|
||||
|
||||
export const openAIChatGenerateOutputSchema = z.object({
|
||||
role: z.enum(['assistant', 'system', 'user']),
|
||||
content: z.string(),
|
||||
finish_reason: z.union([z.enum(['stop', 'length']), z.null()]),
|
||||
});
|
||||
|
||||
const openAIChatGenerateWithFunctionsOutputSchema = z.union([
|
||||
openAIChatGenerateOutputSchema,
|
||||
z.object({
|
||||
function_name: z.string(),
|
||||
function_arguments: z.record(z.any()),
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
export const llmOpenAIRouter = createTRPCRouter({
|
||||
|
||||
/* OpenAI: List the Models available */
|
||||
/* [OpenAI] List the Models available */
|
||||
listModels: publicProcedure
|
||||
.input(listModelsInputSchema)
|
||||
.output(listModelsOutputSchema)
|
||||
.output(llmsListModelsOutputSchema)
|
||||
.query(async ({ input: { access } }): Promise<{ models: ModelDescriptionSchema[] }> => {
|
||||
|
||||
let models: ModelDescriptionSchema[];
|
||||
@@ -195,7 +195,7 @@ export const llmOpenAIRouter = createTRPCRouter({
|
||||
case 'openrouter':
|
||||
models = openAIModels
|
||||
.sort(openRouterModelFamilySortFn)
|
||||
.map(model => openRouterModelToModelDescription(model.id, model.created, (model as any)?.['context_length']));
|
||||
.map(openRouterModelToModelDescription);
|
||||
break;
|
||||
|
||||
}
|
||||
@@ -203,10 +203,10 @@ export const llmOpenAIRouter = createTRPCRouter({
|
||||
return { models };
|
||||
}),
|
||||
|
||||
/* OpenAI: chat generation */
|
||||
/* [OpenAI] (non streaming) chat generation */
|
||||
chatGenerateWithFunctions: publicProcedure
|
||||
.input(chatGenerateWithFunctionsInputSchema)
|
||||
.output(openAIChatGenerateWithFunctionsOutputSchema)
|
||||
.output(llmsChatGenerateWithFunctionsOutputSchema)
|
||||
.mutation(async ({ input }) => {
|
||||
|
||||
const { access, model, history, functions, forceFunctionName } = input;
|
||||
@@ -234,11 +234,48 @@ export const llmOpenAIRouter = createTRPCRouter({
|
||||
: parseChatGenerateOutput(message as OpenAIWire.ChatCompletion.ResponseMessage, finish_reason);
|
||||
}),
|
||||
|
||||
/* OpenAI: check for content policy violations */
|
||||
/* [OpenAI] images/generations */
|
||||
createImages: publicProcedure
|
||||
.input(createImagesInputSchema)
|
||||
.output(t2iCreateImagesOutputSchema)
|
||||
.mutation(async ({ input: { access, request } }) => {
|
||||
|
||||
// Validate input
|
||||
if (request.model === 'dall-e-3' && request.count > 1)
|
||||
throw new TRPCError({ code: 'BAD_REQUEST', message: `[OpenAI Issue] dall-e-3 model does not support more than 1 image` });
|
||||
|
||||
// create 1 image (dall-e-3 won't support more than 1, so better transfer the burden to the client)
|
||||
const wireOpenAICreateImageOutput = await openaiPOST<WireOpenAICreateImageOutput, WireOpenAICreateImageRequest>(
|
||||
access, null,
|
||||
{
|
||||
prompt: request.prompt,
|
||||
model: request.model,
|
||||
n: request.count,
|
||||
quality: request.quality,
|
||||
response_format: request.asUrl ? 'url' : 'b64_json',
|
||||
size: request.size,
|
||||
style: request.style,
|
||||
user: 'big-agi',
|
||||
},
|
||||
'/v1/images/generations',
|
||||
);
|
||||
|
||||
// expect a single image and as URL
|
||||
const imagesOutput = wireOpenAICreateImageOutputSchema.parse(wireOpenAICreateImageOutput);
|
||||
return imagesOutput.data.map(image => {
|
||||
if ('b64_json' in image)
|
||||
throw new TRPCError({ code: 'INTERNAL_SERVER_ERROR', message: `[OpenAI Issue] Expected a url, got a b64_json (which is not implemented yet)` });
|
||||
return {
|
||||
imageUrl: image.url,
|
||||
altText: image.revised_prompt || request.prompt,
|
||||
};
|
||||
});
|
||||
}),
|
||||
|
||||
/* [OpenAI] check for content policy violations */
|
||||
moderation: publicProcedure
|
||||
.input(moderationInputSchema)
|
||||
.mutation(async ({ input }): Promise<OpenAIWire.Moderation.Response> => {
|
||||
const { access, text } = input;
|
||||
.mutation(async ({ input: { access, text } }): Promise<OpenAIWire.Moderation.Response> => {
|
||||
try {
|
||||
|
||||
return await openaiPOST<OpenAIWire.Moderation.Response, OpenAIWire.Moderation.Request>(access, null, {
|
||||
@@ -258,34 +295,11 @@ export const llmOpenAIRouter = createTRPCRouter({
|
||||
});
|
||||
|
||||
|
||||
type ModelSchema = z.infer<typeof openAIModelSchema>;
|
||||
type HistorySchema = z.infer<typeof openAIHistorySchema>;
|
||||
type FunctionsSchema = z.infer<typeof openAIFunctionsSchema>;
|
||||
|
||||
async function openaiGET<TOut extends object>(access: OpenAIAccessSchema, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = openAIAccess(access, null, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut>(url, 'GET', headers, undefined, `OpenAI/${access.dialect}`);
|
||||
}
|
||||
|
||||
async function openaiPOST<TOut extends object, TPostBody extends object>(access: OpenAIAccessSchema, modelRefId: string | null, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = openAIAccess(access, modelRefId, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut, TPostBody>(url, 'POST', headers, body, `OpenAI/${access.dialect}`);
|
||||
}
|
||||
|
||||
|
||||
const DEFAULT_HELICONE_OPENAI_HOST = 'oai.hconeai.com';
|
||||
const DEFAULT_MISTRAL_HOST = 'https://api.mistral.ai';
|
||||
const DEFAULT_OPENAI_HOST = 'api.openai.com';
|
||||
const DEFAULT_OPENROUTER_HOST = 'https://openrouter.ai/api';
|
||||
|
||||
export function fixupHost(host: string, apiPath: string): string {
|
||||
if (!host.startsWith('http'))
|
||||
host = `https://${host}`;
|
||||
if (host.endsWith('/') && apiPath.startsWith('/'))
|
||||
host = host.slice(0, -1);
|
||||
return host;
|
||||
}
|
||||
|
||||
export function openAIAccess(access: OpenAIAccessSchema, modelRefId: string | null, apiPath: string): { headers: HeadersInit, url: string } {
|
||||
switch (access.dialect) {
|
||||
|
||||
@@ -400,7 +414,7 @@ export function openAIAccess(access: OpenAIAccessSchema, modelRefId: string | nu
|
||||
}
|
||||
}
|
||||
|
||||
export function openAIChatCompletionPayload(model: ModelSchema, history: HistorySchema, functions: FunctionsSchema | null, forceFunctionName: string | null, n: number, stream: boolean): OpenAIWire.ChatCompletion.Request {
|
||||
export function openAIChatCompletionPayload(model: OpenAIModelSchema, history: OpenAIHistorySchema, functions: OpenAIFunctionsSchema | null, forceFunctionName: string | null, n: number, stream: boolean): OpenAIWire.ChatCompletion.Request {
|
||||
return {
|
||||
model: model.id,
|
||||
messages: history,
|
||||
@@ -412,6 +426,16 @@ export function openAIChatCompletionPayload(model: ModelSchema, history: History
|
||||
};
|
||||
}
|
||||
|
||||
async function openaiGET<TOut extends object>(access: OpenAIAccessSchema, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = openAIAccess(access, null, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut>(url, 'GET', headers, undefined, `OpenAI/${access.dialect}`);
|
||||
}
|
||||
|
||||
async function openaiPOST<TOut extends object, TPostBody extends object>(access: OpenAIAccessSchema, modelRefId: string | null, body: TPostBody, apiPath: string /*, signal?: AbortSignal*/): Promise<TOut> {
|
||||
const { headers, url } = openAIAccess(access, modelRefId, apiPath);
|
||||
return await fetchJsonOrTRPCError<TOut, TPostBody>(url, 'POST', headers, body, `OpenAI/${access.dialect}`);
|
||||
}
|
||||
|
||||
function parseChatGenerateFCOutput(isFunctionsCall: boolean, message: OpenAIWire.ChatCompletion.ResponseFunctionCall) {
|
||||
// NOTE: Defensive: we run extensive validation because the API is not well tested and documented at the moment
|
||||
if (!isFunctionsCall)
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
/**
|
||||
* OpenAI API types - https://platform.openai.com/docs/api-reference/
|
||||
*
|
||||
* Notes:
|
||||
* - 2023-12-22:
|
||||
* Below we have the manually typed types for the OpenAI API. Everywhere else we are switching
|
||||
* to Zod inferred types, and we shall do it here sooner (so we can validate upon parsing too).
|
||||
*
|
||||
* - [FN0613]: function calling capability - only 2023-06-13 and later Chat models
|
||||
*/
|
||||
export namespace OpenAIWire {
|
||||
@@ -155,3 +162,46 @@ export namespace OpenAIWire {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// OpenAI text to image generation - https://platform.openai.com/docs/api-reference/images/create
|
||||
|
||||
const wireOpenAICreateImageRequestSchema = z.object({
|
||||
// The maximum length is 1000 characters for dall-e-2 and 4000 characters for dall-e-3
|
||||
prompt: z.string().max(4000),
|
||||
|
||||
// The model to use for image generation
|
||||
model: z.enum(['dall-e-2', 'dall-e-3']).optional().default('dall-e-2'),
|
||||
|
||||
// The number of images to generate. Must be between 1 and 10. For dall-e-3, only n=1 is supported.
|
||||
n: z.number().min(1).max(10).nullable().optional(),
|
||||
|
||||
// 'hd' creates images with finer details and greater consistency across the image. This param is only supported for dall-e-3
|
||||
quality: z.enum(['standard', 'hd']).optional(),
|
||||
|
||||
// The format in which the generated images are returned
|
||||
response_format: z.enum(['url', 'b64_json']).optional().default('url'),
|
||||
|
||||
// 'dall-e-2': must be one of 256x256, 512x512, or 1024x1024
|
||||
// 'dall-e-3': must be one of 1024x1024, 1792x1024, or 1024x1792
|
||||
size: z.enum(['256x256', '512x512', '1024x1024', '1792x1024', '1024x1792']).optional().default('1024x1024'),
|
||||
|
||||
// only used by 'dall-e-3': 'vivid' (hyper-real and dramatic images) or 'natural'
|
||||
style: z.enum(['vivid', 'natural']).optional().default('vivid'),
|
||||
|
||||
// A unique identifier representing your end-user
|
||||
user: z.string().optional(),
|
||||
});
|
||||
|
||||
export type WireOpenAICreateImageRequest = z.infer<typeof wireOpenAICreateImageRequestSchema>;
|
||||
|
||||
export const wireOpenAICreateImageOutputSchema = z.object({
|
||||
created: z.number(),
|
||||
data: z.array(z.object({
|
||||
b64_json: z.string().optional(),
|
||||
url: z.string().optional(),
|
||||
revised_prompt: z.string().optional(),
|
||||
})),
|
||||
});
|
||||
|
||||
export type WireOpenAICreateImageOutput = z.infer<typeof wireOpenAICreateImageOutputSchema>;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
|
||||
export const wireOpenrouterModelsListOutputSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
description: z.string(),
|
||||
pricing: z.object({
|
||||
prompt: z.string(),
|
||||
completion: z.string(),
|
||||
}),
|
||||
context_length: z.number(),
|
||||
architecture: z.object({
|
||||
tokenizer: z.string(),
|
||||
instruct_type: z.string().nullable(),
|
||||
}),
|
||||
top_provider: z.object({
|
||||
max_completion_tokens: z.number().nullable(),
|
||||
}),
|
||||
|
||||
// when logged in
|
||||
per_request_limits: z.object({
|
||||
prompt_tokens: z.string(),
|
||||
completion_tokens: z.string(),
|
||||
}).nullable(), // null on 'openrouter/auto'
|
||||
});
|
||||
@@ -19,7 +19,10 @@ export interface DLLM<TSourceSetup = unknown, TLLMOptions = unknown> {
|
||||
// modelcaps: DModelCapability[];
|
||||
contextTokens: number;
|
||||
maxOutputTokens: number;
|
||||
hidden: boolean;
|
||||
hidden: boolean; // hidden from Chat model UI selectors
|
||||
|
||||
// temporary special flags - not graduated yet
|
||||
isFree: boolean; // model is free to use
|
||||
|
||||
// llm -> source
|
||||
sId: DModelSourceId;
|
||||
@@ -236,10 +239,6 @@ export const useModelsStore = create<LlmsStore>()(
|
||||
);
|
||||
|
||||
|
||||
const defaultChatSuffixPreference = ['gpt-4-1106-preview', 'gpt-4-0613', 'gpt-4', 'gpt-4-32k', 'gpt-3.5-turbo'];
|
||||
const defaultFastSuffixPreference = ['gpt-3.5-turbo-1106', 'gpt-3.5-turbo-16k-0613', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo'];
|
||||
const defaultFuncSuffixPreference = ['gpt-4-1106-preview', 'gpt-3.5-turbo-16k-0613', 'gpt-3.5-turbo-0613', 'gpt-4-0613'];
|
||||
|
||||
export function findLLMOrThrow<TSourceSetup, TLLMOptions>(llmId: DLLMId): DLLM<TSourceSetup, TLLMOptions> {
|
||||
const llm = useModelsStore.getState().llms.find(llm => llm.id === llmId);
|
||||
if (!llm) throw new Error(`LLM ${llmId} not found`);
|
||||
@@ -247,16 +246,17 @@ export function findLLMOrThrow<TSourceSetup, TLLMOptions>(llmId: DLLMId): DLLM<T
|
||||
return llm as DLLM<TSourceSetup, TLLMOptions>;
|
||||
}
|
||||
|
||||
function findLlmIdBySuffix(llms: DLLM[], suffixes: string[], fallbackToFirst: boolean): DLLMId | null {
|
||||
if (!llms?.length) return null;
|
||||
for (const suffix of suffixes)
|
||||
for (const llm of llms)
|
||||
if (llm.id.endsWith(suffix))
|
||||
return llm.id;
|
||||
// otherwise return first id
|
||||
return fallbackToFirst ? llms[0].id : null;
|
||||
export function findSourceOrThrow<TSourceSetup>(sourceId: DModelSourceId) {
|
||||
const source: DModelSource<TSourceSetup> | undefined = useModelsStore.getState().sources.find(source => source.id === sourceId);
|
||||
if (!source) throw new Error(`ModelSource ${sourceId} not found`);
|
||||
return source;
|
||||
}
|
||||
|
||||
|
||||
const defaultChatSuffixPreference = ['gpt-4-1106-preview', 'gpt-4-0613', 'gpt-4', 'gpt-4-32k', 'gpt-3.5-turbo'];
|
||||
const defaultFastSuffixPreference = ['gpt-3.5-turbo-1106', 'gpt-3.5-turbo-16k-0613', 'gpt-3.5-turbo-0613', 'gpt-3.5-turbo-16k', 'gpt-3.5-turbo'];
|
||||
const defaultFuncSuffixPreference = ['gpt-4-1106-preview', 'gpt-3.5-turbo-16k-0613', 'gpt-3.5-turbo-0613', 'gpt-4-0613'];
|
||||
|
||||
function updateSelectedIds(allLlms: DLLM[], chatLlmId: DLLMId | null, fastLlmId: DLLMId | null, funcLlmId: DLLMId | null): Partial<ModelsData> {
|
||||
if (chatLlmId && !allLlms.find(llm => llm.id === chatLlmId)) chatLlmId = null;
|
||||
if (!chatLlmId) chatLlmId = findLlmIdBySuffix(allLlms, defaultChatSuffixPreference, true);
|
||||
@@ -270,6 +270,17 @@ function updateSelectedIds(allLlms: DLLM[], chatLlmId: DLLMId | null, fastLlmId:
|
||||
return { chatLLMId: chatLlmId, fastLLMId: fastLlmId, funcLLMId: funcLlmId };
|
||||
}
|
||||
|
||||
function findLlmIdBySuffix(llms: DLLM[], suffixes: string[], fallbackToFirst: boolean): DLLMId | null {
|
||||
if (!llms?.length) return null;
|
||||
for (const suffix of suffixes)
|
||||
for (const llm of llms)
|
||||
if (llm.id.endsWith(suffix))
|
||||
return llm.id;
|
||||
// otherwise return first id
|
||||
return fallbackToFirst ? llms[0].id : null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Current 'Chat' LLM, or null
|
||||
*/
|
||||
@@ -279,5 +290,4 @@ export function useChatLLM() {
|
||||
const chatLLM = chatLLMId ? state.llms.find(llm => llm.id === chatLLMId) ?? null : null;
|
||||
return { chatLLM };
|
||||
}, shallow);
|
||||
}
|
||||
|
||||
}
|
||||
+2
@@ -29,6 +29,8 @@ export interface IModelVendor<TSourceSetup = unknown, TAccess = unknown, TLLMOpt
|
||||
|
||||
getTransportAccess(setup?: Partial<TSourceSetup>): TAccess;
|
||||
|
||||
getRateLimitDelay?(llm: TDLLM): number;
|
||||
|
||||
rpcUpdateModelsQuery: (
|
||||
access: TAccess,
|
||||
enabled: boolean,
|
||||
|
||||
@@ -72,7 +72,7 @@ export function AnthropicSourceSetup(props: { sourceId: DModelSourceId }) {
|
||||
Advanced: You set the Helicone key, and Anthropic text will be routed through Helicone.
|
||||
</Alert>}
|
||||
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} error={isError} advanced={advanced} />
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} loading={isFetching} error={isError} advanced={advanced} />
|
||||
|
||||
{isError && <InlineError error={error} />}
|
||||
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ export function AzureSourceSetup(props: { sourceId: DModelSourceId }) {
|
||||
placeholder='...'
|
||||
/>
|
||||
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} error={isError} />
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} loading={isFetching} error={isError} />
|
||||
|
||||
{isError && <InlineError error={error} />}
|
||||
|
||||
|
||||
+1
-3
@@ -86,9 +86,7 @@ export function GeminiSourceSetup(props: { sourceId: DModelSourceId }) {
|
||||
of being unsafe.
|
||||
</FormHelperText>
|
||||
|
||||
<SetupFormRefetchButton
|
||||
refetch={refetch} disabled={!shallFetchSucceed || isFetching} error={isError}
|
||||
/>
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} loading={isFetching} error={isError} />
|
||||
|
||||
{isError && <InlineError error={error} />}
|
||||
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ export function LocalAISourceSetup(props: { sourceId: DModelSourceId }) {
|
||||
value={oaiHost} onChange={value => updateSetup({ oaiHost: value })}
|
||||
/>
|
||||
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} error={isError} />
|
||||
<SetupFormRefetchButton refetch={refetch} disabled={!shallFetchSucceed || isFetching} loading={isFetching} error={isError} />
|
||||
|
||||
{isError && <InlineError error={error} />}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user