complete customChatGpt model selection

This commit is contained in:
Danny Avila 2023-03-03 15:52:06 -05:00
parent 0f9cfd0395
commit 214228542a
18 changed files with 742 additions and 63 deletions

View file

@ -31,7 +31,7 @@ Currently, this project is only functional with the `text-davinci-003` model.
> This is a work in progress. I'm building this in public. You can follow the progress here or on my [Linkedin](https://www.linkedin.com/in/danny-avila).
> Here are my planned/recently finished features.
Here are my planned/recently finished features.
- [x] Rename, delete conversations
- [x] Persistent conversation
@ -43,7 +43,7 @@ Currently, this project is only functional with the `text-davinci-003` model.
- [x] Markdown handling
- [x] Language Detection for code blocks
- [x] 'Copy to clipboard' button for code blocks
- [ ] Set user/model label and prompt prefix view option
- [ ] Customize prompt prefix/label (custom ChatGPT using official API)
- [ ] AI model change handling (whether to pseudo-persist convos or start new convos within existing convo)
- [ ] Server convo pagination (limit fetch and load more with 'show more' button)
- [ ] Bing AI Styling (for suggested responses, convo end, etc.)
@ -58,25 +58,26 @@ Currently, this project is only functional with the `text-davinci-003` model.
- Response streaming identical to ChatGPT
- UI from original ChatGPT, including Dark mode
- AI model selection
- AI model selection, including OpenAI's official ChatGPT API
### Tech Stack
- Utilizes [node-chatgpt-api](https://github.com/waylaidwanderer/node-chatgpt-api)
- Response streaming identical to ChatGPT through server-sent events
- Use of Tailwind CSS (like the official site) and [shadcn/ui](https://github.com/shadcn/ui) components
- useSWR, Redux Toolkit, Express, MongoDB, [Keyv](https://www.npmjs.com/package/keyv)
- highlight.js, useSWR, Redux, Express, MongoDB, [Keyv](https://www.npmjs.com/package/keyv)
## Use Cases ##
![use case example](./public/use_case.png "GPT is down! Plus is too expensive!")
- ChatGPT is down ( and don't want to pay for ChatGPT Plus).
- One stop shop for all conversational AIs, with the added bonus of searching past conversations.
- Using the official API, you'd have to generate 7.5 million words to expense the same cost as ChatGPT Plus ($20).
- ChatGPT Free is down.
- ChatGPT/Google Bard/Bing AI conversations are lost in space or
cannot be searched past a certain timeframe.
- Quick one stop shop for all conversational AIs, with the added bonus of searching
## Origin ##
This project was originally created as a Minimum Viable Product (or MVP) for the [@HackReactor](https://github.com/hackreactor/) Bootcamp. It was built with OpenAI response streaming and most of the UI completed in under 20 hours. 20 hours in, I had most of the UI and basic functionality done. This was created without using any boilerplates or templates, including create-react-app and other toolchains. The purpose of the exercise was to learn setting up a full stack project from scratch. Please feel free to give feedback, suggestions, or fork the project for your own use.
This project was originally created as a Minimum Viable Product (or MVP) for the [@HackReactor](https://github.com/hackreactor/) Bootcamp. It was built with OpenAI response streaming and most of the UI completed in under 20 hours. During the end of that time, I had most of the UI and basic functionality done. This was created without using any boilerplates or templates, including create-react-app and other toolchains. I didn't follow any 'un-official chatgpt' video tutorials, and simply referenced the official site for the UI. The purpose of the exercise was to learn setting up a full stack project from scratch. Please feel free to give feedback, suggestions, or fork the project for your own use.
<!-- ## Solution ##
Serves and searches all conversations reliably. All AI convos under one house.

37
app/chatgpt-custom.js Normal file
View file

@ -0,0 +1,37 @@
require('dotenv').config();
const { KeyvFile } = require('keyv-file');
const clientOptions = {
modelOptions: {
model: 'gpt-3.5-turbo'
},
debug: false
};
const customClient = async ({ text, progressCallback, convo, promptPrefix, chatGptLabel }) => {
const ChatGPTClient = (await import('@waylaidwanderer/chatgpt-api')).default;
const store = {
store: new KeyvFile({ filename: './data/cache.json' })
};
clientOptions.chatGptLabel = chatGptLabel;
if (promptPrefix.length > 0) {
clientOptions.promptPrefix = promptPrefix;
}
const client = new ChatGPTClient(process.env.OPENAI_KEY, clientOptions, store);
let options = {
onProgress: async (partialRes) => await progressCallback(partialRes)
};
if (!!convo.parentMessageId && !!convo.conversationId) {
options = { ...options, ...convo };
}
const res = await client.sendMessage(text, options);
return res;
};
module.exports = customClient;

View file

@ -1,5 +1,6 @@
const { askClient } = require('./chatgpt-client');
const { browserClient } = require('./chatgpt-browser');
const customClient = require('./chatgpt-custom');
const { askBing } = require('./bingai');
const titleConvo = require('./titleConvo');
const detectCode = require('./detectCode');
@ -7,6 +8,7 @@ const detectCode = require('./detectCode');
module.exports = {
askClient,
browserClient,
customClient,
askBing,
titleConvo,
detectCode

View file

@ -11,13 +11,13 @@ const titleConvo = async ({ message, response, model }) => {
{
role: 'system',
content:
'You are a helpful title-generator with one job: titling in title case the conversation provided by a user. You do not reply with anything but a succinct title that summarizes the conversation in title case, ideally around 5 words or less. You do not refer to the participants of the conversation by name. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title.'
'You are a title-generator with one job: titling the conversation provided by a user in title case.'
},
{ role: 'user', content: `Please title this conversation: User:"${message}" ${model}:"${response}" Title:` },
{ role: 'user', content: `In 5 words or less, summarize the conversation below with a title in title case. Don't refer to the participants of the conversation by name. Do not include punctuation or quotation marks. Your response should be in title case, exclusively containing the title. Conversation:\n\nUser: "${message}"\n\n${model}: "${response}"\n\nTitle: ` },
]
});
return completion.data.choices[0].message.content;
return completion.data.choices[0].message.content.replace(/"/g, '');
};
module.exports = titleConvo;

105
package-lock.json generated
View file

@ -10,7 +10,10 @@
"license": "ISC",
"dependencies": {
"@keyv/mongo": "^2.1.8",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-tabs": "^1.0.2",
"@reduxjs/toolkit": "^1.9.2",
"@vscode/vscode-languagedetection": "^1.0.22",
@ -3716,6 +3719,24 @@
"@babel/runtime": "^7.13.10"
}
},
"node_modules/@radix-ui/react-alert-dialog": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.2.tgz",
"integrity": "sha512-0MtxV53FaEEBOKRgyLnEqHZKKDS5BldQ9oUBsKVXWI5FHbl2jp35qs+0aJET+K5hJDsc40kQUzP7g+wC7tqrqA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dialog": "1.0.2",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-slot": "1.0.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz",
@ -3767,6 +3788,32 @@
"react": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-dialog": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.2.tgz",
"integrity": "sha512-EKxxp2WNSmUPkx4trtWNmZ4/vAYEg7JkAfa1HKBUnaubw9eHzf1Orr9B472lJYaYz327RHDrd4R95fsw7VR8DA==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dismissable-layer": "1.0.2",
"@radix-ui/react-focus-guards": "1.0.0",
"@radix-ui/react-focus-scope": "1.0.1",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-portal": "1.0.1",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz",
@ -3852,6 +3899,19 @@
"react": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-label": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.0.tgz",
"integrity": "sha512-7qCcZ3j2VQspWjy+gKR4W+V/z0XueQjeiZnlPOtsyiP9HaS8bfSU7ECoI3bvvdYntQj7NElW7OAYsYRW4MQvCg==",
"dependencies": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0"
}
},
"node_modules/@radix-ui/react-menu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.2.tgz",
@ -17716,6 +17776,20 @@
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-alert-dialog": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.0.2.tgz",
"integrity": "sha512-0MtxV53FaEEBOKRgyLnEqHZKKDS5BldQ9oUBsKVXWI5FHbl2jp35qs+0aJET+K5hJDsc40kQUzP7g+wC7tqrqA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dialog": "1.0.2",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-slot": "1.0.1"
}
},
"@radix-ui/react-arrow": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz",
@ -17753,6 +17827,28 @@
"@babel/runtime": "^7.13.10"
}
},
"@radix-ui/react-dialog": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.0.2.tgz",
"integrity": "sha512-EKxxp2WNSmUPkx4trtWNmZ4/vAYEg7JkAfa1HKBUnaubw9eHzf1Orr9B472lJYaYz327RHDrd4R95fsw7VR8DA==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/primitive": "1.0.0",
"@radix-ui/react-compose-refs": "1.0.0",
"@radix-ui/react-context": "1.0.0",
"@radix-ui/react-dismissable-layer": "1.0.2",
"@radix-ui/react-focus-guards": "1.0.0",
"@radix-ui/react-focus-scope": "1.0.1",
"@radix-ui/react-id": "1.0.0",
"@radix-ui/react-portal": "1.0.1",
"@radix-ui/react-presence": "1.0.0",
"@radix-ui/react-primitive": "1.0.1",
"@radix-ui/react-slot": "1.0.1",
"@radix-ui/react-use-controllable-state": "1.0.0",
"aria-hidden": "^1.1.1",
"react-remove-scroll": "2.5.5"
}
},
"@radix-ui/react-direction": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.0.0.tgz",
@ -17817,6 +17913,15 @@
"@radix-ui/react-use-layout-effect": "1.0.0"
}
},
"@radix-ui/react-label": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.0.tgz",
"integrity": "sha512-7qCcZ3j2VQspWjy+gKR4W+V/z0XueQjeiZnlPOtsyiP9HaS8bfSU7ECoI3bvvdYntQj7NElW7OAYsYRW4MQvCg==",
"requires": {
"@babel/runtime": "^7.13.10",
"@radix-ui/react-primitive": "1.0.1"
}
},
"@radix-ui/react-menu": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.2.tgz",

View file

@ -22,7 +22,10 @@
"homepage": "https://github.com/danny-avila/rpp2210-mvp#readme",
"dependencies": {
"@keyv/mongo": "^2.1.8",
"@radix-ui/react-alert-dialog": "^1.0.2",
"@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-dropdown-menu": "^2.0.2",
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-tabs": "^1.0.2",
"@reduxjs/toolkit": "^1.9.2",
"@vscode/vscode-languagedetection": "^1.0.22",

View file

@ -2,14 +2,21 @@ const express = require('express');
const crypto = require('crypto');
const router = express.Router();
const askBing = require('./askBing');
const { titleConvo, askClient, browserClient, detectCode } = require('../../app/');
const {
titleConvo,
askClient,
browserClient,
customClient,
detectCode
} = require('../../app/');
const { saveMessage, deleteMessages, saveConvo } = require('../../models');
const { handleError, sendMessage } = require('./handlers');
router.use('/bing', askBing);
router.post('/', async (req, res) => {
const { model, text, parentMessageId, conversationId } = req.body;
const { model, text, parentMessageId, conversationId, chatGptLabel, promptPrefix } =
req.body;
if (!text.trim().includes(' ') && text.length < 5) {
return handleError(res, 'Prompt empty or too short');
}
@ -17,9 +24,24 @@ router.post('/', async (req, res) => {
const userMessageId = crypto.randomUUID();
let userMessage = { id: userMessageId, sender: 'User', text };
console.log('ask log', { model, ...userMessage, parentMessageId, conversationId });
console.log('ask log', {
model,
...userMessage,
parentMessageId,
conversationId,
chatGptLabel,
promptPrefix
});
const client = model === 'chatgpt' ? askClient : browserClient;
let client;
if (model === 'chatgpt') {
client = askClient;
} else if (model === 'chatgptCustom') {
client = customClient;
} else {
client = browserClient;
}
res.writeHead(200, {
Connection: 'keep-alive',
@ -61,7 +83,9 @@ router.post('/', async (req, res) => {
convo: {
parentMessageId,
conversationId
}
},
chatGptLabel,
promptPrefix
});
console.log('CLIENT RESPONSE', gptResponse);
@ -87,9 +111,13 @@ router.post('/', async (req, res) => {
}
if (!parentMessageId) {
gptResponse.title = await titleConvo({ model, message: text, response: JSON.stringify(gptResponse.text) });
gptResponse.title = await titleConvo({
model,
message: text,
response: JSON.stringify(gptResponse.text)
});
}
gptResponse.sender = model;
gptResponse.sender = model === 'chatgptCustom' ? chatGptLabel : model;
gptResponse.final = true;
gptResponse.text = await detectCode(gptResponse.text);
await saveMessage(gptResponse);

View file

@ -54,7 +54,7 @@ export default function Message({
let icon = `${sender}:`;
let backgroundColor = bgColors[sender];
if (notUser) {
if (notUser && backgroundColor) {
props.className =
'w-full border-b border-black/10 dark:border-gray-900/50 text-gray-800 dark:text-gray-100 group bg-gray-100 dark:bg-[#444654]';
icon = (
@ -77,7 +77,9 @@ export default function Message({
onMouseOut={handleMouseOut}
>
<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">
<strong className="relative flex w-[30px] flex-col items-end">{icon}</strong>
<strong className="relative flex w-[30px] flex-col items-end text-right">
{icon}
</strong>
<div className="relative flex w-[calc(100%-50px)] flex-col gap-1 whitespace-pre-wrap md:gap-3 lg:w-[calc(100%-115px)]">
<div className="flex flex-grow flex-col gap-3">
{error ? (

View file

@ -1,10 +1,13 @@
import React, { useEffect } from 'react';
import React, { useState, useEffect } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { useSelector, useDispatch } from 'react-redux';
import { setModel } from '~/store/submitSlice';
import { setModel, setDisabled, setCustomGpt } from '~/store/submitSlice';
import GPTIcon from '../svg/GPTIcon';
import BingIcon from '../svg/BingIcon';
import { Button } from '../ui/Button.tsx';
import { Input } from '../ui/Input.tsx';
import { Label } from '../ui/Label.tsx';
import {
DropdownMenu,
DropdownMenuContent,
@ -15,25 +18,56 @@ import {
DropdownMenuTrigger
} from '../ui/DropdownMenu.tsx';
import {
Dialog,
DialogTrigger,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '../ui/Dialog.tsx';
export default function ModelMenu() {
const dispatch = useDispatch();
const { model } = useSelector((state) => state.submit);
const onChange = (value) => {
dispatch(setModel(value));
};
const [chatGptLabel, setChatGptLabel] = useState('');
const [promptPrefix, setPromptPrefix] = useState('');
const [required, setRequired] = useState(false);
useEffect(() => {
const lastSelectedModel = JSON.parse(localStorage.getItem('model'));
if (lastSelectedModel) {
if (lastSelectedModel && lastSelectedModel !== 'chatgptCustom') {
dispatch(setModel(lastSelectedModel));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
localStorage.setItem('model', JSON.stringify(model));
}, [model]);
const onChange = (value) => {
if (value === 'chatgptCustom') {
// dispatch(setDisabled(true));
} else {
dispatch(setModel(value));
dispatch(setDisabled(false));
}
};
const submitHandler = (e) => {
if (chatGptLabel.length === 0) {
e.preventDefault();
setRequired(true);
return;
}
dispatch(setCustomGpt({ chatGptLabel, promptPrefix }));
dispatch(setModel('chatgptCustom'));
dispatch(setDisabled(false));
};
const defaultColorProps = [
'text-gray-500',
'hover:bg-gray-100',
@ -54,32 +88,100 @@ export default function ModelMenu() {
'dark:disabled:hover:bg-transparent'
];
const requiredProp = required ? { required: true } : {};
const colorProps = model === 'chatgpt' ? chatgptColorProps : defaultColorProps;
const icon = model === 'bingai' ? <BingIcon button={true} /> : <GPTIcon button={true} /> ;
const icon = model === 'bingai' ? <BingIcon button={true} /> : <GPTIcon button={true} />;
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
className={`absolute bottom-0.5 rounded-md border-0 p-1 pl-2 outline-none ${colorProps.join(' ')} focus:ring-0 focus:ring-offset-0 disabled:bottom-0.5 md:pl-1 md:bottom-1 md:left-2 md:disabled:bottom-1`}
>
{icon}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Select a Model</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={model}
onValueChange={onChange}
>
<DropdownMenuRadioItem value="chatgpt">ChatGPT</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bingai">BingAI</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="chatgptBrowser">{'ChatGPT (free)'}</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<Dialog>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
className={`absolute bottom-0.5 rounded-md border-0 p-1 pl-2 outline-none ${colorProps.join(
' '
)} focus:ring-0 focus:ring-offset-0 disabled:bottom-0.5 md:bottom-1 md:left-2 md:pl-1 md:disabled:bottom-1`}
>
{icon}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>Select a Model</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={model}
onValueChange={onChange}
>
<DropdownMenuRadioItem value="chatgpt">
ChatGPT <sup>$</sup>
</DropdownMenuRadioItem>
<DialogTrigger asChild>
<DropdownMenuRadioItem value="chatgptCustom">
CustomGPT <sup>$</sup>
</DropdownMenuRadioItem>
</DialogTrigger>
<DropdownMenuRadioItem value="bingai">BingAI</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="chatgptBrowser">ChatGPT</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
<DialogContent>
<DialogHeader>
<DialogTitle>Customize ChatGPT</DialogTitle>
<DialogDescription>
Note: important instructions are often better placed in your message rather than
the prefix.{' '}
<a href="https://platform.openai.com/docs/guides/chat/instructing-chat-models">
<u>More info here</u>
</a>
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label
htmlFor="chatGptLabel"
className="text-right"
>
Custom Name
</Label>
<Input
id="chatGptLabel"
value={chatGptLabel}
onChange={(e) => setChatGptLabel(e.target.value)}
placeholder="Set a custom name for ChatGPT"
className="col-span-3 invalid:border-red-400 invalid:text-red-600 invalid:placeholder-red-600 invalid:placeholder-opacity-70
focus:ring-opacity-20 focus:invalid:border-red-400 focus:invalid:ring-red-400 focus:invalid:ring-opacity-20"
{...requiredProp}
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label
htmlFor="promptPrefix"
className="text-right"
>
Prompt Prefix
</Label>
<TextareaAutosize
id="promptPrefix"
value={promptPrefix}
onChange={(e) => setPromptPrefix(e.target.value)}
placeholder="Set custom instructions. Defaults to: 'You are ChatGPT, a large language model trained by OpenAI.'"
className="col-span-3 flex h-20 w-full resize-none rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900"
/>
</div>
</div>
<DialogFooter>
<DialogClose>Cancel</DialogClose>
<DialogClose
onClick={submitHandler}
className="inline-flex h-10 items-center justify-center rounded-md border-none bg-slate-900 py-2 px-4 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900"
>
Submit
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import { useSelector } from 'react-redux';
export default function SubmitButton({ submitMessage }) {
const { isSubmitting } = useSelector((state) => state.submit);
const { isSubmitting, disabled } = useSelector((state) => state.submit);
const clickHandler = (e) => {
e.preventDefault();
submitMessage();
@ -22,6 +22,7 @@ export default function SubmitButton({ submitMessage }) {
return (
<button
onClick={clickHandler}
disabled={disabled}
className="absolute bottom-1.5 right-1 rounded-md p-1 text-gray-500 hover:bg-gray-100 disabled:hover:bg-transparent dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:disabled:hover:bg-transparent md:bottom-2.5 md:right-2"
>
<svg

View file

@ -15,7 +15,7 @@ export default function TextChat({ messages }) {
const [errorMessage, setErrorMessage] = useState('');
const dispatch = useDispatch();
const convo = useSelector((state) => state.convo);
const { isSubmitting, model } = useSelector((state) => state.submit);
const { isSubmitting, disabled, model, chatGptLabel, promptPrefix } = useSelector((state) => state.submit);
const { text } = useSelector((state) => state.text);
const { error } = convo;
@ -30,15 +30,20 @@ export default function TextChat({ messages }) {
dispatch(setSubmitState(true));
const message = text.trim();
const currentMsg = { sender: 'User', text: message, current: true };
const initialResponse = { sender: model, text: '' };
const sender = model === 'chatgptCustom' ? chatGptLabel : model
const initialResponse = { sender, text: '' };
dispatch(setMessages([...messages, currentMsg, initialResponse]));
dispatch(setText(''));
const messageHandler = (data) => {
dispatch(setMessages([...messages, currentMsg, { sender: model, text: data }]));
dispatch(setMessages([...messages, currentMsg, { sender, text: data }]));
};
const convoHandler = (data) => {
dispatch(
setMessages([...messages, currentMsg, { sender: model, text: data.text || data.response }])
setMessages([
...messages,
currentMsg,
{ sender, text: data.text || data.response }
])
);
if (
@ -116,7 +121,9 @@ export default function TextChat({ messages }) {
convo,
messageHandler,
convoHandler,
errorHandler
errorHandler,
chatGptLabel,
promptPrefix
};
console.log('User Input:', message);
handleSubmit(submission);
@ -167,7 +174,13 @@ export default function TextChat({ messages }) {
errorMessage={errorMessage}
/>
) : (
<div className="relative flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-gray-700 dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4">
<div
className={`relative flex w-full flex-grow flex-col rounded-md border border-black/10 ${
disabled ? 'bg-gray-100' : 'bg-white'
} py-2 shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 ${
disabled ? 'dark:bg-gray-900' : 'dark:bg-gray-700'
} dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] md:py-3 md:pl-4`}
>
<ModelMenu />
<TextareaAutosize
tabIndex="0"
@ -177,7 +190,8 @@ export default function TextChat({ messages }) {
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onChange={changeHandler}
placeholder=""
placeholder={disabled ? 'Choose another model or customize GPT again' : ''}
disabled={disabled}
className="m-0 h-auto max-h-52 resize-none overflow-auto border-0 bg-transparent p-0 pl-9 pr-8 leading-6 focus:outline-none focus:ring-0 focus-visible:ring-0 dark:bg-transparent md:pl-8"
/>
<SubmitButton submitMessage={submitMessage} />

View file

@ -0,0 +1,156 @@
"use client"
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "../../utils"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = ({
className,
children,
...props
}: AlertDialogPrimitive.AlertDialogPortalProps) => (
<AlertDialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-end justify-center sm:items-center">
{children}
</div>
</AlertDialogPrimitive.Portal>
)
AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity animate-in fade-in",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full max-w-lg scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full",
"dark:bg-slate-900",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-slate-900",
"dark:text-slate-50",
className
)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-slate-500", "dark:text-slate-400", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-slate-900 py-2 px-4 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
className
)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
"mt-2 inline-flex h-10 items-center justify-center rounded-md border border-slate-200 bg-transparent py-2 px-4 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}

View file

@ -0,0 +1,144 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "../../utils"
const Dialog = DialogPrimitive.Root
const DialogTrigger = DialogPrimitive.Trigger
const DialogPortal = ({
className,
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal className={cn(className)} {...props}>
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
)
DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<DialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=closed]:fade-out",
className
)}
{...props}
ref={ref}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full gap-4 rounded-b-lg bg-white p-6 animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
"dark:bg-slate-900",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName
const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"
const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"
const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-slate-900",
"dark:text-slate-50",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName
const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-slate-500", "dark:text-slate-400", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName
const DialogClose = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Close>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Close
ref={ref}
className={cn(
"mt-2 inline-flex h-10 items-center justify-center rounded-md border border-slate-200 bg-transparent py-2 px-4 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0",
className
)}
{...props}
/>
))
DialogClose.displayName = DialogPrimitive.Title.displayName
export {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
DialogClose,
}

View file

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "../../utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
return (
<input
className={cn(
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View file

@ -0,0 +1,21 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cn } from "../../utils"
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
"text-sm font-medium dark:text-gray-200 leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -0,0 +1,26 @@
/* eslint-disable */
import * as React from "react"
import TextareaAutosize from 'react-textarea-autosize';
import { cn } from "../../utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex h-20 w-full resize-none rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

View file

@ -2,7 +2,10 @@ import { createSlice } from '@reduxjs/toolkit';
const initialState = {
isSubmitting: false,
model: 'bingai'
disabled: false,
model: 'chatgpt',
promptPrefix: '',
chatGptLabel: '',
};
const currentSlice = createSlice({
@ -12,12 +15,20 @@ const currentSlice = createSlice({
setSubmitState: (state, action) => {
state.isSubmitting = action.payload;
},
setDisabled: (state, action) => {
state.disabled = action.payload;
},
setModel: (state, action) => {
state.model = action.payload;
},
setCustomGpt: (state, action) => {
console.log('setCustomGpt', action.payload);
state.promptPrefix = action.payload.promptPrefix;
state.chatGptLabel = action.payload.chatGptLabel;
},
}
});
export const { setSubmitState, setModel } = currentSlice.actions;
export const { setSubmitState, setDisabled, setModel, setCustomGpt } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -8,9 +8,11 @@ export default function handleSubmit({
convo,
messageHandler,
convoHandler,
errorHandler
errorHandler,
chatGptLabel,
promptPrefix
}) {
let payload = { model, text };
let payload = { model, text, chatGptLabel, promptPrefix };
if (convo.conversationId && convo.parentMessageId) {
payload = {
...payload,