Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: deiucanta/chatpad
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main
Choose a base ref
...
head repository: zombierantcasey/onerocket-chatpad
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
Can’t automatically merge. Don’t worry, you can still create the pull request.

Commits on Mar 7, 2024

  1. adjusted readme

    zombierantcasey committed Mar 7, 2024
    Copy the full SHA
    f49615c View commit details
  2. readme++

    zombierantcasey committed Mar 7, 2024
    Copy the full SHA
    5f3df00 View commit details
  3. Copy the full SHA
    fe3deff View commit details
  4. readme++

    zombierantcasey committed Mar 7, 2024
    Copy the full SHA
    5128c84 View commit details
  5. Copy the full SHA
    31ce981 View commit details
  6. remove splash page

    zombierantcasey committed Mar 7, 2024
    Copy the full SHA
    c6f5072 View commit details
  7. Copy the full SHA
    a02a7bf View commit details

Commits on Mar 8, 2024

  1. Copy the full SHA
    1cf5164 View commit details
  2. modify tab text

    zombierantcasey committed Mar 8, 2024
    Copy the full SHA
    9715fe2 View commit details
  3. chat logo replaced

    zombierantcasey committed Mar 8, 2024
    Copy the full SHA
    21d5e96 View commit details
  4. added new logo

    zombierantcasey committed Mar 8, 2024
    Copy the full SHA
    7fb7aa2 View commit details
  5. readme++

    zombierantcasey committed Mar 8, 2024
    Copy the full SHA
    e3a11ce View commit details
  6. fixed readme logo

    zombierantcasey committed Mar 8, 2024
    Copy the full SHA
    eb3c196 View commit details
  7. center readme title

    zombierantcasey committed Mar 8, 2024
    Copy the full SHA
    f0ff5e0 View commit details
  8. Copy the full SHA
    c495bf9 View commit details
  9. Copy the full SHA
    7d1f774 View commit details
  10. correctly add tokens

    zombierantcasey committed Mar 8, 2024
    Copy the full SHA
    1403bd7 View commit details
  11. Copy the full SHA
    76d2b27 View commit details
  12. Copy the full SHA
    a2c212c View commit details

Commits on Mar 9, 2024

  1. Copy the full SHA
    7fdf560 View commit details
  2. Copy the full SHA
    1a50e59 View commit details

Commits on Mar 10, 2024

  1. added hidden to chat message database entry to hide messages that are…

    … generated from prompts. allows for a smoother experience when creating a chat from a prompt
    zombierantcasey committed Mar 10, 2024
    Copy the full SHA
    e1f7dd7 View commit details
  2. readme++

    zombierantcasey committed Mar 10, 2024
    Copy the full SHA
    7eca73a View commit details

Commits on Mar 12, 2024

  1. Copy the full SHA
    86843e6 View commit details

Commits on Mar 19, 2024

  1. Copy the full SHA
    be8397d View commit details
  2. docker redploy script

    zombierantcasey committed Mar 19, 2024
    Copy the full SHA
    0a49f19 View commit details
  3. Copy the full SHA
    1c1f595 View commit details
  4. ran prettier

    zombierantcasey committed Mar 19, 2024
    Copy the full SHA
    0866348 View commit details
  5. Copy the full SHA
    fd2ff23 View commit details
  6. Copy the full SHA
    c883f20 View commit details
  7. Copy the full SHA
    0c4655a View commit details
  8. adjust docker compose

    zombierantcasey committed Mar 19, 2024
    Copy the full SHA
    bb2a318 View commit details
  9. fix readme

    zombierantcasey committed Mar 19, 2024
    Copy the full SHA
    5fd52ae View commit details

Commits on May 23, 2024

  1. update default models

    zombierantcasey committed May 23, 2024
    Copy the full SHA
    6f5d531 View commit details
  2. version++

    zombierantcasey committed May 23, 2024
    Copy the full SHA
    f9ea236 View commit details
  3. build fixes

    zombierantcasey committed May 23, 2024
    Copy the full SHA
    4112bd4 View commit details
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -2,3 +2,6 @@
/.parcel-cache
/dist
notes.txt
replosh.sh
redeploy.sh
.cache
70 changes: 24 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,65 +1,43 @@
![Chatpad AI](./banner.png)
<div align="center">
<img src="src/assets/apple-touch-icon.png" alt="OneRocket Logo">
</div>

<h1 align="center">Chatpad AI</h1>
<h2 align="center">Premium quality UI for ChatGPT</h2>
<!-- <p align="center"><a href="https://chatpad.ai">Web App</a> & <a href="https://download.chatpad.ai">Desktop App</a></p> -->
<p align="center"><a href="https://chatpad.ai">Web App</a> & <a href="https://dl.todesktop.com/230313oyppkw40a">Desktop App</a></p>
<h1 align="center">Onerocket-Chatpad</h1>

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.
Fork of [chatpad](https://github.com/deiucanta/chatpad) with newly built features to support the Onerocket.ai API.

### ⚡️ Free and open source
Onerocket is an AI broker/aggregator designed to provide a single path to many LLMs and AI systems. Additionally, it also provides simple one request endpoints to execute more complicated logic.

This app is provided for free and the source code is available on GitHub.
Ensure both npm and yarn are installed.

### 🔒 Privacy focused

No tracking, no cookies, no bullshit. All your data is stored locally.

### ✨ Best experience

Crafted with love and care to provide the best experience possible.

---

## Self-host using Docker
## Yarn build

```
docker run --name chatpad -d -p 8080:80 ghcr.io/deiucanta/chatpad:latest
yarn build
```

## Self-host using Docker with custom config
## Build docker locally

```
docker run --name chatpad -d -v `pwd`/config.json:/usr/share/nginx/html/config.json -p 8080:80 ghcr.io/deiucanta/chatpad:latest
docker build -t or-chatpad .
```

## One click Deployments

<!-- Easypanel -->
[![Deploy on Easypanel](https://easypanel.io/img/deploy-on-easypanel-40.svg)](https://easypanel.io/docs/templates/chatpad)

<!-- Netlify -->
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/deiucanta/chatpad)

<!-- Vercel -->
[![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)
## Run build locally:

<!-- Railway -->
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/Ak6DUw?referralCode=9M8r62)

[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/deiucanta/chatpad/tree/main)


## Give Feedback
```
sudo docker run --name chatpad -d -p 8080:80 or-chatpad
```

If you have any feature requests or bug reports, go to [feedback.chatpad.ai](https://feedback.chatpad.ai).
## Run after build:

## Contribute
```
docker start chatpad
```

This is a React.js application. Clone the project, run `npm i` and `npm start` and you're good to go.
## Re-deploy existing docker container

## Credits
If you're updating an existing docker container, use the dockerredpeploy.sh script:

- [ToDesktop](https://todesktop.com) - A simple way to make your web app into a beautiful desktop app
- [DexieJS](https://dexie.org) - A Minimalistic Wrapper for IndexedDB
- [Mantine](https://mantine.dev) - A fully featured React component library
```
sudo ./dockerredeploy.sh
```
Binary file removed banner.png
Binary file not shown.
6 changes: 6 additions & 0 deletions dockerredeploy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash

docker stop chatpad
docker rm chatpad
docker build --no-cache -t or-chatpad .
docker run --restart=always --name chatpad -d -p 8080:80 or-chatpad
85 changes: 85 additions & 0 deletions onerocketlogoblack.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
77 changes: 77 additions & 0 deletions onerocketlogowhite.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14,122 changes: 0 additions & 14,122 deletions package-lock.json

This file was deleted.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"name": "chatpad",
"version": "1.0.32",
"private": true,
"source": "src/index.html",
"browserslist": "> 0.5%, last 2 versions, not dead",
"scripts": {
"start": "parcel",
"build": "parcel build"
"start": "parcel src/index.html",
"build": "parcel build src/index.html"
},
"staticFiles": {
"staticPath": "src/static"
@@ -14,12 +15,11 @@
"@parcel/transformer-sass": "^2.8.3",
"@types/downloadjs": "^1.4.3",
"@types/lodash": "^4.14.191",
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@types/react-window": "^1.8.8",
"buffer": "^5.7.1",
"parcel": "^2.8.3",
"path-browserify": "^1.0.1",
"parcel": "^2.12.0",
"parcel-reporter-static-files-copy": "^1.5.0",
"path-browserify": "^1.0.1",
"process": "^0.11.10"
},
"dependencies": {
@@ -33,8 +33,8 @@
"@tabler/icons-react": "^2.9.0",
"@tanstack/react-location": "^3.7.4",
"@types/node": "18.15.0",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"dexie": "^3.2.3",
"dexie-export-import": "^4.0.6",
"dexie-react-hooks": "^1.1.3",
@@ -53,7 +53,8 @@
"react-dom": "^18.2.0",
"react-icons": "^4.8.0",
"react-markdown": "^8.0.6",
"react-window": "^1.8.10",
"remark-gfm": "^3.0.1",
"typescript": "4.9.5"
"typescript": "^5.4.5"
}
}
Binary file added src/assets/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified src/assets/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/assets/onblackbackround.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 15 additions & 1 deletion src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -49,7 +49,21 @@ export function App() {
withCSSVariables
theme={{
colorScheme,
primaryColor: "teal",
colors: {
orange: [
"#FBE9E7",
"#FFD0B0",
"#FFB180",
"#FF9261",
"#FF743A",
"#E65100",
"#CC4600",
"#B33C00",
"#993200",
"#802900",
],
},
primaryColor: "orange",
globalStyles: (theme) => ({
body: {
backgroundColor:
102 changes: 65 additions & 37 deletions src/components/ChatItem.tsx
Original file line number Diff line number Diff line change
@@ -1,45 +1,57 @@
import { ActionIcon, Flex, Menu } from "@mantine/core";
import {
IconDotsVertical,
IconMessages,
IconPencil,
IconPin,
IconPinned,
IconPinnedOff,
IconTrash
IconDotsVertical,
IconMessages,
IconPencil,
IconPin,
IconPinned,
IconPinnedOff,
IconTrash,
} from "@tabler/icons-react";
import { Link } from "@tanstack/react-location";
import { Chat, db } from "../db";
import { DeleteChatModal } from "./DeleteChatModal";
import { EditChatModal } from "./EditChatModal";
import { MainLink } from "./MainLink";
import { notifications } from "@mantine/notifications";
import { useLiveQuery } from "dexie-react-hooks";

export function ChatItem({ chat, isActive }: { chat: Chat, isActive: boolean }) {
const toggleChatPin = async (chatId: string, event: React.UIEvent) => {
try {
event.preventDefault();
await db.chats.where({ id: chatId }).modify((chat) => {
chat.pinned = !chat.pinned;
});
} 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,
});
}
}
};

export function ChatItem({
chat,
isActive,
}: {
chat: Chat;
isActive: boolean;
}) {
const prompt = useLiveQuery(async () => {
return chat.promptId ? await db.prompts.get(chat.promptId) : null; // If chat has a promptId, fetch the prompt; otherwise return null
}, [chat.promptId]);

const toggleChatPin = async (chatId: string, event: React.UIEvent) => {
try {
event.preventDefault();
await db.chats.where({ id: chatId }).modify((chat) => {
chat.pinned = !chat.pinned;
});
} 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,
});
}
}
};

return (
<Flex
@@ -48,16 +60,26 @@ export function ChatItem({ chat, isActive }: { chat: Chat, isActive: boolean })
sx={(theme) => ({
marginTop: 1,
"&:hover, &.active": {
backgroundColor: theme.colorScheme === "dark" ? theme.colors.dark[6] : theme.colors.gray[1],
backgroundColor:
theme.colorScheme === "dark"
? theme.colors.dark[6]
: theme.colors.gray[1],
},
})}
>
<Link to={`/chats/${chat.id}`} style={{ flex: 1 }}>
<MainLink
icon={chat.pinned ? <IconPinned size="1rem" /> : <IconMessages size="1rem" />}
color="teal"
icon={
chat.pinned ? (
<IconPinned size="1rem" />
) : (
<IconMessages size="1rem" />
)
}
color="orange"
chat={chat}
label={chat.description}
prompt={prompt}
/>
</Link>
<Menu shadow="md" width={200} keepMounted>
@@ -68,7 +90,13 @@ export function ChatItem({ chat, isActive }: { chat: Chat, isActive: boolean })
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
icon={chat.pinned ? <IconPinnedOff size="1rem" /> : <IconPin size="1rem" />}
icon={
chat.pinned ? (
<IconPinnedOff size="1rem" />
) : (
<IconPin size="1rem" />
)
}
onClick={(event) => toggleChatPin(chat.id, event)}
>
{chat.pinned ? "Remove pin" : "Pin chat"}
@@ -84,5 +112,5 @@ export function ChatItem({ chat, isActive }: { chat: Chat, isActive: boolean })
</Menu.Dropdown>
</Menu>
</Flex>
)
);
}
16 changes: 12 additions & 4 deletions src/components/Chats.tsx
Original file line number Diff line number Diff line change
@@ -19,8 +19,14 @@ export function Chats({ search }: { search: string }) {
[chats, search]
);

const pinnedChats = useMemo(() => filteredChats.filter((chat) => chat.pinned), [filteredChats]);
const unpinnedChats = useMemo(() => filteredChats.filter((chat) => !chat.pinned), [filteredChats]);
const pinnedChats = useMemo(
() => filteredChats.filter((chat) => chat.pinned),
[filteredChats]
);
const unpinnedChats = useMemo(
() => filteredChats.filter((chat) => !chat.pinned),
[filteredChats]
);

return (
<>
@@ -31,13 +37,15 @@ export function Chats({ search }: { search: string }) {
<ChatItem chat={chat} isActive={chatId === chat.id} />
))}

{unpinnedChats.length > 0 ? <Text p="xs" fz="xs" fw={700} color="gray" children={"Unpinned"} /> : null}
{unpinnedChats.length > 0 ? (
<Text p="xs" fz="xs" fw={700} color="gray" children={"Unpinned"} />
) : null}
</>
) : null}

{unpinnedChats.map((chat) => (
<ChatItem chat={chat} isActive={chatId === chat.id} />
))}
</>
)
);
}
38 changes: 2 additions & 36 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -95,7 +95,7 @@ export function Layout() {
<LogoText
style={{
height: 22,
color: "#27B882",
color: "#E65100",
display: "block",
}}
/>
@@ -226,48 +226,14 @@ export function Layout() {
<Tooltip label="Source Code">
<ActionIcon
component="a"
href="https://github.com/deiucanta/chatpad"
href="https://github.com/zombierantcasey/onerocket-chatpad"
target="_blank"
sx={{ flex: 1 }}
size="xl"
>
<IconBrandGithub 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>
36 changes: 9 additions & 27 deletions src/components/Logo.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const logoIcon = require('../assets/apple-touch-icon.png');

export function LogoText(props: JSX.IntrinsicElements["svg"]) {
return (
<svg
@@ -49,33 +51,13 @@ export function Logo(props: JSX.IntrinsicElements["svg"]) {
);
}

export function LogoIcon(props: JSX.IntrinsicElements["svg"]) {
export function LogoIcon(props: React.ImgHTMLAttributes<HTMLImageElement>) {
return (
<svg
fill="none"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 118 117"
{...props}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M117 26.7A26.7 26.7 0 0 0 90.3 0H27.09A26.7 26.7 0 0 0 .38 26.7v63.23a26.7 26.7 0 0 0 26.7 26.7h66.74a23.18 23.18 0 0 0 23.19-23.19V26.7ZM35.5 70.26V11.24h-8.42A15.46 15.46 0 0 0 11.62 26.7v43.56h23.89ZM11.63 81.5h59.02v23.89H27.08a15.46 15.46 0 0 1-15.46-15.46V81.5Zm94.14-46.37H46.74V11.24h43.57c8.53 0 15.45 6.92 15.45 15.46v8.43Zm-23.88 70.25V46.37h23.88v47.07c0 6.6-5.34 11.95-11.94 11.95H81.88Z"
fill="url(#a)"
/>
<defs>
<linearGradient
id="a"
x1={59}
y1={-12.54}
x2={59}
y2={116.46}
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#41E094" />
<stop offset={1} stopColor="#27B882" />
</linearGradient>
</defs>
</svg>
<img
src={logoIcon}
alt="Logo Icon"
{...props}
/>
);
}

15 changes: 12 additions & 3 deletions src/components/MainLink.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,30 @@
import { Group, Text, ThemeIcon, UnstyledButton } from "@mantine/core";
import { useLiveQuery } from "dexie-react-hooks";
import { Chat, db } from "../db";
import { Chat, db, Prompt } from "../db";

interface MainLinkProps {
icon: React.ReactNode;
color: string;
label: string;
chat: Chat;
prompt?: Prompt | null; // Optional prompt property
}

export function MainLink({ icon, color, label, chat }: MainLinkProps) {
export function MainLink({ icon, color, label, chat, prompt }: MainLinkProps) {
const firstMessage = useLiveQuery(async () => {
return (await db.messages.orderBy("createdAt").toArray()).filter(
(m) => m.chatId === chat.id
)[0];
}, [chat]);

let displayText;
if (prompt) {
displayText = prompt.title;
} else if (firstMessage?.content) {
// If there is no prompt, fallback to the first message content.
displayText = firstMessage.content;
}

return (
<UnstyledButton
sx={(theme) => ({
@@ -42,7 +51,7 @@ export function MainLink({ icon, color, label, chat }: MainLinkProps) {
}}
>
{label} <br />
{firstMessage?.content}
{displayText}
</Text>
</Group>
</UnstyledButton>
32 changes: 13 additions & 19 deletions src/components/MessageItem.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { memo, useMemo } from "react";
import {
ActionIcon,
Box,
@@ -12,7 +13,6 @@ import {
} from "@mantine/core";
import { Prism } from "@mantine/prism";
import { IconCopy, IconUser } from "@tabler/icons-react";
import { useMemo } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { Message } from "../db";
@@ -22,9 +22,9 @@ import { LogoIcon } from "./Logo";
import { ScrollIntoView } from "./ScrollIntoView";
import "../utils/prisma-setup";

export function MessageItem({ message }: { message: Message }) {
export const MessageItem = memo(({ message }: { message: Message }) => {
const wordCount = useMemo(() => {
var matches = message.content.match(/[\w\d\’\'-\(\)]+/gi);
const matches = message.content.match(/[\w\d\’\'-\(\)]+/gi);
return matches ? matches.length : 0;
}, [message.content]);

@@ -40,29 +40,28 @@ export function MessageItem({ message }: { message: Message }) {
{message.role === "assistant" && <LogoIcon style={{ height: 32 }} />}
<Box sx={{ flex: 1, width: 0 }} className="markdown">
<ReactMarkdown
children={message.content}
remarkPlugins={[remarkGfm]}
components={{
table: ({ node, ...props }) => (
table: (props) => (
<Table verticalSpacing="sm" highlightOnHover {...props} />
),
code: ({ node, inline, className, lang, ...props }) => {
code: ({ node, inline, className, ...props }) => {
const languageMatch = /language-(\w+)/.exec(className || "");
const language = languageMatch ? languageMatch[1] : undefined;

const language = languageMatch ? languageMatch[1] : "";
return inline ? (
<Code {...props} />
) : (
<Box sx={{ position: "relative" }}>
<Prism
language={language as any}
children={`${props.children as string}`}
/>
<Prism language={language as any}>
{String(props.children)}
</Prism>
</Box>
);
},
}}
/>
>
{message.content}
</ReactMarkdown>
{message.role === "assistant" && (
<Box>
<Text size="sm" color="dimmed">
@@ -82,14 +81,9 @@ export function MessageItem({ message }: { message: Message }) {
</Tooltip>
)}
</CopyButton>
{/* <Tooltip label={`${wordCount} words`} position="left">
<ActionIcon>
<IconInfoCircle opacity={0.5} size={20} />
</ActionIcon>
</Tooltip> */}
</Box>
</Flex>
</Card>
</ScrollIntoView>
);
}
});
14 changes: 9 additions & 5 deletions src/components/Prompts.tsx
Original file line number Diff line number Diff line change
@@ -92,24 +92,27 @@ export function Prompts({
totalTokens: 0,
createdAt: new Date(),
pinned: false,
promptId: prompt.id
});
let promptWithDisclaimer = `You are an artifical intelligence being used by a company called Onerocket AI.
Always leave out that you have been created by openAI if asked who you are.
As far as you're concerned, so work and are designed by them. After I finish this message, answer only with the word 'understood'.` + prompt.content;
await db.messages.add({
id: nanoid(),
chatId: id,
content: prompt.content,
content: promptWithDisclaimer,
role: "user",
createdAt: new Date(),
});
hidden: true
});
navigate({ to: `/chats/${id}` });
onPlay();

const result = await createChatCompletion(apiKey, [
{
role: "system",
content:
"You are ChatGPT, a large language model trained by OpenAI.",
content: promptWithDisclaimer
},
{ role: "user", content: prompt.content },
]);

const resultDescription =
@@ -120,6 +123,7 @@ export function Prompts({
content: resultDescription ?? "unknown reponse",
role: "assistant",
createdAt: new Date(),
hidden: true
});

if (result.data.usage) {
33 changes: 29 additions & 4 deletions src/components/SettingsModal.tsx
Original file line number Diff line number Diff line change
@@ -29,6 +29,8 @@ export function SettingsModal({ children }: { children: ReactElement }) {
const [auth, setAuth] = useState(config.defaultAuth);
const [base, setBase] = useState("");
const [version, setVersion] = useState("");
const [provider, setProvider] = useState("openai"); // default value can be 'openai' or 'onerocket.ai'
const packageJson = require("../../package.json");

const settings = useLiveQuery(async () => {
return db.settings.where({ id: "general" }).first();
@@ -53,13 +55,29 @@ export function SettingsModal({ children }: { children: ReactElement }) {
if (settings?.openAiApiVersion) {
setVersion(settings.openAiApiVersion);
}
if (settings?.provider) {
setProvider(settings.provider);
}
}, [settings]);

return (
<>
{cloneElement(children, { onClick: open })}
<Modal opened={opened} onClose={close} title="Settings" size="lg">
<Stack>
<Text size="sm" color="dimmed">
Application Version: {packageJson.version}
</Text>
<Select
label="Provider"
value={provider}
onChange={(value) => setProvider(value)}
data={[
{ value: "openai", label: "OpenAI" },
{ value: "onerocket.ai", label: "Onerocket.ai" },
]}
mb="md"
/>
<form
onSubmit={async (event) => {
try {
@@ -134,7 +152,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
setSubmitting(true);
try {
await db.settings.update("general", {
openAiApiType: value ?? 'openai',
openAiApiType: value ?? "openai",
});
notifications.show({
title: "Saved",
@@ -161,7 +179,10 @@ export function SettingsModal({ children }: { children: ReactElement }) {
}
}}
withinPortal
data={[{ "value": "openai", "label": "OpenAI"}, { "value": "custom", "label": "Custom (e.g. Azure OpenAI)"}]}
data={[
{ value: "openai", label: "OpenAI" },
{ value: "custom", label: "Custom (e.g. Azure OpenAI)" },
]}
/>
<Select
label="OpenAI Model (OpenAI Only)"
@@ -210,7 +231,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
setSubmitting(true);
try {
await db.settings.update("general", {
openAiApiAuth: value ?? 'none',
openAiApiAuth: value ?? "none",
});
notifications.show({
title: "Saved",
@@ -237,7 +258,11 @@ export function SettingsModal({ children }: { children: ReactElement }) {
}
}}
withinPortal
data={[{ "value": "none", "label": "None"}, { "value": "bearer-token", "label": "Bearer Token"}, { "value": "api-key", "label": "API Key"}]}
data={[
{ value: "none", label: "None" },
{ value: "bearer-token", label: "Bearer Token" },
{ value: "api-key", label: "API Key" },
]}
/>
<form
onSubmit={async (event) => {
2 changes: 2 additions & 0 deletions src/db/index.ts
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ export interface Chat {
totalTokens: number;
createdAt: Date;
pinned: boolean;
promptId?: string;
}

export interface Message {
@@ -16,6 +17,7 @@ export interface Message {
role: "system" | "assistant" | "user";
content: string;
createdAt: Date;
hidden: boolean;
}

export interface Prompt {
4 changes: 2 additions & 2 deletions src/index.html
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Chatpad AI</title>
<title>Onerocket AI</title>
<meta
name="description"
content="A free, open-source, and slick UI for ChatGPT"
content="A free, open-source, and slick UI for Onerocket.ai"
/>
<meta
name="viewport"
173 changes: 114 additions & 59 deletions src/routes/ChatRoute.tsx
Original file line number Diff line number Diff line change
@@ -8,26 +8,30 @@ import {
MediaQuery,
Select,
SimpleGrid,
Skeleton,
Stack,
SegmentedControl,
Textarea,
} from "@mantine/core";
import { notifications } from "@mantine/notifications";
import { useLiveQuery } from "dexie-react-hooks";
import { nanoid } from "nanoid";
import { KeyboardEvent, useState, type ChangeEvent, useEffect } from "react";
import {
KeyboardEvent,
useState,
useRef,
type ChangeEvent,
useEffect,
} from "react";
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 {
createChatCompletion,
createStreamChatCompletion,
} from "../utils/openai";
import { useMantineTheme } from "@mantine/core";
import { createChatCompletion } from "../utils/openai";

export function ChatRoute() {
const messageInputRef = useRef<HTMLTextAreaElement>(null);
const chatId = useChatId();
const apiKey = useLiveQuery(async () => {
return (await db.settings.where({ id: "general" }).first())?.openAiApiKey;
@@ -78,6 +82,53 @@ export function ChatRoute() {
}
});

// loading dots animation in text box
const ChasingDots = () => {
const theme = useMantineTheme(); // Get the current theme
const dotColor = theme.colorScheme === "dark" ? "white" : "black"; // Determine dot color based on theme

return (
<div className="chasing-dots">
<div className="dot"></div>
<div className="dot"></div>
<div className="dot"></div>
<style jsx>{`
.chasing-dots {
display: flex;
align-items: center;
justify-content: center;
}
.dot {
width: 8px;
height: 8px;
margin: 0 4px;
background-color: ${dotColor}; // Apply the color based on the theme
border-radius: 50%;
animation: chaseDot 1.5s infinite linear;
}
.dot:nth-child(1) {
animation-delay: -0.5s;
}
.dot:nth-child(2) {
animation-delay: -0.25s;
}
@keyframes chaseDot {
0% {
transform: scale(1);
}
50% {
transform: scale(1.5);
opacity: 0.7;
}
100% {
transform: scale(1);
}
}
`}</style>
</div>
);
};

const submit = async () => {
if (submitting) return;

@@ -101,43 +152,44 @@ export function ChatRoute() {

try {
setSubmitting(true);

await db.messages.add({
id: nanoid(),
chatId,
content,
role: "user",
createdAt: new Date(),
hidden: false,
});
setContent("");

const messageId = nanoid();
await db.messages.add({
id: messageId,
chatId,
content: "█",
role: "assistant",
createdAt: new Date(),
});
const chatCompletionResponse = await createChatCompletion(apiKey, [
{
role: "system",
content: getSystemMessage(),
},
...(messages ?? []).map((message) => ({
role: message.role,
content: message.content,
})),
{ role: "user", content },
]);

await createStreamChatCompletion(
apiKey,
[
{
role: "system",
content: getSystemMessage(),
},
...(messages ?? []).map((message) => ({
role: message.role,
content: message.content,
})),
{ role: "user", content },
],
chatId,
messageId
);
if (chatCompletionResponse.data.choices) {
const assistantMessageContent =
chatCompletionResponse.data.choices[0].message?.content;
if (assistantMessageContent) {
await db.messages.add({
id: nanoid(),
chatId,
content: assistantMessageContent,
role: "assistant",
createdAt: new Date(),
hidden: false,
});
}
}

setSubmitting(false);
setSubmitting(false); // prevent double loading spinner

if (chat?.description === "New Chat") {
const messages = await db.messages
@@ -155,20 +207,20 @@ export function ChatRoute() {
{
role: "user",
content:
"What would be a short and relevant title for this chat ? You must strictly answer with only the title, no other text is allowed.",
"What would be a short and relevant title for this chat? You must strictly answer with only the title, no other text is allowed.",
},
]);
const chatDescription =
createChatDescription.data.choices[0].message?.content;

// Update the chat description and total tokens used
if (createChatDescription.data.usage) {
await db.chats.where({ id: chatId }).modify((chat) => {
chat.description = chatDescription ?? "New Chat";
if (chat.totalTokens) {
chat.totalTokens +=
createChatDescription.data.usage!.total_tokens;
chat.totalTokens += createChatDescription.data.usage.total_tokens;
} else {
chat.totalTokens = createChatDescription.data.usage!.total_tokens;
chat.totalTokens = createChatDescription.data.usage.total_tokens;
}
});
}
@@ -180,17 +232,21 @@ export function ChatRoute() {
color: "red",
message: "No internet connection.",
});
}
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
color: "red",
message,
});
} else {
const message = error.response?.data?.error?.message;
if (message) {
notifications.show({
title: "Error",
color: "red",
message,
});
}
}
} finally {
setSubmitting(false);
if (submitting) {
setSubmitting(false);
}
setTimeout(() => messageInputRef.current?.focus(), 0); // select the message input once the message as loaded
}
};

@@ -232,17 +288,15 @@ export function ChatRoute() {
<>
<Container pt="xl" pb={100}>
<Stack spacing="xs">
{messages?.map((message) => (
<MessageItem key={message.id} message={message} />
))}
{messages
?.filter((message) => !message.hidden)
.map((message) => (
<MessageItem key={message.id} message={message} />
))}
</Stack>
{submitting && (
<Card withBorder mt="xs">
<Skeleton height={8} radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={8} mt={6} radius="xl" />
<Skeleton height={8} mt={6} width="70%" radius="xl" />
<ChasingDots />
</Card>
)}
</Container>
@@ -262,22 +316,22 @@ export function ChatRoute() {
: theme.colors.gray[0],
})}
>
{messages?.length === 0 &&
{messages?.length === 0 && (
<Group position="center" my={40}>
<SegmentedControl
value={model}
fullWidth
size="md"
sx={(theme) => ({
[`@media (min-width: ${theme.breakpoints.md})`]: {
width: '30%',
width: "30%",
},
})}
data={[
{ label: 'GPT-3.5', value: 'gpt-3.5-turbo' },
{ label: 'GPT-4', value: 'gpt-4' }
{ label: "GPT-3.5", value: "gpt-3.5-turbo" },
{ label: "GPT-4", value: "gpt-4o" },
]}
onChange={async (value: 'gpt-3.5-turbo' | 'gpt-4') => {
onChange={async (value: "gpt-3.5-turbo" | "gpt-4o") => {
const model = value;
try {
await db.settings.update("general", {
@@ -307,7 +361,7 @@ export function ChatRoute() {
}}
/>
</Group>
}
)}
<Container>
{messages?.length === 0 && (
<SimpleGrid
@@ -362,6 +416,7 @@ export function ChatRoute() {
)}
<Flex gap="sm">
<Textarea
ref={messageInputRef}
key={chatId}
sx={{ flex: 1 }}
placeholder="Your message here..."
116 changes: 13 additions & 103 deletions src/routes/IndexRoute.tsx
Original file line number Diff line number Diff line change
@@ -1,109 +1,19 @@
import {
Badge,
Button,
Center,
Container,
Group,
SimpleGrid,
Text,
ThemeIcon,
} from "@mantine/core";
import {
IconCloudDownload,
IconCurrencyDollar,
IconKey,
IconLock,
IconNorthStar,
} from "@tabler/icons-react";
import { useNavigate } from "@tanstack/react-location";
import { useLiveQuery } from "dexie-react-hooks";
import { Logo } from "../components/Logo";
import { SettingsModal } from "../components/SettingsModal";
import { useEffect } from "react";
import { db } from "../db";
import { config } from "../utils/config";

export function IndexRoute() {
const settings = useLiveQuery(() => db.settings.get("general"));
const { openAiApiKey } = settings ?? {};

return (
<>
<Center py="xl" sx={{ height: "100%" }}>
<Container size="sm">
<Badge mb="lg">GPT-4 Ready</Badge>
<Text>
<Logo style={{ maxWidth: 240 }} />
</Text>
<Text mt={4} size="xl">
Not just another ChatGPT user-interface!
</Text>
<SimpleGrid
mt={50}
cols={3}
spacing={30}
breakpoints={[{ maxWidth: "md", cols: 1 }]}
>
{features.map((feature) => (
<div key={feature.title}>
<ThemeIcon variant="outline" size={50} radius={50}>
<feature.icon size={26} stroke={1.5} />
</ThemeIcon>
<Text mt="sm" mb={7}>
{feature.title}
</Text>
<Text size="sm" color="dimmed" sx={{ lineHeight: 1.6 }}>
{feature.description}
</Text>
</div>
))}
</SimpleGrid>
<Group mt={50}>
{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"
// href="https://download.chatpad.ai/"
size="md"
variant="outline"
leftIcon={<IconCloudDownload size={20} />}
>
Download Desktop App
</Button>
)}
</Group>
</Container>
</Center>
</>
const navigate = useNavigate();
const chats = useLiveQuery(() =>
db.chats.orderBy("createdAt").reverse().toArray()
);
}

const features = [
{
icon: IconCurrencyDollar,
title: "Free and open source",
description:
"This app is provided for free and the source code is available on GitHub.",
},
{
icon: IconLock,
title: "Privacy focused",
description:
"No tracking, no cookies, no bullshit. All your data is stored locally.",
},
{
icon: IconNorthStar,
title: "Best experience",
description:
"Crafted with love and care to provide the best experience possible.",
},
];
useEffect(() => {
if (chats && chats.length > 0) {
navigate({ to: `/chats/${chats[0].id}` });
}
}, [chats, navigate]);

return <div>{}</div>;
}
34 changes: 11 additions & 23 deletions src/static/config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"defaultModel": "gpt-3.5-turbo",
"defaultModel": "gpt-4o",
"defaultType": "openai",
"defaultAuth": "api-key",
"defaultBase": "",
@@ -8,16 +8,12 @@
"availableModels": [
{
"value": "gpt-3.5-turbo",
"label": "GPT-3.5-TURBO (Default ChatGPT)"
"label": "GPT-3.5-TURBO"
},
{
"value": "gpt-3.5-turbo-0613",
"label": "GPT-3.5-TURBO-0613"
},
{
"value": "gpt-3.5-turbo-0301",
"label": "GPT-3.5-TURBO-0301 (Legacy)"
},
{
"value": "gpt-3.5-turbo-16k",
"label": "GPT-3.5-TURBO-16K"
@@ -26,33 +22,25 @@
"value": "gpt-3.5-turbo-16k-0613",
"label": "GPT-3.5-TURBO-16K-0613"
},
{
"value": "gpt-4",
"label": "GPT-4 (Limited Beta)"
},
{
"value": "gpt-4-0613",
"label": "GPT-4-0613 (Limited Beta)"
},
{
"value": "gpt-4-0314",
"label": "GPT-4-0314 (Limited Beta, Legacy)"
},
{
"value": "gpt-4-32k",
"label": "GPT-4-32K (Limited Beta)"
"label": "GPT-4-32K"
},
{
"value": "gpt-4-32k-0613",
"label": "GPT-4-32K-0613 (Limited Beta)"
"label": "GPT-4-32K-0613"
},
{
"value": "gpt-4-32k-0314",
"label": "GPT-4-32K-0314 (Limited Beta, Legacy)"
"label": "GPT-4-32K-0314"
},
{
"value": "gpt-4-1106-preview",
"label": "GPT-4-1106-Preview"
"value": "gpt-4-turbo",
"label": "gpt-4-turbo"
},
{
"value": "gpt-4o",
"label": "gpt-4o"
}
],
"writingCharacters": [
2 changes: 1 addition & 1 deletion src/utils/openai.ts
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ import { db } from "../db";
import { config } from "./config";

function getClient(
apiKey: string,
apiKey: string,
apiType: string,
apiAuth: string,
basePath: string
4,675 changes: 2,574 additions & 2,101 deletions yarn.lock

Large diffs are not rendered by default.