forked from prehistoric-systems/chatpad
merged add config options
This commit is contained in:
parent
61f15eb82e
commit
e60272eefa
15 changed files with 5804 additions and 327 deletions
4
.parcelrc
Normal file
4
.parcelrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"extends": ["@parcel/config-default"],
|
||||
"reporters": ["...", "parcel-reporter-static-files-copy"]
|
||||
}
|
||||
|
|
@ -24,7 +24,13 @@ Crafted with love and care to provide the best experience possible.
|
|||
## 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
|
||||
|
|
|
|||
23
package-lock.json
generated
23
package-lock.json
generated
|
|
@ -44,7 +44,7 @@
|
|||
"@types/react-dom": "^18.0.11",
|
||||
"buffer": "^5.7.1",
|
||||
"parcel": "^2.8.3",
|
||||
"path-browserify": "^1.0.1",
|
||||
"parcel-reporter-static-files-copy": "^1.5.0",
|
||||
"process": "^0.11.10"
|
||||
}
|
||||
},
|
||||
|
|
@ -6776,6 +6776,18 @@
|
|||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
|
@ -12924,6 +12936,15 @@
|
|||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@
|
|||
"start": "parcel",
|
||||
"build": "parcel build"
|
||||
},
|
||||
"staticFiles": {
|
||||
"staticPath": "src/static"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/transformer-sass": "^2.8.3",
|
||||
"@types/downloadjs": "^1.4.3",
|
||||
|
|
@ -16,6 +19,7 @@
|
|||
"buffer": "^5.7.1",
|
||||
"parcel": "^2.8.3",
|
||||
"path-browserify": "^1.0.1",
|
||||
"parcel-reporter-static-files-copy": "^1.5.0",
|
||||
"process": "^0.11.10"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ import { DatabaseModal } from "./DatabaseModal";
|
|||
import { LogoText } from "./Logo";
|
||||
import { Prompts } from "./Prompts";
|
||||
import { SettingsModal } from "./SettingsModal";
|
||||
import { config } from "../utils/config";
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
|
|
@ -186,35 +187,41 @@ export function Layout() {
|
|||
</Navbar.Section>
|
||||
<Navbar.Section sx={{ borderTop: border }} p="xs">
|
||||
<Center>
|
||||
<Tooltip
|
||||
label={colorScheme === "dark" ? "Light Mode" : "Dark Mode"}
|
||||
>
|
||||
<ActionIcon
|
||||
sx={{ flex: 1 }}
|
||||
size="xl"
|
||||
onClick={() => toggleColorScheme()}
|
||||
{config.allowDarkModeToggle && (
|
||||
<Tooltip
|
||||
label={colorScheme === "dark" ? "Light Mode" : "Dark Mode"}
|
||||
>
|
||||
{colorScheme === "dark" ? (
|
||||
<IconSunHigh size={20} />
|
||||
) : (
|
||||
<IconMoonStars size={20} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<SettingsModal>
|
||||
<Tooltip label="Settings">
|
||||
<ActionIcon sx={{ flex: 1 }} size="xl">
|
||||
<IconSettings size={20} />
|
||||
<ActionIcon
|
||||
sx={{ flex: 1 }}
|
||||
size="xl"
|
||||
onClick={() => toggleColorScheme()}
|
||||
>
|
||||
{colorScheme === "dark" ? (
|
||||
<IconSunHigh size={20} />
|
||||
) : (
|
||||
<IconMoonStars size={20} />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</SettingsModal>
|
||||
<DatabaseModal>
|
||||
<Tooltip label="Database">
|
||||
<ActionIcon sx={{ flex: 1 }} size="xl">
|
||||
<IconDatabase size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</DatabaseModal>
|
||||
)}
|
||||
{config.allowSettingsModal && (
|
||||
<SettingsModal>
|
||||
<Tooltip label="Settings">
|
||||
<ActionIcon sx={{ flex: 1 }} size="xl">
|
||||
<IconSettings size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</SettingsModal>
|
||||
)}
|
||||
{config.allowDatabaseModal && (
|
||||
<DatabaseModal>
|
||||
<Tooltip label="Database">
|
||||
<ActionIcon sx={{ flex: 1 }} size="xl">
|
||||
<IconDatabase size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</DatabaseModal>
|
||||
)}
|
||||
<Tooltip label="Source Code">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
|
|
@ -226,36 +233,40 @@ export function Layout() {
|
|||
<IconBrandGithub size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Follow on Twitter">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href="https://twitter.com/deiucanta"
|
||||
target="_blank"
|
||||
sx={{ flex: 1 }}
|
||||
size="xl"
|
||||
>
|
||||
<IconBrandTwitter size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<Tooltip label="Give Feedback">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href="https://feedback.chatpad.ai"
|
||||
onClick={(event) => {
|
||||
if (window.todesktop) {
|
||||
event.preventDefault();
|
||||
window.todesktop.contents.openUrlInBrowser(
|
||||
"https://feedback.chatpad.ai"
|
||||
);
|
||||
}
|
||||
}}
|
||||
target="_blank"
|
||||
sx={{ flex: 1 }}
|
||||
size="xl"
|
||||
>
|
||||
<IconMessage size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
{config.showTwitterLink && (
|
||||
<Tooltip label="Follow on Twitter">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href="https://twitter.com/deiucanta"
|
||||
target="_blank"
|
||||
sx={{ flex: 1 }}
|
||||
size="xl"
|
||||
>
|
||||
<IconBrandTwitter size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
{config.showFeedbackLink && (
|
||||
<Tooltip label="Give Feedback">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href="https://feedback.chatpad.ai"
|
||||
onClick={(event) => {
|
||||
if (window.todesktop) {
|
||||
event.preventDefault();
|
||||
window.todesktop.contents.openUrlInBrowser(
|
||||
"https://feedback.chatpad.ai"
|
||||
);
|
||||
}
|
||||
}}
|
||||
target="_blank"
|
||||
sx={{ flex: 1 }}
|
||||
size="xl"
|
||||
>
|
||||
<IconMessage size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Center>
|
||||
</Navbar.Section>
|
||||
</Navbar>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
List,
|
||||
Modal,
|
||||
PasswordInput,
|
||||
TextInput,
|
||||
Select,
|
||||
Stack,
|
||||
Text,
|
||||
|
|
@ -15,7 +16,7 @@ import { notifications } from "@mantine/notifications";
|
|||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { cloneElement, ReactElement, useEffect, useState } from "react";
|
||||
import { db } from "../db";
|
||||
import { availableModels, defaultModel } from "../utils/constants";
|
||||
import { config } from "../utils/config";
|
||||
import { checkOpenAIKey } from "../utils/openai";
|
||||
|
||||
export function SettingsModal({ children }: { children: ReactElement }) {
|
||||
|
|
@ -23,7 +24,11 @@ export function SettingsModal({ children }: { children: ReactElement }) {
|
|||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
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 () => {
|
||||
return db.settings.where({ id: "general" }).first();
|
||||
|
|
@ -36,6 +41,18 @@ export function SettingsModal({ children }: { children: ReactElement }) {
|
|||
if (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]);
|
||||
|
||||
return (
|
||||
|
|
@ -111,20 +128,213 @@ export function SettingsModal({ children }: { children: ReactElement }) {
|
|||
</List.Item>
|
||||
</List>
|
||||
<Select
|
||||
label="OpenAI Model"
|
||||
value={model}
|
||||
onChange={(value) => {
|
||||
db.settings.update("general", {
|
||||
openAiModel: value ?? undefined,
|
||||
});
|
||||
label="OpenAI Type"
|
||||
value={type}
|
||||
onChange={async (value) => {
|
||||
setSubmitting(true);
|
||||
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
|
||||
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">
|
||||
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>
|
||||
<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>
|
||||
</Modal>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Dexie, { Table } from "dexie";
|
||||
import "dexie-export-import";
|
||||
import { config } from "../utils/config";
|
||||
|
||||
export interface Chat {
|
||||
id: string;
|
||||
|
|
@ -27,6 +28,10 @@ export interface Settings {
|
|||
id: "general";
|
||||
openAiApiKey?: string;
|
||||
openAiModel?: string;
|
||||
openAiApiType?: 'openai' | 'custom';
|
||||
openAiApiAuth?: 'none' | 'bearer-token' | 'api-key';
|
||||
openAiApiBase?: string;
|
||||
openAiApiVersion?: string;
|
||||
}
|
||||
|
||||
export class Database extends Dexie {
|
||||
|
|
@ -47,6 +52,12 @@ export class Database extends Dexie {
|
|||
this.on("populate", async () => {
|
||||
db.settings.add({
|
||||
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 }),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 { loadConfig } from "./utils/config";
|
||||
|
||||
const container = document.getElementById("app");
|
||||
const root = createRoot(container!);
|
||||
root.render(<App />);
|
||||
loadConfig().then(() => {
|
||||
const container = document.getElementById("app");
|
||||
const root = ReactDOM.createRoot(container!);
|
||||
root.render(<App />);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -19,13 +19,11 @@ import { AiOutlineSend } from "react-icons/ai";
|
|||
import { MessageItem } from "../components/MessageItem";
|
||||
import { db } from "../db";
|
||||
import { useChatId } from "../hooks/useChatId";
|
||||
import { config } from "../utils/config";
|
||||
import {
|
||||
writingCharacters,
|
||||
writingFormats,
|
||||
writingStyles,
|
||||
writingTones,
|
||||
} from "../utils/constants";
|
||||
import { createChatCompletion, createStreamChatCompletion } from "../utils/openai";
|
||||
createChatCompletion,
|
||||
createStreamChatCompletion,
|
||||
} from "../utils/openai";
|
||||
|
||||
export function ChatRoute() {
|
||||
const chatId = useChatId();
|
||||
|
|
@ -101,7 +99,7 @@ export function ChatRoute() {
|
|||
});
|
||||
setContent("");
|
||||
|
||||
const messageId = nanoid()
|
||||
const messageId = nanoid();
|
||||
await db.messages.add({
|
||||
id: messageId,
|
||||
chatId,
|
||||
|
|
@ -110,17 +108,22 @@ export function ChatRoute() {
|
|||
createdAt: new Date(),
|
||||
});
|
||||
|
||||
await createStreamChatCompletion(apiKey, [
|
||||
{
|
||||
role: "system",
|
||||
content: getSystemMessage(),
|
||||
},
|
||||
...(messages ?? []).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
})),
|
||||
{ role: "user", content },
|
||||
], chatId, messageId);
|
||||
await createStreamChatCompletion(
|
||||
apiKey,
|
||||
[
|
||||
{
|
||||
role: "system",
|
||||
content: getSystemMessage(),
|
||||
},
|
||||
...(messages ?? []).map((message) => ({
|
||||
role: message.role,
|
||||
content: message.content,
|
||||
})),
|
||||
{ role: "user", content },
|
||||
],
|
||||
chatId,
|
||||
messageId
|
||||
);
|
||||
|
||||
setSubmitting(false);
|
||||
|
||||
|
|
@ -260,7 +263,7 @@ export function ChatRoute() {
|
|||
<Select
|
||||
value={writingCharacter}
|
||||
onChange={setWritingCharacter}
|
||||
data={writingCharacters}
|
||||
data={config.writingCharacters}
|
||||
placeholder="Character"
|
||||
variant="filled"
|
||||
searchable
|
||||
|
|
@ -270,7 +273,7 @@ export function ChatRoute() {
|
|||
<Select
|
||||
value={writingTone}
|
||||
onChange={setWritingTone}
|
||||
data={writingTones}
|
||||
data={config.writingTones}
|
||||
placeholder="Tone"
|
||||
variant="filled"
|
||||
searchable
|
||||
|
|
@ -280,7 +283,7 @@ export function ChatRoute() {
|
|||
<Select
|
||||
value={writingStyle}
|
||||
onChange={setWritingStyle}
|
||||
data={writingStyles}
|
||||
data={config.writingStyles}
|
||||
placeholder="Style"
|
||||
variant="filled"
|
||||
searchable
|
||||
|
|
@ -290,7 +293,7 @@ export function ChatRoute() {
|
|||
<Select
|
||||
value={writingFormat}
|
||||
onChange={setWritingFormat}
|
||||
data={writingFormats}
|
||||
data={config.writingFormats}
|
||||
placeholder="Format"
|
||||
variant="filled"
|
||||
searchable
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import { useLiveQuery } from "dexie-react-hooks";
|
|||
import { Logo } from "../components/Logo";
|
||||
import { SettingsModal } from "../components/SettingsModal";
|
||||
import { db } from "../db";
|
||||
import { config } from "../utils/config";
|
||||
|
||||
export function IndexRoute() {
|
||||
const settings = useLiveQuery(() => db.settings.get("general"));
|
||||
|
|
@ -56,16 +57,18 @@ export function IndexRoute() {
|
|||
))}
|
||||
</SimpleGrid>
|
||||
<Group mt={50}>
|
||||
<SettingsModal>
|
||||
<Button
|
||||
size="md"
|
||||
variant={openAiApiKey ? "light" : "filled"}
|
||||
leftIcon={<IconKey size={20} />}
|
||||
>
|
||||
{openAiApiKey ? "Change OpenAI Key" : "Enter OpenAI Key"}
|
||||
</Button>
|
||||
</SettingsModal>
|
||||
{!window.todesktop && (
|
||||
{config.allowSettingsModal && (
|
||||
<SettingsModal>
|
||||
<Button
|
||||
size="md"
|
||||
variant={openAiApiKey ? "light" : "filled"}
|
||||
leftIcon={<IconKey size={20} />}
|
||||
>
|
||||
{openAiApiKey ? "Change OpenAI Key" : "Enter OpenAI Key"}
|
||||
</Button>
|
||||
</SettingsModal>
|
||||
)}
|
||||
{config.showDownloadLink && !window.todesktop && (
|
||||
<Button
|
||||
component="a"
|
||||
href="https://dl.todesktop.com/230313oyppkw40a"
|
||||
|
|
|
|||
207
src/static/config.json
Normal file
207
src/static/config.json
Normal 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
42
src/utils/config.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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" },
|
||||
];
|
||||
|
|
@ -1,12 +1,21 @@
|
|||
import { encode } from "gpt-token-utils";
|
||||
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
|
||||
import { db } from "../db";
|
||||
import { defaultModel } from "./constants";
|
||||
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({
|
||||
apiKey,
|
||||
...((apiType === "openai" ||
|
||||
(apiType === "custom" && apiAuth === "bearer-token")) && {
|
||||
apiKey: apiKey,
|
||||
}),
|
||||
...(apiType === "custom" && { basePath: basePath }),
|
||||
});
|
||||
return new OpenAIApi(configuration);
|
||||
}
|
||||
|
|
@ -15,27 +24,26 @@ export async function createStreamChatCompletion(
|
|||
apiKey: string,
|
||||
messages: ChatCompletionRequestMessage[],
|
||||
chatId: string,
|
||||
messageId: string,
|
||||
messageId: string
|
||||
) {
|
||||
const settings = await db.settings.get("general");
|
||||
const model = settings?.openAiModel ?? defaultModel;
|
||||
const model = settings?.openAiModel ?? config.defaultModel;
|
||||
|
||||
return OpenAIExt.streamClientChatCompletion(
|
||||
{
|
||||
model,
|
||||
messages
|
||||
messages,
|
||||
},
|
||||
{
|
||||
apiKey: apiKey,
|
||||
handler: {
|
||||
handler: {
|
||||
onContent(content, isFinal, stream) {
|
||||
setStreamContent(messageId, content, isFinal);
|
||||
if(isFinal){
|
||||
if (isFinal) {
|
||||
setTotalTokens(chatId, content);
|
||||
}
|
||||
},
|
||||
onDone(stream) {
|
||||
},
|
||||
onDone(stream) {},
|
||||
onError(error, stream) {
|
||||
console.error(error);
|
||||
},
|
||||
|
|
@ -44,12 +52,16 @@ export async function createStreamChatCompletion(
|
|||
);
|
||||
}
|
||||
|
||||
function setStreamContent(messageId:string, content:string, isFinal:boolean){
|
||||
content = (isFinal ? content : content + "█")
|
||||
db.messages.update(messageId, {content: content})
|
||||
function setStreamContent(
|
||||
messageId: string,
|
||||
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;
|
||||
db.chats.where({ id: chatId }).modify((chat) => {
|
||||
if (chat.totalTokens) {
|
||||
|
|
@ -65,14 +77,29 @@ export async function createChatCompletion(
|
|||
messages: ChatCompletionRequestMessage[]
|
||||
) {
|
||||
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);
|
||||
return client.createChatCompletion({
|
||||
model,
|
||||
stream: false,
|
||||
messages,
|
||||
});
|
||||
const client = getClient(apiKey, type, auth, base);
|
||||
return client.createChatCompletion(
|
||||
{
|
||||
model,
|
||||
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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue