merged add config options

This commit is contained in:
serban-alexandru 2023-05-08 16:45:50 +03:00
parent 61f15eb82e
commit e60272eefa
15 changed files with 5804 additions and 327 deletions

4
.parcelrc Normal file
View file

@ -0,0 +1,4 @@
{
"extends": ["@parcel/config-default"],
"reporters": ["...", "parcel-reporter-static-files-copy"]
}

View file

@ -24,7 +24,13 @@ Crafted with love and care to provide the best experience possible.
## Self-host using Docker ## Self-host using Docker
``` ```
docker run --name chatpad -d -p 1234:80 ghcr.io/deiucanta/chatpad:latest docker run --name chatpad -d -p 8080:80 ghcr.io/deiucanta/chatpad:latest
```
## Self-host using Docker with custom config
```
docker run --name chatpad -d -v `pwd`/config.json:/usr/share/nginx/html/config.json -p 8080:80 ghcr.io/deiucanta/chatpad:latest
``` ```
## One click Deployments ## One click Deployments

23
package-lock.json generated
View file

@ -44,7 +44,7 @@
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"buffer": "^5.7.1", "buffer": "^5.7.1",
"parcel": "^2.8.3", "parcel": "^2.8.3",
"path-browserify": "^1.0.1", "parcel-reporter-static-files-copy": "^1.5.0",
"process": "^0.11.10" "process": "^0.11.10"
} }
}, },
@ -6776,6 +6776,18 @@
"url": "https://opencollective.com/parcel" "url": "https://opencollective.com/parcel"
} }
}, },
"node_modules/parcel-reporter-static-files-copy": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/parcel-reporter-static-files-copy/-/parcel-reporter-static-files-copy-1.5.0.tgz",
"integrity": "sha512-dsY3MQkbYSgEqS0/22vtD2mZtel8UC0ItH0ok8LmgFeCMTsdhyOtJgvt945ODIzu9lYc/sCIzksM8C77uSE3Fg==",
"dev": true,
"dependencies": {
"@parcel/plugin": "^2.0.0-beta.1"
},
"engines": {
"parcel": "^2.0.0-beta.1"
}
},
"node_modules/parent-module": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -12924,6 +12936,15 @@
"v8-compile-cache": "^2.0.0" "v8-compile-cache": "^2.0.0"
} }
}, },
"parcel-reporter-static-files-copy": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/parcel-reporter-static-files-copy/-/parcel-reporter-static-files-copy-1.5.0.tgz",
"integrity": "sha512-dsY3MQkbYSgEqS0/22vtD2mZtel8UC0ItH0ok8LmgFeCMTsdhyOtJgvt945ODIzu9lYc/sCIzksM8C77uSE3Fg==",
"dev": true,
"requires": {
"@parcel/plugin": "^2.0.0-beta.1"
}
},
"parent-module": { "parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",

View file

@ -7,6 +7,9 @@
"start": "parcel", "start": "parcel",
"build": "parcel build" "build": "parcel build"
}, },
"staticFiles": {
"staticPath": "src/static"
},
"devDependencies": { "devDependencies": {
"@parcel/transformer-sass": "^2.8.3", "@parcel/transformer-sass": "^2.8.3",
"@types/downloadjs": "^1.4.3", "@types/downloadjs": "^1.4.3",
@ -16,6 +19,7 @@
"buffer": "^5.7.1", "buffer": "^5.7.1",
"parcel": "^2.8.3", "parcel": "^2.8.3",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"parcel-reporter-static-files-copy": "^1.5.0",
"process": "^0.11.10" "process": "^0.11.10"
}, },
"dependencies": { "dependencies": {

View file

@ -40,6 +40,7 @@ import { DatabaseModal } from "./DatabaseModal";
import { LogoText } from "./Logo"; import { LogoText } from "./Logo";
import { Prompts } from "./Prompts"; import { Prompts } from "./Prompts";
import { SettingsModal } from "./SettingsModal"; import { SettingsModal } from "./SettingsModal";
import { config } from "../utils/config";
declare global { declare global {
interface Window { interface Window {
@ -186,35 +187,41 @@ export function Layout() {
</Navbar.Section> </Navbar.Section>
<Navbar.Section sx={{ borderTop: border }} p="xs"> <Navbar.Section sx={{ borderTop: border }} p="xs">
<Center> <Center>
<Tooltip {config.allowDarkModeToggle && (
label={colorScheme === "dark" ? "Light Mode" : "Dark Mode"} <Tooltip
> label={colorScheme === "dark" ? "Light Mode" : "Dark Mode"}
<ActionIcon
sx={{ flex: 1 }}
size="xl"
onClick={() => toggleColorScheme()}
> >
{colorScheme === "dark" ? ( <ActionIcon
<IconSunHigh size={20} /> sx={{ flex: 1 }}
) : ( size="xl"
<IconMoonStars size={20} /> onClick={() => toggleColorScheme()}
)} >
</ActionIcon> {colorScheme === "dark" ? (
</Tooltip> <IconSunHigh size={20} />
<SettingsModal> ) : (
<Tooltip label="Settings"> <IconMoonStars size={20} />
<ActionIcon sx={{ flex: 1 }} size="xl"> )}
<IconSettings size={20} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
</SettingsModal> )}
<DatabaseModal> {config.allowSettingsModal && (
<Tooltip label="Database"> <SettingsModal>
<ActionIcon sx={{ flex: 1 }} size="xl"> <Tooltip label="Settings">
<IconDatabase size={20} /> <ActionIcon sx={{ flex: 1 }} size="xl">
</ActionIcon> <IconSettings size={20} />
</Tooltip> </ActionIcon>
</DatabaseModal> </Tooltip>
</SettingsModal>
)}
{config.allowDatabaseModal && (
<DatabaseModal>
<Tooltip label="Database">
<ActionIcon sx={{ flex: 1 }} size="xl">
<IconDatabase size={20} />
</ActionIcon>
</Tooltip>
</DatabaseModal>
)}
<Tooltip label="Source Code"> <Tooltip label="Source Code">
<ActionIcon <ActionIcon
component="a" component="a"
@ -226,36 +233,40 @@ export function Layout() {
<IconBrandGithub size={20} /> <IconBrandGithub size={20} />
</ActionIcon> </ActionIcon>
</Tooltip> </Tooltip>
<Tooltip label="Follow on Twitter"> {config.showTwitterLink && (
<ActionIcon <Tooltip label="Follow on Twitter">
component="a" <ActionIcon
href="https://twitter.com/deiucanta" component="a"
target="_blank" href="https://twitter.com/deiucanta"
sx={{ flex: 1 }} target="_blank"
size="xl" sx={{ flex: 1 }}
> size="xl"
<IconBrandTwitter size={20} /> >
</ActionIcon> <IconBrandTwitter size={20} />
</Tooltip> </ActionIcon>
<Tooltip label="Give Feedback"> </Tooltip>
<ActionIcon )}
component="a" {config.showFeedbackLink && (
href="https://feedback.chatpad.ai" <Tooltip label="Give Feedback">
onClick={(event) => { <ActionIcon
if (window.todesktop) { component="a"
event.preventDefault(); href="https://feedback.chatpad.ai"
window.todesktop.contents.openUrlInBrowser( onClick={(event) => {
"https://feedback.chatpad.ai" if (window.todesktop) {
); event.preventDefault();
} window.todesktop.contents.openUrlInBrowser(
}} "https://feedback.chatpad.ai"
target="_blank" );
sx={{ flex: 1 }} }
size="xl" }}
> target="_blank"
<IconMessage size={20} /> sx={{ flex: 1 }}
</ActionIcon> size="xl"
</Tooltip> >
<IconMessage size={20} />
</ActionIcon>
</Tooltip>
)}
</Center> </Center>
</Navbar.Section> </Navbar.Section>
</Navbar> </Navbar>

View file

@ -6,6 +6,7 @@ import {
List, List,
Modal, Modal,
PasswordInput, PasswordInput,
TextInput,
Select, Select,
Stack, Stack,
Text, Text,
@ -15,7 +16,7 @@ import { notifications } from "@mantine/notifications";
import { useLiveQuery } from "dexie-react-hooks"; import { useLiveQuery } from "dexie-react-hooks";
import { cloneElement, ReactElement, useEffect, useState } from "react"; import { cloneElement, ReactElement, useEffect, useState } from "react";
import { db } from "../db"; import { db } from "../db";
import { availableModels, defaultModel } from "../utils/constants"; import { config } from "../utils/config";
import { checkOpenAIKey } from "../utils/openai"; import { checkOpenAIKey } from "../utils/openai";
export function SettingsModal({ children }: { children: ReactElement }) { export function SettingsModal({ children }: { children: ReactElement }) {
@ -23,7 +24,11 @@ export function SettingsModal({ children }: { children: ReactElement }) {
const [submitting, setSubmitting] = useState(false); const [submitting, setSubmitting] = useState(false);
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [model, setModel] = useState(defaultModel); const [model, setModel] = useState(config.defaultModel);
const [type, setType] = useState(config.defaultType);
const [auth, setAuth] = useState(config.defaultAuth);
const [base, setBase] = useState("");
const [version, setVersion] = useState("");
const settings = useLiveQuery(async () => { const settings = useLiveQuery(async () => {
return db.settings.where({ id: "general" }).first(); return db.settings.where({ id: "general" }).first();
@ -36,6 +41,18 @@ export function SettingsModal({ children }: { children: ReactElement }) {
if (settings?.openAiModel) { if (settings?.openAiModel) {
setModel(settings.openAiModel); setModel(settings.openAiModel);
} }
if (settings?.openAiApiType) {
setType(settings.openAiApiType);
}
if (settings?.openAiApiAuth) {
setAuth(settings.openAiApiAuth);
}
if (settings?.openAiApiBase) {
setBase(settings.openAiApiBase);
}
if (settings?.openAiApiVersion) {
setVersion(settings.openAiApiVersion);
}
}, [settings]); }, [settings]);
return ( return (
@ -111,20 +128,213 @@ export function SettingsModal({ children }: { children: ReactElement }) {
</List.Item> </List.Item>
</List> </List>
<Select <Select
label="OpenAI Model" label="OpenAI Type"
value={model} value={type}
onChange={(value) => { onChange={async (value) => {
db.settings.update("general", { setSubmitting(true);
openAiModel: value ?? undefined, try {
}); await db.settings.update("general", {
openAiApiType: value ?? 'openai',
});
notifications.show({
title: "Saved",
message: "Your OpenAI Type has been saved.",
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
color: "red",
message: "No internet connection.",
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
color: "red",
message,
});
}
} finally {
setSubmitting(false);
}
}} }}
withinPortal withinPortal
data={availableModels} data={[{ "value": "openai", "label": "OpenAI"}, { "value": "custom", "label": "Custom (e.g. Azure OpenAI)"}]}
/>
<Select
label="OpenAI Model (OpenAI Only)"
value={model}
onChange={async (value) => {
setSubmitting(true);
try {
await db.settings.update("general", {
openAiModel: value ?? undefined,
});
notifications.show({
title: "Saved",
message: "Your OpenAI Model has been saved.",
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
color: "red",
message: "No internet connection.",
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
color: "red",
message,
});
}
} finally {
setSubmitting(false);
}
}}
withinPortal
data={config.availableModels}
/> />
<Alert color="orange" title="Warning"> <Alert color="orange" title="Warning">
The displayed cost was not updated yet to reflect the costs for each The displayed cost was not updated yet to reflect the costs for each
model. Right now it will always show the cost for GPT-3.5. model. Right now it will always show the cost for GPT-3.5 on OpenAI.
</Alert> </Alert>
<Select
label="OpenAI Auth (Custom Only)"
value={auth}
onChange={async (value) => {
setSubmitting(true);
try {
await db.settings.update("general", {
openAiApiAuth: value ?? 'none',
});
notifications.show({
title: "Saved",
message: "Your OpenAI Auth has been saved.",
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
color: "red",
message: "No internet connection.",
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
color: "red",
message,
});
}
} finally {
setSubmitting(false);
}
}}
withinPortal
data={[{ "value": "none", "label": "None"}, { "value": "bearer-token", "label": "Bearer Token"}, { "value": "api-key", "label": "API Key"}]}
/>
<form
onSubmit={async (event) => {
try {
setSubmitting(true);
event.preventDefault();
await db.settings.where({ id: "general" }).modify((row) => {
row.openAiApiBase = base;
console.log(row);
});
notifications.show({
title: "Saved",
message: "Your OpenAI Base has been saved.",
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
color: "red",
message: "No internet connection.",
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
color: "red",
message,
});
}
} finally {
setSubmitting(false);
}
}}
>
<Flex gap="xs" align="end">
<TextInput
label="OpenAI API Base (Custom Only)"
placeholder="https://<resource-name>.openai.azure.com/openai/deployments/<deployment>"
sx={{ flex: 1 }}
value={base}
onChange={(event) => setBase(event.currentTarget.value)}
formNoValidate
/>
<Button type="submit" loading={submitting}>
Save
</Button>
</Flex>
</form>
<form
onSubmit={async (event) => {
try {
setSubmitting(true);
event.preventDefault();
await db.settings.where({ id: "general" }).modify((row) => {
row.openAiApiVersion = version;
console.log(row);
});
notifications.show({
title: "Saved",
message: "Your OpenAI Version has been saved.",
});
} catch (error: any) {
if (error.toJSON().message === "Network Error") {
notifications.show({
title: "Error",
color: "red",
message: "No internet connection.",
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
color: "red",
message,
});
}
} finally {
setSubmitting(false);
}
}}
>
<Flex gap="xs" align="end">
<TextInput
label="OpenAI API Version (Custom Only)"
placeholder="2023-03-15-preview"
sx={{ flex: 1 }}
value={version}
onChange={(event) => setVersion(event.currentTarget.value)}
formNoValidate
/>
<Button type="submit" loading={submitting}>
Save
</Button>
</Flex>
</form>
</Stack> </Stack>
</Modal> </Modal>
</> </>

View file

@ -1,5 +1,6 @@
import Dexie, { Table } from "dexie"; import Dexie, { Table } from "dexie";
import "dexie-export-import"; import "dexie-export-import";
import { config } from "../utils/config";
export interface Chat { export interface Chat {
id: string; id: string;
@ -27,6 +28,10 @@ export interface Settings {
id: "general"; id: "general";
openAiApiKey?: string; openAiApiKey?: string;
openAiModel?: string; openAiModel?: string;
openAiApiType?: 'openai' | 'custom';
openAiApiAuth?: 'none' | 'bearer-token' | 'api-key';
openAiApiBase?: string;
openAiApiVersion?: string;
} }
export class Database extends Dexie { export class Database extends Dexie {
@ -47,6 +52,12 @@ export class Database extends Dexie {
this.on("populate", async () => { this.on("populate", async () => {
db.settings.add({ db.settings.add({
id: "general", id: "general",
openAiModel: config.defaultModel,
openAiApiType: config.defaultType,
openAiApiAuth: config.defaultAuth,
...(config.defaultKey != '' && { openAiApiKey: config.defaultKey }),
...(config.defaultBase != '' && { openAiApiBase: config.defaultBase }),
...(config.defaultVersion != '' && { openAiApiVersion: config.defaultVersion }),
}); });
}); });
} }

View file

@ -1,6 +1,10 @@
import { createRoot } from "react-dom/client"; import React from "react";
import ReactDOM from "react-dom";
import { App } from "./components/App"; import { App } from "./components/App";
import { loadConfig } from "./utils/config";
const container = document.getElementById("app"); loadConfig().then(() => {
const root = createRoot(container!); const container = document.getElementById("app");
root.render(<App />); const root = ReactDOM.createRoot(container!);
root.render(<App />);
});

View file

@ -19,13 +19,11 @@ import { AiOutlineSend } from "react-icons/ai";
import { MessageItem } from "../components/MessageItem"; import { MessageItem } from "../components/MessageItem";
import { db } from "../db"; import { db } from "../db";
import { useChatId } from "../hooks/useChatId"; import { useChatId } from "../hooks/useChatId";
import { config } from "../utils/config";
import { import {
writingCharacters, createChatCompletion,
writingFormats, createStreamChatCompletion,
writingStyles, } from "../utils/openai";
writingTones,
} from "../utils/constants";
import { createChatCompletion, createStreamChatCompletion } from "../utils/openai";
export function ChatRoute() { export function ChatRoute() {
const chatId = useChatId(); const chatId = useChatId();
@ -101,7 +99,7 @@ export function ChatRoute() {
}); });
setContent(""); setContent("");
const messageId = nanoid() const messageId = nanoid();
await db.messages.add({ await db.messages.add({
id: messageId, id: messageId,
chatId, chatId,
@ -110,17 +108,22 @@ export function ChatRoute() {
createdAt: new Date(), createdAt: new Date(),
}); });
await createStreamChatCompletion(apiKey, [ await createStreamChatCompletion(
{ apiKey,
role: "system", [
content: getSystemMessage(), {
}, role: "system",
...(messages ?? []).map((message) => ({ content: getSystemMessage(),
role: message.role, },
content: message.content, ...(messages ?? []).map((message) => ({
})), role: message.role,
{ role: "user", content }, content: message.content,
], chatId, messageId); })),
{ role: "user", content },
],
chatId,
messageId
);
setSubmitting(false); setSubmitting(false);
@ -260,7 +263,7 @@ export function ChatRoute() {
<Select <Select
value={writingCharacter} value={writingCharacter}
onChange={setWritingCharacter} onChange={setWritingCharacter}
data={writingCharacters} data={config.writingCharacters}
placeholder="Character" placeholder="Character"
variant="filled" variant="filled"
searchable searchable
@ -270,7 +273,7 @@ export function ChatRoute() {
<Select <Select
value={writingTone} value={writingTone}
onChange={setWritingTone} onChange={setWritingTone}
data={writingTones} data={config.writingTones}
placeholder="Tone" placeholder="Tone"
variant="filled" variant="filled"
searchable searchable
@ -280,7 +283,7 @@ export function ChatRoute() {
<Select <Select
value={writingStyle} value={writingStyle}
onChange={setWritingStyle} onChange={setWritingStyle}
data={writingStyles} data={config.writingStyles}
placeholder="Style" placeholder="Style"
variant="filled" variant="filled"
searchable searchable
@ -290,7 +293,7 @@ export function ChatRoute() {
<Select <Select
value={writingFormat} value={writingFormat}
onChange={setWritingFormat} onChange={setWritingFormat}
data={writingFormats} data={config.writingFormats}
placeholder="Format" placeholder="Format"
variant="filled" variant="filled"
searchable searchable

View file

@ -19,6 +19,7 @@ import { useLiveQuery } from "dexie-react-hooks";
import { Logo } from "../components/Logo"; import { Logo } from "../components/Logo";
import { SettingsModal } from "../components/SettingsModal"; import { SettingsModal } from "../components/SettingsModal";
import { db } from "../db"; import { db } from "../db";
import { config } from "../utils/config";
export function IndexRoute() { export function IndexRoute() {
const settings = useLiveQuery(() => db.settings.get("general")); const settings = useLiveQuery(() => db.settings.get("general"));
@ -56,16 +57,18 @@ export function IndexRoute() {
))} ))}
</SimpleGrid> </SimpleGrid>
<Group mt={50}> <Group mt={50}>
<SettingsModal> {config.allowSettingsModal && (
<Button <SettingsModal>
size="md" <Button
variant={openAiApiKey ? "light" : "filled"} size="md"
leftIcon={<IconKey size={20} />} variant={openAiApiKey ? "light" : "filled"}
> leftIcon={<IconKey size={20} />}
{openAiApiKey ? "Change OpenAI Key" : "Enter OpenAI Key"} >
</Button> {openAiApiKey ? "Change OpenAI Key" : "Enter OpenAI Key"}
</SettingsModal> </Button>
{!window.todesktop && ( </SettingsModal>
)}
{config.showDownloadLink && !window.todesktop && (
<Button <Button
component="a" component="a"
href="https://dl.todesktop.com/230313oyppkw40a" href="https://dl.todesktop.com/230313oyppkw40a"

207
src/static/config.json Normal file
View file

@ -0,0 +1,207 @@
{
"defaultModel": "gpt-3.5-turbo",
"defaultType": "openai",
"defaultAuth": "api-key",
"defaultBase": "",
"defaultVersion": "",
"defaultKey": "",
"availableModels": [
{
"value": "gpt-3.5-turbo",
"label": "GPT-3.5-TURBO (Default ChatGPT)"
},
{
"value": "gpt-3.5-turbo-0301",
"label": "GPT-3.5-TURBO-0301"
},
{
"value": "gpt-4",
"label": "GPT-4 (Limited Beta)"
},
{
"value": "gpt-4-0314",
"label": "GPT-4-0314 (Limited Beta)"
},
{
"value": "gpt-4-32k",
"label": "GPT-4-32K (Limited Beta)"
},
{
"value": "gpt-4-32k-0314",
"label": "GPT-4-32K-0314 (Limited Beta)"
}
],
"writingCharacters": [
{
"label": "Standup Comedian",
"value": "A performer who entertains audiences by telling jokes and humorous stories."
},
{
"label": "Life Coach",
"value": "A professional who helps individuals identify and achieve their personal and professional goals."
},
{
"label": "Career Counselor",
"value": "A professional who helps individuals explore and choose careers, develop job search strategies, and improve job performance."
},
{
"label": "Nutritionist",
"value": "A health professional who specializes in the study of nutrition and its effects on the body."
},
{
"label": "Product Manager",
"value": "A professional who oversees the development and marketing of a company's products."
},
{
"label": "Personal Trainer",
"value": "A fitness professional who works with individuals to develop personalized exercise programs and improve their overall health and fitness."
},
{
"label": "Life Hacker",
"value": "A person who uses unconventional methods to solve problems and increase productivity in everyday life."
},
{
"label": "Travel Advisor",
"value": "A professional who helps individuals plan and book travel arrangements."
},
{
"label": "Mindfulness Coach",
"value": "A professional who helps individuals develop mindfulness practices to reduce stress and improve well-being."
},
{
"label": "Financial Advisor",
"value": "A professional who provides guidance and advice on financial planning, investment strategies, and retirement planning."
},
{
"label": "Language Tutor",
"value": "A teacher who helps individuals learn and improve their language skills."
},
{
"label": "Travel Guide",
"value": "A professional who leads tours and provides information about local attractions and culture."
},
{
"label": "Marketing Expert",
"value": "A professional who develops and implements marketing strategies to promote products and services."
},
{
"label": "Software Developer",
"value": "A professional who designs, develops, and maintains software applications and systems."
},
{
"label": "Dating Coach",
"value": "A professional who helps individuals improve their dating and relationship skills."
},
{
"label": "DIY Expert",
"value": "A person who is skilled at completing a wide range of do-it-yourself projects around the home."
},
{
"label": "Journalist",
"value": "A professional who investigates and reports on current events and news stories."
},
{
"label": "Tech Writer",
"value": "A professional who writes about technology and related topics for a variety of audiences."
},
{
"label": "Professional Chef",
"value": "A skilled culinary professional who prepares meals and manages kitchen operations."
},
{
"label": "Professional Salesperson",
"value": "A professional who sells products and services to businesses and consumers."
},
{
"label": "Startup Tech Lawyer",
"value": "A legal professional who specializes in providing legal advice and services to startup technology companies."
},
{
"label": "Graphic Designer",
"value": "A professional who designs visual materials such as logos, brochures, and websites."
},
{
"label": "Academic Researcher",
"value": "A professional who conducts research and produces scholarly work in a particular academic field."
},
{
"label": "Customer Support Agent",
"value": "A professional who provides assistance and support to customers who have questions or problems with a company's products or services."
},
{
"label": "HR Consultant",
"value": "A professional who provides guidance and advice to organizations on human resource management and strategy"
}
],
"writingTones": [
"Assertive",
"Authoritative",
"Casual",
"Confident",
"Condescending",
"Conversational",
"Diplomatic",
"Direct",
"Eloquent",
"Formal",
"Friendly",
"Humorous",
"Informative",
"Inspiring",
"Intense",
"Irritable",
"Joking",
"Polite",
"Sarcastic",
"Sincere",
"Soothing",
"Stern",
"Sympathetic",
"Tactful",
"Witty"
],
"writingStyles": [
"Academic",
"Analytical",
"Argumentative",
"Conversational",
"Creative",
"Critical",
"Descriptive",
"Explanatory",
"Informative",
"Instructive",
"Investigative",
"Journalistic",
"Metaphorical",
"Narrative",
"Persuasive",
"Poetic",
"Satirical",
"Technical"
],
"writingFormats": [
{
"value": "Answer as concise as possible",
"label": "Concise"
},
{
"value": "Think step-by-step",
"label": "Step-by-step"
},
{
"value": "Answer in painstakingly detail",
"label": "Extreme Detail"
},
{
"value": "Explain like I'm five",
"label": "Explain Like I'm Five"
}
],
"showDownloadLink": true,
"allowDarkModeToggle": true,
"allowSettingsModal": true,
"allowDatabaseModal": true,
"showTwitterLink": true,
"showFeedbackLink": true
}

42
src/utils/config.ts Normal file
View file

@ -0,0 +1,42 @@
interface Config {
defaultModel: AvailableModel["value"];
defaultType: 'openai' | 'custom';
defaultAuth: 'none' | 'bearer-token' | 'api-key';
defaultBase: string;
defaultVersion: string;
defaultKey: string;
availableModels: AvailableModel[];
writingCharacters: WritingCharacter[];
writingTones: string[];
writingStyles: string[];
writingFormats: WritingFormat[];
showDownloadLink: boolean;
allowDarkModeToggle: boolean;
allowSettingsModal: boolean;
allowDatabaseModal: boolean;
showTwitterLink: boolean;
showFeedbackLink: boolean;
}
interface AvailableModel {
value: string;
label: string;
}
interface WritingCharacter {
label: string;
value: string;
}
interface WritingFormat {
value: string;
label: string;
}
export let config: Config;
export const loadConfig = async () => {
const response = await fetch("config.json");
config = await response.json();
return config;
};

View file

@ -1,200 +0,0 @@
export const defaultModel = "gpt-3.5-turbo";
export const availableModels = [
{
value: "gpt-3.5-turbo",
label: "GPT-3.5-TURBO (Default ChatGPT)",
},
{ value: "gpt-3.5-turbo-0301", label: "GPT-3.5-TURBO-0301" },
{ value: "gpt-4", label: "GPT-4 (Limited Beta)" },
{ value: "gpt-4-0314", label: "GPT-4-0314 (Limited Beta)" },
{ value: "gpt-4-32k", label: "GPT-4-32K (Limited Beta)" },
{
value: "gpt-4-32k-0314",
label: "GPT-4-32K-0314 (Limited Beta)",
},
];
export const writingCharacters = [
{
label: "Standup Comedian",
value:
"A performer who entertains audiences by telling jokes and humorous stories.",
},
{
label: "Life Coach",
value:
"A professional who helps individuals identify and achieve their personal and professional goals.",
},
{
label: "Career Counselor",
value:
"A professional who helps individuals explore and choose careers, develop job search strategies, and improve job performance.",
},
{
label: "Nutritionist",
value:
"A health professional who specializes in the study of nutrition and its effects on the body.",
},
{
label: "Product Manager",
value:
"A professional who oversees the development and marketing of a company's products.",
},
{
label: "Personal Trainer",
value:
"A fitness professional who works with individuals to develop personalized exercise programs and improve their overall health and fitness.",
},
{
label: "Life Hacker",
value:
"A person who uses unconventional methods to solve problems and increase productivity in everyday life.",
},
{
label: "Travel Advisor",
value:
"A professional who helps individuals plan and book travel arrangements.",
},
{
label: "Mindfulness Coach",
value:
"A professional who helps individuals develop mindfulness practices to reduce stress and improve well-being.",
},
{
label: "Financial Advisor",
value:
"A professional who provides guidance and advice on financial planning, investment strategies, and retirement planning.",
},
{
label: "Language Tutor",
value:
"A teacher who helps individuals learn and improve their language skills.",
},
{
label: "Travel Guide",
value:
"A professional who leads tours and provides information about local attractions and culture.",
},
{
label: "Marketing Expert",
value:
"A professional who develops and implements marketing strategies to promote products and services.",
},
{
label: "Software Developer",
value:
"A professional who designs, develops, and maintains software applications and systems.",
},
{
label: "Dating Coach",
value:
"A professional who helps individuals improve their dating and relationship skills.",
},
{
label: "DIY Expert",
value:
"A person who is skilled at completing a wide range of do-it-yourself projects around the home.",
},
{
label: "Journalist",
value:
"A professional who investigates and reports on current events and news stories.",
},
{
label: "Tech Writer",
value:
"A professional who writes about technology and related topics for a variety of audiences.",
},
{
label: "Professional Chef",
value:
"A skilled culinary professional who prepares meals and manages kitchen operations.",
},
{
label: "Professional Salesperson",
value:
"A professional who sells products and services to businesses and consumers.",
},
{
label: "Startup Tech Lawyer",
value:
"A legal professional who specializes in providing legal advice and services to startup technology companies.",
},
{
label: "Graphic Designer",
value:
"A professional who designs visual materials such as logos, brochures, and websites.",
},
{
label: "Academic Researcher",
value:
"A professional who conducts research and produces scholarly work in a particular academic field.",
},
{
label: "Customer Support Agent",
value:
"A professional who provides assistance and support to customers who have questions or problems with a company's products or services.",
},
{
label: "HR Consultant",
value:
"A professional who provides guidance and advice to organizations on human resource management and strategy",
},
];
export const writingTones = [
"Assertive",
"Authoritative",
"Casual",
"Confident",
"Condescending",
"Conversational",
"Diplomatic",
"Direct",
"Eloquent",
"Formal",
"Friendly",
"Humorous",
"Informative",
"Inspiring",
"Intense",
"Irritable",
"Joking",
"Polite",
"Sarcastic",
"Sincere",
"Soothing",
"Stern",
"Sympathetic",
"Tactful",
"Witty",
];
export const writingStyles = [
"Academic",
"Analytical",
"Argumentative",
"Conversational",
"Creative",
"Critical",
"Descriptive",
"Explanatory",
"Informative",
"Instructive",
"Investigative",
"Journalistic",
"Metaphorical",
"Narrative",
"Persuasive",
"Poetic",
"Satirical",
"Technical",
];
export const writingFormats = [
{ value: "Answer as concise as possible", label: "Concise" },
{ value: "Think step-by-step", label: "Step-by-step" },
{ value: "Answer in painstakingly detail", label: "Extreme Detail" },
{ value: "Explain like I'm five", label: "Explain Like I'm Five" },
];

View file

@ -1,12 +1,21 @@
import { encode } from "gpt-token-utils";
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"; import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
import { db } from "../db";
import { defaultModel } from "./constants";
import { OpenAIExt } from "openai-ext"; import { OpenAIExt } from "openai-ext";
import { encode } from 'gpt-token-utils' import { db } from "../db";
import { config } from "./config";
function getClient(apiKey: string) { function getClient(
apiKey: string,
apiType: string,
apiAuth: string,
basePath: string
) {
const configuration = new Configuration({ const configuration = new Configuration({
apiKey, ...((apiType === "openai" ||
(apiType === "custom" && apiAuth === "bearer-token")) && {
apiKey: apiKey,
}),
...(apiType === "custom" && { basePath: basePath }),
}); });
return new OpenAIApi(configuration); return new OpenAIApi(configuration);
} }
@ -15,27 +24,26 @@ export async function createStreamChatCompletion(
apiKey: string, apiKey: string,
messages: ChatCompletionRequestMessage[], messages: ChatCompletionRequestMessage[],
chatId: string, chatId: string,
messageId: string, messageId: string
) { ) {
const settings = await db.settings.get("general"); const settings = await db.settings.get("general");
const model = settings?.openAiModel ?? defaultModel; const model = settings?.openAiModel ?? config.defaultModel;
return OpenAIExt.streamClientChatCompletion( return OpenAIExt.streamClientChatCompletion(
{ {
model, model,
messages messages,
}, },
{ {
apiKey: apiKey, apiKey: apiKey,
handler: { handler: {
onContent(content, isFinal, stream) { onContent(content, isFinal, stream) {
setStreamContent(messageId, content, isFinal); setStreamContent(messageId, content, isFinal);
if(isFinal){ if (isFinal) {
setTotalTokens(chatId, content); setTotalTokens(chatId, content);
} }
}, },
onDone(stream) { onDone(stream) {},
},
onError(error, stream) { onError(error, stream) {
console.error(error); console.error(error);
}, },
@ -44,12 +52,16 @@ export async function createStreamChatCompletion(
); );
} }
function setStreamContent(messageId:string, content:string, isFinal:boolean){ function setStreamContent(
content = (isFinal ? content : content + "█") messageId: string,
db.messages.update(messageId, {content: content}) content: string,
isFinal: boolean
) {
content = isFinal ? content : content + "█";
db.messages.update(messageId, { content: content });
} }
function setTotalTokens(chatId:string, content:string){ function setTotalTokens(chatId: string, content: string) {
let total_tokens = encode(content).length; let total_tokens = encode(content).length;
db.chats.where({ id: chatId }).modify((chat) => { db.chats.where({ id: chatId }).modify((chat) => {
if (chat.totalTokens) { if (chat.totalTokens) {
@ -65,14 +77,29 @@ export async function createChatCompletion(
messages: ChatCompletionRequestMessage[] messages: ChatCompletionRequestMessage[]
) { ) {
const settings = await db.settings.get("general"); const settings = await db.settings.get("general");
const model = settings?.openAiModel ?? defaultModel; const model = settings?.openAiModel ?? config.defaultModel;
const type = settings?.openAiApiType ?? config.defaultType;
const auth = settings?.openAiApiAuth ?? config.defaultAuth;
const base = settings?.openAiApiBase ?? config.defaultBase;
const version = settings?.openAiApiVersion ?? config.defaultVersion;
const client = getClient(apiKey); const client = getClient(apiKey, type, auth, base);
return client.createChatCompletion({ return client.createChatCompletion(
model, {
stream: false, model,
messages, stream: false,
}); messages,
},
{
headers: {
"Content-Type": "application/json",
...(type === "custom" && auth === "api-key" && { "api-key": apiKey }),
},
params: {
...(type === "custom" && { "api-version": version }),
},
}
);
} }
export async function checkOpenAIKey(apiKey: string) { export async function checkOpenAIKey(apiKey: string) {

5124
yarn.lock Normal file

File diff suppressed because it is too large Load diff