diff --git a/README.md b/README.md index 7e0cde8..6ca6df8 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,34 @@ To use the realistic AI text-to-speech feature, you will need to add your Eleven Your API key is stored only on your device and never transmitted to anyone except ElevenLabs. +## Running on your own computer + +1. First, you'll need to have Git installed on your computer. If you don't have it installed already, you can download it from the official Git website: https://git-scm.com/downloads. + +2. Once Git is installed, you can clone the Chat with GPT repository by running the following command in your terminal or command prompt: + +``` +git clone https://github.com/cogentapps/chat-with-gpt.git +``` + +3. Next, you'll need to have Node.js and npm (Node Package Manager) installed on your computer. You can download the latest version of Node.js from the official Node.js website: https://nodejs.org/en/download/ + +4. Once Node.js is installed, navigate to the root directory of the Chat with GPT repository in your terminal or command prompt and run the following command to install the required dependencies: + +``` +npm install +``` + +This will install all the required dependencies specified in the package.json file. + +5. Finally, run the following command to start the development server: + +``` +npm run start +``` + +This will start the development server on port 3000. You can then open your web browser and navigate to http://localhost:3000 to view the Chat with GPT webapp running locally on your computer. + ## Roadmap - Edit messages (coming soon) diff --git a/package.json b/package.json index 7fd1536..3d36757 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "chat-with-gpt", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { "@emotion/css": "^11.10.6", "@emotion/styled": "^11.10.6", diff --git a/src/backend.ts b/src/backend.ts index d126cba..ffbb8fa 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -1,6 +1,27 @@ import EventEmitter from 'events'; import { Chat } from './types'; +/* + +Sync and login requires a backend implementation. + +Example syncing: + +const customBackend = new MyCustomBackend(); +customBackend.register(); + +In your custom backend, load saved chats from the server and call chatManager.loadChat(chat); + +chatManager.on('messages', async (messages: Message[]) => { + // send messages to server +}); + +chatManager.on('title', async (id: string, title: string) => { + // send updated chat title to server +}); + +*/ + export let backend: Backend | null = null; export class Backend extends EventEmitter { @@ -13,21 +34,21 @@ export class Backend extends EventEmitter { } get isAuthenticated() { + // return whether the user is currently signed in return false; } async signIn(options?: any) { + // sign in the user } async shareChat(chat: Chat): Promise { + // create a public share from the chat, and return the share's ID return null; } async getSharedChat(id: string): Promise { + // load a publicly shared chat from its ID return null; } -} - -export function getBackend() { - return backend; } \ No newline at end of file diff --git a/src/chat-manager.ts b/src/chat-manager.ts index 10d7506..f04720d 100644 --- a/src/chat-manager.ts +++ b/src/chat-manager.ts @@ -8,6 +8,7 @@ import { createStreamingChatCompletion } from './openai'; import { createTitle } from './titles'; import { ellipsize, sleep } from './utils'; import * as idb from './idb'; +import { getTokenCountForMessages, selectMessagesToSendSafely } from './tokenizer'; export const channel = new BroadcastChannel('chats'); @@ -102,9 +103,10 @@ export class ChatManager extends EventEmitter { ? chat.messages.getMessageChainTo(message.parentID) : []; messages.push(newMessage); + + const messagesToSend = selectMessagesToSendSafely(messages.map(getOpenAIMessageFromMessage)); - const response = await createStreamingChatCompletion(messages.map(getOpenAIMessageFromMessage), - message.requestedParameters); + const response = await createStreamingChatCompletion(messagesToSend, message.requestedParameters); response.on('error', () => { if (!reply.content) { @@ -250,7 +252,7 @@ export class Search { if (!chat.title) { chat.title = ellipsize(description, 100); } - + if (!chat.title || !description) { continue; } diff --git a/src/components/header.tsx b/src/components/header.tsx index d116d67..1ac8c64 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,15 +1,17 @@ import styled from '@emotion/styled'; import Helmet from 'react-helmet'; import { useSpotlight } from '@mantine/spotlight'; -import { Button, ButtonProps, TextInput } from '@mantine/core'; +import { Button, ButtonProps } from '@mantine/core'; import { useCallback, useState } from 'react'; -import { useNavigate } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; import { APP_NAME } from '../values'; import { useAppContext } from '../context'; import { backend } from '../backend'; +import { MenuItem, primaryMenu, secondaryMenu } from '../menus'; -const Container = styled.div` +const HeaderContainer = styled.div` display: flex; + flex-shrink: 0; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem; @@ -56,7 +58,44 @@ const Container = styled.div` font-size: 90%; } - i + span { + i + span, .mantine-Button-root span.hide-on-mobile { + @media (max-width: 40em) { + position: absolute; + left: -9999px; + top: -9999px; + } + } + + .mantine-Button-root { + @media (max-width: 40em) { + padding: 0.5rem; + } + } +`; + +const SubHeaderContainer = styled.div` + display: flex; + flex-direction: row; + font-family: "Work Sans", sans-serif; + line-height: 1.7; + font-size: 80%; + opacity: 0.7; + margin: 0.5rem 1rem 0 1rem; + gap: 1rem; + + .spacer { + flex-grow: 1; + } + + a { + color: white; + } + + .fa { + font-size: 90%; + } + + .fa + span { @media (max-width: 40em) { position: absolute; left: -9999px; @@ -68,8 +107,8 @@ const Container = styled.div` function HeaderButton(props: ButtonProps & { icon?: string, onClick?: any, children?: any }) { return ( + ); +} + +export function SubHeader(props: any) { + return + {primaryMenu.map(item => )} +
+ {secondaryMenu.map(item => )} + ; } \ No newline at end of file diff --git a/src/components/input.tsx b/src/components/input.tsx index 8ed0d55..d13eaca 100644 --- a/src/components/input.tsx +++ b/src/components/input.tsx @@ -1,17 +1,13 @@ import styled from '@emotion/styled'; import { Button, ActionIcon, Textarea } from '@mantine/core'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } from 'react'; +import { useLocation } from 'react-router-dom'; import { useAppContext } from '../context'; -import { Parameters } from '../types'; const Container = styled.div` background: #292933; border-top: thin solid #393933; padding: 1rem 1rem 0 1rem; - position: absolute; - bottom: 0rem; - left: 0; - right: 0; .inner { max-width: 50rem; @@ -30,10 +26,10 @@ export declare type OnSubmit = (name?: string) => Promise; function PaperPlaneSubmitButton(props: { onSubmit: any, disabled?: boolean }) { return ( - props.onSubmit()}> + ); @@ -41,27 +37,24 @@ function PaperPlaneSubmitButton(props: { onSubmit: any, disabled?: boolean }) { export interface MessageInputProps { disabled?: boolean; - parameters: Parameters; - onSubmit: OnSubmit; } export default function MessageInput(props: MessageInputProps) { const context = useAppContext(); - - const [message, setMessage] = useState(''); + const pathname = useLocation().pathname; const onChange = useCallback((e: React.ChangeEvent) => { - setMessage(e.target.value); - }, []); + context.setMessage(e.target.value); + }, [context.setMessage]); const onSubmit = useCallback(async () => { - if (await props.onSubmit(message)) { - setMessage(''); + if (await context.onNewMessage(context.message)) { + context.setMessage(''); } - }, [message, props.onSubmit]); + }, [context.message, context.onNewMessage, context.setMessage]); const onKeyDown = useCallback((e: React.KeyboardEvent) => { - if (e.key === 'Enter'&& e.shiftKey === false && !props.disabled) { + if (e.key === 'Enter' && e.shiftKey === false && !props.disabled) { e.preventDefault(); onSubmit(); } @@ -81,31 +74,41 @@ export default function MessageInput(props: MessageInputProps) { const openSystemPromptPanel = useCallback(() => context.settings.open('options', 'system-prompt'), []); const openTemperaturePanel = useCallback(() => context.settings.open('options', 'temperature'), []); + const messagesToDisplay = context.currentChat.messagesToDisplay; + const disabled = context.generating + || messagesToDisplay[messagesToDisplay.length - 1]?.role === 'user' + || (messagesToDisplay.length > 0 && !messagesToDisplay[messagesToDisplay.length - 1]?.done); + + const isLandingPage = pathname === '/'; + if (context.isShare || (!isLandingPage && !context.id)) { + return null; + } + return
-