Added GPT-4-vision
This commit is contained in:
+154
-97
@@ -2,6 +2,7 @@ import {
|
||||
IconArrowDown,
|
||||
IconBolt,
|
||||
IconBrandGoogle,
|
||||
IconPhoto,
|
||||
IconPlayerStop,
|
||||
IconRepeat,
|
||||
IconSend,
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
|
||||
import { useTranslation } from 'next-i18next';
|
||||
|
||||
import { Message } from '@/types/chat';
|
||||
import { Content, Message } from '@/types/chat';
|
||||
import { Plugin } from '@/types/plugin';
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
@@ -62,6 +63,40 @@ export const ChatInput = ({
|
||||
const [isModalVisible, setIsModalVisible] = useState(false);
|
||||
const [showPluginSelect, setShowPluginSelect] = useState(false);
|
||||
const [plugin, setPlugin] = useState<Plugin | null>(null);
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const imageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const handleImageInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const base64Image = reader.result;
|
||||
setImages((prevImages) => [...prevImages, base64Image as string]);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const ImagePreview = ({ images }: { images: string[] }) => {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{images.map((image, index) => (
|
||||
<img
|
||||
key={index}
|
||||
src={image}
|
||||
alt={`uploaded-${index}`}
|
||||
className="w-20 h-20 object-cover rounded-md border"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleImageInputClick = () => {
|
||||
imageInputRef.current?.click();
|
||||
};
|
||||
|
||||
const promptListRef = useRef<HTMLUListElement | null>(null);
|
||||
|
||||
@@ -97,7 +132,14 @@ export const ChatInput = ({
|
||||
return;
|
||||
}
|
||||
|
||||
onSend({ role: 'user', content }, plugin);
|
||||
var messageContent:Content[] = [{"type": "text", "text": content}];
|
||||
if(images && images.length >0){
|
||||
var imageMessages = images.map(image => { return {type: "image_url", image_url:{"url": image}}});
|
||||
messageContent = [...messageContent, ...imageMessages]
|
||||
}
|
||||
|
||||
onSend({ role: 'user', content:messageContent }, plugin);
|
||||
setImages([]);
|
||||
setContent('');
|
||||
setPlugin(null);
|
||||
|
||||
@@ -233,9 +275,8 @@ export const ChatInput = ({
|
||||
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]);
|
||||
|
||||
@@ -280,103 +321,119 @@ export const ChatInput = ({
|
||||
)}
|
||||
|
||||
<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>
|
||||
<ImagePreview images={images} />
|
||||
|
||||
{showPluginSelect && (
|
||||
<div className="absolute left-0 bottom-14 rounded bg-white dark:bg-[#343541]">
|
||||
<PluginSelect
|
||||
plugin={plugin}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
<div className="relative mx-0 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-0">
|
||||
<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 rounded bg-white dark:bg-[#343541]">
|
||||
<PluginSelect
|
||||
plugin={plugin}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
setShowPluginSelect(false);
|
||||
textareaRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
onPluginChange={(plugin: Plugin) => {
|
||||
setPlugin(plugin);
|
||||
setShowPluginSelect(false);
|
||||
textareaRef.current?.focus();
|
||||
}
|
||||
}}
|
||||
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-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'
|
||||
}`,
|
||||
}}
|
||||
placeholder={
|
||||
t('Type a message or type "/" to select a prompt...') || ''
|
||||
}
|
||||
value={content}
|
||||
rows={1}
|
||||
onCompositionStart={() => setIsTyping(true)}
|
||||
onCompositionEnd={() => setIsTyping(false)}
|
||||
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}
|
||||
>
|
||||
{messageIsStreaming ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-t-2 border-neutral-800 opacity-60 dark:border-neutral-100"></div>
|
||||
) : (
|
||||
<IconSend size={18} />
|
||||
if (textareaRef && textareaRef.current) {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showScrollDownButton && (
|
||||
<div className="absolute bottom-12 right-0 lg:bottom-0 lg:-right-10">
|
||||
<button
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-neutral-300 text-gray-800 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-neutral-200"
|
||||
onClick={onScrollDownClick}
|
||||
>
|
||||
<IconArrowDown size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPromptList && filteredPrompts.length > 0 && (
|
||||
<div className="absolute bottom-12 w-full">
|
||||
<PromptList
|
||||
activePromptIndex={activePromptIndex}
|
||||
prompts={filteredPrompts}
|
||||
onSelect={handleInitModal}
|
||||
onMouseOver={setActivePromptIndex}
|
||||
promptListRef={promptListRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isModalVisible && (
|
||||
<VariableModal
|
||||
prompt={filteredPrompts[activePromptIndex]}
|
||||
variables={variables}
|
||||
onSubmit={handleSubmit}
|
||||
onClose={() => setIsModalVisible(false)}
|
||||
<input
|
||||
ref={imageInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
style={{ display: 'none' }}
|
||||
onChange={handleImageInputChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="absolute left-10 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={handleImageInputClick}
|
||||
>
|
||||
<IconPhoto size={20} />
|
||||
</button>
|
||||
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
className="m-0 w-full resize-none border-0 bg-transparent p-0 py-2 pr-8 pl-20 text-black dark:bg-transparent dark:text-white md:py-3 md:pl-20"
|
||||
style={{
|
||||
resize: 'none',
|
||||
bottom: `${textareaRef?.current?.scrollHeight}px`,
|
||||
maxHeight: '400px',
|
||||
overflow: `${textareaRef.current && textareaRef.current.scrollHeight > 400
|
||||
? 'auto'
|
||||
: 'hidden'
|
||||
}`,
|
||||
}}
|
||||
placeholder={
|
||||
t('Type a message or type "/" to select a prompt...') || ''
|
||||
}
|
||||
value={content}
|
||||
rows={1}
|
||||
onCompositionStart={() => setIsTyping(true)}
|
||||
onCompositionEnd={() => setIsTyping(false)}
|
||||
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}
|
||||
>
|
||||
{messageIsStreaming ? (
|
||||
<div className="h-4 w-4 animate-spin rounded-full border-t-2 border-neutral-800 opacity-60 dark:border-neutral-100"></div>
|
||||
) : (
|
||||
<IconSend size={18} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
{showScrollDownButton && (
|
||||
<div className="absolute bottom-12 right-0 lg:bottom-0 lg:-right-10">
|
||||
<button
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full bg-neutral-300 text-gray-800 shadow-md hover:shadow-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-gray-700 dark:text-neutral-200"
|
||||
onClick={onScrollDownClick}
|
||||
>
|
||||
<IconArrowDown size={18} />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPromptList && filteredPrompts.length > 0 && (
|
||||
<div className="absolute bottom-12 w-full">
|
||||
<PromptList
|
||||
activePromptIndex={activePromptIndex}
|
||||
prompts={filteredPrompts}
|
||||
onSelect={handleInitModal}
|
||||
onMouseOver={setActivePromptIndex}
|
||||
promptListRef={promptListRef}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isModalVisible && (
|
||||
<VariableModal
|
||||
prompt={filteredPrompts[activePromptIndex]}
|
||||
variables={variables}
|
||||
onSubmit={handleSubmit}
|
||||
onClose={() => setIsModalVisible(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-3 pt-2 pb-3 text-center text-[12px] text-black/50 dark:text-white/50 md:px-4 md:pt-3 md:pb-6">
|
||||
|
||||
Reference in New Issue
Block a user