push (#414)
This commit is contained in:
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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(
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
@@ -12,6 +19,7 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { PluginSelect } from './PluginSelect';
|
||||
import { PromptList } from './PromptList';
|
||||
import { VariableModal } from './VariableModal';
|
||||
|
||||
@@ -19,9 +27,8 @@ 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>;
|
||||
@@ -31,7 +38,6 @@ export const ChatInput: FC<Props> = ({
|
||||
messageIsStreaming,
|
||||
model,
|
||||
conversationIsEmpty,
|
||||
messages,
|
||||
prompts,
|
||||
onSend,
|
||||
onRegenerate,
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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')}
|
||||
@@ -250,7 +263,7 @@ 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')}
|
||||
@@ -258,16 +271,42 @@ export const ChatInput: FC<Props> = ({
|
||||
)}
|
||||
|
||||
<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...') || ''
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user