feat: customizable and reusable prompts

This commit is contained in:
BetaHuhn 2023-08-30 15:11:26 +02:00
parent 19db10b1b0
commit 0d265e51ff
8 changed files with 412 additions and 171 deletions

View file

@ -2,6 +2,8 @@ import {
ActionIcon,
Button,
Modal,
Select,
SimpleGrid,
Stack,
Textarea,
TextInput,
@ -13,31 +15,45 @@ import { IconPlaylistAdd, IconPlus } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { detaDB, generateKey, Prompt } from "../db";
import { usePrompts } from "../hooks/contexts";
import { config } from "../utils/config";
export function CreatePromptModal({ content }: { content?: string }) {
export function CreatePromptModal({ content, title: titleProp, open: openProp }: { content?: string, title?: string, open?: boolean }) {
const [opened, { open, close }] = useDisclosure(false);
const [submitting, setSubmitting] = useState(false);
const { setPrompts } = usePrompts()
const [writingCharacter, setWritingCharacter] = useState<string | null>(null);
const [writingTone, setWritingTone] = useState<string | null>(null);
const [writingStyle, setWritingStyle] = useState<string | null>(null);
const [writingFormat, setWritingFormat] = useState<string | null>(null);
const [value, setValue] = useState("");
const [title, setTitle] = useState("");
useEffect(() => {
setValue(content ?? "");
}, [content]);
setTitle(titleProp ?? "");
if (openProp) {
open()
}
}, [content, titleProp, openProp]);
return (
<>
{content ? (
<Tooltip label="Save Prompt" position="top">
<ActionIcon onClick={open}>
<IconPlaylistAdd opacity={0.5} size={20} />
</ActionIcon>
</Tooltip>
) : (
<Button fullWidth onClick={open} leftIcon={<IconPlus size={20} />}>
New Prompt
</Button>
{!openProp && (
<>
{content ? (
<Tooltip label="Save Prompt" position="top">
<ActionIcon onClick={open}>
<IconPlaylistAdd opacity={0.5} size={20} />
</ActionIcon>
</Tooltip>
) : (
<Button fullWidth onClick={open} leftIcon={<IconPlus size={20} />}>
New Prompt
</Button>
)}
</>
)}
<Modal opened={opened} onClose={close} title="Create Prompt" size="lg">
<form
@ -49,6 +65,10 @@ export function CreatePromptModal({ content }: { content?: string }) {
const item = await detaDB.prompts.put({
title,
content: value,
writingCharacter,
writingTone,
writingStyle,
writingFormat,
createdAt: new Date().toISOString(),
}, generateKey())
@ -62,6 +82,10 @@ export function CreatePromptModal({ content }: { content?: string }) {
setTitle("")
setValue("")
setWritingCharacter(null)
setWritingTone(null)
setWritingStyle(null)
setWritingFormat(null)
close();
} catch (error: any) {
@ -88,13 +112,62 @@ export function CreatePromptModal({ content }: { content?: string }) {
<Stack>
<TextInput
label="Title"
placeholder="Prompt title"
value={title}
onChange={(event) => setTitle(event.currentTarget.value)}
formNoValidate
data-autofocus
/>
<SimpleGrid
spacing="xs"
breakpoints={[
{ minWidth: "sm", cols: 4 },
{ maxWidth: "sm", cols: 2 },
]}
>
<Select
value={writingCharacter}
onChange={setWritingCharacter}
data={config.writingCharacters}
placeholder="Character"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
<Select
value={writingTone}
onChange={setWritingTone}
data={config.writingTones}
placeholder="Tone"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
<Select
value={writingStyle}
onChange={setWritingStyle}
data={config.writingStyles}
placeholder="Style"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
<Select
value={writingFormat}
onChange={setWritingFormat}
data={config.writingFormats}
placeholder="Format"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
</SimpleGrid>
<Textarea
placeholder="Content"
placeholder="Further instructions..."
autosize
minRows={5}
maxRows={10}

View file

@ -2,6 +2,8 @@ import {
ActionIcon,
Button,
Modal,
Select,
SimpleGrid,
Stack,
Textarea,
TextInput,
@ -13,6 +15,7 @@ import { IconPencil } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import { detaDB, Prompt } from "../db";
import { usePrompts } from "../hooks/contexts";
import { config } from "../utils/config";
export function EditPromptModal({ prompt }: { prompt: Prompt }) {
const [opened, { open, close }] = useDisclosure(false);
@ -20,11 +23,20 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
const { setPrompts } = usePrompts()
const [writingCharacter, setWritingCharacter] = useState<string | null>(null);
const [writingTone, setWritingTone] = useState<string | null>(null);
const [writingStyle, setWritingStyle] = useState<string | null>(null);
const [writingFormat, setWritingFormat] = useState<string | null>(null);
const [value, setValue] = useState("");
const [title, setTitle] = useState("");
useEffect(() => {
setValue(prompt?.content ?? "");
setTitle(prompt?.title ?? "");
setWritingCharacter(prompt?.writingCharacter ?? null);
setWritingTone(prompt?.writingTone ?? null);
setWritingStyle(prompt?.writingStyle ?? null);
setWritingFormat(prompt?.writingFormat ?? null);
}, [prompt]);
return (
@ -36,10 +48,21 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
setSubmitting(true);
event.preventDefault();
await detaDB.prompts.update({ title: title, content: value }, prompt.key)
const updates: Partial<Prompt> = {
title,
content: value,
writingCharacter,
writingFormat,
writingStyle,
writingTone
}
console.log({ updates })
await detaDB.prompts.update(updates, prompt.key)
setPrompts(current => (current || []).map(item => {
if (item.key === prompt.key) {
return { ...item, title, content: value };
return { ...item, ...updates };
}
return item;
@ -80,8 +103,57 @@ export function EditPromptModal({ prompt }: { prompt: Prompt }) {
formNoValidate
data-autofocus
/>
<SimpleGrid
spacing="xs"
breakpoints={[
{ minWidth: "sm", cols: 4 },
{ maxWidth: "sm", cols: 2 },
]}
>
<Select
value={writingCharacter}
onChange={setWritingCharacter}
data={config.writingCharacters}
placeholder="Character"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
<Select
value={writingTone}
onChange={setWritingTone}
data={config.writingTones}
placeholder="Tone"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
<Select
value={writingStyle}
onChange={setWritingStyle}
data={config.writingStyles}
placeholder="Style"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
<Select
value={writingFormat}
onChange={setWritingFormat}
data={config.writingFormats}
placeholder="Format"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
</SimpleGrid>
<Textarea
label="Content"
placeholder="Further instructions..."
autosize
minRows={5}
maxRows={10}

View file

@ -4,7 +4,6 @@ import {
Box,
Burger,
Button,
Center,
Header,
MediaQuery,
Navbar,
@ -17,14 +16,9 @@ import {
useMantineTheme,
} from "@mantine/core";
import {
IconBrandGithub,
IconBrandTwitter,
IconMessage,
IconMoonStars,
IconPlus,
IconSearch,
IconSettings,
IconSunHigh,
IconX,
} from "@tabler/icons-react";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-location";
@ -49,7 +43,7 @@ export function Layout() {
const theme = useMantineTheme();
const [opened, setOpened] = useState(false);
const [tab, setTab] = useState<"Chats" | "Prompts">("Chats");
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const { colorScheme } = useMantineColorScheme();
const navigate = useNavigate();
const router = useRouter();
@ -140,7 +134,8 @@ export function Layout() {
height: 60,
display: "flex",
alignItems: "center",
justifyContent: "center",
justifyContent: "space-between",
padding: 10,
borderBottom: border,
}}
>
@ -157,16 +152,29 @@ export function Layout() {
}}
/>
</Link>
<MediaQuery largerThan="md" styles={{ display: "none" }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size="sm"
color={theme.colors.gray[6]}
className="app-region-no-drag"
sx={{ position: "fixed", right: 16 }}
/>
</MediaQuery>
<Box
style={{
display: "flex",
alignItems: "center"
}}
>
<SettingsModal>
<Tooltip label="Settings">
<ActionIcon size="xl">
<IconSettings size={20} />
</ActionIcon>
</Tooltip>
</SettingsModal>
<MediaQuery largerThan="md" styles={{ display: "none" }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size="sm"
color={theme.colors.gray[6]}
className="app-region-no-drag"
/>
</MediaQuery>
</Box>
</Box>
</Navbar.Section>
<Navbar.Section
@ -185,36 +193,6 @@ export function Layout() {
onChange={(value) => setTab(value as typeof tab)}
data={["Chats", "Prompts"]}
/>
<Box sx={{ padding: 4 }}>
{tab === "Chats" && (
<Button
fullWidth
leftIcon={<IconPlus size={20} />}
onClick={async () => {
const item = await detaDB.chats.put({
description: "New Chat",
totalTokens: 0,
createdAt: new Date().toISOString(),
}, generateKey())
setChats(chats => ([...(chats || []), item as unknown as Chat]))
const id = item!.key as string
// const id = nanoid();
// db.chats.add({
// id,
// description: "New Chat",
// totalTokens: 0,
// createdAt: new Date(),
// });
navigate({ to: `/chats/${id}`, replace: true });
}}
>
New Chat
</Button>
)}
{tab === "Prompts" && <CreatePromptModal />}
</Box>
</Navbar.Section>
<Navbar.Section
sx={(theme) => ({
@ -251,7 +229,39 @@ export function Layout() {
<Prompts search={search} onPlay={() => setTab("Chats")} />
)}
</Navbar.Section>
<Navbar.Section sx={{ borderTop: border }} p="xs">
<Navbar.Section>
<Box sx={{ padding: 10 }}>
{tab === "Chats" && (
<Button
fullWidth
leftIcon={<IconPlus size={20} />}
onClick={async () => {
const item = await detaDB.chats.put({
description: "New Chat",
totalTokens: 0,
createdAt: new Date().toISOString(),
}, generateKey())
setChats(chats => ([...(chats || []), item as unknown as Chat]))
const id = item!.key as string
// const id = nanoid();
// db.chats.add({
// id,
// description: "New Chat",
// totalTokens: 0,
// createdAt: new Date(),
// });
navigate({ to: `/chats/${id}`, replace: true });
}}
>
New Chat
</Button>
)}
{tab === "Prompts" && <CreatePromptModal />}
</Box>
</Navbar.Section>
{/* <Navbar.Section sx={{ borderTop: border }} p="xs">
<Center>
{config.allowDarkModeToggle && (
<Tooltip
@ -327,7 +337,7 @@ export function Layout() {
</Tooltip>
)}
</Center>
</Navbar.Section>
</Navbar.Section> */}
</Navbar>
}
header={

View file

@ -3,7 +3,6 @@ import { IconPlayerPlay } from "@tabler/icons-react";
import { useNavigate } from "@tanstack/react-location";
import { useMemo } from "react";
import { Chat, detaDB, generateKey } from "../db";
import { createChatCompletion } from "../utils/openai";
import { DeletePromptModal } from "./DeletePromptModal";
import { EditPromptModal } from "./EditPromptModal";
import { useChats, usePrompts, useSettings } from "../hooks/contexts";
@ -75,7 +74,7 @@ export function Prompts({
overflow: "hidden",
}}
>
{prompt.content}
{prompt.content || prompt.writingCharacter || prompt.writingTone || prompt.writingFormat || prompt.writingStyle}
</Text>
</Box>
<Group spacing="none">
@ -86,7 +85,13 @@ export function Prompts({
if (!settings?.openAiApiKey) return;
const item = await detaDB.chats.put({
description: "New Chat",
description: prompt.title ? `New ${prompt.title} Chat` : "New Chat",
prompt: prompt.key,
writingInstructions: prompt.content,
writingCharacter: prompt.writingCharacter,
writingTone: prompt.writingTone,
writingStyle: prompt.writingStyle,
writingFormat: prompt.writingFormat,
totalTokens: 0,
createdAt: new Date().toISOString(),
}, generateKey())
@ -94,47 +99,47 @@ export function Prompts({
const chat = item as unknown as Chat
setChats(current => ([...(current || []), chat]))
await detaDB.messages.put({
chatId: chat.key,
content: prompt.content,
role: "user",
createdAt: new Date().toISOString(),
}, generateKey())
// const systemMessage = getSystemMessage(prompt);
// await detaDB.messages.put({
// chatId: chat.key,
// content: systemMessage,
// role: "system",
// createdAt: new Date().toISOString(),
// }, generateKey())
navigate({ to: `/chats/${chat.key}` });
onPlay();
const result = await createChatCompletion(settings, [
{
role: "system",
content:
"You are ChatGPT, a large language model trained by OpenAI.",
},
{ role: "user", content: prompt.content },
]);
// const result = await createChatCompletion(settings, [
// {
// role: "system",
// content: systemMessage,
// },
// ]);
const resultDescription =
result.data.choices[0].message?.content;
// const resultDescription =
// result.data.choices[0].message?.content;
await detaDB.messages.put({
chatId: chat.key,
content: resultDescription ?? "unknown reponse",
role: "assistant",
createdAt: new Date().toISOString(),
}, generateKey())
// await detaDB.messages.put({
// chatId: chat.key,
// content: resultDescription ?? "unknown reponse",
// role: "assistant",
// createdAt: new Date().toISOString(),
// }, generateKey())
if (result.data.usage) {
// todo: add to chat totalTokens
await detaDB.chats.update({ totalTokens: result.data.usage!.total_tokens }, chat.key)
// if (result.data.usage) {
// // todo: add to chat totalTokens
// await detaDB.chats.update({ totalTokens: result.data.usage!.total_tokens }, chat.key)
// await db.chats.where({ id: chat.key }).modify((chat) => {
// if (chat.totalTokens) {
// chat.totalTokens += result.data.usage!.total_tokens;
// } else {
// chat.totalTokens = result.data.usage!.total_tokens;
// }
// });
}
// // await db.chats.where({ id: chat.key }).modify((chat) => {
// // if (chat.totalTokens) {
// // chat.totalTokens += result.data.usage!.total_tokens;
// // } else {
// // chat.totalTokens = result.data.usage!.total_tokens;
// // }
// // });
// }
}}
>
<IconPlayerPlay size={20} />

View file

@ -201,6 +201,10 @@
"Technical"
],
"writingFormats": [
{
"value": "Answer with a One-Liner",
"label": "One-Liner"
},
{
"value": "Answer as concise as possible",
"label": "Concise"

View file

@ -6,7 +6,13 @@ export interface Chat {
key: string;
description: string;
totalTokens: number;
createdAt: Date;
prompt?: string;
writingInstructions?: string | null;
writingCharacter?: string | null;
writingTone?: string | null;
writingStyle?: string | null;
writingFormat?: string | null;
createdAt: string;
}
export interface Message {
@ -14,14 +20,18 @@ export interface Message {
chatId: string;
role: "system" | "assistant" | "user";
content: string;
createdAt: Date;
createdAt: string;
}
export interface Prompt {
key: string;
title: string;
content: string;
createdAt: Date;
writingCharacter?: string | null;
writingTone?: string | null;
writingStyle?: string | null;
writingFormat?: string | null;
createdAt: string;
}
export interface Settings {

View file

@ -6,7 +6,6 @@ import {
Flex,
MediaQuery,
Select,
SimpleGrid,
Skeleton,
Stack,
Textarea,
@ -15,20 +14,23 @@ import { notifications } from "@mantine/notifications";
import { KeyboardEvent, useState, type ChangeEvent, useEffect } from "react";
import { AiOutlineSend } from "react-icons/ai";
import { MessageItem } from "../components/MessageItem";
import { Chat, Message, detaDB, generateKey } from "../db";
import { Chat, Message, Prompt, detaDB, generateKey } from "../db";
import { useChatId } from "../hooks/useChatId";
import { config } from "../utils/config";
import {
createChatCompletion,
createStreamChatCompletion,
getSystemMessage,
} from "../utils/openai";
import { useChat, useChats, useSettings } from "../hooks/contexts";
import { useChat, useChats, usePrompts, useSettings } from "../hooks/contexts";
import { CreatePromptModal } from "../components/CreatePromptModal";
export function ChatRoute() {
const chatId = useChatId();
const { settings } = useSettings()
const { prompts } = usePrompts()
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
@ -54,6 +56,8 @@ export function ChatRoute() {
const [content, setContent] = useState("");
const [contentDraft, setContentDraft] = useState("");
const [submitting, setSubmitting] = useState(false);
const [promptKey, setPromptKey] = useState<string | null>(null);
const [newPromptTitle, setNewPromptTitle] = useState<string | null>(null);
const { setChats } = useChats()
const { chat, setChat } = useChat()
@ -61,8 +65,13 @@ export function ChatRoute() {
useEffect(() => {
const dataFetch = async () => {
const item = await detaDB.chats.get(chatId!);
const fetchedChat = item as unknown as Chat
setChat(item as unknown as Chat);
setChat(fetchedChat);
if (fetchedChat.prompt) {
setPromptKey(fetchedChat.prompt)
}
};
if (!chat) {
@ -75,24 +84,6 @@ export function ChatRoute() {
// return db.chats.get(chatId);
// }, [chatId]);
const [writingCharacter, setWritingCharacter] = useState<string | null>(null);
const [writingTone, setWritingTone] = useState<string | null>(null);
const [writingStyle, setWritingStyle] = useState<string | null>(null);
const [writingFormat, setWritingFormat] = useState<string | null>(null);
const getSystemMessage = () => {
const message: string[] = [];
if (writingCharacter) message.push(`You are ${writingCharacter}.`);
if (writingTone) message.push(`Respond in ${writingTone} tone.`);
if (writingStyle) message.push(`Respond in ${writingStyle} style.`);
if (writingFormat) message.push(writingFormat);
if (message.length === 0)
message.push(
"You are ChatGPT, a large language model trained by OpenAI."
);
return message.join(" ");
};
const submit = async () => {
if (submitting) return;
@ -117,6 +108,42 @@ export function ChatRoute() {
try {
setSubmitting(true);
let systemMessageValue = ""
if (promptKey) {
const item = await detaDB.prompts.get(promptKey);
if (item) {
const prompt = item as unknown as Prompt
systemMessageValue = getSystemMessage({
content: prompt.content,
character: prompt?.writingCharacter ?? undefined,
tone: prompt?.writingTone ?? undefined,
style: prompt?.writingStyle ?? undefined,
format: prompt?.writingFormat ?? undefined,
})
const updates = {
writingInstructions: prompt.content,
writingCharacter: prompt.writingCharacter,
writingTone: prompt.writingTone,
writingStyle: prompt.writingStyle,
writingFormat: prompt.writingFormat,
}
setChat(chat => ({ ...chat!, ...updates }))
await detaDB.chats.update(updates, chatId)
}
}
if (!systemMessageValue) {
systemMessageValue = getSystemMessage({
content: chat?.writingInstructions ?? undefined,
character: chat?.writingCharacter ?? undefined,
tone: chat?.writingTone ?? undefined,
style: chat?.writingStyle ?? undefined,
format: chat?.writingFormat ?? undefined,
})
}
const userMessage = await detaDB.messages.put({
chatId,
content,
@ -134,6 +161,7 @@ export function ChatRoute() {
// createdAt: new Date(),
// });
setContent("");
setPromptKey(null);
const systemMessage = await detaDB.messages.put({
chatId,
@ -159,7 +187,7 @@ export function ChatRoute() {
[
{
role: "system",
content: getSystemMessage(),
content: systemMessageValue,
},
...(messages ?? []).map((message) => ({
role: message.role,
@ -191,7 +219,7 @@ export function ChatRoute() {
const createChatDescription = await createChatCompletion(settings, [
{
role: "system",
content: getSystemMessage(),
content: systemMessageValue,
},
...(messages ?? []).map((message) => ({
role: message.role,
@ -329,55 +357,80 @@ export function ChatRoute() {
>
<Container>
{messages?.length === 0 && (
<SimpleGrid
<Box
mb="sm"
spacing="xs"
breakpoints={[
{ minWidth: "sm", cols: 4 },
{ maxWidth: "sm", cols: 2 },
]}
style={{
display: "flex",
justifyContent: "flex-end",
}}
>
<Select
value={writingCharacter}
onChange={setWritingCharacter}
data={config.writingCharacters}
placeholder="Character"
value={promptKey}
onChange={setPromptKey}
data={prompts.map(prompt => ({ value: prompt.key, label: prompt.title }))}
placeholder="Select Prompt"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
creatable
getCreateLabel={(query) => `+ Create "${query}" Prompt`}
onCreate={(query) => {
setNewPromptTitle(query)
return query;
}}
/>
<Select
value={writingTone}
onChange={setWritingTone}
data={config.writingTones}
placeholder="Tone"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
<Select
value={writingStyle}
onChange={setWritingStyle}
data={config.writingStyles}
placeholder="Style"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
<Select
value={writingFormat}
onChange={setWritingFormat}
data={config.writingFormats}
placeholder="Format"
variant="filled"
searchable
clearable
sx={{ flex: 1 }}
/>
</SimpleGrid>
{newPromptTitle && <CreatePromptModal title={newPromptTitle} open={true} />}
</Box>
// <SimpleGrid
// mb="sm"
// spacing="xs"
// breakpoints={[
// { minWidth: "sm", cols: 4 },
// { maxWidth: "sm", cols: 2 },
// ]}
// >
// <Select
// value={writingCharacter}
// onChange={setWritingCharacter}
// data={config.writingCharacters}
// placeholder="Character"
// variant="filled"
// searchable
// clearable
// sx={{ flex: 1 }}
// />
// <Select
// value={writingTone}
// onChange={setWritingTone}
// data={config.writingTones}
// placeholder="Tone"
// variant="filled"
// searchable
// clearable
// sx={{ flex: 1 }}
// />
// <Select
// value={writingStyle}
// onChange={setWritingStyle}
// data={config.writingStyles}
// placeholder="Style"
// variant="filled"
// searchable
// clearable
// sx={{ flex: 1 }}
// />
// <Select
// value={writingFormat}
// onChange={setWritingFormat}
// data={config.writingFormats}
// placeholder="Format"
// variant="filled"
// searchable
// clearable
// sx={{ flex: 1 }}
// />
// </SimpleGrid>
)}
<Flex gap="sm">
<Textarea

View file

@ -119,3 +119,17 @@ export async function checkOpenAIKey(settings: Settings) {
},
]);
}
export function getSystemMessage(prompt: { content?: string, character?: string, tone?: string, style?: string, format?: string }) {
const message: string[] = [];
if (prompt.character) message.push(`You are ${prompt.character}`);
if (prompt.tone) message.push(`Respond in ${prompt.tone.toLowerCase()} tone.`);
if (prompt.style) message.push(`Respond in ${prompt.style.toLowerCase()} style.`);
if (prompt.format) message.push(`${prompt.format.toLowerCase()}.`);
if (message.length === 0)
message.push(
"You are ChatGPT, a large language model trained by OpenAI."
);
if (prompt.content) message.push(prompt.content);
return message.join(" ");
};