mirror of
https://github.com/enricoros/big-AGI.git
synced 2026-05-10 21:50:14 -07:00
Compare commits
249 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7d6d7e619b | |||
| 34caa16e39 | |||
| 976426dbd3 | |||
| b4d8e39d56 | |||
| 11c41e7381 | |||
| 358d8a54ff | |||
| 3c8fedce68 | |||
| 1744b5b9d0 | |||
| 0c15476dd2 | |||
| 94ef76c67e | |||
| bd5bf6f94f | |||
| 1fbf454c3c | |||
| 07b62fe5c1 | |||
| 7fbf6ee2e8 | |||
| ba66fc30c5 | |||
| 45b7ed3220 | |||
| 20f1c4c0ae | |||
| 97b6fc5e2b | |||
| 44d8c30187 | |||
| e3957bf08b | |||
| acfe0aba21 | |||
| 6247b5411b | |||
| 5cc0b0a011 | |||
| 1fed2fb18c | |||
| 8a0e7a4e3d | |||
| 29a784c6c6 | |||
| 409a3ee194 | |||
| 54caa3e01a | |||
| e1a723a39f | |||
| 463ea35d7c | |||
| f751c91c68 | |||
| ad24c8771a | |||
| 6f82e2c3ed | |||
| f4b39071f0 | |||
| 621c968f3f | |||
| 564cf0fed0 | |||
| dee9492d4c | |||
| 6ae026f7c5 | |||
| 6bcbe286f3 | |||
| 6f35f72607 | |||
| 3a7aa75538 | |||
| e4e7ac260a | |||
| b8aaa4bb42 | |||
| 7793e2694b | |||
| 83f2c72f29 | |||
| 1caeaee7f0 | |||
| f354134234 | |||
| 66219d30e0 | |||
| b9e3942ed8 | |||
| 2354cdc1d1 | |||
| d929438df9 | |||
| 1acaed1de7 | |||
| 16195f8a55 | |||
| d7fc8c178f | |||
| 2894e16706 | |||
| c2340f3432 | |||
| 3b7b3106db | |||
| cff92819f9 | |||
| 2f981d852b | |||
| 8eef74d776 | |||
| 60e46204dc | |||
| 6a5d783435 | |||
| 0223e076c4 | |||
| ce80c78319 | |||
| cc0085ae61 | |||
| f28e243b9d | |||
| 2e4532593f | |||
| 1f10905a03 | |||
| 88762db484 | |||
| 3b5ab0ac70 | |||
| 8903c9296b | |||
| 97858a3c94 | |||
| 0ec3e83518 | |||
| 8c007b5bf7 | |||
| 768236b0e2 | |||
| 495d78b885 | |||
| 34b1e515fe | |||
| 79edbd3fa5 | |||
| f50d9994e2 | |||
| 1603d3085f | |||
| ccf7036f33 | |||
| a0a1a5e3c1 | |||
| fbf9120859 | |||
| 8a770beec3 | |||
| 6b31669765 | |||
| 26d72fc2d8 | |||
| 5eb56d0994 | |||
| dbc4a922d5 | |||
| 141f423842 | |||
| 667f2433ab | |||
| fd930ef548 | |||
| 7eadfb1a63 | |||
| 67cb07ac92 | |||
| 96d28c43fc | |||
| e57e3f5f0a | |||
| 7b99bd71da | |||
| 861a037321 | |||
| 84cbe6c434 | |||
| 2cbb811523 | |||
| 8ef4faa10f | |||
| f6a1c9bf52 | |||
| 5d9f6fb4f5 | |||
| 66840a8ecd | |||
| a8ee6b255a | |||
| bd73d1c533 | |||
| e33c0ebc42 | |||
| 57e4a35fee | |||
| d490b57410 | |||
| 0416602e5f | |||
| ddc27b2eb9 | |||
| 374deb147b | |||
| d2eabd1ad0 | |||
| efbc625cc3 | |||
| 91ae0b8cb0 | |||
| ddc5741b00 | |||
| 4729aca6b0 | |||
| bb4fc3a70c | |||
| 5d8084b650 | |||
| f316b892f5 | |||
| cbda1d7cd0 | |||
| 2f8e879976 | |||
| cc0ac5ae3c | |||
| 0185d24fb3 | |||
| 97dbdc9c31 | |||
| a07c66c9a3 | |||
| 308bd25bc0 | |||
| 70066a03b6 | |||
| a7f3872af3 | |||
| 22e10e675a | |||
| 89679e946d | |||
| 1d1bb9d3df | |||
| 8faf2b2595 | |||
| e47ad9700e | |||
| 372b19a057 | |||
| cbe156a868 | |||
| 181a3881e2 | |||
| 3eef03b303 | |||
| ad56e3165c | |||
| b1a96b6e75 | |||
| 56419b1b4e | |||
| 372f14a9c5 | |||
| e1ec56a120 | |||
| 5bb11249d6 | |||
| 9fbcca1ff2 | |||
| 323f2b2c3e | |||
| b971d38dd5 | |||
| 278f479a3a | |||
| 03aea5678d | |||
| b62b8ee7e6 | |||
| 63f55551e5 | |||
| b185fbc57d | |||
| ceb9d58e72 | |||
| a0bb515a4f | |||
| 2cfac2f18b | |||
| d412f538b2 | |||
| 94f90ad861 | |||
| 4a402e7937 | |||
| c226d6c391 | |||
| 67410e6c59 | |||
| 419c361147 | |||
| 3769a53ffa | |||
| ec4aaa3bfb | |||
| be52680fcd | |||
| 9d41ab9339 | |||
| f126fc3087 | |||
| 764377037c | |||
| 8e09eaab45 | |||
| 6523da186c | |||
| 6471fd8b6f | |||
| 247a74881a | |||
| 3ef09f0a5f | |||
| b924d331f9 | |||
| 14041b6012 | |||
| 2c6cc5ecec | |||
| ac022b1df0 | |||
| 0a2081de08 | |||
| 64a8e554c7 | |||
| 082d29fd2f | |||
| ba5cf9d002 | |||
| 57a55318df | |||
| e70f4f7a59 | |||
| 1d217fad67 | |||
| e95d46f085 | |||
| f4577878e1 | |||
| 1bd1e5c8e3 | |||
| c975dee965 | |||
| 9d690f4219 | |||
| 29ddb3f58d | |||
| 8626bc0b1c | |||
| c362cf6596 | |||
| 97264fc5ff | |||
| 494c4409c1 | |||
| d46e366c81 | |||
| 6afe33ee9c | |||
| 903c9e1cc3 | |||
| 3ef43fc3f5 | |||
| b1c3be05dd | |||
| efee23b4a7 | |||
| 06b67a7586 | |||
| 889a2dbf9d | |||
| 2f80fcc888 | |||
| f7ee479c1d | |||
| 94fa0981fe | |||
| 4c74afe438 | |||
| f76cea22de | |||
| 3d49110808 | |||
| 88a4579f7a | |||
| 241bde0333 | |||
| 73c7867cd6 | |||
| b35254f7ad | |||
| 213e78c956 | |||
| 7bf552c491 | |||
| 3bf9923f86 | |||
| a6a8a28f59 | |||
| 56a8e452bf | |||
| 6bec0bf70d | |||
| 5dc9c8f90e | |||
| e3290e12b1 | |||
| 9f37ce9e42 | |||
| 8904c0c811 | |||
| b0d021b7f2 | |||
| 0175f3b8a1 | |||
| 0fa9d5bf62 | |||
| 4919e38e3e | |||
| 2e99533f96 | |||
| f095645d89 | |||
| 757c83142e | |||
| 36d274ca9f | |||
| ec11b61f67 | |||
| 7765271d63 | |||
| 7c2464bba7 | |||
| 17e010f93c | |||
| 452d630a2a | |||
| f317a3e38f | |||
| f56195058e | |||
| 2e93dbb10c | |||
| f862456d73 | |||
| d99b0b2137 | |||
| 1d390f9aa7 | |||
| 514beb7940 | |||
| c7bdfce734 | |||
| e5fe4b06ad | |||
| 89b7c265d3 | |||
| 698c31943e | |||
| b70060d46e | |||
| 6ddc5ef53e | |||
| 212023c7e4 | |||
| b687f23c95 | |||
| 7a05d01554 |
@@ -32,6 +32,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
@@ -49,13 +55,15 @@ jobs:
|
||||
type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main-stable' }}
|
||||
type=ref,event=tag # Use the tag name as a tag for tag builds
|
||||
type=semver,pattern={{version}} # Generate semantic versioning tags for tag builds
|
||||
type=sha # Just in case none of the above applies
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
build-args: NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
|
||||
build-args: NEXT_PUBLIC_GA4_MEASUREMENT_ID=${{ secrets.GA4_MEASUREMENT_ID }}
|
||||
|
||||
@@ -15,9 +15,39 @@ Or fork & run on Vercel
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
|
||||
|
||||
## 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [documentation](docs/README.md)
|
||||
## 👉 [roadmap](https://github.com/users/enricoros/projects/4/views/2) 👉 [installation](docs/installation.md) 👉 [documentation](docs/README.md)
|
||||
|
||||
big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap
|
||||
> Note: bigger better features (incl. Beam-2) are being cooked outside of `main`.
|
||||
|
||||
[//]: # (big-AGI is an open book; see the **[ready-to-ship and future ideas](https://github.com/users/enricoros/projects/4/views/2)** in our open roadmap)
|
||||
|
||||
### What's New in 1.16.1...1.16.3 · Jun 20, 2024 (patch releases)
|
||||
|
||||
- 1.16.3: Anthropic Claude 3.5 Sonnet model support
|
||||
- 1.16.2: Improve web downloads, as text, markdwon, or HTML
|
||||
- 1.16.2: Proper support for Gemini models
|
||||
- 1.16.2: Added the latest Mistral model
|
||||
- 1.16.2: Tokenizer support for gpt-4o
|
||||
- 1.16.2: Updates to Beam
|
||||
- 1.16.1: Support for the new OpenAI GPT-4o 2024-05-13 model
|
||||
|
||||
### What's New in 1.16.0 · May 9, 2024 · Crystal Clear
|
||||
|
||||
- [Beam](https://big-agi.com/blog/beam-multi-model-ai-reasoning) core and UX improvements based on user feedback
|
||||
- Chat cost estimation 💰 (enable it in Labs / hover the token counter)
|
||||
- Save/load chat files with Ctrl+S / Ctrl+O on desktop
|
||||
- Major enhancements to the Auto-Diagrams tool
|
||||
- YouTube Transcriber Persona for chatting with video content, [#500](https://github.com/enricoros/big-AGI/pull/500)
|
||||
- Improved formula rendering (LaTeX), and dark-mode diagrams, [#508](https://github.com/enricoros/big-AGI/issues/508), [#520](https://github.com/enricoros/big-AGI/issues/520)
|
||||
- Models update: **Anthropic**, **Groq**, **Ollama**, **OpenAI**, **OpenRouter**, **Perplexity**
|
||||
- Code soft-wrap, chat text selection toolbar, 3x faster on Apple silicon, and more [#517](https://github.com/enricoros/big-AGI/issues/517), [507](https://github.com/enricoros/big-AGI/pull/507)
|
||||
|
||||
#### 3,000 Commits Milestone · April 7, 2024
|
||||
|
||||

|
||||
|
||||
- 🥇 Today we <b>celebrate commit 3000</b> in just over one year, and going stronger 🚀
|
||||
- 📢️ Thanks everyone for your support and words of love for Big-AGI, we are committed to creating the best AI experiences for everyone.
|
||||
|
||||
### What's New in 1.15.0 · April 1, 2024 · Beam
|
||||
|
||||
@@ -26,9 +56,11 @@ big-AGI is an open book; see the **[ready-to-ship and future ideas](https://gith
|
||||
- Message **Starring ⭐**: star important messages within chats, to attach them later. [#476](https://github.com/enricoros/big-AGI/issues/476)
|
||||
- Enhanced the default Persona
|
||||
- Fixes to Gemini models and SVGs, improvements to UI and icons
|
||||
- 1.15.1: Support for Gemini Pro 1.5 and OpenAI Turbo models
|
||||
- Beast release, over 430 commits, 10,000+ lines changed: [release notes](https://github.com/enricoros/big-AGI/releases/tag/v1.15.0), and changes [v1.14.1...v1.15.0](https://github.com/enricoros/big-AGI/compare/v1.14.1...v1.15.0)
|
||||
|
||||
### What's New in 1.14.1 · March 7, 2024 · Modelmorphic
|
||||
<details>
|
||||
<summary>What's New in 1.14.1 · March 7, 2024 · Modelmorphic</summary>
|
||||
|
||||
- **Anthropic** [Claude-3](https://www.anthropic.com/news/claude-3-family) model family support. [#443](https://github.com/enricoros/big-AGI/issues/443)
|
||||
- New **[Perplexity](https://www.perplexity.ai/)** and **[Groq](https://groq.com/)** integration (thanks @Penagwin). [#407](https://github.com/enricoros/big-AGI/issues/407), [#427](https://github.com/enricoros/big-AGI/issues/427)
|
||||
@@ -38,7 +70,10 @@ big-AGI is an open book; see the **[ready-to-ship and future ideas](https://gith
|
||||
- Enhanced UX with auto-sizing charts, refined search and folder functionalities, perfected scaling
|
||||
- And with more UI improvements, documentation, bug fixes (20 tickets), and developer enhancements
|
||||
|
||||
### What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>What's New in 1.13.0 · Feb 8, 2024 · Multi + Mind</summary>
|
||||
|
||||
https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385686b13
|
||||
|
||||
@@ -50,6 +85,8 @@ https://github.com/enricoros/big-AGI/assets/32999/01732528-730e-41dc-adc7-511385
|
||||
- Better looking chats with improved spacing, fonts, and menus
|
||||
- More: new video player, [LM Studio tutorial](https://github.com/enricoros/big-AGI/blob/main/docs/config-local-lmstudio.md) (thanks @aj47), [MongoDB support](https://github.com/enricoros/big-AGI/blob/main/docs/deploy-database.md) (thanks @ranfysvalle02), and speedups
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>What's New in 1.12.0 · Jan 26, 2024 · AGI Hotline</summary>
|
||||
|
||||
@@ -141,6 +178,22 @@ Add extra functionality with these integrations:
|
||||
|
||||
<br/>
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
To get started with big-AGI, follow our comprehensive [Installation Guide](docs/installation.md).
|
||||
The guide covers various installation options, whether you're spinning it up on
|
||||
your local computer, deploying on Vercel, on Cloudflare, or rolling it out
|
||||
through Docker.
|
||||
|
||||
Whether you're a developer, system integrator, or enterprise user, you'll find step-by-step instructions
|
||||
to set up big-AGI quickly and easily.
|
||||
|
||||
[](docs/installation.md)
|
||||
|
||||
Or bring your API keys and jump straight into our free instance on [big-AGI.com](https://big-agi.com).
|
||||
|
||||
<br/>
|
||||
|
||||
# 🌟 Get Involved!
|
||||
|
||||
[//]: # ([](https://discord.gg/MkH4qj2Jp9))
|
||||
@@ -150,86 +203,10 @@ Add extra functionality with these integrations:
|
||||
- [ ] ⭐ **Give us a star** on GitHub 👆
|
||||
- [ ] 🚀 **Do you like code**? You'll love this gem of a project! [_Pick up a task!_](https://github.com/users/enricoros/projects/4/views/4) - _easy_ to _pro_
|
||||
- [ ] 💡 Got a feature suggestion? [_Add your roadmap ideas_](https://github.com/enricoros/big-agi/issues/new?&template=roadmap-request.md)
|
||||
- [ ] ✨ Deploy your [fork](docs/customizations.md) for your friends and family, or [customize it for work](docs/customizations.md)
|
||||
- [ ] Check out some of the big-AGI [**community projects**](docs/customizations.md)
|
||||
|
||||
| Project | Features | GitHub |
|
||||
|---------|----------------------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| CoolAGI | Code Interpreter, Vision, Mind maps, and much more | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
|
||||
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
|
||||
- [ ] ✨ [Deploy](docs/installation.md) your [fork](docs/customizations.md) for your friends and family, or [customize it for work](docs/customizations.md)
|
||||
|
||||
<br/>
|
||||
|
||||
# 🧩 Develop
|
||||
|
||||
[//]: # ()
|
||||
|
||||
[//]: # ()
|
||||
|
||||
[//]: # ()
|
||||
|
||||
To download and run this Typescript/React/Next.js project locally, the only prerequisite is Node.js with the `npm` package manager.
|
||||
Clone this repo, install the dependencies (all local), and run the development server (which auto-watches the
|
||||
files for changes):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/enricoros/big-agi.git
|
||||
cd big-agi
|
||||
npm install
|
||||
npm run dev
|
||||
|
||||
# You will see something like:
|
||||
#
|
||||
# ▲ Next.js 14.1.0
|
||||
# - Local: http://localhost:3000
|
||||
# ✓ Ready in 2.6s
|
||||
```
|
||||
|
||||
The development app will be running on `http://localhost:3000`. Development builds have the advantage of not requiring
|
||||
a build step, but can be slower than production builds. Also, development builds won't have timeout on edge functions.
|
||||
|
||||
## 🛠️ Deploy from source
|
||||
|
||||
The _production_ build of the application is optimized for performance and is performed by the `npm run build` command,
|
||||
after installing the required dependencies.
|
||||
|
||||
```bash
|
||||
# .. repeat the steps above up to `npm install`, then:
|
||||
npm run build
|
||||
next start --port 3000
|
||||
```
|
||||
|
||||
The app will be running on the specified port, e.g. `http://localhost:3000`.
|
||||
|
||||
Want to deploy with username/password? See the [Authentication](docs/deploy-authentication.md) guide.
|
||||
|
||||
## 🐳 Deploy with Docker
|
||||
|
||||
For more detailed information on deploying with Docker, please refer to the [docker deployment documentation](docs/deploy-docker.md).
|
||||
|
||||
Build and run:
|
||||
|
||||
```bash
|
||||
docker build -t big-agi .
|
||||
docker run -d -p 3000:3000 big-agi
|
||||
```
|
||||
|
||||
Or run the official container:
|
||||
|
||||
- manually: `docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi`
|
||||
- or, with docker-compose: `docker-compose up` or see [the documentation](docs/deploy-docker.md) for a composer file with integrated browsing
|
||||
|
||||
## ☁️ Deploy on Cloudflare Pages
|
||||
|
||||
Please refer to the [Cloudflare deployment documentation](docs/deploy-cloudflare.md).
|
||||
|
||||
## 🚀 Deploy on Vercel
|
||||
|
||||
Create your GitHub fork, create a Vercel project over that fork, and deploy it. Or press the button below for convenience.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
|
||||
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/stargazers))
|
||||
|
||||
[//]: # ([](https://github.com/enricoros/big-agi/network))
|
||||
|
||||
+8
-14
@@ -37,22 +37,16 @@ System integrators, administrators, whitelabelers: instead of using the public b
|
||||
|
||||
Step-by-step deployment and system configuration instructions.
|
||||
|
||||
- **Deploy Your Own**
|
||||
- straightforward: **Local development**, **Vercel 1-Click**
|
||||
- **[Cloudflare Deployment](deploy-cloudflare.md)**
|
||||
- **[Docker Deployment](deploy-docker.md)**: Containers for Local or Cloud deployments
|
||||
- **[Installation](installation.md)**: Set up your own instance of big-AGI and related products
|
||||
- build from source or use pre-built
|
||||
- locally, in the public cloud, or on your own servers
|
||||
|
||||
|
||||
- **Deployment Server Features**
|
||||
- **[Database Setup](deploy-database.md)**: Optional, only required to enable "Chat Link Sharing"
|
||||
- **[Environment Variables](environment-variables.md)**: 📌 Set server-side API keys and special features in your deployments
|
||||
- **[HTTP Basic Authentication](deploy-authentication.md)**: Optional, Secure your big-AGI instance with a username and password
|
||||
|
||||
## Customization & Derivative UIs
|
||||
|
||||
👏 Customize big-AGI to fit your needs.
|
||||
|
||||
- **[Customizing big-AGI](customizations.md)**: how to alter source code and server-side configuration
|
||||
- **Advanced Customizations**:
|
||||
- **[Source code alterations guide](customizations.md)**: source code primer and alterations guidelines
|
||||
- **[Basic Authentication](deploy-authentication.md)**: Optional, adds a username and password wall
|
||||
- **[Database Setup](deploy-database.md)**: Optional, enables "Chat Link Sharing"
|
||||
- **[Environment Variables](environment-variables.md)**: 📌 Pre-configures models and services
|
||||
|
||||
## Support and Community
|
||||
|
||||
|
||||
+30
-2
@@ -5,11 +5,39 @@ by release.
|
||||
|
||||
- For the live roadmap, please see [the GitHub project](https://github.com/users/enricoros/projects/4/views/2)
|
||||
|
||||
### 1.16.0 - Mar 2024
|
||||
### 1.17.0 - Jun 2024
|
||||
|
||||
- milestone: [1.16.0](https://github.com/enricoros/big-agi/milestone/16)
|
||||
- milestone: [1.17.0](https://github.com/enricoros/big-agi/milestone/17)
|
||||
- work in progress: [big-AGI open roadmap](https://github.com/users/enricoros/projects/4/views/2), [help here](https://github.com/users/enricoros/projects/4/views/4)
|
||||
|
||||
### What's New in 1.16.1...1.16.3 · Jun 20, 2024 (patch releases)
|
||||
|
||||
- 1.16.3: Anthropic Claude 3.5 Sonnet model support
|
||||
- 1.16.2: Improve web downloads, as text, markdwon, or HTML
|
||||
- 1.16.2: Proper support for Gemini models
|
||||
- 1.16.2: Added the latest Mistral model
|
||||
- 1.16.2: Tokenizer support for gpt-4o
|
||||
- 1.16.2: Updates to Beam
|
||||
- 1.16.1: Support for the new OpenAI GPT-4o 2024-05-13 model
|
||||
|
||||
### What's New in 1.16.0 · May 9, 2024 · Crystal Clear
|
||||
|
||||
- [Beam](https://big-agi.com/blog/beam-multi-model-ai-reasoning) core and UX improvements based on user feedback
|
||||
- Chat cost estimation 💰 (enable it in Labs / hover the token counter)
|
||||
- Save/load chat files with Ctrl+S / Ctrl+O on desktop
|
||||
- Major enhancements to the Auto-Diagrams tool
|
||||
- YouTube Transcriber Persona for chatting with video content, [#500](https://github.com/enricoros/big-AGI/pull/500)
|
||||
- Improved formula rendering (LaTeX), and dark-mode diagrams, [#508](https://github.com/enricoros/big-AGI/issues/508), [#520](https://github.com/enricoros/big-AGI/issues/520)
|
||||
- Models update: **Anthropic**, **Groq**, **Ollama**, **OpenAI**, **OpenRouter**, **Perplexity**
|
||||
- Code soft-wrap, chat text selection toolbar, 3x faster on Apple silicon, and more [#517](https://github.com/enricoros/big-AGI/issues/517), [507](https://github.com/enricoros/big-AGI/pull/507)
|
||||
- Developers: update the LLMs data structures
|
||||
|
||||
### What's New in 1.15.1 · April 10, 2024 (minor release, models support)
|
||||
|
||||
- Support for the newly released Gemini Pro 1.5 models
|
||||
- Support for the new OpenAI 2024-04-09 Turbo models
|
||||
- Resilience fixes after the large success of 1.15.0
|
||||
|
||||
### What's New in 1.15.0 · April 1, 2024 · Beam
|
||||
|
||||
- ⚠️ [**Beam**: the multi-model AI chat](https://big-agi.com/blog/beam-multi-model-ai-reasoning). find better answers, faster - a game-changer for brainstorming, decision-making, and creativity. [#443](https://github.com/enricoros/big-AGI/issues/443)
|
||||
|
||||
@@ -20,6 +20,9 @@ If you have an `API Endpoint` and `API Key`, you can configure big-AGI as follow
|
||||
The deployed models are now available in the application. If you don't have a configured
|
||||
Azure OpenAI service instance, continue with the next section.
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
## Setting Up Azure
|
||||
|
||||
### Step 1: Azure Account & Subscription
|
||||
|
||||
@@ -68,7 +68,7 @@ The chat agent won't be able to access the web sites if the browserless containe
|
||||
- MAX_CONCURRENT_SESSIONS=10
|
||||
```
|
||||
|
||||
You can then add the proyy lines to your `.env` file.
|
||||
You can then add the proxy lines to your `.env` file.
|
||||
|
||||
```
|
||||
https_proxy=http://PROXY-IP:PROXY-PORT
|
||||
@@ -115,4 +115,4 @@ If you encounter any issues or have questions about configuring the browse funct
|
||||
|
||||
Enjoy the enhanced browsing experience within `big-AGI` and explore the web without ever leaving your chat!
|
||||
|
||||
Last updated on Feb 27, 2024 ([edit on GitHub](https://github.com/enricoros/big-AGI/edit/main/docs/config-feature-browse.md))
|
||||
Last updated on Feb 27, 2024 ([edit on GitHub](https://github.com/enricoros/big-AGI/edit/main/docs/config-feature-browse.md))
|
||||
|
||||
@@ -37,6 +37,9 @@ Check the URL and modify if different.
|
||||
2. Enter the API URL: `http://localhost:1234` (modify if different)
|
||||
3. Refresh by clicking on the `Models` button to load models from LM Studio
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Missing @mui/material**: Execute `npm install @mui/material` or `yarn add @mui/material`
|
||||
|
||||
@@ -36,6 +36,9 @@ Follow the guide at: https://localai.io/basics/getting_started/
|
||||
- Load the models (click on `Models 🔄`)
|
||||
- Select the model and chat
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
### Integration: Models Gallery
|
||||
|
||||
If the running LocalAI instance is configured with a [Model Gallery](https://localai.io/models/):
|
||||
|
||||
@@ -13,7 +13,7 @@ _Last updated Dec 16, 2023_
|
||||
|
||||
1. **Ensure Ollama API Server is Running**: Follow the official instructions to get Ollama up and running on your machine
|
||||
- For detailed instructions on setting up the Ollama API server, please refer to the
|
||||
[Ollama download page](https://ollama.ai/download) and [instructions for linux](https://github.com/jmorganca/ollama/blob/main/docs/linux.md).
|
||||
[Ollama download page](https://ollama.ai/download) and [instructions for linux](https://github.com/jmorganca/ollama/blob/main/docs/linux.md).
|
||||
2. **Add Ollama as a Model Source**: In `big-AGI`, navigate to the **Models** section, select **Add a model source**, and choose **Ollama**
|
||||
3. **Enter Ollama Host URL**: Provide the Ollama Host URL where the API server is accessible (e.g., `http://localhost:11434`)
|
||||
4. **Refresh Model List**: Once connected, refresh the list of available models to include the Ollama models
|
||||
@@ -22,6 +22,9 @@ _Last updated Dec 16, 2023_
|
||||
you'll have to press the 'Pull' button again, until a green message appears.
|
||||
5. **Chat with Ollama models**: select an Ollama model and begin chatting with AI personas
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
**Visual Configuration Guide**:
|
||||
|
||||
* After adding the `Ollama` model vendor, entering the IP address of an Ollama server, and refreshing models:<br/>
|
||||
@@ -37,7 +40,7 @@ _Last updated Dec 16, 2023_
|
||||
|
||||
### ⚠️ Network Troubleshooting
|
||||
|
||||
If you get errors about the server having trouble connecting with Ollama, please see
|
||||
If you get errors about the server having trouble connecting with Ollama, please see
|
||||
[this message](https://github.com/enricoros/big-AGI/issues/276#issuecomment-1858591483) on Issue #276.
|
||||
|
||||
And in brief, make sure the Ollama endpoint is accessible from the servers where you run big-AGI (which could
|
||||
@@ -69,15 +72,19 @@ Then, edit the nginx configuration file `/etc/nginx/sites-enabled/default` and a
|
||||
|
||||
```nginx
|
||||
location /ollama/ {
|
||||
proxy_pass http://localhost:11434;
|
||||
proxy_pass http://127.0.0.1:11434/;
|
||||
|
||||
# Disable buffering for the streaming responses (SSE)
|
||||
proxy_set_header Connection '';
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# Disable buffering for the streaming responses
|
||||
chunked_transfer_encoding off;
|
||||
proxy_buffering off;
|
||||
proxy_cache off;
|
||||
|
||||
# Longer timeouts
|
||||
proxy_read_timeout 3600;
|
||||
proxy_connect_timeout 3600;
|
||||
proxy_send_timeout 3600;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -25,15 +25,15 @@ This guide assumes that **big-AGI** is already installed on your system. Note th
|
||||
- Stop the Web UI as we need to modify the startup flags to enable the OpenAI API
|
||||
2. Enable the **openai extension**
|
||||
- Edit `CMD_FLAGS.txt`
|
||||
- Make sure that `--listen --api` is present and uncommented
|
||||
- Make sure that `--listen --api` is present and uncommented
|
||||
3. Restart text-generation-webui
|
||||
- Double-click on "start"
|
||||
- You should see something like:
|
||||
- You should see something like:
|
||||
```
|
||||
2023-12-07 21:51:21 INFO:Loading the extension "openai"...
|
||||
2023-12-07 21:51:21 INFO:OpenAI-compatible API URL:
|
||||
|
||||
http://0.0.0.0:5000
|
||||
|
||||
http://0.0.0.0:5000
|
||||
...
|
||||
INFO: Uvicorn running on http://0.0.0.0:5000 (Press CTRL+C to quit)
|
||||
Running on local URL: http://0.0.0.0:7860
|
||||
|
||||
@@ -22,6 +22,9 @@ This document details the process of integrating OpenRouter with big-AGI.
|
||||

|
||||
4. OpenAI GPT4-32k and other models will now be accessible and selectable in the application.
|
||||
|
||||
In addition to using the UI, configuration can also be done using
|
||||
[environment variables](environment-variables.md).
|
||||
|
||||
### Pricing
|
||||
|
||||
OpenRouter independently manages its service and pricing and is not affiliated with big-AGI.
|
||||
|
||||
@@ -66,7 +66,7 @@ Test your application thoroughly using local development (refer to README.md for
|
||||
|
||||
We introduced the `/info/debug` page that provides a detailed overview of the application's environment, including the API keys, environment variables, and other configuration settings.
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## Community Projects - Share Your Project
|
||||
|
||||
@@ -74,12 +74,12 @@ After deployment, share your project with the community. We will link to your pr
|
||||
|
||||
| Project | Features | GitHub |
|
||||
|----------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------|
|
||||
| 🚀 CoolAGI: Where AI meets Imagination<br/> | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
|
||||
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
|
||||
| 🚀 CoolAGI: Where AI meets Imagination<br/> | Code Interpreter, Vision, Mind maps, Web Searches, Advanced Data Analytics, Large Data Handling and more! | [nextgen-user/CoolAGI](https://github.com/nextgen-user/CoolAGI) |
|
||||
| HL-GPT | Fully remodeled UI | [harlanlewis/nextjs-chatgpt-app](https://github.com/harlanlewis/nextjs-chatgpt-app) |
|
||||
|
||||
For public projects, update your README.md with your modifications and submit a pull request to add your project to our list, aiding in its discovery.
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
|
||||
## Best Practices
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ As of Feb 27, 2024, this feature is in development.
|
||||
|
||||
## Configurations
|
||||
|
||||
| Scope | Default | Description / Instructions |
|
||||
| Scope | Default | Description / Instructions |
|
||||
|-----------------------------------------------------------------------------------------|------------------|-------------------------------------------------------------------------------------------------------------------------|
|
||||
| Your source builds of big-AGI | None | **Vercel**: enable Vercel Analytics from the dashboard. · **Google Analytics**: set environment variable at build time. |
|
||||
| Your docker builds of big-AGI | None | **Vercel**: n/a. · **Google Analytics**: set environment variable at `docker build` time. |
|
||||
|
||||
@@ -9,7 +9,7 @@ Docker ensures faster development cycles, easier collaboration, and seamless env
|
||||
```bash
|
||||
git clone https://github.com/enricoros/big-agi.git
|
||||
cd big-agi
|
||||
```
|
||||
```
|
||||
2. **Build the Docker Image**: Build a local docker image from the provided Dockerfile:
|
||||
```bash
|
||||
docker build -t big-agi .
|
||||
|
||||
@@ -91,7 +91,7 @@ requiring the user to enter an API key
|
||||
| `ANTHROPIC_API_HOST` | Changes the backend host for the Anthropic vendor, to enable platforms such as [config-aws-bedrock.md](config-aws-bedrock.md) | Optional |
|
||||
| `GEMINI_API_KEY` | The API key for Google AI's Gemini | Optional |
|
||||
| `GROQ_API_KEY` | The API key for Groq Cloud | Optional |
|
||||
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
|
||||
| `LOCALAI_API_HOST` | Sets the URL of the LocalAI server, or defaults to http://127.0.0.1:8080 | Optional |
|
||||
| `LOCALAI_API_KEY` | The (Optional) API key for LocalAI | Optional |
|
||||
| `MISTRAL_API_KEY` | The API key for Mistral | Optional |
|
||||
| `OLLAMA_API_HOST` | Changes the backend host for the Ollama vendor. See [config-local-ollama.md](config-local-ollama) | |
|
||||
@@ -128,7 +128,7 @@ Enable the app to Talk, Draw, and Google things up.
|
||||
| `GOOGLE_CSE_ID` | Google Custom/Programmable Search Engine ID - [Link to PSE](https://programmablesearchengine.google.com/) |
|
||||
| **Browse** | |
|
||||
| `PUPPETEER_WSS_ENDPOINT` | Puppeteer WebSocket endpoint - used for browsing (pade downloadeing), etc. |
|
||||
| **Backend** | |
|
||||
| **Backend** | |
|
||||
| `BACKEND_ANALYTICS` | Semicolon-separated list of analytics flags (see backend.analytics.ts). Flags: `domain` logs the responding domain. |
|
||||
| `HTTP_BASIC_AUTH_USERNAME` | See the [Authentication](deploy-authentication.md) guide. Username for HTTP Basic Authentication. |
|
||||
| `HTTP_BASIC_AUTH_PASSWORD` | Password for HTTP Basic Authentication. |
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# Installation Guide
|
||||
|
||||
Welcome to the big-AGI Installation Guide - Whether you're a developer
|
||||
eager to explore, a system integrator, or an enterprise looking for a
|
||||
white-label solution, this comprehensive guide ensures a smooth setup
|
||||
process for your own instance of big-AGI and related products.
|
||||
|
||||
**Try big-AGI** - You don't need to install anything if you want to play with big-AGI
|
||||
and have your API keys to various model services. You can access our free instance on [big-AGI.com](https://big-agi.com).
|
||||
The free instance runs the latest `main-stable` branch from this repository.
|
||||
|
||||
## 🧩 Build-your-own
|
||||
|
||||
If you want to change the code, have a deeper configuration,
|
||||
add your own models, or run your own instance, follow the steps below.
|
||||
|
||||
### Local Development
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- Node.js and npm installed on your machine.
|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Clone the big-AGI repository:
|
||||
```bash
|
||||
git clone https://github.com/enricoros/big-AGI.git
|
||||
cd big-AGI
|
||||
```
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
3. Run the development server:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
Your big-AGI instance is now running at `http://localhost:3000`.
|
||||
|
||||
### Local Production build
|
||||
|
||||
The production build is optimized for performance and follows
|
||||
the same steps 1 and 2 as for [local development](#local-development).
|
||||
|
||||
3. Build the production version:
|
||||
```bash
|
||||
# .. repeat the steps above up to `npm install`, then:
|
||||
npm run build
|
||||
```
|
||||
4. Start the production server (`npx` may be optional):
|
||||
```bash
|
||||
npx next start --port 3000
|
||||
```
|
||||
Your big-AGI production instance is on `http://localhost:3000`.
|
||||
|
||||
### Advanced Customization
|
||||
|
||||
Want to pre-enable models, customize the interface, or deploy with username/password or alter code to your needs?
|
||||
Check out the [Customizations Guide](README.md) for detailed instructions.
|
||||
|
||||
## ☁️ Cloud Deployment Options
|
||||
|
||||
To deploy big-AGI on a public server, you have several options. Choose the one that best fits your needs.
|
||||
|
||||
### Deploy on Vercel
|
||||
|
||||
Install big-AGI on Vercel with just a few clicks.
|
||||
|
||||
Create your GitHub fork, create a Vercel project over that fork, and deploy it. Or press the button below for convenience.
|
||||
|
||||
[](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI&env=OPENAI_API_KEY&envDescription=Backend%20API%20keys%2C%20optional%20and%20may%20be%20overridden%20by%20the%20UI.&envLink=https%3A%2F%2Fgithub.com%2Fenricoros%2Fbig-AGI%2Fblob%2Fmain%2Fdocs%2Fenvironment-variables.md&project-name=big-AGI)
|
||||
|
||||
### Deploy on Cloudflare
|
||||
|
||||
Deploy on Cloudflare's global network by installing big-AGI on
|
||||
Cloudflare Pages. Check out the [Cloudflare Installation Guide](deploy-cloudflare.md)
|
||||
for step-by-step instructions.
|
||||
|
||||
### Docker Deployments
|
||||
|
||||
Containerize your big-AGI installation using Docker for portability and scalability.
|
||||
Our [Docker Deployment Guide](deploy-docker.md) will walk you through the process,
|
||||
or follow the steps below for a quick start.
|
||||
|
||||
1. (optional) Build the Docker image - if you do not want to use the [pre-built Docker images](https://github.com/enricoros/big-AGI/pkgs/container/big-agi):
|
||||
```bash
|
||||
docker build -t big-agi .
|
||||
```
|
||||
2. Run the Docker container with either:
|
||||
```bash
|
||||
# 2A. if you built the image yourself:
|
||||
docker run -d -p 3000:3000 big-agi
|
||||
|
||||
# 2B. or use the pre-built image:
|
||||
docker run -d -p 3000:3000 ghcr.io/enricoros/big-agi
|
||||
|
||||
# 2C. or use docker-compose:
|
||||
docker-compose up
|
||||
```
|
||||
Access your big-AGI instance at `http://localhost:3000`.
|
||||
|
||||
### Midori AI Subsystem for Docker Deployment
|
||||
|
||||
Follow the instructions found on [Midori AI Subsystem Site](https://io.midori-ai.xyz/subsystem/manager/)
|
||||
for your host OS. After completing the setup process, install the Big-AGI docker backend to the Midori AI Subsystem.
|
||||
|
||||
## Enterprise-Grade Installation
|
||||
|
||||
For businesses seeking a fully-managed, scalable solution, consider our managed installations.
|
||||
Enjoy all the features of big-AGI without the hassle of infrastructure management. [hello@big-agi.com](mailto:hello@big-agi.com) to learn more.
|
||||
|
||||
## Support
|
||||
|
||||
Join our vibrant community of developers, researchers, and AI enthusiasts. Share your projects, get help, and collaborate with others.
|
||||
|
||||
- [Discord Community](https://discord.gg/MkH4qj2Jp9)
|
||||
- [Twitter](https://twitter.com/yourusername)
|
||||
|
||||
For any questions or inquiries, please don't hesitate to [reach out to our team](mailto:hello@big-agi.com).
|
||||
Generated
+1032
-456
File diff suppressed because it is too large
Load Diff
+31
-25
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "big-agi",
|
||||
"version": "1.15.0",
|
||||
"version": "1.16.0",
|
||||
"private": true,
|
||||
"author": "Enrico Ros <enrico.ros@gmail.com>",
|
||||
"repository": "https://github.com/enricoros/big-agi",
|
||||
@@ -21,14 +21,15 @@
|
||||
"@emotion/cache": "^11.11.0",
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/server": "^11.11.0",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.14",
|
||||
"@mui/joy": "^5.0.0-beta.32",
|
||||
"@next/bundle-analyzer": "^14.1.4",
|
||||
"@next/third-parties": "^14.1.4",
|
||||
"@prisma/client": "^5.11.0",
|
||||
"@emotion/styled": "^11.11.5",
|
||||
"@mui/icons-material": "^5.15.17",
|
||||
"@mui/joy": "^5.0.0-beta.36",
|
||||
"@mui/material": "^5.15.17",
|
||||
"@next/bundle-analyzer": "^14.2.3",
|
||||
"@next/third-parties": "^14.2.3",
|
||||
"@prisma/client": "^5.13.0",
|
||||
"@sanity/diff-match-patch": "^3.1.1",
|
||||
"@t3-oss/env-nextjs": "^0.9.2",
|
||||
"@t3-oss/env-nextjs": "^0.10.1",
|
||||
"@tanstack/react-query": "~4.36.1",
|
||||
"@trpc/client": "10.44.1",
|
||||
"@trpc/next": "10.44.1",
|
||||
@@ -37,49 +38,54 @@
|
||||
"@vercel/analytics": "^1.2.2",
|
||||
"@vercel/speed-insights": "^1.0.10",
|
||||
"browser-fs-access": "^0.35.0",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"eventsource-parser": "^1.1.2",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"next": "^14.1.4",
|
||||
"next": "~14.1.4",
|
||||
"nprogress": "^0.2.0",
|
||||
"pdfjs-dist": "4.0.379",
|
||||
"pdfjs-dist": "4.2.67",
|
||||
"plantuml-encoder": "^1.4.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-csv": "^2.2.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-katex": "^3.0.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-player": "^2.15.1",
|
||||
"react-resizable-panels": "^2.0.13",
|
||||
"react-player": "^2.16.0",
|
||||
"react-resizable-panels": "^2.0.19",
|
||||
"react-timeago": "^7.2.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"sharp": "^0.33.2",
|
||||
"remark-math": "^6.0.0",
|
||||
"sharp": "^0.33.3",
|
||||
"superjson": "^2.2.1",
|
||||
"tesseract.js": "^5.0.5",
|
||||
"tiktoken": "^1.0.13",
|
||||
"tesseract.js": "^5.1.0",
|
||||
"tiktoken": "^1.0.15",
|
||||
"turndown": "^7.2.0",
|
||||
"uuid": "^9.0.1",
|
||||
"zod": "^3.22.4",
|
||||
"zod": "^3.23.8",
|
||||
"zustand": "^4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/puppeteer": "0.0.5",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/node": "^20.12.11",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@types/plantuml-encoder": "^1.4.2",
|
||||
"@types/prismjs": "^1.26.3",
|
||||
"@types/react": "^18.2.67",
|
||||
"@types/prismjs": "^1.26.4",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-csv": "^1.1.10",
|
||||
"@types/react-dom": "^18.2.22",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-katex": "^3.0.4",
|
||||
"@types/react-timeago": "^4.1.7",
|
||||
"@types/turndown": "^5.0.4",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "^14.1.4",
|
||||
"eslint-config-next": "^14.2.3",
|
||||
"prettier": "^3.2.5",
|
||||
"prisma": "^5.11.0",
|
||||
"typescript": "^5.4.3"
|
||||
"prisma": "^5.13.0",
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.0.0 || ^18.0.0"
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Brand } from '~/common/app.config';
|
||||
import { ROUTE_APP_CHAT, ROUTE_INDEX } from '~/common/app.routes';
|
||||
|
||||
// apps access
|
||||
import { incrementalNewsVersion } from '../../src/apps/news/news.version';
|
||||
import { incrementalNewsVersion, useAppNewsStateStore } from '../../src/apps/news/news.version';
|
||||
|
||||
// capabilities access
|
||||
import { useCapabilityBrowserSpeechRecognition, useCapabilityElevenLabs, useCapabilityTextToImage } from '~/common/components/useCapabilities';
|
||||
@@ -32,6 +32,7 @@ import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
// utils access
|
||||
import { clientHostName, isChromeDesktop, isFirefox, isIPhoneUser, isMacUser, isPwa, isVercelFromFrontend } from '~/common/util/pwaUtils';
|
||||
import { getGA4MeasurementId } from '~/common/components/GoogleAnalytics';
|
||||
import { prettyTimestampForFilenames } from '~/common/util/timeUtils';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
|
||||
@@ -80,7 +81,8 @@ function AppDebug() {
|
||||
const chatsCount = useChatStore.getState().conversations?.length;
|
||||
const uxLabsExperiments = Object.entries(useUXLabsStore.getState()).filter(([_k, v]) => v === true).map(([k, _]) => k).join(', ');
|
||||
const { folders, enableFolders } = useFolderStore.getState();
|
||||
const { lastSeenNewsVersion, usageCount } = useAppStateStore.getState();
|
||||
const { lastSeenNewsVersion } = useAppNewsStateStore.getState();
|
||||
const { usageCount } = useAppStateStore.getState();
|
||||
|
||||
|
||||
// derived state
|
||||
@@ -127,7 +129,7 @@ function AppDebug() {
|
||||
const handleDownload = async () => {
|
||||
fileSave(
|
||||
new Blob([JSON.stringify({ client: cClient, agi: cProduct, backend: cBackend }, null, 2)], { type: 'application/json' }),
|
||||
{ fileName: `big-agi-debug-${new Date().toISOString().replace(/:/g, '-')}.json`, extensions: ['.json'] },
|
||||
{ fileName: `big-agi_debug_${prettyTimestampForFilenames()}.json`, extensions: ['.json'] },
|
||||
)
|
||||
.then(() => setSaved(true))
|
||||
.catch(e => console.error('Error saving debug.json', e));
|
||||
|
||||
@@ -77,9 +77,12 @@ function AppShareTarget() {
|
||||
setIsDownloading(true);
|
||||
callBrowseFetchPage(intentURL)
|
||||
.then(page => {
|
||||
if (page.stopReason !== 'error')
|
||||
queueComposerTextAndLaunchApp('\n\n```' + intentURL + '\n' + page.content + '\n```\n');
|
||||
else
|
||||
if (page.stopReason !== 'error') {
|
||||
let pageContent = page.content.markdown || page.content.text || page.content.html || '';
|
||||
if (pageContent)
|
||||
pageContent = '\n\n```' + intentURL + '\n' + pageContent + '\n```\n';
|
||||
queueComposerTextAndLaunchApp(pageContent);
|
||||
} else
|
||||
setErrorMessage('Could not read any data' + page.error ? ': ' + page.error : '');
|
||||
})
|
||||
.catch(error => setErrorMessage(error?.message || error || 'Unknown error'))
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 248 KiB |
+28
-3
@@ -3,9 +3,16 @@
|
||||
"short_name": "big-AGI",
|
||||
"theme_color": "#32383E",
|
||||
"background_color": "#9FA6AD",
|
||||
"description": "Personal AGI App",
|
||||
"description": "Your Generative AI Suite",
|
||||
"categories": [
|
||||
"productivity",
|
||||
"AI",
|
||||
"tool",
|
||||
"utilities"
|
||||
],
|
||||
"display": "standalone",
|
||||
"start_url": "/",
|
||||
"start_url": "/?source=pwa",
|
||||
"scope": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.png",
|
||||
@@ -24,6 +31,17 @@
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "/link/share_target",
|
||||
"accept": {
|
||||
"application/big-agi": [
|
||||
".agi",
|
||||
".agi.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"share_target": {
|
||||
"action": "/link/share_target",
|
||||
"method": "GET",
|
||||
@@ -33,5 +51,12 @@
|
||||
"text": "text",
|
||||
"url": "url"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Call",
|
||||
"url": "/call",
|
||||
"description": "Call a Persona"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -30,8 +30,16 @@ export function AppBeam() {
|
||||
|
||||
// state
|
||||
const [showDebug, setShowDebug] = React.useState(false);
|
||||
const conversation = React.useRef<DConversation>(initTestConversation());
|
||||
const beamStoreApi = React.useRef(initTestBeamStore(conversation.current.messages)).current;
|
||||
|
||||
const [conversation, setConversation] = React.useState<DConversation>(() => initTestConversation());
|
||||
const [beamStoreApi] = React.useState(() => createBeamVanillaStore());
|
||||
|
||||
|
||||
// reinit the beam store if the conversation changes
|
||||
React.useEffect(() => {
|
||||
initTestBeamStore(conversation.messages, beamStoreApi);
|
||||
}, [beamStoreApi, conversation]);
|
||||
|
||||
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
@@ -44,7 +52,7 @@ export function AppBeam() {
|
||||
|
||||
|
||||
const handleClose = React.useCallback(() => {
|
||||
beamStoreApi.getState().terminate();
|
||||
beamStoreApi.getState().terminateKeepingSettings();
|
||||
}, [beamStoreApi]);
|
||||
|
||||
|
||||
@@ -56,10 +64,7 @@ export function AppBeam() {
|
||||
</Button>
|
||||
|
||||
{/* 'open' */}
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={() => {
|
||||
conversation.current = initTestConversation();
|
||||
initTestBeamStore(conversation.current.messages, beamStoreApi);
|
||||
}}>
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={() => setConversation(initTestConversation())}>
|
||||
.open
|
||||
</Button>
|
||||
|
||||
@@ -67,7 +72,7 @@ export function AppBeam() {
|
||||
<Button size='sm' variant='plain' color='neutral' onClick={handleClose}>
|
||||
.close
|
||||
</Button>
|
||||
</>, [beamStoreApi, handleClose, showDebug]), null, 'AppBeam');
|
||||
</>, [handleClose, showDebug]), null, 'AppBeam');
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import { Box, Card, ListDivider, ListItemDecorator, MenuItem, Switch, Typography } from '@mui/joy';
|
||||
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
|
||||
@@ -99,7 +99,7 @@ export function Telephone(props: {
|
||||
|
||||
// external state
|
||||
const { chatLLMId, chatLLMDropdown } = useChatLLMDropdown();
|
||||
const { chatTitle, reMessages } = useChatStore(state => {
|
||||
const { chatTitle, reMessages } = useChatStore(useShallow(state => {
|
||||
const conversation = props.callIntent.conversationId
|
||||
? state.conversations.find(conversation => conversation.id === props.callIntent.conversationId) ?? null
|
||||
: null;
|
||||
@@ -107,7 +107,7 @@ export function Telephone(props: {
|
||||
chatTitle: conversation ? conversationTitle(conversation) : null,
|
||||
reMessages: conversation ? conversation.messages : null,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
const persona = SystemPurposes[props.callIntent.personaId as SystemPurposeId] ?? undefined;
|
||||
const personaCallStarters = persona?.call?.starters ?? undefined;
|
||||
const personaVoiceId = overridePersonaVoice ? undefined : (persona?.voices?.elevenLabs?.voiceId ?? undefined);
|
||||
@@ -225,7 +225,7 @@ export function Telephone(props: {
|
||||
let finalText = '';
|
||||
let error: any | null = null;
|
||||
setPersonaTextInterim('💭...');
|
||||
llmStreamingChatGenerate(chatLLMId, callPrompt, null, null, responseAbortController.current.signal, ({ textSoFar }) => {
|
||||
llmStreamingChatGenerate(chatLLMId, callPrompt, 'call', callMessages[0].id, null, null, responseAbortController.current.signal, ({ textSoFar }) => {
|
||||
const text = textSoFar?.trim();
|
||||
if (text) {
|
||||
finalText = text;
|
||||
|
||||
+122
-186
@@ -1,11 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { useTheme } from '@mui/joy';
|
||||
|
||||
import { DEV_MODE_SETTINGS } from '../settings-modal/UxLabsSettings';
|
||||
import { DiagramConfig, DiagramsModal } from '~/modules/aifn/digrams/DiagramsModal';
|
||||
import { FlattenerModal } from '~/modules/aifn/flatten/FlattenerModal';
|
||||
import { TradeConfig, TradeModal } from '~/modules/trade/TradeModal';
|
||||
import { downloadConversation, openAndLoadConversations } from '~/modules/trade/trade.client';
|
||||
import { getChatLLMId, useChatLLM } from '~/modules/llms/store-llms';
|
||||
import { imaginePromptFromText } from '~/modules/aifn/imagine/imaginePromptFromText';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
@@ -16,17 +19,17 @@ import { ConfirmationModal } from '~/common/components/ConfirmationModal';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { PanelResizeInset } from '~/common/components/panes/GoodPanelResizeHandler';
|
||||
import { PreferencesTab, useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
import { ScrollToBottomButton } from '~/common/scroll-to-bottom/ScrollToBottomButton';
|
||||
import { addSnackbar, removeSnackbar } from '~/common/components/useSnackbarsStore';
|
||||
import { createDMessage, DConversationId, DMessage, getConversation, getConversationSystemPurposeId, useConversation } from '~/common/state/store-chats';
|
||||
import { getUXLabsHighPerformance, useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
import { createDMessage, DConversationId, DMessage, DMessageMetadata, getConversation, getConversationSystemPurposeId, useConversation } from '~/common/state/store-chats';
|
||||
import { themeBgAppChatComposer } from '~/common/app.theme';
|
||||
import { useFolderStore } from '~/common/state/store-folders';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { useOptimaLayout, usePluggableOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { useRouterQuery } from '~/common/app.routes';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './components/composer/composer.types';
|
||||
import { ChatBarAltBeam } from './components/ChatBarAltBeam';
|
||||
@@ -37,14 +40,9 @@ import { ChatDrawerMemo } from './components/ChatDrawer';
|
||||
import { ChatMessageList } from './components/ChatMessageList';
|
||||
import { ChatPageMenuItems } from './components/ChatPageMenuItems';
|
||||
import { Composer } from './components/composer/Composer';
|
||||
import { getInstantAppChatPanesCount, usePanesManager } from './components/panes/usePanesManager';
|
||||
import { usePanesManager } from './components/panes/usePanesManager';
|
||||
|
||||
import { DEV_MODE_SETTINGS } from '../settings-modal/UxLabsSettings';
|
||||
import { extractChatCommand, findAllChatCommands } from './commands/commands.registry';
|
||||
import { runAssistantUpdatingState } from './editors/chat-stream';
|
||||
import { runBrowseGetPageUpdatingState } from './editors/browse-load';
|
||||
import { runImageGenerationUpdatingState } from './editors/image-generate';
|
||||
import { runReActUpdatingState } from './editors/react-tangent';
|
||||
import { _handleExecute } from './editors/_handleExecute';
|
||||
|
||||
|
||||
// what to say when a chat is new and has no title
|
||||
@@ -67,6 +65,19 @@ export interface AppChatIntent {
|
||||
}
|
||||
|
||||
|
||||
const composerOpenSx: SxProps = {
|
||||
zIndex: 21, // just to allocate a surface, and potentially have a shadow
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
borderTop: `1px solid`,
|
||||
borderTopColor: 'divider',
|
||||
p: { xs: 1, md: 2 },
|
||||
};
|
||||
|
||||
const composerClosedSx: SxProps = {
|
||||
display: 'none',
|
||||
};
|
||||
|
||||
|
||||
export function AppChat() {
|
||||
|
||||
// state
|
||||
@@ -90,7 +101,7 @@ export function AppChat() {
|
||||
|
||||
const showAltTitleBar = useUXLabsStore(state => DEV_MODE_SETTINGS && state.labsChatBarAlt === 'title');
|
||||
|
||||
const { openLlmOptions } = useOptimaLayout();
|
||||
const { openLlmOptions, openModelsSetup, openPreferencesTab } = useOptimaLayout();
|
||||
|
||||
const { chatLLM } = useChatLLM();
|
||||
|
||||
@@ -186,116 +197,20 @@ export function AppChat() {
|
||||
|
||||
// Execution
|
||||
|
||||
const _handleExecute = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]): Promise<void> => {
|
||||
const chatLLMId = getChatLLMId();
|
||||
if (!chatModeId || !conversationId || !chatLLMId) return;
|
||||
const handleExecuteAndOutcome = React.useCallback(async (chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) => {
|
||||
const outcome = await _handleExecute(chatModeId, conversationId, history);
|
||||
if (outcome === 'err-no-chatllm')
|
||||
openModelsSetup();
|
||||
else if (outcome === 'err-t2i-unconfigured')
|
||||
openPreferencesTab(PreferencesTab.Draw);
|
||||
else if (outcome === 'err-no-persona')
|
||||
addSnackbar({ key: 'chat-no-persona', message: 'No persona selected.', type: 'issue' });
|
||||
else if (outcome === 'err-no-conversation')
|
||||
addSnackbar({ key: 'chat-no-conversation', message: 'No active conversation.', type: 'issue' });
|
||||
return outcome === true;
|
||||
}, [openModelsSetup, openPreferencesTab]);
|
||||
|
||||
// Update the system message from the active persona to the history
|
||||
// NOTE: this does NOT call setMessages anymore (optimization). make sure to:
|
||||
// 1. all the callers need to pass a new array
|
||||
// 2. all the exit points need to call setMessages
|
||||
const cHandler = ConversationsManager.getHandler(conversationId);
|
||||
cHandler.inlineUpdatePurposeInHistory(history, chatLLMId);
|
||||
|
||||
// Valid /commands are intercepted here, and override chat modes, generally for mechanics or sidebars
|
||||
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
|
||||
if (lastMessage?.role === 'user') {
|
||||
const chatCommand = extractChatCommand(lastMessage.text)[0];
|
||||
if (chatCommand && chatCommand.type === 'cmd') {
|
||||
switch (chatCommand.providerId) {
|
||||
case 'ass-browse':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runBrowseGetPageUpdatingState(cHandler, chatCommand.params);
|
||||
|
||||
case 'ass-t2i':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runImageGenerationUpdatingState(cHandler, chatCommand.params);
|
||||
|
||||
case 'ass-react':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runReActUpdatingState(cHandler, chatCommand.params, chatLLMId);
|
||||
|
||||
case 'chat-alter':
|
||||
// /clear
|
||||
if (chatCommand.command === '/clear') {
|
||||
if (chatCommand.params === 'all')
|
||||
return cHandler.messagesReplace([]);
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Issue: this command requires the \'all\' parameter to confirm the operation.', undefined, 'issue', false);
|
||||
return;
|
||||
}
|
||||
// /assistant, /system
|
||||
Object.assign(lastMessage, {
|
||||
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
|
||||
sender: 'Bot',
|
||||
text: chatCommand.params || '',
|
||||
} satisfies Partial<DMessage>);
|
||||
return cHandler.messagesReplace(history);
|
||||
|
||||
case 'cmd-help':
|
||||
const chatCommandsText = findAllChatCommands()
|
||||
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
|
||||
.join('\n');
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Available Chat Commands:\n' + chatCommandsText, undefined, 'help', false);
|
||||
return;
|
||||
|
||||
case 'mode-beam':
|
||||
if (chatCommand.isError)
|
||||
return cHandler.messagesReplace(history);
|
||||
// remove '/beam ', as we want to be a user chat message
|
||||
Object.assign(lastMessage, { text: chatCommand.params || '' });
|
||||
cHandler.messagesReplace(history);
|
||||
return ConversationsManager.getHandler(conversationId).beamInvoke(history, [], null);
|
||||
|
||||
default:
|
||||
return cHandler.messagesReplace([...history, createDMessage('assistant', 'This command is not supported.')]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get the system purpose (note: we don't react to it, or it would invalidate half UI components..)
|
||||
if (!getConversationSystemPurposeId(conversationId)) {
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Issue: no Persona selected.', undefined, 'issue', false);
|
||||
return;
|
||||
}
|
||||
|
||||
// synchronous long-duration tasks, which update the state as they go
|
||||
switch (chatModeId) {
|
||||
case 'generate-text':
|
||||
cHandler.messagesReplace(history);
|
||||
return await runAssistantUpdatingState(conversationId, history, chatLLMId, getUXLabsHighPerformance() ? 0 : getInstantAppChatPanesCount());
|
||||
|
||||
case 'generate-text-beam':
|
||||
cHandler.messagesReplace(history);
|
||||
return cHandler.beamInvoke(history, [], null);
|
||||
|
||||
case 'append-user':
|
||||
return cHandler.messagesReplace(history);
|
||||
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text) break;
|
||||
// also add a 'fake' user message with the '/draw' command
|
||||
cHandler.messagesReplace(history.map(message => (message.id !== lastMessage.id) ? message : {
|
||||
...message,
|
||||
text: `/draw ${lastMessage.text}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(cHandler, lastMessage.text);
|
||||
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text) break;
|
||||
cHandler.messagesReplace(history);
|
||||
return await runReActUpdatingState(cHandler, lastMessage.text, chatLLMId);
|
||||
}
|
||||
|
||||
// ISSUE: if we're here, it means we couldn't do the job, at least sync the history
|
||||
console.log('Chat execute: issue running', chatModeId, conversationId, lastMessage);
|
||||
cHandler.messagesReplace(history);
|
||||
}, []);
|
||||
|
||||
const handleComposerAction = React.useCallback((chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart): boolean => {
|
||||
const handleComposerAction = React.useCallback((conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: ComposerOutputMultiPart, metadata?: DMessageMetadata): boolean => {
|
||||
// validate inputs
|
||||
if (multiPartMessage.length !== 1 || multiPartMessage[0].type !== 'text-block') {
|
||||
addSnackbar({
|
||||
@@ -311,35 +226,38 @@ export function AppChat() {
|
||||
const userText = multiPartMessage[0].text;
|
||||
|
||||
// multicast: send the message to all the panes
|
||||
const uniqueIds = new Set([conversationId]);
|
||||
const uniqueConversationIds = new Set([conversationId]);
|
||||
if (willMulticast)
|
||||
chatPanes.forEach(pane => pane.conversationId && uniqueIds.add(pane.conversationId));
|
||||
chatPanes.forEach(pane => pane.conversationId && uniqueConversationIds.add(pane.conversationId));
|
||||
|
||||
// we loop to handle both the normal and multicast modes
|
||||
let enqueued = false;
|
||||
for (const _cId of uniqueIds) {
|
||||
const _conversation = getConversation(_cId);
|
||||
if (_conversation) {
|
||||
// start execution fire/forget
|
||||
void _handleExecute(chatModeId, _cId, [..._conversation.messages, createDMessage('user', userText)]);
|
||||
enqueued = true;
|
||||
}
|
||||
}
|
||||
return enqueued;
|
||||
}, [chatPanes, willMulticast, _handleExecute]);
|
||||
let enqueuedAny = false;
|
||||
for (const _cId of uniqueConversationIds) {
|
||||
const history = getConversation(_cId)?.messages;
|
||||
if (!history) continue;
|
||||
|
||||
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[]): Promise<void> => {
|
||||
await _handleExecute('generate-text', conversationId, history);
|
||||
}, [_handleExecute]);
|
||||
const newUserMessage = createDMessage('user', userText);
|
||||
if (metadata) newUserMessage.metadata = metadata;
|
||||
|
||||
// fire/forget
|
||||
void handleExecuteAndOutcome(chatModeId, _cId, [...history, newUserMessage]);
|
||||
enqueuedAny = true;
|
||||
}
|
||||
return enqueuedAny;
|
||||
}, [chatPanes, handleExecuteAndOutcome, willMulticast]);
|
||||
|
||||
const handleConversationExecuteHistory = React.useCallback(async (conversationId: DConversationId, history: DMessage[]) => {
|
||||
await handleExecuteAndOutcome('generate-text', conversationId, history);
|
||||
}, [handleExecuteAndOutcome]);
|
||||
|
||||
const handleMessageRegenerateLastInFocusedPane = React.useCallback(async () => {
|
||||
const focusedConversation = getConversation(focusedPaneConversationId);
|
||||
if (focusedConversation?.messages?.length) {
|
||||
const lastMessage = focusedConversation.messages[focusedConversation.messages.length - 1];
|
||||
const history = lastMessage.role === 'assistant' ? focusedConversation.messages.slice(0, -1) : [...focusedConversation.messages];
|
||||
return await _handleExecute('generate-text', focusedConversation.id, history);
|
||||
await handleExecuteAndOutcome('generate-text', focusedConversation.id, history);
|
||||
}
|
||||
}, [_handleExecute, focusedPaneConversationId]);
|
||||
}, [focusedPaneConversationId, handleExecuteAndOutcome]);
|
||||
|
||||
const handleMessageBeamLastInFocusedPane = React.useCallback(async () => {
|
||||
// Ctrl + Shift + B
|
||||
@@ -355,16 +273,16 @@ export function AppChat() {
|
||||
|
||||
const handleTextDiagram = React.useCallback((diagramConfig: DiagramConfig | null) => setDiagramConfig(diagramConfig), []);
|
||||
|
||||
const handleTextImagine = React.useCallback(async (conversationId: DConversationId, messageText: string): Promise<void> => {
|
||||
const handleTextImagine = React.useCallback(async (conversationId: DConversationId, messageText: string) => {
|
||||
const conversation = getConversation(conversationId);
|
||||
if (!conversation)
|
||||
return;
|
||||
const imaginedPrompt = await imaginePromptFromText(messageText) || 'An error sign.';
|
||||
return await _handleExecute('generate-image', conversationId, [
|
||||
const imaginedPrompt = await imaginePromptFromText(messageText, conversationId) || 'An error sign.';
|
||||
await handleExecuteAndOutcome('generate-image', conversationId, [
|
||||
...conversation.messages,
|
||||
createDMessage('user', imaginedPrompt),
|
||||
]);
|
||||
}, [_handleExecute]);
|
||||
}, [handleExecuteAndOutcome]);
|
||||
|
||||
const handleTextSpeak = React.useCallback(async (text: string): Promise<void> => {
|
||||
await speakText(text);
|
||||
@@ -398,6 +316,32 @@ export function AppChat() {
|
||||
setTradeConfig({ dir: 'export', conversationId, exportAll });
|
||||
}, []);
|
||||
|
||||
const handleFileOpenConversation = React.useCallback(() => {
|
||||
openAndLoadConversations(true)
|
||||
.then((outcome) => {
|
||||
// activate the last (most recent) imported conversation
|
||||
if (outcome?.activateConversationId) {
|
||||
showNextTitleChange.current = true;
|
||||
handleOpenConversationInFocusedPane(outcome.activateConversationId);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
addSnackbar({ key: 'chat-import-fail', message: 'Could not open the file.', type: 'issue' });
|
||||
});
|
||||
}, [handleOpenConversationInFocusedPane]);
|
||||
|
||||
const handleFileSaveConversation = React.useCallback((conversationId: DConversationId | null) => {
|
||||
const conversation = getConversation(conversationId);
|
||||
conversation && downloadConversation(conversation, 'json')
|
||||
.then(() => {
|
||||
addSnackbar({ key: 'chat-save-as-ok', message: 'File saved.', type: 'success' });
|
||||
})
|
||||
.catch((err: any) => {
|
||||
if (err?.name !== 'AbortError')
|
||||
addSnackbar({ key: 'chat-save-as-fail', message: `Could not save the file. ${err?.message || ''}`, type: 'issue' });
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConversationBranch = React.useCallback((srcConversationId: DConversationId, messageId: string | null): DConversationId | null => {
|
||||
// clone data
|
||||
const branchedConversationId = branchConversation(srcConversationId, messageId);
|
||||
@@ -458,6 +402,8 @@ export function AppChat() {
|
||||
['b', true, true, false, handleMessageBeamLastInFocusedPane],
|
||||
['r', true, true, false, handleMessageRegenerateLastInFocusedPane],
|
||||
['n', true, false, true, handleConversationNewInFocusedPane],
|
||||
['o', true, false, false, handleFileOpenConversation],
|
||||
['s', true, false, false, () => handleFileSaveConversation(focusedPaneConversationId)],
|
||||
['b', true, false, true, () => isFocusedChatEmpty || (focusedPaneConversationId && handleConversationBranch(focusedPaneConversationId, null))],
|
||||
['x', true, false, true, () => isFocusedChatEmpty || (focusedPaneConversationId && handleConversationClear(focusedPaneConversationId))],
|
||||
['d', true, false, true, () => focusedPaneConversationId && handleDeleteConversations([focusedPaneConversationId], false)],
|
||||
@@ -467,7 +413,7 @@ export function AppChat() {
|
||||
['o', true, true, false, handleOpenChatLlmOptions],
|
||||
['+', true, true, false, useUIPreferencesStore.getState().increaseContentScaling],
|
||||
['-', true, true, false, useUIPreferencesStore.getState().decreaseContentScaling],
|
||||
], [focusedPaneConversationId, handleConversationBranch, handleConversationClear, handleConversationNewInFocusedPane, handleDeleteConversations, handleMessageBeamLastInFocusedPane, handleMessageRegenerateLastInFocusedPane, handleNavigateHistoryInFocusedPane, handleOpenChatLlmOptions, isFocusedChatEmpty]);
|
||||
], [focusedPaneConversationId, handleConversationBranch, handleConversationClear, handleConversationNewInFocusedPane, handleFileOpenConversation, handleFileSaveConversation, handleDeleteConversations, handleMessageBeamLastInFocusedPane, handleMessageRegenerateLastInFocusedPane, handleNavigateHistoryInFocusedPane, handleOpenChatLlmOptions, isFocusedChatEmpty]);
|
||||
useGlobalShortcuts(shortcuts);
|
||||
|
||||
|
||||
@@ -531,8 +477,8 @@ export function AppChat() {
|
||||
const _paneIsFocused = idx === focusedPaneIndex;
|
||||
const _paneConversationId = pane.conversationId;
|
||||
const _paneChatHandler = chatHandlers[idx] ?? null;
|
||||
const _paneChatBeamStore = beamsStores[idx] ?? null;
|
||||
const _paneChatBeamIsOpen = !!beamsOpens?.[idx];
|
||||
const _paneBeamStore = beamsStores[idx] ?? null;
|
||||
const _paneBeamIsOpen = !!beamsOpens?.[idx] && !!_paneBeamStore;
|
||||
const _panesCount = chatPanes.length;
|
||||
const _keyAndId = `chat-pane-${pane.paneId}`;
|
||||
const _sepId = `sep-pane-${idx}`;
|
||||
@@ -580,47 +526,45 @@ export function AppChat() {
|
||||
<ScrollToBottom
|
||||
bootToBottom
|
||||
stickToBottomInitial
|
||||
sx={_paneChatBeamIsOpen ? { display: 'none' } : undefined}
|
||||
sx={{ display: 'flex', flexDirection: 'column' }}
|
||||
>
|
||||
|
||||
<ChatMessageList
|
||||
conversationId={_paneConversationId}
|
||||
conversationHandler={_paneChatHandler}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
|
||||
fitScreen={isMobile || isMultiPane}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
sx={{
|
||||
minHeight: '100%', // ensures filling of the blank space on newer chats
|
||||
}}
|
||||
/>
|
||||
{!_paneBeamIsOpen && (
|
||||
<ChatMessageList
|
||||
conversationId={_paneConversationId}
|
||||
conversationHandler={_paneChatHandler}
|
||||
capabilityHasT2I={capabilityHasT2I}
|
||||
chatLLMContextTokens={chatLLM?.contextTokens ?? null}
|
||||
fitScreen={isMobile || isMultiPane}
|
||||
isMessageSelectionMode={isMessageSelectionMode}
|
||||
setIsMessageSelectionMode={setIsMessageSelectionMode}
|
||||
onConversationBranch={handleConversationBranch}
|
||||
onConversationExecuteHistory={handleConversationExecuteHistory}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/*<Ephemerals*/}
|
||||
{/* conversationId={_paneConversationId}*/}
|
||||
{/* sx={{*/}
|
||||
{/* // TODO: Fixme post panels?*/}
|
||||
{/* // flexGrow: 0.1,*/}
|
||||
{/* flexShrink: 0.5,*/}
|
||||
{/* overflowY: 'auto',*/}
|
||||
{/* minHeight: 64,*/}
|
||||
{/* }}*/}
|
||||
{/*/>*/}
|
||||
{_paneBeamIsOpen && (
|
||||
<ChatBeamWrapper
|
||||
beamStore={_paneBeamStore}
|
||||
isMobile={isMobile}
|
||||
inlineSx={{
|
||||
flexGrow: 1,
|
||||
// minHeight: 'calc(100vh - 69px - var(--AGI-Nav-width))',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Visibility and actions are handled via Context */}
|
||||
<ScrollToBottomButton />
|
||||
|
||||
</ScrollToBottom>
|
||||
|
||||
{(_paneChatBeamIsOpen && !!_paneChatBeamStore) && (
|
||||
<ChatBeamWrapper beamStore={_paneChatBeamStore} isMobile={isMobile} />
|
||||
)}
|
||||
|
||||
</Panel>
|
||||
|
||||
{/* Panel Separators & Resizers */}
|
||||
@@ -646,15 +590,7 @@ export function AppChat() {
|
||||
onAction={handleComposerAction}
|
||||
onTextImagine={handleTextImagine}
|
||||
setIsMulticast={setIsComposerMulticast}
|
||||
sx={beamOpenStoreInFocusedPane ? {
|
||||
display: 'none',
|
||||
} : {
|
||||
zIndex: 21, // just to allocate a surface, and potentially have a shadow
|
||||
backgroundColor: themeBgAppChatComposer,
|
||||
borderTop: `1px solid`,
|
||||
borderTopColor: 'divider',
|
||||
p: { xs: 1, md: 2 },
|
||||
}}
|
||||
sx={beamOpenStoreInFocusedPane ? composerClosedSx : composerOpenSx}
|
||||
/>
|
||||
|
||||
{/* Diagrams */}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import type { ICommandsProvider } from './ICommandsProvider';
|
||||
|
||||
@@ -7,11 +6,11 @@ export const CommandsBeam: ICommandsProvider = {
|
||||
id: 'mode-beam',
|
||||
rank: 9,
|
||||
|
||||
getCommands: () => useUXLabsStore.getState().labsBeam ? [{
|
||||
getCommands: () => [{
|
||||
primary: '/beam',
|
||||
arguments: ['prompt'],
|
||||
description: 'Combine the smarts of models',
|
||||
Icon: ChatBeamIcon,
|
||||
}] : [],
|
||||
}],
|
||||
|
||||
};
|
||||
|
||||
@@ -31,7 +31,7 @@ export function ChatBarAltBeam(props: {
|
||||
requiresConfirmation: store.isScattering || store.isGatheringAny || store.raysReady > 0,
|
||||
// actions
|
||||
setIsMaximized: store.setIsMaximized,
|
||||
terminateBeam: store.terminate,
|
||||
terminateBeam: store.terminateKeepingSettings,
|
||||
})));
|
||||
|
||||
|
||||
@@ -63,16 +63,7 @@ export function ChatBarAltBeam(props: {
|
||||
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', gap: { xs: 1, md: 3 }, alignItems: 'center' }}>
|
||||
|
||||
{/* [desktop] maximize button, or a disabled spacer */}
|
||||
{props.isMobile ? null : (
|
||||
<GoodTooltip title='Maximize'>
|
||||
<IconButton size='sm' onClick={handleMaximizeBeam}>
|
||||
<FullscreenRoundedIcon />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: { xs: 1, md: 2 }, alignItems: 'center' }}>
|
||||
|
||||
{/* Title & Status */}
|
||||
<Typography level='title-md'>
|
||||
@@ -89,11 +80,24 @@ export function ChatBarAltBeam(props: {
|
||||
</Typography>
|
||||
|
||||
{/* Right Close Icon */}
|
||||
<GoodTooltip usePlain title={<Box sx={{ p: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>Close Beam Mode <KeyStroke combo='Esc' /></Box>}>
|
||||
<IconButton aria-label='Close' size='sm' onClick={handleCloseBeam}>
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
|
||||
{/* [desktop] maximize button, or a disabled spacer */}
|
||||
{!props.isMobile && (
|
||||
<GoodTooltip usePlain title={<Box sx={{ p: 1 }}>Maximize</Box>}>
|
||||
<IconButton size='sm' onClick={handleMaximizeBeam}>
|
||||
<FullscreenRoundedIcon />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
)}
|
||||
|
||||
<GoodTooltip usePlain title={<Box sx={{ p: 1, display: 'flex', flexDirection: 'column', gap: 1 }}>Back to Chat <KeyStroke combo='Esc' /></Box>}>
|
||||
<IconButton aria-label='Close' size='sm' onClick={handleCloseBeam}>
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
</GoodTooltip>
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
|
||||
@@ -1,16 +1,25 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Modal, ModalClose } from '@mui/joy';
|
||||
|
||||
import { BeamStoreApi, useBeamStore } from '~/modules/beam/store-beam.hooks';
|
||||
import { BeamView } from '~/modules/beam/BeamView';
|
||||
|
||||
import { themeZIndexBeamView } from '~/common/app.theme';
|
||||
import { ScrollToBottom } from '~/common/scroll-to-bottom/ScrollToBottom';
|
||||
|
||||
|
||||
/*const overlaySx: SxProps = {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: themeZIndexBeamView, // stay on top of Message > Chips (:1), and Overlays (:2) - note: Desktop Drawer (:26)
|
||||
}*/
|
||||
|
||||
|
||||
export function ChatBeamWrapper(props: {
|
||||
beamStore: BeamStoreApi,
|
||||
isMobile: boolean,
|
||||
inlineSx?: SxProps,
|
||||
}) {
|
||||
|
||||
// state
|
||||
@@ -36,16 +45,14 @@ export function ChatBeamWrapper(props: {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
}}>
|
||||
{beamView}
|
||||
<ScrollToBottom disableAutoStick>
|
||||
{beamView}
|
||||
</ScrollToBottom>
|
||||
<ModalClose sx={{ color: 'white', backgroundColor: 'background.surface', boxShadow: 'xs', mr: 2 }} />
|
||||
</Box>
|
||||
</Modal>
|
||||
) : (
|
||||
<Box sx={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: themeZIndexBeamView, // stay on top of Message > Chips (:1), and Overlays (:2) - note: Desktop Drawer (:26)
|
||||
}}>
|
||||
<Box sx={props.inlineSx}>
|
||||
{beamView}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -277,7 +277,6 @@ function ChatDrawer(props: {
|
||||
<Button
|
||||
// variant='outlined'
|
||||
variant={disableNewButton ? undefined : 'soft'}
|
||||
color='primary'
|
||||
disabled={disableNewButton}
|
||||
onClick={handleButtonNew}
|
||||
sx={{
|
||||
@@ -285,16 +284,12 @@ function ChatDrawer(props: {
|
||||
justifyContent: 'flex-start',
|
||||
padding: '0px 0.75rem',
|
||||
|
||||
// text size
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'lg',
|
||||
|
||||
// style
|
||||
// backgroundColor: 'background.popup',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
borderRadius: 'sm',
|
||||
'--ListItemDecorator-size': 'calc(2.5rem - 1px)', // compensate for the border
|
||||
// backgroundColor: 'background.popup',
|
||||
// boxShadow: (disableNewButton || props.isMobile) ? 'none' : 'xs',
|
||||
// transition: 'box-shadow 0.2s',
|
||||
}}
|
||||
@@ -315,7 +310,7 @@ function ChatDrawer(props: {
|
||||
bottomBarBasis={filteredChatsBarBasis}
|
||||
onConversationActivate={handleConversationActivate}
|
||||
onConversationBranch={onConversationBranch}
|
||||
onConversationDelete={handleConversationDeleteNoConfirmation}
|
||||
onConversationDeleteNoConfirmation={handleConversationDeleteNoConfirmation}
|
||||
onConversationExport={onConversationsExportDialog}
|
||||
onConversationFolderChange={handleConversationFolderChange}
|
||||
/>
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ChatDrawerItemMemo = React.memo(ChatDrawerItem, (prev, next) =>
|
||||
prev.bottomBarBasis === next.bottomBarBasis &&
|
||||
prev.onConversationActivate === next.onConversationActivate &&
|
||||
prev.onConversationBranch === next.onConversationBranch &&
|
||||
prev.onConversationDelete === next.onConversationDelete &&
|
||||
prev.onConversationDeleteNoConfirmation === next.onConversationDeleteNoConfirmation &&
|
||||
prev.onConversationExport === next.onConversationExport &&
|
||||
prev.onConversationFolderChange === next.onConversationFolderChange,
|
||||
);
|
||||
@@ -76,7 +76,7 @@ function ChatDrawerItem(props: {
|
||||
bottomBarBasis: number,
|
||||
onConversationActivate: (conversationId: DConversationId, closeMenu: boolean) => void,
|
||||
onConversationBranch: (conversationId: DConversationId, messageId: string | null) => void,
|
||||
onConversationDelete: (conversationId: DConversationId) => void,
|
||||
onConversationDeleteNoConfirmation: (conversationId: DConversationId) => void,
|
||||
onConversationExport: (conversationId: DConversationId, exportAll: boolean) => void,
|
||||
onConversationFolderChange: (folderChangeRequest: FolderChangeRequest) => void,
|
||||
}) {
|
||||
@@ -155,7 +155,16 @@ function ChatDrawerItem(props: {
|
||||
|
||||
// Delete
|
||||
|
||||
const handleDeleteButtonShow = React.useCallback(() => setDeleteArmed(true), []);
|
||||
const { onConversationDeleteNoConfirmation } = props;
|
||||
const handleDeleteButtonShow = React.useCallback((event: React.MouseEvent) => {
|
||||
// special case: if 'Shift' is pressed, delete immediately
|
||||
if (event.shiftKey) {
|
||||
event.stopPropagation();
|
||||
onConversationDeleteNoConfirmation(conversationId);
|
||||
return;
|
||||
}
|
||||
setDeleteArmed(true);
|
||||
}, [conversationId, onConversationDeleteNoConfirmation]);
|
||||
|
||||
const handleDeleteButtonHide = React.useCallback(() => setDeleteArmed(false), []);
|
||||
|
||||
@@ -163,9 +172,9 @@ function ChatDrawerItem(props: {
|
||||
if (deleteArmed) {
|
||||
setDeleteArmed(false);
|
||||
event.stopPropagation();
|
||||
props.onConversationDelete(conversationId);
|
||||
onConversationDeleteNoConfirmation(conversationId);
|
||||
}
|
||||
}, [conversationId, deleteArmed, props]);
|
||||
}, [conversationId, deleteArmed, onConversationDeleteNoConfirmation]);
|
||||
|
||||
|
||||
const textSymbol = SystemPurposes[systemPurposeId]?.symbol || '❓';
|
||||
|
||||
@@ -136,6 +136,10 @@ export function ChatMessageList(props: {
|
||||
}), false);
|
||||
}, [conversationId, editMessage]);
|
||||
|
||||
const handleReplyTo = React.useCallback((_messageId: string, text: string) => {
|
||||
props.conversationHandler?.getOverlayStore().getState().setReplyToText(text);
|
||||
}, [props.conversationHandler]);
|
||||
|
||||
const handleTextDiagram = React.useCallback(async (messageId: string, text: string) => {
|
||||
conversationId && onTextDiagram({ conversationId: conversationId, messageId, text });
|
||||
}, [conversationId, onTextDiagram]);
|
||||
@@ -225,12 +229,15 @@ export function ChatMessageList(props: {
|
||||
|
||||
return (
|
||||
<List sx={{
|
||||
p: 0, ...(props.sx || {}),
|
||||
// this makes sure that the the window is scrolled to the bottom (column-reverse)
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
p: 0,
|
||||
...(props.sx || {}),
|
||||
|
||||
// fix for the double-border on the last message (one by the composer, one to the bottom of the message)
|
||||
// marginBottom: '-1px',
|
||||
|
||||
// layout
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
|
||||
{optionalTranslationWarning}
|
||||
@@ -276,9 +283,10 @@ export function ChatMessageList(props: {
|
||||
onMessageEdit={handleMessageEdit}
|
||||
onMessageToggleUserFlag={handleMessageToggleUserFlag}
|
||||
onMessageTruncate={handleMessageTruncate}
|
||||
// onReplyTo={handleReplyTo}
|
||||
onTextDiagram={handleTextDiagram}
|
||||
onTextImagine={handleTextImagine}
|
||||
onTextSpeak={handleTextSpeak}
|
||||
onTextImagine={capabilityHasT2I ? handleTextImagine : undefined}
|
||||
onTextSpeak={isSpeakable ? handleTextSpeak : undefined}
|
||||
/>
|
||||
|
||||
);
|
||||
|
||||
@@ -7,7 +7,6 @@ import { KeyStroke, platformAwareKeystrokes } from '~/common/components/KeyStrok
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { ChatModeId } from '../../AppChat';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
|
||||
interface ChatModeDescription {
|
||||
@@ -63,7 +62,6 @@ export function ChatModeMenu(props: {
|
||||
}) {
|
||||
|
||||
// external state
|
||||
const labsBeam = useUXLabsStore(state => state.labsBeam);
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
|
||||
return (
|
||||
@@ -81,7 +79,6 @@ export function ChatModeMenu(props: {
|
||||
|
||||
{/* ChatMode items */}
|
||||
{Object.entries(ChatModeItems)
|
||||
.filter(([key, _data]) => key !== 'generate-text-beam' || labsBeam)
|
||||
.filter(([_key, data]) => !data.hideOnDesktop || props.isMobile)
|
||||
.map(([key, data]) =>
|
||||
<MenuItem key={'chat-mode-' + key} onClick={() => props.onSetChatModeId(key as ChatModeId)}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { fileOpen, FileWithHandle } from 'browser-fs-access';
|
||||
|
||||
import { Box, Button, ButtonGroup, Card, Dropdown, Grid, IconButton, Menu, MenuButton, MenuItem, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
@@ -23,10 +23,11 @@ import type { LLMOptionsOpenAI } from '~/modules/llms/vendors/openai/openai.vend
|
||||
import { useBrowseCapability } from '~/modules/browse/store-module-browsing';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { PreferencesTab, useOptimaLayout } from '~/common/layout/optima/useOptimaLayout';
|
||||
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
|
||||
import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
import { conversationTitle, DConversationId, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { conversationTitle, DConversationId, DMessageMetadata, getConversation, useChatStore } from '~/common/state/store-chats';
|
||||
import { countModelTokens } from '~/common/util/token-counter';
|
||||
import { isMacUser } from '~/common/util/pwaUtils';
|
||||
import { launchAppCall } from '~/common/app.routes';
|
||||
@@ -36,6 +37,7 @@ import { playSoundUrl } from '~/common/util/audioUtils';
|
||||
import { supportsClipboardRead } from '~/common/util/clipboardUtils';
|
||||
import { supportsScreenCapture } from '~/common/util/screenCaptureUtils';
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
import { useChatOverlayStore } from '~/common/chats/store-chat-overlay-vanilla';
|
||||
import { useDebouncer } from '~/common/components/useDebouncer';
|
||||
import { useGlobalShortcut } from '~/common/components/useGlobalShortcut';
|
||||
import { useUICounter, useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
@@ -48,7 +50,7 @@ import { useActileManager } from './actile/useActileManager';
|
||||
|
||||
import type { AttachmentId } from './attachments/store-attachments';
|
||||
import { Attachments } from './attachments/Attachments';
|
||||
import { getTextBlockText, useLLMAttachments } from './attachments/useLLMAttachments';
|
||||
import { getSingleTextBlockText, useLLMAttachments } from './attachments/useLLMAttachments';
|
||||
import { useAttachments } from './attachments/useAttachments';
|
||||
|
||||
import type { ComposerOutputMultiPart } from './composer.types';
|
||||
@@ -63,6 +65,7 @@ import { ButtonMicMemo } from './buttons/ButtonMic';
|
||||
import { ButtonMultiChatMemo } from './buttons/ButtonMultiChat';
|
||||
import { ButtonOptionsDraw } from './buttons/ButtonOptionsDraw';
|
||||
import { ChatModeMenu } from './ChatModeMenu';
|
||||
import { ReplyToBubble } from '../message/ReplyToBubble';
|
||||
import { TokenBadgeMemo } from './TokenBadge';
|
||||
import { TokenProgressbarMemo } from './TokenProgressbar';
|
||||
import { useComposerStartupText } from './store-composer';
|
||||
@@ -98,7 +101,7 @@ export function Composer(props: {
|
||||
capabilityHasT2I: boolean;
|
||||
isMulticast: boolean | null;
|
||||
isDeveloperMode: boolean;
|
||||
onAction: (chatModeId: ChatModeId, conversationId: DConversationId, multiPartMessage: ComposerOutputMultiPart) => boolean;
|
||||
onAction: (conversationId: DConversationId, chatModeId: ChatModeId, multiPartMessage: ComposerOutputMultiPart, metadata?: DMessageMetadata) => boolean;
|
||||
onTextImagine: (conversationId: DConversationId, text: string) => void;
|
||||
setIsMulticast: (on: boolean) => void;
|
||||
sx?: SxProps;
|
||||
@@ -114,11 +117,11 @@ export function Composer(props: {
|
||||
|
||||
// external state
|
||||
const { openPreferencesTab /*, setIsFocusedMode*/ } = useOptimaLayout();
|
||||
const { labsAttachScreenCapture, labsBeam, labsCameraDesktop } = useUXLabsStore(state => ({
|
||||
const { labsAttachScreenCapture, labsCameraDesktop, labsShowCost } = useUXLabsStore(useShallow(state => ({
|
||||
labsAttachScreenCapture: state.labsAttachScreenCapture,
|
||||
labsBeam: state.labsBeam,
|
||||
labsCameraDesktop: state.labsCameraDesktop,
|
||||
}), shallow);
|
||||
labsShowCost: state.labsShowCost,
|
||||
})));
|
||||
const timeToShowTips = useAppStateStore(state => state.usageCount > 2);
|
||||
const { novel: explainShiftEnter, touch: touchShiftEnter } = useUICounter('composer-shift-enter');
|
||||
const { novel: explainAltEnter, touch: touchAltEnter } = useUICounter('composer-alt-enter');
|
||||
@@ -126,7 +129,7 @@ export function Composer(props: {
|
||||
const [startupText, setStartupText] = useComposerStartupText();
|
||||
const enterIsNewline = useUIPreferencesStore(state => state.enterIsNewline);
|
||||
const chatMicTimeoutMs = useChatMicTimeoutMsValue();
|
||||
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(state => {
|
||||
const { assistantAbortible, systemPurposeId, tokenCount: _historyTokenCount, stopTyping } = useChatStore(useShallow(state => {
|
||||
const conversation = state.conversations.find(_c => _c.id === props.conversationId);
|
||||
return {
|
||||
assistantAbortible: conversation ? !!conversation.abortController : false,
|
||||
@@ -134,11 +137,18 @@ export function Composer(props: {
|
||||
tokenCount: conversation ? conversation.tokenCount : 0,
|
||||
stopTyping: state.stopTyping,
|
||||
};
|
||||
}, shallow);
|
||||
}));
|
||||
const { inComposer: browsingInComposer } = useBrowseCapability();
|
||||
const { attachAppendClipboardItems, attachAppendDataTransfer, attachAppendEgoMessage, attachAppendFile, attachments: _attachments, clearAttachments, removeAttachment } =
|
||||
useAttachments(browsingInComposer && !composeText.startsWith('/'));
|
||||
|
||||
// external overlay state (extra conversationId-dependent state)
|
||||
const conversationHandler = props.conversationId ? ConversationsManager.getHandler(props.conversationId) : null;
|
||||
const conversationOverlayStore = conversationHandler?.getOverlayStore() ?? null;
|
||||
const { replyToGenerateText } = useChatOverlayStore(conversationOverlayStore, useShallow(store => ({
|
||||
replyToGenerateText: chatModeId === 'generate-text' ? store.replyToText?.trim() || null : null,
|
||||
})));
|
||||
|
||||
|
||||
// derived state
|
||||
|
||||
@@ -163,6 +173,8 @@ export function Composer(props: {
|
||||
const tokensHistory = _historyTokenCount;
|
||||
const tokensReponseMax = (props.chatLLM?.options as LLMOptionsOpenAI /* FIXME: BIG ASSUMPTION */)?.llmResponseTokens || 0;
|
||||
const tokenLimit = props.chatLLM?.contextTokens || 0;
|
||||
const tokenPriceIn = props.chatLLM?.pricing?.chatIn;
|
||||
const tokenPriceOut = props.chatLLM?.pricing?.chatOut;
|
||||
|
||||
|
||||
// Effect: load initial text if queued up (e.g. by /link/share_targe)
|
||||
@@ -174,6 +186,18 @@ export function Composer(props: {
|
||||
}, [setComposeText, setStartupText, startupText]);
|
||||
|
||||
|
||||
// Overlay actions
|
||||
|
||||
const handleReplyToCleared = React.useCallback(() => {
|
||||
conversationOverlayStore?.getState().setReplyToText(null);
|
||||
}, [conversationOverlayStore]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (replyToGenerateText)
|
||||
setTimeout(() => props.composerTextAreaRef.current?.focus(), 1 /* prevent focus theft */);
|
||||
}, [replyToGenerateText, props.composerTextAreaRef]);
|
||||
|
||||
|
||||
// Primary button
|
||||
|
||||
const { conversationId, onAction } = props;
|
||||
@@ -182,28 +206,32 @@ export function Composer(props: {
|
||||
if (!conversationId)
|
||||
return false;
|
||||
|
||||
// get attachments
|
||||
const multiPartMessage = llmAttachments.getAttachmentsOutputs(composerText || null);
|
||||
// get the multipart output including all attachments
|
||||
const multiPartMessage = llmAttachments.collapseWithAttachments(composerText || null);
|
||||
if (!multiPartMessage.length)
|
||||
return false;
|
||||
|
||||
// metadata
|
||||
const metadata = replyToGenerateText ? { inReplyToText: replyToGenerateText } : undefined;
|
||||
|
||||
// send the message
|
||||
const enqueued = onAction(_chatModeId, conversationId, multiPartMessage);
|
||||
const enqueued = onAction(conversationId, _chatModeId, multiPartMessage, metadata);
|
||||
if (enqueued) {
|
||||
clearAttachments();
|
||||
handleReplyToCleared();
|
||||
setComposeText('');
|
||||
}
|
||||
|
||||
return enqueued;
|
||||
}, [clearAttachments, conversationId, llmAttachments, onAction, setComposeText]);
|
||||
}, [clearAttachments, conversationId, handleReplyToCleared, llmAttachments, onAction, replyToGenerateText, setComposeText]);
|
||||
|
||||
const handleSendClicked = React.useCallback(() => {
|
||||
handleSendAction(chatModeId, composeText);
|
||||
}, [chatModeId, composeText, handleSendAction]);
|
||||
|
||||
const handleSendTextBeamClicked = React.useCallback(() => {
|
||||
labsBeam && handleSendAction('generate-text-beam', composeText);
|
||||
}, [composeText, handleSendAction, labsBeam]);
|
||||
handleSendAction('generate-text-beam', composeText);
|
||||
}, [composeText, handleSendAction]);
|
||||
|
||||
const handleStopClicked = React.useCallback(() => {
|
||||
!!props.conversationId && stopTyping(props.conversationId);
|
||||
@@ -304,15 +332,15 @@ export function Composer(props: {
|
||||
|
||||
// Alt (Windows) or Option (Mac) + Enter: append the message instead of sending it
|
||||
if (e.altKey) {
|
||||
touchAltEnter();
|
||||
handleSendAction('append-user', composeText);
|
||||
if (handleSendAction('append-user', composeText))
|
||||
touchAltEnter();
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
// Ctrl (Windows) or Command (Mac) + Enter: send for beaming
|
||||
if (labsBeam && ((isMacUser && e.metaKey && !e.ctrlKey) || (!isMacUser && e.ctrlKey && !e.metaKey))) {
|
||||
touchCtrlEnter();
|
||||
handleSendAction('generate-text-beam', composeText);
|
||||
if ((isMacUser && e.metaKey && !e.ctrlKey) || (!isMacUser && e.ctrlKey && !e.metaKey)) {
|
||||
if (handleSendAction('generate-text-beam', composeText))
|
||||
touchCtrlEnter();
|
||||
return e.preventDefault();
|
||||
}
|
||||
|
||||
@@ -326,7 +354,7 @@ export function Composer(props: {
|
||||
}
|
||||
}
|
||||
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, labsBeam, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
|
||||
}, [actileInterceptKeydown, assistantAbortible, chatModeId, composeText, enterIsNewline, handleSendAction, touchAltEnter, touchCtrlEnter, touchShiftEnter]);
|
||||
|
||||
|
||||
// Focus mode
|
||||
@@ -427,8 +455,8 @@ export function Composer(props: {
|
||||
|
||||
const handleAttachmentInlineText = React.useCallback((attachmentId: AttachmentId) => {
|
||||
setComposeText(currentText => {
|
||||
const attachmentOutputs = llmAttachments.getAttachmentOutputs(currentText, attachmentId);
|
||||
const inlinedText = getTextBlockText(attachmentOutputs) || '';
|
||||
const inlinedMultiPart = llmAttachments.collapseWithAttachment(currentText, attachmentId);
|
||||
const inlinedText = getSingleTextBlockText(inlinedMultiPart) || '';
|
||||
removeAttachment(attachmentId);
|
||||
return inlinedText;
|
||||
});
|
||||
@@ -436,8 +464,8 @@ export function Composer(props: {
|
||||
|
||||
const handleAttachmentsInlineText = React.useCallback(() => {
|
||||
setComposeText(currentText => {
|
||||
const attachmentsOutputs = llmAttachments.getAttachmentsOutputs(currentText);
|
||||
const inlinedText = getTextBlockText(attachmentsOutputs) || '';
|
||||
const inlinedMultiPart = llmAttachments.collapseWithAttachments(currentText);
|
||||
const inlinedText = getSingleTextBlockText(inlinedMultiPart) || '';
|
||||
clearAttachments();
|
||||
return inlinedText;
|
||||
});
|
||||
@@ -495,7 +523,8 @@ export function Composer(props: {
|
||||
const isReAct = chatModeId === 'generate-react';
|
||||
const isDraw = chatModeId === 'generate-image';
|
||||
|
||||
const showChatExtras = isText;
|
||||
const showChatReplyTo = !!replyToGenerateText;
|
||||
const showChatExtras = isText && !showChatReplyTo;
|
||||
|
||||
const buttonVariant: VariantProp = (isAppend || (isMobile && isTextBeam)) ? 'outlined' : 'solid';
|
||||
|
||||
@@ -525,15 +554,16 @@ export function Composer(props: {
|
||||
isDraw ? 'Describe an idea or a drawing...'
|
||||
: isReAct ? 'Multi-step reasoning question...'
|
||||
: isTextBeam ? 'Beam: combine the smarts of models...'
|
||||
: props.isDeveloperMode ? 'Chat with me' + (isDesktop ? ' · drop source' : '') + ' · attach code...'
|
||||
: props.capabilityHasT2I ? 'Chat · /beam · /draw · drop files...'
|
||||
: 'Chat · /react · drop files...';
|
||||
: showChatReplyTo ? 'Chat about this'
|
||||
: props.isDeveloperMode ? 'Chat with me' + (isDesktop ? ' · drop source' : '') + ' · attach code...'
|
||||
: props.capabilityHasT2I ? 'Chat · /beam · /draw · drop files...'
|
||||
: 'Chat · /react · drop files...';
|
||||
if (isDesktop && timeToShowTips) {
|
||||
if (explainShiftEnter)
|
||||
textPlaceholder += !enterIsNewline ? '\n\n💡 Shift + Enter to add a new line' : '\n\n💡 Shift + Enter to send';
|
||||
else if (explainAltEnter)
|
||||
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Alt + Enter to just append the message');
|
||||
else if (labsBeam && explainCtrlEnter)
|
||||
else if (explainCtrlEnter)
|
||||
textPlaceholder += platformAwareKeystrokes('\n\n💡 Tip: Ctrl + Enter to beam');
|
||||
}
|
||||
|
||||
@@ -618,7 +648,7 @@ export function Composer(props: {
|
||||
variant='outlined'
|
||||
color={isDraw ? 'warning' : isReAct ? 'success' : undefined}
|
||||
autoFocus
|
||||
minRows={isMobile ? 4 : 5}
|
||||
minRows={isMobile ? 4 : showChatReplyTo ? 4 : 5}
|
||||
maxRows={isMobile ? 8 : 10}
|
||||
placeholder={textPlaceholder}
|
||||
value={composeText}
|
||||
@@ -629,6 +659,7 @@ export function Composer(props: {
|
||||
onPasteCapture={handleAttachCtrlV}
|
||||
// onFocusCapture={handleFocusModeOn}
|
||||
// onBlurCapture={handleFocusModeOff}
|
||||
endDecorator={showChatReplyTo && <ReplyToBubble replyToText={replyToGenerateText} onClear={handleReplyToCleared} className='reply-to-bubble' />}
|
||||
slotProps={{
|
||||
textarea: {
|
||||
enterKeyHint: enterIsNewline ? 'enter' : 'send',
|
||||
@@ -641,16 +672,16 @@ export function Composer(props: {
|
||||
}}
|
||||
sx={{
|
||||
backgroundColor: 'background.level1',
|
||||
'&:focus-within': { backgroundColor: 'background.popup' },
|
||||
'&:focus-within': { backgroundColor: 'background.popup', '.reply-to-bubble': { backgroundColor: 'background.popup' } },
|
||||
lineHeight: lineHeightTextareaMd,
|
||||
}} />
|
||||
|
||||
{tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
|
||||
<TokenProgressbarMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} />
|
||||
{!showChatReplyTo && tokenLimit > 0 && (tokensComposer > 0 || (tokensHistory + tokensReponseMax) > 0) && (
|
||||
<TokenProgressbarMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} tokenPriceIn={tokenPriceIn} tokenPriceOut={tokenPriceOut} />
|
||||
)}
|
||||
|
||||
{!!tokenLimit && (
|
||||
<TokenBadgeMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} showExcess absoluteBottomRight />
|
||||
{!showChatReplyTo && tokenLimit > 0 && (
|
||||
<TokenBadgeMemo direct={tokensComposer} history={tokensHistory} responseMax={tokensReponseMax} limit={tokenLimit} tokenPriceIn={tokenPriceIn} tokenPriceOut={tokenPriceOut} showCost={labsShowCost} showExcess absoluteBottomRight />
|
||||
)}
|
||||
|
||||
</Box>
|
||||
@@ -678,20 +709,32 @@ export function Composer(props: {
|
||||
{/* overlay: Mic */}
|
||||
{micIsRunning && (
|
||||
<Card
|
||||
color='primary' variant='soft' invertedColors
|
||||
color='primary' variant='soft'
|
||||
sx={{
|
||||
display: 'flex',
|
||||
position: 'absolute', bottom: 0, left: 0, right: 0, top: 0,
|
||||
// alignItems: 'center', justifyContent: 'center',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.solidBg',
|
||||
borderRadius: 'sm',
|
||||
zIndex: zIndexComposerOverlayMic,
|
||||
px: 1.5, py: 1,
|
||||
pl: 1.5,
|
||||
pr: { xs: 1.5, md: 5 },
|
||||
py: 0.625,
|
||||
overflow: 'auto',
|
||||
}}>
|
||||
<Typography sx={{
|
||||
color: 'primary.softColor',
|
||||
lineHeight: lineHeightTextareaMd,
|
||||
'& .interim': {
|
||||
textDecoration: 'underline',
|
||||
textDecorationThickness: '0.25em',
|
||||
textDecorationColor: 'rgba(var(--joy-palette-primary-mainChannel) / 0.1)',
|
||||
textDecorationSkipInk: 'none',
|
||||
textUnderlineOffset: '0.25em',
|
||||
},
|
||||
}}>
|
||||
<Typography>
|
||||
{speechInterimResult.transcript}{' '}
|
||||
<span style={{ opacity: 0.8 }}>{speechInterimResult.interimTranscript}</span>
|
||||
<span className={speechInterimResult.interimTranscript !== 'Listening...' ? 'interim' : undefined}>{speechInterimResult.interimTranscript}</span>
|
||||
</Typography>
|
||||
</Card>
|
||||
)}
|
||||
@@ -799,9 +842,10 @@ export function Composer(props: {
|
||||
</ButtonGroup>
|
||||
|
||||
{/* [desktop] secondary-top buttons */}
|
||||
{labsBeam && isDesktop && showChatExtras && !assistantAbortible && (
|
||||
{isDesktop && showChatExtras && !assistantAbortible && (
|
||||
<ButtonBeamMemo
|
||||
disabled={!props.conversationId || !chatLLMId || !llmAttachments.isOutputAttacheable}
|
||||
hasContent={!!composeText}
|
||||
onClick={handleSendTextBeamClicked}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -3,41 +3,81 @@ import * as React from 'react';
|
||||
import { Badge, Box, ColorPaletteProp, Tooltip } from '@mui/joy';
|
||||
|
||||
|
||||
function alignRight(value: number, columnSize: number = 7) {
|
||||
function alignRight(value: number, columnSize: number = 8) {
|
||||
const str = value.toLocaleString();
|
||||
return str.padStart(columnSize);
|
||||
}
|
||||
|
||||
function formatCost(cost: number) {
|
||||
return cost < 1
|
||||
? (cost * 100).toFixed(cost < 0.010 ? 2 : 1) + ' ¢'
|
||||
: '$ ' + cost.toFixed(2);
|
||||
}
|
||||
|
||||
export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, historyTokens?: number, responseMaxTokens?: number): {
|
||||
color: ColorPaletteProp, message: string, remainingTokens: number
|
||||
|
||||
export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, historyTokens?: number, responseMaxTokens?: number, tokenPriceIn?: number, tokenPriceOut?: number): {
|
||||
color: ColorPaletteProp,
|
||||
message: string,
|
||||
remainingTokens: number,
|
||||
costMax?: number,
|
||||
costMin?: number,
|
||||
} {
|
||||
const usedTokens = directTokens + (historyTokens || 0) + (responseMaxTokens || 0);
|
||||
const remainingTokens = tokenLimit - usedTokens;
|
||||
const usedInputTokens = directTokens + (historyTokens || 0);
|
||||
const usedMaxTokens = usedInputTokens + (responseMaxTokens || 0);
|
||||
const remainingTokens = tokenLimit - usedMaxTokens;
|
||||
const gteLimit = (remainingTokens <= 0 && tokenLimit > 0);
|
||||
|
||||
// message
|
||||
let message: string = gteLimit ? '⚠️ ' : '';
|
||||
|
||||
// costs
|
||||
let costMax: number | undefined = undefined;
|
||||
let costMin: number | undefined = undefined;
|
||||
|
||||
// no limit: show used tokens only
|
||||
if (!tokenLimit) {
|
||||
message += `Requested: ${usedTokens.toLocaleString()} tokens`;
|
||||
message += `Requested: ${usedMaxTokens.toLocaleString()} tokens`;
|
||||
}
|
||||
// has full information (d + i < l)
|
||||
else if (historyTokens || responseMaxTokens) {
|
||||
message +=
|
||||
`${Math.abs(remainingTokens).toLocaleString()} ${remainingTokens >= 0 ? 'available' : 'excess'} message tokens\n\n` +
|
||||
`▶ ${Math.abs(remainingTokens).toLocaleString()} ${remainingTokens >= 0 ? 'available' : 'excess'} message tokens\n\n` +
|
||||
` = Model max tokens: ${alignRight(tokenLimit)}\n` +
|
||||
` - This message: ${alignRight(directTokens)}\n` +
|
||||
` - History: ${alignRight(historyTokens || 0)}\n` +
|
||||
` - Max response: ${alignRight(responseMaxTokens || 0)}`;
|
||||
|
||||
// add the price, if available
|
||||
if (tokenPriceIn || tokenPriceOut) {
|
||||
costMin = tokenPriceIn ? usedInputTokens * tokenPriceIn / 1E6 : undefined;
|
||||
const costOutMax = (tokenPriceOut && responseMaxTokens) ? responseMaxTokens * tokenPriceOut / 1E6 : undefined;
|
||||
if (costMin || costOutMax) {
|
||||
message += `\n\n\n▶ Chat Turn Cost (max, approximate)\n`;
|
||||
|
||||
if (costMin) message += '\n' +
|
||||
` Input tokens: ${alignRight(usedInputTokens)}\n` +
|
||||
` Input Price $/M: ${tokenPriceIn!.toFixed(2).padStart(8)}\n` +
|
||||
` Input cost: ${('$' + costMin!.toFixed(4)).padStart(8)}\n`;
|
||||
|
||||
if (costOutMax) message += '\n' +
|
||||
` Max output tokens: ${alignRight(responseMaxTokens!)}\n` +
|
||||
` Output Price $/M: ${tokenPriceOut!.toFixed(2).padStart(8)}\n` +
|
||||
` Max output cost: ${('$' + costOutMax!.toFixed(4)).padStart(8)}\n`;
|
||||
|
||||
if (costMin) message += '\n' +
|
||||
` > Min turn cost: ${formatCost(costMin).padStart(8)}`;
|
||||
costMax = (costMin && costOutMax) ? costMin + costOutMax : undefined;
|
||||
if (costMax) message += '\n' +
|
||||
` < Max turn cost: ${formatCost(costMax).padStart(8)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Cleaner mode: d + ? < R (total is the remaining in this case)
|
||||
else {
|
||||
message +=
|
||||
`${(tokenLimit + usedTokens).toLocaleString()} available tokens after deleting this\n\n` +
|
||||
`${(tokenLimit + usedMaxTokens).toLocaleString()} available tokens after deleting this\n\n` +
|
||||
` = Currently free: ${alignRight(tokenLimit)}\n` +
|
||||
` + This message: ${alignRight(usedTokens)}`;
|
||||
` + This message: ${alignRight(usedMaxTokens)}`;
|
||||
}
|
||||
|
||||
const color: ColorPaletteProp =
|
||||
@@ -47,23 +87,21 @@ export function tokensPrettyMath(tokenLimit: number | 0, directTokens: number, h
|
||||
? 'warning'
|
||||
: 'primary';
|
||||
|
||||
return { color, message, remainingTokens };
|
||||
return { color, message, remainingTokens, costMax, costMin };
|
||||
}
|
||||
|
||||
|
||||
export const TokenTooltip = (props: { message: string | null, color: ColorPaletteProp, placement?: 'top' | 'top-end', children: React.JSX.Element }) =>
|
||||
export const TokenTooltip = (props: { message: string | null, color: ColorPaletteProp, placement?: 'top' | 'top-end', children: React.ReactElement }) =>
|
||||
<Tooltip
|
||||
placement={props.placement}
|
||||
variant={props.color !== 'primary' ? 'solid' : 'soft'} color={props.color}
|
||||
title={props.message
|
||||
? <Box sx={{ p: 2, whiteSpace: 'pre' }}>
|
||||
{props.message}
|
||||
</Box>
|
||||
: null
|
||||
}
|
||||
title={props.message ? <Box sx={{ p: 2, whiteSpace: 'pre' }}>{props.message}</Box> : null}
|
||||
sx={{
|
||||
fontFamily: 'code',
|
||||
boxShadow: 'xl',
|
||||
// fontSize: '0.8125rem',
|
||||
border: '1px solid',
|
||||
borderColor: `${props.color}.outlinedColor`,
|
||||
boxShadow: 'md',
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
@@ -76,38 +114,65 @@ export const TokenTooltip = (props: { message: string | null, color: ColorPalett
|
||||
export const TokenBadgeMemo = React.memo(TokenBadge);
|
||||
|
||||
function TokenBadge(props: {
|
||||
direct: number, history?: number, responseMax?: number, limit: number,
|
||||
showExcess?: boolean, absoluteBottomRight?: boolean, inline?: boolean,
|
||||
direct: number,
|
||||
history?: number,
|
||||
responseMax?: number,
|
||||
limit: number,
|
||||
|
||||
tokenPriceIn?: number,
|
||||
tokenPriceOut?: number,
|
||||
|
||||
showCost?: boolean
|
||||
showExcess?: boolean,
|
||||
absoluteBottomRight?: boolean,
|
||||
inline?: boolean,
|
||||
}) {
|
||||
|
||||
const { message, color, remainingTokens } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax);
|
||||
const { message, color, remainingTokens, costMax, costMin } =
|
||||
tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut);
|
||||
|
||||
// show the direct tokens, unless we exceed the limit and 'showExcess' is enabled
|
||||
const value = (props.showExcess && (props.limit && remainingTokens <= 0))
|
||||
? Math.abs(remainingTokens)
|
||||
: props.direct;
|
||||
let badgeValue: string;
|
||||
|
||||
const showAltCosts = !!props.showCost && !!costMax && costMin !== undefined;
|
||||
if (showAltCosts) {
|
||||
badgeValue = '< ' + formatCost(costMax);
|
||||
} else {
|
||||
|
||||
// show the direct tokens, unless we exceed the limit and 'showExcess' is enabled
|
||||
const value = (props.showExcess && (props.limit && remainingTokens <= 0))
|
||||
? Math.abs(remainingTokens)
|
||||
: props.direct;
|
||||
|
||||
badgeValue = value.toLocaleString();
|
||||
}
|
||||
|
||||
const shallHide = !props.direct && remainingTokens >= 0 && !showAltCosts;
|
||||
if (shallHide) return null;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant='solid' color={color} max={100000}
|
||||
invisible={!props.direct && remainingTokens >= 0}
|
||||
badgeContent={
|
||||
<TokenTooltip color={color} message={message}>
|
||||
<span>{value.toLocaleString()}</span>
|
||||
</TokenTooltip>
|
||||
}
|
||||
sx={{
|
||||
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: 8 }),
|
||||
cursor: 'help',
|
||||
}}
|
||||
slotProps={{
|
||||
badge: {
|
||||
sx: {
|
||||
fontFamily: 'code',
|
||||
...((props.absoluteBottomRight || props.inline) && { position: 'static', transform: 'none' }),
|
||||
<TokenTooltip color={color} message={message} placement='top-end'>
|
||||
<Badge
|
||||
variant='soft' color={color} max={1000000}
|
||||
// invisible={shallHide}
|
||||
badgeContent={badgeValue}
|
||||
slotProps={{
|
||||
root: {
|
||||
sx: {
|
||||
...((props.absoluteBottomRight) && { position: 'absolute', bottom: 8, right: 8 }),
|
||||
cursor: 'help',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
badge: {
|
||||
sx: {
|
||||
// the badge (not the tooltip)
|
||||
// boxShadow: 'sm',
|
||||
fontFamily: 'code',
|
||||
fontSize: 'xs',
|
||||
...((props.absoluteBottomRight || props.inline) && { position: 'static', transform: 'none' }),
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</TokenTooltip>
|
||||
);
|
||||
}
|
||||
@@ -12,7 +12,15 @@ import { tokensPrettyMath, TokenTooltip } from './TokenBadge';
|
||||
*/
|
||||
export const TokenProgressbarMemo = React.memo(TokenProgressbar);
|
||||
|
||||
function TokenProgressbar(props: { direct: number, history: number, responseMax: number, limit: number }) {
|
||||
function TokenProgressbar(props: {
|
||||
direct: number,
|
||||
history: number,
|
||||
responseMax: number,
|
||||
limit: number,
|
||||
|
||||
tokenPriceIn?: number,
|
||||
tokenPriceOut?: number,
|
||||
}) {
|
||||
// external state
|
||||
const theme = useTheme();
|
||||
|
||||
@@ -40,7 +48,7 @@ function TokenProgressbar(props: { direct: number, history: number, responseMax:
|
||||
const overflowColor = theme.palette.danger.softColor;
|
||||
|
||||
// tooltip message/color
|
||||
const { message, color } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax);
|
||||
const { message, color } = tokensPrettyMath(props.limit, props.direct, props.history, props.responseMax, props.tokenPriceIn, props.tokenPriceOut);
|
||||
|
||||
// sizes
|
||||
const containerHeight = 8;
|
||||
|
||||
@@ -153,7 +153,11 @@ export function AttachmentMenu(props: {
|
||||
{/* Converters: {aConverters.map(((converter, idx) => ` ${converter.id}${(idx === aConverterIdx) ? '*' : ''}`)).join(', ')}*/}
|
||||
{/*</Typography>*/}
|
||||
<Typography level='body-xs'>
|
||||
🡒 {isOutputMissing ? 'empty' : aOutputs.map(output => `${output.type}, ${output.type === 'text-block' ? output.text.length.toLocaleString() : '(base64 image)'} bytes`).join(' · ')}
|
||||
🡒 {isOutputMissing ? 'empty' : aOutputs.map(output => `${output.type}, ${output.type === 'text-block'
|
||||
? output.text.length.toLocaleString()
|
||||
: output.type === 'image-part'
|
||||
? output.base64Url.length.toLocaleString()
|
||||
: '(other)'} bytes`).join(' · ')}
|
||||
</Typography>
|
||||
{!!tokenCountApprox && <Typography level='body-xs'>
|
||||
🡒 {tokenCountApprox.toLocaleString()} tokens
|
||||
|
||||
@@ -153,7 +153,7 @@ export function Attachments(props: {
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleClearAttachments}>
|
||||
<ListItemDecorator><ClearIcon /></ListItemDecorator>
|
||||
Clear
|
||||
Clear{attachments.length > 5 ? <span style={{ opacity: 0.5 }}> {attachments.length} attachments</span> : null}
|
||||
</MenuItem>
|
||||
</CloseableMenu>
|
||||
)}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { callBrowseFetchPage } from '~/modules/browse/browse.client';
|
||||
|
||||
import { createBase36Uid } from '~/common/util/textUtils';
|
||||
import { htmlTableToMarkdown } from '~/common/util/htmlTableToMarkdown';
|
||||
import { pdfToText } from '~/common/util/pdfUtils';
|
||||
import { pdfToImageDataURLs, pdfToText } from '~/common/util/pdfUtils';
|
||||
|
||||
import type { Attachment, AttachmentConverter, AttachmentId, AttachmentInput, AttachmentSource } from './store-attachments';
|
||||
import type { ComposerOutputMultiPart } from '../composer.types';
|
||||
@@ -58,16 +58,12 @@ export async function attachmentLoadInputAsync(source: Readonly<AttachmentSource
|
||||
edit({ label: source.refUrl, ref: source.refUrl });
|
||||
try {
|
||||
const page = await callBrowseFetchPage(source.url);
|
||||
if (page.content) {
|
||||
edit({
|
||||
input: {
|
||||
mimeType: 'text/plain',
|
||||
data: page.content,
|
||||
dataSize: page.content.length,
|
||||
},
|
||||
});
|
||||
} else
|
||||
edit({ inputError: 'No content found at this link' });
|
||||
edit(
|
||||
page.content.markdown ? { input: { mimeType: 'text/markdown', data: page.content.markdown, dataSize: page.content.markdown.length } }
|
||||
: page.content.text ? { input: { mimeType: 'text/plain', data: page.content.text, dataSize: page.content.text.length } }
|
||||
: page.content.html ? { input: { mimeType: 'text/html', data: page.content.html, dataSize: page.content.html.length } }
|
||||
: { inputError: 'No content found at this link' },
|
||||
);
|
||||
} catch (error: any) {
|
||||
edit({ inputError: `Issue downloading page: ${error?.message || (typeof error === 'string' ? error : JSON.stringify(error))}` });
|
||||
}
|
||||
@@ -297,7 +293,7 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
|
||||
|
||||
case 'pdf-text':
|
||||
if (!(input.data instanceof ArrayBuffer)) {
|
||||
console.log('Expected ArrayBuffer for PDF converter, got:', typeof input.data);
|
||||
console.log('Expected ArrayBuffer for PDF text converter, got:', typeof input.data);
|
||||
break;
|
||||
}
|
||||
// duplicate the ArrayBuffer to avoid mutation
|
||||
@@ -312,7 +308,29 @@ export async function attachmentPerformConversion(attachment: Readonly<Attachmen
|
||||
break;
|
||||
|
||||
case 'pdf-images':
|
||||
// TODO: extract all pages as individual images
|
||||
if (!(input.data instanceof ArrayBuffer)) {
|
||||
console.log('Expected ArrayBuffer for PDF images converter, got:', typeof input.data);
|
||||
break;
|
||||
}
|
||||
// duplicate the ArrayBuffer to avoid mutation
|
||||
const pdfData2 = new Uint8Array(input.data.slice(0));
|
||||
try {
|
||||
const imageDataURLs = await pdfToImageDataURLs(pdfData2);
|
||||
imageDataURLs.forEach((pdfImg, index) => {
|
||||
outputs.push({
|
||||
type: 'image-part',
|
||||
base64Url: pdfImg.base64Url,
|
||||
metadata: {
|
||||
title: `Page ${index + 1}`,
|
||||
width: pdfImg.width,
|
||||
height: pdfImg.height,
|
||||
},
|
||||
collapsible: false,
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error converting PDF to images:', error);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'image':
|
||||
|
||||
@@ -10,8 +10,8 @@ import type { ComposerOutputMultiPart, ComposerOutputPartType } from '../compose
|
||||
|
||||
export interface LLMAttachments {
|
||||
attachments: LLMAttachment[];
|
||||
getAttachmentOutputs: (initialTextBlockText: string | null, attachmentId: AttachmentId) => ComposerOutputMultiPart;
|
||||
getAttachmentsOutputs: (initialTextBlockText: string | null) => ComposerOutputMultiPart;
|
||||
collapseWithAttachment: (initialTextBlockText: string | null, attachmentId: AttachmentId) => ComposerOutputMultiPart;
|
||||
collapseWithAttachments: (initialTextBlockText: string | null) => ComposerOutputMultiPart;
|
||||
isOutputAttacheable: boolean;
|
||||
isOutputTextInlineable: boolean;
|
||||
tokenCountApprox: number;
|
||||
@@ -37,13 +37,13 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
|
||||
|
||||
const llmAttachments = attachments.map(attachment => toLLMAttachment(attachment, supportedOutputPartTypes, chatLLMId));
|
||||
|
||||
const getAttachmentOutputs = (initialTextBlockText: string | null, attachmentId: AttachmentId): ComposerOutputMultiPart => {
|
||||
const collapseWithAttachment = (initialTextBlockText: string | null, attachmentId: AttachmentId): ComposerOutputMultiPart => {
|
||||
// get outputs of a specific attachment
|
||||
const outputs = attachments.find(a => a.id === attachmentId)?.outputs || [];
|
||||
return attachmentCollapseOutputs(initialTextBlockText, outputs);
|
||||
};
|
||||
|
||||
const getAttachmentsOutputs = (initialTextBlockText: string | null): ComposerOutputMultiPart => {
|
||||
const collapseWithAttachments = (initialTextBlockText: string | null): ComposerOutputMultiPart => {
|
||||
// accumulate all outputs of all attachments
|
||||
const allOutputs = llmAttachments.reduce((acc, a) => acc.concat(a.attachment.outputs), [] as ComposerOutputMultiPart);
|
||||
return attachmentCollapseOutputs(initialTextBlockText, allOutputs);
|
||||
@@ -51,8 +51,8 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
|
||||
|
||||
return {
|
||||
attachments: llmAttachments,
|
||||
getAttachmentOutputs,
|
||||
getAttachmentsOutputs,
|
||||
collapseWithAttachment,
|
||||
collapseWithAttachments,
|
||||
isOutputAttacheable: llmAttachments.every(a => a.isOutputAttachable),
|
||||
isOutputTextInlineable: llmAttachments.every(a => a.isOutputTextInlineable),
|
||||
tokenCountApprox: llmAttachments.reduce((acc, a) => acc + (a.tokenCountApprox || 0), 0),
|
||||
@@ -60,7 +60,7 @@ export function useLLMAttachments(attachments: Attachment[], chatLLMId: DLLMId |
|
||||
}, [attachments, chatLLMId]);
|
||||
}
|
||||
|
||||
export function getTextBlockText(outputs: ComposerOutputMultiPart): string | null {
|
||||
export function getSingleTextBlockText(outputs: ComposerOutputMultiPart): string | null {
|
||||
const textOutputs = outputs.filter(part => part.type === 'text-block');
|
||||
return (textOutputs.length === 1 && textOutputs[0].type === 'text-block') ? textOutputs[0].text : null;
|
||||
}
|
||||
|
||||
@@ -11,10 +11,14 @@ import { animationEnterBelow } from '~/common/util/animUtils';
|
||||
const desktopLegend =
|
||||
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
|
||||
Combine the answers from multiple models<br />
|
||||
{/*{platformAwareKeystrokes('Ctrl + Enter')}*/}
|
||||
<KeyStroke combo='Ctrl + Enter' sx={{ mt: 0.5, mb: 0.25 }} />
|
||||
</Box>;
|
||||
|
||||
const desktopLegendNoContent =
|
||||
<Box sx={{ px: 1, py: 0.75, lineHeight: '1.5rem' }}>
|
||||
Enter the text to Beam, then press this
|
||||
</Box>;
|
||||
|
||||
const mobileSx: SxProps = {
|
||||
mr: { xs: 1, md: 2 },
|
||||
};
|
||||
@@ -31,13 +35,13 @@ const desktopSx: SxProps = {
|
||||
|
||||
export const ButtonBeamMemo = React.memo(ButtonBeam);
|
||||
|
||||
function ButtonBeam(props: { isMobile?: boolean, disabled?: boolean, onClick: () => void }) {
|
||||
function ButtonBeam(props: { isMobile?: boolean, disabled?: boolean, hasContent?: boolean, onClick: () => void }) {
|
||||
return props.isMobile ? (
|
||||
<IconButton variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} sx={mobileSx}>
|
||||
<ChatBeamIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<Tooltip disableInteractive variant='solid' arrow placement='right' title={desktopLegend}>
|
||||
<Tooltip disableInteractive variant='solid' arrow placement='right' title={props.hasContent ? desktopLegend : desktopLegendNoContent}>
|
||||
<Button variant='soft' color='primary' disabled={props.disabled} onClick={props.onClick} endDecorator={<ChatBeamIcon />} sx={desktopSx}>
|
||||
Beam
|
||||
</Button>
|
||||
|
||||
@@ -9,6 +9,13 @@ export type ComposerOutputPart = {
|
||||
// TODO: not implemented yet
|
||||
type: 'image-part',
|
||||
base64Url: string,
|
||||
metadata: {
|
||||
title?: string,
|
||||
generatedBy?: string,
|
||||
altText?: string,
|
||||
width?: number,
|
||||
height?: number,
|
||||
},
|
||||
collapsible: false,
|
||||
};
|
||||
|
||||
|
||||
@@ -36,8 +36,9 @@ export function FolderListItem(props: {
|
||||
|
||||
|
||||
// Menu
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
setMenuAnchorEl(event.currentTarget);
|
||||
const handleMenuToggle = (event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
setMenuAnchorEl(anchor => anchor ? null : event.currentTarget);
|
||||
setDeleteArmed(false); // Reset delete armed state
|
||||
};
|
||||
|
||||
@@ -188,9 +189,11 @@ export function FolderListItem(props: {
|
||||
|
||||
{/* Icon to show the Popup menu */}
|
||||
<IconButton
|
||||
size='sm'
|
||||
variant='outlined'
|
||||
className='menu-icon'
|
||||
onClick={handleMenuOpen}
|
||||
onClick={handleMenuToggle}
|
||||
onContextMenu={handleMenuToggle}
|
||||
sx={{
|
||||
visibility: 'hidden',
|
||||
my: '-0.25rem', /* absorb the button padding */
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Avatar, Box, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import AccountTreeTwoToneIcon from '@mui/icons-material/AccountTreeTwoTone';
|
||||
import { Avatar, Box, ButtonGroup, CircularProgress, IconButton, ListDivider, ListItem, ListItemDecorator, MenuItem, Switch, Tooltip, Typography } from '@mui/joy';
|
||||
import { ClickAwayListener, Popper } from '@mui/base';
|
||||
import AccountTreeOutlinedIcon from '@mui/icons-material/AccountTreeOutlined';
|
||||
import ClearIcon from '@mui/icons-material/Clear';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import DifferenceIcon from '@mui/icons-material/Difference';
|
||||
import EditRoundedIcon from '@mui/icons-material/EditRounded';
|
||||
import Face6Icon from '@mui/icons-material/Face6';
|
||||
import ForkRightIcon from '@mui/icons-material/ForkRight';
|
||||
import FormatPaintTwoToneIcon from '@mui/icons-material/FormatPaintTwoTone';
|
||||
import FormatPaintOutlinedIcon from '@mui/icons-material/FormatPaintOutlined';
|
||||
import MoreVertIcon from '@mui/icons-material/MoreVert';
|
||||
import RecordVoiceOverTwoToneIcon from '@mui/icons-material/RecordVoiceOverTwoTone';
|
||||
import RecordVoiceOverOutlinedIcon from '@mui/icons-material/RecordVoiceOverOutlined';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
import ReplyRoundedIcon from '@mui/icons-material/ReplyRounded';
|
||||
import SettingsSuggestIcon from '@mui/icons-material/SettingsSuggest';
|
||||
import SmartToyOutlinedIcon from '@mui/icons-material/SmartToyOutlined';
|
||||
import StarOutlineRoundedIcon from '@mui/icons-material/StarOutlineRounded';
|
||||
@@ -32,29 +34,31 @@ import { DMessage, DMessageUserFlag, messageHasUserFlag } from '~/common/state/s
|
||||
import { InlineTextarea } from '~/common/components/InlineTextarea';
|
||||
import { KeyStroke } from '~/common/components/KeyStroke';
|
||||
import { Link } from '~/common/components/Link';
|
||||
import { adjustContentScaling, themeScalingMap } from '~/common/app.theme';
|
||||
import { adjustContentScaling, themeScalingMap, themeZIndexPageBar } from '~/common/app.theme';
|
||||
import { animationColorRainbow } from '~/common/util/animUtils';
|
||||
import { copyToClipboard } from '~/common/util/clipboardUtils';
|
||||
import { prettyBaseModel } from '~/common/util/modelUtils';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { useUXLabsStore } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { ReplyToBubble } from './ReplyToBubble';
|
||||
import { useChatShowTextDiff } from '../../store-app-chat';
|
||||
|
||||
|
||||
// Enable the menu on text selection
|
||||
const ENABLE_SELECTION_RIGHT_CLICK_MENU: boolean = true;
|
||||
const ENABLE_SELECTION_RIGHT_CLICK_MENU = false;
|
||||
const ENABLE_SELECTION_TOOLBAR = true;
|
||||
const SELECTION_TOOLBAR_MIN_LENGTH = 3;
|
||||
|
||||
// Enable the hover button to copy the whole message. The Copy button is also available in Blocks, or in the Avatar Menu.
|
||||
const ENABLE_COPY_MESSAGE_OVERLAY: boolean = false;
|
||||
|
||||
|
||||
export function messageBackground(messageRole: DMessage['role'] | string, wasEdited: boolean, unknownAssistantIssue: boolean): string {
|
||||
export function messageBackground(messageRole: DMessage['role'] | string, wasEdited: boolean, isAssistantIssue: boolean): string {
|
||||
switch (messageRole) {
|
||||
case 'user':
|
||||
return 'primary.plainHoverBg'; // was .background.level1
|
||||
case 'assistant':
|
||||
return unknownAssistantIssue ? 'danger.softBg' : 'background.surface';
|
||||
return isAssistantIssue ? 'danger.softBg' : 'background.surface';
|
||||
case 'system':
|
||||
return wasEdited ? 'warning.softHoverBg' : 'neutral.softBg';
|
||||
default:
|
||||
@@ -114,7 +118,7 @@ export function makeAvatar(messageAvatar: string | null, messageRole: DMessage['
|
||||
|
||||
// icon: text-to-image
|
||||
if (isTextToImage)
|
||||
return <FormatPaintTwoToneIcon sx={{
|
||||
return <FormatPaintOutlinedIcon sx={{
|
||||
...avatarIconSx,
|
||||
animation: `${animationColorRainbow} 1s linear 2.66`,
|
||||
}} />;
|
||||
@@ -228,6 +232,7 @@ export function ChatMessage(props: {
|
||||
onMessageEdit?: (messageId: string, text: string) => void,
|
||||
onMessageToggleUserFlag?: (messageId: string, flag: DMessageUserFlag) => void,
|
||||
onMessageTruncate?: (messageId: string) => void,
|
||||
onReplyTo?: (messageId: string, selectedText: string) => void,
|
||||
onTextDiagram?: (messageId: string, text: string) => Promise<void>
|
||||
onTextImagine?: (text: string) => Promise<void>
|
||||
onTextSpeak?: (text: string) => Promise<void>
|
||||
@@ -235,20 +240,21 @@ export function ChatMessage(props: {
|
||||
}) {
|
||||
|
||||
// state
|
||||
const blocksRendererRef = React.useRef<HTMLDivElement>(null);
|
||||
const [isHovering, setIsHovering] = React.useState(false);
|
||||
const [opsMenuAnchor, setOpsMenuAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [selMenuAnchor, setSelMenuAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [selMenuText, setSelMenuText] = React.useState<string | null>(null);
|
||||
const [selToolbarAnchor, setSelToolbarAnchor] = React.useState<HTMLElement | null>(null);
|
||||
const [selText, setSelText] = React.useState<string | null>(null);
|
||||
const [isEditing, setIsEditing] = React.useState(false);
|
||||
|
||||
// external state
|
||||
const labsBeam = useUXLabsStore(state => state.labsBeam);
|
||||
const { showAvatar, contentScaling, doubleClickToEdit, renderMarkdown } = useUIPreferencesStore(state => ({
|
||||
const { showAvatar, contentScaling, doubleClickToEdit, renderMarkdown } = useUIPreferencesStore(useShallow(state => ({
|
||||
showAvatar: props.showAvatar !== undefined ? props.showAvatar : state.zenMode !== 'cleaner',
|
||||
contentScaling: adjustContentScaling(state.contentScaling, props.adjustContentScaling),
|
||||
doubleClickToEdit: state.doubleClickToEdit,
|
||||
renderMarkdown: state.renderMarkdown,
|
||||
}), shallow);
|
||||
})));
|
||||
const [showDiff, setShowDiff] = useChatShowTextDiff();
|
||||
const textDiffs = useSanityTextDiffs(props.message.text, props.diffPreviousText, showDiff);
|
||||
|
||||
@@ -262,6 +268,7 @@ export function ChatMessage(props: {
|
||||
role: messageRole,
|
||||
purposeId: messagePurposeId,
|
||||
originLLM: messageOriginLLM,
|
||||
metadata: messageMetadata,
|
||||
created: messageCreated,
|
||||
updated: messageUpdated,
|
||||
} = props.message;
|
||||
@@ -272,10 +279,10 @@ export function ChatMessage(props: {
|
||||
const fromSystem = messageRole === 'system';
|
||||
const wasEdited = !!messageUpdated;
|
||||
|
||||
const textSel = selMenuText ? selMenuText : messageText;
|
||||
const textSel = selText ? selText : messageText;
|
||||
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 couldDiagram = textSel.length >= 100 && !isSpecialT2I;
|
||||
const couldImagine = textSel.length >= 3 && !isSpecialT2I;
|
||||
const couldSpeak = couldImagine;
|
||||
|
||||
|
||||
@@ -290,21 +297,27 @@ export function ChatMessage(props: {
|
||||
|
||||
const { onMessageToggleUserFlag } = props;
|
||||
|
||||
const closeOpsMenu = () => setOpsMenuAnchor(null);
|
||||
const handleOpsMenuToggle = React.useCallback((event: React.MouseEvent<HTMLElement>) => {
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
setOpsMenuAnchor(anchor => anchor ? null : event.currentTarget);
|
||||
}, []);
|
||||
|
||||
const handleCloseOpsMenu = React.useCallback(() => setOpsMenuAnchor(null), []);
|
||||
|
||||
const handleOpsCopy = (e: React.MouseEvent) => {
|
||||
copyToClipboard(textSel, 'Text');
|
||||
e.preventDefault();
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
};
|
||||
|
||||
const handleOpsEdit = React.useCallback((e: React.MouseEvent) => {
|
||||
if (messageTyping && !isEditing) return; // don't allow editing while typing
|
||||
setIsEditing(!isEditing);
|
||||
e.preventDefault();
|
||||
closeOpsMenu();
|
||||
}, [isEditing, messageTyping]);
|
||||
handleCloseOpsMenu();
|
||||
}, [handleCloseOpsMenu, isEditing, messageTyping]);
|
||||
|
||||
const handleOpsToggleStarred = React.useCallback(() => {
|
||||
onMessageToggleUserFlag?.(messageId, 'starred');
|
||||
@@ -312,21 +325,21 @@ export function ChatMessage(props: {
|
||||
|
||||
const handleOpsAssistantFrom = async (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
await props.onMessageAssistantFrom?.(messageId, fromAssistant ? -1 : 0);
|
||||
};
|
||||
|
||||
const handleOpsBeamFrom = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
closeOpsMenu();
|
||||
labsBeam && await props.onMessageBeam?.(messageId);
|
||||
handleCloseOpsMenu();
|
||||
await props.onMessageBeam?.(messageId);
|
||||
};
|
||||
|
||||
const handleOpsBranch = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // to try to not steal the focus from the banched conversation
|
||||
props.onMessageBranch?.(messageId);
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsToggleShowDiff = () => setShowDiff(!showDiff);
|
||||
@@ -335,8 +348,9 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextDiagram) {
|
||||
await props.onTextDiagram(messageId, textSel);
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -344,8 +358,19 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextImagine) {
|
||||
await props.onTextImagine(textSel);
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpsReplyTo = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (props.onReplyTo && textSel.trim().length >= SELECTION_TOOLBAR_MIN_LENGTH) {
|
||||
props.onReplyTo(messageId, textSel.trim());
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -353,14 +378,15 @@ export function ChatMessage(props: {
|
||||
e.preventDefault();
|
||||
if (props.onTextSpeak) {
|
||||
await props.onTextSpeak(textSel);
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
closeSelectionMenu();
|
||||
closeToolbar();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpsTruncate = (_e: React.MouseEvent) => {
|
||||
props.onMessageTruncate?.(messageId);
|
||||
closeOpsMenu();
|
||||
handleCloseOpsMenu();
|
||||
};
|
||||
|
||||
const handleOpsDelete = (_e: React.MouseEvent) => {
|
||||
@@ -395,17 +421,17 @@ export function ChatMessage(props: {
|
||||
document.body.appendChild(anchorEl);
|
||||
|
||||
setSelMenuAnchor(anchorEl);
|
||||
setSelMenuText(selectedText);
|
||||
setSelText(selectedText);
|
||||
}, [removeSelectionAnchor]);
|
||||
|
||||
const closeSelectionMenu = React.useCallback(() => {
|
||||
// window.getSelection()?.removeAllRanges?.();
|
||||
removeSelectionAnchor();
|
||||
setSelMenuAnchor(null);
|
||||
setSelMenuText(null);
|
||||
setSelText(null);
|
||||
}, [removeSelectionAnchor]);
|
||||
|
||||
const handleMouseUp = React.useCallback((event: MouseEvent) => {
|
||||
const handleContextMenu = React.useCallback((event: MouseEvent) => {
|
||||
const selection = window.getSelection();
|
||||
if (selection && selection.rangeCount > 0) {
|
||||
const range = selection.getRangeAt(0);
|
||||
@@ -416,16 +442,74 @@ export function ChatMessage(props: {
|
||||
}, [openSelectionMenu]);
|
||||
|
||||
|
||||
// Selection Toolbar
|
||||
|
||||
const closeToolbar = React.useCallback((anchorEl?: HTMLElement) => {
|
||||
window.getSelection()?.removeAllRanges?.();
|
||||
try {
|
||||
const anchor = anchorEl || selToolbarAnchor;
|
||||
anchor && document.body.removeChild(anchor);
|
||||
} catch (e) {
|
||||
// ignore...
|
||||
}
|
||||
setSelToolbarAnchor(null);
|
||||
setSelText(null);
|
||||
}, [selToolbarAnchor]);
|
||||
|
||||
const handleOpenToolbar = React.useCallback((_event: MouseEvent) => {
|
||||
// check for selection
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.rangeCount <= 0) return;
|
||||
|
||||
// check for enought selection
|
||||
const selectionText = selection.toString().trim();
|
||||
if (selectionText.length < SELECTION_TOOLBAR_MIN_LENGTH) return;
|
||||
|
||||
// check for the selection being inside the blocks renderer (core of the message)
|
||||
const selectionRange = selection.getRangeAt(0);
|
||||
const blocksElement = blocksRendererRef.current;
|
||||
if (!blocksElement || !blocksElement.contains(selectionRange.commonAncestorContainer)) return;
|
||||
|
||||
const rangeRects = selectionRange.getClientRects();
|
||||
if (rangeRects.length <= 0) return;
|
||||
|
||||
const firstRect = rangeRects[0];
|
||||
const anchorEl = document.createElement('div');
|
||||
anchorEl.style.position = 'fixed';
|
||||
anchorEl.style.left = `${firstRect.left + window.scrollX}px`;
|
||||
anchorEl.style.top = `${firstRect.top + window.scrollY}px`;
|
||||
document.body.appendChild(anchorEl);
|
||||
anchorEl.setAttribute('role', 'dialog');
|
||||
|
||||
// auto-close logic on unselect
|
||||
const closeOnUnselect = () => {
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.toString().trim() === '') {
|
||||
closeToolbar(anchorEl);
|
||||
document.removeEventListener('selectionchange', closeOnUnselect);
|
||||
}
|
||||
};
|
||||
document.addEventListener('selectionchange', closeOnUnselect);
|
||||
|
||||
setSelToolbarAnchor(anchorEl);
|
||||
setSelText(selectionText);
|
||||
}, [closeToolbar]);
|
||||
|
||||
|
||||
// Blocks renderer
|
||||
|
||||
const handleBlocksContextMenu = React.useCallback((event: React.MouseEvent) => {
|
||||
handleMouseUp(event.nativeEvent);
|
||||
}, [handleMouseUp]);
|
||||
handleContextMenu(event.nativeEvent);
|
||||
}, [handleContextMenu]);
|
||||
|
||||
const handleBlocksDoubleClick = React.useCallback((event: React.MouseEvent) => {
|
||||
doubleClickToEdit && props.onMessageEdit && handleOpsEdit(event);
|
||||
}, [doubleClickToEdit, handleOpsEdit, props.onMessageEdit]);
|
||||
|
||||
const handleBlocksMouseUp = React.useCallback((event: React.MouseEvent) => {
|
||||
handleOpenToolbar(event.nativeEvent);
|
||||
}, [handleOpenToolbar]);
|
||||
|
||||
|
||||
// prettier upstream errors
|
||||
const { isAssistantError, errorMessage } = React.useMemo(
|
||||
@@ -446,6 +530,7 @@ export function ChatMessage(props: {
|
||||
return (
|
||||
<ListItem
|
||||
role='chat-message'
|
||||
onMouseUp={(ENABLE_SELECTION_TOOLBAR && !fromSystem && !isAssistantError) ? handleBlocksMouseUp : undefined}
|
||||
sx={{
|
||||
// style
|
||||
backgroundColor: backgroundColor,
|
||||
@@ -468,92 +553,97 @@ export function ChatMessage(props: {
|
||||
}),
|
||||
|
||||
// style: make room for a top decorator if set
|
||||
...(!!props.topDecorator && {
|
||||
pt: '2.5rem',
|
||||
}),
|
||||
'&:hover > button': { opacity: 1 },
|
||||
|
||||
// layout
|
||||
display: 'flex',
|
||||
flexDirection: !fromAssistant ? 'row-reverse' : 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: { xs: 0, md: 1 },
|
||||
display: 'block', // this is Needed, otherwise there will be a horizontal overflow
|
||||
|
||||
...props.sx,
|
||||
}}
|
||||
>
|
||||
|
||||
{/* (Optional) underlayed top decorator */}
|
||||
{props.topDecorator && (
|
||||
<Box sx={{ position: 'absolute', left: 0, right: 0, top: 0, textAlign: 'center' }}>
|
||||
{props.topDecorator}
|
||||
</Box>
|
||||
)}
|
||||
{props.topDecorator}
|
||||
|
||||
{/* Avatar (Persona) */}
|
||||
{showAvatar && (
|
||||
<Box sx={personaSx}>
|
||||
{/* Message Row: Avatar, Blocks (1 text -> blocksRenderer) */}
|
||||
<Box sx={{
|
||||
display: 'flex',
|
||||
flexDirection: !fromAssistant ? 'row-reverse' : 'row',
|
||||
alignItems: 'flex-start',
|
||||
gap: { xs: 0, md: 1 },
|
||||
}}>
|
||||
|
||||
{/* Persona Avatar or Menu Button */}
|
||||
<Box
|
||||
onClick={event => setOpsMenuAnchor(event.currentTarget)}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
sx={{ display: 'flex' }}
|
||||
>
|
||||
{(isHovering || opsMenuAnchor) ? (
|
||||
<IconButton variant={opsMenuAnchor ? 'solid' : 'soft'} color={(fromAssistant || fromSystem) ? 'neutral' : 'primary'} sx={avatarIconSx}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
avatarEl
|
||||
{/* Avatar (Persona) */}
|
||||
{showAvatar && (
|
||||
<Box sx={personaSx}>
|
||||
|
||||
{/* Persona Avatar or Menu Button */}
|
||||
<Box
|
||||
onClick={handleOpsMenuToggle}
|
||||
onContextMenu={handleOpsMenuToggle}
|
||||
onMouseEnter={() => setIsHovering(true)}
|
||||
onMouseLeave={() => setIsHovering(false)}
|
||||
sx={{ display: 'flex' }}
|
||||
>
|
||||
{(isHovering || opsMenuAnchor) ? (
|
||||
<IconButton variant={opsMenuAnchor ? 'solid' : 'soft'} color={(fromAssistant || fromSystem) ? 'neutral' : 'primary'} sx={avatarIconSx}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
avatarEl
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Assistant model name */}
|
||||
{fromAssistant && (
|
||||
<Tooltip arrow title={messageTyping ? null : (messageOriginLLM || 'unk-model')} variant='solid'>
|
||||
<Typography level='body-xs' sx={{
|
||||
overflowWrap: 'anywhere',
|
||||
...(messageTyping ? { animation: `${animationColorRainbow} 5s linear infinite` } : {}),
|
||||
}}>
|
||||
{prettyBaseModel(messageOriginLLM)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Assistant model name */}
|
||||
{fromAssistant && (
|
||||
<Tooltip arrow title={messageTyping ? null : (messageOriginLLM || 'unk-model')} variant='solid'>
|
||||
<Typography level='body-xs' sx={{
|
||||
overflowWrap: 'anywhere',
|
||||
...(messageTyping ? { animation: `${animationColorRainbow} 5s linear infinite` } : {}),
|
||||
}}>
|
||||
{prettyBaseModel(messageOriginLLM)}
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
)}
|
||||
)}
|
||||
|
||||
|
||||
{/* Edit / Blocks */}
|
||||
{isEditing ? (
|
||||
{/* Edit / Blocks */}
|
||||
{isEditing ? (
|
||||
|
||||
<InlineTextarea
|
||||
initialText={messageText} onEdit={handleTextEdited}
|
||||
sx={editBlocksSx}
|
||||
/>
|
||||
<InlineTextarea
|
||||
initialText={messageText} onEdit={handleTextEdited}
|
||||
sx={editBlocksSx}
|
||||
/>
|
||||
|
||||
) : (
|
||||
) : (
|
||||
|
||||
<BlocksRenderer
|
||||
text={messageText}
|
||||
fromRole={messageRole}
|
||||
contentScaling={contentScaling}
|
||||
errorMessage={errorMessage}
|
||||
fitScreen={props.fitScreen}
|
||||
isBottom={props.isBottom}
|
||||
renderTextAsMarkdown={renderMarkdown}
|
||||
renderTextDiff={textDiffs || undefined}
|
||||
showDate={props.showBlocksDate === true ? messageUpdated || messageCreated || undefined : undefined}
|
||||
showUnsafeHtml={props.showUnsafeHtml}
|
||||
wasUserEdited={wasEdited}
|
||||
onContextMenu={(props.onMessageEdit && ENABLE_SELECTION_RIGHT_CLICK_MENU) ? handleBlocksContextMenu : undefined}
|
||||
onDoubleClick={(props.onMessageEdit && doubleClickToEdit) ? handleBlocksDoubleClick : undefined}
|
||||
optiAllowMemo={messageTyping}
|
||||
/>
|
||||
<BlocksRenderer
|
||||
ref={blocksRendererRef}
|
||||
text={messageText}
|
||||
fromRole={messageRole}
|
||||
contentScaling={contentScaling}
|
||||
errorMessage={errorMessage}
|
||||
fitScreen={props.fitScreen}
|
||||
isBottom={props.isBottom}
|
||||
renderTextAsMarkdown={renderMarkdown}
|
||||
renderTextDiff={textDiffs || undefined}
|
||||
showDate={props.showBlocksDate === true ? messageUpdated || messageCreated || undefined : undefined}
|
||||
showUnsafeHtml={props.showUnsafeHtml}
|
||||
wasUserEdited={wasEdited}
|
||||
onContextMenu={(props.onMessageEdit && ENABLE_SELECTION_RIGHT_CLICK_MENU) ? handleBlocksContextMenu : undefined}
|
||||
onDoubleClick={(props.onMessageEdit && doubleClickToEdit) ? handleBlocksDoubleClick : undefined}
|
||||
optiAllowMemo={messageTyping}
|
||||
/>
|
||||
|
||||
)}
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
{/* Reply-To Bubble */}
|
||||
{!!messageMetadata?.inReplyToText && <ReplyToBubble inlineMessage replyToText={messageMetadata.inReplyToText} className='reply-to-bubble' />}
|
||||
|
||||
|
||||
{/* Overlay copy icon */}
|
||||
@@ -575,7 +665,7 @@ export function ChatMessage(props: {
|
||||
{!!opsMenuAnchor && (
|
||||
<CloseableMenu
|
||||
dense placement='bottom-end'
|
||||
open anchorEl={opsMenuAnchor} onClose={closeOpsMenu}
|
||||
open anchorEl={opsMenuAnchor} onClose={handleCloseOpsMenu}
|
||||
sx={{ minWidth: 280 }}
|
||||
>
|
||||
|
||||
@@ -637,6 +727,26 @@ export function ChatMessage(props: {
|
||||
<span style={{ opacity: 0.5 }}>after this</span>
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Diagram / Draw / Speak */}
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && (
|
||||
<MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
|
||||
<ListItemDecorator><AccountTreeOutlinedIcon /></ListItemDecorator>
|
||||
Auto-Diagram ...
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextImagine && (
|
||||
<MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintOutlinedIcon />}</ListItemDecorator>
|
||||
Auto-Draw
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextSpeak && (
|
||||
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverOutlinedIcon />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Diff Viewer */}
|
||||
{!!props.diffPreviousText && <ListDivider />}
|
||||
{!!props.diffPreviousText && (
|
||||
@@ -646,26 +756,6 @@ export function ChatMessage(props: {
|
||||
<Switch checked={showDiff} onChange={handleOpsToggleShowDiff} sx={{ ml: 'auto' }} />
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Diagram / Draw / Speak */}
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && (
|
||||
<MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram}>
|
||||
<ListItemDecorator><AccountTreeTwoToneIcon /></ListItemDecorator>
|
||||
Auto-Diagram ...
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextImagine && (
|
||||
<MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintTwoToneIcon />}</ListItemDecorator>
|
||||
Auto-Draw
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onTextSpeak && (
|
||||
<MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverTwoToneIcon />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>
|
||||
)}
|
||||
{/* Beam/Restart */}
|
||||
{(!!props.onMessageAssistantFrom || !!props.onMessageBeam) && <ListDivider />}
|
||||
{!!props.onMessageAssistantFrom && (
|
||||
@@ -678,7 +768,7 @@ export function ChatMessage(props: {
|
||||
: <Box sx={{ flexGrow: 1, display: 'flex', justifyContent: 'space-between', gap: 1 }}>Retry<KeyStroke combo='Ctrl + Shift + R' /></Box>}
|
||||
</MenuItem>
|
||||
)}
|
||||
{!!props.onMessageBeam && labsBeam && (
|
||||
{!!props.onMessageBeam && (
|
||||
<MenuItem disabled={fromSystem} onClick={handleOpsBeamFrom}>
|
||||
<ListItemDecorator>
|
||||
<ChatBeamIcon color={fromSystem ? undefined : 'primary'} />
|
||||
@@ -693,6 +783,71 @@ export function ChatMessage(props: {
|
||||
</CloseableMenu>
|
||||
)}
|
||||
|
||||
|
||||
{/* Selection Toolbar */}
|
||||
{ENABLE_SELECTION_TOOLBAR && !!selToolbarAnchor && (
|
||||
<Popper placement='top-start' open anchorEl={selToolbarAnchor} slotProps={{
|
||||
root: { style: { zIndex: themeZIndexPageBar + 1 } },
|
||||
}}>
|
||||
<ClickAwayListener onClickAway={() => closeToolbar()}>
|
||||
<ButtonGroup
|
||||
variant='plain'
|
||||
sx={{
|
||||
'--ButtonGroup-separatorColor': 'none !important',
|
||||
'--ButtonGroup-separatorSize': 0,
|
||||
borderRadius: '0',
|
||||
backgroundColor: 'background.popup',
|
||||
border: '1px solid',
|
||||
borderColor: 'primary.outlinedBorder',
|
||||
boxShadow: '0px 4px 12px -4px rgb(var(--joy-palette-neutral-darkChannel) / 50%)',
|
||||
mb: 1,
|
||||
ml: -1,
|
||||
alignItems: 'center',
|
||||
'& > button': {
|
||||
'--Icon-fontSize': '1rem',
|
||||
minHeight: '2.5rem',
|
||||
minWidth: '2.75rem',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!!props.onReplyTo && fromAssistant && <Tooltip disableInteractive arrow placement='top' title='Reply'>
|
||||
<IconButton color='primary' onClick={handleOpsReplyTo}>
|
||||
<ReplyRoundedIcon sx={{ fontSize: 'xl' }} />
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{/*{!!props.onMessageBeam && fromAssistant && <Tooltip disableInteractive arrow placement='top' title='Beam'>*/}
|
||||
{/* <IconButton color='primary'>*/}
|
||||
{/* <ChatBeamIcon sx={{ fontSize: 'xl' }} />*/}
|
||||
{/* </IconButton>*/}
|
||||
{/*</Tooltip>}*/}
|
||||
{!!props.onReplyTo && fromAssistant && <MoreVertIcon sx={{ color: 'neutral.outlinedBorder', fontSize: 'md' }} />}
|
||||
<Tooltip disableInteractive arrow placement='top' title='Copy'>
|
||||
<IconButton onClick={handleOpsCopy}>
|
||||
<ContentCopyIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{(!!props.onTextDiagram || !!props.onTextSpeak) && <MoreVertIcon sx={{ color: 'neutral.outlinedBorder', fontSize: 'md' }} />}
|
||||
{!!props.onTextDiagram && <Tooltip disableInteractive arrow placement='top' title={couldDiagram ? 'Auto-Diagram...' : 'Too short to Auto-Diagram'}>
|
||||
<IconButton onClick={couldDiagram ? handleOpsDiagram : undefined}>
|
||||
<AccountTreeOutlinedIcon sx={{ color: couldDiagram ? 'primary' : 'neutral.plainDisabledColor' }} />
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
{/*{!!props.onTextImagine && <Tooltip disableInteractive arrow placement='top' title='Auto-Draw'>*/}
|
||||
{/* <IconButton onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>*/}
|
||||
{/* {!props.isImagining ? <FormatPaintOutlinedIcon /> : <CircularProgress sx={{ '--CircularProgress-size': '16px' }} />}*/}
|
||||
{/* </IconButton>*/}
|
||||
{/*</Tooltip>}*/}
|
||||
{!!props.onTextSpeak && <Tooltip disableInteractive arrow placement='top' title='Speak'>
|
||||
<IconButton onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
{!props.isSpeaking ? <RecordVoiceOverOutlinedIcon /> : <CircularProgress sx={{ '--CircularProgress-size': '16px' }} />}
|
||||
</IconButton>
|
||||
</Tooltip>}
|
||||
</ButtonGroup>
|
||||
</ClickAwayListener>
|
||||
</Popper>
|
||||
)}
|
||||
|
||||
|
||||
{/* Selection (Contextual) Menu */}
|
||||
{!!selMenuAnchor && (
|
||||
<CloseableMenu
|
||||
@@ -706,15 +861,15 @@ export function ChatMessage(props: {
|
||||
</MenuItem>
|
||||
{!!props.onTextDiagram && <ListDivider />}
|
||||
{!!props.onTextDiagram && <MenuItem onClick={handleOpsDiagram} disabled={!couldDiagram || props.isImagining}>
|
||||
<ListItemDecorator><AccountTreeTwoToneIcon /></ListItemDecorator>
|
||||
<ListItemDecorator><AccountTreeOutlinedIcon /></ListItemDecorator>
|
||||
Auto-Diagram ...
|
||||
</MenuItem>}
|
||||
{!!props.onTextImagine && <MenuItem onClick={handleOpsImagine} disabled={!couldImagine || props.isImagining}>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintTwoToneIcon />}</ListItemDecorator>
|
||||
<ListItemDecorator>{props.isImagining ? <CircularProgress size='sm' /> : <FormatPaintOutlinedIcon />}</ListItemDecorator>
|
||||
Auto-Draw
|
||||
</MenuItem>}
|
||||
{!!props.onTextSpeak && <MenuItem onClick={handleOpsSpeak} disabled={!couldSpeak || props.isSpeaking}>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverTwoToneIcon />}</ListItemDecorator>
|
||||
<ListItemDecorator>{props.isSpeaking ? <CircularProgress size='sm' /> : <RecordVoiceOverOutlinedIcon />}</ListItemDecorator>
|
||||
Speak
|
||||
</MenuItem>}
|
||||
</CloseableMenu>
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, IconButton, Tooltip, Typography } from '@mui/joy';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
import ReplyRoundedIcon from '@mui/icons-material/ReplyRounded';
|
||||
|
||||
|
||||
// configuration
|
||||
const INLINE_COLOR = 'primary';
|
||||
|
||||
|
||||
const bubbleComposerSx: SxProps = {
|
||||
// contained
|
||||
width: '100%',
|
||||
zIndex: 2, // stays on top of the 'tokens' bubble in the composer
|
||||
|
||||
// style
|
||||
backgroundColor: 'background.surface',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
borderRadius: 'sm',
|
||||
boxShadow: 'xs',
|
||||
padding: '0.5rem 0.25rem 0.5rem 0.5rem',
|
||||
|
||||
// layout
|
||||
display: 'flex',
|
||||
alignItems: 'start',
|
||||
};
|
||||
|
||||
const inlineMessageSx: SxProps = {
|
||||
...bubbleComposerSx,
|
||||
|
||||
// redefine
|
||||
// border: 'none',
|
||||
mt: 1,
|
||||
borderColor: `${INLINE_COLOR}.outlinedColor`,
|
||||
borderRadius: 'sm',
|
||||
boxShadow: 'xs',
|
||||
width: undefined,
|
||||
padding: '0.375rem 0.25rem 0.375rem 0.5rem',
|
||||
|
||||
// self-layout (parent: 'block', as 'grid' was not working and the user would scroll the app on the x-axis on mobile)
|
||||
// ml: 'auto',
|
||||
float: 'inline-end',
|
||||
mr: { xs: 7.75, md: 10.5 }, // personaSx.minWidth + gap (md: 1) + 1.5 (text margin)
|
||||
|
||||
};
|
||||
|
||||
|
||||
export function ReplyToBubble(props: {
|
||||
replyToText: string | null,
|
||||
inlineMessage?: boolean
|
||||
onClear?: () => void,
|
||||
className?: string,
|
||||
}) {
|
||||
return (
|
||||
<Box className={props.className} sx={!props.inlineMessage ? bubbleComposerSx : inlineMessageSx}>
|
||||
<Tooltip disableInteractive arrow title='Referring to this assistant text' placement='top'>
|
||||
<ReplyRoundedIcon sx={{
|
||||
color: props.inlineMessage ? `${INLINE_COLOR}.outlinedColor` : 'primary.solidBg',
|
||||
fontSize: 'xl',
|
||||
mt: 0.125,
|
||||
}} />
|
||||
</Tooltip>
|
||||
<Typography level='body-sm' sx={{
|
||||
flex: 1,
|
||||
ml: 1,
|
||||
mr: 0.5,
|
||||
overflow: 'auto',
|
||||
maxHeight: '5.75rem',
|
||||
lineHeight: 'xl',
|
||||
color: /*props.inlineMessage ? 'text.tertiary' :*/ 'text.secondary',
|
||||
whiteSpace: 'break-spaces', // 'balance'
|
||||
}}>
|
||||
{props.replyToText}
|
||||
</Typography>
|
||||
{!!props.onClear && (
|
||||
<IconButton size='sm' onClick={props.onClear} sx={{ my: -0.5, background: 'none' }}>
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Alert, Avatar, Box, Button, Card, CardContent, Checkbox, IconButton, Input, List, ListItem, ListItemButton, Textarea, Tooltip, Typography } from '@mui/joy';
|
||||
@@ -10,17 +11,19 @@ import EditNoteIcon from '@mui/icons-material/EditNote';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import TelegramIcon from '@mui/icons-material/Telegram';
|
||||
|
||||
import { SystemPurposeData, SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
|
||||
import { bareBonesPromptMixer } from '~/modules/persona/pmix/pmix';
|
||||
import { useChatLLM } from '~/modules/llms/store-llms';
|
||||
|
||||
import { DConversationId, useChatStore } from '~/common/state/store-chats';
|
||||
import { DConversationId, DMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { ExpanderControlledBox } from '~/common/components/ExpanderControlledBox';
|
||||
import { lineHeightTextareaMd } from '~/common/app.theme';
|
||||
import { navigateToPersonas } from '~/common/app.routes';
|
||||
import { useChipBoolean } from '~/common/components/useChipBoolean';
|
||||
import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
|
||||
import { SystemPurposeData, SystemPurposeId, SystemPurposes } from '../../../../data';
|
||||
import { YouTubeURLInput } from './YouTubeURLInput';
|
||||
import { usePurposeStore } from './store-purposes';
|
||||
|
||||
|
||||
@@ -116,6 +119,8 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const [filteredIDs, setFilteredIDs] = React.useState<SystemPurposeId[] | null>(null);
|
||||
const [editMode, setEditMode] = React.useState(false);
|
||||
const [isYouTubeTranscriberActive, setIsYouTubeTranscriberActive] = React.useState(false);
|
||||
|
||||
|
||||
// external state
|
||||
const showFinder = useUIPreferencesStore(state => state.showPersonaFinder);
|
||||
@@ -153,11 +158,52 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
|
||||
// Handlers
|
||||
|
||||
// Modify the handlePurposeChanged function to check for the YouTube Transcriber
|
||||
const handlePurposeChanged = React.useCallback((purposeId: SystemPurposeId | null) => {
|
||||
if (purposeId && setSystemPurposeId)
|
||||
setSystemPurposeId(props.conversationId, purposeId);
|
||||
if (purposeId) {
|
||||
if (purposeId === 'YouTubeTranscriber') {
|
||||
// If the YouTube Transcriber tile is clicked, set the state accordingly
|
||||
setIsYouTubeTranscriberActive(true);
|
||||
} else {
|
||||
setIsYouTubeTranscriberActive(false);
|
||||
}
|
||||
if (setSystemPurposeId) {
|
||||
setSystemPurposeId(props.conversationId, purposeId);
|
||||
}
|
||||
}
|
||||
}, [props.conversationId, setSystemPurposeId]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const isTranscriberActive = systemPurposeId === 'YouTubeTranscriber';
|
||||
setIsYouTubeTranscriberActive(isTranscriberActive);
|
||||
}, [systemPurposeId]);
|
||||
|
||||
|
||||
// Implement handleAddMessage function
|
||||
const handleAddMessage = (messageText: string) => {
|
||||
// Retrieve the appendMessage action from the useChatStore
|
||||
const { appendMessage } = useChatStore.getState();
|
||||
|
||||
const conversationId = props.conversationId;
|
||||
|
||||
// Create a new message object
|
||||
const newMessage: DMessage = {
|
||||
id: uuidv4(),
|
||||
text: messageText,
|
||||
sender: 'Bot',
|
||||
avatar: null,
|
||||
typing: false,
|
||||
role: 'assistant' as 'assistant',
|
||||
tokenCount: 0,
|
||||
created: Date.now(),
|
||||
updated: null,
|
||||
};
|
||||
|
||||
// Append the new message to the conversation
|
||||
appendMessage(conversationId, newMessage);
|
||||
};
|
||||
|
||||
|
||||
const handleCustomSystemMessageChange = React.useCallback((v: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
// TODO: persist this change? Right now it's reset every time.
|
||||
// maybe we shall have a "save" button just save on a state to persist between sessions
|
||||
@@ -418,6 +464,17 @@ export function PersonaSelector(props: { conversationId: DConversationId, runExa
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* [row -1] YouTube URL */}
|
||||
{isYouTubeTranscriberActive && (
|
||||
<YouTubeURLInput
|
||||
onSubmit={(url) => handleAddMessage(url)}
|
||||
isFetching={false}
|
||||
sx={{
|
||||
gridColumn: '1 / -1',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</Box>
|
||||
|
||||
</Box>
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, Input } from '@mui/joy';
|
||||
import YouTubeIcon from '@mui/icons-material/YouTube';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { useYouTubeTranscript, YTVideoTranscript } from '~/modules/youtube/useYouTubeTranscript';
|
||||
|
||||
|
||||
interface YouTubeURLInputProps {
|
||||
onSubmit: (transcript: string) => void;
|
||||
isFetching: boolean;
|
||||
sx?: SxProps;
|
||||
}
|
||||
|
||||
export const YouTubeURLInput: React.FC<YouTubeURLInputProps> = ({ onSubmit, isFetching, sx }) => {
|
||||
const [url, setUrl] = React.useState('');
|
||||
const [submitFlag, setSubmitFlag] = React.useState(false);
|
||||
|
||||
// Function to extract video ID from URL
|
||||
function extractVideoID(videoURL: string): string | null {
|
||||
const regExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|embed\/)|youtu\.be\/)([^#&?]*).*/;
|
||||
const match = videoURL.match(regExp);
|
||||
return (match && match[1]?.length == 11) ? match[1] : null;
|
||||
}
|
||||
|
||||
const videoID = extractVideoID(url);
|
||||
|
||||
// Callback function to handle new transcript
|
||||
const handleNewTranscript = (newTranscript: YTVideoTranscript) => {
|
||||
onSubmit(newTranscript.transcript); // Pass the transcript text to the onSubmit handler
|
||||
setSubmitFlag(false); // Reset submit flag after handling
|
||||
};
|
||||
|
||||
const { transcript, isFetching: isTranscriptFetching, isError, error } = useYouTubeTranscript(videoID && submitFlag ? videoID : null, handleNewTranscript);
|
||||
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUrl(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault(); // Prevent form from causing a page reload
|
||||
setSubmitFlag(true); // Set flag to indicate a submit action
|
||||
};
|
||||
|
||||
return (
|
||||
<Box sx={{ mb: 1, ...sx }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
required
|
||||
type='url'
|
||||
fullWidth
|
||||
disabled={isFetching || isTranscriptFetching}
|
||||
variant='outlined'
|
||||
placeholder='Enter YouTube Video URL'
|
||||
value={url}
|
||||
onChange={handleChange}
|
||||
startDecorator={<YouTubeIcon sx={{ color: '#f00' }} />}
|
||||
sx={{ mb: 1.5, backgroundColor: 'background.popup' }}
|
||||
/>
|
||||
<Button
|
||||
type='submit'
|
||||
variant='solid'
|
||||
disabled={isFetching || isTranscriptFetching || !url}
|
||||
loading={isFetching || isTranscriptFetching}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
Get Transcript
|
||||
</Button>
|
||||
{isError && <div>Error fetching transcript. Please try again.</div>}
|
||||
</form>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -18,7 +18,7 @@ export const usePurposeStore = create<PurposeStore>()(
|
||||
(set) => ({
|
||||
|
||||
// default state
|
||||
hiddenPurposeIDs: ['Developer', 'Designer'],
|
||||
hiddenPurposeIDs: ['Developer', 'Designer', 'YouTubeTranscriber'],
|
||||
|
||||
toggleHiddenPurposeId: (purposeId: string) => {
|
||||
set(state => {
|
||||
@@ -37,14 +37,19 @@ export const usePurposeStore = create<PurposeStore>()(
|
||||
|
||||
/* versioning:
|
||||
* 1: hide 'Developer' as 'DeveloperPreview' is best
|
||||
* 2: add a hidden 'YouTubeTranscriber' purpose
|
||||
*/
|
||||
version: 1,
|
||||
version: 2,
|
||||
|
||||
migrate: (state: any, fromVersion: number): PurposeStore => {
|
||||
// 0 -> 1: rename 'enterToSend' to 'enterIsNewline' (flip the meaning)
|
||||
if (state && fromVersion === 0)
|
||||
if (!state.hiddenPurposeIDs.includes('Developer'))
|
||||
state.hiddenPurposeIDs.push('Developer');
|
||||
// 1 -> 2: add a hidden 'YouTubeTranscriber' purpose
|
||||
if (state && fromVersion === 1)
|
||||
if (!state.hiddenPurposeIDs.includes('YouTubeTranscriber'))
|
||||
state.hiddenPurposeIDs.push('YouTubeTranscriber');
|
||||
return state;
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { getChatLLMId } from '~/modules/llms/store-llms';
|
||||
import { updateHistoryForReplyTo } from '~/modules/aifn/replyto/replyTo';
|
||||
|
||||
import { ConversationsManager } from '~/common/chats/ConversationsManager';
|
||||
import { createDMessage, DConversationId, DMessage, getConversationSystemPurposeId } from '~/common/state/store-chats';
|
||||
import { getUXLabsHighPerformance } from '~/common/state/store-ux-labs';
|
||||
|
||||
import { extractChatCommand, findAllChatCommands } from '../commands/commands.registry';
|
||||
import { getInstantAppChatPanesCount } from '../components/panes/usePanesManager';
|
||||
|
||||
import { runAssistantUpdatingState } from './chat-stream';
|
||||
import { runBrowseGetPageUpdatingState } from './browse-load';
|
||||
import { runImageGenerationUpdatingState } from './image-generate';
|
||||
import { runReActUpdatingState } from './react-tangent';
|
||||
|
||||
import type { ChatModeId } from '../AppChat';
|
||||
|
||||
|
||||
export async function _handleExecute(chatModeId: ChatModeId, conversationId: DConversationId, history: DMessage[]) {
|
||||
|
||||
// Handle missing conversation
|
||||
if (!conversationId)
|
||||
return 'err-no-conversation';
|
||||
|
||||
const chatLLMId = getChatLLMId();
|
||||
|
||||
// Update the system message from the active persona to the history
|
||||
// NOTE: this does NOT call setMessages anymore (optimization). make sure to:
|
||||
// 1. all the callers need to pass a new array
|
||||
// 2. all the exit points need to call setMessages
|
||||
const cHandler = ConversationsManager.getHandler(conversationId);
|
||||
cHandler.inlineUpdatePurposeInHistory(history, chatLLMId || undefined);
|
||||
|
||||
// FIXME: shouldn't do this for all the code paths. The advantage for having it here (vs Composer output only) is re-executing history
|
||||
// TODO: move this to the server side after transferring metadata?
|
||||
updateHistoryForReplyTo(history);
|
||||
|
||||
// Handle unconfigured
|
||||
if (!chatLLMId || !chatModeId) {
|
||||
// set the history (e.g. the updated system prompt and the user prompt) at least, see #523
|
||||
cHandler.messagesReplace(history);
|
||||
return !chatLLMId ? 'err-no-chatllm' : 'err-no-chatmode';
|
||||
}
|
||||
|
||||
// Valid /commands are intercepted here, and override chat modes, generally for mechanics or sidebars
|
||||
const lastMessage = history.length > 0 ? history[history.length - 1] : null;
|
||||
if (lastMessage?.role === 'user') {
|
||||
const chatCommand = extractChatCommand(lastMessage.text)[0];
|
||||
if (chatCommand && chatCommand.type === 'cmd') {
|
||||
switch (chatCommand.providerId) {
|
||||
case 'ass-browse':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runBrowseGetPageUpdatingState(cHandler, chatCommand.params);
|
||||
|
||||
case 'ass-t2i':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runImageGenerationUpdatingState(cHandler, chatCommand.params);
|
||||
|
||||
case 'ass-react':
|
||||
cHandler.messagesReplace(history); // show command
|
||||
return await runReActUpdatingState(cHandler, chatCommand.params, chatLLMId);
|
||||
|
||||
case 'chat-alter':
|
||||
// /clear
|
||||
if (chatCommand.command === '/clear') {
|
||||
if (chatCommand.params === 'all') {
|
||||
cHandler.messagesReplace([]);
|
||||
} else {
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Issue: this command requires the \'all\' parameter to confirm the operation.', undefined, 'issue', false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// /assistant, /system
|
||||
Object.assign(lastMessage, {
|
||||
role: chatCommand.command.startsWith('/s') ? 'system' : chatCommand.command.startsWith('/a') ? 'assistant' : 'user',
|
||||
sender: 'Bot',
|
||||
text: chatCommand.params || '',
|
||||
} satisfies Partial<DMessage>);
|
||||
cHandler.messagesReplace(history);
|
||||
return true;
|
||||
|
||||
case 'cmd-help':
|
||||
const chatCommandsText = findAllChatCommands()
|
||||
.map(cmd => ` - ${cmd.primary}` + (cmd.alternatives?.length ? ` (${cmd.alternatives.join(', ')})` : '') + `: ${cmd.description}`)
|
||||
.join('\n');
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Available Chat Commands:\n' + chatCommandsText, undefined, 'help', false);
|
||||
return true;
|
||||
|
||||
case 'mode-beam':
|
||||
if (chatCommand.isError) {
|
||||
cHandler.messagesReplace(history);
|
||||
return false;
|
||||
}
|
||||
// remove '/beam ', as we want to be a user chat message
|
||||
Object.assign(lastMessage, { text: chatCommand.params || '' });
|
||||
cHandler.messagesReplace(history);
|
||||
ConversationsManager.getHandler(conversationId).beamInvoke(history, [], null);
|
||||
return true;
|
||||
|
||||
default:
|
||||
cHandler.messagesReplace([...history, createDMessage('assistant', 'This command is not supported.')]);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// get the system purpose (note: we don't react to it, or it would invalidate half UI components..)
|
||||
if (!getConversationSystemPurposeId(conversationId)) {
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.messageAppendAssistant('Issue: no Persona selected.', undefined, 'issue', false);
|
||||
return 'err-no-persona';
|
||||
}
|
||||
|
||||
// synchronous long-duration tasks, which update the state as they go
|
||||
switch (chatModeId) {
|
||||
case 'generate-text':
|
||||
cHandler.messagesReplace(history);
|
||||
return await runAssistantUpdatingState(conversationId, history, chatLLMId, getUXLabsHighPerformance() ? 0 : getInstantAppChatPanesCount());
|
||||
|
||||
case 'generate-text-beam':
|
||||
cHandler.messagesReplace(history);
|
||||
cHandler.beamInvoke(history, [], null);
|
||||
return true;
|
||||
|
||||
case 'append-user':
|
||||
cHandler.messagesReplace(history);
|
||||
return true;
|
||||
|
||||
case 'generate-image':
|
||||
if (!lastMessage?.text) break;
|
||||
// also add a 'fake' user message with the '/draw' command
|
||||
cHandler.messagesReplace(history.map(message => (message.id !== lastMessage.id) ? message : {
|
||||
...message,
|
||||
text: `/draw ${lastMessage.text}`,
|
||||
}));
|
||||
return await runImageGenerationUpdatingState(cHandler, lastMessage.text);
|
||||
|
||||
case 'generate-react':
|
||||
if (!lastMessage?.text) break;
|
||||
cHandler.messagesReplace(history);
|
||||
return await runReActUpdatingState(cHandler, lastMessage.text, chatLLMId);
|
||||
}
|
||||
|
||||
// ISSUE: if we're here, it means we couldn't do the job, at least sync the history
|
||||
console.log('Chat execute: issue running', chatModeId, conversationId, lastMessage);
|
||||
cHandler.messagesReplace(history);
|
||||
return false;
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import type { ConversationHandler } from '~/common/chats/ConversationHandler';
|
||||
export const runBrowseGetPageUpdatingState = async (cHandler: ConversationHandler, url?: string) => {
|
||||
if (!url) {
|
||||
cHandler.messageAppendAssistant('Issue: no URL provided.', undefined, 'issue', false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// noinspection HttpUrlsUsage
|
||||
@@ -15,9 +15,12 @@ export const runBrowseGetPageUpdatingState = async (cHandler: ConversationHandle
|
||||
|
||||
try {
|
||||
const page = await callBrowseFetchPage(url);
|
||||
cHandler.messageEdit(assistantMessageId, { text: page.content || 'Issue: page load did not produce an answer: no text found', typing: false }, true);
|
||||
const pageContent = page.content.markdown || page.content.text || page.content.html || 'Issue: page load did not produce an answer: no text found';
|
||||
cHandler.messageEdit(assistantMessageId, { text: pageContent, typing: false }, true);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
cHandler.messageEdit(assistantMessageId, { text: 'Issue: browse did not produce an answer (error: ' + (error?.message || error?.toString() || 'unknown') + ').', typing: false }, true);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import type { DLLMId } from '~/modules/llms/store-llms';
|
||||
import type { StreamingClientUpdate } from '~/modules/llms/vendors/unifiedStreamingClient';
|
||||
import { autoSuggestions } from '~/modules/aifn/autosuggestions/autoSuggestions';
|
||||
import { conversationAutoTitle } from '~/modules/aifn/autotitle/autoTitle';
|
||||
import { llmStreamingChatGenerate, VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
import { llmStreamingChatGenerate, VChatContextRef, VChatMessageIn, VChatStreamContextName } from '~/modules/llms/llm.client';
|
||||
import { speakText } from '~/modules/elevenlabs/elevenlabs.client';
|
||||
|
||||
import type { DMessage } from '~/common/state/store-chats';
|
||||
@@ -31,9 +31,11 @@ export async function runAssistantUpdatingState(conversationId: string, history:
|
||||
cHandler.setAbortController(abortController);
|
||||
|
||||
// stream the assistant's messages
|
||||
await streamAssistantMessage(
|
||||
const messageStatus = await streamAssistantMessage(
|
||||
assistantLlmId,
|
||||
history.map((m): VChatMessageIn => ({ role: m.role, content: m.text })),
|
||||
'conversation',
|
||||
conversationId,
|
||||
parallelViewCount,
|
||||
autoSpeak,
|
||||
(update) => cHandler.messageEdit(assistantMessageId, update, false),
|
||||
@@ -41,6 +43,7 @@ export async function runAssistantUpdatingState(conversationId: string, history:
|
||||
);
|
||||
|
||||
// clear to send, again
|
||||
// FIXME: race condition?
|
||||
cHandler.setAbortController(null);
|
||||
|
||||
if (autoTitleChat) {
|
||||
@@ -50,6 +53,8 @@ export async function runAssistantUpdatingState(conversationId: string, history:
|
||||
|
||||
if (autoSuggestDiagrams || autoSuggestQuestions)
|
||||
autoSuggestions(conversationId, assistantMessageId, autoSuggestDiagrams, autoSuggestQuestions);
|
||||
|
||||
return messageStatus.outcome === 'success';
|
||||
}
|
||||
|
||||
type StreamMessageOutcome = 'success' | 'aborted' | 'errored';
|
||||
@@ -58,6 +63,8 @@ type StreamMessageStatus = { outcome: StreamMessageOutcome, errorMessage?: strin
|
||||
export async function streamAssistantMessage(
|
||||
llmId: DLLMId,
|
||||
messagesHistory: VChatMessageIn[],
|
||||
contextName: VChatStreamContextName,
|
||||
contextRef: VChatContextRef,
|
||||
throttleUnits: number, // 0: disable, 1: default throttle (12Hz), 2+ reduce the message frequency with the square root
|
||||
autoSpeak: ChatAutoSpeakType,
|
||||
editMessage: (update: Partial<DMessage>) => void,
|
||||
@@ -89,7 +96,7 @@ export async function streamAssistantMessage(
|
||||
const incrementalAnswer: Partial<DMessage> = { text: '' };
|
||||
|
||||
try {
|
||||
await llmStreamingChatGenerate(llmId, messagesHistory, null, null, abortSignal, (update: StreamingClientUpdate) => {
|
||||
await llmStreamingChatGenerate(llmId, messagesHistory, contextName, contextRef, null, null, abortSignal, (update: StreamingClientUpdate) => {
|
||||
const textSoFar = update.textSoFar;
|
||||
|
||||
// grow the incremental message
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { TextToImageProvider } from '~/common/components/useCapabilities';
|
||||
export async function runImageGenerationUpdatingState(cHandler: ConversationHandler, imageText?: string) {
|
||||
if (!imageText) {
|
||||
cHandler.messageAppendAssistant('Issue: no image description provided.', undefined, 'issue', false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Acquire the active TextToImageProvider
|
||||
@@ -19,7 +19,7 @@ export async function runImageGenerationUpdatingState(cHandler: ConversationHand
|
||||
t2iProvider = getActiveTextToImageProviderOrThrow();
|
||||
} catch (error: any) {
|
||||
cHandler.messageAppendAssistant(`[Issue] Sorry, I can't generate images right now. ${error?.message || error?.toString() || 'Unknown error'}.`, undefined, 'issue', false);
|
||||
return;
|
||||
return 'err-t2i-unconfigured';
|
||||
}
|
||||
|
||||
// if the imageText ends with " xN" or " [N]" (where N is a number), then we'll generate N images
|
||||
@@ -36,8 +36,10 @@ export async function runImageGenerationUpdatingState(cHandler: ConversationHand
|
||||
try {
|
||||
const imageUrls = await t2iGenerateImageOrThrow(t2iProvider, imageText, repeat);
|
||||
cHandler.messageEdit(assistantMessageId, { text: imageUrls.join('\n'), typing: false }, true);
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
const errorMessage = error?.message || error?.toString() || 'Unknown error';
|
||||
cHandler.messageEdit(assistantMessageId, { text: `[Issue] Sorry, I couldn't create an image for you. ${errorMessage}`, typing: false }, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,11 @@ const EPHEMERAL_DELETION_DELAY = 5 * 1000;
|
||||
export async function runReActUpdatingState(cHandler: ConversationHandler, question: string | undefined, assistantLlmId: DLLMId) {
|
||||
if (!question) {
|
||||
cHandler.messageAppendAssistant('Issue: no question provided.', undefined, 'issue', false);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// create a blank and 'typing' message for the assistant - to be filled when we're done
|
||||
const assistantModelLabel = 'react-' + assistantLlmId.slice(4, 7); // HACK: this is used to change the Avatar animation
|
||||
const assistantModelLabel = 'react-' + assistantLlmId; //.slice(4, 7); // HACK: this is used to change the Avatar animation
|
||||
const assistantMessageId = cHandler.messageAppendAssistant(STREAM_TEXT_INDICATOR, undefined, assistantModelLabel, true);
|
||||
const { enableReactTool: enableBrowse } = useBrowseStore.getState();
|
||||
|
||||
@@ -42,9 +42,11 @@ export async function runReActUpdatingState(cHandler: ConversationHandler, quest
|
||||
cHandler.messageEdit(assistantMessageId, { text: reactResult, typing: false }, false);
|
||||
setTimeout(() => eHandler.delete(), EPHEMERAL_DELETION_DELAY);
|
||||
|
||||
return true;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
logToEphemeral(ephemeralText + `\nIssue: ${error || 'unknown'}`);
|
||||
cHandler.messageEdit(assistantMessageId, { text: 'Issue: ReAct did not produce an answer.', typing: false }, false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
+22
-19
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
import NextImage from 'next/image';
|
||||
import TimeAgo from 'react-timeago';
|
||||
|
||||
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, Grid, IconButton, Typography } from '@mui/joy';
|
||||
import { AspectRatio, Box, Button, Card, CardContent, CardOverflow, Container, Grid, Typography } from '@mui/joy';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
|
||||
@@ -17,7 +17,8 @@ import { beamNewsCallout } from './beam.data';
|
||||
|
||||
|
||||
// number of news items to show by default, before the expander
|
||||
const DEFAULT_NEWS_COUNT = 4;
|
||||
const NEWS_INITIAL_COUNT = 3;
|
||||
const NEWS_LOAD_STEP = 2;
|
||||
|
||||
|
||||
export const newsRoadmapCallout =
|
||||
@@ -54,12 +55,15 @@ export const newsRoadmapCallout =
|
||||
|
||||
export function AppNews() {
|
||||
// state
|
||||
const [lastNewsIdx, setLastNewsIdx] = React.useState<number>(DEFAULT_NEWS_COUNT - 1);
|
||||
const [lastNewsIdx, setLastNewsIdx] = React.useState<number>(NEWS_INITIAL_COUNT - 1);
|
||||
|
||||
// news selection
|
||||
const news = NewsItems.filter((_, idx) => idx <= lastNewsIdx);
|
||||
const firstNews = news[0] ?? null;
|
||||
|
||||
// show expander
|
||||
const canExpand = news.length < NewsItems.length;
|
||||
|
||||
return (
|
||||
|
||||
<Box sx={{
|
||||
@@ -103,13 +107,11 @@ export function AppNews() {
|
||||
<Container disableGutters maxWidth='sm'>
|
||||
{news?.map((ni, idx) => {
|
||||
// const firstCard = idx === 0;
|
||||
const hasCardAfter = news.length < NewsItems.length;
|
||||
const showExpander = hasCardAfter && (idx === news.length - 1);
|
||||
const addPadding = false; //!firstCard; // || showExpander;
|
||||
return <React.Fragment key={idx}>
|
||||
|
||||
{/* Inject the Beam item here*/}
|
||||
{idx === 0 && (
|
||||
{idx === 2 && (
|
||||
<Box sx={{ mb: 3 }}>
|
||||
{beamNewsCallout}
|
||||
</Box>
|
||||
@@ -150,19 +152,6 @@ export function AppNews() {
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{showExpander && (
|
||||
<IconButton
|
||||
variant='solid'
|
||||
onClick={() => setLastNewsIdx(idx + 1)}
|
||||
sx={{
|
||||
position: 'absolute', right: 0, bottom: 0, mr: -1, mb: -1,
|
||||
// backgroundColor: 'background.surface',
|
||||
borderRadius: '50%',
|
||||
}}
|
||||
>
|
||||
<ExpandMoreIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{!!ni.versionCoverImage && (
|
||||
@@ -181,6 +170,7 @@ export function AppNews() {
|
||||
</AspectRatio>
|
||||
</CardOverflow>
|
||||
)}
|
||||
|
||||
</Card>
|
||||
|
||||
{/* Inject the roadmap item here*/}
|
||||
@@ -192,6 +182,19 @@ export function AppNews() {
|
||||
|
||||
</React.Fragment>;
|
||||
})}
|
||||
|
||||
{canExpand && (
|
||||
<Button
|
||||
fullWidth
|
||||
variant='soft'
|
||||
color='neutral'
|
||||
onClick={() => setLastNewsIdx(index => index + NEWS_LOAD_STEP)}
|
||||
endDecorator={<ExpandMoreIcon />}
|
||||
>
|
||||
Previous News
|
||||
</Button>
|
||||
)}
|
||||
|
||||
</Container>
|
||||
|
||||
{/*<Typography sx={{ textAlign: 'center' }}>*/}
|
||||
|
||||
@@ -14,7 +14,7 @@ export const beamNewsCallout =
|
||||
<Card variant='solid' invertedColors>
|
||||
<CardContent sx={{ gap: 2 }}>
|
||||
<Typography level='title-lg'>
|
||||
Beam - just launched in 1.15
|
||||
Beam - launched in 1.15
|
||||
</Typography>
|
||||
<Typography level='body-sm'>
|
||||
Beam is a world-first, multi-model AI chat modality that accelerates the discovery of superior solutions by leveraging the collective strengths of diverse LLMs.
|
||||
|
||||
+48
-33
@@ -1,14 +1,12 @@
|
||||
import * as React from 'react';
|
||||
import { StaticImageData } from 'next/image';
|
||||
|
||||
import { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Chip, SvgIconProps, Typography } from '@mui/joy';
|
||||
import AutoStoriesOutlinedIcon from '@mui/icons-material/AutoStoriesOutlined';
|
||||
import GoogleIcon from '@mui/icons-material/Google';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
|
||||
import { AnthropicIcon } from '~/common/components/icons/vendors/AnthropicIcon';
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { ExternalLink } from '~/common/components/ExternalLink';
|
||||
import { GroqIcon } from '~/common/components/icons/vendors/GroqIcon';
|
||||
import { LocalAIIcon } from '~/common/components/icons/vendors/LocalAIIcon';
|
||||
import { MistralIcon } from '~/common/components/icons/vendors/MistralIcon';
|
||||
@@ -19,8 +17,12 @@ import { Link } from '~/common/components/Link';
|
||||
import { clientUtmSource } from '~/common/util/pwaUtils';
|
||||
import { platformAwareKeystrokes } from '~/common/components/KeyStroke';
|
||||
|
||||
import { beamBlogUrl } from './beam.data';
|
||||
|
||||
|
||||
// Cover Images
|
||||
// A landscape image of a capybara made entirely of clear, translucent crystal, wearing oversized black sunglasses, sitting at a sleek, minimalist desk. The desk is bathed in a soft, ethereal light emanating from within the capybara, symbolizing clarity and transparency. The capybara is typing on a futuristic, holographic keyboard, with floating code snippets and diagrams surrounding it, illustrating an improved developer experience and Auto-Diagrams feature. The background is a clean, white space with subtle, geometric patterns. Close-up photography style with a bokeh effect.
|
||||
import coverV116 from '../../../public/images/covers/release-cover-v1.16.0.png';
|
||||
// (not exactly) Imagine a futuristic, holographically bounded space. Inside this space, four capybaras stand. Three of them are in various stages of materialization, their forms made up of thousands of tiny, vibrant particles of electric blues, purples, and greens. These particles represent the merging of different intelligent inputs, symbolizing the concept of 'Beaming'. Positioned slightly towards the center and ahead of the others, the fourth capybara is fully materialized and composed of shimmering golden cotton candy, representing the optimal solution the 'Beam' feature seeks to achieve. The golden capybara gazes forward confidently, embodying a target achieved. Illuminated grid lines softly glow on the floor and walls of the setting, amplifying the futuristic aspect. In front of the golden capybara, floating, holographic interfaces depict complex networks of points and lines symbolizing the solution space 'Beaming' explores. The capybara interacts with these interfaces, implying the user's ability to control and navigate towards the best outcomes.
|
||||
import coverV115 from '../../../public/images/covers/release-cover-v1.15.0.png';
|
||||
// An image of a capybara sculpted entirely from iridescent blue cotton candy, gazing into a holographic galaxy of floating AI model icons (representing various AI models like Perplexity, Groq, etc.). The capybara is wearing a lightweight, futuristic headset, and its paws are gesturing as if orchestrating the movement of the models in the galaxy. The backdrop is minimalist, with occasional bursts of neon light beams, creating a sense of depth and wonder. Close-up photography, bokeh effect, with a dark but vibrant background to make the colors pop.
|
||||
@@ -29,7 +31,6 @@ import coverV114 from '../../../public/images/covers/release-cover-v1.14.0.png';
|
||||
import coverV113 from '../../../public/images/covers/release-cover-v1.13.0.png';
|
||||
// An image of a capybara sculpted entirely from black cotton candy, set against a minimalist backdrop with splashes of bright, contrasting sparkles. The capybara is calling on a 3D origami old-school pink telephone and the camera is zooming on the telephone. Close up photography, bokeh, white background.
|
||||
import coverV112 from '../../../public/images/covers/release-cover-v1.12.0.png';
|
||||
import { beamBlogUrl, beamReleaseDate } from './beam.data';
|
||||
|
||||
|
||||
interface NewsItem {
|
||||
@@ -51,30 +52,54 @@ interface NewsItem {
|
||||
// news and feature surfaces
|
||||
export const NewsItems: NewsItem[] = [
|
||||
/*{
|
||||
versionCode: '1.16.0',
|
||||
versionCode: '1.17.0',
|
||||
items: [
|
||||
Screen Capture (when removed from labs)
|
||||
Auto-Merge
|
||||
Draw
|
||||
...
|
||||
Screen Capture (when removed from labs)
|
||||
]
|
||||
}*/
|
||||
{
|
||||
versionCode: '1.15.0',
|
||||
versionCode: '1.16.3',
|
||||
versionName: 'Crystal Clear',
|
||||
versionDate: new Date('2024-06-07T05:00:00Z'),
|
||||
// versionDate: new Date('2024-05-13T19:00:00Z'),
|
||||
// versionDate: new Date('2024-05-09T00:00:00Z'),
|
||||
versionCoverImage: coverV116,
|
||||
items: [
|
||||
{ text: <><B href={beamBlogUrl} wow>Beam</B> core and UX improvements based on user feedback</>, issue: 470, icon: ChatBeamIcon },
|
||||
{ text: <>Chat <B>Cost estimation</B> with supported models* 💰</> },
|
||||
{ text: <>Major <B>Auto-Diagrams</B> enhancements</> },
|
||||
{ text: <>Save/load chat files with Ctrl+S / O</>, issue: 466 },
|
||||
{ text: <><B issue={500}>YouTube Transcriber</B> persona: chat with videos</>, issue: 500 },
|
||||
{ text: <>Improved <B issue={508}>formula render</B>, dark-mode diagrams</>, issue: 508 },
|
||||
{ text: <>More: <B issue={517}>code soft-wrap</B>, selection toolbar, <B issue={507}>3x faster</B> on Apple silicon</>, issue: 507 },
|
||||
{ text: <>Updated <B>Anthropic</B>*, <B>Groq</B>, <B>Ollama</B>, <B>OpenAI</B>*, <B>OpenRouter</B>*, and <B>Perplexity</B></> },
|
||||
{ text: <>Developers: update LLMs data structures</>, dev: true },
|
||||
{ text: <>1.16.1: Support for <B>OpenAI</B> <B href='https://openai.com/index/hello-gpt-4o/'>GPT-4o</B></> },
|
||||
{ text: <>1.16.2: Proper <B>Gemini</B> support, <B>HTML/Markdown</B> downloads, and latest <B>Mistral</B></> },
|
||||
{ text: <>1.16.3: Support for <B href='https://www.anthropic.com/news/claude-3-5-sonnet'>Claude 3.5 Sonnet</B> (refresh your <B>Anthropic</B> models)</> },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.15',
|
||||
versionName: 'Beam',
|
||||
versionDate: new Date(beamReleaseDate),
|
||||
versionDate: new Date('2024-04-10T08:00:00Z'),
|
||||
versionCoverImage: coverV115,
|
||||
items: [
|
||||
{ text: <><B href={beamBlogUrl} wow>Beam</B>: Find better answers with multi-model AI reasoning</>, issue: 443, icon: ChatBeamIcon },
|
||||
{ text: <><B>Explore diverse perspectives</B> and <B>synthesize optimal responses</B></>, noBullet: true },
|
||||
// { text: <><B>Explore diverse perspectives</B> and <B>synthesize optimal responses</B></>, noBullet: true },
|
||||
{ text: <><B issue={436}>Auto-configure</B> models for managed deployments</>, issue: 436 },
|
||||
{ text: <>Message <B issue={476}>starring ⭐</B>, filtering and attachment</>, issue: 476 },
|
||||
{ text: <>Default persona improvements</> },
|
||||
{ text: <>Fixes to Gemini models and SVGs, improvements to UI and icons, and more</> },
|
||||
{ text: <>Developers: imperative LLM models discovery</>, dev: true },
|
||||
{ text: <>1.15.1: Support for <B>Gemini Pro 1.5</B> and <B>OpenAI 2024-04-09</B> models</> },
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.14.1',
|
||||
versionCode: '1.14',
|
||||
versionName: 'Modelmorphic',
|
||||
versionCoverImage: coverV114,
|
||||
versionDate: new Date('2024-03-07T08:00:00Z'),
|
||||
@@ -93,7 +118,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.13.0',
|
||||
versionCode: '1.13',
|
||||
versionName: 'Multi + Mind',
|
||||
versionMoji: '🧠🔀',
|
||||
versionDate: new Date('2024-02-08T07:47:00Z'),
|
||||
@@ -109,7 +134,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.12.0',
|
||||
versionCode: '1.12',
|
||||
versionName: 'AGI Hotline',
|
||||
versionMoji: '✨🗣️',
|
||||
versionDate: new Date('2024-01-26T12:30:00Z'),
|
||||
@@ -128,7 +153,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.11.0',
|
||||
versionCode: '1.11',
|
||||
versionName: 'Singularity',
|
||||
versionMoji: '🌌🌠',
|
||||
versionDate: new Date('2024-01-16T06:30:00Z'),
|
||||
@@ -142,7 +167,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.10.0',
|
||||
versionCode: '1.10',
|
||||
versionName: 'The Year of AGI',
|
||||
// versionMoji: '🎊✨',
|
||||
versionDate: new Date('2024-01-06T08:00:00Z'),
|
||||
@@ -156,7 +181,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.9.0',
|
||||
versionCode: '1.9',
|
||||
versionName: 'Creative Horizons',
|
||||
// versionMoji: '🎨🌌',
|
||||
versionDate: new Date('2023-12-28T22:30:00Z'),
|
||||
@@ -171,7 +196,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.8.0',
|
||||
versionCode: '1.8',
|
||||
versionName: 'To The Moon And Back',
|
||||
// versionMoji: '🚀🌕🔙❤️',
|
||||
versionDate: new Date('2023-12-20T09:30:00Z'),
|
||||
@@ -188,7 +213,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.7.0',
|
||||
versionCode: '1.7',
|
||||
versionName: 'Attachment Theory',
|
||||
// versionDate: new Date('2023-12-11T06:00:00Z'), // 1.7.3
|
||||
versionDate: new Date('2023-12-10T12:00:00Z'), // 1.7.0
|
||||
@@ -204,7 +229,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.6.0',
|
||||
versionCode: '1.6',
|
||||
versionName: 'Surf\'s Up',
|
||||
versionDate: new Date('2023-11-28T21:00:00Z'),
|
||||
items: [
|
||||
@@ -219,7 +244,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.5.0',
|
||||
versionCode: '1.5',
|
||||
versionName: 'Loaded!',
|
||||
versionDate: new Date('2023-11-19T21:00:00Z'),
|
||||
items: [
|
||||
@@ -235,7 +260,7 @@ export const NewsItems: NewsItem[] = [
|
||||
],
|
||||
},
|
||||
{
|
||||
versionCode: '1.4.0',
|
||||
versionCode: '1.4',
|
||||
items: [
|
||||
{ text: <><B>Share and clone</B> conversations, with public links</> },
|
||||
{ text: <><B code='/docs/config-azure-openai.md'>Azure</B> models, incl. gpt-4-32k</> },
|
||||
@@ -277,15 +302,6 @@ export const NewsItems: NewsItem[] = [
|
||||
];
|
||||
|
||||
|
||||
const wowStyle: SxProps = {
|
||||
textDecoration: 'underline',
|
||||
textDecorationThickness: '0.4em',
|
||||
textDecorationColor: 'rgba(var(--joy-palette-primary-lightChannel) / 1)',
|
||||
// textDecorationColor: 'rgba(0 255 0 / 0.5)',
|
||||
textDecorationSkipInk: 'none',
|
||||
// textUnderlineOffset: '-0.5em',
|
||||
};
|
||||
|
||||
function B(props: {
|
||||
// one-of
|
||||
href?: string,
|
||||
@@ -299,7 +315,6 @@ function B(props: {
|
||||
props.issue ? `${Brand.URIs.OpenRepo}/issues/${props.issue}`
|
||||
: props.code ? `${Brand.URIs.OpenRepo}/blob/main/${props.code}`
|
||||
: props.href;
|
||||
const isExtIcon = !props.issue;
|
||||
const boldText = (
|
||||
<Typography component='span' color={!!href ? 'primary' : 'neutral'} sx={{ fontWeight: 'lg' }}>
|
||||
{props.children}
|
||||
@@ -308,8 +323,8 @@ function B(props: {
|
||||
if (!href)
|
||||
return boldText;
|
||||
return (
|
||||
<Link href={href + clientUtmSource()} target='_blank' sx={props.wow ? wowStyle : undefined}>
|
||||
{boldText} {isExtIcon ? <LaunchIcon sx={{ mx: 0.5, fontSize: 16 }} /> : <AutoStoriesOutlinedIcon sx={{ mx: 0.5, fontSize: 16 }} />}
|
||||
</Link>
|
||||
<ExternalLink href={href + clientUtmSource()} highlight={props.wow} icon={props.issue ? 'issue' : undefined}>
|
||||
{boldText}
|
||||
</ExternalLink>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,40 @@
|
||||
// NOTE: this is a separate file to help with bundle tracing, as it's included by the ProviderBootstrapLogic (i.e. by All pages)
|
||||
|
||||
// update this variable every time you want to broadcast a new version to clients
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { useAppStateStore } from '~/common/state/store-appstate';
|
||||
|
||||
|
||||
export const incrementalNewsVersion: number = 15;
|
||||
// update this variable every time you want to broadcast a new version to clients
|
||||
export const incrementalNewsVersion: number = 16.1; // not notifying for 16.3
|
||||
|
||||
|
||||
interface NewsState {
|
||||
lastSeenNewsVersion: number;
|
||||
}
|
||||
|
||||
export const useAppNewsStateStore = create<NewsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
lastSeenNewsVersion: 0,
|
||||
}),
|
||||
{
|
||||
name: 'app-news',
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
export function shallRedirectToNews() {
|
||||
const { usageCount, lastSeenNewsVersion } = useAppStateStore.getState();
|
||||
const { lastSeenNewsVersion } = useAppNewsStateStore.getState();
|
||||
const { usageCount } = useAppStateStore.getState();
|
||||
const isNewsOutdated = (lastSeenNewsVersion || 0) < incrementalNewsVersion;
|
||||
return isNewsOutdated && usageCount > 2;
|
||||
}
|
||||
|
||||
export function markNewsAsSeen() {
|
||||
const { setLastSeenNewsVersion } = useAppStateStore.getState();
|
||||
setLastSeenNewsVersion(incrementalNewsVersion);
|
||||
useAppNewsStateStore.setState({ lastSeenNewsVersion: incrementalNewsVersion });
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Alert, Box, Button, Card, CardContent, CircularProgress, Divider, FormLabel, Grid, IconButton, LinearProgress, Tab, tabClasses, TabList, TabPanel, Tabs, Typography } from '@mui/joy';
|
||||
import AddIcon from '@mui/icons-material/Add';
|
||||
@@ -102,8 +103,11 @@ export function Creator(props: { display: boolean }) {
|
||||
strings: editedInstructions, stringEditors: instructionEditors,
|
||||
} = useFormEditTextArray(Prompts, PromptTitles);
|
||||
|
||||
const creationChainSteps = React.useMemo(() => {
|
||||
return createChain(editedInstructions, PromptTitles);
|
||||
const { steps: creationChainSteps, id: chainId } = React.useMemo(() => {
|
||||
return {
|
||||
steps: createChain(editedInstructions, PromptTitles),
|
||||
id: uuidv4(),
|
||||
};
|
||||
}, [editedInstructions]);
|
||||
|
||||
const llmLabel = personaLlm?.label || undefined;
|
||||
@@ -122,7 +126,7 @@ export function Creator(props: { display: boolean }) {
|
||||
chainError,
|
||||
userCancelChain,
|
||||
restartChain,
|
||||
} = useLLMChain(creationChainSteps, personaLlm?.id, chainInputText ?? undefined, savePersona);
|
||||
} = useLLMChain(creationChainSteps, personaLlm?.id, chainInputText ?? undefined, savePersona, 'persona-extract', chainId);
|
||||
|
||||
|
||||
// Reset the relevant state when the selected tab changes
|
||||
@@ -187,7 +191,7 @@ export function Creator(props: { display: boolean }) {
|
||||
fontWeight: 'lg',
|
||||
},
|
||||
// first element
|
||||
'& > *:first-child': { borderTopLeftRadius: '0.5rem' },
|
||||
'& > *:first-of-type': { borderTopLeftRadius: '0.5rem' },
|
||||
}}
|
||||
>
|
||||
<Tab>From YouTube</Tab>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Box, Button, IconButton, ListItemButton, ListItemDecorator, Sheet, Tooltip, Typography } from '@mui/joy';
|
||||
import { Box, Button, IconButton, ListItemDecorator, Sheet, Tooltip } from '@mui/joy';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
|
||||
@@ -136,17 +136,28 @@ export function CreatorDrawer(props: {
|
||||
</Sheet>
|
||||
) : (
|
||||
// Create Button
|
||||
<ListItemButton
|
||||
<Button
|
||||
variant={props.selectedSimplePersonaId ? 'plain' : 'soft'}
|
||||
onClick={handleSimplePersonaUnselect}
|
||||
sx={{
|
||||
m: 2,
|
||||
|
||||
// ...PageDrawerTallItemSx,
|
||||
justifyContent: 'flex-start',
|
||||
padding: '0px 0.75rem',
|
||||
|
||||
// style
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
borderRadius: 'sm',
|
||||
'--ListItemDecorator-size': 'calc(2.5rem - 1px)', // compensate for the border
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator>
|
||||
<Diversity2Icon />
|
||||
</ListItemDecorator>
|
||||
<Typography level='title-sm' sx={!props.selectedSimplePersonaId ? { fontWeight: 'lg' } : undefined}>
|
||||
Create
|
||||
</Typography>
|
||||
</ListItemButton>
|
||||
<ListItemDecorator><Diversity2Icon /></ListItemDecorator>
|
||||
{/*<Typography level='title-sm' sx={!props.selectedSimplePersonaId ? { fontWeight: 'lg' } : undefined}>*/}
|
||||
Create
|
||||
{/*</Typography>*/}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Personas [] */}
|
||||
|
||||
@@ -200,7 +200,7 @@ export function SettingsModal(props: {
|
||||
|
||||
<TabPanel value={PreferencesTab.Tools} variant='outlined' sx={{ p: 'var(--Tabs-gap)', borderRadius: 'md' }}>
|
||||
<Topics>
|
||||
<Topic icon={<SearchIcon />} title='Browsing' startCollapsed>
|
||||
<Topic icon={<SearchIcon />} title='Browsing'>
|
||||
<BrowseSettings />
|
||||
</Topic>
|
||||
<Topic icon={<SearchIcon />} title='Google Search API' startCollapsed>
|
||||
|
||||
@@ -19,12 +19,14 @@ const shortcutsMd = platformAwareKeystrokes(`
|
||||
| Ctrl + Shift + V | Attach clipboard (better than Ctrl + V) |
|
||||
| Ctrl + M | Microphone (voice typing) |
|
||||
| **Chats** | |
|
||||
| Ctrl + Alt + Left | **Previous** chat (in history) |
|
||||
| Ctrl + Alt + Right | **Next** chat (in history) |
|
||||
| Ctrl + O | Open Chat File ... |
|
||||
| Ctrl + S | Save Chat File ... |
|
||||
| Ctrl + Alt + N | **New** chat |
|
||||
| Ctrl + Alt + X | **Reset** chat |
|
||||
| Ctrl + Alt + D | **Delete** chat |
|
||||
| Ctrl + Alt + B | **Branch** chat |
|
||||
| Ctrl + Alt + Left | **Previous** chat (in history) |
|
||||
| Ctrl + Alt + Right | **Next** chat (in history) |
|
||||
| **Settings** | |
|
||||
| Ctrl + Shift + P | ⚙️ Preferences |
|
||||
| Ctrl + Shift + M | 🧠 Models |
|
||||
|
||||
@@ -2,11 +2,11 @@ import * as React from 'react';
|
||||
|
||||
import { FormControl, Typography } from '@mui/joy';
|
||||
import AddAPhotoIcon from '@mui/icons-material/AddAPhoto';
|
||||
import LocalAtmOutlinedIcon from '@mui/icons-material/LocalAtmOutlined';
|
||||
import ScreenshotMonitorIcon from '@mui/icons-material/ScreenshotMonitor';
|
||||
import SpeedIcon from '@mui/icons-material/Speed';
|
||||
import TitleIcon from '@mui/icons-material/Title';
|
||||
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { FormLabelStart } from '~/common/components/forms/FormLabelStart';
|
||||
import { FormSwitchControl } from '~/common/components/forms/FormSwitchControl';
|
||||
import { Link } from '~/common/components/Link';
|
||||
@@ -24,48 +24,57 @@ export function UxLabsSettings() {
|
||||
const isMobile = useIsMobile();
|
||||
const {
|
||||
labsAttachScreenCapture, setLabsAttachScreenCapture,
|
||||
labsBeam, setLabsBeam,
|
||||
labsCameraDesktop, setLabsCameraDesktop,
|
||||
labsChatBarAlt, setLabsChatBarAlt,
|
||||
labsHighPerformance, setLabsHighPerformance,
|
||||
labsShowCost, setLabsShowCost,
|
||||
} = useUXLabsStore();
|
||||
|
||||
return <>
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><ChatBeamIcon color={labsBeam ? 'primary' : undefined} sx={{ mr: 0.25 }} />Chat Beam</>} description={'v1.15 · ' + (labsBeam ? 'Active' : 'Off')}
|
||||
checked={labsBeam} onChange={setLabsBeam}
|
||||
/>
|
||||
{/* 'v1.15 · ' + .. */}
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><SpeedIcon color={labsHighPerformance ? 'primary' : undefined} sx={{ mr: 0.25 }} />Performance</>} description={'v1.14 · ' + (labsHighPerformance ? 'Unlocked' : 'Default')}
|
||||
title={<><SpeedIcon sx={{ fontSize: 'lg', mr: 0.5, mb: 0.25 }} />Performance</>} description={labsHighPerformance ? 'Unlocked' : 'Default'}
|
||||
checked={labsHighPerformance} onChange={setLabsHighPerformance}
|
||||
/>
|
||||
|
||||
{DEV_MODE_SETTINGS && <FormSwitchControl
|
||||
title={<><TitleIcon color={labsChatBarAlt ? 'primary' : undefined} sx={{ mr: 0.25 }} />Chat Title</>} description={'v1.14 · ' + (labsChatBarAlt === 'title' ? 'Show Title' : 'Show Models')}
|
||||
title={<><TitleIcon sx={{ fontSize: 'lg', mr: 0.5, mb: 0.25 }} />Chat Title</>} description={labsChatBarAlt === 'title' ? 'Show Title' : 'Show Models'}
|
||||
checked={labsChatBarAlt === 'title'} onChange={(on) => setLabsChatBarAlt(on ? 'title' : false)}
|
||||
/>}
|
||||
|
||||
{!isMobile && <FormSwitchControl
|
||||
title={<><ScreenshotMonitorIcon color={labsAttachScreenCapture ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Screen Capture</>} description={'v1.13 · ' + (labsAttachScreenCapture ? 'Enabled' : 'Disabled')}
|
||||
title={<><ScreenshotMonitorIcon sx={{ fontSize: 'lg', mr: 0.5, mb: 0.25 }} /> Screen Capture</>} description={labsAttachScreenCapture ? 'Enabled' : 'Disabled'}
|
||||
checked={labsAttachScreenCapture} onChange={setLabsAttachScreenCapture}
|
||||
/>}
|
||||
|
||||
{!isMobile && <FormSwitchControl
|
||||
title={<><AddAPhotoIcon color={labsCameraDesktop ? 'primary' : undefined} sx={{ mr: 0.25 }} /> Webcam</>} description={/*'v1.8 · ' +*/ (labsCameraDesktop ? 'Enabled' : 'Disabled')}
|
||||
title={<><AddAPhotoIcon sx={{ fontSize: 'lg', mr: 0.5, mb: 0.25 }} /> Webcam Capture</>} description={/*'v1.8 · ' +*/ (labsCameraDesktop ? 'Enabled' : 'Disabled')}
|
||||
checked={labsCameraDesktop} onChange={setLabsCameraDesktop}
|
||||
/>}
|
||||
|
||||
<FormSwitchControl
|
||||
title={<><LocalAtmOutlinedIcon sx={{ fontSize: 'lg', mr: 0.5, mb: 0.25 }} />Cost of messages</>} description={labsShowCost ? 'Show when available' : 'Disabled'}
|
||||
checked={labsShowCost} onChange={setLabsShowCost}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Other Graduated (removed or backlog):
|
||||
- <Link href='https://github.com/enricoros/big-AGI/issues/359' target='_blank'>Draw App</Link>
|
||||
- Text Tools: dinamically shown where applicable (e.g. Diff)
|
||||
- Chat Mode: follow-ups; moved to Chat Advanced UI
|
||||
*/}
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormLabelStart title='Graduated' description='Ex-labs' />
|
||||
<Typography level='body-xs'>
|
||||
<Link href='https://github.com/enricoros/big-AGI/issues/208' target='_blank'>Split Chats</Link>
|
||||
{' · '}<Link href='https://github.com/enricoros/big-AGI/issues/359' target='_blank'>Draw App</Link>
|
||||
<Link href='https://big-agi.com/blog/beam-multi-model-ai-reasoning' target='_blank'>Beam</Link>
|
||||
{' · '}<Link href='https://github.com/enricoros/big-AGI/issues/208' target='_blank'>Split Chats</Link>
|
||||
{' · '}<Link href='https://github.com/enricoros/big-AGI/issues/354' target='_blank'>Call AGI</Link>
|
||||
{' · '}<Link href='https://github.com/enricoros/big-AGI/issues/282' target='_blank'>Persona Creator</Link>
|
||||
{' · '}<Link href='https://github.com/enricoros/big-agi/issues/192' target='_blank'>Auto Diagrams</Link>
|
||||
{' · '}Imagine · Relative chat size · Text Tools · LLM Overheat
|
||||
{' · '}Imagine · Chat Search · Text Tools · LLM Overheat
|
||||
</Typography>
|
||||
</FormControl>
|
||||
|
||||
|
||||
@@ -169,6 +169,7 @@ export function adjustContentScaling(scaling: ContentScaling, offset?: number) {
|
||||
interface ContentScalingOptions {
|
||||
// BlocksRenderer
|
||||
blockCodeFontSize: string;
|
||||
blockCodeMarginY: number;
|
||||
blockFontSize: string;
|
||||
blockImageGap: number;
|
||||
blockLineHeight: string | number;
|
||||
@@ -182,6 +183,7 @@ interface ContentScalingOptions {
|
||||
export const themeScalingMap: Record<ContentScaling, ContentScalingOptions> = {
|
||||
xs: {
|
||||
blockCodeFontSize: '0.75rem',
|
||||
blockCodeMarginY: 0.5,
|
||||
blockFontSize: 'xs',
|
||||
blockImageGap: 1,
|
||||
blockLineHeight: 1.666667,
|
||||
@@ -191,6 +193,7 @@ export const themeScalingMap: Record<ContentScaling, ContentScalingOptions> = {
|
||||
},
|
||||
sm: {
|
||||
blockCodeFontSize: '0.75rem',
|
||||
blockCodeMarginY: 1,
|
||||
blockFontSize: 'sm',
|
||||
blockImageGap: 1.5,
|
||||
blockLineHeight: 1.714286,
|
||||
@@ -200,6 +203,7 @@ export const themeScalingMap: Record<ContentScaling, ContentScalingOptions> = {
|
||||
},
|
||||
md: {
|
||||
blockCodeFontSize: '0.875rem',
|
||||
blockCodeMarginY: 1.5,
|
||||
blockFontSize: 'md',
|
||||
blockImageGap: 2,
|
||||
blockLineHeight: 1.75,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ChatActions, createDMessage, DConversationId, DMessage, getConversation
|
||||
import { createBeamVanillaStore } from '~/modules/beam/store-beam-vanilla';
|
||||
|
||||
import { EphemeralHandler, EphemeralsStore } from './EphemeralsStore';
|
||||
import { createChatOverlayVanillaStore } from './store-chat-overlay-vanilla';
|
||||
|
||||
|
||||
/**
|
||||
@@ -21,6 +22,7 @@ export class ConversationHandler {
|
||||
private readonly conversationId: DConversationId;
|
||||
|
||||
private readonly beamStore = createBeamVanillaStore();
|
||||
private readonly overlayStore = createChatOverlayVanillaStore();
|
||||
readonly ephemeralsStore: EphemeralsStore = new EphemeralsStore();
|
||||
|
||||
|
||||
@@ -84,7 +86,7 @@ export class ConversationHandler {
|
||||
|
||||
// if zeroing the messages, also terminate an active beam
|
||||
if (!messages.length)
|
||||
this.beamStore.getState().terminate();
|
||||
this.beamStore.getState().terminateKeepingSettings();
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +102,7 @@ export class ConversationHandler {
|
||||
* @param destReplaceMessageId If set, the output will replace the message with this id, otherwise it will append to the history
|
||||
*/
|
||||
beamInvoke(viewHistory: Readonly<DMessage[]>, importMessages: DMessage[], destReplaceMessageId: DMessage['id'] | null): void {
|
||||
const { open: beamOpen, importRays: beamImportRays, terminate: beamTerminate } = this.beamStore.getState();
|
||||
const { open: beamOpen, importRays: beamImportRays, terminateKeepingSettings } = this.beamStore.getState();
|
||||
|
||||
const onBeamSuccess = (messageText: string, llmId: DLLMId) => {
|
||||
// set output when going back to the chat
|
||||
@@ -116,11 +118,11 @@ export class ConversationHandler {
|
||||
}
|
||||
|
||||
// close beam
|
||||
this.beamStore.getState().terminate();
|
||||
terminateKeepingSettings();
|
||||
};
|
||||
|
||||
beamOpen(viewHistory, useModelsStore.getState().chatLLMId, onBeamSuccess);
|
||||
importMessages.length && beamImportRays(importMessages);
|
||||
importMessages.length && beamImportRays(importMessages, useModelsStore.getState().chatLLMId);
|
||||
}
|
||||
|
||||
|
||||
@@ -130,4 +132,9 @@ export class ConversationHandler {
|
||||
return new EphemeralHandler(title, initialText, this.ephemeralsStore);
|
||||
}
|
||||
|
||||
|
||||
// Overlay Store
|
||||
|
||||
getOverlayStore = () => this.overlayStore;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { StoreApi, useStore } from 'zustand';
|
||||
import { createStore, StateCreator } from 'zustand/vanilla';
|
||||
|
||||
|
||||
/// Composer Slice: per-chat composer overlay state ///
|
||||
|
||||
interface ComposerOverlayState {
|
||||
|
||||
// if set, this is the 'reply to' mode text
|
||||
replyToText: string | null;
|
||||
|
||||
}
|
||||
|
||||
const initComposerOverlayStateSlice = (): ComposerOverlayState => ({
|
||||
|
||||
replyToText: null,
|
||||
|
||||
});
|
||||
|
||||
interface ComposerOverlayStore extends ComposerOverlayState {
|
||||
|
||||
setReplyToText: (text: string | null) => void;
|
||||
|
||||
}
|
||||
|
||||
const createComposerOverlayStoreSlice: StateCreator<ComposerOverlayStore, [], [], ComposerOverlayStore> = (_set, _get) => ({
|
||||
|
||||
// init state
|
||||
...initComposerOverlayStateSlice(),
|
||||
|
||||
// actions
|
||||
setReplyToText: (text: string | null) => _set({ replyToText: text }),
|
||||
|
||||
});
|
||||
|
||||
|
||||
/// Chat Overlay Store: per-chat overlay state ///
|
||||
// Note: at this time there are numerous overlay stores, including beam (vanilla), ephemerals (EventTarget), and this one.
|
||||
|
||||
export type OverlayStore = ComposerOverlayStore;
|
||||
|
||||
export type OverlayStoreApi = Readonly<StoreApi<OverlayStore>>;
|
||||
|
||||
export const createChatOverlayVanillaStore = () => createStore<OverlayStore>()((...a) => ({
|
||||
|
||||
...createComposerOverlayStoreSlice(...a),
|
||||
|
||||
}));
|
||||
|
||||
|
||||
const fallbackOverlayStore = createChatOverlayVanillaStore();
|
||||
|
||||
export const useChatOverlayStore = <T, >(vanillaStore: OverlayStoreApi | null, selector: (store: OverlayStore) => T): T =>
|
||||
useStore(vanillaStore || fallbackOverlayStore, selector);
|
||||
@@ -0,0 +1,15 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { Typography } from '@mui/joy';
|
||||
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
|
||||
|
||||
export function AlreadySet(props: { required?: boolean }) {
|
||||
return (
|
||||
<Typography level='body-sm' startDecorator={props.required ? undefined : <CheckRoundedIcon color='success' />}>
|
||||
{/*Installed Already*/}
|
||||
{props.required ? 'required' : 'Already set on server'}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,26 @@
|
||||
import React from 'react';
|
||||
import { sendGAEvent } from '@next/third-parties/google';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Box, Button, Step, stepClasses, StepIndicator, stepIndicatorClasses, Stepper, Typography } from '@mui/joy';
|
||||
import ArrowBackRoundedIcon from '@mui/icons-material/ArrowBackRounded';
|
||||
import ArrowForwardRoundedIcon from '@mui/icons-material/ArrowForwardRounded';
|
||||
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
||||
import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded';
|
||||
|
||||
import { ChatMessageMemo } from '../../apps/chat/components/message/ChatMessage';
|
||||
import { BlocksRenderer } from '~/modules/blocks/BlocksRenderer';
|
||||
|
||||
import { AgiSquircleIcon } from '~/common/components/icons/AgiSquircleIcon';
|
||||
import { ChatBeamIcon } from '~/common/components/icons/ChatBeamIcon';
|
||||
import { GlobalShortcutItem, ShortcutKeyName, useGlobalShortcuts } from '~/common/components/useGlobalShortcut';
|
||||
import { createDMessage } from '~/common/state/store-chats';
|
||||
import { hasGoogleAnalytics } from '~/common/components/GoogleAnalytics';
|
||||
import { useIsMobile } from '~/common/components/useMatchMedia';
|
||||
import { animationTextShadowLimey } from '~/common/util/animUtils';
|
||||
|
||||
|
||||
// configuration
|
||||
const colorButtons = 'neutral' as const;
|
||||
const colorStepper = 'neutral' as const;
|
||||
|
||||
|
||||
// Steps - the top stepper
|
||||
@@ -25,13 +33,13 @@ interface ExplainerStep {
|
||||
const stepSequenceSx: SxProps = {
|
||||
// width: '100%',
|
||||
[`& .${stepClasses.completed}::after`]: {
|
||||
bgcolor: 'primary.500',
|
||||
bgcolor: `${colorStepper}.500`,
|
||||
},
|
||||
[`& .${stepClasses.active} .${stepIndicatorClasses.root}`]: {
|
||||
borderColor: 'primary.500',
|
||||
borderColor: `${colorStepper}.500`,
|
||||
},
|
||||
[`& .${stepClasses.root}:has(+ .${stepClasses.active})::after`]: {
|
||||
color: 'primary.500',
|
||||
color: `${colorStepper}.500`,
|
||||
backgroundColor: 'transparent',
|
||||
backgroundImage: 'radial-gradient(currentColor 2px, transparent 2px)',
|
||||
backgroundSize: '7px 7px',
|
||||
@@ -39,6 +47,18 @@ const stepSequenceSx: SxProps = {
|
||||
},
|
||||
};
|
||||
|
||||
const buttonBaseSx: SxProps = {
|
||||
justifyContent: 'space-between',
|
||||
minHeight: '2.5rem',
|
||||
minWidth: 120,
|
||||
};
|
||||
|
||||
const buttonNextSx: SxProps = {
|
||||
...buttonBaseSx,
|
||||
boxShadow: `0 8px 24px -4px rgb(var(--joy-palette-${colorButtons}-mainChannel) / 20%)`,
|
||||
minWidth: 180,
|
||||
};
|
||||
|
||||
|
||||
function AllStepsStepper(props: {
|
||||
steps: ExplainerStep[],
|
||||
@@ -57,10 +77,14 @@ function AllStepsStepper(props: {
|
||||
orientation='vertical'
|
||||
completed={completed}
|
||||
active={active}
|
||||
onClick={() => props.onStepClicked(stepIndex)}
|
||||
indicator={
|
||||
<StepIndicator variant={(completed || active) ? 'solid' : 'outlined'} color='primary'>
|
||||
{completed ? <CheckRoundedIcon /> : active ? <KeyboardArrowDownRoundedIcon /> : undefined}
|
||||
<StepIndicator
|
||||
variant={(completed || active) ? 'solid' : 'outlined'}
|
||||
color={colorStepper}
|
||||
onClick={() => props.onStepClicked(stepIndex)}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
>
|
||||
{completed ? <CheckRoundedIcon sx={{ fontSize: 'md' }} /> : active ? <KeyboardArrowDownRoundedIcon sx={{ fontSize: 'lg' }} /> : undefined}
|
||||
</StepIndicator>
|
||||
}
|
||||
>
|
||||
@@ -90,9 +114,10 @@ export interface ExplainerPage extends ExplainerStep {
|
||||
}
|
||||
|
||||
export function ExplainerCarousel(props: {
|
||||
explainerId: string,
|
||||
steps: ExplainerPage[],
|
||||
footer?: React.ReactNode,
|
||||
showPrevious?: boolean,
|
||||
noStepper?: boolean,
|
||||
onFinished: () => any,
|
||||
}) {
|
||||
|
||||
@@ -104,26 +129,34 @@ export function ExplainerCarousel(props: {
|
||||
|
||||
// derived state
|
||||
const { onFinished } = props;
|
||||
const isFirstPage = stepIndex === 0;
|
||||
const isLastPage = stepIndex === props.steps.length - 1;
|
||||
const activeStep = props.steps[stepIndex] ?? null;
|
||||
|
||||
// handlers
|
||||
|
||||
const mdText = activeStep?.mdContent ?? null;
|
||||
const mdMessage = React.useMemo(() => {
|
||||
return mdText ? createDMessage('assistant', mdText) : null;
|
||||
}, [mdText]);
|
||||
|
||||
const handlePrevPage = React.useCallback(() => {
|
||||
setStepIndex(step => step > 0 ? step - 1 : step);
|
||||
}, []);
|
||||
|
||||
const handleNextPage = React.useCallback(() => {
|
||||
if (isLastPage)
|
||||
if (isLastPage) {
|
||||
hasGoogleAnalytics && sendGAEvent('event', 'tutorial_complete', { tutorial_id: props.explainerId });
|
||||
onFinished();
|
||||
else
|
||||
} else
|
||||
setStepIndex(step => step < props.steps.length - 1 ? step + 1 : step);
|
||||
}, [isLastPage, onFinished, props.steps.length]);
|
||||
}, [isLastPage, onFinished, props.explainerId, props.steps.length]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const recordTutorialBegun = () => {
|
||||
hasGoogleAnalytics && sendGAEvent('event', 'tutorial_begin', { tutorial_id: props.explainerId });
|
||||
};
|
||||
|
||||
const timeoutId = setTimeout(recordTutorialBegun, 500);
|
||||
return () => clearTimeout(timeoutId);
|
||||
}, [props.explainerId]);
|
||||
|
||||
|
||||
const shortcuts = React.useMemo((): GlobalShortcutItem[] => [
|
||||
@@ -149,7 +182,7 @@ export function ExplainerCarousel(props: {
|
||||
// content
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'space-around',
|
||||
justifyContent: 'space-evenly',
|
||||
gap: 2,
|
||||
}}>
|
||||
|
||||
@@ -159,85 +192,91 @@ export function ExplainerCarousel(props: {
|
||||
level='h1'
|
||||
component='h1'
|
||||
sx={{
|
||||
fontSize: isMobile ? '2rem' : '2.75rem',
|
||||
fontSize: isMobile ? '2rem' : '2.5rem',
|
||||
fontWeight: 'md',
|
||||
textAlign: 'center',
|
||||
whiteSpace: 'balance',
|
||||
}}>
|
||||
{activeStep?.titlePrefix}{' '}
|
||||
{!!activeStep?.titleSquircle && <AgiSquircleIcon inverted sx={{ color: 'white', fontSize: isMobile ? '1.55rem' : '2.04rem', borderRadius: 'md' }} />}
|
||||
{!!activeStep?.titleSquircle && '-'}
|
||||
{!!activeStep?.titleSpark && <Box component='span' sx={{ fontWeight: 'lg', /*animation: `${animationTextShadowLimey} 15s linear infinite`*/ color: 'primary.softColor' }}>
|
||||
{!!activeStep?.titleSpark && <Box component='span' sx={{
|
||||
fontWeight: 'lg',
|
||||
color: 'neutral.softColor',
|
||||
animation: `${animationTextShadowLimey} 5s infinite`,
|
||||
/*, animation: `${animationTextShadowLimey} 15s linear infinite`*/
|
||||
}}>
|
||||
{activeStep.titleSpark}
|
||||
</Box>}{activeStep?.titleSuffix}
|
||||
</Typography>
|
||||
|
||||
|
||||
{/* All Steps */}
|
||||
<Box>
|
||||
<AllStepsStepper
|
||||
steps={props.steps}
|
||||
activeIndex={stepIndex}
|
||||
isMobile={isMobile}
|
||||
onStepClicked={setStepIndex}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
|
||||
{/* Page Message */}
|
||||
{!!mdMessage && (
|
||||
<ChatMessageMemo
|
||||
message={mdMessage}
|
||||
fitScreen={isMobile}
|
||||
showAvatar={false}
|
||||
adjustContentScaling={isMobile ? 0 : undefined}
|
||||
sx={{
|
||||
minHeight: '19rem', // 256px
|
||||
py: 2,
|
||||
border: 'none',
|
||||
bordreRadius: 0,
|
||||
borderRadius: 'xl',
|
||||
// boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-darkChannel) / 0.12)',
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
|
||||
|
||||
{/* Main Card with the markdown body */}
|
||||
{!!mdText && (
|
||||
<Box sx={{
|
||||
minHeight: '24rem',
|
||||
backgroundColor: 'background.popup',
|
||||
borderRadius: 'lg',
|
||||
boxShadow: '0 60px 32px -60px rgb(var(--joy-palette-primary-darkChannel) / 0.14)',
|
||||
mb: 2,
|
||||
px: { xs: 1, md: 2 },
|
||||
py: 2,
|
||||
|
||||
// customize the embedded GitHub Markdown for transparent images
|
||||
['.markdown-body img']: {
|
||||
'--color-canvas-default': 'transparent!important',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
}}>
|
||||
<BlocksRenderer
|
||||
text={mdText}
|
||||
fromRole='assistant'
|
||||
contentScaling='md'
|
||||
fitScreen={isMobile}
|
||||
renderTextAsMarkdown
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
|
||||
{/* Buttons */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1 }}>
|
||||
{/* Advance Button */}
|
||||
<Button
|
||||
variant='solid'
|
||||
size='lg'
|
||||
endDecorator={isLastPage ? <ChatBeamIcon /> : <ArrowForwardRoundedIcon />}
|
||||
color={colorButtons}
|
||||
onClick={handleNextPage}
|
||||
sx={{
|
||||
boxShadow: '0 8px 24px -4px rgb(var(--joy-palette-primary-mainChannel) / 20%)',
|
||||
minWidth: 180,
|
||||
}}
|
||||
endDecorator={isLastPage ? <ChatBeamIcon /> : <ArrowForwardRoundedIcon />}
|
||||
sx={buttonNextSx}
|
||||
>
|
||||
{isLastPage ? 'Start' : 'Next'}
|
||||
</Button>
|
||||
|
||||
{/* Back Button */}
|
||||
<Button
|
||||
variant='outlined'
|
||||
color='neutral'
|
||||
variant='plain'
|
||||
color={colorButtons}
|
||||
disabled={isFirstPage}
|
||||
onClick={handlePrevPage}
|
||||
sx={{
|
||||
minWidth: 140,
|
||||
}}
|
||||
startDecorator={<ArrowBackRoundedIcon />}
|
||||
sx={buttonBaseSx}
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
</Box>
|
||||
|
||||
|
||||
{/* All Steps */}
|
||||
{props.noStepper ? null : (
|
||||
<AllStepsStepper
|
||||
steps={props.steps}
|
||||
activeIndex={stepIndex}
|
||||
isMobile={isMobile}
|
||||
onStepClicked={setStepIndex}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
||||
{/* Final words of wisdom (also perfect for centering the other components) */}
|
||||
{props.footer}
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { SxProps, TypographySystem } from '@mui/joy/styles/types';
|
||||
import AutoStoriesOutlinedIcon from '@mui/icons-material/AutoStoriesOutlined';
|
||||
import LaunchIcon from '@mui/icons-material/Launch';
|
||||
|
||||
import { Link } from './Link';
|
||||
|
||||
|
||||
const wowStyle: SxProps = {
|
||||
textDecoration: 'underline',
|
||||
textDecorationThickness: '0.4em',
|
||||
textDecorationColor: 'rgba(var(--joy-palette-primary-lightChannel) / 1)',
|
||||
// textDecorationColor: 'rgba(0 255 0 / 0.5)',
|
||||
textDecorationSkipInk: 'none',
|
||||
// textUnderlineOffset: '-0.5em',
|
||||
};
|
||||
|
||||
|
||||
export function ExternalLink(props: {
|
||||
href: string,
|
||||
level?: keyof TypographySystem | 'inherit',
|
||||
highlight?: boolean,
|
||||
icon?: 'issue',
|
||||
children: React.ReactNode,
|
||||
}) {
|
||||
return (
|
||||
<Link level={props.level} href={props.href} target='_blank' sx={props.highlight ? wowStyle : undefined}>
|
||||
{props.children} {props.icon === 'issue'
|
||||
? <AutoStoriesOutlinedIcon sx={{ mx: 0.5, fontSize: 16 }} />
|
||||
: <LaunchIcon sx={{ mx: 0.5, fontSize: 16 }} />
|
||||
}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ export const GoodTooltip = (props: {
|
||||
title: React.ReactNode,
|
||||
placement?: 'top' | 'bottom' | 'top-start',
|
||||
isError?: boolean, isWarning?: boolean,
|
||||
arrow?: boolean,
|
||||
usePlain?: boolean,
|
||||
children: React.JSX.Element,
|
||||
sx?: SxProps
|
||||
@@ -19,6 +20,7 @@ export const GoodTooltip = (props: {
|
||||
title={props.title}
|
||||
placement={props.placement}
|
||||
disableInteractive
|
||||
arrow={props.arrow}
|
||||
variant={(props.isError || props.isWarning) ? 'soft' : props.usePlain ? 'plain' : undefined}
|
||||
color={props.isError ? 'danger' : props.isWarning ? 'warning' : undefined}
|
||||
sx={{
|
||||
|
||||
@@ -7,7 +7,7 @@ import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
|
||||
|
||||
export function FormInputKey(props: {
|
||||
id: string, // introduced to avoid clashes
|
||||
autoCompleteId: string, // introduced to avoid clashes
|
||||
label?: string, rightLabel?: string | React.JSX.Element,
|
||||
description?: string | React.JSX.Element,
|
||||
value: string, onChange: (value: string) => void,
|
||||
@@ -27,8 +27,10 @@ export function FormInputKey(props: {
|
||||
</IconButton>
|
||||
), [props.value, props.noKey, isVisible]);
|
||||
|
||||
const acId = (props.noKey ? 'input-text-' : 'input-key-') + props.autoCompleteId;
|
||||
|
||||
return (
|
||||
<FormControl>
|
||||
<FormControl id={acId}>
|
||||
|
||||
{!!props.label && <Box sx={{ display: 'flex', flexDirection: 'row', alignItems: 'baseline', flexWrap: 'wrap', justifyContent: 'space-between' }}>
|
||||
<FormLabel>{props.label}</FormLabel>
|
||||
@@ -38,7 +40,10 @@ export function FormInputKey(props: {
|
||||
</Box>}
|
||||
|
||||
<Input
|
||||
id={props.id}
|
||||
key={acId}
|
||||
name={acId}
|
||||
autoComplete='off'
|
||||
// autoComplete={props.noKey ? 'off' : 'new-password'}
|
||||
variant={props.required ? 'outlined' : 'outlined' /* 'soft */}
|
||||
value={props.value} onChange={handleChange}
|
||||
placeholder={props.required ? props.placeholder ? 'required: ' + props.placeholder : 'required' : props.placeholder || '...'}
|
||||
|
||||
@@ -39,6 +39,7 @@ const FormLabelStartBase = (props: {
|
||||
{!!props.description && (
|
||||
<FormHelperText
|
||||
sx={{
|
||||
fontSize: 'xs',
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -9,16 +9,28 @@ import { FormLabelStart } from './FormLabelStart';
|
||||
* Text form field (e.g. enter a host)
|
||||
*/
|
||||
export function FormTextField(props: {
|
||||
autoCompleteId: string,
|
||||
title: string | React.JSX.Element,
|
||||
description?: string | React.JSX.Element,
|
||||
tooltip?: string | React.JSX.Element,
|
||||
placeholder?: string, isError?: boolean, disabled?: boolean,
|
||||
value: string | undefined, onChange: (text: string) => void,
|
||||
}) {
|
||||
const acId = 'text-' + props.autoCompleteId;
|
||||
return (
|
||||
<FormControl orientation='horizontal' disabled={props.disabled} sx={{ flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<FormControl
|
||||
id={acId}
|
||||
orientation='horizontal'
|
||||
disabled={props.disabled}
|
||||
sx={{
|
||||
flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<FormLabelStart title={props.title} description={props.description} tooltip={props.tooltip} />
|
||||
<Input
|
||||
key={acId}
|
||||
name={acId}
|
||||
autoComplete='off'
|
||||
variant='outlined' placeholder={props.placeholder} error={props.isError}
|
||||
value={props.value} onChange={event => props.onChange(event.target.value)}
|
||||
sx={{ flexGrow: 1 }}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { FormRadioOption } from './FormRadioControl';
|
||||
/**
|
||||
* Warning: this must be a constant to avoid re-rendering the radio group
|
||||
*/
|
||||
export function useFormRadio<T extends string>(initialValue: T, options: FormRadioOption<T>[], label?: string, hidden?: boolean): [T | null, React.JSX.Element | null] {
|
||||
export function useFormRadio<T extends string>(initialValue: T, options: FormRadioOption<T>[], label?: string, hidden?: boolean): [T | null, React.JSX.Element | null, React.Dispatch<React.SetStateAction<T | null>>] {
|
||||
|
||||
// state
|
||||
const [value, setValue] = React.useState<T | null>(initialValue);
|
||||
@@ -33,5 +33,5 @@ export function useFormRadio<T extends string>(initialValue: T, options: FormRad
|
||||
[handleChange, hidden, label, options, value],
|
||||
);
|
||||
|
||||
return [value, component];
|
||||
return [value, component, setValue];
|
||||
}
|
||||
@@ -39,7 +39,7 @@ const DesktopDrawerTranslatingSheet = styled(Sheet)(({ theme }) => ({
|
||||
// borderBottomRightRadius: 'var(--AGI-Optima-Radius)',
|
||||
// contain: 'strict',
|
||||
// boxShadow: theme.shadow.md, // too thin and complex; also tried 40px blurs
|
||||
boxShadow: `1px 2px 6px 0 rgba(${theme.palette.neutral.darkChannel} / 0.12)`,
|
||||
boxShadow: `0px 0px 6px 0 rgba(${theme.palette.neutral.darkChannel} / 0.12)`,
|
||||
|
||||
// content layout
|
||||
display: 'flex',
|
||||
|
||||
@@ -4,7 +4,7 @@ import Router from 'next/router';
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { Divider, Dropdown, ListItemDecorator, Menu, MenuButton, MenuItem, Tooltip } from '@mui/joy';
|
||||
import MenuIcon from '@mui/icons-material/Menu';
|
||||
import MoreHorizRoundedIcon from '@mui/icons-material/MoreHorizRounded';
|
||||
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
|
||||
|
||||
import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
@@ -93,7 +93,7 @@ export function DesktopNav(props: { component: React.ElementType, currentApp?: N
|
||||
<Dropdown key='n-app-overflow'>
|
||||
<Tooltip disableInteractive enterDelay={600} title='More Apps'>
|
||||
<MenuButton slots={{ root: DesktopNavIcon }} slotProps={{ root: { className: navItemClasses.typeApp } }}>
|
||||
<MoreHorizRoundedIcon />
|
||||
<MoreHorizIcon />
|
||||
</MenuButton>
|
||||
</Tooltip>
|
||||
<Menu variant='solid' invertedColors placement='right-start'>
|
||||
|
||||
@@ -108,6 +108,11 @@ export function PageBar(props: { component: React.ElementType, currentApp?: NavI
|
||||
return <CommonPageMenuItems onClose={closePageMenu} />;
|
||||
}, [closePageMenu]);
|
||||
|
||||
const handlePageContextMenu = React.useCallback((event: React.MouseEvent) => {
|
||||
event.preventDefault(); // added for the Right mouse click (to prevent the menu)
|
||||
openPageMenu();
|
||||
}, [openPageMenu]);
|
||||
|
||||
// [Desktop] hide the app bar if the current app doesn't use it
|
||||
const desktopHide = !!props.currentApp?.hideBar && !props.isMobile;
|
||||
if (desktopHide)
|
||||
@@ -165,7 +170,12 @@ export function PageBar(props: { component: React.ElementType, currentApp?: NavI
|
||||
|
||||
{/* Page Menu Anchor */}
|
||||
<InvertedBarCornerItem>
|
||||
<IconButton disabled={!pageMenuAnchor /*|| (!appMenuItems && !props.isMobile)*/} onClick={openPageMenu} ref={pageMenuAnchor}>
|
||||
<IconButton
|
||||
ref={pageMenuAnchor}
|
||||
disabled={!pageMenuAnchor /*|| (!appMenuItems && !props.isMobile)*/}
|
||||
onClick={openPageMenu}
|
||||
onContextMenu={handlePageContextMenu}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</InvertedBarCornerItem>
|
||||
|
||||
@@ -57,8 +57,8 @@ export function PageWrapper(props: { component: React.ElementType, currentApp?:
|
||||
sx={{
|
||||
boxShadow: {
|
||||
xs: 'none',
|
||||
md: amplitude === 'narrow' ? 'md' : 'none',
|
||||
xl: amplitude !== 'full' ? 'lg' : 'none',
|
||||
md: amplitude === 'narrow' ? '0px 0px 4px 0 rgba(50 56 62 / 0.12)' : 'none',
|
||||
xl: amplitude !== 'full' ? '0px 0px 4px 0 rgba(50 56 62 / 0.12)' : 'none',
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { IconButton, Sheet, Typography } from '@mui/joy';
|
||||
import { Box, IconButton, Typography } from '@mui/joy';
|
||||
import CloseRoundedIcon from '@mui/icons-material/CloseRounded';
|
||||
|
||||
|
||||
@@ -11,23 +11,24 @@ export const PageDrawerHeader = (props: {
|
||||
sx?: SxProps,
|
||||
children?: React.ReactNode,
|
||||
}) =>
|
||||
<Sheet
|
||||
variant='outlined'
|
||||
<Box
|
||||
// variant='soft'
|
||||
// invertedColors
|
||||
sx={{
|
||||
minHeight: 'var(--AGI-Nav-width)',
|
||||
|
||||
// content
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 1,
|
||||
|
||||
// style
|
||||
borderTop: 'none',
|
||||
borderLeft: 'none',
|
||||
borderRight: 'none',
|
||||
backgroundColor: 'background.popup',
|
||||
// borderLeft: 'none',
|
||||
// borderRight: 'none',
|
||||
// borderTop: 'none',
|
||||
// borderTopRightRadius: 'var(--AGI-Optima-Radius)',
|
||||
|
||||
// layout
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -41,4 +42,4 @@ export const PageDrawerHeader = (props: {
|
||||
<CloseRoundedIcon />
|
||||
</IconButton>
|
||||
|
||||
</Sheet>;
|
||||
</Box>;
|
||||
@@ -2,9 +2,9 @@ import { createStore } from 'zustand/vanilla';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { DModelSource, useModelsStore } from '~/modules/llms/store-llms';
|
||||
import { createModelSourceForVendor, findAccessForSourceOrThrow, findAllVendors } from '~/modules/llms/vendors/vendors.registry';
|
||||
import { createModelSourceForVendor, findAllVendors } from '~/modules/llms/vendors/vendors.registry';
|
||||
import { getBackendCapabilities } from '~/modules/backend/store-backend-capabilities';
|
||||
import { updateModelsForSource } from '~/modules/llms/vendors/useLlmUpdateModels';
|
||||
import { llmsUpdateModelsForSourceOrThrow } from '~/modules/llms/llm.client';
|
||||
|
||||
|
||||
interface AutoConfStore {
|
||||
@@ -65,12 +65,8 @@ const autoConfVanillaStore = createStore<AutoConfStore>()(persist((_set, _get) =
|
||||
source = useModelsStore.getState().sources.find(_s => _s.id === source.id)!;
|
||||
}
|
||||
|
||||
// get the access, assuming there's no client config and the server will do all
|
||||
const transportAcess = findAccessForSourceOrThrow(source.id);
|
||||
|
||||
// fetch models
|
||||
const data = await vendor.rpcUpdateModelsOrThrow(transportAcess);
|
||||
return updateModelsForSource(data, source, true);
|
||||
// auto-configure this source
|
||||
await llmsUpdateModelsForSourceOrThrow(source.id, true);
|
||||
})
|
||||
.catch(error => {
|
||||
// catches errors and logs them, but does not stop the chain
|
||||
|
||||
@@ -4,6 +4,8 @@ import type { SxProps } from '@mui/joy/styles/types';
|
||||
import { IconButton } from '@mui/joy';
|
||||
import KeyboardDoubleArrowDownIcon from '@mui/icons-material/KeyboardDoubleArrowDown';
|
||||
|
||||
import { themeZIndexBeamView } from '~/common/app.theme';
|
||||
|
||||
import { useScrollToBottom } from './useScrollToBottom';
|
||||
|
||||
|
||||
@@ -11,6 +13,9 @@ const inlineButtonSx: SxProps = {
|
||||
// style it
|
||||
// NOTE: just an IconButton when inline
|
||||
|
||||
// for usage inside BeamGatherPane, to not enlarge the row
|
||||
my: -0.25,
|
||||
|
||||
// fade it in when hovering
|
||||
// transition: 'all 0.15s',
|
||||
// '&:hover': {
|
||||
@@ -27,7 +32,7 @@ const absoluteButtonSx: SxProps = {
|
||||
borderColor: 'neutral.500',
|
||||
borderRadius: '50%',
|
||||
boxShadow: 'sm',
|
||||
zIndex: 3, // stay on top of the Chat Message buttons (e.g. copy)
|
||||
zIndex: themeZIndexBeamView + 1, // stay on top of the Chat Message buttons (e.g. copy)
|
||||
|
||||
// place this on the bottom-right corner (FAB-like)
|
||||
position: 'absolute',
|
||||
@@ -57,6 +62,7 @@ export function ScrollToBottomButton(props: { inline?: boolean }) {
|
||||
aria-label='Scroll To Bottom'
|
||||
variant='plain'
|
||||
onClick={handleStickToBottom}
|
||||
size={props.inline ? 'sm' : undefined}
|
||||
sx={props.inline ? inlineButtonSx : absoluteButtonSx}
|
||||
>
|
||||
<KeyboardDoubleArrowDownIcon sx={{ fontSize: 'xl' }} />
|
||||
|
||||
@@ -6,24 +6,13 @@ import { persist } from 'zustand/middleware';
|
||||
|
||||
interface AppStateData {
|
||||
usageCount: number;
|
||||
lastSeenNewsVersion: number;
|
||||
// suppressedItems: Record<string, boolean>;
|
||||
}
|
||||
|
||||
interface AppStateActions {
|
||||
setLastSeenNewsVersion: (version: number) => void;
|
||||
}
|
||||
|
||||
|
||||
export const useAppStateStore = create<AppStateData & AppStateActions>()(
|
||||
export const useAppStateStore = create<AppStateData>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
|
||||
usageCount: 0,
|
||||
lastSeenNewsVersion: 0,
|
||||
// suppressedItems: {},
|
||||
|
||||
setLastSeenNewsVersion: (version: number) => set({ lastSeenNewsVersion: version }),
|
||||
|
||||
}),
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { DLLMId, getChatLLMId } from '~/modules/llms/store-llms';
|
||||
|
||||
import { IDB_MIGRATION_INITIAL, idbStateStorage } from '../util/idbUtils';
|
||||
import { idbStateStorage } from '../util/idbUtils';
|
||||
import { countModelTokens } from '../util/token-counter';
|
||||
import { defaultSystemPurposeId, SystemPurposeId } from '../../data';
|
||||
|
||||
@@ -65,7 +65,8 @@ export interface DMessage {
|
||||
purposeId?: SystemPurposeId; // only assistant/system
|
||||
originLLM?: string; // only assistant - model that generated this message, goes beyond known models
|
||||
|
||||
userFlags?: DMessageUserFlag[]; // user-set per-message flags
|
||||
metadata?: DMessageMetadata; // metadata, mainly at creation and for UI
|
||||
userFlags?: DMessageUserFlag[]; // (UI) user-set per-message flags
|
||||
|
||||
tokenCount: number; // cache for token count, using the current Conversation model (0 = not yet calculated)
|
||||
|
||||
@@ -76,6 +77,10 @@ export interface DMessage {
|
||||
export type DMessageUserFlag =
|
||||
| 'starred'; // user starred this
|
||||
|
||||
export interface DMessageMetadata {
|
||||
inReplyToText?: string; // text this was in reply to
|
||||
}
|
||||
|
||||
export function createDMessage(role: DMessage['role'], text: string): DMessage {
|
||||
return {
|
||||
id: uuidv4(),
|
||||
@@ -130,6 +135,7 @@ export interface ChatActions {
|
||||
appendMessage: (conversationId: string, message: DMessage) => void;
|
||||
deleteMessage: (conversationId: string, messageId: string) => void;
|
||||
editMessage: (conversationId: string, messageId: string, update: Partial<DMessage> | ((message: DMessage) => Partial<DMessage>), touchUpdated: boolean) => void;
|
||||
updateMetadata: (conversationId: string, messageId: string, metadataDelta: Partial<DMessageMetadata>, touchUpdated?: boolean) => void;
|
||||
setSystemPurposeId: (conversationId: string, systemPurposeId: SystemPurposeId) => void;
|
||||
setAutoTitle: (conversationId: string, autoTitle: string) => void;
|
||||
setUserTitle: (conversationId: string, userTitle: string) => void;
|
||||
@@ -345,10 +351,31 @@ export const useChatStore = create<ConversationsStore>()(devtools(
|
||||
return {
|
||||
messages,
|
||||
tokenCount: messages.reduce((sum, message) => sum + 4 + message.tokenCount || 0, 3),
|
||||
...(touchUpdated && { updated: Date.now() }),
|
||||
updated: touchUpdated ? Date.now() : conversation.updated,
|
||||
};
|
||||
}),
|
||||
|
||||
updateMetadata: (conversationId: string, messageId: string, metadataDelta: Partial<DMessageMetadata>, touchUpdated: boolean = true) => {
|
||||
_get()._editConversation(conversationId, conversation => {
|
||||
const messages = conversation.messages.map(message =>
|
||||
message.id !== messageId ? message
|
||||
: {
|
||||
...message,
|
||||
metadata: {
|
||||
...message.metadata,
|
||||
...metadataDelta,
|
||||
},
|
||||
updated: touchUpdated ? Date.now() : message.updated,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
messages,
|
||||
updated: touchUpdated ? Date.now() : conversation.updated,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
setSystemPurposeId: (conversationId: string, systemPurposeId: SystemPurposeId) =>
|
||||
_get()._editConversation(conversationId,
|
||||
{
|
||||
@@ -380,10 +407,7 @@ export const useChatStore = create<ConversationsStore>()(devtools(
|
||||
storage: createJSONStorage(() => idbStateStorage),
|
||||
|
||||
// Migrations
|
||||
migrate: (persistedState: unknown, fromVersion: number): ConversationsStore => {
|
||||
// -1 -> 3: migration loading from localStorage to IndexedDB
|
||||
if (fromVersion === IDB_MIGRATION_INITIAL)
|
||||
return _migrateLocalStorageData() as any;
|
||||
migrate: (persistedState: unknown, _fromVersion: number): ConversationsStore => {
|
||||
|
||||
// other: just proceed
|
||||
return persistedState as any;
|
||||
@@ -438,32 +462,6 @@ function getNextBranchTitle(currentTitle: string): string {
|
||||
return `(1) ${currentTitle}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the chats stored in the localStorage, and rename the key for
|
||||
* backup/data loss prevention purposes
|
||||
*/
|
||||
function _migrateLocalStorageData(): ChatState | {} {
|
||||
const key = 'app-chats';
|
||||
const value = localStorage.getItem(key);
|
||||
if (!value) return {};
|
||||
try {
|
||||
// parse the localStorage state
|
||||
const localStorageState = JSON.parse(value)?.state;
|
||||
|
||||
// backup and delete the localStorage key
|
||||
const backupKey = `${key}-v2`;
|
||||
localStorage.setItem(backupKey, value);
|
||||
localStorage.removeItem(key);
|
||||
|
||||
// match the state from localstorage
|
||||
return {
|
||||
conversations: localStorageState?.conversations ?? [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('LocalStorage migration error', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to count the tokens in a DMessage object
|
||||
|
||||
@@ -3,6 +3,7 @@ import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import type { ContentScaling } from '~/common/app.theme';
|
||||
import { browserLangOrUS } from '~/common/util/pwaUtils';
|
||||
|
||||
|
||||
// UI Preferences
|
||||
@@ -31,6 +32,9 @@ interface UIPreferencesStore {
|
||||
renderMarkdown: boolean;
|
||||
setRenderMarkdown: (renderMarkdown: boolean) => void;
|
||||
|
||||
renderCodeSoftWrap: boolean;
|
||||
setRenderCodeSoftWrap: (renderCodeSoftWrap: boolean) => void;
|
||||
|
||||
// showPersonaExamples: boolean;
|
||||
// setShowPersonaExamples: (showPersonaExamples: boolean) => void;
|
||||
|
||||
@@ -54,7 +58,7 @@ export const useUIPreferencesStore = create<UIPreferencesStore>()(
|
||||
|
||||
// UI Features
|
||||
|
||||
preferredLanguage: (typeof navigator !== 'undefined') && navigator.language || 'en-US',
|
||||
preferredLanguage: browserLangOrUS,
|
||||
setPreferredLanguage: (preferredLanguage: string) => set({ preferredLanguage }),
|
||||
|
||||
centerMode: 'wide',
|
||||
@@ -74,6 +78,9 @@ export const useUIPreferencesStore = create<UIPreferencesStore>()(
|
||||
renderMarkdown: true,
|
||||
setRenderMarkdown: (renderMarkdown: boolean) => set({ renderMarkdown }),
|
||||
|
||||
renderCodeSoftWrap: false,
|
||||
setRenderCodeSoftWrap: (renderCodeSoftWrap: boolean) => set({ renderCodeSoftWrap }),
|
||||
|
||||
// showPersonaExamples: false,
|
||||
// setShowPersonaExamples: (showPersonaExamples: boolean) => set({ showPersonaExamples }),
|
||||
|
||||
|
||||
@@ -4,20 +4,14 @@ import { persist } from 'zustand/middleware';
|
||||
|
||||
// UX Labs Experiments
|
||||
|
||||
/**
|
||||
* Graduated:
|
||||
* - see `UxLabsSettings.tsx`, and also:
|
||||
* - Text Tools: dinamically shown where applicable
|
||||
* - Chat Mode: follow-ups; moved to Chat Advanced UI
|
||||
*/
|
||||
// UxLabsSettings.tsx contains the graduated settings, but the following are not stated:
|
||||
// - Text Tools: dinamically shown where applicable
|
||||
// - Chat Mode: Follow-Ups; moved to Chat Advanced UI
|
||||
interface UXLabsStore {
|
||||
|
||||
labsAttachScreenCapture: boolean;
|
||||
setLabsAttachScreenCapture: (labsAttachScreenCapture: boolean) => void;
|
||||
|
||||
labsBeam: boolean;
|
||||
setLabsBeam: (labsBeam: boolean) => void;
|
||||
|
||||
labsCameraDesktop: boolean;
|
||||
setLabsCameraDesktop: (labsCameraDesktop: boolean) => void;
|
||||
|
||||
@@ -27,6 +21,9 @@ interface UXLabsStore {
|
||||
labsHighPerformance: boolean;
|
||||
setLabsHighPerformance: (labsHighPerformance: boolean) => void;
|
||||
|
||||
labsShowCost: boolean;
|
||||
setLabsShowCost: (labsShowCost: boolean) => void;
|
||||
|
||||
}
|
||||
|
||||
export const useUXLabsStore = create<UXLabsStore>()(
|
||||
@@ -36,9 +33,6 @@ export const useUXLabsStore = create<UXLabsStore>()(
|
||||
labsAttachScreenCapture: false,
|
||||
setLabsAttachScreenCapture: (labsAttachScreenCapture: boolean) => set({ labsAttachScreenCapture }),
|
||||
|
||||
labsBeam: true,
|
||||
setLabsBeam: (labsBeam: boolean) => set({ labsBeam }),
|
||||
|
||||
labsCameraDesktop: false,
|
||||
setLabsCameraDesktop: (labsCameraDesktop: boolean) => set({ labsCameraDesktop }),
|
||||
|
||||
@@ -48,6 +42,9 @@ export const useUXLabsStore = create<UXLabsStore>()(
|
||||
labsHighPerformance: false,
|
||||
setLabsHighPerformance: (labsHighPerformance: boolean) => set({ labsHighPerformance }),
|
||||
|
||||
labsShowCost: true, // release 1.16.0 with this enabled by default
|
||||
setLabsShowCost: (labsShowCost: boolean) => set({ labsShowCost }),
|
||||
|
||||
}),
|
||||
{
|
||||
name: 'app-ux-labs',
|
||||
|
||||
@@ -250,20 +250,20 @@ export const animationShadowLimey = keyframes`
|
||||
box-shadow: 2px 2px 12px -6px rgb(255, 153, 0);
|
||||
}`;
|
||||
|
||||
/*export const animationTextShadowLimey = keyframes`
|
||||
export const animationTextShadowLimey = keyframes`
|
||||
100%, 0% {
|
||||
text-shadow: 2px 2px 0 white, 4px 4px 0 rgb(183, 255, 0);
|
||||
text-shadow: 2px 2px 0 rgba(183, 255, 0, 0.5);
|
||||
}
|
||||
25% {
|
||||
text-shadow: 2px 2px 0 white, 4px 4px 0 rgb(255, 251, 0);
|
||||
text-shadow: 2px 2px 0 rgba(255, 251, 0, 0.5);
|
||||
}
|
||||
50% {
|
||||
text-shadow: 2px 2px 0 white, 4px 4px 0 rgba(0, 255, 81);
|
||||
text-shadow: 2px 2px 0 rgba(0, 255, 81, 0.5);
|
||||
}
|
||||
75% {
|
||||
text-shadow: 2px 2px 0 white, 4px 4px 0 rgb(255, 153, 0);
|
||||
text-shadow: 2px 2px 0 rgba(255, 153, 0, 0.5);
|
||||
}`;
|
||||
*/
|
||||
|
||||
// export const animationShadowBlueDarker = keyframes`
|
||||
// 0%, 100% {
|
||||
// box-shadow: 3px 3px 0 rgb(135, 206, 235), /* Sky Blue */ 6px 6px 0 rgb(70, 130, 180), /* Steel Blue */ 9px 9px 0 rgb(0, 128, 128); /* Teal */
|
||||
|
||||
@@ -4,6 +4,10 @@ import { isBrowser, isFirefox } from './pwaUtils';
|
||||
export function copyToClipboard(text: string, typeLabel: string) {
|
||||
if (!isBrowser)
|
||||
return;
|
||||
if (!window.navigator.clipboard?.writeText) {
|
||||
alert('Clipboard access is blocked. Please enable it in your browser settings.');
|
||||
return;
|
||||
}
|
||||
window.navigator.clipboard.writeText(text)
|
||||
.then(() => {
|
||||
addSnackbar({
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import type { StateStorage } from 'zustand/middleware';
|
||||
import { del as idbDel, get as idbGet, set as idbSet } from 'idb-keyval';
|
||||
|
||||
// used by the state storage middleware to detect data migration from the old state storage (localStorage)
|
||||
// NOTE: remove past 2024-03-19 (6 months past release of this utility conversion)
|
||||
export const IDB_MIGRATION_INITIAL = -1;
|
||||
|
||||
|
||||
// set to true to enable debugging
|
||||
const DEBUG_SCHEDULER = false;
|
||||
@@ -130,17 +126,6 @@ export const idbStateStorage: StateStorage = {
|
||||
if (DEBUG_SCHEDULER)
|
||||
console.warn(' (read bytes:', value?.length?.toLocaleString(), ')');
|
||||
|
||||
/* IMPORTANT!
|
||||
* We modify the default behavior of `getItem` to return a {version: -1} object if a key is not found.
|
||||
* This is to trigger the migration across state storage implementations, as Zustand would not call the
|
||||
* 'migrate' function otherwise.
|
||||
* See 'https://github.com/enricoros/big-agi/pull/158' for more details
|
||||
*/
|
||||
if (value === undefined) {
|
||||
return JSON.stringify({
|
||||
version: IDB_MIGRATION_INITIAL,
|
||||
});
|
||||
}
|
||||
return value || null;
|
||||
},
|
||||
setItem: (name: string, value: string): void => {
|
||||
|
||||
@@ -10,12 +10,7 @@
|
||||
* @param pdfBuffer The content of a PDF file
|
||||
*/
|
||||
export async function pdfToText(pdfBuffer: ArrayBuffer): Promise<string> {
|
||||
// Dynamically import the 'pdfjs-dist' library [nextjs]
|
||||
const { getDocument, GlobalWorkerOptions } = await import('pdfjs-dist');
|
||||
|
||||
// Set the worker script path
|
||||
GlobalWorkerOptions.workerSrc = '/workers/pdf.worker.min.mjs';
|
||||
|
||||
const { getDocument } = await dynamicImportPdfJs();
|
||||
const pdf = await getDocument(pdfBuffer).promise;
|
||||
const textPages: string[] = []; // Initialize an array to hold text from all pages
|
||||
|
||||
@@ -25,10 +20,81 @@ export async function pdfToText(pdfBuffer: ArrayBuffer): Promise<string> {
|
||||
const strings = content.items
|
||||
.filter(isTextItem) // Use the type guard to filter out items with the 'str' property
|
||||
.map((item) => (item as { str: string }).str); // Use type assertion to ensure that the item has the 'str' property
|
||||
textPages.push(strings.join(' ') + '\n'); // Add the joined strings to the array
|
||||
|
||||
// textPages.push(strings.join(' ')); // Add the joined strings to the array
|
||||
// New way: join the strings to form a page text. treat empty lines as newlines, otherwise join with a space (or not if the line is just 1 space)
|
||||
textPages.push(strings.reduce((acc, str) => {
|
||||
// empty line -> newline
|
||||
if (str === '')
|
||||
return acc + '\n';
|
||||
|
||||
// single space
|
||||
if (str === ' ')
|
||||
return acc + str;
|
||||
|
||||
// trick: de-hyphenation of consecutive lines
|
||||
if (/\w-$/.test(acc) && /^\w/.test(str))
|
||||
return acc.slice(0, -1) + str;
|
||||
|
||||
// add a space if the last char is not a space or return (regex)
|
||||
if (/\S$/.test(acc))
|
||||
return acc + ' ' + str;
|
||||
|
||||
// otherwise just concatenate
|
||||
return acc + str;
|
||||
}, ''));
|
||||
}
|
||||
return textPages.join('\n\n'); // Join all the page texts at the end
|
||||
}
|
||||
|
||||
|
||||
type PdfPageImage = { base64Url: string, scale: number, width: number, height: number };
|
||||
|
||||
/**
|
||||
* Renders all pages of a PDF to images
|
||||
*
|
||||
* @param pdfBuffer The content of a PDF file
|
||||
* @param scale The scale factor for the image resolution (default 1.5 for moderate quality)
|
||||
*/
|
||||
export async function pdfToImageDataURLs(pdfBuffer: ArrayBuffer, scale = 1.5): Promise<PdfPageImage[]> {
|
||||
const { getDocument } = await dynamicImportPdfJs();
|
||||
const pdf = await getDocument({ data: pdfBuffer }).promise;
|
||||
const images: PdfPageImage[] = [];
|
||||
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i);
|
||||
const viewport = page.getViewport({ scale });
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
await page.render({
|
||||
canvasContext: context!,
|
||||
viewport,
|
||||
}).promise;
|
||||
|
||||
images.push({
|
||||
base64Url: canvas.toDataURL('image/jpeg'),
|
||||
scale,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
});
|
||||
}
|
||||
|
||||
return textPages.join(''); // Join all the page texts at the end
|
||||
return images;
|
||||
}
|
||||
|
||||
|
||||
// Dynamically import the 'pdfjs-dist' library
|
||||
async function dynamicImportPdfJs() {
|
||||
// Dynamically import the 'pdfjs-dist' library [nextjs]
|
||||
const { getDocument, GlobalWorkerOptions } = await import('pdfjs-dist');
|
||||
|
||||
// Set the worker script path
|
||||
GlobalWorkerOptions.workerSrc = '/workers/pdf.worker.min.mjs';
|
||||
|
||||
return { getDocument };
|
||||
}
|
||||
|
||||
// Type guard to check if an item has a 'str' property
|
||||
|
||||
@@ -10,6 +10,11 @@ export const isMacUser = /Macintosh|MacIntel|MacPPC|Mac68K|iPad/.test(safeUA);
|
||||
export const isChromeDesktop = safeUA.includes('Chrome') && !safeUA.includes('Mobile');
|
||||
export const isFirefox = safeUA.includes('Firefox');
|
||||
|
||||
// frontend language
|
||||
const browserLang = isBrowser ? window.navigator.language : '';
|
||||
export const browserLangOrUS = browserLang || 'en-US';
|
||||
export const browserLangNotUS = browserLangOrUS !== 'en-US';
|
||||
|
||||
// deployment environment
|
||||
export const isVercelFromBackendOrSSR = !!process.env.VERCEL_ENV;
|
||||
export const isVercelFromFrontend = !!process.env.NEXT_PUBLIC_VERCEL_URL;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export function prettyTimestampForFilenames(useSeconds: boolean = true) {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, '0'); // JavaScript months are 0-based.
|
||||
const day = String(now.getDate()).padStart(2, '0');
|
||||
const hour = String(now.getHours()).padStart(2, '0');
|
||||
const minute = String(now.getMinutes()).padStart(2, '0');
|
||||
const second = String(now.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}-${hour}${minute}${useSeconds ? second : ''}`; // YYYY-MM-DD_HHMM[SS] format
|
||||
}
|
||||
@@ -6,14 +6,20 @@ import { DLLMId, findLLMOrThrow } from '~/modules/llms/store-llms';
|
||||
// Do not set this to true in production, it's very verbose
|
||||
const DEBUG_TOKEN_COUNT = false;
|
||||
|
||||
// Globals
|
||||
// const tokenEncodings: string[] = ['gpt2', 'r50k_base', 'p50k_base', 'p50k_edit', 'cl100k_base', 'o200k_base'] satisfies TiktokenEncoding[];
|
||||
|
||||
// global symbols to dynamically load the Tiktoken library
|
||||
// Global symbols to dynamically load the Tiktoken library
|
||||
let get_encoding: ((encoding: TiktokenEncoding) => Tiktoken) | null = null;
|
||||
let encoding_for_model: ((model: TiktokenModel) => Tiktoken) | null = null;
|
||||
let preloadPromise: Promise<void> | null = null;
|
||||
let informTheUser = false;
|
||||
|
||||
export function preloadTiktokenLibrary() {
|
||||
/**
|
||||
* Preloads the Tiktoken library if not already loaded.
|
||||
* @returns {Promise<void>} A promise that resolves when the library is loaded.
|
||||
*/
|
||||
export function preloadTiktokenLibrary(): Promise<void> {
|
||||
if (!preloadPromise) {
|
||||
preloadPromise = import('tiktoken')
|
||||
.then(tiktoken => {
|
||||
@@ -33,16 +39,21 @@ export function preloadTiktokenLibrary() {
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper around the Tiktoken library, to keep tokenizers for all models in a cache
|
||||
*
|
||||
* We also preload the tokenizer for the default model, so that the first time a user types
|
||||
* a message, it doesn't stall loading the tokenizer.
|
||||
* Wrapper around the Tiktoken library to keep tokenizers for all models in a cache.
|
||||
* Also, preloads the tokenizer for the default model to avoid initial stall.
|
||||
*/
|
||||
export const countModelTokens: (text: string, llmId: DLLMId, debugFrom: string) => number | null = (() => {
|
||||
// return () => 0;
|
||||
const tokenEncoders: { [modelId: string]: Tiktoken } = {};
|
||||
let encodingCL100K: Tiktoken | null = null;
|
||||
let encodingDefault: Tiktoken | null = null;
|
||||
|
||||
/**
|
||||
* Counts the tokens in the given text for the specified model.
|
||||
* @param {string} text - The text to tokenize.
|
||||
* @param {DLLMId} llmId - The ID of the LLM.
|
||||
* @param {string} debugFrom - Debug information.
|
||||
* @returns {number | null} The token count or null if not ready.
|
||||
*/
|
||||
function _tokenCount(text: string, llmId: DLLMId, debugFrom: string): number | null {
|
||||
|
||||
// The library shall have been preloaded - if not, attempt to start its loading and return null to indicate we're not ready to count
|
||||
@@ -55,21 +66,23 @@ export const countModelTokens: (text: string, llmId: DLLMId, debugFrom: string)
|
||||
return null;
|
||||
}
|
||||
|
||||
const { options: { llmRef: openaiModel } } = findLLMOrThrow(llmId);
|
||||
const openaiModel = findLLMOrThrow(llmId)?.options?.llmRef;
|
||||
if (!openaiModel) throw new Error(`LLM ${llmId} has no LLM reference id`);
|
||||
|
||||
if (!(openaiModel in tokenEncoders)) {
|
||||
try {
|
||||
tokenEncoders[openaiModel] = encoding_for_model(openaiModel as TiktokenModel);
|
||||
} catch (e) {
|
||||
// make sure we recycle the default encoding across all models
|
||||
if (!encodingCL100K)
|
||||
encodingCL100K = get_encoding('cl100k_base');
|
||||
tokenEncoders[openaiModel] = encodingCL100K;
|
||||
// fallback to the default encoding across all models (not just OpenAI - this will be used everywhere..)
|
||||
if (!encodingDefault)
|
||||
encodingDefault = get_encoding('cl100k_base');
|
||||
tokenEncoders[openaiModel] = encodingDefault;
|
||||
}
|
||||
}
|
||||
let count: number = 0;
|
||||
|
||||
// Note: the try/catch shouldn't be necessary, but there could be corner cases where the tiktoken library throws
|
||||
// https://github.com/enricoros/big-agi/issues/182
|
||||
let count = 0;
|
||||
try {
|
||||
count = tokenEncoders[openaiModel]?.encode(text, 'all', [])?.length || 0;
|
||||
} catch (e) {
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
* Functions to deal with HTML5Video elements.
|
||||
*/
|
||||
|
||||
import { prettyTimestampForFilenames } from './timeUtils';
|
||||
|
||||
export function downloadVideoFrameAsPNG(videoElement: HTMLVideoElement, prefixName: string) {
|
||||
// current video frame -> canvas -> dataURL PNG
|
||||
const renderedFrame = _renderVideoFrameToCanvas(videoElement);
|
||||
@@ -30,9 +32,8 @@ export async function renderVideoFrameAsPNGFile(videoElement: HTMLVideoElement,
|
||||
}
|
||||
|
||||
function _prettyFileName(prefixName: string, renderedFrame: HTMLCanvasElement) {
|
||||
const prettyDate = new Date().toISOString().replace(/[:-]/g, '').replace('T', '-').replace('Z', '');
|
||||
const prettyResolution = `${renderedFrame.width}x${renderedFrame.height}`;
|
||||
return `${prefixName}-${prettyDate}-${prettyResolution}.png`;
|
||||
return `${prefixName}_${prettyTimestampForFilenames()}_${prettyResolution}.png`;
|
||||
}
|
||||
|
||||
function _renderVideoFrameToCanvas(videoElement: HTMLVideoElement): HTMLCanvasElement {
|
||||
|
||||
+15
-2
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
|
||||
export type SystemPurposeId = 'Catalyst' | 'Custom' | 'Designer' | 'Developer' | 'DeveloperPreview' | 'Executive' | 'Generic' | 'Scientist';
|
||||
export type SystemPurposeId = 'Catalyst' | 'Custom' | 'Designer' | 'Developer' | 'DeveloperPreview' | 'Executive' | 'Generic' | 'Scientist' | 'YouTubeTranscriber';
|
||||
|
||||
export const defaultSystemPurposeId: SystemPurposeId = 'Generic';
|
||||
|
||||
@@ -96,7 +96,10 @@ Current date: {{LocaleNow}}
|
||||
Designer: {
|
||||
title: 'Designer',
|
||||
description: 'Helps you design',
|
||||
systemMessage: 'You are an AI visual design assistant. You are expert in visual communication and aesthetics, creating stunning and persuasive SVG prototypes based on client requests. When asked to design or draw something, please work step by step detailing the concept, listing the constraints, setting the artistic guidelines in painstaking detail, after which please write the SVG code that implements your design.',
|
||||
systemMessage: `
|
||||
You are an AI visual design assistant. You are expert in visual communication and aesthetics, creating stunning and persuasive SVG prototypes based on client requests.
|
||||
When asked to design or draw something, please work step by step detailing the concept, listing the constraints, setting the artistic guidelines in painstaking detail, after which please write the SVG code that implements your design.
|
||||
{{RenderSVG}}`.trim(),
|
||||
symbol: '🖌️',
|
||||
examples: ['minimalist logo for a tech startup', 'infographic on climate change', 'suggest color schemes for a website'],
|
||||
call: { starters: ['Hey! What\'s the vision?', 'Designer on call. What\'s the project?', 'Ready for design talk.', 'Hey.'] },
|
||||
@@ -110,4 +113,14 @@ Current date: {{LocaleNow}}
|
||||
call: { starters: ['What\'s the task?', 'What can I do?', 'Ready for your task.', 'Yes?'] },
|
||||
voices: { elevenLabs: { voiceId: 'flq6f7yk4E4fJM5XTYuZ' } },
|
||||
},
|
||||
YouTubeTranscriber: {
|
||||
title: 'YouTube Transcriber',
|
||||
description: 'Enter a YouTube URL to get the transcript and chat about the content.',
|
||||
systemMessage: 'You are an expert in understanding video transcripts and answering questions about video content.',
|
||||
symbol: '📺',
|
||||
examples: ['Analyze the sentiment of this video', 'Summarize the key points of the lecture'],
|
||||
call: { starters: ['Enter a YouTube URL to begin.', 'Ready to transcribe YouTube content.', 'Paste the YouTube link here.'] },
|
||||
voices: { elevenLabs: { voiceId: 'z9fAnlkpzviPz146aGWa' } },
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { llmChatGenerateOrThrow, VChatFunctionIn } from '~/modules/llms/llm.client';
|
||||
import { llmChatGenerateOrThrow, VChatFunctionIn, VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
import { useModelsStore } from '~/modules/llms/store-llms';
|
||||
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
@@ -83,13 +83,18 @@ export function autoSuggestions(conversationId: string, assistantMessageId: stri
|
||||
|
||||
// Follow-up: Auto-Diagrams
|
||||
if (suggestDiagrams) {
|
||||
void llmChatGenerateOrThrow(funcLLMId, [
|
||||
{ role: 'system', content: systemMessage.text },
|
||||
{ role: 'user', content: userMessage.text },
|
||||
{ role: 'assistant', content: assistantMessageText },
|
||||
], [suggestPlantUMLFn], 'draw_plantuml_diagram',
|
||||
const instructions: VChatMessageIn[] = [
|
||||
{ role: 'system', content: systemMessage.text },
|
||||
{ role: 'user', content: userMessage.text },
|
||||
{ role: 'assistant', content: assistantMessageText },
|
||||
];
|
||||
llmChatGenerateOrThrow(
|
||||
funcLLMId,
|
||||
instructions,
|
||||
'chat-followup-diagram', conversationId,
|
||||
[suggestPlantUMLFn], 'draw_plantuml_diagram',
|
||||
).then(chatResponse => {
|
||||
|
||||
// cheap way to check if the function was supported
|
||||
if (!('function_arguments' in chatResponse))
|
||||
return;
|
||||
|
||||
@@ -110,7 +115,8 @@ export function autoSuggestions(conversationId: string, assistantMessageId: stri
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('autoSuggestions::diagram:', err);
|
||||
// Likely the model did not support function calling
|
||||
// console.log('autoSuggestions: diagram error:', err);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getFastLLMId } from '~/modules/llms/store-llms';
|
||||
import { llmChatGenerateOrThrow } from '~/modules/llms/llm.client';
|
||||
import { llmChatGenerateOrThrow, VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
|
||||
import { useChatStore } from '~/common/state/store-chats';
|
||||
|
||||
@@ -34,21 +34,23 @@ export async function conversationAutoTitle(conversationId: string, forceReplace
|
||||
|
||||
try {
|
||||
// LLM chat-generate call
|
||||
const instructions: VChatMessageIn[] = [
|
||||
{ role: 'system', content: `You are an AI conversation titles assistant who specializes in creating expressive yet few-words chat titles.` },
|
||||
{
|
||||
role: 'user', content:
|
||||
'Analyze the given short conversation (every line is truncated) and extract a concise chat title that ' +
|
||||
'summarizes the conversation in as little as a couple of words.\n' +
|
||||
'Only respond with the lowercase short title and nothing else.\n' +
|
||||
'\n' +
|
||||
'```\n' +
|
||||
historyLines.join('\n') +
|
||||
'```\n',
|
||||
},
|
||||
];
|
||||
const chatResponse = await llmChatGenerateOrThrow(
|
||||
fastLLMId,
|
||||
[
|
||||
{ role: 'system', content: `You are an AI conversation titles assistant who specializes in creating expressive yet few-words chat titles.` },
|
||||
{
|
||||
role: 'user', content:
|
||||
'Analyze the given short conversation (every line is truncated) and extract a concise chat title that ' +
|
||||
'summarizes the conversation in as little as a couple of words.\n' +
|
||||
'Only respond with the lowercase short title and nothing else.\n' +
|
||||
'\n' +
|
||||
'```\n' +
|
||||
historyLines.join('\n') +
|
||||
'```\n',
|
||||
},
|
||||
],
|
||||
instructions,
|
||||
'chat-ai-title', conversationId,
|
||||
null, null,
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { Box, Button, ButtonGroup, CircularProgress, Divider, FormControl, FormLabel, Grid, IconButton, Input } from '@mui/joy';
|
||||
import AccountTreeTwoToneIcon from '@mui/icons-material/AccountTreeTwoTone';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ReplayIcon from '@mui/icons-material/Replay';
|
||||
@@ -13,6 +14,7 @@ import { llmStreamingChatGenerate } from '~/modules/llms/llm.client';
|
||||
|
||||
import { GoodModal } from '~/common/components/GoodModal';
|
||||
import { InlineError } from '~/common/components/InlineError';
|
||||
import { adjustContentScaling } from '~/common/app.theme';
|
||||
import { createDMessage, useChatStore } from '~/common/state/store-chats';
|
||||
import { useFormRadio } from '~/common/components/forms/useFormRadio';
|
||||
import { useFormRadioLlmType } from '~/common/components/forms/useFormRadioLlmType';
|
||||
@@ -22,6 +24,10 @@ import { useUIPreferencesStore } from '~/common/state/store-ui';
|
||||
import { bigDiagramPrompt, DiagramLanguage, diagramLanguages, DiagramType, diagramTypes } from './diagrams.data';
|
||||
|
||||
|
||||
// configuration
|
||||
const DIAGRAM_ACTOR_PREFIX = 'diagram';
|
||||
|
||||
|
||||
// Used by the callers to setup the diagam session
|
||||
export interface DiagramConfig {
|
||||
conversationId: string;
|
||||
@@ -37,7 +43,10 @@ function hotFixDiagramCode(llmCode: string): string {
|
||||
llmCode = '```\n' + llmCode + '\n```';
|
||||
// fix generation mistakes
|
||||
return llmCode
|
||||
.replaceAll('@endmindmap\n@enduml', '@endmindmap')
|
||||
.replaceAll('@startumd', '@startuml') // haiku
|
||||
.replaceAll('@endutml', '@enduml') // haiku
|
||||
.replaceAll('@endmindmap\n@enduml', '@endmindmap') // gpt-3.5
|
||||
.replaceAll('@endmindmap\n@end', '@endmindmap') // gpt-3.5
|
||||
.replaceAll('```\n```', '```');
|
||||
}
|
||||
|
||||
@@ -47,8 +56,8 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
|
||||
// state
|
||||
const [showOptions, setShowOptions] = React.useState(true);
|
||||
const [diagramCode, setDiagramCode] = React.useState<string | null>(null);
|
||||
const [diagramType, diagramComponent] = useFormRadio<DiagramType>('auto', diagramTypes, 'Visualize');
|
||||
const [diagramLanguage, languageComponent] = useFormRadio<DiagramLanguage>('plantuml', diagramLanguages, 'Style');
|
||||
const [diagramType, diagramComponent] = useFormRadio<DiagramType>('mind', diagramTypes, 'Diagram');
|
||||
const [diagramLanguage, languageComponent, setDiagramLanguage] = useFormRadio<DiagramLanguage>('mermaid', diagramLanguages, 'Syntax');
|
||||
const [customInstruction, setCustomInstruction] = React.useState<string>('');
|
||||
const [errorMessage, setErrorMessage] = React.useState<string | null>(null);
|
||||
const [abortController, setAbortController] = React.useState<AbortController | null>(null);
|
||||
@@ -56,10 +65,11 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
|
||||
// external state
|
||||
const isMobile = useIsMobile();
|
||||
const contentScaling = useUIPreferencesStore(state => state.contentScaling);
|
||||
const [diagramLlm, llmComponent] = useFormRadioLlmType('Generator');
|
||||
const [diagramLlm, llmComponent] = useFormRadioLlmType('Generator', 'chat');
|
||||
|
||||
// derived state
|
||||
const { conversationId, text: subject } = props.config;
|
||||
const { conversationId, messageId, text: subject } = props.config;
|
||||
const diagramLlmId = diagramLlm?.id;
|
||||
|
||||
|
||||
/**
|
||||
@@ -88,7 +98,7 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
|
||||
const diagramPrompt = bigDiagramPrompt(diagramType, diagramLanguage, systemMessage.text, subject, customInstruction);
|
||||
|
||||
try {
|
||||
await llmStreamingChatGenerate(diagramLlm.id, diagramPrompt, null, null, stepAbortController.signal,
|
||||
await llmStreamingChatGenerate(diagramLlm.id, diagramPrompt, 'ai-diagram', messageId, null, null, stepAbortController.signal,
|
||||
({ textSoFar }) => textSoFar && setDiagramCode(diagramCode = textSoFar),
|
||||
);
|
||||
} catch (error: any) {
|
||||
@@ -99,7 +109,7 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
|
||||
setAbortController(null);
|
||||
}
|
||||
|
||||
}, [abortController, conversationId, diagramLanguage, diagramLlm, diagramType, subject, customInstruction]);
|
||||
}, [abortController, conversationId, customInstruction, diagramLanguage, diagramLlm, diagramType, messageId, subject]);
|
||||
|
||||
|
||||
// [Effect] Auto-abort on unmount
|
||||
@@ -113,95 +123,146 @@ export function DiagramsModal(props: { config: DiagramConfig, onClose: () => voi
|
||||
}, [abortController]);
|
||||
|
||||
|
||||
const handleInsertAndClose = () => {
|
||||
// custom instruction
|
||||
|
||||
const handleCustomInstructionKeyDown = React.useCallback((event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void handleGenerateNew();
|
||||
}
|
||||
}, [handleGenerateNew]);
|
||||
|
||||
const handleCustomInstructionChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setCustomInstruction(event.target.value);
|
||||
}, []);
|
||||
|
||||
|
||||
// done
|
||||
|
||||
const handleAppendMessageAndClose = React.useCallback(() => {
|
||||
if (!diagramCode)
|
||||
return setErrorMessage('Nothing to add to the conversation.');
|
||||
|
||||
const diagramMessage = createDMessage('assistant', diagramCode);
|
||||
// diagramMessage.purposeId = conversation.systemPurposeId;
|
||||
diagramMessage.originLLM = 'diagram';
|
||||
diagramMessage.originLLM = DIAGRAM_ACTOR_PREFIX + (diagramLlmId ? `-${diagramLlmId}` : '');
|
||||
|
||||
useChatStore.getState().appendMessage(conversationId, diagramMessage);
|
||||
props.onClose();
|
||||
};
|
||||
}, [conversationId, diagramCode, diagramLlmId, props]);
|
||||
|
||||
|
||||
return <GoodModal
|
||||
title='Generate Diagram' noTitleBar
|
||||
open onClose={props.onClose}
|
||||
sx={{ maxWidth: { xs: '100vw', md: '95vw' } }}
|
||||
startButton={
|
||||
<Button variant='soft' color='success' disabled={!diagramCode || !!abortController} endDecorator={<TelegramIcon />} onClick={handleInsertAndClose}>
|
||||
Add To Chat
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
// [effect] Auto-switch language to match diagram type
|
||||
React.useEffect(() => {
|
||||
setDiagramLanguage(diagramType === 'mind' ? 'mermaid' : 'plantuml');
|
||||
}, [diagramType, setDiagramLanguage]);
|
||||
|
||||
{showOptions && (
|
||||
<Grid container spacing={2}>
|
||||
<Grid xs={12} md={6}>
|
||||
{diagramComponent}
|
||||
</Grid>
|
||||
{languageComponent && (
|
||||
|
||||
return (
|
||||
<GoodModal
|
||||
titleStartDecorator={<AutoFixHighIcon sx={{ fontSize: 'md', mr: 1 }} />}
|
||||
title={<>
|
||||
Auto-Diagram
|
||||
<IconButton
|
||||
aria-label={showOptions ? 'Hide Options' : 'Show Options'}
|
||||
size='sm'
|
||||
onClick={() => setShowOptions(options => !options)}
|
||||
sx={{ ml: 1, my: -0.5 }}
|
||||
>
|
||||
{showOptions ? <ExpandMoreIcon /> : <ExpandLessIcon />}
|
||||
</IconButton>
|
||||
</>}
|
||||
hideBottomClose
|
||||
open onClose={props.onClose}
|
||||
sx={{ maxWidth: { xs: '100vw', md: '95vw', lg: '88vw' } }}
|
||||
>
|
||||
|
||||
{showOptions && (
|
||||
<Grid container spacing={2}>
|
||||
<Grid xs={12} md={6}>
|
||||
{languageComponent}
|
||||
{diagramComponent}
|
||||
</Grid>
|
||||
{languageComponent && (
|
||||
<Grid xs={12} md={6}>
|
||||
{languageComponent}
|
||||
</Grid>
|
||||
)}
|
||||
<Grid xs={12} md={6}>
|
||||
{llmComponent}
|
||||
</Grid>
|
||||
<Grid xs={12} md={6}>
|
||||
<FormControl>
|
||||
<FormLabel>Customize</FormLabel>
|
||||
<Input
|
||||
title='Custom Instruction'
|
||||
placeholder='e.g. visualize as state'
|
||||
value={customInstruction}
|
||||
onKeyDown={handleCustomInstructionKeyDown}
|
||||
onChange={handleCustomInstructionChange}
|
||||
endDecorator={(abortController && customInstruction) ? <CircularProgress size='sm' /> : undefined}
|
||||
/>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid xs={12} xl={6}>
|
||||
{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>
|
||||
</Grid>
|
||||
)}
|
||||
)}
|
||||
|
||||
<ButtonGroup color='primary' sx={{ flexGrow: 1 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant={abortController ? 'soft' : 'solid'} color='primary'
|
||||
disabled={!diagramLlm}
|
||||
onClick={abortController ? () => abortController.abort() : handleGenerateNew}
|
||||
endDecorator={abortController ? <StopOutlinedIcon /> : diagramCode ? <ReplayIcon /> : <AccountTreeTwoToneIcon />}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
{abortController ? 'Stop' : diagramCode ? 'Regenerate' : 'Generate'}
|
||||
</Button>
|
||||
<IconButton onClick={() => setShowOptions(options => !options)}>
|
||||
{showOptions ? <ExpandLessIcon /> : <ExpandMoreIcon />}
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
{errorMessage && <InlineError error={errorMessage} />}
|
||||
|
||||
{errorMessage && <InlineError error={errorMessage} />}
|
||||
{!showOptions && !!abortController && <Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress size='lg' />
|
||||
</Box>}
|
||||
|
||||
{!showOptions && !!abortController && <Box sx={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<CircularProgress size='lg' />
|
||||
</Box>}
|
||||
{!!diagramCode && (!abortController || showOptions) && (
|
||||
<Box sx={{
|
||||
backgroundColor: 'background.level2',
|
||||
marginX: 'calc(-1 * var(--Card-padding))',
|
||||
minHeight: 96,
|
||||
p: { xs: 1, md: 2 },
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<BlocksRenderer
|
||||
text={diagramCode}
|
||||
fromRole='assistant'
|
||||
fitScreen={isMobile}
|
||||
contentScaling={adjustContentScaling(contentScaling, -1)}
|
||||
renderTextAsMarkdown={false}
|
||||
specialDiagramMode
|
||||
// onMessageEdit={(text) => setMessage({ ...message, text })}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!diagramCode && <Divider />}
|
||||
|
||||
{/* End */}
|
||||
<Box sx={{ mt: 'auto', display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between' }}>
|
||||
|
||||
{/* Add Message to Chat (once complete) */}
|
||||
<Button variant='soft' color='success' disabled={!diagramCode || !!abortController} endDecorator={<TelegramIcon />} onClick={handleAppendMessageAndClose}>
|
||||
Add To Chat
|
||||
</Button>
|
||||
|
||||
{/* Button Group to toggle controls visibility - NOT enabled at the moment */}
|
||||
<ButtonGroup variant='solid' color='primary' sx={{ ml: 'auto' }}>
|
||||
{/*<IconButton*/}
|
||||
{/* aria-label={showOptions ? 'Hide Options' : 'Show Options'}*/}
|
||||
{/* onClick={() => setShowOptions(options => !options)}*/}
|
||||
{/*>*/}
|
||||
{/* {showOptions ? <ExpandLessIcon /> : <ExpandMoreIcon />}*/}
|
||||
{/*</IconButton>*/}
|
||||
<Button
|
||||
variant={abortController ? 'soft' : 'solid'} color='primary'
|
||||
disabled={!diagramLlm}
|
||||
onClick={abortController ? () => abortController.abort() : handleGenerateNew}
|
||||
endDecorator={abortController ? <StopOutlinedIcon /> : diagramCode ? <ReplayIcon /> : <AccountTreeTwoToneIcon />}
|
||||
sx={{ minWidth: isMobile ? 160 : 220 }}
|
||||
>
|
||||
{abortController ? 'Stop' : diagramCode ? 'Regenerate' : 'Generate'}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
{!!diagramCode && (!abortController || showOptions) && (
|
||||
<Box sx={{
|
||||
backgroundColor: 'background.level2',
|
||||
marginX: 'calc(-1 * var(--Card-padding))',
|
||||
minHeight: 96,
|
||||
p: { xs: 1, md: 2 },
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
<BlocksRenderer
|
||||
text={diagramCode}
|
||||
fromRole='assistant'
|
||||
fitScreen={isMobile}
|
||||
contentScaling={contentScaling}
|
||||
renderTextAsMarkdown={false}
|
||||
specialDiagramMode
|
||||
// onMessageEdit={(text) => setMessage({ ...message, text })}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!diagramCode && <Divider />}
|
||||
|
||||
</GoodModal>;
|
||||
</GoodModal>
|
||||
);
|
||||
}
|
||||
@@ -7,7 +7,7 @@ export type DiagramLanguage = 'mermaid' | 'plantuml';
|
||||
|
||||
// NOTE: keep these global, or it will trigger re-renders
|
||||
export const diagramTypes: FormRadioOption<DiagramType>[] = [
|
||||
{ label: 'Auto-diagram', value: 'auto' },
|
||||
{ label: 'Automatic', value: 'auto' },
|
||||
{ label: 'Mindmap', value: 'mind' },
|
||||
];
|
||||
|
||||
@@ -16,7 +16,8 @@ export const diagramLanguages: FormRadioOption<DiagramLanguage>[] = [
|
||||
{ label: 'Mermaid (mindmaps)', value: 'mermaid' },
|
||||
];
|
||||
|
||||
const mermaidMindmapExample = `
|
||||
const mermaidMindmapExample = `For example:
|
||||
\`\`\`mermaid
|
||||
mindmap
|
||||
root((mindmap))
|
||||
Origins
|
||||
@@ -32,42 +33,43 @@ mindmap
|
||||
Tools
|
||||
Pen and paper
|
||||
Mermaid
|
||||
`.trim();
|
||||
|
||||
function mermaidDiagramPrompt(diagramType: DiagramType): { sys: string, usr: string } {
|
||||
let promptDetails = diagramType === 'auto'
|
||||
? 'You create a valid Mermaid diagram markdown (```mermaid\\n...), ready to be rendered into a diagram. Ensure the code contains no external references, and all names are properly enclosed in double quotes and escaped if necessary. Choose the most suitable diagram type from the following supported types: flowchart, sequence, class, state, erd, gantt, pie, git.'
|
||||
: 'You create a valid Mermaid mindmap markdown (```mermaid\\n...), ready to be rendered into a mind map. Ensure the code contains no external references, and all names are properly enclosed in double quotes and escaped if necessary. For example:\n' + mermaidMindmapExample + '\n';
|
||||
return {
|
||||
sys: `You are an AI that generates correct Mermaid code based on provided text. ${promptDetails}`,
|
||||
usr: `Generate the Mermaid code for a ${diagramType === 'auto' ? 'suitable diagram' : 'mind map'} that represents the preceding assistant message.`,
|
||||
};
|
||||
}
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
function plantumlDiagramPrompt(diagramType: DiagramType): { sys: string, usr: string } {
|
||||
switch (diagramType) {
|
||||
case 'auto':
|
||||
return {
|
||||
sys: 'You are an AI that writes PlantUML code based on provided text. You create a valid PlantUML string, enclosed by "```\n@startuml" and "@enduml\n```", ready to be rendered into a diagram or mindmap, ensuring the code contains no external references and all names are properly escaped without spaces. You choose the most suitable diagram type—sequence, class, use case, activity, component, state, object, deployment, wireframe, mindmap, gantt, or flowchart.',
|
||||
usr: 'Generate the PlantUML code for the diagram type that best represents the preceding assistant message.',
|
||||
sys: 'Generate a valid PlantUML diagram markdown (```plantuml\\n@startuml\\n...@enduml\\n```), ready for rendering. No external references allowed and all strings must be escaped correctly (each in a single line). Choose the most suitable PlantUML diagram type: sequence, class, use case, activity, component, state, object, deployment, wireframe, mindmap, gantt, or flowchart.',
|
||||
usr: 'Generate the PlantUML code for a suitable diagram that best captures the essence of the preceding message.',
|
||||
};
|
||||
case 'mind':
|
||||
return {
|
||||
sys: 'You are an AI that writes PlantUML code based on provided text. You create a valid PlantUML string, enclosed by "```\n@startmindmap" and "@endmindmap\n```", ready to be rendered into a mind map, ensuring the code contains no external references and all names are properly escaped without spaces.',
|
||||
usr: 'Generate the PlantUML code for a mind map based on the preceding assistant message.',
|
||||
sys: 'Generate a valid PlantUML mindmap markdown (```plantuml\\n@startmindmap\\n...@endmindmap\\n\`\`\`), ready for rendering. No external references allowed. Use one or more asterisks to indent and separate with spaces.',
|
||||
usr: 'Generate a PlantUML mindmap that effectively summarizes the key points from the preceding message.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function mermaidDiagramPrompt(diagramType: DiagramType): { sys: string, usr: string } {
|
||||
let promptDetails = diagramType === 'auto'
|
||||
? 'Generate a valid Mermaid diagram markdown (```mermaid\\n...```), ready for rendering. The code should have no external references and all names must be in double quotes and properly escaped. Select the most appropriate Mermaid diagram type: flowchart, sequence, class, state, erd, gantt, pie, or git.'
|
||||
: 'Generate a valid Mermaid mindmap markdown (```mermaid\\n...```), ready for rendering. The code should have no external references and all names must be in double quotes and properly escaped. ' + mermaidMindmapExample;
|
||||
return {
|
||||
sys: `Your task is to generate accurate and well-structured Mermaid code from the given text. ${promptDetails}`,
|
||||
usr: `Generate the Mermaid code for a ${diagramType === 'auto' ? 'suitable diagram' : 'mind map'} that ${diagramType === 'auto' ? 'best captures the essence' : 'effectively summarizes the key points'} of the preceding message.`,
|
||||
};
|
||||
}
|
||||
|
||||
const sysSuffixPM = 'The next three messages will outline: 1. your personality, 2. the data you\'ll work with, and 3. a clear restatement of the instructions.';
|
||||
const usrSuffixCoT = 'Please think step by step, then generate valid diagram code in a markdown block as instructed, and stop your response.';
|
||||
|
||||
export function bigDiagramPrompt(diagramType: DiagramType, diagramLanguage: DiagramLanguage, chatSystemPrompt: string, subject: string, customInstruction: string): VChatMessageIn[] {
|
||||
const { sys, usr } = diagramLanguage === 'mermaid' ? mermaidDiagramPrompt(diagramType) : plantumlDiagramPrompt(diagramType);
|
||||
if (customInstruction) {
|
||||
customInstruction = 'Also consider the following instructions: ' + customInstruction;
|
||||
}
|
||||
return [
|
||||
{ role: 'system', content: sys },
|
||||
{ role: 'system', content: chatSystemPrompt },
|
||||
{ role: 'system', content: sys + '\n' + sysSuffixPM },
|
||||
{ role: 'user', content: chatSystemPrompt },
|
||||
{ role: 'assistant', content: subject },
|
||||
{ role: 'user', content: `${usr} ${customInstruction}` },
|
||||
{ role: 'user', content: (!customInstruction?.trim() ? usr : `${usr} Also consider the following instructions: ${customInstruction.trim()}`) + '\n' + usrSuffixCoT },
|
||||
];
|
||||
}
|
||||
@@ -117,7 +117,7 @@ export function FlattenerModal(props: {
|
||||
await startStreaming(llm.id, [
|
||||
{ role: 'system', content: flattenProfile.systemPrompt },
|
||||
{ role: 'user', content: encodeConversationAsUserMessage(flattenProfile.userPrompt, messages) },
|
||||
]);
|
||||
], 'ai-flattener', messages[0].id);
|
||||
|
||||
}, [llm, props.conversationId, startStreaming]);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getFastLLMId } from '~/modules/llms/store-llms';
|
||||
import { llmChatGenerateOrThrow } from '~/modules/llms/llm.client';
|
||||
import { llmChatGenerateOrThrow, VChatMessageIn } from '~/modules/llms/llm.client';
|
||||
|
||||
|
||||
const simpleImagineSystemPrompt =
|
||||
@@ -10,14 +10,15 @@ Provide output as a lowercase prompt and nothing else.`;
|
||||
/**
|
||||
* Creates a caption for a drawing or photo given some description - used to elevate the quality of the imaging
|
||||
*/
|
||||
export async function imaginePromptFromText(messageText: string): Promise<string | null> {
|
||||
export async function imaginePromptFromText(messageText: string, contextRef: string): Promise<string | null> {
|
||||
const fastLLMId = getFastLLMId();
|
||||
if (!fastLLMId) return null;
|
||||
try {
|
||||
const chatResponse = await llmChatGenerateOrThrow(fastLLMId, [
|
||||
const instructions: VChatMessageIn[] = [
|
||||
{ role: 'system', content: simpleImagineSystemPrompt },
|
||||
{ role: 'user', content: 'Write a prompt, based on the following input.\n\n```\n' + messageText.slice(0, 1000) + '\n```\n' },
|
||||
], null, null);
|
||||
];
|
||||
const chatResponse = await llmChatGenerateOrThrow(fastLLMId, instructions, 'draw-expand-prompt', contextRef, null, null);
|
||||
return chatResponse.content?.trim() ?? null;
|
||||
} catch (error: any) {
|
||||
console.error('imaginePromptFromText: fetch request error:', error);
|
||||
|
||||
@@ -132,7 +132,7 @@ export class Agent {
|
||||
S.messages.push({ role: 'user', content: prompt });
|
||||
let content: string;
|
||||
try {
|
||||
content = (await llmChatGenerateOrThrow(llmId, S.messages, null, null, 500)).content;
|
||||
content = (await llmChatGenerateOrThrow(llmId, S.messages, 'chat-react-turn', null, null, null, 500)).content;
|
||||
} catch (error: any) {
|
||||
content = `Error in llmChatGenerateOrThrow: ${error}`;
|
||||
}
|
||||
@@ -194,7 +194,8 @@ async function search(query: string): Promise<string> {
|
||||
async function browse(url: string): Promise<string> {
|
||||
try {
|
||||
const page = await callBrowseFetchPage(url);
|
||||
return JSON.stringify(page.content ? { text: page.content } : { error: 'Issue reading the page' });
|
||||
const pageContent = page.content.markdown || page.content.text || page.content.html || '';
|
||||
return JSON.stringify(pageContent ? { text: pageContent } : { error: 'Issue reading the page' });
|
||||
} catch (error) {
|
||||
console.error('Error browsing:', (error as Error).message);
|
||||
return 'An error occurred while browsing to the URL. Missing WSS Key?';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user