Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 65d1af5d06 | |||
| ec20db24de | |||
| 1ebae96dca | |||
| 1594f4d29e | |||
| 8ef65cf5dd | |||
| 683530e522 | |||
| cf93a32851 |
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
node_modules
|
node_modules
|
||||||
test-results
|
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
# Chatbot UI
|
|
||||||
DEFAULT_MODEL=gpt-3.5-turbo
|
|
||||||
DEFAULT_SYSTEM_PROMPT=You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.
|
|
||||||
OPENAI_API_KEY=YOUR_KEY
|
|
||||||
|
|
||||||
# Google
|
|
||||||
GOOGLE_API_KEY=YOUR_API_KEY
|
|
||||||
GOOGLE_CSE_ID=YOUR_ENGINE_ID
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
name: Docker
|
|
||||||
|
|
||||||
# This workflow uses actions that are not certified by GitHub.
|
|
||||||
# They are provided by a third-party and are governed by
|
|
||||||
# separate terms of service, privacy policy, and support
|
|
||||||
# documentation.
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ "main" ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
# Use docker.io for Docker Hub if empty
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
# github.repository as <account>/<repo>
|
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
# This is used to complete the identity challenge
|
|
||||||
# with sigstore/fulcio when running outside of PRs.
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
# Workaround: https://github.com/docker/build-push-action/issues/461
|
|
||||||
- name: Setup Docker buildx
|
|
||||||
uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf
|
|
||||||
|
|
||||||
# Login against a Docker registry except on PR
|
|
||||||
# https://github.com/docker/login-action
|
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
# Extract metadata (tags, labels) for Docker
|
|
||||||
# https://github.com/docker/metadata-action
|
|
||||||
- name: Extract Docker metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
|
|
||||||
# Build and push Docker image with Buildx (don't push on PR)
|
|
||||||
# https://github.com/docker/build-push-action
|
|
||||||
- name: Build and push Docker image
|
|
||||||
id: build-and-push
|
|
||||||
uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
name: Run Unit Tests
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: node:16
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Run Vitest Suite
|
|
||||||
run: npm test
|
|
||||||
+2
-3
@@ -7,12 +7,11 @@
|
|||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
/test-results
|
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
/dist
|
dist
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/build
|
||||||
@@ -36,4 +35,4 @@ yarn-error.log*
|
|||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
.idea
|
.idea
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
# Contributing Guidelines
|
|
||||||
**Welcome to Chatbot UI!**
|
|
||||||
|
|
||||||
We appreciate your interest in contributing to our project.
|
|
||||||
|
|
||||||
Before you get started, please read our guidelines for contributing.
|
|
||||||
|
|
||||||
## Types of Contributions
|
|
||||||
We welcome the following types of contributions:
|
|
||||||
|
|
||||||
- Bug fixes
|
|
||||||
- New features
|
|
||||||
- Documentation improvements
|
|
||||||
- Code optimizations
|
|
||||||
- Translations
|
|
||||||
- Tests
|
|
||||||
|
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
To get started, fork the project on GitHub and clone it locally on your machine. Then, create a new branch to work on your changes.
|
|
||||||
|
|
||||||
```
|
|
||||||
git clone https://github.com/mckaywrigley/chatbot-ui.git
|
|
||||||
cd chatbot-ui
|
|
||||||
git checkout -b my-branch-name
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Before submitting your pull request, please make sure your changes pass our automated tests and adhere to our code style guidelines.
|
|
||||||
|
|
||||||
## Pull Request Process
|
|
||||||
1. Fork the project on GitHub.
|
|
||||||
2. Clone your forked repository locally on your machine.
|
|
||||||
3. Create a new branch from the main branch.
|
|
||||||
4. Make your changes on the new branch.
|
|
||||||
5. Ensure that your changes adhere to our code style guidelines and pass our automated tests.
|
|
||||||
6. Commit your changes and push them to your forked repository.
|
|
||||||
7. Submit a pull request to the main branch of the main repository.
|
|
||||||
|
|
||||||
## Contact
|
|
||||||
If you have any questions or need help getting started, feel free to reach out to me on [Twitter](https://twitter.com/mckaywrigley).
|
|
||||||
@@ -19,8 +19,6 @@ COPY --from=dependencies /app/node_modules ./node_modules
|
|||||||
COPY --from=build /app/.next ./.next
|
COPY --from=build /app/.next ./.next
|
||||||
COPY --from=build /app/public ./public
|
COPY --from=build /app/public ./public
|
||||||
COPY --from=build /app/package*.json ./
|
COPY --from=build /app/package*.json ./
|
||||||
COPY --from=build /app/next.config.js ./next.config.js
|
|
||||||
COPY --from=build /app/next-i18next.config.js ./next-i18next.config.js
|
|
||||||
|
|
||||||
# Expose the port the app will run on
|
# Expose the port the app will run on
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
include .env
|
|
||||||
|
|
||||||
.PHONY: all
|
|
||||||
|
|
||||||
build:
|
|
||||||
docker build -t chatbot-ui .
|
|
||||||
|
|
||||||
run:
|
|
||||||
export $(cat .env | xargs)
|
|
||||||
docker stop chatbot-ui || true && docker rm chatbot-ui || true
|
|
||||||
docker run --name chatbot-ui --rm -e OPENAI_API_KEY=${OPENAI_API_KEY} -p 3000:3000 chatbot-ui
|
|
||||||
|
|
||||||
logs:
|
|
||||||
docker logs -f chatbot-ui
|
|
||||||
|
|
||||||
push:
|
|
||||||
docker tag chatbot-ui:latest ${DOCKER_USER}/chatbot-ui:${DOCKER_TAG}
|
|
||||||
docker push ${DOCKER_USER}/chatbot-ui:${DOCKER_TAG}
|
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
# Chatbot UI
|
# Chatbot UI
|
||||||
|
|
||||||
|
**Note: Chatbot UI Pro has been renamed to Chatbot UI.**
|
||||||
|
|
||||||
Chatbot UI is an advanced chatbot kit for OpenAI's chat models built on top of [Chatbot UI Lite](https://github.com/mckaywrigley/chatbot-ui-lite) using Next.js, TypeScript, and Tailwind CSS.
|
Chatbot UI is an advanced chatbot kit for OpenAI's chat models built on top of [Chatbot UI Lite](https://github.com/mckaywrigley/chatbot-ui-lite) using Next.js, TypeScript, and Tailwind CSS.
|
||||||
|
|
||||||
See a [demo](https://twitter.com/mckaywrigley/status/1640380021423603713?s=46&t=AowqkodyK6B4JccSOxSPew).
|
It aims to mimic ChatGPT's interface and functionality.
|
||||||
|
|
||||||
|
All conversations are stored locally on your device.
|
||||||
|
|
||||||
|
See a [demo](https://twitter.com/mckaywrigley/status/1636103188733640704).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -14,18 +20,14 @@ Expect frequent improvements.
|
|||||||
|
|
||||||
**Next up:**
|
**Next up:**
|
||||||
|
|
||||||
- [ ] Delete messages
|
- [ ] More custom model settings
|
||||||
- [ ] More model settings
|
- [ ] Regenerate & edit responses
|
||||||
- [ ] Plugins
|
- [ ] Saving via data export
|
||||||
|
- [ ] Folders
|
||||||
|
- [ ] Prompt templates
|
||||||
|
|
||||||
**Recent updates:**
|
**Recent updates:**
|
||||||
|
|
||||||
- [x] Prompt templates (3/27/23)
|
|
||||||
- [x] Regenerate & edit responses (3/25/23)
|
|
||||||
- [x] Folders (3/24/23)
|
|
||||||
- [x] Search chat content (3/23/23)
|
|
||||||
- [x] Stop message generation (3/22/23)
|
|
||||||
- [x] Import/Export chats (3/22/23)
|
|
||||||
- [x] Custom system prompt (3/21/23)
|
- [x] Custom system prompt (3/21/23)
|
||||||
- [x] Error handling (3/20/23)
|
- [x] Error handling (3/20/23)
|
||||||
- [x] GPT-4 support (access required) (3/20/23)
|
- [x] GPT-4 support (access required) (3/20/23)
|
||||||
@@ -59,17 +61,15 @@ Fork Chatbot UI on Replit [here](https://replit.com/@MckayWrigley/chatbot-ui-pro
|
|||||||
|
|
||||||
**Docker**
|
**Docker**
|
||||||
|
|
||||||
Build locally:
|
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
docker build -t chatgpt-ui .
|
docker build -t chatgpt-ui .
|
||||||
docker run -e OPENAI_API_KEY=xxxxxxxx -p 3000:3000 chatgpt-ui
|
docker run -e OPENAI_API_KEY=xxxxxxxx -p 3000:3000 chatgpt-ui
|
||||||
```
|
```
|
||||||
|
|
||||||
Pull from ghcr:
|
**Desktop App**
|
||||||
|
|
||||||
```
|
```
|
||||||
docker run -e OPENAI_API_KEY=xxxxxxxx -p 3000:3000 ghcr.io/mckaywrigley/chatbot-ui:main
|
npm run tauri build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Running Locally
|
## Running Locally
|
||||||
@@ -94,10 +94,6 @@ Create a .env.local file in the root of the repo with your OpenAI API Key:
|
|||||||
OPENAI_API_KEY=YOUR_KEY
|
OPENAI_API_KEY=YOUR_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
> You can set `OPENAI_API_HOST` where access to the official OpenAI host is restricted or unavailable, allowing users to configure an alternative host for their specific needs.
|
|
||||||
|
|
||||||
> Additionally, if you have multiple OpenAI Organizations, you can set `OPENAI_ORGANIZATION` to specify one.
|
|
||||||
|
|
||||||
**4. Run App**
|
**4. Run App**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -108,19 +104,6 @@ npm run dev
|
|||||||
|
|
||||||
You should be able to start chatting.
|
You should be able to start chatting.
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
When deploying the application, the following environment variables can be set:
|
|
||||||
|
|
||||||
| Environment Variable | Default value | Description |
|
|
||||||
| --------------------- | ------------------------------ | ------------------------------------------------------- |
|
|
||||||
| OPENAI_API_KEY | | The default API key used for authentication with OpenAI |
|
|
||||||
| DEFAULT_MODEL | `gpt-3.5-turbo` | The default model to use on new conversations |
|
|
||||||
| DEFAULT_SYSTEM_PROMPT | [see here](utils/app/const.ts) | The defaut system prompt to use on new conversations |
|
|
||||||
|
|
||||||
If you do not provide an OpenAI API key with `OPENAI_API_KEY`, users will have to provide their own key.
|
|
||||||
If you don't have an OpenAI API key, you can get one [here](https://platform.openai.com/account/api-keys).
|
|
||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
If you have any questions, feel free to reach out to me on [Twitter](https://twitter.com/mckaywrigley).
|
If you have any questions, feel free to reach out to me on [Twitter](https://twitter.com/mckaywrigley).
|
||||||
|
|||||||
@@ -1,261 +0,0 @@
|
|||||||
import { ExportFormatV1, ExportFormatV2, ExportFormatV4 } from '@/types/export';
|
|
||||||
import { OpenAIModels, OpenAIModelID } from '@/types/openai';
|
|
||||||
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
|
||||||
import { it, describe, expect } from 'vitest';
|
|
||||||
|
|
||||||
import {
|
|
||||||
cleanData,
|
|
||||||
isExportFormatV1,
|
|
||||||
isExportFormatV2,
|
|
||||||
isExportFormatV3,
|
|
||||||
isExportFormatV4,
|
|
||||||
isLatestExportFormat,
|
|
||||||
} from '@/utils/app/importExport';
|
|
||||||
|
|
||||||
describe('Export Format Functions', () => {
|
|
||||||
describe('isExportFormatV1', () => {
|
|
||||||
it('should return true for v1 format', () => {
|
|
||||||
const obj = [{ id: 1 }];
|
|
||||||
expect(isExportFormatV1(obj)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for non-v1 formats', () => {
|
|
||||||
const obj = { version: 3, history: [], folders: [] };
|
|
||||||
expect(isExportFormatV1(obj)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isExportFormatV2', () => {
|
|
||||||
it('should return true for v2 format', () => {
|
|
||||||
const obj = { history: [], folders: [] };
|
|
||||||
expect(isExportFormatV2(obj)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for non-v2 formats', () => {
|
|
||||||
const obj = { version: 3, history: [], folders: [] };
|
|
||||||
expect(isExportFormatV2(obj)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isExportFormatV3', () => {
|
|
||||||
it('should return true for v3 format', () => {
|
|
||||||
const obj = { version: 3, history: [], folders: [] };
|
|
||||||
expect(isExportFormatV3(obj)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for non-v3 formats', () => {
|
|
||||||
const obj = { version: 4, history: [], folders: [] };
|
|
||||||
expect(isExportFormatV3(obj)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('isExportFormatV4', () => {
|
|
||||||
it('should return true for v4 format', () => {
|
|
||||||
const obj = { version: 4, history: [], folders: [], prompts: [] };
|
|
||||||
expect(isExportFormatV4(obj)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false for non-v4 formats', () => {
|
|
||||||
const obj = { version: 5, history: [], folders: [], prompts: [] };
|
|
||||||
expect(isExportFormatV4(obj)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cleanData Functions', () => {
|
|
||||||
describe('cleaning v1 data', () => {
|
|
||||||
it('should return the latest format', () => {
|
|
||||||
const data = [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'conversation 1',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: "what's up ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Hi',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
] as ExportFormatV1;
|
|
||||||
const obj = cleanData(data);
|
|
||||||
expect(isLatestExportFormat(obj)).toBe(true);
|
|
||||||
expect(obj).toEqual({
|
|
||||||
version: 4,
|
|
||||||
history: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'conversation 1',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: "what's up ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Hi',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
|
||||||
folderId: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
folders: [],
|
|
||||||
prompts:[]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cleaning v2 data', () => {
|
|
||||||
it('should return the latest format', () => {
|
|
||||||
const data = {
|
|
||||||
history: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'conversation 1',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: "what's up ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Hi',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
folders: [
|
|
||||||
{
|
|
||||||
id: 1,
|
|
||||||
name: 'folder 1',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as ExportFormatV2;
|
|
||||||
const obj = cleanData(data);
|
|
||||||
expect(isLatestExportFormat(obj)).toBe(true);
|
|
||||||
expect(obj).toEqual({
|
|
||||||
version: 4,
|
|
||||||
history: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'conversation 1',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: "what's up ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Hi',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
|
||||||
folderId: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
folders: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'folder 1',
|
|
||||||
type: 'chat',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
prompts: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('cleaning v4 data', () => {
|
|
||||||
it('should return the latest format', () => {
|
|
||||||
const data = {
|
|
||||||
version: 4,
|
|
||||||
history: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'conversation 1',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: "what's up ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Hi',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
|
||||||
folderId: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
folders: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'folder 1',
|
|
||||||
type: 'chat',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
prompts: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'prompt 1',
|
|
||||||
description: '',
|
|
||||||
content: '',
|
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
|
||||||
folderId: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as ExportFormatV4;
|
|
||||||
|
|
||||||
const obj = cleanData(data);
|
|
||||||
expect(isLatestExportFormat(obj)).toBe(true);
|
|
||||||
expect(obj).toEqual({
|
|
||||||
version: 4,
|
|
||||||
history: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'conversation 1',
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'user',
|
|
||||||
content: "what's up ?",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: 'assistant',
|
|
||||||
content: 'Hi',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
|
||||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
|
||||||
folderId: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
folders: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'folder 1',
|
|
||||||
type: 'chat',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
prompts: [
|
|
||||||
{
|
|
||||||
id: '1',
|
|
||||||
name: 'prompt 1',
|
|
||||||
description: '',
|
|
||||||
content: '',
|
|
||||||
model: OpenAIModels[OpenAIModelID.GPT_3_5],
|
|
||||||
folderId: null,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
+88
-309
@@ -1,334 +1,113 @@
|
|||||||
import { Conversation, Message } from '@/types/chat';
|
import { Conversation, KeyValuePair, Message, OpenAIModel } from "@/types";
|
||||||
import { KeyValuePair } from '@/types/data';
|
import { FC, MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
import { ErrorMessage } from '@/types/error';
|
import { ChatInput } from "./ChatInput";
|
||||||
import { OpenAIModel, OpenAIModelID } from '@/types/openai';
|
import { ChatLoader } from "./ChatLoader";
|
||||||
import { Plugin } from '@/types/plugin';
|
import { ChatMessage } from "./ChatMessage";
|
||||||
import { Prompt } from '@/types/prompt';
|
import { ModelSelect } from "./ModelSelect";
|
||||||
import { throttle } from '@/utils';
|
import { Regenerate } from "./Regenerate";
|
||||||
import { IconArrowDown, IconClearAll, IconSettings } from '@tabler/icons-react';
|
import { SystemPrompt } from "./SystemPrompt";
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import {
|
|
||||||
FC,
|
|
||||||
MutableRefObject,
|
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { Spinner } from '../Global/Spinner';
|
|
||||||
import { ChatInput } from './ChatInput';
|
|
||||||
import { ChatLoader } from './ChatLoader';
|
|
||||||
import { ChatMessage } from './ChatMessage';
|
|
||||||
import { ErrorMessageDiv } from './ErrorMessageDiv';
|
|
||||||
import { ModelSelect } from './ModelSelect';
|
|
||||||
import { SystemPrompt } from './SystemPrompt';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
conversation: Conversation;
|
conversation: Conversation;
|
||||||
models: OpenAIModel[];
|
models: OpenAIModel[];
|
||||||
apiKey: string;
|
|
||||||
serverSideApiKeyIsSet: boolean;
|
|
||||||
defaultModelId: OpenAIModelID;
|
|
||||||
messageIsStreaming: boolean;
|
messageIsStreaming: boolean;
|
||||||
modelError: ErrorMessage | null;
|
modelError: boolean;
|
||||||
|
messageError: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
prompts: Prompt[];
|
lightMode: "light" | "dark";
|
||||||
onSend: (
|
onSend: (message: Message, isResend: boolean) => void;
|
||||||
message: Message,
|
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||||
deleteCount: number,
|
|
||||||
plugin: Plugin | null,
|
|
||||||
) => void;
|
|
||||||
onUpdateConversation: (
|
|
||||||
conversation: Conversation,
|
|
||||||
data: KeyValuePair,
|
|
||||||
) => void;
|
|
||||||
onEditMessage: (message: Message, messageIndex: number) => void;
|
|
||||||
stopConversationRef: MutableRefObject<boolean>;
|
stopConversationRef: MutableRefObject<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Chat: FC<Props> = memo(
|
export const Chat: FC<Props> = ({ conversation, models, messageIsStreaming, modelError, messageError, loading, lightMode, onSend, onUpdateConversation, stopConversationRef }) => {
|
||||||
({
|
const [currentMessage, setCurrentMessage] = useState<Message>();
|
||||||
conversation,
|
|
||||||
models,
|
|
||||||
apiKey,
|
|
||||||
serverSideApiKeyIsSet,
|
|
||||||
defaultModelId,
|
|
||||||
messageIsStreaming,
|
|
||||||
modelError,
|
|
||||||
loading,
|
|
||||||
prompts,
|
|
||||||
onSend,
|
|
||||||
onUpdateConversation,
|
|
||||||
onEditMessage,
|
|
||||||
stopConversationRef,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('chat');
|
|
||||||
const [currentMessage, setCurrentMessage] = useState<Message>();
|
|
||||||
const [autoScrollEnabled, setAutoScrollEnabled] = useState<boolean>(true);
|
|
||||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
|
||||||
const [showScrollDownButton, setShowScrollDownButton] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const chatContainerRef = useRef<HTMLDivElement>(null);
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
const scrollToBottom = useCallback(() => {
|
const scrollToBottom = () => {
|
||||||
if (autoScrollEnabled) {
|
messagesEndRef.current?.scrollIntoView({ behavior: "auto" });
|
||||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
};
|
||||||
textareaRef.current?.focus();
|
|
||||||
}
|
|
||||||
}, [autoScrollEnabled]);
|
|
||||||
|
|
||||||
const handleScroll = () => {
|
useEffect(() => {
|
||||||
if (chatContainerRef.current) {
|
scrollToBottom();
|
||||||
const { scrollTop, scrollHeight, clientHeight } =
|
}, [conversation.messages]);
|
||||||
chatContainerRef.current;
|
|
||||||
const bottomTolerance = 30;
|
|
||||||
|
|
||||||
if (scrollTop + clientHeight < scrollHeight - bottomTolerance) {
|
return (
|
||||||
setAutoScrollEnabled(false);
|
<div className="relative flex-1 overflow-none dark:bg-[#343541] bg-white">
|
||||||
setShowScrollDownButton(true);
|
{modelError ? (
|
||||||
} else {
|
<div className="flex flex-col justify-center mx-auto h-full w-[300px] sm:w-[500px] space-y-6">
|
||||||
setAutoScrollEnabled(true);
|
<div className="text-center text-red-500">Error fetching models.</div>
|
||||||
setShowScrollDownButton(false);
|
<div className="text-center text-red-500">Make sure your OpenAI API key is set in the bottom left of the sidebar or in a .env.local file and refresh.</div>
|
||||||
}
|
<div className="text-center text-red-500">If you completed this step, OpenAI may be experiencing issues.</div>
|
||||||
}
|
</div>
|
||||||
};
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="overflow-scroll max-h-full">
|
||||||
|
{conversation.messages.length === 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-col mx-auto pt-12 space-y-10 w-[350px] sm:w-[600px]">
|
||||||
|
<div className="text-4xl font-semibold text-center text-gray-800 dark:text-gray-100">{models.length === 0 ? "Loading..." : "Chatbot UI"}</div>
|
||||||
|
|
||||||
const handleScrollDown = () => {
|
{models.length > 0 && (
|
||||||
chatContainerRef.current?.scrollTo({
|
<div className="flex flex-col h-full space-y-4 border p-4 rounded border-neutral-500">
|
||||||
top: chatContainerRef.current.scrollHeight,
|
<ModelSelect
|
||||||
behavior: 'smooth',
|
model={conversation.model}
|
||||||
});
|
models={models}
|
||||||
};
|
onModelChange={(model) => onUpdateConversation(conversation, { key: "model", value: model })}
|
||||||
|
/>
|
||||||
|
|
||||||
const handleSettings = () => {
|
<SystemPrompt
|
||||||
setShowSettings(!showSettings);
|
conversation={conversation}
|
||||||
};
|
onChangePrompt={(prompt) => onUpdateConversation(conversation, { key: "prompt", value: prompt })}
|
||||||
|
/>
|
||||||
const onClearAll = () => {
|
|
||||||
if (confirm(t<string>('Are you sure you want to clear all messages?'))) {
|
|
||||||
onUpdateConversation(conversation, { key: 'messages', value: [] });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollDown = () => {
|
|
||||||
if (autoScrollEnabled) {
|
|
||||||
messagesEndRef.current?.scrollIntoView(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const throttledScrollDown = throttle(scrollDown, 250);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
throttledScrollDown();
|
|
||||||
setCurrentMessage(
|
|
||||||
conversation.messages[conversation.messages.length - 2],
|
|
||||||
);
|
|
||||||
}, [conversation.messages, throttledScrollDown]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([entry]) => {
|
|
||||||
setAutoScrollEnabled(entry.isIntersecting);
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
textareaRef.current?.focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
root: null,
|
|
||||||
threshold: 0.5,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const messagesEndElement = messagesEndRef.current;
|
|
||||||
if (messagesEndElement) {
|
|
||||||
observer.observe(messagesEndElement);
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
if (messagesEndElement) {
|
|
||||||
observer.unobserve(messagesEndElement);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}, [messagesEndRef]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex-1 overflow-hidden bg-white dark:bg-[#343541]">
|
|
||||||
{!(apiKey || serverSideApiKeyIsSet) ? (
|
|
||||||
<div className="mx-auto flex h-full w-[300px] flex-col justify-center space-y-6 sm:w-[600px]">
|
|
||||||
<div className="text-center text-4xl font-bold text-black dark:text-white">
|
|
||||||
Welcome to Chatbot UI
|
|
||||||
</div>
|
|
||||||
<div className="text-center text-lg text-black dark:text-white">
|
|
||||||
<div className="mb-8">{`Chatbot UI is an open source clone of OpenAI's ChatGPT UI.`}</div>
|
|
||||||
<div className="mb-2 font-bold">
|
|
||||||
Important: Chatbot UI is 100% unaffiliated with OpenAI.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="text-center text-gray-500 dark:text-gray-400">
|
|
||||||
<div className="mb-2">
|
|
||||||
Chatbot UI allows you to plug in your API key to use this UI
|
|
||||||
with their API.
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
It is <span className="italic">only</span> used to communicate
|
|
||||||
with their API.
|
|
||||||
</div>
|
|
||||||
<div className="mb-2">
|
|
||||||
{t(
|
|
||||||
'Please set your OpenAI API key in the bottom left of the sidebar.',
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{t(
|
|
||||||
"If you don't have an OpenAI API key, you can get one here: ",
|
|
||||||
)}
|
|
||||||
<a
|
|
||||||
href="https://platform.openai.com/account/api-keys"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="text-blue-500 hover:underline"
|
|
||||||
>
|
|
||||||
openai.com
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : modelError ? (
|
|
||||||
<ErrorMessageDiv error={modelError} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className="max-h-full overflow-x-hidden"
|
|
||||||
ref={chatContainerRef}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
>
|
|
||||||
{conversation.messages.length === 0 ? (
|
|
||||||
<>
|
|
||||||
<div className="mx-auto flex w-[350px] flex-col space-y-10 pt-12 sm:w-[600px]">
|
|
||||||
<div className="text-center text-3xl font-semibold text-gray-800 dark:text-gray-100">
|
|
||||||
{models.length === 0 ? (
|
|
||||||
<div>
|
|
||||||
<Spinner size="16px" className="mx-auto" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
'Chatbot UI'
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{models.length > 0 && (
|
|
||||||
<div className="flex h-full flex-col space-y-4 rounded-lg border border-neutral-200 p-4 dark:border-neutral-600">
|
|
||||||
<ModelSelect
|
|
||||||
model={conversation.model}
|
|
||||||
models={models}
|
|
||||||
defaultModelId={defaultModelId}
|
|
||||||
onModelChange={(model) =>
|
|
||||||
onUpdateConversation(conversation, {
|
|
||||||
key: 'model',
|
|
||||||
value: model,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SystemPrompt
|
|
||||||
conversation={conversation}
|
|
||||||
prompts={prompts}
|
|
||||||
onChangePrompt={(prompt) =>
|
|
||||||
onUpdateConversation(conversation, {
|
|
||||||
key: 'prompt',
|
|
||||||
value: prompt,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="flex justify-center border border-b-neutral-300 bg-neutral-100 py-2 text-sm text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200">
|
|
||||||
{t('Model')}: {conversation.model.name}
|
|
||||||
<button
|
|
||||||
className="ml-2 cursor-pointer hover:opacity-50"
|
|
||||||
onClick={handleSettings}
|
|
||||||
>
|
|
||||||
<IconSettings size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="ml-2 cursor-pointer hover:opacity-50"
|
|
||||||
onClick={onClearAll}
|
|
||||||
>
|
|
||||||
<IconClearAll size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{showSettings && (
|
|
||||||
<div className="flex flex-col space-y-10 md:mx-auto md:max-w-xl md:gap-6 md:py-3 md:pt-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
|
|
||||||
<div className="flex h-full flex-col space-y-4 border-b border-neutral-200 p-4 dark:border-neutral-600 md:rounded-lg md:border">
|
|
||||||
<ModelSelect
|
|
||||||
model={conversation.model}
|
|
||||||
models={models}
|
|
||||||
defaultModelId={defaultModelId}
|
|
||||||
onModelChange={(model) =>
|
|
||||||
onUpdateConversation(conversation, {
|
|
||||||
key: 'model',
|
|
||||||
value: model,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="flex justify-center py-2 text-neutral-500 bg-neutral-100 dark:bg-[#444654] dark:text-neutral-200 text-sm border border-b-neutral-300 dark:border-none">Model: {conversation.model.name}</div>
|
||||||
|
|
||||||
{conversation.messages.map((message, index) => (
|
{conversation.messages.map((message, index) => (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
key={index}
|
key={index}
|
||||||
message={message}
|
message={message}
|
||||||
messageIndex={index}
|
lightMode={lightMode}
|
||||||
onEditMessage={onEditMessage}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{loading && <ChatLoader />}
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="h-[162px] bg-white dark:bg-[#343541]"
|
|
||||||
ref={messagesEndRef}
|
|
||||||
/>
|
/>
|
||||||
</>
|
))}
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ChatInput
|
{loading && <ChatLoader />}
|
||||||
stopConversationRef={stopConversationRef}
|
|
||||||
textareaRef={textareaRef}
|
<div
|
||||||
messageIsStreaming={messageIsStreaming}
|
className="bg-white dark:bg-[#343541] h-[162px]"
|
||||||
conversationIsEmpty={conversation.messages.length === 0}
|
ref={messagesEndRef}
|
||||||
model={conversation.model}
|
/>
|
||||||
prompts={prompts}
|
</>
|
||||||
onSend={(message, plugin) => {
|
)}
|
||||||
setCurrentMessage(message);
|
</div>
|
||||||
onSend(message, 0, plugin);
|
|
||||||
}}
|
{messageError ? (
|
||||||
|
<Regenerate
|
||||||
onRegenerate={() => {
|
onRegenerate={() => {
|
||||||
if (currentMessage) {
|
if (currentMessage) {
|
||||||
onSend(currentMessage, 2, null);
|
onSend(currentMessage, true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</>
|
) : (
|
||||||
)}
|
<ChatInput
|
||||||
{showScrollDownButton && (
|
stopConversationRef={stopConversationRef}
|
||||||
<div className="absolute bottom-0 right-0 mb-4 mr-4 pb-20">
|
messageIsStreaming={messageIsStreaming}
|
||||||
<button
|
onSend={(message) => {
|
||||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-neutral-300 text-gray-800 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-neutral-200"
|
setCurrentMessage(message);
|
||||||
onClick={handleScrollDown}
|
onSend(message, false);
|
||||||
>
|
}}
|
||||||
<IconArrowDown size={18} />
|
model={conversation.model}
|
||||||
</button>
|
/>
|
||||||
</div>
|
)}
|
||||||
)}
|
</>
|
||||||
</div>
|
)}
|
||||||
);
|
</div>
|
||||||
},
|
);
|
||||||
);
|
};
|
||||||
Chat.displayName = 'Chat';
|
|
||||||
|
|||||||
+57
-292
@@ -1,83 +1,30 @@
|
|||||||
import { Message } from '@/types/chat';
|
import { Message, OpenAIModel, OpenAIModelID } from "@/types";
|
||||||
import { OpenAIModel } from '@/types/openai';
|
import { IconPlayerStop, IconSend } from "@tabler/icons-react";
|
||||||
import { Plugin } from '@/types/plugin';
|
import { FC, KeyboardEvent, MutableRefObject, useEffect, useRef, useState } from "react";
|
||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import {
|
|
||||||
IconBolt,
|
|
||||||
IconBrandGoogle,
|
|
||||||
IconPlayerStop,
|
|
||||||
IconRepeat,
|
|
||||||
IconSend,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import {
|
|
||||||
FC,
|
|
||||||
KeyboardEvent,
|
|
||||||
MutableRefObject,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { PluginSelect } from './PluginSelect';
|
|
||||||
import { PromptList } from './PromptList';
|
|
||||||
import { VariableModal } from './VariableModal';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messageIsStreaming: boolean;
|
messageIsStreaming: boolean;
|
||||||
|
onSend: (message: Message) => void;
|
||||||
model: OpenAIModel;
|
model: OpenAIModel;
|
||||||
conversationIsEmpty: boolean;
|
|
||||||
prompts: Prompt[];
|
|
||||||
onSend: (message: Message, plugin: Plugin | null) => void;
|
|
||||||
onRegenerate: () => void;
|
|
||||||
stopConversationRef: MutableRefObject<boolean>;
|
stopConversationRef: MutableRefObject<boolean>;
|
||||||
textareaRef: MutableRefObject<HTMLTextAreaElement | null>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatInput: FC<Props> = ({
|
export const ChatInput: FC<Props> = ({ onSend, messageIsStreaming, model, stopConversationRef }) => {
|
||||||
messageIsStreaming,
|
|
||||||
model,
|
|
||||||
conversationIsEmpty,
|
|
||||||
prompts,
|
|
||||||
onSend,
|
|
||||||
onRegenerate,
|
|
||||||
stopConversationRef,
|
|
||||||
textareaRef,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('chat');
|
|
||||||
|
|
||||||
const [content, setContent] = useState<string>();
|
const [content, setContent] = useState<string>();
|
||||||
const [isTyping, setIsTyping] = useState<boolean>(false);
|
const [isTyping, setIsTyping] = useState<boolean>(false);
|
||||||
const [showPromptList, setShowPromptList] = useState(false);
|
|
||||||
const [activePromptIndex, setActivePromptIndex] = useState(0);
|
|
||||||
const [promptInputValue, setPromptInputValue] = useState('');
|
|
||||||
const [variables, setVariables] = useState<string[]>([]);
|
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
||||||
const [showPluginSelect, setShowPluginSelect] = useState(false);
|
|
||||||
const [plugin, setPlugin] = useState<Plugin | null>(null);
|
|
||||||
|
|
||||||
const promptListRef = useRef<HTMLUListElement | null>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const filteredPrompts = prompts.filter((prompt) =>
|
|
||||||
prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const maxLength = model.maxLength;
|
const maxLength = model.id === OpenAIModelID.GPT_3_5 ? 12000 : 24000;
|
||||||
|
|
||||||
if (value.length > maxLength) {
|
if (value.length > maxLength) {
|
||||||
alert(
|
alert(`Message limit is ${maxLength} characters`);
|
||||||
t(
|
|
||||||
`Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`,
|
|
||||||
{ maxLength, valueLength: value.length },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setContent(value);
|
setContent(value);
|
||||||
updatePromptListVisibility(value);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSend = () => {
|
const handleSend = () => {
|
||||||
@@ -86,231 +33,74 @@ export const ChatInput: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!content) {
|
if (!content) {
|
||||||
alert(t('Please enter a message'));
|
alert("Please enter a message");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
onSend({ role: 'user', content }, plugin);
|
onSend({ role: "user", content });
|
||||||
setContent('');
|
setContent("");
|
||||||
setPlugin(null);
|
|
||||||
|
|
||||||
if (window.innerWidth < 640 && textareaRef && textareaRef.current) {
|
if (window.innerWidth < 640 && textareaRef && textareaRef.current) {
|
||||||
textareaRef.current.blur();
|
textareaRef.current.blur();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleStopConversation = () => {
|
const isMobile = () => {
|
||||||
|
const userAgent = typeof window.navigator === "undefined" ? "" : navigator.userAgent;
|
||||||
|
const mobileRegex = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
|
||||||
|
return mobileRegex.test(userAgent);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||||
|
if (!isTyping) {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey && !isMobile()) {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSend();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (textareaRef && textareaRef.current) {
|
||||||
|
textareaRef.current.style.height = "inherit";
|
||||||
|
textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`;
|
||||||
|
textareaRef.current.style.overflow = `${textareaRef?.current?.scrollHeight > 400 ? "auto" : "hidden"}`;
|
||||||
|
}
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
|
function handleStopConversation() {
|
||||||
stopConversationRef.current = true;
|
stopConversationRef.current = true;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
stopConversationRef.current = false;
|
stopConversationRef.current = false;
|
||||||
}, 1000);
|
}, 1000);
|
||||||
};
|
}
|
||||||
|
|
||||||
const isMobile = () => {
|
|
||||||
const userAgent =
|
|
||||||
typeof window.navigator === 'undefined' ? '' : navigator.userAgent;
|
|
||||||
const mobileRegex =
|
|
||||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini|Mobile|mobile|CriOS/i;
|
|
||||||
return mobileRegex.test(userAgent);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInitModal = () => {
|
|
||||||
const selectedPrompt = filteredPrompts[activePromptIndex];
|
|
||||||
if (selectedPrompt) {
|
|
||||||
setContent((prevContent) => {
|
|
||||||
const newContent = prevContent?.replace(
|
|
||||||
/\/\w*$/,
|
|
||||||
selectedPrompt.content,
|
|
||||||
);
|
|
||||||
return newContent;
|
|
||||||
});
|
|
||||||
handlePromptSelect(selectedPrompt);
|
|
||||||
}
|
|
||||||
setShowPromptList(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (showPromptList) {
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
setActivePromptIndex((prevIndex) =>
|
|
||||||
prevIndex < prompts.length - 1 ? prevIndex + 1 : prevIndex,
|
|
||||||
);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
setActivePromptIndex((prevIndex) =>
|
|
||||||
prevIndex > 0 ? prevIndex - 1 : prevIndex,
|
|
||||||
);
|
|
||||||
} else if (e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
setActivePromptIndex((prevIndex) =>
|
|
||||||
prevIndex < prompts.length - 1 ? prevIndex + 1 : 0,
|
|
||||||
);
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleInitModal();
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowPromptList(false);
|
|
||||||
} else {
|
|
||||||
setActivePromptIndex(0);
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Enter' && !isTyping && !isMobile() && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSend();
|
|
||||||
} else if (e.key === '/' && e.metaKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowPluginSelect(!showPluginSelect);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseVariables = (content: string) => {
|
|
||||||
const regex = /{{(.*?)}}/g;
|
|
||||||
const foundVariables = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = regex.exec(content)) !== null) {
|
|
||||||
foundVariables.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundVariables;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePromptListVisibility = useCallback((text: string) => {
|
|
||||||
const match = text.match(/\/\w*$/);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
setShowPromptList(true);
|
|
||||||
setPromptInputValue(match[0].slice(1));
|
|
||||||
} else {
|
|
||||||
setShowPromptList(false);
|
|
||||||
setPromptInputValue('');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePromptSelect = (prompt: Prompt) => {
|
|
||||||
const parsedVariables = parseVariables(prompt.content);
|
|
||||||
setVariables(parsedVariables);
|
|
||||||
|
|
||||||
if (parsedVariables.length > 0) {
|
|
||||||
setIsModalVisible(true);
|
|
||||||
} else {
|
|
||||||
setContent((prevContent) => {
|
|
||||||
const updatedContent = prevContent?.replace(/\/\w*$/, prompt.content);
|
|
||||||
return updatedContent;
|
|
||||||
});
|
|
||||||
updatePromptListVisibility(prompt.content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (updatedVariables: string[]) => {
|
|
||||||
const newContent = content?.replace(/{{(.*?)}}/g, (match, variable) => {
|
|
||||||
const index = variables.indexOf(variable);
|
|
||||||
return updatedVariables[index];
|
|
||||||
});
|
|
||||||
|
|
||||||
setContent(newContent);
|
|
||||||
|
|
||||||
if (textareaRef && textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (promptListRef.current) {
|
|
||||||
promptListRef.current.scrollTop = activePromptIndex * 30;
|
|
||||||
}
|
|
||||||
}, [activePromptIndex]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (textareaRef && textareaRef.current) {
|
|
||||||
textareaRef.current.style.height = 'inherit';
|
|
||||||
textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`;
|
|
||||||
textareaRef.current.style.overflow = `${
|
|
||||||
textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden'
|
|
||||||
}`;
|
|
||||||
}
|
|
||||||
}, [content]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleOutsideClick = (e: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
promptListRef.current &&
|
|
||||||
!promptListRef.current.contains(e.target as Node)
|
|
||||||
) {
|
|
||||||
setShowPromptList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('click', handleOutsideClick);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('click', handleOutsideClick);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="absolute bottom-0 left-0 w-full border-transparent bg-gradient-to-b from-transparent via-white to-white pt-6 dark:border-white/20 dark:via-[#343541] dark:to-[#343541] md:pt-2">
|
<div className="absolute bottom-0 left-0 w-full dark:border-white/20 border-transparent dark:bg-[#444654] dark:bg-gradient-to-t from-[#343541] via-[#343541] to-[#343541]/0 bg-white dark:!bg-transparent dark:bg-vert-dark-gradient pt-6 md:pt-2">
|
||||||
<div className="stretch mx-2 mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:mt-[52px] md:last:mb-6 lg:mx-auto lg:max-w-3xl">
|
<div className="stretch mx-2 md:mt-[52px] mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:last:mb-6 lg:mx-auto lg:max-w-3xl">
|
||||||
{messageIsStreaming && (
|
{messageIsStreaming && (
|
||||||
<button
|
<button
|
||||||
className="absolute top-0 left-0 right-0 mx-auto mb-3 flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:mb-0 md:mt-2"
|
className="absolute -top-2 md:top-0 left-0 right-0 mx-auto dark:bg-[#343541] border w-fit border-gray-500 py-2 px-4 rounded text-black dark:text-white hover:opacity-50"
|
||||||
onClick={handleStopConversation}
|
onClick={handleStopConversation}
|
||||||
>
|
>
|
||||||
<IconPlayerStop size={16} /> {t('Stop Generating')}
|
<IconPlayerStop
|
||||||
|
size={16}
|
||||||
|
className="inline-block mb-[2px]"
|
||||||
|
/>{" "}
|
||||||
|
Stop Generating
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
<div className="flex flex-col w-full py-2 flex-grow md:py-3 md:pl-4 relative border border-black/10 bg-white dark:border-gray-900/50 dark:text-white dark:bg-[#40414F] rounded-md shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]">
|
||||||
{!messageIsStreaming && !conversationIsEmpty && (
|
|
||||||
<button
|
|
||||||
className="absolute top-0 left-0 right-0 mx-auto mb-3 flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:mb-0 md:mt-2"
|
|
||||||
onClick={onRegenerate}
|
|
||||||
>
|
|
||||||
<IconRepeat size={16} /> {t('Regenerate response')}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative mx-2 flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-[#40414F] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] sm:mx-4">
|
|
||||||
<button
|
|
||||||
className="absolute left-2 top-2 rounded-sm p-1 text-neutral-800 opacity-60 hover:bg-neutral-200 hover:text-neutral-900 dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200"
|
|
||||||
onClick={() => setShowPluginSelect(!showPluginSelect)}
|
|
||||||
onKeyDown={(e) => {}}
|
|
||||||
>
|
|
||||||
{plugin ? <IconBrandGoogle size={20} /> : <IconBolt size={20} />}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showPluginSelect && (
|
|
||||||
<div className="absolute left-0 bottom-14 bg-white dark:bg-[#343541]">
|
|
||||||
<PluginSelect
|
|
||||||
plugin={plugin}
|
|
||||||
onPluginChange={(plugin: Plugin) => {
|
|
||||||
setPlugin(plugin);
|
|
||||||
setShowPluginSelect(false);
|
|
||||||
|
|
||||||
if (textareaRef && textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className="m-0 w-full resize-none border-0 bg-transparent p-0 py-2 pr-8 pl-10 text-black dark:bg-transparent dark:text-white md:py-3 md:pl-10"
|
className="text-black dark:text-white m-0 w-full resize-none outline-none border-0 bg-transparent p-0 pr-7 focus:ring-0 focus-visible:ring-0 dark:bg-transparent pl-2 md:pl-0"
|
||||||
style={{
|
style={{
|
||||||
resize: 'none',
|
resize: "none",
|
||||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||||
maxHeight: '400px',
|
maxHeight: "400px",
|
||||||
overflow: `${
|
overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 ? "auto" : "hidden"}`
|
||||||
textareaRef.current && textareaRef.current.scrollHeight > 400
|
|
||||||
? 'auto'
|
|
||||||
: 'hidden'
|
|
||||||
}`,
|
|
||||||
}}
|
}}
|
||||||
placeholder={
|
placeholder="Type a message..."
|
||||||
t('Type a message or type "/" to select a prompt...') || ''
|
|
||||||
}
|
|
||||||
value={content}
|
value={content}
|
||||||
rows={1}
|
rows={1}
|
||||||
onCompositionStart={() => setIsTyping(true)}
|
onCompositionStart={() => setIsTyping(true)}
|
||||||
@@ -320,39 +110,17 @@ export const ChatInput: FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="absolute right-2 top-2 rounded-sm p-1 text-neutral-800 opacity-60 hover:bg-neutral-200 hover:text-neutral-900 dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200"
|
className="absolute right-5 focus:outline-none text-neutral-800 hover:text-neutral-900 dark:text-neutral-100 dark:hover:text-neutral-200 dark:bg-opacity-50 hover:bg-neutral-200 p-1 rounded-sm"
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
>
|
>
|
||||||
{messageIsStreaming ? (
|
<IconSend
|
||||||
<div className="h-4 w-4 animate-spin rounded-full border-t-2 border-neutral-800 opacity-60 dark:border-neutral-100"></div>
|
size={16}
|
||||||
) : (
|
className="opacity-60"
|
||||||
<IconSend size={18} />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showPromptList && filteredPrompts.length > 0 && (
|
|
||||||
<div className="absolute bottom-12 w-full">
|
|
||||||
<PromptList
|
|
||||||
activePromptIndex={activePromptIndex}
|
|
||||||
prompts={filteredPrompts}
|
|
||||||
onSelect={handleInitModal}
|
|
||||||
onMouseOver={setActivePromptIndex}
|
|
||||||
promptListRef={promptListRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isModalVisible && (
|
|
||||||
<VariableModal
|
|
||||||
prompt={prompts[activePromptIndex]}
|
|
||||||
variables={variables}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onClose={() => setIsModalVisible(false)}
|
|
||||||
/>
|
/>
|
||||||
)}
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-3 pt-2 pb-3 text-center text-[12px] text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6">
|
<div className="px-3 pt-2 pb-3 text-center text-xs text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/mckaywrigley/chatbot-ui"
|
href="https://github.com/mckaywrigley/chatbot-ui"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -361,10 +129,7 @@ export const ChatInput: FC<Props> = ({
|
|||||||
>
|
>
|
||||||
ChatBot UI
|
ChatBot UI
|
||||||
</a>
|
</a>
|
||||||
.{' '}
|
. Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.
|
||||||
{t(
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.",
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { IconDots } from '@tabler/icons-react';
|
import { IconDots } from "@tabler/icons-react";
|
||||||
import { FC } from 'react';
|
import { FC } from "react";
|
||||||
|
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export const ChatLoader: FC<Props> = () => {
|
export const ChatLoader: FC<Props> = () => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="group border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100"
|
className={`flex justify-center py-[20px] sm:py-[30px] whitespace-pre-wrap dark:bg-[#444654] dark:text-neutral-100 bg-neutral-100 text-neutral-900 dark:border-none"`}
|
||||||
style={{ overflowWrap: 'anywhere' }}
|
style={{ overflowWrap: "anywhere" }}
|
||||||
>
|
>
|
||||||
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
|
<div className="w-full sm:w-4/5 md:w-3/5 lg:w-[600px] xl:w-[800px] flex px-4">
|
||||||
<div className="min-w-[40px] text-right font-bold">AI:</div>
|
<div className="mr-1 sm:mr-2 font-bold min-w-[40px]">AI:</div>
|
||||||
<IconDots className="animate-pulse" />
|
<IconDots className="animate-pulse" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+54
-216
@@ -1,227 +1,65 @@
|
|||||||
import { Message } from '@/types/chat';
|
import { Message } from "@/types";
|
||||||
import { IconCheck, IconCopy, IconEdit, IconUser, IconRobot } from '@tabler/icons-react';
|
import { FC } from "react";
|
||||||
import { useTranslation } from 'next-i18next';
|
import ReactMarkdown from "react-markdown";
|
||||||
import { FC, memo, useEffect, useRef, useState } from 'react';
|
import remarkGfm from "remark-gfm";
|
||||||
import rehypeMathjax from 'rehype-mathjax';
|
import { CodeBlock } from "../Markdown/CodeBlock";
|
||||||
import remarkGfm from 'remark-gfm';
|
|
||||||
import remarkMath from 'remark-math';
|
|
||||||
import { CodeBlock } from '../Markdown/CodeBlock';
|
|
||||||
import { MemoizedReactMarkdown } from '../Markdown/MemoizedReactMarkdown';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message;
|
message: Message;
|
||||||
messageIndex: number;
|
lightMode: "light" | "dark";
|
||||||
onEditMessage: (message: Message, messageIndex: number) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChatMessage: FC<Props> = memo(
|
export const ChatMessage: FC<Props> = ({ message, lightMode }) => {
|
||||||
({ message, messageIndex, onEditMessage }) => {
|
return (
|
||||||
const { t } = useTranslation('chat');
|
<div
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
className={`group ${message.role === "assistant" ? "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-gray-50 dark:bg-[#444654]" : "text-gray-800 dark:text-gray-100 border-b border-black/10 dark:border-gray-900/50 bg-white dark:bg-[#343541]"}`}
|
||||||
const [isTyping, setIsTyping] = useState<boolean>(false);
|
style={{ overflowWrap: "anywhere" }}
|
||||||
const [messageContent, setMessageContent] = useState(message.content);
|
>
|
||||||
const [messagedCopied, setMessageCopied] = useState(false);
|
<div className="text-base gap-4 md:gap-6 md:max-w-2xl lg:max-w-2xl xl:max-w-3xl p-4 md:py-6 flex lg:px-0 m-auto">
|
||||||
|
<div className="font-bold min-w-[40px]">{message.role === "assistant" ? "AI:" : "You:"}</div>
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
<div className="prose dark:prose-invert mt-[-2px]">
|
||||||
|
{message.role === "user" ? (
|
||||||
const toggleEditing = () => {
|
<div className="prose dark:prose-invert whitespace-pre-wrap">{message.content}</div>
|
||||||
setIsEditing(!isEditing);
|
) : (
|
||||||
};
|
<ReactMarkdown
|
||||||
|
remarkPlugins={[remarkGfm]}
|
||||||
const handleInputChange = (
|
components={{
|
||||||
event: React.ChangeEvent<HTMLTextAreaElement>,
|
code({ node, inline, className, children, ...props }) {
|
||||||
) => {
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
setMessageContent(event.target.value);
|
return !inline && match ? (
|
||||||
if (textareaRef.current) {
|
<CodeBlock
|
||||||
textareaRef.current.style.height = 'inherit';
|
key={Math.random()}
|
||||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
language={match[1]}
|
||||||
}
|
value={String(children).replace(/\n$/, "")}
|
||||||
};
|
lightMode={lightMode}
|
||||||
|
{...props}
|
||||||
const handleEditMessage = () => {
|
|
||||||
if (message.content != messageContent) {
|
|
||||||
onEditMessage({ ...message, content: messageContent }, messageIndex);
|
|
||||||
}
|
|
||||||
setIsEditing(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePressEnter = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (e.key === 'Enter' && !isTyping && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleEditMessage();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const copyOnClick = () => {
|
|
||||||
if (!navigator.clipboard) return;
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(message.content).then(() => {
|
|
||||||
setMessageCopied(true);
|
|
||||||
setTimeout(() => {
|
|
||||||
setMessageCopied(false);
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (textareaRef.current) {
|
|
||||||
textareaRef.current.style.height = 'inherit';
|
|
||||||
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
|
|
||||||
}
|
|
||||||
}, [isEditing]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`group px-4 ${
|
|
||||||
message.role === 'assistant'
|
|
||||||
? 'border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100'
|
|
||||||
: 'border-b border-black/10 bg-white text-gray-800 dark:border-gray-900/50 dark:bg-[#343541] dark:text-gray-100'
|
|
||||||
}`}
|
|
||||||
style={{ overflowWrap: 'anywhere' }}
|
|
||||||
>
|
|
||||||
<div className="relative m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
|
|
||||||
<div className="min-w-[40px] text-right font-bold">
|
|
||||||
{message.role === 'assistant' ? <IconRobot size={30}/> : <IconUser size={30}/>}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="prose mt-[-2px] w-full dark:prose-invert">
|
|
||||||
{message.role === 'user' ? (
|
|
||||||
<div className="flex w-full">
|
|
||||||
{isEditing ? (
|
|
||||||
<div className="flex w-full flex-col">
|
|
||||||
<textarea
|
|
||||||
ref={textareaRef}
|
|
||||||
className="w-full resize-none whitespace-pre-wrap border-none dark:bg-[#343541]"
|
|
||||||
value={messageContent}
|
|
||||||
onChange={handleInputChange}
|
|
||||||
onKeyDown={handlePressEnter}
|
|
||||||
onCompositionStart={() => setIsTyping(true)}
|
|
||||||
onCompositionEnd={() => setIsTyping(false)}
|
|
||||||
style={{
|
|
||||||
fontFamily: 'inherit',
|
|
||||||
fontSize: 'inherit',
|
|
||||||
lineHeight: 'inherit',
|
|
||||||
padding: '0',
|
|
||||||
margin: '0',
|
|
||||||
overflow: 'hidden',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-10 flex justify-center space-x-4">
|
|
||||||
<button
|
|
||||||
className="h-[40px] rounded-md bg-blue-500 px-4 py-1 text-sm font-medium text-white enabled:hover:bg-blue-600 disabled:opacity-50"
|
|
||||||
onClick={handleEditMessage}
|
|
||||||
disabled={messageContent.trim().length <= 0}
|
|
||||||
>
|
|
||||||
{t('Save & Submit')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="h-[40px] rounded-md border border-neutral-300 px-4 py-1 text-sm font-medium text-neutral-700 hover:bg-neutral-100 dark:border-neutral-700 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
|
||||||
onClick={() => {
|
|
||||||
setMessageContent(message.content);
|
|
||||||
setIsEditing(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Cancel')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="prose whitespace-pre-wrap dark:prose-invert">
|
|
||||||
{message.content}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(window.innerWidth < 640 || !isEditing) && (
|
|
||||||
<button
|
|
||||||
className={`absolute translate-x-[1000px] text-gray-500 hover:text-gray-700 focus:translate-x-0 group-hover:translate-x-0 dark:text-gray-400 dark:hover:text-gray-300 ${
|
|
||||||
window.innerWidth < 640
|
|
||||||
? 'right-3 bottom-1'
|
|
||||||
: 'right-0 top-[26px]'
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
onClick={toggleEditing}
|
|
||||||
>
|
|
||||||
<IconEdit size={20} />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
className={`absolute ${
|
|
||||||
window.innerWidth < 640
|
|
||||||
? 'right-3 bottom-1'
|
|
||||||
: 'right-0 top-[26px] m-0'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{messagedCopied ? (
|
|
||||||
<IconCheck
|
|
||||||
size={20}
|
|
||||||
className="text-green-500 dark:text-green-400"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<code
|
||||||
className="translate-x-[1000px] text-gray-500 hover:text-gray-700 focus:translate-x-0 group-hover:translate-x-0 dark:text-gray-400 dark:hover:text-gray-300"
|
className={className}
|
||||||
onClick={copyOnClick}
|
{...props}
|
||||||
>
|
>
|
||||||
<IconCopy size={20} />
|
{children}
|
||||||
</button>
|
</code>
|
||||||
)}
|
);
|
||||||
</div>
|
},
|
||||||
|
table({ children }) {
|
||||||
<MemoizedReactMarkdown
|
return <table className="border-collapse border border-black dark:border-white py-1 px-3">{children}</table>;
|
||||||
className="prose dark:prose-invert"
|
},
|
||||||
remarkPlugins={[remarkGfm, remarkMath]}
|
th({ children }) {
|
||||||
rehypePlugins={[rehypeMathjax]}
|
return <th className="border border-black dark:border-white break-words py-1 px-3 bg-gray-500 text-white">{children}</th>;
|
||||||
components={{
|
},
|
||||||
code({ node, inline, className, children, ...props }) {
|
td({ children }) {
|
||||||
const match = /language-(\w+)/.exec(className || '');
|
return <td className="border border-black dark:border-white break-words py-1 px-3">{children}</td>;
|
||||||
|
}
|
||||||
return !inline && match ? (
|
}}
|
||||||
<CodeBlock
|
>
|
||||||
key={Math.random()}
|
{message.content}
|
||||||
language={match[1]}
|
</ReactMarkdown>
|
||||||
value={String(children).replace(/\n$/, '')}
|
)}
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<code className={className} {...props}>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
table({ children }) {
|
|
||||||
return (
|
|
||||||
<table className="border-collapse border border-black py-1 px-3 dark:border-white">
|
|
||||||
{children}
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
th({ children }) {
|
|
||||||
return (
|
|
||||||
<th className="break-words border border-black bg-gray-500 py-1 px-3 text-white dark:border-white">
|
|
||||||
{children}
|
|
||||||
</th>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
td({ children }) {
|
|
||||||
return (
|
|
||||||
<td className="break-words border border-black py-1 px-3 dark:border-white">
|
|
||||||
{children}
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{message.content}
|
|
||||||
</MemoizedReactMarkdown>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
},
|
);
|
||||||
);
|
};
|
||||||
ChatMessage.displayName = 'ChatMessage';
|
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { ErrorMessage } from '@/types/error';
|
|
||||||
import { IconCircleX } from '@tabler/icons-react';
|
|
||||||
import { FC } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
error: ErrorMessage;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ErrorMessageDiv: FC<Props> = ({ error }) => {
|
|
||||||
return (
|
|
||||||
<div className="mx-6 flex h-full flex-col items-center justify-center text-red-500">
|
|
||||||
<div className="mb-5">
|
|
||||||
<IconCircleX size={36} />
|
|
||||||
</div>
|
|
||||||
<div className="mb-3 text-2xl font-medium">{error.title}</div>
|
|
||||||
{error.messageLines.map((line, index) => (
|
|
||||||
<div key={index} className="text-center">
|
|
||||||
{' '}
|
|
||||||
{line}{' '}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="mt-4 text-xs opacity-50 dark:text-red-400">
|
|
||||||
{error.code ? <i>Code: {error.code}</i> : ''}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,60 +1,33 @@
|
|||||||
import { OpenAIModel, OpenAIModelID } from '@/types/openai';
|
import { OpenAIModel } from "@/types";
|
||||||
import { useTranslation } from 'next-i18next';
|
import { FC } from "react";
|
||||||
import { IconExternalLink } from '@tabler/icons-react';
|
|
||||||
import { FC } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
model: OpenAIModel;
|
model: OpenAIModel;
|
||||||
models: OpenAIModel[];
|
models: OpenAIModel[];
|
||||||
defaultModelId: OpenAIModelID;
|
|
||||||
onModelChange: (model: OpenAIModel) => void;
|
onModelChange: (model: OpenAIModel) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ModelSelect: FC<Props> = ({
|
export const ModelSelect: FC<Props> = ({ model, models, onModelChange }) => {
|
||||||
model,
|
|
||||||
models,
|
|
||||||
defaultModelId,
|
|
||||||
onModelChange,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('chat');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className="mb-2 text-left text-neutral-700 dark:text-neutral-400">
|
<label className="text-left mb-2 dark:text-neutral-400 text-neutral-700">Model</label>
|
||||||
{t('Model')}
|
<select
|
||||||
</label>
|
className="w-full p-3 dark:text-white dark:bg-[#343541] border border-neutral-500 rounded-lg appearance-none focus:shadow-outline text-neutral-900 cursor-pointer"
|
||||||
<div className="w-full rounded-lg border border-neutral-200 bg-transparent pr-2 text-neutral-900 dark:border-neutral-600 dark:text-white">
|
placeholder="Select a model"
|
||||||
<select
|
value={model.id}
|
||||||
className="w-full bg-transparent p-2"
|
onChange={(e) => {
|
||||||
placeholder={t('Select a model') || ''}
|
onModelChange(models.find((model) => model.id === e.target.value) as OpenAIModel);
|
||||||
value={model?.id || defaultModelId}
|
}}
|
||||||
onChange={(e) => {
|
>
|
||||||
onModelChange(
|
{models.map((model) => (
|
||||||
models.find(
|
<option
|
||||||
(model) => model.id === e.target.value,
|
key={model.id}
|
||||||
) as OpenAIModel,
|
value={model.id}
|
||||||
);
|
>
|
||||||
}}
|
{model.name}
|
||||||
>
|
</option>
|
||||||
{models.map((model) => (
|
))}
|
||||||
<option
|
</select>
|
||||||
key={model.id}
|
|
||||||
value={model.id}
|
|
||||||
className="dark:bg-[#343541] dark:text-white"
|
|
||||||
>
|
|
||||||
{model.id === defaultModelId
|
|
||||||
? `Default (${model.name})`
|
|
||||||
: model.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="w-full mt-3 text-left text-neutral-700 dark:text-neutral-400 flex items-center">
|
|
||||||
<a href="https://platform.openai.com/account/usage" target="_blank" className="flex items-center">
|
|
||||||
<IconExternalLink size={18} className={"inline mr-1"} />
|
|
||||||
{t('View Account Usage')}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
import { Plugin, PluginList } from '@/types/plugin';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { FC, useEffect, useRef } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
plugin: Plugin | null;
|
|
||||||
onPluginChange: (plugin: Plugin) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PluginSelect: FC<Props> = ({ plugin, onPluginChange }) => {
|
|
||||||
const { t } = useTranslation('chat');
|
|
||||||
|
|
||||||
const selectRef = useRef<HTMLSelectElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectRef.current) {
|
|
||||||
selectRef.current.focus();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<div className="w-full rounded-lg border border-neutral-200 bg-transparent pr-2 text-neutral-900 dark:border-neutral-600 dark:text-white">
|
|
||||||
<select
|
|
||||||
ref={selectRef}
|
|
||||||
className="w-full cursor-pointer bg-transparent p-2"
|
|
||||||
placeholder={t('Select a plugin') || ''}
|
|
||||||
value={plugin?.id || ''}
|
|
||||||
onChange={(e) => {
|
|
||||||
onPluginChange(
|
|
||||||
PluginList.find(
|
|
||||||
(plugin) => plugin.id === e.target.value,
|
|
||||||
) as Plugin,
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
key="none"
|
|
||||||
value=""
|
|
||||||
className="dark:bg-[#343541] dark:text-white"
|
|
||||||
>
|
|
||||||
Select Plugin
|
|
||||||
</option>
|
|
||||||
|
|
||||||
{PluginList.map((plugin) => (
|
|
||||||
<option
|
|
||||||
key={plugin.id}
|
|
||||||
value={plugin.id}
|
|
||||||
className="dark:bg-[#343541] dark:text-white"
|
|
||||||
>
|
|
||||||
{plugin.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import { FC, MutableRefObject } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
prompts: Prompt[];
|
|
||||||
activePromptIndex: number;
|
|
||||||
onSelect: () => void;
|
|
||||||
onMouseOver: (index: number) => void;
|
|
||||||
promptListRef: MutableRefObject<HTMLUListElement | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PromptList: FC<Props> = ({
|
|
||||||
prompts,
|
|
||||||
activePromptIndex,
|
|
||||||
onSelect,
|
|
||||||
onMouseOver,
|
|
||||||
promptListRef,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<ul
|
|
||||||
ref={promptListRef}
|
|
||||||
className="z-10 max-h-52 w-full overflow-scroll rounded border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-neutral-500 dark:bg-[#343541] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]"
|
|
||||||
>
|
|
||||||
{prompts.map((prompt, index) => (
|
|
||||||
<li
|
|
||||||
key={prompt.id}
|
|
||||||
className={`${
|
|
||||||
index === activePromptIndex
|
|
||||||
? 'bg-gray-200 dark:bg-[#202123] dark:text-black'
|
|
||||||
: ''
|
|
||||||
} cursor-pointer px-3 py-2 text-sm text-black dark:text-white`}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
onSelect();
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => onMouseOver(index)}
|
|
||||||
>
|
|
||||||
{prompt.name}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,24 +1,20 @@
|
|||||||
import { IconRefresh } from '@tabler/icons-react';
|
import { IconRefresh } from "@tabler/icons-react";
|
||||||
import { useTranslation } from 'next-i18next';
|
import { FC } from "react";
|
||||||
import { FC } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onRegenerate: () => void;
|
onRegenerate: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Regenerate: FC<Props> = ({ onRegenerate }) => {
|
export const Regenerate: FC<Props> = ({ onRegenerate }) => {
|
||||||
const { t } = useTranslation('chat');
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-4 left-0 right-0 ml-auto mr-auto w-full px-2 sm:absolute sm:bottom-8 sm:left-[280px] sm:w-1/2 lg:left-[200px]">
|
<div className="fixed sm:absolute bottom-4 sm:bottom-8 w-full sm:w-1/2 px-2 left-0 sm:left-[280px] lg:left-[200px] right-0 ml-auto mr-auto">
|
||||||
<div className="mb-4 text-center text-red-500">
|
<div className="text-center mb-4 text-red-500">Sorry, there was an error.</div>
|
||||||
{t('Sorry, there was an error.')}
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
className="flex h-12 gap-2 w-full items-center justify-center rounded-lg border border-b-neutral-300 bg-neutral-100 text-sm font-semibold text-neutral-500 dark:border-none dark:bg-[#444654] dark:text-neutral-200"
|
className="flex items-center justify-center w-full h-12 bg-neutral-100 dark:bg-[#444654] text-neutral-500 dark:text-neutral-200 text-sm font-semibold rounded-lg border border-b-neutral-300 dark:border-none"
|
||||||
onClick={onRegenerate}
|
onClick={onRegenerate}
|
||||||
>
|
>
|
||||||
<IconRefresh />
|
<IconRefresh className="mr-2" />
|
||||||
<div>{t('Regenerate response')}</div>
|
<div>Regenerate response</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,164 +1,36 @@
|
|||||||
import { Conversation } from '@/types/chat';
|
import { Conversation } from "@/types";
|
||||||
import { OpenAIModelID } from '@/types/openai';
|
import { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const";
|
||||||
import { Prompt } from '@/types/prompt';
|
import { FC, useEffect, useRef, useState } from "react";
|
||||||
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import {
|
|
||||||
FC,
|
|
||||||
KeyboardEvent,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import { PromptList } from './PromptList';
|
|
||||||
import { VariableModal } from './VariableModal';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
conversation: Conversation;
|
conversation: Conversation;
|
||||||
prompts: Prompt[];
|
|
||||||
onChangePrompt: (prompt: string) => void;
|
onChangePrompt: (prompt: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SystemPrompt: FC<Props> = ({
|
export const SystemPrompt: FC<Props> = ({ conversation, onChangePrompt }) => {
|
||||||
conversation,
|
const [value, setValue] = useState<string>("");
|
||||||
prompts,
|
|
||||||
onChangePrompt,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('chat');
|
|
||||||
|
|
||||||
const [value, setValue] = useState<string>('');
|
|
||||||
const [activePromptIndex, setActivePromptIndex] = useState(0);
|
|
||||||
const [showPromptList, setShowPromptList] = useState(false);
|
|
||||||
const [promptInputValue, setPromptInputValue] = useState('');
|
|
||||||
const [variables, setVariables] = useState<string[]>([]);
|
|
||||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const promptListRef = useRef<HTMLUListElement | null>(null);
|
|
||||||
|
|
||||||
const filteredPrompts = prompts.filter((prompt) =>
|
|
||||||
prompt.name.toLowerCase().includes(promptInputValue.toLowerCase()),
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const value = e.target.value;
|
const value = e.target.value;
|
||||||
const maxLength = conversation.model.maxLength;
|
const maxLength = 4000;
|
||||||
|
|
||||||
if (value.length > maxLength) {
|
if (value.length > maxLength) {
|
||||||
alert(
|
alert(`Prompt limit is ${maxLength} characters`);
|
||||||
t(
|
|
||||||
`Prompt limit is {{maxLength}} characters. You have entered {{valueLength}} characters.`,
|
|
||||||
{ maxLength, valueLength: value.length },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setValue(value);
|
setValue(value);
|
||||||
updatePromptListVisibility(value);
|
|
||||||
|
|
||||||
if (value.length > 0) {
|
if (value.length > 0) {
|
||||||
onChangePrompt(value);
|
onChangePrompt(value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInitModal = () => {
|
|
||||||
const selectedPrompt = filteredPrompts[activePromptIndex];
|
|
||||||
setValue((prevVal) => {
|
|
||||||
const newContent = prevVal?.replace(/\/\w*$/, selectedPrompt.content);
|
|
||||||
return newContent;
|
|
||||||
});
|
|
||||||
handlePromptSelect(selectedPrompt);
|
|
||||||
setShowPromptList(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseVariables = (content: string) => {
|
|
||||||
const regex = /{{(.*?)}}/g;
|
|
||||||
const foundVariables = [];
|
|
||||||
let match;
|
|
||||||
|
|
||||||
while ((match = regex.exec(content)) !== null) {
|
|
||||||
foundVariables.push(match[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return foundVariables;
|
|
||||||
};
|
|
||||||
|
|
||||||
const updatePromptListVisibility = useCallback((text: string) => {
|
|
||||||
const match = text.match(/\/\w*$/);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
setShowPromptList(true);
|
|
||||||
setPromptInputValue(match[0].slice(1));
|
|
||||||
} else {
|
|
||||||
setShowPromptList(false);
|
|
||||||
setPromptInputValue('');
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handlePromptSelect = (prompt: Prompt) => {
|
|
||||||
const parsedVariables = parseVariables(prompt.content);
|
|
||||||
setVariables(parsedVariables);
|
|
||||||
|
|
||||||
if (parsedVariables.length > 0) {
|
|
||||||
setIsModalVisible(true);
|
|
||||||
} else {
|
|
||||||
const updatedContent = value?.replace(/\/\w*$/, prompt.content);
|
|
||||||
|
|
||||||
setValue(updatedContent);
|
|
||||||
onChangePrompt(updatedContent);
|
|
||||||
|
|
||||||
updatePromptListVisibility(prompt.content);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (updatedVariables: string[]) => {
|
|
||||||
const newContent = value?.replace(/{{(.*?)}}/g, (match, variable) => {
|
|
||||||
const index = variables.indexOf(variable);
|
|
||||||
return updatedVariables[index];
|
|
||||||
});
|
|
||||||
|
|
||||||
setValue(newContent);
|
|
||||||
onChangePrompt(newContent);
|
|
||||||
|
|
||||||
if (textareaRef && textareaRef.current) {
|
|
||||||
textareaRef.current.focus();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (showPromptList) {
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
setActivePromptIndex((prevIndex) =>
|
|
||||||
prevIndex < prompts.length - 1 ? prevIndex + 1 : prevIndex,
|
|
||||||
);
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
setActivePromptIndex((prevIndex) =>
|
|
||||||
prevIndex > 0 ? prevIndex - 1 : prevIndex,
|
|
||||||
);
|
|
||||||
} else if (e.key === 'Tab') {
|
|
||||||
e.preventDefault();
|
|
||||||
setActivePromptIndex((prevIndex) =>
|
|
||||||
prevIndex < prompts.length - 1 ? prevIndex + 1 : 0,
|
|
||||||
);
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleInitModal();
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
e.preventDefault();
|
|
||||||
setShowPromptList(false);
|
|
||||||
} else {
|
|
||||||
setActivePromptIndex(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (textareaRef && textareaRef.current) {
|
if (textareaRef && textareaRef.current) {
|
||||||
textareaRef.current.style.height = 'inherit';
|
textareaRef.current.style.height = "inherit";
|
||||||
textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`;
|
textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`;
|
||||||
}
|
}
|
||||||
}, [value]);
|
}, [value]);
|
||||||
@@ -171,70 +43,23 @@ export const SystemPrompt: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [conversation]);
|
}, [conversation]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleOutsideClick = (e: MouseEvent) => {
|
|
||||||
if (
|
|
||||||
promptListRef.current &&
|
|
||||||
!promptListRef.current.contains(e.target as Node)
|
|
||||||
) {
|
|
||||||
setShowPromptList(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('click', handleOutsideClick);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('click', handleOutsideClick);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<label className="mb-2 text-left text-neutral-700 dark:text-neutral-400">
|
<label className="text-left dark:text-neutral-400 text-neutral-700 mb-2">System Prompt</label>
|
||||||
{t('System Prompt')}
|
|
||||||
</label>
|
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className="w-full rounded-lg border border-neutral-200 bg-transparent px-4 py-3 text-neutral-900 dark:border-neutral-600 dark:text-neutral-100"
|
className="w-full rounded-lg px-4 py-2 focus:outline-none dark:bg-[#40414F] dark:border-opacity-50 dark:border-neutral-800 dark:text-neutral-100 border border-neutral-500 shadow text-neutral-900"
|
||||||
style={{
|
style={{
|
||||||
resize: 'none',
|
resize: "none",
|
||||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||||
maxHeight: '300px',
|
maxHeight: "300px",
|
||||||
overflow: `${
|
overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400 ? "auto" : "hidden"}`
|
||||||
textareaRef.current && textareaRef.current.scrollHeight > 400
|
|
||||||
? 'auto'
|
|
||||||
: 'hidden'
|
|
||||||
}`,
|
|
||||||
}}
|
}}
|
||||||
placeholder={
|
placeholder="Enter a prompt"
|
||||||
t(`Enter a prompt or type "/" to select a prompt...`) || ''
|
value={value}
|
||||||
}
|
|
||||||
value={t(value) || ''}
|
|
||||||
rows={1}
|
rows={1}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{showPromptList && filteredPrompts.length > 0 && (
|
|
||||||
<div>
|
|
||||||
<PromptList
|
|
||||||
activePromptIndex={activePromptIndex}
|
|
||||||
prompts={filteredPrompts}
|
|
||||||
onSelect={handleInitModal}
|
|
||||||
onMouseOver={setActivePromptIndex}
|
|
||||||
promptListRef={promptListRef}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isModalVisible && (
|
|
||||||
<VariableModal
|
|
||||||
prompt={prompts[activePromptIndex]}
|
|
||||||
variables={variables}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onClose={() => setIsModalVisible(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
prompt: Prompt;
|
|
||||||
variables: string[];
|
|
||||||
onSubmit: (updatedVariables: string[]) => void;
|
|
||||||
onClose: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VariableModal: FC<Props> = ({
|
|
||||||
prompt,
|
|
||||||
variables,
|
|
||||||
onSubmit,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
const [updatedVariables, setUpdatedVariables] = useState<
|
|
||||||
{ key: string; value: string }[]
|
|
||||||
>(
|
|
||||||
variables
|
|
||||||
.map((variable) => ({ key: variable, value: '' }))
|
|
||||||
.filter(
|
|
||||||
(item, index, array) =>
|
|
||||||
array.findIndex((t) => t.key === item.key) === index,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
|
||||||
const nameInputRef = useRef<HTMLTextAreaElement>(null);
|
|
||||||
|
|
||||||
const handleChange = (index: number, value: string) => {
|
|
||||||
setUpdatedVariables((prev) => {
|
|
||||||
const updated = [...prev];
|
|
||||||
updated[index].value = value;
|
|
||||||
return updated;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
if (updatedVariables.some((variable) => variable.value === '')) {
|
|
||||||
alert('Please fill out all variables');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit(updatedVariables.map((variable) => variable.value));
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
handleSubmit();
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleOutsideClick = (e: MouseEvent) => {
|
|
||||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('click', handleOutsideClick);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('click', handleOutsideClick);
|
|
||||||
};
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (nameInputRef.current) {
|
|
||||||
nameInputRef.current.focus();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50"
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={modalRef}
|
|
||||||
className="dark:border-netural-400 inline-block max-h-[400px] transform overflow-hidden overflow-y-auto rounded-lg border border-gray-300 bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all dark:bg-[#202123] sm:my-8 sm:max-h-[600px] sm:w-full sm:max-w-lg sm:p-6 sm:align-middle"
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
<div className="mb-4 text-xl font-bold text-black dark:text-neutral-200">
|
|
||||||
{prompt.name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-4 text-sm italic text-black dark:text-neutral-200">
|
|
||||||
{prompt.description}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{updatedVariables.map((variable, index) => (
|
|
||||||
<div className="mb-4" key={index}>
|
|
||||||
<div className="mb-2 text-sm font-bold text-neutral-200">
|
|
||||||
{variable.key}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
ref={index === 0 ? nameInputRef : undefined}
|
|
||||||
className="mt-1 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
|
||||||
style={{ resize: 'none' }}
|
|
||||||
placeholder={`Enter a value for ${variable.key}...`}
|
|
||||||
value={variable.value}
|
|
||||||
onChange={(e) => handleChange(index, e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="mt-6 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow hover:bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-white dark:text-black dark:hover:bg-neutral-300"
|
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
Submit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
import { Conversation } from '@/types/chat';
|
|
||||||
import { KeyValuePair } from '@/types/data';
|
|
||||||
import { SupportedExportFormats } from '@/types/export';
|
|
||||||
import { Folder } from '@/types/folder';
|
|
||||||
import { PluginKey } from '@/types/plugin';
|
|
||||||
import { IconFolderPlus, IconMessagesOff, IconPlus } from '@tabler/icons-react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { FC, useEffect, useState } from 'react';
|
|
||||||
import { ChatFolders } from '../Folders/Chat/ChatFolders';
|
|
||||||
import { Search } from '../Sidebar/Search';
|
|
||||||
import { ChatbarSettings } from './ChatbarSettings';
|
|
||||||
import { Conversations } from './Conversations';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
loading: boolean;
|
|
||||||
conversations: Conversation[];
|
|
||||||
lightMode: 'light' | 'dark';
|
|
||||||
selectedConversation: Conversation;
|
|
||||||
apiKey: string;
|
|
||||||
pluginKeys: PluginKey[];
|
|
||||||
folders: Folder[];
|
|
||||||
onCreateFolder: (name: string) => void;
|
|
||||||
onDeleteFolder: (folderId: string) => void;
|
|
||||||
onUpdateFolder: (folderId: string, name: string) => void;
|
|
||||||
onNewConversation: () => void;
|
|
||||||
onToggleLightMode: (mode: 'light' | 'dark') => void;
|
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
|
||||||
onDeleteConversation: (conversation: Conversation) => void;
|
|
||||||
onUpdateConversation: (
|
|
||||||
conversation: Conversation,
|
|
||||||
data: KeyValuePair,
|
|
||||||
) => void;
|
|
||||||
onApiKeyChange: (apiKey: string) => void;
|
|
||||||
onClearConversations: () => void;
|
|
||||||
onExportConversations: () => void;
|
|
||||||
onImportConversations: (data: SupportedExportFormats) => void;
|
|
||||||
onPluginKeyChange: (pluginKey: PluginKey) => void;
|
|
||||||
onClearPluginKey: (pluginKey: PluginKey) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Chatbar: FC<Props> = ({
|
|
||||||
loading,
|
|
||||||
conversations,
|
|
||||||
lightMode,
|
|
||||||
selectedConversation,
|
|
||||||
apiKey,
|
|
||||||
pluginKeys,
|
|
||||||
folders,
|
|
||||||
onCreateFolder,
|
|
||||||
onDeleteFolder,
|
|
||||||
onUpdateFolder,
|
|
||||||
onNewConversation,
|
|
||||||
onToggleLightMode,
|
|
||||||
onSelectConversation,
|
|
||||||
onDeleteConversation,
|
|
||||||
onUpdateConversation,
|
|
||||||
onApiKeyChange,
|
|
||||||
onClearConversations,
|
|
||||||
onExportConversations,
|
|
||||||
onImportConversations,
|
|
||||||
onPluginKeyChange,
|
|
||||||
onClearPluginKey,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('sidebar');
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
||||||
const [filteredConversations, setFilteredConversations] =
|
|
||||||
useState<Conversation[]>(conversations);
|
|
||||||
|
|
||||||
const handleUpdateConversation = (
|
|
||||||
conversation: Conversation,
|
|
||||||
data: KeyValuePair,
|
|
||||||
) => {
|
|
||||||
onUpdateConversation(conversation, data);
|
|
||||||
setSearchTerm('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeleteConversation = (conversation: Conversation) => {
|
|
||||||
onDeleteConversation(conversation);
|
|
||||||
setSearchTerm('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: any) => {
|
|
||||||
if (e.dataTransfer) {
|
|
||||||
const conversation = JSON.parse(e.dataTransfer.getData('conversation'));
|
|
||||||
onUpdateConversation(conversation, { key: 'folderId', value: 0 });
|
|
||||||
|
|
||||||
e.target.style.background = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowDrop = (e: any) => {
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlightDrop = (e: any) => {
|
|
||||||
e.target.style.background = '#343541';
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeHighlight = (e: any) => {
|
|
||||||
e.target.style.background = 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchTerm) {
|
|
||||||
setFilteredConversations(
|
|
||||||
conversations.filter((conversation) => {
|
|
||||||
const searchable =
|
|
||||||
conversation.name.toLocaleLowerCase() +
|
|
||||||
' ' +
|
|
||||||
conversation.messages.map((message) => message.content).join(' ');
|
|
||||||
return searchable.toLowerCase().includes(searchTerm.toLowerCase());
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setFilteredConversations(conversations);
|
|
||||||
}
|
|
||||||
}, [searchTerm, conversations]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`fixed top-0 bottom-0 z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 transition-all sm:relative sm:top-0`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<button
|
|
||||||
className="flex w-[190px] flex-shrink-0 cursor-pointer select-none items-center gap-3 rounded-md border border-white/20 p-3 text-[14px] leading-normal text-white transition-colors duration-200 hover:bg-gray-500/10"
|
|
||||||
onClick={() => {
|
|
||||||
onNewConversation();
|
|
||||||
setSearchTerm('');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconPlus size={18} />
|
|
||||||
{t('New chat')}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="ml-2 flex flex-shrink-0 cursor-pointer items-center gap-3 rounded-md border border-white/20 p-3 text-[14px] leading-normal text-white transition-colors duration-200 hover:bg-gray-500/10"
|
|
||||||
onClick={() => onCreateFolder(t('New folder'))}
|
|
||||||
>
|
|
||||||
<IconFolderPlus size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{conversations.length > 1 && (
|
|
||||||
<Search
|
|
||||||
placeholder="Search conversations..."
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
onSearch={setSearchTerm}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-grow overflow-auto">
|
|
||||||
{folders.length > 0 && (
|
|
||||||
<div className="flex border-b border-white/20 pb-2">
|
|
||||||
<ChatFolders
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
conversations={filteredConversations.filter(
|
|
||||||
(conversation) => conversation.folderId,
|
|
||||||
)}
|
|
||||||
folders={folders}
|
|
||||||
onDeleteFolder={onDeleteFolder}
|
|
||||||
onUpdateFolder={onUpdateFolder}
|
|
||||||
selectedConversation={selectedConversation}
|
|
||||||
loading={loading}
|
|
||||||
onSelectConversation={onSelectConversation}
|
|
||||||
onDeleteConversation={handleDeleteConversation}
|
|
||||||
onUpdateConversation={handleUpdateConversation}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{conversations.length > 0 ? (
|
|
||||||
<div
|
|
||||||
className="pt-2"
|
|
||||||
onDrop={(e) => handleDrop(e)}
|
|
||||||
onDragOver={allowDrop}
|
|
||||||
onDragEnter={highlightDrop}
|
|
||||||
onDragLeave={removeHighlight}
|
|
||||||
>
|
|
||||||
<Conversations
|
|
||||||
loading={loading}
|
|
||||||
conversations={filteredConversations.filter(
|
|
||||||
(conversation) => !conversation.folderId,
|
|
||||||
)}
|
|
||||||
selectedConversation={selectedConversation}
|
|
||||||
onSelectConversation={onSelectConversation}
|
|
||||||
onDeleteConversation={handleDeleteConversation}
|
|
||||||
onUpdateConversation={handleUpdateConversation}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="mt-8 flex flex-col items-center gap-3 text-sm leading-normal text-white opacity-50">
|
|
||||||
<IconMessagesOff />
|
|
||||||
{t('No conversations.')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ChatbarSettings
|
|
||||||
lightMode={lightMode}
|
|
||||||
apiKey={apiKey}
|
|
||||||
pluginKeys={pluginKeys}
|
|
||||||
conversationsCount={conversations.length}
|
|
||||||
onToggleLightMode={onToggleLightMode}
|
|
||||||
onApiKeyChange={onApiKeyChange}
|
|
||||||
onClearConversations={onClearConversations}
|
|
||||||
onExportConversations={onExportConversations}
|
|
||||||
onImportConversations={onImportConversations}
|
|
||||||
onPluginKeyChange={onPluginKeyChange}
|
|
||||||
onClearPluginKey={onClearPluginKey}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { SupportedExportFormats } from '@/types/export';
|
|
||||||
import { PluginKey } from '@/types/plugin';
|
|
||||||
import { IconFileExport, IconMoon, IconSun } from '@tabler/icons-react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import { Import } from '../Settings/Import';
|
|
||||||
import { Key } from '../Settings/Key';
|
|
||||||
import { SidebarButton } from '../Sidebar/SidebarButton';
|
|
||||||
import { ClearConversations } from './ClearConversations';
|
|
||||||
import { PluginKeys } from './PluginKeys';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
lightMode: 'light' | 'dark';
|
|
||||||
apiKey: string;
|
|
||||||
pluginKeys: PluginKey[];
|
|
||||||
conversationsCount: number;
|
|
||||||
onToggleLightMode: (mode: 'light' | 'dark') => void;
|
|
||||||
onApiKeyChange: (apiKey: string) => void;
|
|
||||||
onClearConversations: () => void;
|
|
||||||
onExportConversations: () => void;
|
|
||||||
onImportConversations: (data: SupportedExportFormats) => void;
|
|
||||||
onPluginKeyChange: (pluginKey: PluginKey) => void;
|
|
||||||
onClearPluginKey: (pluginKey: PluginKey) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatbarSettings: FC<Props> = ({
|
|
||||||
lightMode,
|
|
||||||
apiKey,
|
|
||||||
pluginKeys,
|
|
||||||
conversationsCount,
|
|
||||||
onToggleLightMode,
|
|
||||||
onApiKeyChange,
|
|
||||||
onClearConversations,
|
|
||||||
onExportConversations,
|
|
||||||
onImportConversations,
|
|
||||||
onPluginKeyChange,
|
|
||||||
onClearPluginKey,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('sidebar');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center space-y-1 border-t border-white/20 pt-1 text-sm">
|
|
||||||
{conversationsCount > 0 ? (
|
|
||||||
<ClearConversations onClearConversations={onClearConversations} />
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Import onImport={onImportConversations} />
|
|
||||||
|
|
||||||
<SidebarButton
|
|
||||||
text={t('Export data')}
|
|
||||||
icon={<IconFileExport size={18} />}
|
|
||||||
onClick={() => onExportConversations()}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SidebarButton
|
|
||||||
text={lightMode === 'light' ? t('Dark mode') : t('Light mode')}
|
|
||||||
icon={
|
|
||||||
lightMode === 'light' ? <IconMoon size={18} /> : <IconSun size={18} />
|
|
||||||
}
|
|
||||||
onClick={() =>
|
|
||||||
onToggleLightMode(lightMode === 'light' ? 'dark' : 'light')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Key apiKey={apiKey} onApiKeyChange={onApiKeyChange} />
|
|
||||||
|
|
||||||
<PluginKeys
|
|
||||||
pluginKeys={pluginKeys}
|
|
||||||
onPluginKeyChange={onPluginKeyChange}
|
|
||||||
onClearPluginKey={onClearPluginKey}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
import { Conversation } from '@/types/chat';
|
|
||||||
import { KeyValuePair } from '@/types/data';
|
|
||||||
import {
|
|
||||||
IconCheck,
|
|
||||||
IconMessage,
|
|
||||||
IconPencil,
|
|
||||||
IconTrash,
|
|
||||||
IconX,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { DragEvent, FC, KeyboardEvent, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
selectedConversation: Conversation;
|
|
||||||
conversation: Conversation;
|
|
||||||
loading: boolean;
|
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
|
||||||
onDeleteConversation: (conversation: Conversation) => void;
|
|
||||||
onUpdateConversation: (
|
|
||||||
conversation: Conversation,
|
|
||||||
data: KeyValuePair,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ConversationComponent: FC<Props> = ({
|
|
||||||
selectedConversation,
|
|
||||||
conversation,
|
|
||||||
loading,
|
|
||||||
onSelectConversation,
|
|
||||||
onDeleteConversation,
|
|
||||||
onUpdateConversation,
|
|
||||||
}) => {
|
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
|
||||||
const [isRenaming, setIsRenaming] = useState(false);
|
|
||||||
const [renameValue, setRenameValue] = useState('');
|
|
||||||
|
|
||||||
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleRename(selectedConversation);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDragStart = (
|
|
||||||
e: DragEvent<HTMLButtonElement>,
|
|
||||||
conversation: Conversation,
|
|
||||||
) => {
|
|
||||||
if (e.dataTransfer) {
|
|
||||||
e.dataTransfer.setData('conversation', JSON.stringify(conversation));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRename = (conversation: Conversation) => {
|
|
||||||
if (renameValue.trim().length > 0) {
|
|
||||||
onUpdateConversation(conversation, { key: 'name', value: renameValue });
|
|
||||||
setRenameValue('');
|
|
||||||
setIsRenaming(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRenaming) {
|
|
||||||
setIsDeleting(false);
|
|
||||||
} else if (isDeleting) {
|
|
||||||
setIsRenaming(false);
|
|
||||||
}
|
|
||||||
}, [isRenaming, isDeleting]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
{isRenaming && selectedConversation.id === conversation.id ? (
|
|
||||||
<div className="flex w-full items-center gap-3 bg-[#343541]/90 p-3 rounded-lg">
|
|
||||||
<IconMessage size={18} />
|
|
||||||
<input
|
|
||||||
className="mr-12 flex-1 overflow-hidden overflow-ellipsis border-neutral-400 bg-transparent text-left text-[12.5px] leading-3 text-white outline-none focus:border-neutral-100"
|
|
||||||
type="text"
|
|
||||||
value={renameValue}
|
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
|
||||||
onKeyDown={handleEnterDown}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90 ${
|
|
||||||
loading ? 'disabled:cursor-not-allowed' : ''
|
|
||||||
} ${
|
|
||||||
selectedConversation.id === conversation.id ? 'bg-[#343541]/90' : ''
|
|
||||||
}`}
|
|
||||||
onClick={() => onSelectConversation(conversation)}
|
|
||||||
disabled={loading}
|
|
||||||
draggable="true"
|
|
||||||
onDragStart={(e) => handleDragStart(e, conversation)}
|
|
||||||
>
|
|
||||||
<IconMessage size={18} />
|
|
||||||
<div
|
|
||||||
className={`relative max-h-5 flex-1 overflow-hidden text-ellipsis whitespace-nowrap break-all text-left text-[12.5px] leading-3 ${
|
|
||||||
selectedConversation.id === conversation.id ? 'pr-12' : 'pr-1'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{conversation.name}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(isDeleting || isRenaming) &&
|
|
||||||
selectedConversation.id === conversation.id && (
|
|
||||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (isDeleting) {
|
|
||||||
onDeleteConversation(conversation);
|
|
||||||
} else if (isRenaming) {
|
|
||||||
handleRename(conversation);
|
|
||||||
}
|
|
||||||
setIsDeleting(false);
|
|
||||||
setIsRenaming(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconCheck size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(false);
|
|
||||||
setIsRenaming(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconX size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{selectedConversation.id === conversation.id &&
|
|
||||||
!isDeleting &&
|
|
||||||
!isRenaming && (
|
|
||||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsRenaming(true);
|
|
||||||
setRenameValue(selectedConversation.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconPencil size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Conversation } from '@/types/chat';
|
|
||||||
import { KeyValuePair } from '@/types/data';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import { ConversationComponent } from './Conversation';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
loading: boolean;
|
|
||||||
conversations: Conversation[];
|
|
||||||
selectedConversation: Conversation;
|
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
|
||||||
onDeleteConversation: (conversation: Conversation) => void;
|
|
||||||
onUpdateConversation: (
|
|
||||||
conversation: Conversation,
|
|
||||||
data: KeyValuePair,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Conversations: FC<Props> = ({
|
|
||||||
loading,
|
|
||||||
conversations,
|
|
||||||
selectedConversation,
|
|
||||||
onSelectConversation,
|
|
||||||
onDeleteConversation,
|
|
||||||
onUpdateConversation,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full flex-col gap-1">
|
|
||||||
{conversations
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((conversation, index) => (
|
|
||||||
<ConversationComponent
|
|
||||||
key={index}
|
|
||||||
selectedConversation={selectedConversation}
|
|
||||||
conversation={conversation}
|
|
||||||
loading={loading}
|
|
||||||
onSelectConversation={onSelectConversation}
|
|
||||||
onDeleteConversation={onDeleteConversation}
|
|
||||||
onUpdateConversation={onUpdateConversation}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
import { PluginID, PluginKey } from '@/types/plugin';
|
|
||||||
import { IconKey } from '@tabler/icons-react';
|
|
||||||
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { SidebarButton } from '../Sidebar/SidebarButton';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
pluginKeys: PluginKey[];
|
|
||||||
onPluginKeyChange: (pluginKey: PluginKey) => void;
|
|
||||||
onClearPluginKey: (pluginKey: PluginKey) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PluginKeys: FC<Props> = ({
|
|
||||||
pluginKeys,
|
|
||||||
onPluginKeyChange,
|
|
||||||
onClearPluginKey,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('sidebar');
|
|
||||||
|
|
||||||
const [isChanging, setIsChanging] = useState(false);
|
|
||||||
|
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const handleEnter = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
setIsChanging(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
|
||||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
|
||||||
window.addEventListener('mouseup', handleMouseUp);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = (e: MouseEvent) => {
|
|
||||||
window.removeEventListener('mouseup', handleMouseUp);
|
|
||||||
setIsChanging(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('mousedown', handleMouseDown);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('mousedown', handleMouseDown);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SidebarButton
|
|
||||||
text={t('Plugin Keys')}
|
|
||||||
icon={<IconKey size={18} />}
|
|
||||||
onClick={() => setIsChanging(true)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isChanging && (
|
|
||||||
<div
|
|
||||||
className="z-100 fixed inset-0 flex items-center justify-center bg-black bg-opacity-50"
|
|
||||||
onKeyDown={handleEnter}
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
|
||||||
<div className="flex min-h-screen items-center justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<div
|
|
||||||
className="hidden sm:inline-block sm:h-screen sm:align-middle"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={modalRef}
|
|
||||||
className="dark:border-netural-400 inline-block max-h-[400px] transform overflow-hidden rounded-lg border border-gray-300 bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all dark:bg-[#202123] sm:my-8 sm:max-h-[600px] sm:w-full sm:max-w-lg sm:p-6 sm:align-middle"
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
<div className="mb-10 text-4xl">Plugin Keys</div>
|
|
||||||
|
|
||||||
<div className="mt-6 rounded border p-4">
|
|
||||||
<div className="text-xl font-bold">Google Search Plugin</div>
|
|
||||||
<div className="mt-4 italic">
|
|
||||||
Please enter your Google API Key and Google CSE ID to enable
|
|
||||||
the Google Search Plugin.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-6 text-sm font-bold text-black dark:text-neutral-200">
|
|
||||||
Google API Key
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
|
||||||
type="password"
|
|
||||||
value={
|
|
||||||
pluginKeys
|
|
||||||
.find((p) => p.pluginId === PluginID.GOOGLE_SEARCH)
|
|
||||||
?.requiredKeys.find((k) => k.key === 'GOOGLE_API_KEY')
|
|
||||||
?.value
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
const pluginKey = pluginKeys.find(
|
|
||||||
(p) => p.pluginId === PluginID.GOOGLE_SEARCH,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pluginKey) {
|
|
||||||
const requiredKey = pluginKey.requiredKeys.find(
|
|
||||||
(k) => k.key === 'GOOGLE_API_KEY',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (requiredKey) {
|
|
||||||
const updatedPluginKey = {
|
|
||||||
...pluginKey,
|
|
||||||
requiredKeys: pluginKey.requiredKeys.map((k) => {
|
|
||||||
if (k.key === 'GOOGLE_API_KEY') {
|
|
||||||
return {
|
|
||||||
...k,
|
|
||||||
value: e.target.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return k;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
onPluginKeyChange(updatedPluginKey);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newPluginKey: PluginKey = {
|
|
||||||
pluginId: PluginID.GOOGLE_SEARCH,
|
|
||||||
requiredKeys: [
|
|
||||||
{
|
|
||||||
key: 'GOOGLE_API_KEY',
|
|
||||||
value: e.target.value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'GOOGLE_CSE_ID',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
onPluginKeyChange(newPluginKey);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-6 text-sm font-bold text-black dark:text-neutral-200">
|
|
||||||
Google CSE ID
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
|
||||||
type="password"
|
|
||||||
value={
|
|
||||||
pluginKeys
|
|
||||||
.find((p) => p.pluginId === PluginID.GOOGLE_SEARCH)
|
|
||||||
?.requiredKeys.find((k) => k.key === 'GOOGLE_CSE_ID')
|
|
||||||
?.value
|
|
||||||
}
|
|
||||||
onChange={(e) => {
|
|
||||||
const pluginKey = pluginKeys.find(
|
|
||||||
(p) => p.pluginId === PluginID.GOOGLE_SEARCH,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pluginKey) {
|
|
||||||
const requiredKey = pluginKey.requiredKeys.find(
|
|
||||||
(k) => k.key === 'GOOGLE_CSE_ID',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (requiredKey) {
|
|
||||||
const updatedPluginKey = {
|
|
||||||
...pluginKey,
|
|
||||||
requiredKeys: pluginKey.requiredKeys.map((k) => {
|
|
||||||
if (k.key === 'GOOGLE_CSE_ID') {
|
|
||||||
return {
|
|
||||||
...k,
|
|
||||||
value: e.target.value,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return k;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
onPluginKeyChange(updatedPluginKey);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const newPluginKey: PluginKey = {
|
|
||||||
pluginId: PluginID.GOOGLE_SEARCH,
|
|
||||||
requiredKeys: [
|
|
||||||
{
|
|
||||||
key: 'GOOGLE_API_KEY',
|
|
||||||
value: '',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'GOOGLE_CSE_ID',
|
|
||||||
value: e.target.value,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
onPluginKeyChange(newPluginKey);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="mt-6 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow hover:bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-white dark:text-black dark:hover:bg-neutral-300"
|
|
||||||
onClick={() => {
|
|
||||||
const pluginKey = pluginKeys.find(
|
|
||||||
(p) => p.pluginId === PluginID.GOOGLE_SEARCH,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pluginKey) {
|
|
||||||
onClearPluginKey(pluginKey);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Clear Google Search Plugin Keys
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="mt-6 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow hover:bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-white dark:text-black dark:hover:bg-neutral-300"
|
|
||||||
onClick={() => setIsChanging(false)}
|
|
||||||
>
|
|
||||||
{t('Save')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
import { Conversation } from '@/types/chat';
|
|
||||||
import { KeyValuePair } from '@/types/data';
|
|
||||||
import { Folder } from '@/types/folder';
|
|
||||||
import {
|
|
||||||
IconCaretDown,
|
|
||||||
IconCaretRight,
|
|
||||||
IconCheck,
|
|
||||||
IconPencil,
|
|
||||||
IconTrash,
|
|
||||||
IconX,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { FC, KeyboardEvent, useEffect, useState } from 'react';
|
|
||||||
import { ConversationComponent } from '../../Chatbar/Conversation';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
searchTerm: string;
|
|
||||||
conversations: Conversation[];
|
|
||||||
currentFolder: Folder;
|
|
||||||
onDeleteFolder: (folder: string) => void;
|
|
||||||
onUpdateFolder: (folder: string, name: string) => void;
|
|
||||||
// conversation props
|
|
||||||
selectedConversation: Conversation;
|
|
||||||
loading: boolean;
|
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
|
||||||
onDeleteConversation: (conversation: Conversation) => void;
|
|
||||||
onUpdateConversation: (
|
|
||||||
conversation: Conversation,
|
|
||||||
data: KeyValuePair,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatFolder: FC<Props> = ({
|
|
||||||
searchTerm,
|
|
||||||
conversations,
|
|
||||||
currentFolder,
|
|
||||||
onDeleteFolder,
|
|
||||||
onUpdateFolder,
|
|
||||||
// conversation props
|
|
||||||
selectedConversation,
|
|
||||||
loading,
|
|
||||||
onSelectConversation,
|
|
||||||
onDeleteConversation,
|
|
||||||
onUpdateConversation,
|
|
||||||
}) => {
|
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
|
||||||
const [isRenaming, setIsRenaming] = useState(false);
|
|
||||||
const [renameValue, setRenameValue] = useState('');
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleRename();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRename = () => {
|
|
||||||
onUpdateFolder(currentFolder.id, renameValue);
|
|
||||||
setRenameValue('');
|
|
||||||
setIsRenaming(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: any, folder: Folder) => {
|
|
||||||
if (e.dataTransfer) {
|
|
||||||
setIsOpen(true);
|
|
||||||
|
|
||||||
const conversation = JSON.parse(e.dataTransfer.getData('conversation'));
|
|
||||||
onUpdateConversation(conversation, { key: 'folderId', value: folder.id });
|
|
||||||
|
|
||||||
e.target.style.background = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowDrop = (e: any) => {
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlightDrop = (e: any) => {
|
|
||||||
e.target.style.background = '#343541';
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeHighlight = (e: any) => {
|
|
||||||
e.target.style.background = 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRenaming) {
|
|
||||||
setIsDeleting(false);
|
|
||||||
} else if (isDeleting) {
|
|
||||||
setIsRenaming(false);
|
|
||||||
}
|
|
||||||
}, [isRenaming, isDeleting]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchTerm) {
|
|
||||||
setIsOpen(true);
|
|
||||||
} else {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
}, [searchTerm]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
{isRenaming ? (
|
|
||||||
<div className="flex w-full items-center gap-3 bg-[#343541]/90 p-3 rounded-lg">
|
|
||||||
{isOpen ? (
|
|
||||||
<IconCaretDown size={18} />
|
|
||||||
) : (
|
|
||||||
<IconCaretRight size={18} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
|
||||||
className="mr-12 flex-1 overflow-hidden overflow-ellipsis border-neutral-400 bg-transparent text-left text-[12.5px] leading-3 text-white outline-none focus:border-neutral-100"
|
|
||||||
type="text"
|
|
||||||
value={renameValue}
|
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
|
||||||
onKeyDown={handleEnterDown}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90`}
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
onDrop={(e) => handleDrop(e, currentFolder)}
|
|
||||||
onDragOver={allowDrop}
|
|
||||||
onDragEnter={highlightDrop}
|
|
||||||
onDragLeave={removeHighlight}
|
|
||||||
>
|
|
||||||
{isOpen ? (
|
|
||||||
<IconCaretDown size={18} />
|
|
||||||
) : (
|
|
||||||
<IconCaretRight size={18} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis whitespace-nowrap break-all text-left text-[12.5px] leading-3">
|
|
||||||
{currentFolder.name}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(isDeleting || isRenaming) && (
|
|
||||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (isDeleting) {
|
|
||||||
onDeleteFolder(currentFolder.id);
|
|
||||||
} else if (isRenaming) {
|
|
||||||
handleRename();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDeleting(false);
|
|
||||||
setIsRenaming(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconCheck size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(false);
|
|
||||||
setIsRenaming(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconX size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isDeleting && !isRenaming && (
|
|
||||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsRenaming(true);
|
|
||||||
setRenameValue(currentFolder.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconPencil size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isOpen
|
|
||||||
? conversations.map((conversation, index) => {
|
|
||||||
if (conversation.folderId === currentFolder.id) {
|
|
||||||
return (
|
|
||||||
<div key={index} className="ml-5 gap-2 border-l pl-2">
|
|
||||||
<ConversationComponent
|
|
||||||
selectedConversation={selectedConversation}
|
|
||||||
conversation={conversation}
|
|
||||||
loading={loading}
|
|
||||||
onSelectConversation={onSelectConversation}
|
|
||||||
onDeleteConversation={onDeleteConversation}
|
|
||||||
onUpdateConversation={onUpdateConversation}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
import { Conversation } from '@/types/chat';
|
|
||||||
import { KeyValuePair } from '@/types/data';
|
|
||||||
import { Folder } from '@/types/folder';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import { ChatFolder } from './ChatFolder';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
searchTerm: string;
|
|
||||||
conversations: Conversation[];
|
|
||||||
folders: Folder[];
|
|
||||||
onDeleteFolder: (folder: string) => void;
|
|
||||||
onUpdateFolder: (folder: string, name: string) => void;
|
|
||||||
// conversation props
|
|
||||||
selectedConversation: Conversation;
|
|
||||||
loading: boolean;
|
|
||||||
onSelectConversation: (conversation: Conversation) => void;
|
|
||||||
onDeleteConversation: (conversation: Conversation) => void;
|
|
||||||
onUpdateConversation: (
|
|
||||||
conversation: Conversation,
|
|
||||||
data: KeyValuePair,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ChatFolders: FC<Props> = ({
|
|
||||||
searchTerm,
|
|
||||||
conversations,
|
|
||||||
folders,
|
|
||||||
onDeleteFolder,
|
|
||||||
onUpdateFolder,
|
|
||||||
// conversation props
|
|
||||||
selectedConversation,
|
|
||||||
loading,
|
|
||||||
onSelectConversation,
|
|
||||||
onDeleteConversation,
|
|
||||||
onUpdateConversation,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full flex-col pt-2">
|
|
||||||
{folders.map((folder, index) => (
|
|
||||||
<ChatFolder
|
|
||||||
key={index}
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
conversations={conversations.filter((c) => c.folderId)}
|
|
||||||
currentFolder={folder}
|
|
||||||
onDeleteFolder={onDeleteFolder}
|
|
||||||
onUpdateFolder={onUpdateFolder}
|
|
||||||
// conversation props
|
|
||||||
selectedConversation={selectedConversation}
|
|
||||||
loading={loading}
|
|
||||||
onSelectConversation={onSelectConversation}
|
|
||||||
onDeleteConversation={onDeleteConversation}
|
|
||||||
onUpdateConversation={onUpdateConversation}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
import { PromptComponent } from '@/components/Promptbar/Prompt';
|
|
||||||
import { Folder } from '@/types/folder';
|
|
||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import {
|
|
||||||
IconCaretDown,
|
|
||||||
IconCaretRight,
|
|
||||||
IconCheck,
|
|
||||||
IconPencil,
|
|
||||||
IconTrash,
|
|
||||||
IconX,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { FC, KeyboardEvent, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
searchTerm: string;
|
|
||||||
prompts: Prompt[];
|
|
||||||
currentFolder: Folder;
|
|
||||||
onDeleteFolder: (folder: string) => void;
|
|
||||||
onUpdateFolder: (folder: string, name: string) => void;
|
|
||||||
// prompt props
|
|
||||||
onDeletePrompt: (prompt: Prompt) => void;
|
|
||||||
onUpdatePrompt: (prompt: Prompt) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PromptFolder: FC<Props> = ({
|
|
||||||
searchTerm,
|
|
||||||
prompts,
|
|
||||||
currentFolder,
|
|
||||||
onDeleteFolder,
|
|
||||||
onUpdateFolder,
|
|
||||||
// prompt props
|
|
||||||
onDeletePrompt,
|
|
||||||
onUpdatePrompt,
|
|
||||||
}) => {
|
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
|
||||||
const [isRenaming, setIsRenaming] = useState(false);
|
|
||||||
const [renameValue, setRenameValue] = useState('');
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleRename();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRename = () => {
|
|
||||||
onUpdateFolder(currentFolder.id, renameValue);
|
|
||||||
setRenameValue('');
|
|
||||||
setIsRenaming(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: any, folder: Folder) => {
|
|
||||||
if (e.dataTransfer) {
|
|
||||||
setIsOpen(true);
|
|
||||||
|
|
||||||
const prompt = JSON.parse(e.dataTransfer.getData('prompt'));
|
|
||||||
|
|
||||||
const updatedPrompt = {
|
|
||||||
...prompt,
|
|
||||||
folderId: folder.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdatePrompt(updatedPrompt);
|
|
||||||
|
|
||||||
e.target.style.background = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowDrop = (e: any) => {
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlightDrop = (e: any) => {
|
|
||||||
e.target.style.background = '#343541';
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeHighlight = (e: any) => {
|
|
||||||
e.target.style.background = 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRenaming) {
|
|
||||||
setIsDeleting(false);
|
|
||||||
} else if (isDeleting) {
|
|
||||||
setIsRenaming(false);
|
|
||||||
}
|
|
||||||
}, [isRenaming, isDeleting]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchTerm) {
|
|
||||||
setIsOpen(true);
|
|
||||||
} else {
|
|
||||||
setIsOpen(false);
|
|
||||||
}
|
|
||||||
}, [searchTerm]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
{isRenaming ? (
|
|
||||||
<div className="flex w-full items-center gap-3 bg-[#343541]/90 p-3">
|
|
||||||
{isOpen ? (
|
|
||||||
<IconCaretDown size={18} />
|
|
||||||
) : (
|
|
||||||
<IconCaretRight size={18} />
|
|
||||||
)}
|
|
||||||
<input
|
|
||||||
className="mr-12 flex-1 overflow-hidden overflow-ellipsis border-neutral-400 bg-transparent text-left text-[12.5px] leading-3 text-white outline-none focus:border-neutral-100"
|
|
||||||
type="text"
|
|
||||||
value={renameValue}
|
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
|
||||||
onKeyDown={handleEnterDown}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
className={`flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90`}
|
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
|
||||||
onDrop={(e) => handleDrop(e, currentFolder)}
|
|
||||||
onDragOver={allowDrop}
|
|
||||||
onDragEnter={highlightDrop}
|
|
||||||
onDragLeave={removeHighlight}
|
|
||||||
>
|
|
||||||
{isOpen ? (
|
|
||||||
<IconCaretDown size={18} />
|
|
||||||
) : (
|
|
||||||
<IconCaretRight size={18} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis whitespace-nowrap break-all text-left text-[12.5px] leading-3">
|
|
||||||
{currentFolder.name}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{(isDeleting || isRenaming) && (
|
|
||||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (isDeleting) {
|
|
||||||
onDeleteFolder(currentFolder.id);
|
|
||||||
} else if (isRenaming) {
|
|
||||||
handleRename();
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDeleting(false);
|
|
||||||
setIsRenaming(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconCheck size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(false);
|
|
||||||
setIsRenaming(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconX size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isDeleting && !isRenaming && (
|
|
||||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsRenaming(true);
|
|
||||||
setRenameValue(currentFolder.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconPencil size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isOpen
|
|
||||||
? prompts.map((prompt, index) => {
|
|
||||||
if (prompt.folderId === currentFolder.id) {
|
|
||||||
return (
|
|
||||||
<div key={index} className="ml-5 gap-2 border-l pl-2">
|
|
||||||
<PromptComponent
|
|
||||||
prompt={prompt}
|
|
||||||
onDeletePrompt={onDeletePrompt}
|
|
||||||
onUpdatePrompt={onUpdatePrompt}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
: null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import { Folder } from '@/types/folder';
|
|
||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import { PromptFolder } from './PromptFolder';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
searchTerm: string;
|
|
||||||
prompts: Prompt[];
|
|
||||||
folders: Folder[];
|
|
||||||
onDeleteFolder: (folder: string) => void;
|
|
||||||
onUpdateFolder: (folder: string, name: string) => void;
|
|
||||||
// prompt props
|
|
||||||
onDeletePrompt: (prompt: Prompt) => void;
|
|
||||||
onUpdatePrompt: (prompt: Prompt) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PromptFolders: FC<Props> = ({
|
|
||||||
searchTerm,
|
|
||||||
prompts,
|
|
||||||
folders,
|
|
||||||
onDeleteFolder,
|
|
||||||
onUpdateFolder,
|
|
||||||
// prompt props
|
|
||||||
onDeletePrompt,
|
|
||||||
onUpdatePrompt,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full flex-col pt-2">
|
|
||||||
{folders.map((folder, index) => (
|
|
||||||
<PromptFolder
|
|
||||||
key={index}
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
prompts={prompts.filter((p) => p.folderId)}
|
|
||||||
currentFolder={folder}
|
|
||||||
onDeleteFolder={onDeleteFolder}
|
|
||||||
onUpdateFolder={onUpdateFolder}
|
|
||||||
// prompt props
|
|
||||||
onDeletePrompt={onDeletePrompt}
|
|
||||||
onUpdatePrompt={onUpdatePrompt}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { FC } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
size?: string;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Spinner: FC<Props> = ({ size = '1em', className="" }) => {
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
stroke="currentColor"
|
|
||||||
fill="none"
|
|
||||||
strokeWidth="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
className={`animate-spin ${className}`}
|
|
||||||
height={size}
|
|
||||||
width={size}
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
>
|
|
||||||
<line x1="12" y1="2" x2="12" y2="6"></line>
|
|
||||||
<line x1="12" y1="18" x2="12" y2="22"></line>
|
|
||||||
<line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line>
|
|
||||||
<line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line>
|
|
||||||
<line x1="2" y1="12" x2="6" y2="12"></line>
|
|
||||||
<line x1="18" y1="12" x2="22" y2="12"></line>
|
|
||||||
<line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line>
|
|
||||||
<line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,96 +1,41 @@
|
|||||||
import {
|
import { FC, useState } from "react";
|
||||||
generateRandomString,
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
programmingLanguages,
|
import { oneDark, oneLight } from "react-syntax-highlighter/dist/cjs/styles/prism";
|
||||||
} from '@/utils/app/codeblock';
|
|
||||||
import { IconCheck, IconClipboard, IconDownload } from '@tabler/icons-react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { FC, memo, useState } from 'react';
|
|
||||||
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
|
|
||||||
import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
language: string;
|
language: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
lightMode: "light" | "dark";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CodeBlock: FC<Props> = memo(({ language, value }) => {
|
export const CodeBlock: FC<Props> = ({ language, value, lightMode }) => {
|
||||||
const { t } = useTranslation('markdown');
|
const [buttonText, setButtonText] = useState("Copy code");
|
||||||
const [isCopied, setIsCopied] = useState<Boolean>(false);
|
|
||||||
|
|
||||||
const copyToClipboard = () => {
|
const copyToClipboard = () => {
|
||||||
if (!navigator.clipboard || !navigator.clipboard.writeText) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(value).then(() => {
|
navigator.clipboard.writeText(value).then(() => {
|
||||||
setIsCopied(true);
|
setButtonText("Copied!");
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsCopied(false);
|
setButtonText("Copy code");
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
const downloadAsFile = () => {
|
|
||||||
const fileExtension = programmingLanguages[language] || '.file';
|
|
||||||
const suggestedFileName = `file-${generateRandomString(
|
|
||||||
3,
|
|
||||||
true,
|
|
||||||
)}${fileExtension}`;
|
|
||||||
const fileName = window.prompt(
|
|
||||||
t('Enter file name') || '',
|
|
||||||
suggestedFileName,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fileName) {
|
|
||||||
// user pressed cancel on prompt
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blob = new Blob([value], { type: 'text/plain' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.download = fileName;
|
|
||||||
link.href = url;
|
|
||||||
link.style.display = 'none';
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<div className="codeblock relative font-sans text-[16px]">
|
<div className="relative text-[16px] pt-2">
|
||||||
<div className="flex items-center justify-between py-1.5 px-4">
|
|
||||||
<span className="text-xs lowercase text-white">{language}</span>
|
|
||||||
|
|
||||||
<div className="flex items-center">
|
|
||||||
<button
|
|
||||||
className="flex gap-1.5 items-center rounded bg-none p-1 text-xs text-white"
|
|
||||||
onClick={copyToClipboard}
|
|
||||||
>
|
|
||||||
{isCopied ? (
|
|
||||||
<IconCheck size={18} />
|
|
||||||
) : (
|
|
||||||
<IconClipboard size={18} />
|
|
||||||
)}
|
|
||||||
{isCopied ? t('Copied!') : t('Copy code')}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="flex items-center rounded bg-none p-1 text-xs text-white"
|
|
||||||
onClick={downloadAsFile}
|
|
||||||
>
|
|
||||||
<IconDownload size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<SyntaxHighlighter
|
<SyntaxHighlighter
|
||||||
language={language}
|
language={language}
|
||||||
style={oneDark}
|
style={lightMode === "light" ? oneLight : oneDark}
|
||||||
customStyle={{ margin: 0 }}
|
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</SyntaxHighlighter>
|
</SyntaxHighlighter>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="absolute top-[-8px] right-[0px] text-white bg-none py-0.5 px-2 rounded focus:outline-none hover:bg-blue-700 text-xs"
|
||||||
|
onClick={copyToClipboard}
|
||||||
|
>
|
||||||
|
{buttonText}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
};
|
||||||
CodeBlock.displayName = 'CodeBlock';
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
import { FC, memo } from 'react';
|
|
||||||
import ReactMarkdown, { Options } from 'react-markdown';
|
|
||||||
|
|
||||||
export const MemoizedReactMarkdown: FC<Options> = memo(ReactMarkdown);
|
|
||||||
@@ -1,28 +1,23 @@
|
|||||||
import { Conversation } from '@/types/chat';
|
import { Conversation } from "@/types";
|
||||||
import { IconPlus } from '@tabler/icons-react';
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { FC } from 'react';
|
import { FC } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedConversation: Conversation;
|
selectedConversation: Conversation;
|
||||||
onNewConversation: () => void;
|
onNewConversation: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Navbar: FC<Props> = ({
|
export const Navbar: FC<Props> = ({ selectedConversation, onNewConversation }) => {
|
||||||
selectedConversation,
|
|
||||||
onNewConversation,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex w-full justify-between bg-[#202123] py-3 px-4">
|
<div className="flex justify-between bg-[#202123] py-3 px-4 w-full">
|
||||||
<div className="mr-4"></div>
|
<div className="mr-4"></div>
|
||||||
|
|
||||||
<div className="max-w-[240px] overflow-hidden text-ellipsis whitespace-nowrap">
|
<div className="max-w-[240px] whitespace-nowrap overflow-hidden text-ellipsis">{selectedConversation.name}</div>
|
||||||
{selectedConversation.name}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<IconPlus
|
<IconPlus
|
||||||
className="cursor-pointer hover:text-neutral-400 mr-8"
|
className="cursor-pointer hover:text-neutral-400"
|
||||||
onClick={onNewConversation}
|
onClick={onNewConversation}
|
||||||
/>
|
/>
|
||||||
</nav>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,116 +0,0 @@
|
|||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import {
|
|
||||||
IconBulbFilled,
|
|
||||||
IconCheck,
|
|
||||||
IconTrash,
|
|
||||||
IconX,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { DragEvent, FC, useEffect, useState } from 'react';
|
|
||||||
import { PromptModal } from './PromptModal';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
prompt: Prompt;
|
|
||||||
onUpdatePrompt: (prompt: Prompt) => void;
|
|
||||||
onDeletePrompt: (prompt: Prompt) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PromptComponent: FC<Props> = ({
|
|
||||||
prompt,
|
|
||||||
onUpdatePrompt,
|
|
||||||
onDeletePrompt,
|
|
||||||
}) => {
|
|
||||||
const [showModal, setShowModal] = useState<boolean>(false);
|
|
||||||
const [isDeleting, setIsDeleting] = useState(false);
|
|
||||||
const [isRenaming, setIsRenaming] = useState(false);
|
|
||||||
const [renameValue, setRenameValue] = useState('');
|
|
||||||
|
|
||||||
const handleDragStart = (e: DragEvent<HTMLButtonElement>, prompt: Prompt) => {
|
|
||||||
if (e.dataTransfer) {
|
|
||||||
e.dataTransfer.setData('prompt', JSON.stringify(prompt));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isRenaming) {
|
|
||||||
setIsDeleting(false);
|
|
||||||
} else if (isDeleting) {
|
|
||||||
setIsRenaming(false);
|
|
||||||
}
|
|
||||||
}, [isRenaming, isDeleting]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="relative flex items-center">
|
|
||||||
<button
|
|
||||||
className="flex w-full cursor-pointer items-center gap-3 rounded-lg p-3 text-sm transition-colors duration-200 hover:bg-[#343541]/90"
|
|
||||||
draggable="true"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setShowModal(true);
|
|
||||||
}}
|
|
||||||
onDragStart={(e) => handleDragStart(e, prompt)}
|
|
||||||
onMouseLeave={() => {
|
|
||||||
setIsDeleting(false);
|
|
||||||
setIsRenaming(false);
|
|
||||||
setRenameValue('');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconBulbFilled size={18} />
|
|
||||||
|
|
||||||
<div className="relative max-h-5 flex-1 overflow-hidden text-ellipsis whitespace-nowrap break-all pr-4 text-left text-[12.5px] leading-3">
|
|
||||||
{prompt.name}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{(isDeleting || isRenaming) && (
|
|
||||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (isDeleting) {
|
|
||||||
onDeletePrompt(prompt);
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsDeleting(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconCheck size={18} />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconX size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!isDeleting && !isRenaming && (
|
|
||||||
<div className="absolute right-1 z-10 flex text-gray-300">
|
|
||||||
<button
|
|
||||||
className="min-w-[20px] p-1 text-neutral-400 hover:text-neutral-100"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setIsDeleting(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconTrash size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{showModal && (
|
|
||||||
<PromptModal
|
|
||||||
prompt={prompt}
|
|
||||||
onClose={() => setShowModal(false)}
|
|
||||||
onUpdatePrompt={onUpdatePrompt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
prompt: Prompt;
|
|
||||||
onClose: () => void;
|
|
||||||
onUpdatePrompt: (prompt: Prompt) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PromptModal: FC<Props> = ({ prompt, onClose, onUpdatePrompt }) => {
|
|
||||||
const { t } = useTranslation('promptbar');
|
|
||||||
const [name, setName] = useState(prompt.name);
|
|
||||||
const [description, setDescription] = useState(prompt.description);
|
|
||||||
const [content, setContent] = useState(prompt.content);
|
|
||||||
|
|
||||||
const modalRef = useRef<HTMLDivElement>(null);
|
|
||||||
const nameInputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleEnter = (e: KeyboardEvent<HTMLDivElement>) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
onUpdatePrompt({ ...prompt, name, description, content: content.trim() });
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
|
||||||
if (modalRef.current && !modalRef.current.contains(e.target as Node)) {
|
|
||||||
window.addEventListener('mouseup', handleMouseUp);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = (e: MouseEvent) => {
|
|
||||||
window.removeEventListener('mouseup', handleMouseUp);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('mousedown', handleMouseDown);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener('mousedown', handleMouseDown);
|
|
||||||
};
|
|
||||||
}, [onClose]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
nameInputRef.current?.focus();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 z-100"
|
|
||||||
onKeyDown={handleEnter}
|
|
||||||
>
|
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
|
||||||
<div className="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
|
||||||
<div
|
|
||||||
className="hidden sm:inline-block sm:h-screen sm:align-middle"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div
|
|
||||||
ref={modalRef}
|
|
||||||
className="dark:border-netural-400 inline-block max-h-[400px] transform overflow-hidden rounded-lg border border-gray-300 bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all dark:bg-[#202123] sm:my-8 sm:max-h-[600px] sm:w-full sm:max-w-lg sm:p-6 sm:align-middle"
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-bold text-black dark:text-neutral-200">
|
|
||||||
{t('Name')}
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
ref={nameInputRef}
|
|
||||||
className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
|
||||||
placeholder={t('A name for your prompt.') || ''}
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-6 text-sm font-bold text-black dark:text-neutral-200">
|
|
||||||
{t('Description')}
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
|
||||||
style={{ resize: 'none' }}
|
|
||||||
placeholder={t('A description for your prompt.') || ''}
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="mt-6 text-sm font-bold text-black dark:text-neutral-200">
|
|
||||||
{t('Prompt')}
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
className="mt-2 w-full rounded-lg border border-neutral-500 px-4 py-2 text-neutral-900 shadow focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-[#40414F] dark:text-neutral-100"
|
|
||||||
style={{ resize: 'none' }}
|
|
||||||
placeholder={
|
|
||||||
t(
|
|
||||||
'Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}',
|
|
||||||
) || ''
|
|
||||||
}
|
|
||||||
value={content}
|
|
||||||
onChange={(e) => setContent(e.target.value)}
|
|
||||||
rows={10}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="w-full px-4 py-2 mt-6 border rounded-lg shadow border-neutral-500 text-neutral-900 hover:bg-neutral-100 focus:outline-none dark:border-neutral-800 dark:border-opacity-50 dark:bg-white dark:text-black dark:hover:bg-neutral-300"
|
|
||||||
onClick={() => {
|
|
||||||
const updatedPrompt = {
|
|
||||||
...prompt,
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
content: content.trim(),
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdatePrompt(updatedPrompt);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{t('Save')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import { Folder } from '@/types/folder';
|
|
||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import {
|
|
||||||
IconFolderPlus,
|
|
||||||
IconMistOff,
|
|
||||||
IconPlus,
|
|
||||||
} from '@tabler/icons-react';
|
|
||||||
import { FC, useEffect, useState } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { PromptFolders } from '../Folders/Prompt/PromptFolders';
|
|
||||||
import { Search } from '../Sidebar/Search';
|
|
||||||
import { PromptbarSettings } from './PromptbarSettings';
|
|
||||||
import { Prompts } from './Prompts';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
prompts: Prompt[];
|
|
||||||
folders: Folder[];
|
|
||||||
onCreateFolder: (name: string) => void;
|
|
||||||
onDeleteFolder: (folderId: string) => void;
|
|
||||||
onUpdateFolder: (folderId: string, name: string) => void;
|
|
||||||
onCreatePrompt: () => void;
|
|
||||||
onUpdatePrompt: (prompt: Prompt) => void;
|
|
||||||
onDeletePrompt: (prompt: Prompt) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Promptbar: FC<Props> = ({
|
|
||||||
folders,
|
|
||||||
prompts,
|
|
||||||
onCreateFolder,
|
|
||||||
onDeleteFolder,
|
|
||||||
onUpdateFolder,
|
|
||||||
onCreatePrompt,
|
|
||||||
onUpdatePrompt,
|
|
||||||
onDeletePrompt,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation('promptbar');
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
|
||||||
const [filteredPrompts, setFilteredPrompts] = useState<Prompt[]>(prompts);
|
|
||||||
|
|
||||||
const handleUpdatePrompt = (prompt: Prompt) => {
|
|
||||||
onUpdatePrompt(prompt);
|
|
||||||
setSearchTerm('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDeletePrompt = (prompt: Prompt) => {
|
|
||||||
onDeletePrompt(prompt);
|
|
||||||
setSearchTerm('');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrop = (e: any) => {
|
|
||||||
if (e.dataTransfer) {
|
|
||||||
const prompt = JSON.parse(e.dataTransfer.getData('prompt'));
|
|
||||||
|
|
||||||
const updatedPrompt = {
|
|
||||||
...prompt,
|
|
||||||
folderId: e.target.dataset.folderId,
|
|
||||||
};
|
|
||||||
|
|
||||||
onUpdatePrompt(updatedPrompt);
|
|
||||||
|
|
||||||
e.target.style.background = 'none';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const allowDrop = (e: any) => {
|
|
||||||
e.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
const highlightDrop = (e: any) => {
|
|
||||||
e.target.style.background = '#343541';
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeHighlight = (e: any) => {
|
|
||||||
e.target.style.background = 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (searchTerm) {
|
|
||||||
setFilteredPrompts(
|
|
||||||
prompts.filter((prompt) => {
|
|
||||||
const searchable =
|
|
||||||
prompt.name.toLowerCase() +
|
|
||||||
' ' +
|
|
||||||
prompt.description.toLowerCase() +
|
|
||||||
' ' +
|
|
||||||
prompt.content.toLowerCase();
|
|
||||||
return searchable.includes(searchTerm.toLowerCase());
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
setFilteredPrompts(prompts);
|
|
||||||
}
|
|
||||||
}, [searchTerm, prompts]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`fixed top-0 right-0 z-50 flex h-full w-[260px] flex-none flex-col space-y-2 bg-[#202123] p-2 text-[14px] transition-all sm:relative sm:top-0`}
|
|
||||||
>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<button
|
|
||||||
className="text-sidebar flex w-[190px] flex-shrink-0 cursor-pointer select-none items-center gap-3 rounded-md border border-white/20 p-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
|
|
||||||
onClick={() => {
|
|
||||||
onCreatePrompt();
|
|
||||||
setSearchTerm('');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<IconPlus size={16} />
|
|
||||||
{t('New prompt')}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="flex items-center flex-shrink-0 gap-3 p-3 ml-2 text-sm text-white transition-colors duration-200 border rounded-md cursor-pointer border-white/20 hover:bg-gray-500/10"
|
|
||||||
onClick={() => onCreateFolder(t('New folder'))}
|
|
||||||
>
|
|
||||||
<IconFolderPlus size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{prompts.length > 1 && (
|
|
||||||
<Search
|
|
||||||
placeholder={t('Search prompts...') || ''}
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
onSearch={setSearchTerm}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex-grow overflow-auto">
|
|
||||||
{folders.length > 0 && (
|
|
||||||
<div className="flex pb-2 border-b border-white/20">
|
|
||||||
<PromptFolders
|
|
||||||
searchTerm={searchTerm}
|
|
||||||
prompts={filteredPrompts}
|
|
||||||
folders={folders}
|
|
||||||
onUpdateFolder={onUpdateFolder}
|
|
||||||
onDeleteFolder={onDeleteFolder}
|
|
||||||
// prompt props
|
|
||||||
onDeletePrompt={handleDeletePrompt}
|
|
||||||
onUpdatePrompt={handleUpdatePrompt}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{prompts.length > 0 ? (
|
|
||||||
<div
|
|
||||||
className="pt-2"
|
|
||||||
onDrop={(e) => handleDrop(e)}
|
|
||||||
onDragOver={allowDrop}
|
|
||||||
onDragEnter={highlightDrop}
|
|
||||||
onDragLeave={removeHighlight}
|
|
||||||
>
|
|
||||||
<Prompts
|
|
||||||
prompts={filteredPrompts.filter((prompt) => !prompt.folderId)}
|
|
||||||
onUpdatePrompt={handleUpdatePrompt}
|
|
||||||
onDeletePrompt={handleDeletePrompt}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="mt-8 text-center text-white opacity-50 select-none">
|
|
||||||
<IconMistOff className="mx-auto mb-3" />
|
|
||||||
<span className="text-[14px] leading-normal">
|
|
||||||
{t('No prompts.')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<PromptbarSettings />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
interface Props {}
|
|
||||||
|
|
||||||
export const PromptbarSettings: FC<Props> = () => {
|
|
||||||
return <div></div>;
|
|
||||||
};
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { Prompt } from '@/types/prompt';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import { PromptComponent } from './Prompt';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
prompts: Prompt[];
|
|
||||||
onUpdatePrompt: (prompt: Prompt) => void;
|
|
||||||
onDeletePrompt: (prompt: Prompt) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Prompts: FC<Props> = ({
|
|
||||||
prompts,
|
|
||||||
onUpdatePrompt,
|
|
||||||
onDeletePrompt,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full flex-col gap-1">
|
|
||||||
{prompts
|
|
||||||
.slice()
|
|
||||||
.reverse()
|
|
||||||
.map((prompt, index) => (
|
|
||||||
<PromptComponent
|
|
||||||
key={index}
|
|
||||||
prompt={prompt}
|
|
||||||
onUpdatePrompt={onUpdatePrompt}
|
|
||||||
onDeletePrompt={onDeletePrompt}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { SupportedExportFormats } from '@/types/export';
|
|
||||||
import { IconFileImport } from '@tabler/icons-react';
|
|
||||||
import { useTranslation } from 'next-i18next';
|
|
||||||
import { FC } from 'react';
|
|
||||||
import { SidebarButton } from '../Sidebar/SidebarButton';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onImport: (data: SupportedExportFormats) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Import: FC<Props> = ({ onImport }) => {
|
|
||||||
const { t } = useTranslation('sidebar');
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<input
|
|
||||||
id="import-file"
|
|
||||||
className="sr-only"
|
|
||||||
tabIndex={-1}
|
|
||||||
type="file"
|
|
||||||
accept=".json"
|
|
||||||
onChange={(e) => {
|
|
||||||
if (!e.target.files?.length) return;
|
|
||||||
|
|
||||||
const file = e.target.files[0];
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
let json = JSON.parse(e.target?.result as string);
|
|
||||||
onImport(json);
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SidebarButton
|
|
||||||
text={t('Import data')}
|
|
||||||
icon={<IconFileImport size={18} />}
|
|
||||||
onClick={() => {
|
|
||||||
const importFile = document.querySelector(
|
|
||||||
'#import-file',
|
|
||||||
) as HTMLInputElement;
|
|
||||||
if (importFile) {
|
|
||||||
importFile.click();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+9
-14
@@ -1,7 +1,6 @@
|
|||||||
import { IconCheck, IconTrash, IconX } from '@tabler/icons-react';
|
import { IconCheck, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
import { useTranslation } from 'next-i18next';
|
import { FC, useState } from "react";
|
||||||
import { FC, useState } from 'react';
|
import { SidebarButton } from "./SidebarButton";
|
||||||
import { SidebarButton } from '../Sidebar/SidebarButton';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onClearConversations: () => void;
|
onClearConversations: () => void;
|
||||||
@@ -10,24 +9,20 @@ interface Props {
|
|||||||
export const ClearConversations: FC<Props> = ({ onClearConversations }) => {
|
export const ClearConversations: FC<Props> = ({ onClearConversations }) => {
|
||||||
const [isConfirming, setIsConfirming] = useState<boolean>(false);
|
const [isConfirming, setIsConfirming] = useState<boolean>(false);
|
||||||
|
|
||||||
const { t } = useTranslation('sidebar');
|
|
||||||
|
|
||||||
const handleClearConversations = () => {
|
const handleClearConversations = () => {
|
||||||
onClearConversations();
|
onClearConversations();
|
||||||
setIsConfirming(false);
|
setIsConfirming(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return isConfirming ? (
|
return isConfirming ? (
|
||||||
<div className="flex w-full cursor-pointer items-center rounded-lg py-3 px-3 hover:bg-gray-500/10">
|
<div className="flex hover:bg-[#343541] py-2 px-2 rounded-md cursor-pointer w-full items-center">
|
||||||
<IconTrash size={18} />
|
<IconTrash size={16} />
|
||||||
|
|
||||||
<div className="ml-3 flex-1 text-left text-[12.5px] leading-3 text-white">
|
<div className="ml-2 flex-1 text-left text-white">Are you sure?</div>
|
||||||
{t('Are you sure?')}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-[40px]">
|
<div className="flex w-[40px]">
|
||||||
<IconCheck
|
<IconCheck
|
||||||
className="ml-auto min-w-[20px] mr-1 text-neutral-400 hover:text-neutral-100"
|
className="ml-auto min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
size={18}
|
size={18}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -47,8 +42,8 @@ export const ClearConversations: FC<Props> = ({ onClearConversations }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
text={t('Clear conversations')}
|
text="Clear conversations"
|
||||||
icon={<IconTrash size={18} />}
|
icon={<IconTrash size={16} />}
|
||||||
onClick={() => setIsConfirming(true)}
|
onClick={() => setIsConfirming(true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Conversation } from "@/types";
|
||||||
|
import { IconCheck, IconMessage, IconPencil, IconTrash, IconX } from "@tabler/icons-react";
|
||||||
|
import { FC, KeyboardEvent, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading: boolean;
|
||||||
|
conversations: Conversation[];
|
||||||
|
selectedConversation: Conversation;
|
||||||
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
|
onRenameConversation: (conversation: Conversation, name: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Conversations: FC<Props> = ({ loading, conversations, selectedConversation, onSelectConversation, onDeleteConversation, onRenameConversation }) => {
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false);
|
||||||
|
const [isRenaming, setIsRenaming] = useState(false);
|
||||||
|
const [renameValue, setRenameValue] = useState("");
|
||||||
|
|
||||||
|
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
handleRename(selectedConversation);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = (conversation: Conversation) => {
|
||||||
|
onRenameConversation(conversation, renameValue);
|
||||||
|
setRenameValue("");
|
||||||
|
setIsRenaming(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isRenaming) {
|
||||||
|
setIsDeleting(false);
|
||||||
|
} else if (isDeleting) {
|
||||||
|
setIsRenaming(false);
|
||||||
|
}
|
||||||
|
}, [isRenaming, isDeleting]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col-reverse gap-1 w-full pt-2">
|
||||||
|
{conversations.map((conversation, index) => (
|
||||||
|
<button
|
||||||
|
key={index}
|
||||||
|
className={`flex gap-3 items-center p-3 text-sm rounded-lg hover:bg-[#343541]/90 transition-colors duration-200 cursor-pointer ${loading ? "disabled:cursor-not-allowed" : ""} ${selectedConversation.id === conversation.id ? "bg-[#343541]/90" : ""}`}
|
||||||
|
onClick={() => onSelectConversation(conversation)}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
<IconMessage
|
||||||
|
className=""
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isRenaming && selectedConversation.id === conversation.id ? (
|
||||||
|
<input
|
||||||
|
className="flex-1 bg-transparent border-b border-neutral-400 focus:border-neutral-100 text-left overflow-hidden overflow-ellipsis pr-1 outline-none text-white"
|
||||||
|
type="text"
|
||||||
|
value={renameValue}
|
||||||
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
|
onKeyDown={handleEnterDown}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-hidden whitespace-nowrap overflow-ellipsis pr-1 flex-1 text-left">{conversation.name}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(isDeleting || isRenaming) && selectedConversation.id === conversation.id && (
|
||||||
|
<div className="flex gap-1 -ml-2">
|
||||||
|
<IconCheck
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={16}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isDeleting) {
|
||||||
|
onDeleteConversation(conversation);
|
||||||
|
} else if (isRenaming) {
|
||||||
|
handleRename(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsDeleting(false);
|
||||||
|
setIsRenaming(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconX
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={16}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(false);
|
||||||
|
setIsRenaming(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selectedConversation.id === conversation.id && !isDeleting && !isRenaming && (
|
||||||
|
<div className="flex gap-1 -ml-2">
|
||||||
|
<IconPencil
|
||||||
|
className="min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsRenaming(true);
|
||||||
|
setRenameValue(selectedConversation.name);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconTrash
|
||||||
|
className=" min-w-[20px] text-neutral-400 hover:text-neutral-100"
|
||||||
|
size={18}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setIsDeleting(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Conversation } from "@/types";
|
||||||
|
import { IconFileImport } from "@tabler/icons-react";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onImport: (conversations: Conversation[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Import: FC<Props> = ({ onImport }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex py-3 px-3 gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer w-full items-center">
|
||||||
|
<input
|
||||||
|
className="opacity-0 absolute w-[200px] cursor-pointer"
|
||||||
|
type="file"
|
||||||
|
accept=".json"
|
||||||
|
onChange={(e) => {
|
||||||
|
if (!e.target.files?.length) return;
|
||||||
|
|
||||||
|
const file = e.target.files[0];
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const conversations: Conversation[] = JSON.parse(e.target?.result as string);
|
||||||
|
onImport(conversations);
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-3 text-left">
|
||||||
|
<IconFileImport size={16} />
|
||||||
|
<div>Import conversations</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { IconCheck, IconKey, IconX } from '@tabler/icons-react';
|
import { IconCheck, IconKey, IconX } from "@tabler/icons-react";
|
||||||
import { useTranslation } from 'next-i18next';
|
import { FC, KeyboardEvent, useState } from "react";
|
||||||
import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react';
|
import { SidebarButton } from "./SidebarButton";
|
||||||
import { SidebarButton } from '../Sidebar/SidebarButton';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
@@ -9,13 +8,11 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => {
|
export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => {
|
||||||
const { t } = useTranslation('sidebar');
|
|
||||||
const [isChanging, setIsChanging] = useState(false);
|
const [isChanging, setIsChanging] = useState(false);
|
||||||
const [newKey, setNewKey] = useState(apiKey);
|
const [newKey, setNewKey] = useState(apiKey);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
const handleEnterDown = (e: KeyboardEvent<HTMLDivElement>) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleUpdateKey(newKey);
|
handleUpdateKey(newKey);
|
||||||
}
|
}
|
||||||
@@ -25,25 +22,17 @@ export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => {
|
|||||||
onApiKeyChange(newKey.trim());
|
onApiKeyChange(newKey.trim());
|
||||||
setIsChanging(false);
|
setIsChanging(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isChanging) {
|
|
||||||
inputRef.current?.focus();
|
|
||||||
}
|
|
||||||
}, [isChanging]);
|
|
||||||
|
|
||||||
return isChanging ? (
|
return isChanging ? (
|
||||||
<div className="duration:200 flex w-full cursor-pointer items-center rounded-md py-3 px-3 transition-colors hover:bg-gray-500/10">
|
<div className="flex transition-colors duration:200 hover:bg-gray-500/10 py-3 px-3 rounded-md cursor-pointer w-full items-center">
|
||||||
<IconKey size={18} />
|
<IconKey size={16} />
|
||||||
|
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
className="ml-2 flex-1 h-[20px] bg-transparent border-b border-neutral-400 focus:border-neutral-100 text-left overflow-hidden overflow-ellipsis pr-1 outline-none text-white"
|
||||||
className="ml-2 h-[20px] flex-1 overflow-hidden overflow-ellipsis border-b border-neutral-400 bg-transparent pr-1 text-[12.5px] leading-3 text-left text-white outline-none focus:border-neutral-100"
|
|
||||||
type="password"
|
type="password"
|
||||||
value={newKey}
|
value={newKey}
|
||||||
onChange={(e) => setNewKey(e.target.value)}
|
onChange={(e) => setNewKey(e.target.value)}
|
||||||
onKeyDown={handleEnterDown}
|
onKeyDown={handleEnterDown}
|
||||||
placeholder={t('API Key') || 'API Key'}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex w-[40px]">
|
<div className="flex w-[40px]">
|
||||||
@@ -69,8 +58,8 @@ export const Key: FC<Props> = ({ apiKey, onApiKeyChange }) => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<SidebarButton
|
<SidebarButton
|
||||||
text={t('OpenAI API Key')}
|
text="OpenAI API Key"
|
||||||
icon={<IconKey size={18} />}
|
icon={<IconKey size={16} />}
|
||||||
onClick={() => setIsChanging(true)}
|
onClick={() => setIsChanging(true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -1,38 +1,34 @@
|
|||||||
import { IconX } from '@tabler/icons-react';
|
import { IconX } from "@tabler/icons-react";
|
||||||
import { useTranslation } from 'next-i18next';
|
import { FC } from "react";
|
||||||
import { FC } from 'react';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
placeholder: string;
|
|
||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
onSearch: (searchTerm: string) => void;
|
onSearch: (searchTerm: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Search: FC<Props> = ({ placeholder, searchTerm, onSearch }) => {
|
export const Search: FC<Props> = ({ searchTerm, onSearch }) => {
|
||||||
const { t } = useTranslation('sidebar');
|
|
||||||
|
|
||||||
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
onSearch(e.target.value);
|
onSearch(e.target.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
onSearch('');
|
onSearch("");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center">
|
<div className="relative flex items-center">
|
||||||
<input
|
<input
|
||||||
className="w-full flex-1 rounded-md border border-neutral-600 bg-[#202123] px-4 py-3 pr-10 text-[14px] leading-3 text-white"
|
className="flex-1 w-full pr-10 bg-[#202123] border border-neutral-600 text-sm rounded-md px-4 py-3 text-white"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t(placeholder) || ''}
|
placeholder="Search conversations..."
|
||||||
value={searchTerm}
|
value={searchTerm}
|
||||||
onChange={handleSearchChange}
|
onChange={handleSearchChange}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{searchTerm && (
|
{searchTerm && (
|
||||||
<IconX
|
<IconX
|
||||||
className="absolute right-4 cursor-pointer text-neutral-300 hover:text-neutral-400"
|
className="absolute right-4 text-neutral-300 cursor-pointer hover:text-neutral-400"
|
||||||
size={18}
|
size={24}
|
||||||
onClick={clearSearch}
|
onClick={clearSearch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { Conversation, KeyValuePair } from "@/types";
|
||||||
|
import { IconArrowBarLeft, IconPlus } from "@tabler/icons-react";
|
||||||
|
import { FC, useEffect, useState } from "react";
|
||||||
|
import { Conversations } from "./Conversations";
|
||||||
|
import { Search } from "./Search";
|
||||||
|
import { SidebarSettings } from "./SidebarSettings";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
loading: boolean;
|
||||||
|
conversations: Conversation[];
|
||||||
|
lightMode: "light" | "dark";
|
||||||
|
selectedConversation: Conversation;
|
||||||
|
apiKey: string;
|
||||||
|
onNewConversation: () => void;
|
||||||
|
onToggleLightMode: (mode: "light" | "dark") => void;
|
||||||
|
onSelectConversation: (conversation: Conversation) => void;
|
||||||
|
onDeleteConversation: (conversation: Conversation) => void;
|
||||||
|
onToggleSidebar: () => void;
|
||||||
|
onUpdateConversation: (conversation: Conversation, data: KeyValuePair) => void;
|
||||||
|
onApiKeyChange: (apiKey: string) => void;
|
||||||
|
onClearConversations: () => void;
|
||||||
|
onExportConversations: () => void;
|
||||||
|
onImportConversations: (conversations: Conversation[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar: FC<Props> = ({ loading, conversations, lightMode, selectedConversation, apiKey, onNewConversation, onToggleLightMode, onSelectConversation, onDeleteConversation, onToggleSidebar, onUpdateConversation, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
|
const [filteredConversations, setFilteredConversations] = useState<Conversation[]>(conversations);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchTerm) {
|
||||||
|
setFilteredConversations(conversations.filter((conversation) => conversation.name.toLowerCase().includes(searchTerm.toLowerCase())));
|
||||||
|
} else {
|
||||||
|
setFilteredConversations(conversations);
|
||||||
|
}
|
||||||
|
}, [searchTerm, conversations]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`h-full flex flex-none space-y-2 p-2 flex-col bg-[#202123] w-[260px] z-10 sm:relative sm:top-0 absolute top-12 bottom-0`}>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<button
|
||||||
|
className="flex gap-3 p-3 items-center w-full sm:w-[200px] rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer text-sm flex-shrink-0 border border-white/20"
|
||||||
|
onClick={() => {
|
||||||
|
onNewConversation();
|
||||||
|
setSearchTerm("");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPlus
|
||||||
|
className=""
|
||||||
|
size={16}
|
||||||
|
/>
|
||||||
|
New chat
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<IconArrowBarLeft
|
||||||
|
className="ml-1 p-1 text-neutral-300 cursor-pointer hover:text-neutral-400 hidden sm:flex"
|
||||||
|
size={32}
|
||||||
|
onClick={onToggleSidebar}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{conversations.length > 1 && (
|
||||||
|
<Search
|
||||||
|
searchTerm={searchTerm}
|
||||||
|
onSearch={setSearchTerm}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-grow overflow-auto">
|
||||||
|
<Conversations
|
||||||
|
loading={loading}
|
||||||
|
conversations={filteredConversations}
|
||||||
|
selectedConversation={selectedConversation}
|
||||||
|
onSelectConversation={onSelectConversation}
|
||||||
|
onDeleteConversation={(conversation) => {
|
||||||
|
onDeleteConversation(conversation);
|
||||||
|
setSearchTerm("");
|
||||||
|
}}
|
||||||
|
onRenameConversation={(conversation, name) => {
|
||||||
|
onUpdateConversation(conversation, { key: "name", value: name });
|
||||||
|
setSearchTerm("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SidebarSettings
|
||||||
|
lightMode={lightMode}
|
||||||
|
apiKey={apiKey}
|
||||||
|
onToggleLightMode={onToggleLightMode}
|
||||||
|
onApiKeyChange={onApiKeyChange}
|
||||||
|
onClearConversations={onClearConversations}
|
||||||
|
onExportConversations={onExportConversations}
|
||||||
|
onImportConversations={onImportConversations}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC } from 'react';
|
import { FC } from "react";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
text: string;
|
text: string;
|
||||||
@@ -8,12 +8,12 @@ interface Props {
|
|||||||
|
|
||||||
export const SidebarButton: FC<Props> = ({ text, icon, onClick }) => {
|
export const SidebarButton: FC<Props> = ({ text, icon, onClick }) => {
|
||||||
return (
|
return (
|
||||||
<button
|
<div
|
||||||
className="flex w-full cursor-pointer select-none items-center gap-3 rounded-md py-3 px-3 text-[14px] leading-3 text-white transition-colors duration-200 hover:bg-gray-500/10"
|
className="flex py-3 px-3 gap-3 rounded-md hover:bg-gray-500/10 transition-colors duration-200 text-white cursor-pointer w-full items-center"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
>
|
>
|
||||||
<div>{icon}</div>
|
<div>{icon}</div>
|
||||||
<span>{text}</span>
|
<div>{text}</div>
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Conversation } from "@/types";
|
||||||
|
import { IconFileExport, IconMoon, IconSun } from "@tabler/icons-react";
|
||||||
|
import { FC } from "react";
|
||||||
|
import { ClearConversations } from "./ClearConversations";
|
||||||
|
import { Import } from "./Import";
|
||||||
|
import { Key } from "./Key";
|
||||||
|
import { SidebarButton } from "./SidebarButton";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
lightMode: "light" | "dark";
|
||||||
|
apiKey: string;
|
||||||
|
onToggleLightMode: (mode: "light" | "dark") => void;
|
||||||
|
onApiKeyChange: (apiKey: string) => void;
|
||||||
|
onClearConversations: () => void;
|
||||||
|
onExportConversations: () => void;
|
||||||
|
onImportConversations: (conversations: Conversation[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SidebarSettings: FC<Props> = ({ lightMode, apiKey, onToggleLightMode, onApiKeyChange, onClearConversations, onExportConversations, onImportConversations }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col pt-1 items-center border-t border-white/20 text-sm space-y-1">
|
||||||
|
<ClearConversations onClearConversations={onClearConversations} />
|
||||||
|
|
||||||
|
<Import onImport={onImportConversations} />
|
||||||
|
|
||||||
|
<SidebarButton
|
||||||
|
text="Export conversations"
|
||||||
|
icon={<IconFileExport size={16} />}
|
||||||
|
onClick={() => onExportConversations()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SidebarButton
|
||||||
|
text={lightMode === "light" ? "Dark mode" : "Light mode"}
|
||||||
|
icon={lightMode === "light" ? <IconMoon size={16} /> : <IconSun size={16} />}
|
||||||
|
onClick={() => onToggleLightMode(lightMode === "light" ? "dark" : "light")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Key
|
||||||
|
apiKey={apiKey}
|
||||||
|
onApiKeyChange={onApiKeyChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# Google Search Tool
|
|
||||||
|
|
||||||
Use the Google Search API to search the web in Chatbot UI.
|
|
||||||
|
|
||||||
## How To Enable
|
|
||||||
|
|
||||||
1. Create a new project at https://console.developers.google.com/apis/dashboard
|
|
||||||
|
|
||||||
2. Create a new API key at https://console.developers.google.com/apis/credentials
|
|
||||||
|
|
||||||
3. Enable the Custom Search API at https://console.developers.google.com/apis/library/customsearch.googleapis.com
|
|
||||||
|
|
||||||
4. Create a new Custom Search Engine at https://cse.google.com/cse/all
|
|
||||||
|
|
||||||
5. Add your API Key and your Custom Search Engine ID to your .env.local file
|
|
||||||
|
|
||||||
6. You can now select the Google Search Tool in the search tools dropdown
|
|
||||||
|
|
||||||
## Usage Limits
|
|
||||||
|
|
||||||
Google gives you 100 free searches per day. You can increase this limit by creating a billing account.
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: chatbot-ui
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Secret
|
|
||||||
metadata:
|
|
||||||
namespace: chatbot-ui
|
|
||||||
name: chatbot-ui
|
|
||||||
type: Opaque
|
|
||||||
data:
|
|
||||||
OPENAI_API_KEY: <base64 encoded key>
|
|
||||||
---
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
namespace: chatbot-ui
|
|
||||||
name: chatbot-ui
|
|
||||||
labels:
|
|
||||||
app: chatbot-ui
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: chatbot-ui
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: chatbot-ui
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: chatbot-ui
|
|
||||||
image: <docker user>/chatbot-ui:latest
|
|
||||||
resources: {}
|
|
||||||
ports:
|
|
||||||
- containerPort: 3000
|
|
||||||
env:
|
|
||||||
- name: OPENAI_API_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: chatbot-ui
|
|
||||||
key: OPENAI_API_KEY
|
|
||||||
---
|
|
||||||
kind: Service
|
|
||||||
apiVersion: v1
|
|
||||||
metadata:
|
|
||||||
namespace: chatbot-ui
|
|
||||||
name: chatbot-ui
|
|
||||||
labels:
|
|
||||||
app: chatbot-ui
|
|
||||||
spec:
|
|
||||||
ports:
|
|
||||||
- name: http
|
|
||||||
protocol: TCP
|
|
||||||
port: 80
|
|
||||||
targetPort: 3000
|
|
||||||
selector:
|
|
||||||
app: chatbot-ui
|
|
||||||
type: ClusterIP
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
i18n: {
|
|
||||||
defaultLocale: "en",
|
|
||||||
locales: [
|
|
||||||
"bn",
|
|
||||||
"de",
|
|
||||||
"en",
|
|
||||||
"es",
|
|
||||||
"fr",
|
|
||||||
"he",
|
|
||||||
"id",
|
|
||||||
"it",
|
|
||||||
"ja",
|
|
||||||
"ko",
|
|
||||||
"pt",
|
|
||||||
"ru",
|
|
||||||
"sv",
|
|
||||||
"te",
|
|
||||||
"vi",
|
|
||||||
"zh",
|
|
||||||
"ar",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
localePath:
|
|
||||||
typeof window === 'undefined'
|
|
||||||
? require('path').resolve('./public/locales')
|
|
||||||
: '/public/locales',
|
|
||||||
};
|
|
||||||
+4
-5
@@ -1,18 +1,17 @@
|
|||||||
const { i18n } = require('./next-i18next.config');
|
|
||||||
|
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
i18n,
|
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
|
||||||
webpack(config, { isServer, dev }) {
|
webpack(config, { isServer, dev }) {
|
||||||
config.experiments = {
|
config.experiments = {
|
||||||
asyncWebAssembly: true,
|
asyncWebAssembly: true,
|
||||||
layers: true,
|
layers: true
|
||||||
};
|
};
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
|
images: {
|
||||||
|
unoptimized: true
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|||||||
Generated
+405
-5479
File diff suppressed because it is too large
Load Diff
+21
-37
@@ -4,53 +4,37 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build && next export -o dist",
|
||||||
|
"export": "next export",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"tauri": "tauri",
|
||||||
"format": "prettier --write .",
|
"lint": "next lint"
|
||||||
"test": "vitest",
|
|
||||||
"coverage": "vitest run --coverage"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dqbd/tiktoken": "^1.0.2",
|
"@dqbd/tiktoken": "^1.0.2",
|
||||||
"@tabler/icons-react": "^2.9.0",
|
"@tabler/icons-react": "^2.9.0",
|
||||||
"eventsource-parser": "^0.1.0",
|
"@tauri-apps/api": "^1.2.0",
|
||||||
"i18next": "^22.4.13",
|
"@tauri-apps/cli": "^1.2.3",
|
||||||
"next": "13.2.4",
|
|
||||||
"next-i18next": "^13.2.2",
|
|
||||||
"openai": "^3.2.1",
|
|
||||||
"react": "18.2.0",
|
|
||||||
"react-dom": "18.2.0",
|
|
||||||
"react-hot-toast": "^2.4.0",
|
|
||||||
"react-i18next": "^12.2.0",
|
|
||||||
"react-markdown": "^8.0.5",
|
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
|
||||||
"rehype-mathjax": "^4.0.2",
|
|
||||||
"remark-gfm": "^3.0.1",
|
|
||||||
"remark-math": "^5.1.1",
|
|
||||||
"uuid": "^9.0.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@mozilla/readability": "^0.4.4",
|
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
|
||||||
"@types/jsdom": "^21.1.1",
|
|
||||||
"@types/node": "18.15.0",
|
"@types/node": "18.15.0",
|
||||||
"@types/react": "18.0.28",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-dom": "18.0.11",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/react-syntax-highlighter": "^15.5.6",
|
|
||||||
"@types/uuid": "^9.0.1",
|
|
||||||
"@vitest/coverage-c8": "^0.29.7",
|
|
||||||
"autoprefixer": "^10.4.14",
|
|
||||||
"endent": "^2.1.0",
|
|
||||||
"eslint": "8.36.0",
|
"eslint": "8.36.0",
|
||||||
"eslint-config-next": "13.2.4",
|
"eslint-config-next": "13.2.4",
|
||||||
"gpt-3-encoder": "^1.1.4",
|
"eventsource-parser": "^0.1.0",
|
||||||
"jsdom": "^21.1.1",
|
"next": "13.2.4",
|
||||||
|
"openai": "^3.2.1",
|
||||||
|
"react": "18.2.0",
|
||||||
|
"react-dom": "18.2.0",
|
||||||
|
"react-markdown": "^8.0.5",
|
||||||
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
|
"remark-gfm": "^3.0.1",
|
||||||
|
"typescript": "4.9.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@types/react-syntax-highlighter": "^15.5.6",
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"prettier": "^2.8.7",
|
"tailwindcss": "^3.2.7"
|
||||||
"prettier-plugin-tailwindcss": "^0.2.5",
|
|
||||||
"tailwindcss": "^3.2.7",
|
|
||||||
"typescript": "4.9.5",
|
|
||||||
"vitest": "^0.29.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-12
@@ -1,18 +1,13 @@
|
|||||||
import '@/styles/globals.css';
|
import "@/styles/globals.css";
|
||||||
import { appWithTranslation } from 'next-i18next';
|
import type { AppProps } from "next/app";
|
||||||
import type { AppProps } from 'next/app';
|
import { Inter } from "next/font/google";
|
||||||
import { Inter } from 'next/font/google';
|
|
||||||
import { Toaster } from 'react-hot-toast';
|
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
function App({ Component, pageProps }: AppProps<{}>) {
|
export default function App({ Component, pageProps }: AppProps<{}>) {
|
||||||
return (
|
return (
|
||||||
<div className={inter.className}>
|
<main className={inter.className}>
|
||||||
<Toaster />
|
|
||||||
<Component {...pageProps} />
|
<Component {...pageProps} />
|
||||||
</div>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default appWithTranslation(App);
|
|
||||||
|
|||||||
+5
-12
@@ -1,17 +1,10 @@
|
|||||||
import { Html, Head, Main, NextScript, DocumentProps } from 'next/document';
|
import { Html, Head, Main, NextScript } from 'next/document'
|
||||||
import i18nextConfig from '../next-i18next.config';
|
|
||||||
|
|
||||||
type Props = DocumentProps & {
|
export default function Document() {
|
||||||
// add custom document props
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function Document(props: Props) {
|
|
||||||
const currentLocale =
|
|
||||||
props.__NEXT_DATA__.locale ?? i18nextConfig.i18n.defaultLocale;
|
|
||||||
return (
|
return (
|
||||||
<Html lang={currentLocale}>
|
<Html lang="en">
|
||||||
<Head>
|
<Head>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes"/>
|
||||||
<meta name="apple-mobile-web-app-title" content="Chatbot UI"></meta>
|
<meta name="apple-mobile-web-app-title" content="Chatbot UI"></meta>
|
||||||
</Head>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
@@ -19,5 +12,5 @@ export default function Document(props: Props) {
|
|||||||
<NextScript />
|
<NextScript />
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-26
@@ -1,13 +1,13 @@
|
|||||||
import { ChatBody, Message } from '@/types/chat';
|
import { ChatBody, Message, OpenAIModelID } from "@/types";
|
||||||
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
import { DEFAULT_SYSTEM_PROMPT } from "@/utils/app/const";
|
||||||
import { OpenAIError, OpenAIStream } from '@/utils/server';
|
import { OpenAIStream } from "@/utils/server";
|
||||||
import tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json';
|
import tiktokenModel from "@dqbd/tiktoken/encoders/cl100k_base.json";
|
||||||
import { Tiktoken, init } from '@dqbd/tiktoken/lite/init';
|
import { init, Tiktoken } from "@dqbd/tiktoken/lite/init";
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
import wasm from '../../node_modules/@dqbd/tiktoken/lite/tiktoken_bg.wasm?module';
|
import wasm from "../../node_modules/@dqbd/tiktoken/lite/tiktoken_bg.wasm?module";
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
runtime: 'edge',
|
runtime: "edge"
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = async (req: Request): Promise<Response> => {
|
const handler = async (req: Request): Promise<Response> => {
|
||||||
@@ -15,27 +15,17 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
const { model, messages, key, prompt } = (await req.json()) as ChatBody;
|
const { model, messages, key, prompt } = (await req.json()) as ChatBody;
|
||||||
|
|
||||||
await init((imports) => WebAssembly.instantiate(wasm, imports));
|
await init((imports) => WebAssembly.instantiate(wasm, imports));
|
||||||
const encoding = new Tiktoken(
|
const encoding = new Tiktoken(tiktokenModel.bpe_ranks, tiktokenModel.special_tokens, tiktokenModel.pat_str);
|
||||||
tiktokenModel.bpe_ranks,
|
|
||||||
tiktokenModel.special_tokens,
|
|
||||||
tiktokenModel.pat_str,
|
|
||||||
);
|
|
||||||
|
|
||||||
let promptToSend = prompt;
|
const tokenLimit = model.id === OpenAIModelID.GPT_4 ? 6000 : 3000;
|
||||||
if (!promptToSend) {
|
let tokenCount = 0;
|
||||||
promptToSend = DEFAULT_SYSTEM_PROMPT;
|
|
||||||
}
|
|
||||||
|
|
||||||
const prompt_tokens = encoding.encode(promptToSend);
|
|
||||||
|
|
||||||
let tokenCount = prompt_tokens.length;
|
|
||||||
let messagesToSend: Message[] = [];
|
let messagesToSend: Message[] = [];
|
||||||
|
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
const message = messages[i];
|
const message = messages[i];
|
||||||
const tokens = encoding.encode(message.content);
|
const tokens = encoding.encode(message.content);
|
||||||
|
|
||||||
if (tokenCount + tokens.length + 1000 > model.tokenLimit) {
|
if (tokenCount + tokens.length > tokenLimit) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
tokenCount += tokens.length;
|
tokenCount += tokens.length;
|
||||||
@@ -44,16 +34,17 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
|
|
||||||
encoding.free();
|
encoding.free();
|
||||||
|
|
||||||
|
let promptToSend = prompt;
|
||||||
|
if (!promptToSend) {
|
||||||
|
promptToSend = DEFAULT_SYSTEM_PROMPT;
|
||||||
|
}
|
||||||
|
|
||||||
const stream = await OpenAIStream(model, promptToSend, key, messagesToSend);
|
const stream = await OpenAIStream(model, promptToSend, key, messagesToSend);
|
||||||
|
|
||||||
return new Response(stream);
|
return new Response(stream);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
if (error instanceof OpenAIError) {
|
return new Response("Error", { status: 500 });
|
||||||
return new Response('Error', { status: 500, statusText: error.message });
|
|
||||||
} else {
|
|
||||||
return new Response('Error', { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,144 +0,0 @@
|
|||||||
import { Message } from '@/types/chat';
|
|
||||||
import { GoogleBody, GoogleSource } from '@/types/google';
|
|
||||||
import { OPENAI_API_HOST } from '@/utils/app/const';
|
|
||||||
import { cleanSourceText } from '@/utils/server/google';
|
|
||||||
import { Readability } from '@mozilla/readability';
|
|
||||||
import endent from 'endent';
|
|
||||||
import jsdom, { JSDOM } from 'jsdom';
|
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
|
|
||||||
const handler = async (req: NextApiRequest, res: NextApiResponse<any>) => {
|
|
||||||
try {
|
|
||||||
const { messages, key, model, googleAPIKey, googleCSEId } =
|
|
||||||
req.body as GoogleBody;
|
|
||||||
|
|
||||||
const userMessage = messages[messages.length - 1];
|
|
||||||
|
|
||||||
const googleRes = await fetch(
|
|
||||||
`https://customsearch.googleapis.com/customsearch/v1?key=${
|
|
||||||
googleAPIKey ? googleAPIKey : process.env.GOOGLE_API_KEY
|
|
||||||
}&cx=${
|
|
||||||
googleCSEId ? googleCSEId : process.env.GOOGLE_CSE_ID
|
|
||||||
}&q=${userMessage.content.trim()}&num=5`,
|
|
||||||
);
|
|
||||||
|
|
||||||
const googleData = await googleRes.json();
|
|
||||||
|
|
||||||
const sources: GoogleSource[] = googleData.items.map((item: any) => ({
|
|
||||||
title: item.title,
|
|
||||||
link: item.link,
|
|
||||||
displayLink: item.displayLink,
|
|
||||||
snippet: item.snippet,
|
|
||||||
image: item.pagemap?.cse_image?.[0]?.src,
|
|
||||||
text: '',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const sourcesWithText: any = await Promise.all(
|
|
||||||
sources.map(async (source) => {
|
|
||||||
try {
|
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
|
||||||
setTimeout(() => reject(new Error('Request timed out')), 5000),
|
|
||||||
);
|
|
||||||
|
|
||||||
const res = (await Promise.race([
|
|
||||||
fetch(source.link),
|
|
||||||
timeoutPromise,
|
|
||||||
])) as any;
|
|
||||||
|
|
||||||
// if (res) {
|
|
||||||
const html = await res.text();
|
|
||||||
|
|
||||||
const virtualConsole = new jsdom.VirtualConsole();
|
|
||||||
virtualConsole.on('error', (error) => {
|
|
||||||
if (!error.message.includes('Could not parse CSS stylesheet')) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const dom = new JSDOM(html, { virtualConsole });
|
|
||||||
const doc = dom.window.document;
|
|
||||||
const parsed = new Readability(doc).parse();
|
|
||||||
|
|
||||||
if (parsed) {
|
|
||||||
let sourceText = cleanSourceText(parsed.textContent);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...source,
|
|
||||||
// TODO: switch to tokens
|
|
||||||
text: sourceText.slice(0, 2000),
|
|
||||||
} as GoogleSource;
|
|
||||||
}
|
|
||||||
// }
|
|
||||||
|
|
||||||
return null;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredSources: GoogleSource[] = sourcesWithText.filter(Boolean);
|
|
||||||
|
|
||||||
const answerPrompt = endent`
|
|
||||||
Provide me with the information I requested. Use the sources to provide an accurate response. Respond in markdown format. Cite the sources you used as a markdown link as you use them at the end of each sentence by number of the source (ex: [[1]](link.com)). Provide an accurate response and then stop. Today's date is ${new Date().toLocaleDateString()}.
|
|
||||||
|
|
||||||
Example Input:
|
|
||||||
What's the weather in San Francisco today?
|
|
||||||
|
|
||||||
Example Sources:
|
|
||||||
[Weather in San Francisco](https://www.google.com/search?q=weather+san+francisco)
|
|
||||||
|
|
||||||
Example Response:
|
|
||||||
It's 70 degrees and sunny in San Francisco today. [[1]](https://www.google.com/search?q=weather+san+francisco)
|
|
||||||
|
|
||||||
Input:
|
|
||||||
${userMessage.content.trim()}
|
|
||||||
|
|
||||||
Sources:
|
|
||||||
${filteredSources.map((source) => {
|
|
||||||
return endent`
|
|
||||||
${source.title} (${source.link}):
|
|
||||||
${source.text}
|
|
||||||
`;
|
|
||||||
})}
|
|
||||||
|
|
||||||
Response:
|
|
||||||
`;
|
|
||||||
|
|
||||||
const answerMessage: Message = { role: 'user', content: answerPrompt };
|
|
||||||
|
|
||||||
const answerRes = await fetch(`${OPENAI_API_HOST}/v1/chat/completions`, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`,
|
|
||||||
...(process.env.OPENAI_ORGANIZATION && {
|
|
||||||
'OpenAI-Organization': process.env.OPENAI_ORGANIZATION,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: model.id,
|
|
||||||
messages: [
|
|
||||||
{
|
|
||||||
role: 'system',
|
|
||||||
content: `Use the sources to provide an accurate response. Respond in markdown format. Cite the sources you used as [1](link), etc, as you use them.`,
|
|
||||||
},
|
|
||||||
answerMessage,
|
|
||||||
],
|
|
||||||
max_tokens: 1000,
|
|
||||||
temperature: 1,
|
|
||||||
stream: false,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const { choices: choices2 } = await answerRes.json();
|
|
||||||
const answer = choices2[0].message.content;
|
|
||||||
|
|
||||||
res.status(200).json({ answer });
|
|
||||||
} catch (error) {
|
|
||||||
return new Response('Error', { status: 500 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default handler;
|
|
||||||
+15
-24
@@ -1,8 +1,7 @@
|
|||||||
import { OpenAIModel, OpenAIModelID, OpenAIModels } from '@/types/openai';
|
import { OpenAIModel, OpenAIModelID, OpenAIModels } from "@/types";
|
||||||
import { OPENAI_API_HOST } from '@/utils/app/const';
|
|
||||||
|
|
||||||
export const config = {
|
export const config = {
|
||||||
runtime: 'edge',
|
runtime: "edge"
|
||||||
};
|
};
|
||||||
|
|
||||||
const handler = async (req: Request): Promise<Response> => {
|
const handler = async (req: Request): Promise<Response> => {
|
||||||
@@ -10,50 +9,42 @@ const handler = async (req: Request): Promise<Response> => {
|
|||||||
const { key } = (await req.json()) as {
|
const { key } = (await req.json()) as {
|
||||||
key: string;
|
key: string;
|
||||||
};
|
};
|
||||||
|
console.log("key", key);
|
||||||
|
|
||||||
const response = await fetch(`${OPENAI_API_HOST}/v1/models`, {
|
const response = await fetch("https://api.openai.com/v1/models", {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`,
|
Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`
|
||||||
...(process.env.OPENAI_ORGANIZATION && {
|
}
|
||||||
'OpenAI-Organization': process.env.OPENAI_ORGANIZATION,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status === 401) {
|
if (response.status !== 200) {
|
||||||
return new Response(response.body, {
|
throw new Error("OpenAI API returned an error");
|
||||||
status: 500,
|
|
||||||
headers: response.headers,
|
|
||||||
});
|
|
||||||
} else if (response.status !== 200) {
|
|
||||||
console.error(
|
|
||||||
`OpenAI API returned an error ${
|
|
||||||
response.status
|
|
||||||
}: ${await response.text()}`,
|
|
||||||
);
|
|
||||||
throw new Error('OpenAI API returned an error');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const json = await response.json();
|
const json = await response.json();
|
||||||
|
|
||||||
|
console.log("json", json);
|
||||||
|
|
||||||
const models: OpenAIModel[] = json.data
|
const models: OpenAIModel[] = json.data
|
||||||
.map((model: any) => {
|
.map((model: any) => {
|
||||||
for (const [key, value] of Object.entries(OpenAIModelID)) {
|
for (const [key, value] of Object.entries(OpenAIModelID)) {
|
||||||
if (value === model.id) {
|
if (value === model.id) {
|
||||||
return {
|
return {
|
||||||
id: model.id,
|
id: model.id,
|
||||||
name: OpenAIModels[value].name,
|
name: OpenAIModels[value].name
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
console.log("models", models);
|
||||||
|
|
||||||
return new Response(JSON.stringify(models), { status: 200 });
|
return new Response(JSON.stringify(models), { status: 200 });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return new Response('Error', { status: 500 });
|
return new Response("Error", { status: 500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+190
-680
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -3,4 +3,4 @@ module.exports = {
|
|||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
trailingComma: 'all',
|
|
||||||
singleQuote: true,
|
|
||||||
plugins: [require('prettier-plugin-tailwindcss')]
|
|
||||||
};
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{
|
|
||||||
"OpenAI API Key Required": " (أوبن أيه أي) OpenAI API Key (مطلوب (مفتاح واجهة برمجة تطبيقات ",
|
|
||||||
"Please set your OpenAI API key in the bottom left of the sidebar.": "يرجى تعيين مفتاح واجهة برمجة تطبيقات أوبن أيه أي الخاص بك في الجزء السفلي الأيسر من الشريط الجانبي",
|
|
||||||
"Stop Generating": "إيقاف التوليد",
|
|
||||||
"Prompt limit is {{maxLength}} characters": "حرفًا {{maxLength}} حد المطالبة هو",
|
|
||||||
"System Prompt": "مطالبة النظام",
|
|
||||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "أنت شات جبت نموذج لغة كبير تم تدريبه بواسطة أوبن أيه أي. اتبع تعليمات المستخدم بعناية. الرد باستخدام الماركداون",
|
|
||||||
"Enter a prompt": "أدخل المطالبة",
|
|
||||||
"Regenerate response": "إعادة توليد الرد",
|
|
||||||
"Sorry, there was an error.": "عذرًا، حدث خطأ",
|
|
||||||
"Model": "النموذج",
|
|
||||||
"Conversation": "المحادثة",
|
|
||||||
"OR": "أو",
|
|
||||||
"Loading...": "...جاري التحميل",
|
|
||||||
"Type a message...": "...اكتب رسالة",
|
|
||||||
"Error fetching models.": "خطأ في جلب النماذج",
|
|
||||||
"AI": "الذكاء الاصطناعي",
|
|
||||||
"You": "أنت",
|
|
||||||
"Cancel": "Cancel",
|
|
||||||
"Save & Submit": "Save & Submit",
|
|
||||||
"Make sure your OpenAI API key is set in the bottom left of the sidebar.": "تأكد من تعيين مفتاح واجهة برمجة تطبيقات الخاص بك في الجزء السفلي الأيسر من الشريط",
|
|
||||||
"If you completed this step, OpenAI may be experiencing issues.": "من مشاكل OpenAI إذا اكتملت هذه الخطوة، فقد يعاني",
|
|
||||||
|
|
||||||
"click if using a .env.local file": ".env.local انقر إذا كنت تستخدم ملف",
|
|
||||||
|
|
||||||
|
|
||||||
"Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "حرفًا {{maxLength}} حد الرسالة هو {{valueLength}} لقد أدخلت ",
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"Please enter a message": "يرجى إدخال رسالة",
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI هي مجموعة متقدمة للدردشة تستخدم",
|
|
||||||
"Are you sure you want to clear all messages?": "هل أنت متأكد أنك تريد مسح كافة الرسائل؟"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"Copy code": "نسخ الكود",
|
|
||||||
"Copied!": "تم النسخ!",
|
|
||||||
"Enter file name": "أدخل اسم الملف"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"New prompt": "مطلب جديد",
|
|
||||||
"New folder": "مجلد جديد",
|
|
||||||
"No prompts.": "لا يوجد مطالبات.",
|
|
||||||
"Search prompts...": "...البحث عن مطالبات",
|
|
||||||
"Name": "الاسم",
|
|
||||||
"Description": "الوصف",
|
|
||||||
"A description for your prompt.": "وصف لمطلبك",
|
|
||||||
"Prompt": "مطلب",
|
|
||||||
"Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "محتوى المطلب. استخدم {{}} للإشارة إلى متغير. مثال: {{الاسم}} هي {{صفة}} {{اسم}}",
|
|
||||||
"Save": "حفظ"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"New folder": "مجلد جديد",
|
|
||||||
"New chat": "محادثة جديدة",
|
|
||||||
"No conversations.": "لا يوجد محادثات",
|
|
||||||
"Search conversations...": "...البحث عن المحادثات",
|
|
||||||
"OpenAI API Key": " (أوبن أيه أي) OpenAI API Key (مفتاح واجهة برمجة تطبيقات)",
|
|
||||||
"Import data": "استيراد المحادثات",
|
|
||||||
"Are you sure?": "هل أنت متأكد؟",
|
|
||||||
"Clear conversations": "مسح المحادثات",
|
|
||||||
"Export data": "تصدير المحادثات",
|
|
||||||
"Dark mode": "الوضع الداكن",
|
|
||||||
"Light mode": "الوضع الفاتح"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"OpenAI API Key Required": "OpenAI API key বাধ্যতামূলক",
|
|
||||||
"Please set your OpenAI API key in the bottom left of the sidebar.": "দয়া করে আপনার OpenAI API key বামে সাইডবারের নিচের দিকে সেট করুন।",
|
|
||||||
"Stop Generating": "বার্তা জেনারেট করা বন্ধ করুন",
|
|
||||||
"Prompt limit is {{maxLength}} characters": "নির্দেশনা (বার্তা) সীমা সর্বোচ্চ {{maxLength}} অক্ষর",
|
|
||||||
"System Prompt": "সিস্টেম নির্দেশনা (বার্তা)",
|
|
||||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "তুমি ChatGPT, OpenAI দ্বারা প্রশিক্ষিত একটি বড় ভাষা মডেল। সাবধানে ব্যবহারকারীর নির্দেশাবলী অনুসরণ করুন. মার্কডাউন ব্যবহার করে উত্তর দিন।",
|
|
||||||
"Enter a prompt": "একটি নির্দেশনা (বার্তা) দিন",
|
|
||||||
"Regenerate response": "বার্তা আবার জেনারেট করুন",
|
|
||||||
"Sorry, there was an error.": "দুঃখিত, কোনো একটি সমস্যা হয়েছে।",
|
|
||||||
"Model": "মডেল",
|
|
||||||
"Conversation": "আলাপচারিতা",
|
|
||||||
"OR": "অথবা",
|
|
||||||
"Loading...": "লোড হচ্ছে...",
|
|
||||||
"Type a message...": "কোনো মেসেজ লিখুন...",
|
|
||||||
"Error fetching models.": "মডেল পেতে সমস্যা হচ্ছে।",
|
|
||||||
"AI": "AI",
|
|
||||||
"You": "তুমি",
|
|
||||||
"Cancel": "Cancel",
|
|
||||||
"Save & Submit": "Save & Submit",
|
|
||||||
"Make sure your OpenAI API key is set in the bottom left of the sidebar.": "নিশ্চিত করুন যে আপনার OpenAI API key সাইডবারের নীচে বাম দিকে সেট করা আছে।",
|
|
||||||
"If you completed this step, OpenAI may be experiencing issues.": "আপনি এই ধাপটি সম্পন্ন করে থাকলে, হতে পারে যে OpenAI কোনো সমস্যার সম্মুখীন হয়েছে।",
|
|
||||||
"click if using a .env.local file": "click if using a .env.local file",
|
|
||||||
"Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "বার্তার সর্বোচ্চ সীমা হল {{maxLength}} অক্ষর৷ আপনি {{valueLength}} অক্ষর লিখেছেন।",
|
|
||||||
"Please enter a message": "দয়া করে একটি মেসেজ লিখুন",
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI হল OpenAI-এর চ্যাট মডেলগুলির জন্য একটি উন্নত চ্যাটবট কিট যার লক্ষ্য হল ChatGPT-এর ইন্টারফেস এবং কার্যকারিতা অনুকরণ করা।",
|
|
||||||
"Are you sure you want to clear all messages?": "সমস্ত বার্তা মুছে ফেলতে আপনি কি নিশ্চিত?"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"Copy code": "কোড কপি করুন",
|
|
||||||
"Copied!": "কপি করা হয়েছে!",
|
|
||||||
"Enter file name": "ফাইল নাম লিখুন"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"New prompt": "New prompt",
|
|
||||||
"New folder": "New folder",
|
|
||||||
"No prompts.": "No prompts.",
|
|
||||||
"Search prompts...": "Search prompts...",
|
|
||||||
"Name": "Name",
|
|
||||||
"Description": "Description",
|
|
||||||
"A description for your prompt.": "A description for your prompt.",
|
|
||||||
"Prompt": "Prompt",
|
|
||||||
"Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}",
|
|
||||||
"Save": "Save"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"New folder": "নতুন ফোল্ডার",
|
|
||||||
"New chat": "নতুন আড্ডা",
|
|
||||||
"No conversations.": "কোনো আলাপচারিতা নেই।",
|
|
||||||
"Search conversations...": "আলাপচারিতা খুঁজুন...",
|
|
||||||
"OpenAI API Key": "OpenAI API Key",
|
|
||||||
"Import data": "আলাপচারিতা ইমপোর্ট",
|
|
||||||
"Are you sure?": "আপনি কি নিশ্চিত?",
|
|
||||||
"Clear conversations": "আলাপচারিতা ক্লিয়ার",
|
|
||||||
"Export data": "আলাপচারিতা এক্সপোর্ট",
|
|
||||||
"Dark mode": "ডার্ক মোড",
|
|
||||||
"Light mode": "লাইট মোড"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"OpenAI API Key Required": "OpenAI API-Schlüssel erforderlich",
|
|
||||||
"Please set your OpenAI API key in the bottom left of the sidebar.": "Bitte trage deinen OpenAI API-Schlüssel in der linken unteren Ecke der Seitenleiste ein.",
|
|
||||||
"Stop Generating": "Generieren stoppen",
|
|
||||||
"Prompt limit is {{maxLength}} characters": "Das Eingabelimit liegt bei {{maxLength}} Zeichen",
|
|
||||||
"System Prompt": "Systemaufforderung",
|
|
||||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "Du bist ChatGPT, ein großes Sprachmodell, das von OpenAI trainiert wurde. Befolge die Anweisungen des Benutzers sorgfältig. Antworte in Markdown-Format.",
|
|
||||||
"Enter a prompt": "Gib eine Anweisung ein",
|
|
||||||
"Regenerate response": "Antwort erneut generieren",
|
|
||||||
"Sorry, there was an error.": "Entschuldigung, es ist ein Fehler aufgetreten.",
|
|
||||||
"Model": "Modell",
|
|
||||||
"Conversation": "Konversation",
|
|
||||||
"OR": "ODER",
|
|
||||||
"Loading...": "Laden...",
|
|
||||||
"Type a message...": "Schreibe eine Nachricht...",
|
|
||||||
"Error fetching models.": "Fehler beim Abrufen der Sprachmodelle.",
|
|
||||||
"AI": "KI",
|
|
||||||
"You": "Du",
|
|
||||||
"Cancel": "Cancel",
|
|
||||||
"Save & Submit": "Save & Submit",
|
|
||||||
"Make sure your OpenAI API key is set in the bottom left of the sidebar.": "Stelle sicher, dass dein OpenAI API-Schlüssel in der unteren linken Ecke der Seitenleiste eingetragen ist.",
|
|
||||||
"If you completed this step, OpenAI may be experiencing issues.": "Wenn dies der Fall ist, könnte OpenAI möglicherweise momentan Probleme haben.",
|
|
||||||
"click if using a .env.local file": "click if using a .env.local file",
|
|
||||||
"Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "Das Nachrichtenlimit beträgt {{maxLength}} Zeichen. Du hast bereits {{valueLength}} Zeichen eingegeben.",
|
|
||||||
"Please enter a message": "Bitte gib eine Nachricht ein",
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI ist ein fortschrittliches Chatbot-Toolkit für OpenAI's Chat-Modelle, das darauf abzielt, die Benutzeroberfläche und Funktionalität von ChatGPT nachzuahmen.",
|
|
||||||
"Are you sure you want to clear all messages?": "Bist du sicher, dass du alle Nachrichten löschen möchtest?"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"Copy code": "Code kopieren",
|
|
||||||
"Copied!": "Kopiert!",
|
|
||||||
"Enter file name": "Dateinamen eingeben"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"New prompt": "New prompt",
|
|
||||||
"New folder": "New folder",
|
|
||||||
"No prompts.": "No prompts.",
|
|
||||||
"Search prompts...": "Search prompts...",
|
|
||||||
"Name": "Name",
|
|
||||||
"Description": "Description",
|
|
||||||
"A description for your prompt.": "A description for your prompt.",
|
|
||||||
"Prompt": "Prompt",
|
|
||||||
"Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}",
|
|
||||||
"Save": "Save"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"New folder": "Neuer Ordner",
|
|
||||||
"New chat": "Neue Konversation",
|
|
||||||
"No conversations.": "Keine Konversationen.",
|
|
||||||
"Search conversations...": "Konversationen suchen...",
|
|
||||||
"OpenAI API Key": "OpenAI API-Schlüssel",
|
|
||||||
"Import data": "Konversationen importieren",
|
|
||||||
"Are you sure?": "Bist du sicher?",
|
|
||||||
"Clear conversations": "Konversationen löschen",
|
|
||||||
"Export data": "Konversationen exportieren",
|
|
||||||
"Dark mode": "Dark Mode",
|
|
||||||
"Light mode": "Light Mode"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"OpenAI API Key Required": "Se requiere la clave de API de OpenAI",
|
|
||||||
"Please set your OpenAI API key in the bottom left of the sidebar.": "Por favor, ingrese su clave de API de OpenAI en la esquina inferior izquierda de la barra lateral.",
|
|
||||||
"Stop Generating": "Dejar de generar",
|
|
||||||
"Prompt limit is {{maxLength}} characters": "El límite del mensaje es de {{maxLength}} caracteres",
|
|
||||||
"System Prompt": "Mensaje del sistema",
|
|
||||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "Eres ChatGPT, un modelo de lenguaje grande entrenado por OpenAI. Sigue las instrucciones del usuario cuidadosamente. Responde usando markdown.",
|
|
||||||
"Enter a prompt": "Ingrese un mensaje",
|
|
||||||
"Regenerate response": "Regenerar respuesta",
|
|
||||||
"Sorry, there was an error.": "Lo sentimos, ha ocurrido un error.",
|
|
||||||
"Model": "Modelo",
|
|
||||||
"Conversation": "Conversación",
|
|
||||||
"OR": "O",
|
|
||||||
"Loading...": "Cargando...",
|
|
||||||
"Type a message...": "Escriba un mensaje...",
|
|
||||||
"Error fetching models.": "Error al obtener los modelos.",
|
|
||||||
"AI": "IA",
|
|
||||||
"You": "Tú",
|
|
||||||
"Cancel": "Cancel",
|
|
||||||
"Save & Submit": "Save & Submit",
|
|
||||||
"Make sure your OpenAI API key is set in the bottom left of the sidebar.": "Asegúrate de que hayas ingresado la clave de API de OpenAI en la esquina inferior izquierda de la barra lateral.",
|
|
||||||
"If you completed this step, OpenAI may be experiencing issues.": "Si completaste este paso, OpenAI podría estar experimentando problemas.",
|
|
||||||
"click if using a .env.local file": "haz clic si estás utilizando un archivo .env.local",
|
|
||||||
"Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "El límite del mensaje es de {{maxLength}} caracteres. Has ingresado {{valueLength}} caracteres.",
|
|
||||||
"Please enter a message": "Por favor, ingrese un mensaje",
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI es un kit avanzado de chatbot para los modelos de chat de OpenAI que busca imitar la interfaz y funcionalidad de ChatGPT.",
|
|
||||||
"Are you sure you want to clear all messages?": "¿Está seguro de que desea borrar todos los mensajes?"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"Copy code": "Copiar código",
|
|
||||||
"Copied!": "¡Copiado!",
|
|
||||||
"Enter file name": "Ingrese el nombre del archivo"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"New prompt": "Nuevo prompt",
|
|
||||||
"New folder": "Nueva carpeta",
|
|
||||||
"No prompts.": "No hay prompts.",
|
|
||||||
"Search prompts...": "Buscar prompts...",
|
|
||||||
"Name": "Nombre",
|
|
||||||
"Description": "Descripción",
|
|
||||||
"A description for your prompt.": "Descripción de su prompt.",
|
|
||||||
"Prompt": "Prompt",
|
|
||||||
"Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Contenido del prompt. Utilice {{}} para indicar una variable. Ej: {{nombre}} es un {{adjetivo}} {{nombre}}",
|
|
||||||
"Save": "Guardar"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"New folder": "Nueva carpeta",
|
|
||||||
"New chat": "Nueva conversación",
|
|
||||||
"No conversations.": "No hay conversaciones.",
|
|
||||||
"Search conversations...": "Buscar conversaciones...",
|
|
||||||
"OpenAI API Key": "Llave de API de OpenAI",
|
|
||||||
"Import data": "Importar conversaciones",
|
|
||||||
"Are you sure?": "¿Estás seguro?",
|
|
||||||
"Clear conversations": "Borrar conversaciones",
|
|
||||||
"Export data": "Exportar conversaciones",
|
|
||||||
"Dark mode": "Modo oscuro",
|
|
||||||
"Light mode": "Modo claro"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"OpenAI API Key Required": "Clé API OpenAI requise",
|
|
||||||
"Please set your OpenAI API key in the bottom left of the sidebar.": "Veuillez saisir votre clé API OpenAI dans le coin inférieur gauche de la barre latérale.",
|
|
||||||
"Stop Generating": "Interrompre la génération",
|
|
||||||
"Prompt limit is {{maxLength}} characters": "La limite du prompt est de {{maxLength}} caractères",
|
|
||||||
"System Prompt": "Prompt du système",
|
|
||||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "Vous êtes ChatGPT, un grand modèle linguistique entraîné par OpenAI. Suivez attentivement les instructions de l'utilisateur. Répondez en utilisant Markdown.",
|
|
||||||
"Enter a prompt": "Entrez un prompt",
|
|
||||||
"Regenerate response": "Régénérer la réponse",
|
|
||||||
"Sorry, there was an error.": "Désolé, une erreur est survenue.",
|
|
||||||
"Model": "Modèle",
|
|
||||||
"Conversation": "Conversation",
|
|
||||||
"OR": "OU",
|
|
||||||
"Loading...": "Chargement...",
|
|
||||||
"Type a message...": "Tapez un message...",
|
|
||||||
"Error fetching models.": "Erreur lors de la récupération des modèles.",
|
|
||||||
"AI": "IA",
|
|
||||||
"You": "Vous",
|
|
||||||
"Cancel": "Cancel",
|
|
||||||
"Save & Submit": "Save & Submit",
|
|
||||||
"Make sure your OpenAI API key is set in the bottom left of the sidebar.": "Assurez-vous que votre clé API OpenAI est définie dans le coin inférieur gauche de la barre latérale.",
|
|
||||||
"If you completed this step, OpenAI may be experiencing issues.": "Si vous avez effectué cette étape, OpenAI peut rencontrer des problèmes.",
|
|
||||||
"click if using a .env.local file": "click if using a .env.local file",
|
|
||||||
"Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "La limite de message est de {{maxLength}} caractères. Vous avez saisi {{valueLength}} caractères.",
|
|
||||||
"Please enter a message": "Veuillez entrer un message",
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI est un kit de chatbot avancé pour les modèles de chat d'OpenAI visant à imiter l'interface et les fonctionnalités de ChatGPT.",
|
|
||||||
"Are you sure you want to clear all messages?": "Êtes-vous sûr de vouloir effacer tous les messages ?"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"Copy code": "Copier le code",
|
|
||||||
"Copied!": "Copié !",
|
|
||||||
"Enter file name": "Entrez le nom du fichier"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"New prompt": "New prompt",
|
|
||||||
"New folder": "New folder",
|
|
||||||
"No prompts.": "No prompts.",
|
|
||||||
"Search prompts...": "Search prompts...",
|
|
||||||
"Name": "Name",
|
|
||||||
"Description": "Description",
|
|
||||||
"A description for your prompt.": "A description for your prompt.",
|
|
||||||
"Prompt": "Prompt",
|
|
||||||
"Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}",
|
|
||||||
"Save": "Save"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"New folder": "Nouveau dossier",
|
|
||||||
"New chat": "Nouvelle discussion",
|
|
||||||
"No conversations.": "Aucune conversation.",
|
|
||||||
"Search conversations...": "Rechercher des conversations...",
|
|
||||||
"OpenAI API Key": "Clé API OpenAI",
|
|
||||||
"Import data": "Importer des conversations",
|
|
||||||
"Are you sure?": "Êtes-vous sûr ?",
|
|
||||||
"Clear conversations": "Effacer les conversations",
|
|
||||||
"Export data": "Exporter les conversations",
|
|
||||||
"Dark mode": "Mode sombre",
|
|
||||||
"Light mode": "Mode clair"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"OpenAI API Key Required": "מפתח openAI API",
|
|
||||||
"Please set your OpenAI API key in the bottom left of the sidebar.": "עליך להזין את המפתח האישי שלך בצידו השמאלי התחתון של תפריט הניווט.",
|
|
||||||
"Stop Generating": "עצור תהליך הפקת התשובה",
|
|
||||||
"Prompt limit is {{maxLength}} characters": "אורך התשובה מוגבל ל {{maxLength}} תווים",
|
|
||||||
"System Prompt": "הגדרת בסיס לכל תשובה של המערכת",
|
|
||||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "You are Hebrew speaking ChatGPT, a large language model trained by OpenAI which responds in Hebrew to any question or User comment. Follow the user's instructions carefully. Respond in Hebrew using markdown.",
|
|
||||||
"Enter a prompt": "הקלד הודעה",
|
|
||||||
"Regenerate response": "הפק תשובה מחדש",
|
|
||||||
"Sorry, there was an error.": "התנצלותנו הכנה, המערכת מדווחת על תקלה",
|
|
||||||
"Model": "מודל",
|
|
||||||
"Conversation": "שיחה",
|
|
||||||
"OR": "או",
|
|
||||||
"Loading...": "טוען...",
|
|
||||||
"Type a message...": "הקלד הודעתך...",
|
|
||||||
"Error fetching models.": "תקלה באיחזור רשימת המודלים",
|
|
||||||
"AI": "המערכת",
|
|
||||||
"You": "אתה",
|
|
||||||
"Cancel": "Cancel",
|
|
||||||
"Save & Submit": "Save & Submit",
|
|
||||||
"Make sure your OpenAI API key is set in the bottom left of the sidebar.": "עליך לוודא שמפתח האישי שלך מוזן בתפריט מצד שמאל",
|
|
||||||
"If you completed this step, OpenAI may be experiencing issues.": "אם טרם השלמת חלק זה יש סבירות גבוהה להתרחשות תקלה",
|
|
||||||
"click if using a .env.local file": "click if using a .env.local file",
|
|
||||||
"Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "מגבלת תווים היא {{maxLength}}. אתה הקלדת עד עכשיו {{valueLength}} תווים.",
|
|
||||||
"Please enter a message": "הקלד את הודעתך",
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "מערכת הצאטבוט היא ערכה מתקדמת לניהול שיחה המכוונת לחקות את המראה והפונקציונאלית של ChatGPT",
|
|
||||||
"Are you sure you want to clear all messages?": "האם אתה בטוח שברצונך למחוק את כל ההודעות?"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"Copy code": "העתק קוד",
|
|
||||||
"Copied!": "נשמר בזכרון",
|
|
||||||
"Enter file name": "הקלד שם לקובץ"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"New prompt": "פקודת מכונה חדשה",
|
|
||||||
"New folder": "תיקיה חדשה",
|
|
||||||
"No prompts.": "לא נמצאו פקודות מכונות",
|
|
||||||
"Search prompts...": "חיפוש פקודות...",
|
|
||||||
"Name": "שם",
|
|
||||||
"Description": "תיאור",
|
|
||||||
"A description for your prompt.": "תיאור שורת הפקודה למכונה",
|
|
||||||
"Prompt": "פקודה",
|
|
||||||
"Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "תיאור הפקודה. השתמש {{}} להגדרת משתנים. לדוגמא {{שם משתנה}} הוא {{תואר}} {{שם עצם}}",
|
|
||||||
"Save": "Save"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"New folder": "תיקיה חדשה",
|
|
||||||
"New chat": "שיחה חדשה",
|
|
||||||
"No conversations.": "אין שיחות חדשות",
|
|
||||||
"Search conversations...": "חיפוש שיחות...",
|
|
||||||
"OpenAI API Key": "מפתח אישי ל openAI",
|
|
||||||
"Import data": "ייבוא שיחות",
|
|
||||||
"Are you sure?": "אתה בטוח?",
|
|
||||||
"Clear conversations": "ניקוי שיחות",
|
|
||||||
"Export data": "ייצוא שיחות",
|
|
||||||
"Dark mode": "מצב כהה",
|
|
||||||
"Light mode": "מצב בהיר"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"OpenAI API Key Required": "Memerlukan Kunci API OpenAI",
|
|
||||||
"Please set your OpenAI API key in the bottom left of the sidebar.": "Silakan atur kunci API OpenAI Anda di bagian kiri bawah bilah sisi.",
|
|
||||||
"Stop Generating": "Berhenti Menghasilkan",
|
|
||||||
"Prompt limit is {{maxLength}} characters": "Batas karakter untuk prompt adalah {{maxLength}} karakter",
|
|
||||||
"System Prompt": "Prompt Sistem",
|
|
||||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "Anda adalah ChatGPT, model bahasa besar yang dilatih oleh OpenAI. Ikuti instruksi pengguna dengan hati-hati. Balas menggunakan markdown.",
|
|
||||||
"Enter a prompt": "Masukkan sebuah prompt",
|
|
||||||
"Regenerate response": "Hasilkan kembali respons",
|
|
||||||
"Sorry, there was an error.": "Maaf, terjadi kesalahan.",
|
|
||||||
"Model": "Model",
|
|
||||||
"Conversation": "Percakapan",
|
|
||||||
"OR": "ATAU",
|
|
||||||
"Loading...": "Memuat...",
|
|
||||||
"Type a message...": "Ketik sebuah pesan...",
|
|
||||||
"Error fetching models.": "Kesalahan dalam mengambil model.",
|
|
||||||
"AI": "AI",
|
|
||||||
"You": "Anda",
|
|
||||||
"Cancel": "Cancel",
|
|
||||||
"Save & Submit": "Save & Submit",
|
|
||||||
"Make sure your OpenAI API key is set in the bottom left of the sidebar.": "Pastikan kunci API OpenAI Anda diatur di bagian kiri bawah bilah sisi.",
|
|
||||||
"If you completed this step, OpenAI may be experiencing issues.": "Jika Anda telah menyelesaikan langkah ini, OpenAI mungkin mengalami masalah.",
|
|
||||||
"click if using a .env.local file": "klik jika menggunakan file .env.local",
|
|
||||||
"Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "Batas karakter untuk pesan adalah {{maxLength}} karakter. Anda telah memasukkan {{valueLength}} karakter.",
|
|
||||||
"Please enter a message": "Silakan masukkan sebuah pesan",
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI adalah kit chatbot canggih untuk model obrolan OpenAI yang bertujuan meniru antarmuka dan fungsionalitas ChatGPT.",
|
|
||||||
"Are you sure you want to clear all messages?": "Apakah Anda yakin ingin menghapus semua pesan?"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"Copy code": "Salin kode",
|
|
||||||
"Copied!": "Kode disalin!",
|
|
||||||
"Enter file name": "Masukkan nama file"
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
{
|
|
||||||
"New prompt": "New prompt",
|
|
||||||
"New folder": "New folder",
|
|
||||||
"No prompts.": "No prompts.",
|
|
||||||
"Search prompts...": "Search prompts...",
|
|
||||||
"Name": "Name",
|
|
||||||
"Description": "Description",
|
|
||||||
"A description for your prompt.": "A description for your prompt.",
|
|
||||||
"Prompt": "Prompt",
|
|
||||||
"Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}": "Prompt content. Use {{}} to denote a variable. Ex: {{name}} is a {{adjective}} {{noun}}",
|
|
||||||
"Save": "Save"
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"New folder": "Folder baru",
|
|
||||||
"New chat": "Percakapan baru",
|
|
||||||
"No conversations.": "Tidak ada percakapan.",
|
|
||||||
"Search conversations...": "Cari percakapan...",
|
|
||||||
"OpenAI API Key": "Kunci API OpenAI",
|
|
||||||
"Import data": "Impor percakapan",
|
|
||||||
"Are you sure?": "Apakah Anda yakin?",
|
|
||||||
"Clear conversations": "Hapus percakapan",
|
|
||||||
"Export data": "Ekspor percakapan",
|
|
||||||
"Dark mode": "Mode gelap",
|
|
||||||
"Light mode": "Mode terang"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
{
|
|
||||||
"OpenAI API Key Required": "E' richiesta la chiave API OpenAI",
|
|
||||||
"Please set your OpenAI API key in the bottom left of the sidebar.": "Per favore, inserisci la tua Chiave API OpenAI in basso a sinistra nella barra laterale",
|
|
||||||
"Stop Generating": "Interrompi la generazione",
|
|
||||||
"Prompt limit is {{maxLength}} characters": "Il limite del messaggio è di {{maxLength}} caratteri",
|
|
||||||
"System Prompt": "Prompt del sistema",
|
|
||||||
"You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.": "Sei ChatGPT, un grande modello di linguaggio addestrato da OpenAI. Segui attentamente le istruzioni dell'utente. Rispondi usando il markdown.",
|
|
||||||
"Enter a prompt": "Inserisci un prompt",
|
|
||||||
"Regenerate response": "Rigenera risposta",
|
|
||||||
"Sorry, there was an error.": "Scusa, si è verificato un errore.",
|
|
||||||
"Model": "Modello",
|
|
||||||
"Conversation": "Conversazione",
|
|
||||||
"OR": "O",
|
|
||||||
"Loading...": "Caricamento...",
|
|
||||||
"Type a message...": "Digita un messaggio...",
|
|
||||||
"Error fetching models.": "Si è verificato un errore nel recupero dei modelli.",
|
|
||||||
"AI": "IA",
|
|
||||||
"You": "Tu",
|
|
||||||
"Cancel": "Annulla",
|
|
||||||
"Save & Submit": "Salva e invia",
|
|
||||||
"Make sure your OpenAI API key is set in the bottom left of the sidebar.": "Assicurati che la tua chiave API OpenAI sia inserita in basso a sinistra nella barra laterale",
|
|
||||||
"If you completed this step, OpenAI may be experiencing issues.": "Se hai completato questo passaggio, OpenAI potrebbe avere problemi.",
|
|
||||||
"click if using a .env.local file": "Fai click se stai utilizzando un file .env.local",
|
|
||||||
"Message limit is {{maxLength}} characters. You have entered {{valueLength}} characters.": "Il limite del messaggio è di {{maxLength}} caratteri. Hai inserito {{valueLength}} caratteri.",
|
|
||||||
"Please enter a message": "Per favore, scrivi un messaggio",
|
|
||||||
"Chatbot UI is an advanced chatbot kit for OpenAI's chat models aiming to mimic ChatGPT's interface and functionality.": "Chatbot UI è un kit avanzato di chatbot per i modelli di chat di OpenAI che mira a imitare l'interfaccia e le funzionalità di ChatGPT.",
|
|
||||||
"Are you sure you want to clear all messages?": "Sei sicuro di voler cancellare tutti i messaggi?"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user