├── .gitignore ├── banner.png ├── src ├── assets │ └── favicon.png ├── hooks │ ├── useApiKey.ts │ └── useChatId.ts ├── index.tsx ├── styles │ └── markdown.scss ├── components │ ├── ScrollIntoView.tsx │ ├── DeleteAllDataModal.tsx │ ├── DeleteChatsModal.tsx │ ├── CharacterCard.tsx │ ├── MainLink.tsx │ ├── Chats.tsx │ ├── EditChatModal.tsx │ ├── DeletePromptModal.tsx │ ├── DeleteChatModal.tsx │ ├── EditPromptModal.tsx │ ├── CreatePromptModal.tsx │ ├── App.tsx │ ├── MessageItem.tsx │ ├── SettingsModal.tsx │ ├── Prompts.tsx │ ├── DatabaseModal.tsx │ ├── Logo.tsx │ └── Layout.tsx ├── index.html ├── utils │ ├── openai.ts │ └── constants.ts ├── db │ └── index.ts └── routes │ ├── IndexRoute.tsx │ └── ChatRoute.tsx ├── tsconfig.json ├── Dockerfile ├── docker └── default.conf.template ├── .github └── workflows │ └── docker.yml ├── package.json ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.parcel-cache 3 | /dist 4 | notes.txt 5 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptive/chatpad/main/banner.png -------------------------------------------------------------------------------- /src/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adaptive/chatpad/main/src/assets/favicon.png -------------------------------------------------------------------------------- /src/hooks/useApiKey.ts: -------------------------------------------------------------------------------- 1 | import { useLocalStorage } from "@mantine/hooks"; 2 | 3 | export function useApiKey() { 4 | return useLocalStorage({ 5 | key: "openai-key", 6 | defaultValue: "", 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from "react-dom/client"; 2 | import { App } from "./components/App"; 3 | 4 | const container = document.getElementById("app"); 5 | const root = createRoot(container!); 6 | root.render(); 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | "jsx": "react-jsx", 5 | "esModuleInterop": true, 6 | "lib": ["es2018", "dom"], 7 | "strict": true, 8 | "skipLibCheck": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/hooks/useChatId.ts: -------------------------------------------------------------------------------- 1 | import { useMatchRoute } from "@tanstack/react-location"; 2 | 3 | export function useChatId() { 4 | const matchRoute = useMatchRoute(); 5 | const match = matchRoute({ to: "/chats/:chatId" }); 6 | return match?.chatId; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/markdown.scss: -------------------------------------------------------------------------------- 1 | .markdown { 2 | pre, p, ul, ol, blockquote { 3 | &:first-child { 4 | margin-top: 0; 5 | } 6 | &:last-child { 7 | margin-bottom: 0; 8 | } 9 | } 10 | ul { 11 | padding-left: 20px; 12 | } 13 | li { 14 | padding-left: 4px; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/ScrollIntoView.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export function ScrollIntoView({ children }: { children: ReactNode }) { 4 | return ( 5 |
{ 7 | if (!node) return; 8 | node.scrollIntoView({ behavior: "smooth" }); 9 | }} 10 | > 11 | {children} 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:alpine 2 | 3 | # Copy the nginx configuration 4 | COPY ./docker/default.conf.template /etc/nginx/templates/default.conf.template 5 | 6 | # Copy the built react application to the nginx folder 7 | COPY ./dist /usr/share/nginx/html 8 | 9 | # Required NGINX env variables 10 | ENV NGINX_ENVSUBST_OUTPUT_DIR=/etc/nginx/conf.d 11 | 12 | # Default env variables 13 | ENV PORT=80 14 | ENV HOST=0.0.0.0 15 | -------------------------------------------------------------------------------- /docker/default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen $PORT; 3 | 4 | root /usr/share/nginx/html; 5 | index index.html; 6 | 7 | server_tokens off; 8 | server_name _; 9 | 10 | gzip on; 11 | gzip_disable "msie6"; 12 | 13 | gzip_vary on; 14 | gzip_proxied any; 15 | gzip_comp_level 6; 16 | gzip_buffers 16 8k; 17 | gzip_http_version 1.1; 18 | gzip_min_length 0; 19 | gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; 20 | 21 | location / { 22 | try_files $uri /index.html; 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Chatpad AI 6 | 10 | 14 | 15 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/utils/openai.ts: -------------------------------------------------------------------------------- 1 | import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"; 2 | import { db } from "../db"; 3 | import { defaultModel } from "./constants"; 4 | 5 | function getClient(apiKey: string) { 6 | const configuration = new Configuration({ 7 | apiKey, 8 | }); 9 | return new OpenAIApi(configuration); 10 | } 11 | 12 | export async function createChatCompletion( 13 | apiKey: string, 14 | messages: ChatCompletionRequestMessage[] 15 | ) { 16 | const settings = await db.settings.get("general"); 17 | const model = settings?.openAiModel ?? defaultModel; 18 | 19 | const client = getClient(apiKey); 20 | return client.createChatCompletion({ 21 | model, 22 | stream: false, 23 | messages, 24 | }); 25 | } 26 | 27 | export async function checkOpenAIKey(apiKey: string) { 28 | return createChatCompletion(apiKey, [ 29 | { 30 | role: "user", 31 | content: "hello", 32 | }, 33 | ]); 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: ci-docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | workflow_dispatch: 9 | 10 | jobs: 11 | docker: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v3 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: 16 20 | cache: 'npm' 21 | - run: npm ci 22 | - run: npm run build 23 | - name: Setup QEMU 24 | uses: docker/setup-qemu-action@v2 25 | - name: Setup Docker Buildx 26 | uses: docker/setup-buildx-action@v2 27 | - name: Login to Github Container Registry 28 | uses: docker/login-action@v2 29 | with: 30 | registry: ghcr.io 31 | username: ${{ github.repository_owner }} 32 | password: ${{ secrets.CR_PAT }} 33 | - name: Build and push 34 | uses: docker/build-push-action@v4 35 | with: 36 | context: . 37 | push: true 38 | platforms: linux/amd64,linux/arm64 39 | tags: ghcr.io/${{ github.repository }}:${{ github.sha }}, ghcr.io/${{ github.repository }}:latest 40 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import Dexie, { Table } from "dexie"; 2 | import "dexie-export-import"; 3 | 4 | export interface Chat { 5 | id: string; 6 | description: string; 7 | totalTokens: number; 8 | createdAt: Date; 9 | } 10 | 11 | export interface Message { 12 | id: string; 13 | chatId: string; 14 | role: "system" | "assistant" | "user"; 15 | content: string; 16 | createdAt: Date; 17 | } 18 | 19 | export interface Prompt { 20 | id: string; 21 | title: string; 22 | content: string; 23 | createdAt: Date; 24 | } 25 | 26 | export interface Settings { 27 | id: "general"; 28 | openAiApiKey?: string; 29 | openAiModel?: string; 30 | } 31 | 32 | export class Database extends Dexie { 33 | chats!: Table; 34 | messages!: Table; 35 | prompts!: Table; 36 | settings!: Table; 37 | 38 | constructor() { 39 | super("chatpad"); 40 | this.version(2).stores({ 41 | chats: "id, createdAt", 42 | messages: "id, chatId, createdAt", 43 | prompts: "id, createdAt", 44 | settings: "id", 45 | }); 46 | 47 | this.on("populate", async () => { 48 | db.settings.add({ 49 | id: "general", 50 | }); 51 | }); 52 | } 53 | } 54 | 55 | export const db = new Database(); 56 | -------------------------------------------------------------------------------- /src/components/DeleteAllDataModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Stack, Text } from "@mantine/core"; 2 | import { useDisclosure } from "@mantine/hooks"; 3 | import { IconTrash } from "@tabler/icons-react"; 4 | import { db } from "../db"; 5 | 6 | export function DeleteAllDataModal({ onOpen }: { onOpen: () => void }) { 7 | const [opened, { open, close }] = useDisclosure(false, { onOpen }); 8 | 9 | return ( 10 | <> 11 | 19 | 26 | 27 | Are you sure you want to delete your data? 28 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/components/DeleteChatsModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Stack, Text } from "@mantine/core"; 2 | import { useDisclosure } from "@mantine/hooks"; 3 | import { IconTrash } from "@tabler/icons-react"; 4 | import { db } from "../db"; 5 | 6 | export function DeleteChatsModal({ onOpen }: { onOpen: () => void }) { 7 | const [opened, { open, close }] = useDisclosure(false, { onOpen }); 8 | 9 | return ( 10 | <> 11 | 19 | 26 | 27 | Are you sure you want to delete your chats? 28 | 39 | 40 | 41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/CharacterCard.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Grid, Group, Text } from "@mantine/core"; 2 | 3 | const CharacterCard = ({ 4 | character, 5 | onClick, 6 | selectedIndex, 7 | i, 8 | }: { 9 | character: { name: string; description: string }; 10 | onClick: any; 11 | selectedIndex: number; 12 | i: number; 13 | }) => { 14 | const isActive = selectedIndex === i; 15 | return ( 16 | 17 | ({ 22 | cursor: "pointer", 23 | height: "100%", 24 | borderColor: isActive 25 | ? `${theme.colors.blue["5"]} !important` 26 | : undefined, 27 | borderRadius: theme.radius.md, 28 | padding: theme.spacing.lg, 29 | // boxShadow: theme.shadows.xs, 30 | ":hover": { 31 | borderColor: `${theme.colors.blue["5"]} !important`, 32 | }, 33 | })} 34 | > 35 | 36 | {character.name} 37 | 38 | 39 | 40 | {character.description} 41 | 42 | 43 | 44 | ); 45 | }; 46 | 47 | export default CharacterCard; 48 | -------------------------------------------------------------------------------- /src/components/MainLink.tsx: -------------------------------------------------------------------------------- 1 | import { Group, Text, ThemeIcon, UnstyledButton } from "@mantine/core"; 2 | import { useLiveQuery } from "dexie-react-hooks"; 3 | import { Chat, db } from "../db"; 4 | 5 | interface MainLinkProps { 6 | icon: React.ReactNode; 7 | color: string; 8 | label: string; 9 | chat: Chat; 10 | } 11 | 12 | export function MainLink({ icon, color, label, chat }: MainLinkProps) { 13 | const firstMessage = useLiveQuery(async () => { 14 | return (await db.messages.orderBy("createdAt").toArray()).filter( 15 | (m) => m.chatId === chat.id 16 | )[0]; 17 | }, [chat]); 18 | 19 | return ( 20 | ({ 22 | // display: "block", 23 | width: "100%", 24 | padding: theme.spacing.xs, 25 | borderRadius: theme.radius.sm, 26 | color: 27 | theme.colorScheme === "dark" ? theme.colors.dark[0] : theme.black, 28 | })} 29 | > 30 | 31 | 32 | {icon} 33 | 34 | 44 | {label}
45 | {firstMessage?.content} 46 |
47 |
48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatpad", 3 | "private": true, 4 | "source": "src/index.html", 5 | "browserslist": "> 0.5%, last 2 versions, not dead", 6 | "scripts": { 7 | "start": "parcel", 8 | "build": "parcel build" 9 | }, 10 | "devDependencies": { 11 | "@parcel/transformer-sass": "^2.8.3", 12 | "@types/downloadjs": "^1.4.3", 13 | "@types/lodash": "^4.14.191", 14 | "@types/react": "^18.0.28", 15 | "@types/react-dom": "^18.0.11", 16 | "buffer": "^5.7.1", 17 | "parcel": "^2.8.3", 18 | "process": "^0.11.10" 19 | }, 20 | "dependencies": { 21 | "@emotion/react": "^11.10.6", 22 | "@emotion/server": "^11.10.0", 23 | "@mantine/core": "^6.0.1", 24 | "@mantine/hooks": "^6.0.1", 25 | "@mantine/next": "^6.0.1", 26 | "@mantine/notifications": "^6.0.1", 27 | "@tabler/icons-react": "^2.9.0", 28 | "@tanstack/react-location": "^3.7.4", 29 | "@types/node": "18.15.0", 30 | "@types/react": "18.0.28", 31 | "@types/react-dom": "18.0.11", 32 | "dexie": "^3.2.3", 33 | "dexie-export-import": "^4.0.6", 34 | "dexie-react-hooks": "^1.1.3", 35 | "downloadjs": "^1.4.7", 36 | "eslint": "8.36.0", 37 | "eslint-config-next": "13.2.4", 38 | "lodash": "^4.17.21", 39 | "nanoid": "^4.0.1", 40 | "next": "13.2.4", 41 | "openai": "^3.2.1", 42 | "react": "^18.2.0", 43 | "react-dom": "^18.2.0", 44 | "react-icons": "^4.8.0", 45 | "react-markdown": "^8.0.6", 46 | "remark-gfm": "^3.0.1", 47 | "typescript": "4.9.5" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/components/Chats.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIcon, Flex, Menu } from "@mantine/core"; 2 | import { IconDotsVertical, IconMessages } from "@tabler/icons-react"; 3 | import { Link } from "@tanstack/react-location"; 4 | import { useLiveQuery } from "dexie-react-hooks"; 5 | import { useMemo } from "react"; 6 | import { db } from "../db"; 7 | import { useChatId } from "../hooks/useChatId"; 8 | import { DeleteChatModal } from "./DeleteChatModal"; 9 | import { EditChatModal } from "./EditChatModal"; 10 | import { MainLink } from "./MainLink"; 11 | 12 | export function Chats({ search }: { search: string }) { 13 | const chatId = useChatId(); 14 | const chats = useLiveQuery(() => 15 | db.chats.orderBy("createdAt").reverse().toArray() 16 | ); 17 | const filteredChats = useMemo( 18 | () => 19 | (chats ?? []).filter((chat) => { 20 | if (!search) return true; 21 | return chat.description.toLowerCase().includes(search); 22 | }), 23 | [chats, search] 24 | ); 25 | 26 | return ( 27 | <> 28 | {filteredChats.map((chat) => ( 29 | ({ 33 | marginTop: 1, 34 | "&:hover, &.active": { 35 | backgroundColor: 36 | theme.colorScheme === "dark" 37 | ? theme.colors.dark[6] 38 | : theme.colors.gray[1], 39 | }, 40 | })} 41 | > 42 | 43 | } 45 | color="teal" 46 | chat={chat} 47 | label={chat.description} 48 | /> 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Edit 59 | 60 | 61 | Delete 62 | 63 | 64 | 65 | 66 | ))} 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/components/EditChatModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Stack, TextInput } from "@mantine/core"; 2 | import { useDisclosure } from "@mantine/hooks"; 3 | import { notifications } from "@mantine/notifications"; 4 | import { cloneElement, ReactElement, useEffect, useState } from "react"; 5 | import { Chat, db } from "../db"; 6 | 7 | export function EditChatModal({ 8 | chat, 9 | children, 10 | }: { 11 | chat: Chat; 12 | children: ReactElement; 13 | }) { 14 | const [opened, { open, close }] = useDisclosure(false); 15 | const [submitting, setSubmitting] = useState(false); 16 | 17 | const [value, setValue] = useState(""); 18 | useEffect(() => { 19 | setValue(chat?.description ?? ""); 20 | }, [chat]); 21 | 22 | return ( 23 | <> 24 | {cloneElement(children, { onClick: open })} 25 | 26 |
{ 28 | try { 29 | setSubmitting(true); 30 | event.preventDefault(); 31 | await db.chats.where({ id: chat.id }).modify((chat) => { 32 | chat.description = value; 33 | }); 34 | notifications.show({ 35 | title: "Saved", 36 | message: "", 37 | }); 38 | close(); 39 | } catch (error: any) { 40 | if (error.toJSON().message === "Network Error") { 41 | notifications.show({ 42 | title: "Error", 43 | color: "red", 44 | message: "No internet connection.", 45 | }); 46 | } 47 | const message = error.response?.data?.error?.message; 48 | if (message) { 49 | notifications.show({ 50 | title: "Error", 51 | color: "red", 52 | message, 53 | }); 54 | } 55 | } finally { 56 | setSubmitting(false); 57 | } 58 | }} 59 | > 60 | 61 | setValue(event.currentTarget.value)} 65 | formNoValidate 66 | data-autofocus 67 | /> 68 | 71 | 72 |
73 |
74 | 75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Chatpad AI](./banner.png) 2 | 3 |

Chatpad AI

4 |

Premium quality UI for ChatGPT

5 | 6 |

Web App & Desktop App

7 | 8 | Recently, there has been a surge of UIs for ChatGPT, making it the new "to-do app" that everyone wants to try their hand at. Chatpad sets itself apart with a broader vision - to become the ultimate interface for ChatGPT users. 9 | 10 | ### ⚡️ Free and open source 11 | 12 | This app is provided for free and the source code is available on GitHub. 13 | 14 | ### 🔒 Privacy focused 15 | 16 | No tracking, no cookies, no bullshit. All your data is stored locally. 17 | 18 | ### ✨ Best experience 19 | 20 | Crafted with love and care to provide the best experience possible. 21 | 22 | --- 23 | 24 | ## Self-host using Docker 25 | 26 | ``` 27 | docker run --name chatpad -d -p 1234:80 ghcr.io/deiucanta/chatpad:latest 28 | ``` 29 | 30 | ## One click Deployments 31 | 32 | 33 | [![Deploy on Easypanel](https://easypanel.io/img/deploy-on-easypanel-40.svg)](https://easypanel.io/docs/templates/chatpad) 34 | 35 | 36 | [![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/deiucanta/chatpad) 37 | 38 | 39 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fdeiucanta%2Fchatpad&project-name=chatpad&repository-name=chatpad-vercel&demo-title=Chatpad&demo-description=The%20Official%20Chatpad%20Website&demo-url=https%3A%2F%2Fchatpad.ai&demo-image=https%3A%2F%2Fraw.githubusercontent.com%2Fdeiucanta%2Fchatpad%2Fmain%2Fbanner.png) 40 | 41 | 42 | [![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/Ak6DUw?referralCode=9M8r62) 43 | 44 | 45 | 46 | 47 | ## Give Feedback 48 | 49 | If you have any feature requests or bug reports, go to [feedback.chatpad.ai](https://feedback.chatpad.ai). 50 | 51 | ## Contribute 52 | 53 | This is a React.js application. Clone the project, run `npm i` and `npm start` and you're good to go. 54 | 55 | ## Credits 56 | 57 | - [ToDesktop](https://todesktop.com) - A simple way to make your web app into a beautiful desktop app 58 | - [DexieJS](https://dexie.org) - A Minimalistic Wrapper for IndexedDB 59 | - [Mantine](https://mantine.dev) - A fully featured React component library 60 | -------------------------------------------------------------------------------- /src/components/DeletePromptModal.tsx: -------------------------------------------------------------------------------- 1 | import { ActionIcon, Button, Modal, Stack, Text, Tooltip } from "@mantine/core"; 2 | import { useDisclosure } from "@mantine/hooks"; 3 | import { notifications } from "@mantine/notifications"; 4 | import { IconTrash } from "@tabler/icons-react"; 5 | import { useNavigate } from "@tanstack/react-location"; 6 | import { useEffect, useState } from "react"; 7 | import { db, Prompt } from "../db"; 8 | import { useApiKey } from "../hooks/useApiKey"; 9 | import { useChatId } from "../hooks/useChatId"; 10 | 11 | export function DeletePromptModal({ prompt }: { prompt: Prompt }) { 12 | const [opened, { open, close }] = useDisclosure(false); 13 | const [submitting, setSubmitting] = useState(false); 14 | 15 | const [key, setKey] = useApiKey(); 16 | 17 | const [value, setValue] = useState(""); 18 | useEffect(() => { 19 | setValue(key); 20 | }, [key]); 21 | const chatId = useChatId(); 22 | const navigate = useNavigate(); 23 | 24 | return ( 25 | <> 26 | 27 |
{ 29 | try { 30 | setSubmitting(true); 31 | event.preventDefault(); 32 | await db.prompts.where({ id: prompt.id }).delete(); 33 | close(); 34 | 35 | notifications.show({ 36 | title: "Deleted", 37 | message: "Chat deleted.", 38 | }); 39 | } catch (error: any) { 40 | if (error.toJSON().message === "Network Error") { 41 | notifications.show({ 42 | title: "Error", 43 | color: "red", 44 | message: "No internet connection.", 45 | }); 46 | } else { 47 | notifications.show({ 48 | title: "Error", 49 | color: "red", 50 | message: 51 | "Can't remove chat. Please refresh the page and try again.", 52 | }); 53 | } 54 | } finally { 55 | setSubmitting(false); 56 | } 57 | }} 58 | > 59 | 60 | Are you sure you want to delete this prompt? 61 | 64 | 65 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/components/DeleteChatModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Stack, Text } from "@mantine/core"; 2 | import { useDisclosure } from "@mantine/hooks"; 3 | import { notifications } from "@mantine/notifications"; 4 | import { useNavigate } from "@tanstack/react-location"; 5 | import { cloneElement, ReactElement, useEffect, useState } from "react"; 6 | import { Chat, db } from "../db"; 7 | import { useApiKey } from "../hooks/useApiKey"; 8 | import { useChatId } from "../hooks/useChatId"; 9 | 10 | export function DeleteChatModal({ 11 | chat, 12 | children, 13 | }: { 14 | chat: Chat; 15 | children: ReactElement; 16 | }) { 17 | const [opened, { open, close }] = useDisclosure(false); 18 | const [submitting, setSubmitting] = useState(false); 19 | 20 | const [key, setKey] = useApiKey(); 21 | 22 | const [value, setValue] = useState(""); 23 | useEffect(() => { 24 | setValue(key); 25 | }, [key]); 26 | const chatId = useChatId(); 27 | const navigate = useNavigate(); 28 | 29 | return ( 30 | <> 31 | {cloneElement(children, { onClick: open })} 32 | 33 |
{ 35 | try { 36 | setSubmitting(true); 37 | event.preventDefault(); 38 | await db.chats.where({ id: chat.id }).delete(); 39 | await db.messages.where({ chatId: chat.id }).delete(); 40 | if (chatId === chat.id) { 41 | navigate({ to: `/` }); 42 | } 43 | close(); 44 | 45 | notifications.show({ 46 | title: "Deleted", 47 | message: "Chat deleted.", 48 | }); 49 | } catch (error: any) { 50 | if (error.toJSON().message === "Network Error") { 51 | notifications.show({ 52 | title: "Error", 53 | color: "red", 54 | message: "No internet connection.", 55 | }); 56 | } else { 57 | notifications.show({ 58 | title: "Error", 59 | color: "red", 60 | message: 61 | "Can't remove chat. Please refresh the page and try again.", 62 | }); 63 | } 64 | } finally { 65 | setSubmitting(false); 66 | } 67 | }} 68 | > 69 | 70 | Are you sure you want to delete this chat? 71 | 74 | 75 |
76 |
77 | 78 | ); 79 | } 80 | -------------------------------------------------------------------------------- /src/components/EditPromptModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ActionIcon, 3 | Button, 4 | Modal, 5 | Stack, 6 | Textarea, 7 | TextInput, 8 | Tooltip, 9 | } from "@mantine/core"; 10 | import { useDisclosure } from "@mantine/hooks"; 11 | import { notifications } from "@mantine/notifications"; 12 | import { IconPencil } from "@tabler/icons-react"; 13 | import { useEffect, useState } from "react"; 14 | import { db, Prompt } from "../db"; 15 | 16 | export function EditPromptModal({ prompt }: { prompt: Prompt }) { 17 | const [opened, { open, close }] = useDisclosure(false); 18 | const [submitting, setSubmitting] = useState(false); 19 | 20 | const [value, setValue] = useState(""); 21 | const [title, setTitle] = useState(""); 22 | useEffect(() => { 23 | setValue(prompt?.content ?? ""); 24 | setTitle(prompt?.title ?? ""); 25 | }, [prompt]); 26 | 27 | return ( 28 | <> 29 | 30 |
{ 32 | try { 33 | setSubmitting(true); 34 | event.preventDefault(); 35 | await db.prompts.where({ id: prompt.id }).modify((chat) => { 36 | chat.title = title; 37 | chat.content = value; 38 | }); 39 | notifications.show({ 40 | title: "Saved", 41 | message: "Prompt updated", 42 | }); 43 | } catch (error: any) { 44 | if (error.toJSON().message === "Network Error") { 45 | notifications.show({ 46 | title: "Error", 47 | color: "red", 48 | message: "No internet connection.", 49 | }); 50 | } 51 | const message = error.response?.data?.error?.message; 52 | if (message) { 53 | notifications.show({ 54 | title: "Error", 55 | color: "red", 56 | message, 57 | }); 58 | } 59 | } finally { 60 | setSubmitting(false); 61 | } 62 | }} 63 | > 64 | 65 | setTitle(event.currentTarget.value)} 69 | formNoValidate 70 | data-autofocus 71 | /> 72 |