Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
mckaywrigley authored Apr 4, 2023
1 parent e8150e7 commit e1f286e
Show file tree
Hide file tree
Showing 19 changed files with 1,695 additions and 277 deletions.
7 changes: 6 additions & 1 deletion .env.local.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Chatbot UI
DEFAULT_MODEL=gpt-3.5-turbo
DEFAULT_SYSTEM_PROMPT=You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.
OPENAI_API_KEY=YOUR_KEY
OPENAI_API_KEY=YOUR_KEY

# Google
GOOGLE_API_KEY=YOUR_API_KEY
GOOGLE_CSE_ID=YOUR_ENGINE_ID
18 changes: 10 additions & 8 deletions components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Conversation, Message } from '@/types/chat';
import { KeyValuePair } from '@/types/data';
import { ErrorMessage } from '@/types/error';
import { OpenAIModel, OpenAIModelID } from '@/types/openai';
import { Plugin } from '@/types/plugin';
import { Prompt } from '@/types/prompt';
import { throttle } from '@/utils';
import { IconArrowDown, IconClearAll, IconSettings } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import {
FC,
memo,
MutableRefObject,
memo,
useCallback,
useEffect,
useRef,
Expand All @@ -33,7 +34,11 @@ interface Props {
modelError: ErrorMessage | null;
loading: boolean;
prompts: Prompt[];
onSend: (message: Message, deleteCount?: number) => void;
onSend: (
message: Message,
deleteCount: number,
plugin: Plugin | null,
) => void;
onUpdateConversation: (
conversation: Conversation,
data: KeyValuePair,
Expand Down Expand Up @@ -116,8 +121,6 @@ export const Chat: FC<Props> = memo(
};
const throttledScrollDown = throttle(scrollDown, 250);

// appear scroll down button only when user scrolls up

useEffect(() => {
throttledScrollDown();
setCurrentMessage(
Expand Down Expand Up @@ -300,16 +303,15 @@ export const Chat: FC<Props> = memo(
textareaRef={textareaRef}
messageIsStreaming={messageIsStreaming}
conversationIsEmpty={conversation.messages.length === 0}
messages={conversation.messages}
model={conversation.model}
prompts={prompts}
onSend={(message) => {
onSend={(message, plugin) => {
setCurrentMessage(message);
onSend(message);
onSend(message, 0, plugin);
}}
onRegenerate={() => {
if (currentMessage) {
onSend(currentMessage, 2);
onSend(currentMessage, 2, null);
}
}}
/>
Expand Down
66 changes: 53 additions & 13 deletions components/Chat/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Message } from '@/types/chat';
import { OpenAIModel } from '@/types/openai';
import { Plugin } from '@/types/plugin';
import { Prompt } from '@/types/prompt';
import { IconPlayerStop, IconRepeat, IconSend } from '@tabler/icons-react';
import {
IconBolt,
IconBrandGoogle,
IconPlayerStop,
IconRepeat,
IconSend,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import {
FC,
Expand All @@ -12,16 +19,16 @@ import {
useRef,
useState,
} from 'react';
import { PluginSelect } from './PluginSelect';
import { PromptList } from './PromptList';
import { VariableModal } from './VariableModal';

interface Props {
messageIsStreaming: boolean;
model: OpenAIModel;
conversationIsEmpty: boolean;
messages: Message[];
prompts: Prompt[];
onSend: (message: Message) => void;
onSend: (message: Message, plugin: Plugin | null) => void;
onRegenerate: () => void;
stopConversationRef: MutableRefObject<boolean>;
textareaRef: MutableRefObject<HTMLTextAreaElement | null>;
Expand All @@ -31,7 +38,6 @@ export const ChatInput: FC<Props> = ({
messageIsStreaming,
model,
conversationIsEmpty,
messages,
prompts,
onSend,
onRegenerate,
Expand All @@ -47,6 +53,8 @@ export const ChatInput: FC<Props> = ({
const [promptInputValue, setPromptInputValue] = useState('');
const [variables, setVariables] = useState<string[]>([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [showPluginSelect, setShowPluginSelect] = useState(false);
const [plugin, setPlugin] = useState<Plugin | null>(null);

const promptListRef = useRef<HTMLUListElement | null>(null);

Expand Down Expand Up @@ -82,8 +90,9 @@ export const ChatInput: FC<Props> = ({
return;
}

onSend({ role: 'user', content });
onSend({ role: 'user', content }, plugin);
setContent('');
setPlugin(null);

if (window.innerWidth < 640 && textareaRef && textareaRef.current) {
textareaRef.current.blur();
Expand Down Expand Up @@ -149,6 +158,9 @@ export const ChatInput: FC<Props> = ({
} else if (e.key === 'Enter' && !isTyping && !isMobile() && !e.shiftKey) {
e.preventDefault();
handleSend();
} else if (e.key === '/' && e.metaKey) {
e.preventDefault();
setShowPluginSelect(!showPluginSelect);
}
};

Expand Down Expand Up @@ -214,8 +226,9 @@ export const ChatInput: FC<Props> = ({
if (textareaRef && textareaRef.current) {
textareaRef.current.style.height = 'inherit';
textareaRef.current.style.height = `${textareaRef.current?.scrollHeight}px`;
textareaRef.current.style.overflow = `${textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden'
}`;
textareaRef.current.style.overflow = `${
textareaRef?.current?.scrollHeight > 400 ? 'auto' : 'hidden'
}`;
}
}, [content]);

Expand All @@ -241,7 +254,7 @@ export const ChatInput: FC<Props> = ({
<div className="stretch mx-2 mt-4 flex flex-row gap-3 last:mb-2 md:mx-4 md:mt-[52px] md:last:mb-6 lg:mx-auto lg:max-w-3xl">
{messageIsStreaming && (
<button
className="absolute top-0 left-0 right-0 mb-3 md:mb-0 md:mt-2 mx-auto flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white"
className="absolute top-0 left-0 right-0 mx-auto mb-3 flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:mb-0 md:mt-2"
onClick={handleStopConversation}
>
<IconPlayerStop size={16} /> {t('Stop Generating')}
Expand All @@ -250,24 +263,50 @@ export const ChatInput: FC<Props> = ({

{!messageIsStreaming && !conversationIsEmpty && (
<button
className="absolute top-0 left-0 right-0 mb-3 md:mb-0 md:mt-2 mx-auto flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white"
className="absolute top-0 left-0 right-0 mx-auto mb-3 flex w-fit items-center gap-3 rounded border border-neutral-200 bg-white py-2 px-4 text-black hover:opacity-50 dark:border-neutral-600 dark:bg-[#343541] dark:text-white md:mb-0 md:mt-2"
onClick={onRegenerate}
>
<IconRepeat size={16} /> {t('Regenerate response')}
</button>
)}

<div className="relative mx-2 flex w-full flex-grow flex-col rounded-md border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-gray-900/50 dark:bg-[#40414F] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] sm:mx-4">
<button
className="absolute left-2 top-2 rounded-sm p-1 text-neutral-800 opacity-60 hover:bg-neutral-200 hover:text-neutral-900 dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200"
onClick={() => setShowPluginSelect(!showPluginSelect)}
onKeyDown={(e) => {}}
>
{plugin ? <IconBrandGoogle size={20} /> : <IconBolt size={20} />}
</button>

{showPluginSelect && (
<div className="absolute left-0 bottom-14 bg-white dark:bg-[#343541]">
<PluginSelect
plugin={plugin}
onPluginChange={(plugin: Plugin) => {
setPlugin(plugin);
setShowPluginSelect(false);

if (textareaRef && textareaRef.current) {
textareaRef.current.focus();
}
}}
/>
</div>
)}

<textarea
ref={textareaRef}
className="m-0 w-full resize-none border-0 bg-transparent p-0 py-2 pr-8 pl-2 text-black dark:bg-transparent dark:text-white md:py-3 md:pl-4"
className="m-0 w-full resize-none border-0 bg-transparent p-0 py-2 pr-8 pl-10 text-black dark:bg-transparent dark:text-white md:py-3 md:pl-10"
style={{
resize: 'none',
bottom: `${textareaRef?.current?.scrollHeight}px`,
maxHeight: '400px',
overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400
? 'auto' : 'hidden'
}`,
overflow: `${
textareaRef.current && textareaRef.current.scrollHeight > 400
? 'auto'
: 'hidden'
}`,
}}
placeholder={
t('Type a message or type "/" to select a prompt...') || ''
Expand All @@ -279,6 +318,7 @@ export const ChatInput: FC<Props> = ({
onChange={handleChange}
onKeyDown={handleKeyDown}
/>

<button
className="absolute right-2 top-2 rounded-sm p-1 text-neutral-800 opacity-60 hover:bg-neutral-200 hover:text-neutral-900 dark:bg-opacity-50 dark:text-neutral-100 dark:hover:text-neutral-200"
onClick={handleSend}
Expand Down
2 changes: 1 addition & 1 deletion components/Chat/ChatLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const ChatLoader: FC<Props> = () => {
className="group border-b border-black/10 bg-gray-50 text-gray-800 dark:border-gray-900/50 dark:bg-[#444654] dark:text-gray-100"
style={{ overflowWrap: 'anywhere' }}
>
<div className="flex gap-4 p-4 m-auto text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="m-auto flex gap-4 p-4 text-base md:max-w-2xl md:gap-6 md:py-6 lg:max-w-2xl lg:px-0 xl:max-w-3xl">
<div className="min-w-[40px] text-right font-bold">AI:</div>
<IconDots className="animate-pulse" />
</div>
Expand Down
58 changes: 58 additions & 0 deletions components/Chat/PluginSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { Plugin, PluginList } from '@/types/plugin';
import { useTranslation } from 'next-i18next';
import { FC, useEffect, useRef } from 'react';

interface Props {
plugin: Plugin | null;
onPluginChange: (plugin: Plugin) => void;
}

export const PluginSelect: FC<Props> = ({ plugin, onPluginChange }) => {
const { t } = useTranslation('chat');

const selectRef = useRef<HTMLSelectElement>(null);

useEffect(() => {
if (selectRef.current) {
selectRef.current.focus();
}
}, []);

return (
<div className="flex flex-col">
<div className="w-full rounded-lg border border-neutral-200 bg-transparent pr-2 text-neutral-900 dark:border-neutral-600 dark:text-white">
<select
ref={selectRef}
className="w-full cursor-pointer bg-transparent p-2"
placeholder={t('Select a plugin') || ''}
value={plugin?.id || ''}
onChange={(e) => {
onPluginChange(
PluginList.find(
(plugin) => plugin.id === e.target.value,
) as Plugin,
);
}}
>
<option
key="none"
value=""
className="dark:bg-[#343541] dark:text-white"
>
Select Plugin
</option>

{PluginList.map((plugin) => (
<option
key={plugin.id}
value={plugin.id}
className="dark:bg-[#343541] dark:text-white"
>
{plugin.name}
</option>
))}
</select>
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion components/Chat/PromptList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const PromptList: FC<Props> = ({
return (
<ul
ref={promptListRef}
className="z-10 w-full rounded border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-neutral-500 dark:bg-[#343541] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] max-h-52 overflow-scroll"
className="z-10 max-h-52 w-full overflow-scroll rounded border border-black/10 bg-white shadow-[0_0_10px_rgba(0,0,0,0.10)] dark:border-neutral-500 dark:bg-[#343541] dark:text-white dark:shadow-[0_0_15px_rgba(0,0,0,0.10)]"
>
{prompts.map((prompt, index) => (
<li
Expand Down
18 changes: 12 additions & 6 deletions components/Chatbar/Chatbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,8 @@ import { Conversation } from '@/types/chat';
import { KeyValuePair } from '@/types/data';
import { SupportedExportFormats } from '@/types/export';
import { Folder } from '@/types/folder';
import {
IconFolderPlus,
IconMessagesOff,
IconPlus,
} from '@tabler/icons-react';
import { PluginKey } from '@/types/plugin';
import { IconFolderPlus, IconMessagesOff, IconPlus } from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { FC, useEffect, useState } from 'react';
import { ChatFolders } from '../Folders/Chat/ChatFolders';
Expand All @@ -20,6 +17,7 @@ interface Props {
lightMode: 'light' | 'dark';
selectedConversation: Conversation;
apiKey: string;
pluginKeys: PluginKey[];
folders: Folder[];
onCreateFolder: (name: string) => void;
onDeleteFolder: (folderId: string) => void;
Expand All @@ -36,6 +34,8 @@ interface Props {
onClearConversations: () => void;
onExportConversations: () => void;
onImportConversations: (data: SupportedExportFormats) => void;
onPluginKeyChange: (pluginKey: PluginKey) => void;
onClearPluginKey: (pluginKey: PluginKey) => void;
}

export const Chatbar: FC<Props> = ({
Expand All @@ -44,6 +44,7 @@ export const Chatbar: FC<Props> = ({
lightMode,
selectedConversation,
apiKey,
pluginKeys,
folders,
onCreateFolder,
onDeleteFolder,
Expand All @@ -57,6 +58,8 @@ export const Chatbar: FC<Props> = ({
onClearConversations,
onExportConversations,
onImportConversations,
onPluginKeyChange,
onClearPluginKey,
}) => {
const { t } = useTranslation('sidebar');
const [searchTerm, setSearchTerm] = useState<string>('');
Expand Down Expand Up @@ -185,7 +188,7 @@ export const Chatbar: FC<Props> = ({
/>
</div>
) : (
<div className="flex flex-col gap-3 items-center text-sm leading-normal mt-8 text-white opacity-50">
<div className="mt-8 flex flex-col items-center gap-3 text-sm leading-normal text-white opacity-50">
<IconMessagesOff />
{t('No conversations.')}
</div>
Expand All @@ -195,12 +198,15 @@ export const Chatbar: FC<Props> = ({
<ChatbarSettings
lightMode={lightMode}
apiKey={apiKey}
pluginKeys={pluginKeys}
conversationsCount={conversations.length}
onToggleLightMode={onToggleLightMode}
onApiKeyChange={onApiKeyChange}
onClearConversations={onClearConversations}
onExportConversations={onExportConversations}
onImportConversations={onImportConversations}
onPluginKeyChange={onPluginKeyChange}
onClearPluginKey={onClearPluginKey}
/>
</div>
);
Expand Down
Loading

0 comments on commit e1f286e

Please sign in to comment.