Merge pull request #4 from cogentapps/2023-03-08

v0.1.1
main
Cogent Apps 2023-03-08 13:56:04 -08:00 committed by GitHub
commit 63bee57226
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 2205 additions and 536 deletions

View File

@ -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)

View File

@ -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",

View File

@ -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<string|null> {
// create a public share from the chat, and return the share's ID
return null;
}
async getSharedChat(id: string): Promise<Chat|null> {
// load a publicly shared chat from its ID
return null;
}
}
export function getBackend() {
return backend;
}

View File

@ -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');
@ -103,8 +104,9 @@ export class ChatManager extends EventEmitter {
: [];
messages.push(newMessage);
const response = await createStreamingChatCompletion(messages.map(getOpenAIMessageFromMessage),
message.requestedParameters);
const messagesToSend = selectMessagesToSendSafely(messages.map(getOpenAIMessageFromMessage));
const response = await createStreamingChatCompletion(messagesToSend, message.requestedParameters);
response.on('error', () => {
if (!reply.content) {

View File

@ -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 (
<Button size='xs'
variant={props.variant || 'subtle'}
onClick={props.onClick}>
variant={props.variant || 'subtle'}
onClick={props.onClick}>
{props.icon && <i className={'fa fa-' + props.icon} />}
{props.children && <span>
{props.children}
@ -78,7 +117,14 @@ function HeaderButton(props: ButtonProps & { icon?: string, onClick?: any, child
)
}
export default function Header(props: { title?: any, onShare?: () => void, share?: boolean, canShare?: boolean }) {
export interface HeaderProps {
title?: any;
onShare?: () => void;
share?: boolean;
canShare?: boolean;
}
export default function Header(props: HeaderProps) {
const context = useAppContext();
const navigate = useNavigate();
const spotlight = useSpotlight();
@ -94,7 +140,7 @@ export default function Header(props: { title?: any, onShare?: () => void, share
context.settings.open(context.apiKeys.openai ? 'options' : 'user');
}, [context, context.apiKeys.openai]);
return <Container>
return <HeaderContainer>
<Helmet>
<title>{props.title ? `${props.title} - ` : ''}{APP_NAME} - Unofficial ChatGPT app</title>
</Helmet>
@ -112,10 +158,27 @@ export default function Header(props: { title?: any, onShare?: () => void, share
Share
</HeaderButton>}
{backend && !context.authenticated && (
<HeaderButton onClick={() => backend?.signIn()}>Sign in to sync</HeaderButton>
<HeaderButton onClick={() => backend?.signIn()}>Sign in <span className="hide-on-mobile">to sync</span></HeaderButton>
)}
<HeaderButton icon="plus" onClick={onNewChat} loading={loading} variant="light">
New Chat
</HeaderButton>
</Container>;
</HeaderContainer>;
}
function SubHeaderMenuItem(props: { item: MenuItem }) {
return (
<Button variant="light" size="xs" compact component={Link} to={props.item.link} key={props.item.link}>
{props.item.icon && <i className={'fa fa-' + props.item.icon} />}
<span>{props.item.label}</span>
</Button>
);
}
export function SubHeader(props: any) {
return <SubHeaderContainer>
{primaryMenu.map(item => <SubHeaderMenuItem item={item} key={item.link} />)}
<div className="spacer" />
{secondaryMenu.map(item => <SubHeaderMenuItem item={item} key={item.link} />)}
</SubHeaderContainer>;
}

View File

@ -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<boolean>;
function PaperPlaneSubmitButton(props: { onSubmit: any, disabled?: boolean }) {
return (
<ActionIcon size="xs"
disabled={props.disabled}
loading={props.disabled}
onClick={() => props.onSubmit()}>
<ActionIcon size="sm"
disabled={props.disabled}
loading={props.disabled}
onClick={props.onSubmit}>
<i className="fa fa-paper-plane" style={{ fontSize: '90%' }} />
</ActionIcon>
);
@ -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<HTMLTextAreaElement>) => {
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<HTMLTextAreaElement>) => {
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 <Container>
<div className="inner">
<Textarea disabled={props.disabled}
<Textarea disabled={props.disabled || disabled}
autosize
minRows={3}
maxRows={12}
placeholder={"Enter a message here..."}
value={message}
value={context.message}
onChange={onChange}
rightSection={rightSection}
onKeyDown={onKeyDown} />
<div>
<Button variant="subtle"
className="settings-button"
size="xs"
compact
onClick={openSystemPromptPanel}>
className="settings-button"
size="xs"
compact
onClick={openSystemPromptPanel}>
<span>Customize system prompt</span>
</Button>
<Button variant="subtle"
className="settings-button"
size="xs"
compact
onClick={openTemperaturePanel}>
<span>Temperature: {props.parameters.temperature.toFixed(1)}</span>
className="settings-button"
size="xs"
compact
onClick={openTemperaturePanel}>
<span>Temperature: {context.parameters.temperature.toFixed(1)}</span>
</Button>
</div>
</div>

View File

@ -0,0 +1,55 @@
import ReactMarkdown from 'react-markdown';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import { Button, CopyButton } from '@mantine/core';
export interface MarkdownProps {
content: string;
className?: string;
}
export function Markdown(props: MarkdownProps) {
const classes = ['prose', 'dark:prose-invert'];
if (props.className) {
classes.push(props.className);
}
return (
<div className={classes.join(' ')}>
<ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<div>
<CopyButton value={String(children)}>
{({ copy, copied }) => (
<Button variant="subtle" size="sm" compact onClick={copy}>
<i className="fa fa-clipboard" />
<span>{copied ? 'Copied' : 'Copy'}</span>
</Button>
)}
</CopyButton>
<SyntaxHighlighter
children={String(children).replace(/\n$/, '')}
style={vscDarkPlus as any}
language={match?.[1] || 'text'}
PreTag="div"
{...props}
/>
</div>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}>{props.content}</ReactMarkdown>
</div>
);
}

View File

@ -1,16 +1,10 @@
import styled from '@emotion/styled';
import ReactMarkdown from 'react-markdown';
import { Button, CopyButton, Loader } from '@mantine/core';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import { Message } from "../types";
import { share } from '../utils';
import { ElevenLabsReaderButton } from '../elevenlabs';
import { Markdown } from './markdown';
// hide for everyone but screen readers
const SROnly = styled.span`
@ -229,38 +223,7 @@ export default function MessageComponent(props: { message: Message, last: boolea
</Button>
)}
</div>
<div className={"prose dark:prose-invert content content-" + props.message.id}>
<ReactMarkdown remarkPlugins={[remarkGfm, remarkMath]}
rehypePlugins={[rehypeKatex]}
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '')
return !inline ? (
<div>
<CopyButton value={String(children)}>
{({ copy, copied }) => (
<Button variant="subtle" size="sm" compact onClick={copy}>
<i className="fa fa-clipboard" />
<span>{copied ? 'Copied' : 'Copy'}</span>
</Button>
)}
</CopyButton>
<SyntaxHighlighter
children={String(children).replace(/\n$/, '')}
style={vscDarkPlus as any}
language={match?.[1] || 'text'}
PreTag="div"
{...props}
/>
</div>
) : (
<code className={className} {...props}>
{children}
</code>
)
}
}}>{props.message.content}</ReactMarkdown>
</div>
<Markdown content={props.message.content} className={"content content-" + props.message.id} />
</div>
{props.last && <EndOfChatMarker />}
</Container>

View File

@ -1,23 +1,9 @@
import styled from '@emotion/styled';
import slugify from 'slugify';
import { useCallback, useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Button, Drawer, Loader } from '@mantine/core';
import { SpotlightProvider } from '@mantine/spotlight';
import { Parameters } from '../types';
import MessageInput from './input';
import Header from './header';
import SettingsScreen from './settings-screen';
import { useChatSpotlightProps } from '../spotlight';
import { useChat } from '../use-chat';
import Message from './message';
import { loadParameters, saveParameters } from '../parameters';
import { useAppContext } from '../context';
import { useDebouncedValue } from '@mantine/hooks';
import { APP_NAME } from '../values';
import { backend } from '../backend';
import Header, { HeaderProps, SubHeader } from './header';
import MessageInput from './input';
import SettingsDrawer from './settings';
const Container = styled.div`
position: absolute;
@ -27,174 +13,30 @@ const Container = styled.div`
bottom: 0;
background: #292933;
color: white;
`;
const Messages = styled.div`
max-height: 100%;
overflow-y: scroll;
`;
const EmptyMessage = styled.div`
min-height: 70vh;
padding-bottom: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: "Work Sans", sans-serif;
line-height: 1.7;
gap: 1rem;
overflow: hidden;
`;
function Empty(props: { loading?: boolean }) {
const context = useAppContext();
return (
<EmptyMessage>
{props.loading && <Loader variant="dots" />}
{!props.loading && <>
<p>Hello, how can I help you today?</p>
{!context.apiKeys.openai && (
<Button size="xs"
variant="light"
compact
onClick={() => context.settings.open('user', 'openai-api-key')}>
Connect your OpenAI account to get started
</Button>
)}
</>}
</EmptyMessage>
);
}
export default function ChatPage(props: any) {
const { id } = useParams();
const context = useAppContext();
export function Page(props: {
id: string;
headerProps?: HeaderProps;
showSubHeader?: boolean;
children: any;
}) {
const spotlightProps = useChatSpotlightProps();
const navigate = useNavigate();
const { chat, messages, chatLoadedAt, leaf } = useChat(id, props.share);
const [generating, setGenerating] = useState(false);
const [_parameters, setParameters] = useState<Parameters>(loadParameters(id));
const [parameters] = useDebouncedValue(_parameters, 2000);
useEffect(() => {
if (id) {
saveParameters(id, parameters);
}
saveParameters('', parameters);
}, [id, parameters]);
const onNewMessage = useCallback(async (message?: string) => {
if (props.share) {
return false;
}
if (!message?.trim().length) {
return false;
}
if (!context.apiKeys.openai) {
context.settings.open('user', 'openai-api-key');
return false;
}
setGenerating(true);
if (chat) {
await context.chat.sendMessage({
chatID: chat.id,
content: message.trim(),
requestedParameters: {
...parameters,
apiKey: context.apiKeys.openai,
},
parentID: leaf?.id,
});
} else if (props.landing) {
const id = await context.chat.createChat();
await context.chat.sendMessage({
chatID: id,
content: message.trim(),
requestedParameters: {
...parameters,
apiKey: context.apiKeys.openai,
},
parentID: leaf?.id,
});
navigate('/chat/' + id);
}
setTimeout(() => setGenerating(false), 4000);
return true;
}, [chat, context.apiKeys.openai, leaf, parameters, props.landing]);
useEffect(() => {
if (props.share) {
return;
}
const shouldScroll = (Date.now() - chatLoadedAt) > 5000;
if (!shouldScroll) {
return;
}
const container = document.querySelector('#messages') as HTMLElement;
const messages = document.querySelectorAll('#messages .message');
if (messages.length) {
const latest = messages[messages.length - 1] as HTMLElement;
const offset = Math.max(0, latest.offsetTop - 100);
setTimeout(() => {
container?.scrollTo({ top: offset, behavior: 'smooth' });
}, 500);
}
}, [chatLoadedAt, messages.length]);
const disabled = generating
|| messages[messages.length - 1]?.role === 'user'
|| (messages.length > 0 && !messages[messages.length - 1]?.done);
const shouldShowChat = id && chat && !!messages.length;
return <SpotlightProvider {...spotlightProps}>
<Container key={chat?.id}>
<Header share={props.share} canShare={messages.length > 1}
title={(id && messages.length) ? chat?.title : null}
onShare={async () => {
if (chat) {
const id = await backend?.shareChat(chat);
if (id) {
const slug = chat.title ? '/' + slugify(chat.title.toLocaleLowerCase()) : '';
const url = window.location.origin + '/s/' + id + slug;
navigator.share?.({
title: chat.title || undefined,
url,
});
}
}
}} />
<Messages id="messages">
{shouldShowChat && <div style={{ paddingBottom: '20rem' }}>
{messages.map((message) => (
<Message message={message}
share={props.share}
last={chat.messages.leafs.some(n => n.id === message.id)} />
))}
</div>}
{!shouldShowChat && <Empty loading={(!props.landing && !chat) || props.share} />}
</Messages>
{!props.share && <MessageInput disabled={disabled} onSubmit={onNewMessage} parameters={parameters} />}
<Drawer size="50rem"
position='right'
opened={!!context.settings.tab}
onClose={() => context.settings.close()}
withCloseButton={false}>
<SettingsScreen parameters={_parameters} setParameters={setParameters} />
</Drawer>
<Container key={props.id}>
<Header share={props.headerProps?.share}
canShare={props.headerProps?.canShare}
title={props.headerProps?.title}
onShare={props.headerProps?.onShare} />
{props.showSubHeader && <SubHeader />}
{props.children}
<MessageInput />
<SettingsDrawer />
</Container>
</SpotlightProvider>;
}

View File

@ -0,0 +1,79 @@
import styled from "@emotion/styled";
import { Markdown } from "../markdown";
import { Page } from "../page";
const title = "Learn about Chat with GPT";
const content = `
# About Chat with GPT
Chat with GPT is an open-source, unofficial ChatGPT app with extra features and more ways to customize your experience.
ChatGPT is an AI assistant developed by OpenAI. It's designed to understand natural language and generate human-like responses to a wide range of questions and prompts. ChatGPT has been trained on a massive dataset of text from the internet, which allows it to draw on a vast amount of knowledge and information to answer questions and engage in conversation. ChatGPT is constantly being improved. Feel free to ask it anything!
[Join the Discord.](https://discord.gg/mS5QvKykvv)
## Features
- 🚀 **Fast** response times.
- 🔎 **Search** through your past chat conversations.
- 📄 View and customize the System Prompt - the **secret prompt** the system shows the AI before your messages.
- 🌡 Adjust the **creativity and randomness** of responses by setting the Temperature setting. Higher temperature means more creativity.
- 💬 Give ChatGPT AI a **realistic human voice** by connecting your ElevenLabs text-to-speech account.
- **Share** your favorite chat sessions online using public share URLs.
- 📋 Easily **copy-and-paste** ChatGPT messages.
- 🖼 **Full markdown support** including code, tables, and math.
- 🫰 Pay for only what you use with the ChatGPT API.
## Bring your own API keys
### OpenAI
To get started with Chat with GPT, you will need to add your OpenAI API key on the settings screen. Click "Connect your OpenAI account to get started" on the home page to begin. Once you have added your API key, you can start chatting with ChatGPT.
Your API key is stored only on your device and is never transmitted to anyone except OpenAI. Please note that OpenAI API key usage is billed at a pay-as-you-go rate, separate from your ChatGPT subscription.
### ElevenLabs
To use the realistic AI text-to-speech feature, you will need to add your ElevenLabs API key by clicking "Play" next to any message.
Your API key is stored only on your device and never transmitted to anyone except ElevenLabs.
## Roadmap
- Edit messages (coming soon)
- Regenerate messages (coming soon)
- [Suggest feature ideas on the Discord](https://discord.gg/mS5QvKykvv)
`;
const Container = styled.div`
flex-grow: 1;
overflow-y: auto;
padding-top: 2rem;
padding-bottom: 3rem;
.inner {
max-width: 50rem;
margin-left: auto;
margin-right: auto;
font-weight: "Work Sans", sans-serif;
* {
color: white !important;
}
h1, h2 {
border-bottom: thin solid rgba(255, 255, 255, 0.2);
padding-bottom: 1rem;
margin-bottom: 1rem;
}
}
`;
export default function AboutPage(props: any) {
return <Page id={'about'} headerProps={{ title }}>
<Container>
<Markdown content={content} className='inner' />
</Container>
</Page>;
}

View File

@ -0,0 +1,100 @@
import styled from '@emotion/styled';
import slugify from 'slugify';
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Loader } from '@mantine/core';
import Message from '../message';
import { useAppContext } from '../../context';
import { backend } from '../../backend';
import { Page } from '../page';
const Messages = styled.div`
max-height: 100%;
flex-grow: 1;
overflow-y: scroll;
display: flex;
flex-direction: column;
`;
const EmptyMessage = styled.div`
flex-grow: 1;
padding-bottom: 5vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: "Work Sans", sans-serif;
line-height: 1.7;
gap: 1rem;
`;
export default function ChatPage(props: any) {
const { id } = useParams();
const context = useAppContext();
useEffect(() => {
if (props.share || !context.currentChat.chatLoadedAt) {
return;
}
const shouldScroll = (Date.now() - context.currentChat.chatLoadedAt) > 5000;
if (!shouldScroll) {
return;
}
const container = document.querySelector('#messages') as HTMLElement;
const messages = document.querySelectorAll('#messages .message');
if (messages.length) {
const latest = messages[messages.length - 1] as HTMLElement;
const offset = Math.max(0, latest.offsetTop - 100);
setTimeout(() => {
container?.scrollTo({ top: offset, behavior: 'smooth' });
}, 500);
}
}, [context.currentChat?.chatLoadedAt, context.currentChat?.messagesToDisplay.length]);
const messagesToDisplay = context.currentChat.messagesToDisplay;
const shouldShowChat = id && context.currentChat.chat && !!messagesToDisplay.length;
return <Page id={id || 'landing'}
headerProps={{
share: context.isShare,
canShare: messagesToDisplay.length > 1,
title: (id && messagesToDisplay.length) ? context.currentChat.chat?.title : null,
onShare: async () => {
if (context.currentChat.chat) {
const id = await backend?.shareChat(context.currentChat.chat);
if (id) {
const slug = context.currentChat.chat.title
? '/' + slugify(context.currentChat.chat.title.toLocaleLowerCase())
: '';
const url = window.location.origin + '/s/' + id + slug;
navigator.share?.({
title: context.currentChat.chat.title || undefined,
url,
});
}
}
},
}}>
<Messages id="messages">
{shouldShowChat && (
<div style={{ paddingBottom: '4.5rem' }}>
{messagesToDisplay.map((message) => (
<Message key={message.id}
message={message}
share={props.share}
last={context.currentChat.chat!.messages.leafs.some(n => n.id === message.id)} />
))}
</div>
)}
{!shouldShowChat && <EmptyMessage>
<Loader variant="dots" />
</EmptyMessage>}
</Messages>
</Page>;
}

View File

@ -0,0 +1,33 @@
import styled from '@emotion/styled';
import { Button } from '@mantine/core';
import MessageInput from '../input';
import SettingsDrawer from '../settings';
import { useAppContext } from '../../context';
import { Page } from '../page';
const Container = styled.div`
flex-grow: 1;
padding-bottom: 5vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: "Work Sans", sans-serif;
line-height: 1.7;
gap: 1rem;
`;
export default function LandingPage(props: any) {
const context = useAppContext();
return <Page id={'landing'} showSubHeader={true}>
<Container>
<p>Hello, how can I help you today?</p>
{!context.apiKeys.openai && (
<Button size="xs" variant="light" compact onClick={() => context.settings.open('user', 'openai-api-key')}>
Connect your OpenAI account to get started
</Button>
)}
</Container>
</Page>;
}

View File

@ -1,242 +0,0 @@
import styled from '@emotion/styled';
import { Button, Grid, Select, Slider, Tabs, Textarea, TextInput } from "@mantine/core";
import { useMediaQuery } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { defaultSystemPrompt } from '../openai';
import { defaultVoiceList, getVoices } from '../elevenlabs';
import { useAppContext } from '../context';
const Container = styled.div`
padding: .4rem 1rem 1rem 1rem;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
max-width: 100vw;
max-height: 100vh;
@media (max-width: 40em) {
padding: 0;
}
.mantine-Tabs-root {
display: flex;
flex-direction: column;
height: calc(100% - 3rem);
@media (max-width: 40em) {
height: calc(100% - 5rem);
}
}
.mantine-Tabs-tab {
padding: 1.2rem 1.618rem 0.8rem 1.618rem;
@media (max-width: 40em) {
padding: 1rem;
span {
display: none;
}
}
}
.mantine-Tabs-panel {
flex-grow: 1;
overflow-y: scroll;
overflow-x: hidden;
min-height: 0;
margin-left: 0;
padding: 1.2rem 0 3rem 0;
@media (max-width: 40em) {
padding: 1.2rem 1rem 3rem 1rem;
}
}
#save {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0 1rem 1rem 1rem;
opacity: 1;
.mantine-Button-root {
height: 3rem;
}
}
`;
const Settings = styled.div`
font-family: "Work Sans", sans-serif;
color: white;
section {
margin-bottom: .618rem;
padding: 0.618rem;
h3 {
font-size: 1rem;
font-weight: bold;
margin-bottom: 1rem;
}
p {
line-height: 1.7;
margin-top: 0.8rem;
font-size: 1rem;
}
a {
color: white;
text-decoration : underline;
}
code {
font-family: "Fira Code", monospace;
}
}
.focused {
border: thin solid rgba(255, 255, 255, 0.1);
border-radius: 0.25rem;
animation: flash 3s;
}
@keyframes flash {
0% {
border-color: rgba(255, 0, 0, 0);
}
50% {
border-color: rgba(255, 0, 0, 1);
}
100% {
border-color: rgba(255, 255, 255, .1);
}
}
`;
export interface SettingsScreenProps {
parameters: any;
setParameters: (parameters: any) => any;
}
export default function SettingsScreen(props: SettingsScreenProps) {
const context = useAppContext();
const small = useMediaQuery('(max-width: 40em)');
const { parameters, setParameters } = props;
const [voices, setVoices] = useState<any[]>(defaultVoiceList);
useEffect(() => {
if (context.apiKeys.elevenlabs) {
getVoices().then(data => {
if (data?.voices?.length) {
setVoices(data.voices);
}
});
}
}, [context.apiKeys.elevenlabs]);
if (!context.settings.tab) {
return null;
}
return (
<Container>
<Tabs defaultValue={context.settings.tab} style={{ margin: '0rem' }}>
<Tabs.List grow={small}>
<Tabs.Tab value="options">Options</Tabs.Tab>
<Tabs.Tab value="user">User</Tabs.Tab>
<Tabs.Tab value="speech">Speech</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="user">
<Settings>
<Grid style={{ marginBottom: '1.618rem' }} gutter={24}>
<Grid.Col span={12}>
<section className={context.settings.option === 'openai-api-key' ? 'focused' : ''}>
<h3>Your OpenAI API Key</h3>
<TextInput
placeholder="Paste your API key here"
value={context.apiKeys.openai || ''}
onChange={event => {
setParameters({ ...parameters, apiKey: event.currentTarget.value });
context.apiKeys.setOpenAIApiKey(event.currentTarget.value);
}} />
<p><a href="https://platform.openai.com/account/api-keys" target="_blank">Find your API key here.</a> Your API key is stored only on this device and never transmitted to anyone except OpenAI.</p>
<p>OpenAI API key usage is billed at a pay-as-you-go rate, separate from your ChatGPT subscription.</p>
</section>
</Grid.Col>
</Grid>
</Settings>
</Tabs.Panel>
<Tabs.Panel value="options">
<Settings>
<Grid style={{ marginBottom: '1.618rem' }} gutter={24}>
<Grid.Col span={12}>
<section className={context.settings.option === 'system-prompt' ? 'focused' : ''}>
<h3>System Prompt</h3>
<Textarea
value={parameters.initialSystemPrompt || defaultSystemPrompt}
onChange={event => setParameters({ ...parameters, initialSystemPrompt: event.currentTarget.value })}
minRows={5}
maxRows={10}
autosize />
<p style={{ marginBottom: '0.7rem' }}>The System Prompt is shown to ChatGPT by the "System" before your first message. The <code style={{ whiteSpace: 'nowrap' }}>{'{{ datetime }}'}</code> tag is automatically replaced by the current date and time.</p>
{(parameters.initialSystemPrompt?.trim() !== defaultSystemPrompt) && <Button size="xs" compact variant="light" onClick={() => setParameters({ ...parameters, initialSystemPrompt: defaultSystemPrompt })}>
Reset to default
</Button>}
</section>
</Grid.Col>
<Grid.Col span={12}>
<section className={context.settings.option === 'temperature' ? 'focused' : ''}>
<h3>Temperature ({parameters.temperature.toFixed(1)})</h3>
<Slider value={parameters.temperature} onChange={value => setParameters({ ...parameters, temperature: value })} step={0.1} min={0} max={1} precision={3} />
<p>The temperature parameter controls the randomness of the AI's responses. Lower values will make the AI more predictable, while higher values will make it more creative.</p>
</section>
</Grid.Col>
</Grid>
</Settings>
</Tabs.Panel>
<Tabs.Panel value="speech">
<Settings>
<Grid style={{ marginBottom: '1.618rem' }} gutter={24}>
<Grid.Col span={12}>
<section className={context.settings.option === 'elevenlabs-api-key' ? 'focused' : ''}>
<h3>Your ElevenLabs Text-to-Speech API Key (optional)</h3>
<TextInput placeholder="Paste your API key here" value={context.apiKeys.elevenlabs || ''} onChange={event => context.apiKeys.setElevenLabsApiKey(event.currentTarget.value)} />
<p>Give ChatGPT a realisic human voice by connecting your ElevenLabs account (preview the available voices below). <a href="https://beta.elevenlabs.io" target="_blank">Click here to sign up.</a></p>
<p>You can find your API key on the Profile tab of the ElevenLabs website. Your API key is stored only on this device and never transmitted to anyone except ElevenLabs.</p>
</section>
</Grid.Col>
<Grid.Col span={12}>
<section className={context.settings.option === 'elevenlabs-voice' ? 'focused' : ''}>
<h3>Voice</h3>
<Select
value={context.voice.id}
onChange={v => context.voice.setVoiceID(v!)}
data={voices.map(v => ({ label: v.name, value: v.voice_id }))} />
<audio controls style={{ display: 'none' }} id="voice-preview" key={context.voice.id}>
<source src={voices.find(v => v.voice_id === context.voice.id)?.preview_url} type="audio/mpeg" />
</audio>
<Button onClick={() => (document.getElementById('voice-preview') as HTMLMediaElement)?.play()} variant='light' compact style={{ marginTop: '1rem' }}>
<i className='fa fa-headphones' />
<span>Preview voice</span>
</Button>
</section>
</Grid.Col>
</Grid>
</Settings>
</Tabs.Panel>
</Tabs>
<div id="save">
<Button variant="light" fullWidth size="md" onClick={() => context.settings.close()}>
Save and Close
</Button>
</div>
</Container>
)
}

View File

@ -0,0 +1,246 @@
import styled from '@emotion/styled';
import { Button, Drawer, Grid, Select, Slider, Tabs, Textarea, TextInput } from "@mantine/core";
import { useMediaQuery } from '@mantine/hooks';
import { useEffect, useState } from 'react';
import { defaultSystemPrompt } from '../openai';
import { defaultVoiceList, getVoices } from '../elevenlabs';
import { useAppContext } from '../context';
const Container = styled.div`
padding: .4rem 1rem 1rem 1rem;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
max-width: 100vw;
max-height: 100vh;
@media (max-width: 40em) {
padding: 0;
}
.mantine-Tabs-root {
display: flex;
flex-direction: column;
height: calc(100% - 3rem);
@media (max-width: 40em) {
height: calc(100% - 5rem);
}
}
.mantine-Tabs-tab {
padding: 1.2rem 1.618rem 0.8rem 1.618rem;
@media (max-width: 40em) {
padding: 1rem;
span {
display: none;
}
}
}
.mantine-Tabs-panel {
flex-grow: 1;
overflow-y: scroll;
overflow-x: hidden;
min-height: 0;
margin-left: 0;
padding: 1.2rem 0 3rem 0;
@media (max-width: 40em) {
padding: 1.2rem 1rem 3rem 1rem;
}
}
#save {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0 1rem 1rem 1rem;
opacity: 1;
.mantine-Button-root {
height: 3rem;
}
}
`;
const Settings = styled.div`
font-family: "Work Sans", sans-serif;
color: white;
section {
margin-bottom: .618rem;
padding: 0.618rem;
h3 {
font-size: 1rem;
font-weight: bold;
margin-bottom: 1rem;
}
p {
line-height: 1.7;
margin-top: 0.8rem;
font-size: 1rem;
}
a {
color: white;
text-decoration : underline;
}
code {
font-family: "Fira Code", monospace;
}
}
.focused {
border: thin solid rgba(255, 255, 255, 0.1);
border-radius: 0.25rem;
animation: flash 3s;
}
@keyframes flash {
0% {
border-color: rgba(255, 0, 0, 0);
}
50% {
border-color: rgba(255, 0, 0, 1);
}
100% {
border-color: rgba(255, 255, 255, .1);
}
}
`;
export interface SettingsDrawerProps {
}
export default function SettingsDrawer(props: SettingsDrawerProps) {
const context = useAppContext();
const small = useMediaQuery('(max-width: 40em)');
const { parameters, setParameters } = context;
const [voices, setVoices] = useState<any[]>(defaultVoiceList);
useEffect(() => {
if (context.apiKeys.elevenlabs) {
getVoices().then(data => {
if (data?.voices?.length) {
setVoices(data.voices);
}
});
}
}, [context.apiKeys.elevenlabs]);
if (!context.settings.tab) {
return null;
}
return (
<Drawer size="50rem"
position='right'
opened={!!context.settings.tab}
onClose={() => context.settings.close()}
withCloseButton={false}>
<Container>
<Tabs defaultValue={context.settings.tab} style={{ margin: '0rem' }}>
<Tabs.List grow={small}>
<Tabs.Tab value="options">Options</Tabs.Tab>
<Tabs.Tab value="user">User</Tabs.Tab>
<Tabs.Tab value="speech">Speech</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="user">
<Settings>
<Grid style={{ marginBottom: '1.618rem' }} gutter={24}>
<Grid.Col span={12}>
<section className={context.settings.option === 'openai-api-key' ? 'focused' : ''}>
<h3>Your OpenAI API Key</h3>
<TextInput
placeholder="Paste your API key here"
value={context.apiKeys.openai || ''}
onChange={event => {
setParameters({ ...parameters, apiKey: event.currentTarget.value });
context.apiKeys.setOpenAIApiKey(event.currentTarget.value);
}} />
<p><a href="https://platform.openai.com/account/api-keys" target="_blank">Find your API key here.</a> Your API key is stored only on this device and never transmitted to anyone except OpenAI.</p>
<p>OpenAI API key usage is billed at a pay-as-you-go rate, separate from your ChatGPT subscription.</p>
</section>
</Grid.Col>
</Grid>
</Settings>
</Tabs.Panel>
<Tabs.Panel value="options">
<Settings>
<Grid style={{ marginBottom: '1.618rem' }} gutter={24}>
<Grid.Col span={12}>
<section className={context.settings.option === 'system-prompt' ? 'focused' : ''}>
<h3>System Prompt</h3>
<Textarea
value={parameters.initialSystemPrompt || defaultSystemPrompt}
onChange={event => setParameters({ ...parameters, initialSystemPrompt: event.currentTarget.value })}
minRows={5}
maxRows={10}
autosize />
<p style={{ marginBottom: '0.7rem' }}>The System Prompt is shown to ChatGPT by the "System" before your first message. The <code style={{ whiteSpace: 'nowrap' }}>{'{{ datetime }}'}</code> tag is automatically replaced by the current date and time.</p>
{parameters.initialSystemPrompt && (parameters.initialSystemPrompt?.trim() !== defaultSystemPrompt.trim()) && <Button size="xs" compact variant="light" onClick={() => setParameters({ ...parameters, initialSystemPrompt: defaultSystemPrompt })}>
Reset to default
</Button>}
</section>
</Grid.Col>
<Grid.Col span={12}>
<section className={context.settings.option === 'temperature' ? 'focused' : ''}>
<h3>Temperature ({parameters.temperature.toFixed(1)})</h3>
<Slider value={parameters.temperature} onChange={value => setParameters({ ...parameters, temperature: value })} step={0.1} min={0} max={1} precision={3} />
<p>The temperature parameter controls the randomness of the AI's responses. Lower values will make the AI more predictable, while higher values will make it more creative.</p>
</section>
</Grid.Col>
</Grid>
</Settings>
</Tabs.Panel>
<Tabs.Panel value="speech">
<Settings>
<Grid style={{ marginBottom: '1.618rem' }} gutter={24}>
<Grid.Col span={12}>
<section className={context.settings.option === 'elevenlabs-api-key' ? 'focused' : ''}>
<h3>Your ElevenLabs Text-to-Speech API Key (optional)</h3>
<TextInput placeholder="Paste your API key here" value={context.apiKeys.elevenlabs || ''} onChange={event => context.apiKeys.setElevenLabsApiKey(event.currentTarget.value)} />
<p>Give ChatGPT a realisic human voice by connecting your ElevenLabs account (preview the available voices below). <a href="https://beta.elevenlabs.io" target="_blank">Click here to sign up.</a></p>
<p>You can find your API key by clicking your avatar or initials in the top right of the ElevenLabs website, then clicking Profile. Your API key is stored only on this device and never transmitted to anyone except ElevenLabs.</p>
</section>
</Grid.Col>
<Grid.Col span={12}>
<section className={context.settings.option === 'elevenlabs-voice' ? 'focused' : ''}>
<h3>Voice</h3>
<Select
value={context.voice.id}
onChange={v => context.voice.setVoiceID(v!)}
data={voices.map(v => ({ label: v.name, value: v.voice_id }))} />
<audio controls style={{ display: 'none' }} id="voice-preview" key={context.voice.id}>
<source src={voices.find(v => v.voice_id === context.voice.id)?.preview_url} type="audio/mpeg" />
</audio>
<Button onClick={() => (document.getElementById('voice-preview') as HTMLMediaElement)?.play()} variant='light' compact style={{ marginTop: '1rem' }}>
<i className='fa fa-headphones' />
<span>Preview voice</span>
</Button>
</section>
</Grid.Col>
</Grid>
</Settings>
</Tabs.Panel>
</Tabs>
<div id="save">
<Button variant="light" fullWidth size="md" onClick={() => context.settings.close()}>
Save and Close
</Button>
</div>
</Container>
</Drawer>
)
}

View File

@ -1,11 +1,19 @@
import { useDebouncedValue } from "@mantine/hooks";
import React, { useState, useRef, useMemo, useEffect, useCallback } from "react";
import { useLocation, useNavigate, useParams } from "react-router-dom";
import { backend } from "./backend";
import ChatManagerInstance, { ChatManager } from "./chat-manager";
import { defaultElevenLabsVoiceID } from "./elevenlabs";
import { loadParameters, saveParameters } from "./parameters";
import { Parameters } from "./types";
import { useChat, UseChatResult } from "./use-chat";
export interface Context {
authenticated: boolean;
chat: ChatManager;
id: string | undefined | null;
currentChat: UseChatResult;
isShare: boolean;
apiKeys: {
openai: string | undefined | null;
setOpenAIApiKey: (apiKey: string | null) => void;
@ -21,13 +29,25 @@ export interface Context {
voice: {
id: string;
setVoiceID: (id: string) => void;
}
};
generating: boolean;
message: string;
parameters: Parameters;
setMessage: (message: string) => void;
setParameters: (parameters: Parameters) => void;
onNewMessage: (message?: string) => Promise<boolean>;
}
const AppContext = React.createContext<Context>({} as any);
export function useCreateAppContext(): Context {
const chat = useRef(ChatManagerInstance);
const { id } = useParams();
const pathname = useLocation().pathname;
const isShare = pathname.startsWith('/s/');
const navigate = useNavigate();
const chatManager = useRef(ChatManagerInstance);
const currentChat = useChat(chatManager.current, id, isShare);
const [authenticated, setAuthenticated] = useState(backend?.isAuthenticated || false);
const updateAuth = useCallback((authenticated: boolean) => setAuthenticated(authenticated), []);
@ -47,11 +67,15 @@ export function useCreateAppContext(): Context {
);
useEffect(() => {
localStorage.setItem('openai-api-key', openaiApiKey || '');
if (openaiApiKey) {
localStorage.setItem('openai-api-key', openaiApiKey || '');
}
}, [openaiApiKey]);
useEffect(() => {
localStorage.setItem('elevenlabs-api-key', elevenLabsApiKey || '');
if (elevenLabsApiKey) {
localStorage.setItem('elevenlabs-api-key', elevenLabsApiKey || '');
}
}, [elevenLabsApiKey]);
const [settingsTab, setSettingsTab] = useState<string | null | undefined>();
@ -63,9 +87,75 @@ export function useCreateAppContext(): Context {
localStorage.setItem('voice-id', voiceID);
}, [voiceID]);
const [generating, setGenerating] = useState(false);
const [message, setMessage] = useState('');
const [_parameters, setParameters] = useState<Parameters>(loadParameters(id));
useEffect(() => {
setParameters(loadParameters(id));
}, [id]);
const [parameters] = useDebouncedValue(_parameters, 2000);
useEffect(() => {
if (id) {
saveParameters(id, parameters);
}
saveParameters('', parameters);
}, [id, parameters]);
const onNewMessage = useCallback(async (message?: string) => {
if (isShare) {
return false;
}
if (!message?.trim().length) {
return false;
}
if (!openaiApiKey) {
setSettingsTab('user');
setOption('openai-api-key');
return false;
}
setGenerating(true);
if (id) {
await chatManager.current.sendMessage({
chatID: id,
content: message.trim(),
requestedParameters: {
...parameters,
apiKey: openaiApiKey,
},
parentID: currentChat.leaf?.id,
});
} else {
const id = await chatManager.current.createChat();
await chatManager.current.sendMessage({
chatID: id,
content: message.trim(),
requestedParameters: {
...parameters,
apiKey: openaiApiKey,
},
parentID: currentChat.leaf?.id,
});
navigate('/chat/' + id);
}
setTimeout(() => setGenerating(false), 4000);
return true;
}, [chatManager, openaiApiKey, id, parameters, message, currentChat.leaf]);
const context = useMemo<Context>(() => ({
authenticated,
chat: chat.current,
id,
chat: chatManager.current,
currentChat,
isShare,
apiKeys: {
openai: openaiApiKey,
elevenlabs: elevenLabsApiKey,
@ -88,7 +178,14 @@ export function useCreateAppContext(): Context {
id: voiceID,
setVoiceID,
},
}), [chat, authenticated, openaiApiKey, elevenLabsApiKey, settingsTab, option, voiceID]);
generating,
message,
parameters,
setMessage,
setParameters,
onNewMessage,
}), [chatManager, authenticated, openaiApiKey, elevenLabsApiKey, settingsTab, option, voiceID,
generating, message, parameters, onNewMessage, currentChat]);
return context;
}

View File

@ -6,26 +6,43 @@ import {
} from "react-router-dom";
import { MantineProvider } from '@mantine/core';
import { ModalsProvider } from '@mantine/modals';
import ChatPage from './components/page';
import { AppContextProvider } from './context';
import LandingPage from './components/pages/landing';
import ChatPage from './components/pages/chat';
import AboutPage from './components/pages/about';
import './index.scss';
const router = createBrowserRouter([
{
path: "/",
element: <ChatPage landing={true} />,
element: <AppContextProvider>
<LandingPage landing={true} />
</AppContextProvider>,
},
{
path: "/chat/:id",
element: <ChatPage />,
element: <AppContextProvider>
<ChatPage />
</AppContextProvider>,
},
{
path: "/s/:id",
element: <ChatPage share={true} />,
element: <AppContextProvider>
<ChatPage share={true} />
</AppContextProvider>,
},
{
path: "/s/:id/*",
element: <ChatPage share={true} />,
element: <AppContextProvider>
<ChatPage share={true} />
</AppContextProvider>,
},
{
path: "/about",
element: <AppContextProvider>
<AboutPage />
</AppContextProvider>,
},
]);
@ -36,11 +53,9 @@ const root = ReactDOM.createRoot(
root.render(
<React.StrictMode>
<MantineProvider theme={{ colorScheme: "dark" }}>
<AppContextProvider>
<ModalsProvider>
<RouterProvider router={router} />
</ModalsProvider>
</AppContextProvider>
<ModalsProvider>
<RouterProvider router={router} />
</ModalsProvider>
</MantineProvider>
</React.StrictMode>
);

30
src/menus.ts 100644
View File

@ -0,0 +1,30 @@
export interface MenuItem {
label: string;
link: string;
icon?: string;
}
export const primaryMenu: MenuItem[] = [
{
label: "About this app",
link: "/about",
},
{
label: "ChatGPT Prompts",
link: "https://github.com/f/awesome-chatgpt-prompts",
icon: "external-link-alt",
},
];
export const secondaryMenu: MenuItem[] = [
{
label: "Discord",
link: "https://discord.gg/mS5QvKykvv",
icon: "discord fab",
},
{
label: "GitHub",
link: "https://github.com/cogentapps/chat-with-gpt",
icon: "github fab",
},
];

134
src/tiktoken/dist/README.md vendored 100644
View File

@ -0,0 +1,134 @@
# ⏳ tiktoken
tiktoken is a [BPE](https://en.wikipedia.org/wiki/Byte_pair_encoding) tokeniser for use with
OpenAI's models, forked from the original tiktoken library to provide NPM bindings for Node and other JS runtimes.
The open source version of `tiktoken` can be installed from NPM:
```
npm install @dqbd/tiktoken
```
## Usage
Basic usage follows:
```typescript
import assert from "node:assert";
import { get_encoding, encoding_for_model } from "@dqbd/tiktoken";
const enc = get_encoding("gpt2");
assert(
new TextDecoder().decode(enc.decode(enc.encode("hello world"))) ===
"hello world"
);
// To get the tokeniser corresponding to a specific model in the OpenAI API:
const enc = encoding_for_model("text-davinci-003");
// Extend existing encoding with custom special tokens
const enc = encoding_for_model("gpt2", {
"<|im_start|>": 100264,
"<|im_end|>": 100265,
});
// don't forget to free the encoder after it is not used
enc.free();
```
If desired, you can create a Tiktoken instance directly with custom ranks, special tokens and regex pattern:
```typescript
import { Tiktoken } from "../pkg";
import { readFileSync } from "fs";
const encoder = new Tiktoken(
readFileSync("./ranks/gpt2.tiktoken").toString("utf-8"),
{ "<|endoftext|>": 50256, "<|im_start|>": 100264, "<|im_end|>": 100265 },
"'s|'t|'re|'ve|'m|'ll|'d| ?\\p{L}+| ?\\p{N}+| ?[^\\s\\p{L}\\p{N}]+|\\s+(?!\\S)|\\s+"
);
```
## Compatibility
As this is a WASM library, there might be some issues with specific runtimes. If you encounter any issues, please open an issue.
| Runtime | Status | Notes |
| ------------------- | ------ | ------------------------------------------ |
| Node.js | ✅ | |
| Bun | ✅ | |
| Vite | ✅ | See [here](#vite) for notes |
| Next.js | ✅ | See [here](#nextjs) for notes |
| Vercel Edge Runtime | ✅ | See [here](#vercel-edge-runtime) for notes |
| Cloudflare Workers | 🚧 | Untested |
| Deno | ❌ | Currently unsupported |
### [Vite](#vite)
If you are using Vite, you will need to add both the `vite-plugin-wasm` and `vite-plugin-top-level-await`. Add the following to your `vite.config.js`:
```js
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [wasm(), topLevelAwait()],
});
```
### [Next.js](#nextjs)
Both API routes and `/pages` are supported with the following configuration. To overcome issues with importing Node.js version, you can import the package from `@dqbd/tiktoken/bundler` instead.
```typescript
import { get_encoding } from "@dqbd/tiktoken/bundler";
import { NextApiRequest, NextApiResponse } from "next";
export default function handler(req: NextApiRequest, res: NextApiResponse) {
const encoder = get_encoding("gpt2");
const message = encoder.encode(`Hello World ${Math.random()}`);
encoder.free();
return res.status(200).json({ message });
}
```
Additional Webpack configuration is required.
```typescript
const config = {
webpack(config, { isServer, dev }) {
config.experiments = {
asyncWebAssembly: true,
layers: true,
};
return config;
},
};
```
### [Vercel Edge Runtime](#vercel-edge-runtime)
Vercel Edge Runtime does support WASM modules by adding a `?module` suffix. Initialize the encoder with the following snippet:
```typescript
import wasm from "@dqbd/tiktoken/tiktoken_bg.wasm?module";
import { init, get_encoding } from "@dqbd/tiktoken/init";
export const config = { runtime: "edge" };
export default async function (req: Request) {
await init((imports) => WebAssembly.instantiate(wasm, imports));
const encoder = get_encoding("cl100k_base");
const tokens = encoder.encode("hello world");
encoder.free();
return new Response(`${encoder.encode("hello world")}`);
}
```
## Acknowledgements
- https://github.com/zurawiki/tiktoken-rs

1
src/tiktoken/dist/bundler.d.ts vendored 100644
View File

@ -0,0 +1 @@
export * from "./tiktoken";

1
src/tiktoken/dist/bundler.js vendored 100644
View File

@ -0,0 +1 @@
export * from "./tiktoken";

8
src/tiktoken/dist/init.d.ts vendored 100644
View File

@ -0,0 +1,8 @@
/* tslint:disable */
/* eslint-disable */
export * from "./tiktoken";
export function init(
callback: (
imports: WebAssembly.Imports
) => Promise<WebAssembly.WebAssemblyInstantiatedSource | WebAssembly.Instance>
): Promise<void>;

20
src/tiktoken/dist/init.js vendored 100644
View File

@ -0,0 +1,20 @@
import * as imports from "./tiktoken_bg.js";
export async function init(cb) {
const res = await cb({
"./tiktoken_bg.js": imports,
});
const instance =
"instance" in res && res.instance instanceof WebAssembly.Instance
? res.instance
: res instanceof WebAssembly.Instance
? res
: null;
if (instance == null) throw new Error("Missing instance");
imports.__wbg_set_wasm(instance.exports);
return imports;
}
export * from "./tiktoken_bg.js";

37
src/tiktoken/dist/package.json vendored 100644
View File

@ -0,0 +1,37 @@
{
"name": "@dqbd/tiktoken",
"version": "1.0.0-alpha.1",
"description": "Javascript bindings for tiktoken",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/dqbd/tiktoken"
},
"dependencies": {
"node-fetch": "^3.3.0"
},
"files": [
"**/*"
],
"main": "tiktoken.node.js",
"types": "tiktoken.d.ts",
"exports": {
".": {
"types": "./tiktoken.d.ts",
"node": "./tiktoken.node.js",
"default": "./tiktoken.js"
},
"./bundler": {
"types": "./bundler.d.ts",
"default": "./bundler.js"
},
"./init": {
"types": "./init.d.ts",
"default": "./init.js"
},
"./tiktoken_bg.wasm": {
"types": "./tiktoken_bg.wasm.d.ts",
"default": "./tiktoken_bg.wasm"
}
}
}

108
src/tiktoken/dist/tiktoken.d.ts vendored 100644
View File

@ -0,0 +1,108 @@
/* tslint:disable */
/* eslint-disable */
export type TiktokenEncoding = "gpt2" | "r50k_base" | "p50k_base" | "p50k_edit" | "cl100k_base";
/**
* @param {TiktokenEncoding} encoding
* @param {Record<string, number>} [extend_special_tokens]
* @returns {Tiktoken}
*/
export function get_encoding(encoding: TiktokenEncoding, extend_special_tokens?: Record<string, number>): Tiktoken;
export type TiktokenModel =
| "text-davinci-003"
| "text-davinci-002"
| "text-davinci-001"
| "text-curie-001"
| "text-babbage-001"
| "text-ada-001"
| "davinci"
| "curie"
| "babbage"
| "ada"
| "code-davinci-002"
| "code-davinci-001"
| "code-cushman-002"
| "code-cushman-001"
| "davinci-codex"
| "cushman-codex"
| "text-davinci-edit-001"
| "code-davinci-edit-001"
| "text-embedding-ada-002"
| "text-similarity-davinci-001"
| "text-similarity-curie-001"
| "text-similarity-babbage-001"
| "text-similarity-ada-001"
| "text-search-davinci-doc-001"
| "text-search-curie-doc-001"
| "text-search-babbage-doc-001"
| "text-search-ada-doc-001"
| "code-search-babbage-code-001"
| "code-search-ada-code-001"
| "gpt2"
| "gpt-3.5-turbo"
| "gpt-3.5-turbo-0301";
/**
* @param {TiktokenModel} encoding
* @param {Record<string, number>} [extend_special_tokens]
* @returns {Tiktoken}
*/
export function encoding_for_model(model: TiktokenModel, extend_special_tokens?: Record<string, number>): Tiktoken;
/**
*/
export class Tiktoken {
free(): void;
/**
* @param {string} tiktoken_bfe
* @param {any} special_tokens
* @param {string} pat_str
*/
constructor(tiktoken_bfe: string, special_tokens: Record<string, number>, pat_str: string);
/**
* @param {string} text
* @param {any} allowed_special
* @param {any} disallowed_special
* @returns {Uint32Array}
*/
encode(text: string, allowed_special?: "all" | string[], disallowed_special?: "all" | string[]): Uint32Array;
/**
* @param {string} text
* @returns {Uint32Array}
*/
encode_ordinary(text: string): Uint32Array;
/**
* @param {string} text
* @param {any} allowed_special
* @param {any} disallowed_special
* @returns {any}
*/
encode_with_unstable(text: string, allowed_special?: "all" | string[], disallowed_special?: "all" | string[]): any;
/**
* @param {Uint8Array} bytes
* @returns {number}
*/
encode_single_token(bytes: Uint8Array): number;
/**
* @param {Uint32Array} tokens
* @returns {Uint8Array}
*/
decode(tokens: Uint32Array): Uint8Array;
/**
* @param {number} token
* @returns {Uint8Array}
*/
decode_single_token_bytes(token: number): Uint8Array;
/**
* @returns {any}
*/
token_byte_values(): Array<Array<number>>;
/**
*/
readonly name: string | undefined;
}

4
src/tiktoken/dist/tiktoken.js vendored 100644
View File

@ -0,0 +1,4 @@
import * as wasm from "./tiktoken_bg.wasm";
import { __wbg_set_wasm } from "./tiktoken_bg.js";
__wbg_set_wasm(wasm);
export * from "./tiktoken_bg.js";

View File

@ -0,0 +1,425 @@
let imports = {};
imports['./tiktoken_bg.js'] = module.exports;
let wasm;
const { TextEncoder, TextDecoder } = require(`util`);
const heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; }
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let WASM_VECTOR_LEN = 0;
let cachedUint8Memory0 = null;
function getUint8Memory0() {
if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8Memory0;
}
let cachedTextEncoder = new TextEncoder('utf-8');
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length);
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len);
const mem = getUint8Memory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3);
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachedInt32Memory0 = null;
function getInt32Memory0() {
if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) {
cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachedInt32Memory0;
}
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
let cachedUint32Memory0 = null;
function getUint32Memory0() {
if (cachedUint32Memory0 === null || cachedUint32Memory0.byteLength === 0) {
cachedUint32Memory0 = new Uint32Array(wasm.memory.buffer);
}
return cachedUint32Memory0;
}
function getArrayU32FromWasm0(ptr, len) {
return getUint32Memory0().subarray(ptr / 4, ptr / 4 + len);
}
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function passArray32ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 4);
getUint32Memory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {string} encoding
* @param {any} extend_special_tokens
* @returns {Tiktoken}
*/
module.exports.get_encoding = function(encoding, extend_special_tokens) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(encoding, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.get_encoding(retptr, ptr0, len0, addHeapObject(extend_special_tokens));
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
if (r2) {
throw takeObject(r1);
}
return Tiktoken.__wrap(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
};
/**
* @param {string} model
* @param {any} extend_special_tokens
* @returns {Tiktoken}
*/
module.exports.encoding_for_model = function(model, extend_special_tokens) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(model, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.encoding_for_model(retptr, ptr0, len0, addHeapObject(extend_special_tokens));
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
if (r2) {
throw takeObject(r1);
}
return Tiktoken.__wrap(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
};
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_export_3(addHeapObject(e));
}
}
/**
*/
class Tiktoken {
static __wrap(ptr) {
const obj = Object.create(Tiktoken.prototype);
obj.ptr = ptr;
return obj;
}
__destroy_into_raw() {
const ptr = this.ptr;
this.ptr = 0;
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_tiktoken_free(ptr);
}
/**
* @param {string} tiktoken_bfe
* @param {any} special_tokens
* @param {string} pat_str
*/
constructor(tiktoken_bfe, special_tokens, pat_str) {
const ptr0 = passStringToWasm0(tiktoken_bfe, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(pat_str, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.tiktoken_new(ptr0, len0, addHeapObject(special_tokens), ptr1, len1);
return Tiktoken.__wrap(ret);
}
/**
* @returns {string | undefined}
*/
get name() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.tiktoken_name(retptr, this.ptr);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
let v0;
if (r0 !== 0) {
v0 = getStringFromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 1);
}
return v0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {string} text
* @param {any} allowed_special
* @param {any} disallowed_special
* @returns {Uint32Array}
*/
encode(text, allowed_special, disallowed_special) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.tiktoken_encode(retptr, this.ptr, ptr0, len0, addHeapObject(allowed_special), addHeapObject(disallowed_special));
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
var r3 = getInt32Memory0()[retptr / 4 + 3];
if (r3) {
throw takeObject(r2);
}
var v1 = getArrayU32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 4);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {string} text
* @returns {Uint32Array}
*/
encode_ordinary(text) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.tiktoken_encode_ordinary(retptr, this.ptr, ptr0, len0);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 4);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {string} text
* @param {any} allowed_special
* @param {any} disallowed_special
* @returns {any}
*/
encode_with_unstable(text, allowed_special, disallowed_special) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.tiktoken_encode_with_unstable(retptr, this.ptr, ptr0, len0, addHeapObject(allowed_special), addHeapObject(disallowed_special));
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
if (r2) {
throw takeObject(r1);
}
return takeObject(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {Uint8Array} bytes
* @returns {number}
*/
encode_single_token(bytes) {
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_export_0);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.tiktoken_encode_single_token(this.ptr, ptr0, len0);
return ret >>> 0;
}
/**
* @param {Uint32Array} tokens
* @returns {Uint8Array}
*/
decode(tokens) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArray32ToWasm0(tokens, wasm.__wbindgen_export_0);
const len0 = WASM_VECTOR_LEN;
wasm.tiktoken_decode(retptr, this.ptr, ptr0, len0);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {number} token
* @returns {Uint8Array}
*/
decode_single_token_bytes(token) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.tiktoken_decode_single_token_bytes(retptr, this.ptr, token);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v0 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 1);
return v0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @returns {any}
*/
token_byte_values() {
const ret = wasm.tiktoken_token_byte_values(this.ptr);
return takeObject(ret);
}
}
module.exports.Tiktoken = Tiktoken;
module.exports.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
module.exports.__wbindgen_is_undefined = function(arg0) {
const ret = getObject(arg0) === undefined;
return ret;
};
module.exports.__wbg_stringify_029a979dfb73aa17 = function() { return handleError(function (arg0) {
const ret = JSON.stringify(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
module.exports.__wbindgen_string_get = function(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
module.exports.__wbindgen_error_new = function(arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
module.exports.__wbg_parse_3ac95b51fc312db8 = function() { return handleError(function (arg0, arg1) {
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
}, arguments) };
module.exports.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
const path = require('path').join(__dirname, 'tiktoken_bg.wasm');
const bytes = require('fs').readFileSync(path);
const wasmModule = new WebAssembly.Module(bytes);
const wasmInstance = new WebAssembly.Instance(wasmModule, imports);
wasm = wasmInstance.exports;
module.exports.__wasm = wasm;

421
src/tiktoken/dist/tiktoken_bg.js vendored 100644
View File

@ -0,0 +1,421 @@
let wasm;
export function __wbg_set_wasm(val) {
wasm = val;
}
const heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
function getObject(idx) { return heap[idx]; }
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
let WASM_VECTOR_LEN = 0;
let cachedUint8Memory0 = null;
function getUint8Memory0() {
if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8Memory0;
}
const lTextEncoder = typeof TextEncoder === 'undefined' ? (0, module.require)('util').TextEncoder : TextEncoder;
let cachedTextEncoder = new lTextEncoder('utf-8');
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length);
getUint8Memory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len);
const mem = getUint8Memory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3);
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachedInt32Memory0 = null;
function getInt32Memory0() {
if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) {
cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachedInt32Memory0;
}
const lTextDecoder = typeof TextDecoder === 'undefined' ? (0, module.require)('util').TextDecoder : TextDecoder;
let cachedTextDecoder = new lTextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
function getStringFromWasm0(ptr, len) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
let cachedUint32Memory0 = null;
function getUint32Memory0() {
if (cachedUint32Memory0 === null || cachedUint32Memory0.byteLength === 0) {
cachedUint32Memory0 = new Uint32Array(wasm.memory.buffer);
}
return cachedUint32Memory0;
}
function getArrayU32FromWasm0(ptr, len) {
return getUint32Memory0().subarray(ptr / 4, ptr / 4 + len);
}
function passArray8ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 1);
getUint8Memory0().set(arg, ptr / 1);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function passArray32ToWasm0(arg, malloc) {
const ptr = malloc(arg.length * 4);
getUint32Memory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function getArrayU8FromWasm0(ptr, len) {
return getUint8Memory0().subarray(ptr / 1, ptr / 1 + len);
}
/**
* @param {string} encoding
* @param {any} extend_special_tokens
* @returns {Tiktoken}
*/
export function get_encoding(encoding, extend_special_tokens) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(encoding, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.get_encoding(retptr, ptr0, len0, addHeapObject(extend_special_tokens));
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
if (r2) {
throw takeObject(r1);
}
return Tiktoken.__wrap(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {string} model
* @param {any} extend_special_tokens
* @returns {Tiktoken}
*/
export function encoding_for_model(model, extend_special_tokens) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(model, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.encoding_for_model(retptr, ptr0, len0, addHeapObject(extend_special_tokens));
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
if (r2) {
throw takeObject(r1);
}
return Tiktoken.__wrap(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_export_3(addHeapObject(e));
}
}
/**
*/
export class Tiktoken {
static __wrap(ptr) {
const obj = Object.create(Tiktoken.prototype);
obj.ptr = ptr;
return obj;
}
__destroy_into_raw() {
const ptr = this.ptr;
this.ptr = 0;
return ptr;
}
free() {
const ptr = this.__destroy_into_raw();
wasm.__wbg_tiktoken_free(ptr);
}
/**
* @param {string} tiktoken_bfe
* @param {any} special_tokens
* @param {string} pat_str
*/
constructor(tiktoken_bfe, special_tokens, pat_str) {
const ptr0 = passStringToWasm0(tiktoken_bfe, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(pat_str, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.tiktoken_new(ptr0, len0, addHeapObject(special_tokens), ptr1, len1);
return Tiktoken.__wrap(ret);
}
/**
* @returns {string | undefined}
*/
get name() {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.tiktoken_name(retptr, this.ptr);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
let v0;
if (r0 !== 0) {
v0 = getStringFromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 1);
}
return v0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {string} text
* @param {any} allowed_special
* @param {any} disallowed_special
* @returns {Uint32Array}
*/
encode(text, allowed_special, disallowed_special) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.tiktoken_encode(retptr, this.ptr, ptr0, len0, addHeapObject(allowed_special), addHeapObject(disallowed_special));
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
var r3 = getInt32Memory0()[retptr / 4 + 3];
if (r3) {
throw takeObject(r2);
}
var v1 = getArrayU32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 4);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {string} text
* @returns {Uint32Array}
*/
encode_ordinary(text) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.tiktoken_encode_ordinary(retptr, this.ptr, ptr0, len0);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU32FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 4);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {string} text
* @param {any} allowed_special
* @param {any} disallowed_special
* @returns {any}
*/
encode_with_unstable(text, allowed_special, disallowed_special) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(text, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
const len0 = WASM_VECTOR_LEN;
wasm.tiktoken_encode_with_unstable(retptr, this.ptr, ptr0, len0, addHeapObject(allowed_special), addHeapObject(disallowed_special));
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var r2 = getInt32Memory0()[retptr / 4 + 2];
if (r2) {
throw takeObject(r1);
}
return takeObject(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {Uint8Array} bytes
* @returns {number}
*/
encode_single_token(bytes) {
const ptr0 = passArray8ToWasm0(bytes, wasm.__wbindgen_export_0);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.tiktoken_encode_single_token(this.ptr, ptr0, len0);
return ret >>> 0;
}
/**
* @param {Uint32Array} tokens
* @returns {Uint8Array}
*/
decode(tokens) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArray32ToWasm0(tokens, wasm.__wbindgen_export_0);
const len0 = WASM_VECTOR_LEN;
wasm.tiktoken_decode(retptr, this.ptr, ptr0, len0);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v1 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 1);
return v1;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @param {number} token
* @returns {Uint8Array}
*/
decode_single_token_bytes(token) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.tiktoken_decode_single_token_bytes(retptr, this.ptr, token);
var r0 = getInt32Memory0()[retptr / 4 + 0];
var r1 = getInt32Memory0()[retptr / 4 + 1];
var v0 = getArrayU8FromWasm0(r0, r1).slice();
wasm.__wbindgen_export_2(r0, r1 * 1);
return v0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
/**
* @returns {any}
*/
token_byte_values() {
const ret = wasm.tiktoken_token_byte_values(this.ptr);
return takeObject(ret);
}
}
export function __wbindgen_object_drop_ref(arg0) {
takeObject(arg0);
};
export function __wbindgen_is_undefined(arg0) {
const ret = getObject(arg0) === undefined;
return ret;
};
export function __wbg_stringify_029a979dfb73aa17() { return handleError(function (arg0) {
const ret = JSON.stringify(getObject(arg0));
return addHeapObject(ret);
}, arguments) };
export function __wbindgen_string_get(arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr0 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_export_0, wasm.__wbindgen_export_1);
var len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
export function __wbindgen_error_new(arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
export function __wbg_parse_3ac95b51fc312db8() { return handleError(function (arg0, arg1) {
const ret = JSON.parse(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
}, arguments) };
export function __wbindgen_throw(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};

Binary file not shown.

View File

@ -0,0 +1,20 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export function __wbg_tiktoken_free(a: number): void;
export function tiktoken_new(a: number, b: number, c: number, d: number, e: number): number;
export function tiktoken_name(a: number, b: number): void;
export function tiktoken_encode(a: number, b: number, c: number, d: number, e: number, f: number): void;
export function tiktoken_encode_ordinary(a: number, b: number, c: number, d: number): void;
export function tiktoken_encode_with_unstable(a: number, b: number, c: number, d: number, e: number, f: number): void;
export function tiktoken_encode_single_token(a: number, b: number, c: number): number;
export function tiktoken_decode(a: number, b: number, c: number, d: number): void;
export function tiktoken_decode_single_token_bytes(a: number, b: number, c: number): void;
export function tiktoken_token_byte_values(a: number): number;
export function get_encoding(a: number, b: number, c: number, d: number): void;
export function encoding_for_model(a: number, b: number, c: number, d: number): void;
export function __wbindgen_export_0(a: number): number;
export function __wbindgen_export_1(a: number, b: number, c: number): number;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_export_2(a: number, b: number): void;
export function __wbindgen_export_3(a: number): void;

View File

@ -0,0 +1,31 @@
{
"name": "@dqbd/tiktoken",
"version": "1.0.0-alpha.1",
"description": "Javascript bindings for tiktoken",
"license": "MIT",
"scripts": {
"build": "run-s build:*",
"build:cleanup": "rm -rf dist/",
"build:rank": "tsx scripts/inline_ranks.ts",
"build:wasm": "run-s wasm:*",
"build:postprocess": "tsx scripts/post_process.ts",
"wasm:bundler": "wasm-pack build --target bundler --release --out-dir dist && rm -rf dist/.gitignore dist/README.md dist/package.json",
"wasm:node": "wasm-pack build --target nodejs --release --out-dir dist/node && rm -rf dist/node/.gitignore dist/node/README.md dist/node/package.json",
"test": "yarn vitest"
},
"repository": {
"type": "git",
"url": "https://github.com/dqbd/tiktoken"
},
"dependencies": {
"node-fetch": "^3.3.0"
},
"devDependencies": {
"@types/node": "^18.14.4",
"npm-run-all": "^4.1.5",
"ts-morph": "^17.0.1",
"tsx": "^3.12.3",
"typescript": "^4.9.5",
"vitest": "^0.28.5"
}
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ESNext", "DOM"],
"module": "ES2020",
"moduleResolution": "node",
"strict": true,
"declaration": true,
"outDir": "./dist",
"allowSyntheticDefaultImports": true,
},
"include": ["./**/*.ts", "./**/*.js"],
"exclude": ["node_modules", "dist"]
}

100
src/tokenizer.ts 100644
View File

@ -0,0 +1,100 @@
import { encoding_for_model } from "./tiktoken/dist/tiktoken";
import { OpenAIMessage } from "./types";
const enc = encoding_for_model("gpt-3.5-turbo");
export function getTokenCount(input: string): number {
return enc.encode(input).length;
}
export function shortenStringToTokenCount(input: string, targetTokenCount: number) {
const tokens = enc.encode(input);
const buffer = enc.decode(tokens.slice(0, targetTokenCount));
return new TextDecoder().decode(buffer) + "(...)";
}
function serializeChatMLMessage(role: string, content: string) {
const encodedContent = JSON.stringify(content)
.replace(/^"/g, '').replace(/"$/g, '');
let chatml = '';
chatml += `{"token": "<|im_start|>"},\n `;
chatml += `"${role.toLocaleLowerCase}\\n${encodedContent}",\n `;
chatml += `{"token": "<|im_end|>"}, "\\n"`;
return chatml;
}
export function getTokenCountForMessages(messages: OpenAIMessage[]): number {
let chatml = '[\n';
for (let i = 0; i < messages.length; i++) {
const m = messages[i];
const serializeMessage = serializeChatMLMessage(m.role, m.content);
chatml += ' ' + serializeMessage;
if (i < messages.length - 1) {
chatml += ',';
}
chatml += '\n';
}
chatml += ']';
return getTokenCount(chatml);
}
export function selectMessagesToSendSafely(messages: OpenAIMessage[]) {
const maxTokens = 2048;
if (getTokenCountForMessages(messages) <= maxTokens) {
return messages;
}
const insertedSystemMessage = serializeChatMLMessage('system', 'Several messages not included due to space constraints');
const insertedSystemMessageTokenCount = getTokenCount(insertedSystemMessage);
const targetTokens = maxTokens - insertedSystemMessageTokenCount;
const firstUserMessageIndex = messages.findIndex(m => m.role === 'user');
let output = [...messages];
let removed = false;
// first, remove items in the 'middle' of the conversation until we're under the limit
for (let i = firstUserMessageIndex + 1; i < messages.length - 1; i++) {
if (getTokenCountForMessages(output) > targetTokens) {
output.splice(i, 1);
removed = true;
}
}
// if we're still over the limit, trim message contents from oldest to newest (excluding the latest)
if (getTokenCountForMessages(output) > targetTokens) {
for (let i = 0; i < output.length - 1 && getTokenCountForMessages(output) > targetTokens; i++) {
output[i].content = shortenStringToTokenCount(output[i].content, 20);
removed = true;
}
}
// if that still didn't work, just keep the system prompt and the latest message (truncated as needed)
if (getTokenCountForMessages(output) > targetTokens) {
const systemMessage = output.find(m => m.role === 'system')!;
const latestMessage = { ...messages[messages.length - 1] };
output = [systemMessage, latestMessage];
removed = true;
const excessTokens = Math.max(0, getTokenCountForMessages(output) - targetTokens);
if (excessTokens) {
const tokens = enc.encode(latestMessage.content);
const buffer = enc.decode(tokens.slice(0, Math.max(0, tokens.length - excessTokens)));
latestMessage.content = new TextDecoder().decode(buffer);
}
}
if (removed) {
output.splice(1, 0, {
role: 'system',
content: 'Several messages not included due to space constraints',
});
}
return output;
}

View File

@ -1,10 +1,18 @@
import { useCallback, useEffect, useState } from "react";
import { backend } from "./backend";
import { ChatManager } from "./chat-manager";
import { useAppContext } from "./context";
import { Chat, Message } from './types';
export function useChat(id: string | undefined | null, share = false) {
const context = useAppContext();
export interface UseChatResult {
chat: Chat | null | undefined;
chatLoadedAt: number;
messages: Message[];
messagesToDisplay: Message[];
leaf: Message | null | undefined;
}
export function useChat(chatManager: ChatManager, id: string | undefined | null, share = false): UseChatResult {
const [chat, setChat] = useState<Chat | null | undefined>(null);
const [version, setVersion] = useState(0);
@ -14,7 +22,7 @@ export function useChat(id: string | undefined | null, share = false) {
const update = useCallback(async () => {
if (id) {
if (!share) {
const c = context.chat.get(id);
const c = chatManager.get(id);
if (c) {
setChat(c);
setVersion(v => v + 1);
@ -35,8 +43,8 @@ export function useChat(id: string | undefined | null, share = false) {
useEffect(() => {
if (id) {
update();
context.chat.on(id, update);
setChat(context.chat.get(id));
chatManager.on(id, update);
setChat(chatManager.get(id));
setLoadedAt(Date.now());
} else {
setChat(null);
@ -44,7 +52,7 @@ export function useChat(id: string | undefined | null, share = false) {
}
return () => {
if (id) {
context.chat.off(id, update);
chatManager.off(id, update);
}
};
}, [id, update]);
@ -52,16 +60,18 @@ export function useChat(id: string | undefined | null, share = false) {
const leaf = chat?.messages.mostRecentLeaf();
let messages: Message[] = [];
let messagesToDisplay: Message[] = [];
if (leaf) {
messages = (chat?.messages.getMessageChainTo(leaf?.id) || [])
.filter(m => ['user', 'assistant'].includes(m.role)) || [];
messages = (chat?.messages.getMessageChainTo(leaf?.id) || []);
messagesToDisplay = messages.filter(m => ['user', 'assistant'].includes(m.role)) || [];
}
return {
chat,
chatLoadedAt,
messages,
messagesToDisplay,
leaf,
};
}