diff --git a/src/components/chat/Conversation.jsx b/src/components/chat/Conversation.jsx index 30558f6..96f4b08 100644 --- a/src/components/chat/Conversation.jsx +++ b/src/components/chat/Conversation.jsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from "react"; import ConversationBubble from "./ConversationBubble"; -import { Send, StopCircleFill } from 'react-bootstrap-icons'; +import { FileImageFill, FileTextFill, Paperclip, Send, StopCircleFill } from 'react-bootstrap-icons'; import useIDB from "../../utils/idb"; import { isModelLoaded, loadModel } from '../../utils/workers/worker' import { getCompletionFunctions } from "../../utils/workers"; @@ -11,6 +11,7 @@ export default function Conversation({ uid }) { const [message, setMessage] = useState(''); const [pending_message, setPendingMessage] = useState(''); const [hide_pending, setHidePending] = useState(true); + const [upload_file, setUploadFile] = useState(null); const chat_functions = useRef(getCompletionFunctions()); const idb = useIDB(); @@ -45,7 +46,6 @@ export default function Conversation({ uid }) { const user_msg = {role: 'user', content: message} setConversation([...conversation, user_msg]) setMessage(''); - setHidePending(false); function cb(text, isFinished) { if(!isFinished) { @@ -69,12 +69,41 @@ export default function Conversation({ uid }) { } } - if(chat_functions.current.type === "Wllama") { + let messages = []; + setHidePending(false); + + if(chat_functions.current.platform === "Wllama") { if(!isModelLoaded()) { - await loadModel(); + await loadModel('completion', (progress)=>{ + setPendingMessage( + typeof progress === 'number' ? + `**Downloading model, ${progress}% completed**` : + '**Loading model...**' + ) + }); + setPendingMessage('') } + messages = [user_msg]; + } else { + let user_message = user_msg; + if(upload_file) { + const is_img = upload_file.type.startsWith('image') + const file_obj = { + content: new Uint8Array(await upload_file.arrayBuffer()) + } + if(!is_img) file_obj.name = upload_file.name; + user_message[ + is_img ? 'image' : 'document' + ] = file_obj; + setUploadFile(null); + } + messages = [ + ...conversation, + user_message + ] } - const result = await chat_functions.current.completions([user_msg], cb); + + const result = await chat_functions.current.completions(messages, cb); if(!result) { setPendingMessage(''); setHidePending(true); @@ -85,6 +114,7 @@ export default function Conversation({ uid }) { useEffect(()=>{ uid && getConversationByUid(); + setUploadFile(null); // eslint-disable-next-line }, [uid]); @@ -115,14 +145,31 @@ export default function Conversation({ uid }) { />
- -
- { - hide_pending ? - : - +
+ { + chat_functions.current && chat_functions.current.platform !== 'Wllama' && +
+ { + upload_file ? + upload_file.type.startsWith("image") ? + : : + + } + setUploadFile(evt.target.files.length ? evt.target.files[0] : null)} /> +
} - + +
+ { + hide_pending ? + : + + } + +
: diff --git a/src/styles/chat.css b/src/styles/chat.css index b2805b3..dd9b4dc 100644 --- a/src/styles/chat.css +++ b/src/styles/chat.css @@ -142,7 +142,8 @@ position: absolute; bottom: 0; left: 0; - display: block; + display: flex; + align-items: center; overflow: hidden; background-color: var(--normal-bg-color); } @@ -186,41 +187,62 @@ animation-delay: .8s; } -.chat > .conversation-main > .send-message-form > -input[type="text"] { +.chat > .conversation-main > .send-message-form > .input-container { width: calc(100% - 20px); - margin-left: 10px; - margin-top: 10px; - position: relative; height: var(--elem-size); - border: 1px solid lightgray; + position: relative; + margin: auto; + border: 2px solid lightgray; + overflow: hidden; border-radius: 30px; +} + +.chat > .conversation-main > .send-message-form > .input-container > +input[type="text"] { + width: 100%; + height: 100%; + position: relative; + border: none; padding: 0px var(--elem-size) 0px 10px; } -.chat > .conversation-main > .send-message-form > +.chat > .conversation-main > .send-message-form > .input-container > input[type="text"]:focus { outline: none; } -.chat > .conversation-main > .send-message-form > -.send-message-button-container { +.chat > .conversation-main > .send-message-form > .input-container > +.button-container { position: absolute; width: var(--elem-size); height: var(--elem-size); - right: 10px; - top: 10px; + right: 0; + top: 0; display: flex; + z-index: 1; + background-color: rgba(230, 230, 230, 0.6); +} +.chat > .conversation-main > .send-message-form > .input-container > +.button-container.file-upload { + right: unset; + left: 0; } -.chat > .conversation-main > .send-message-form > -.send-message-button-container:hover > .button-icon:not(.stop) { +.chat > .conversation-main > +.send-message-form > .input-container:has(>.button-container.file-upload) > +input[type="text"] { + padding-left: calc(5px + var(--elem-size)); +} + + +.chat > .conversation-main > .send-message-form > .input-container > +.button-container:hover > .button-icon.animated { transform: rotate(45deg); } -.chat > .conversation-main > .send-message-form > -.send-message-button-container > input[type="submit"]{ +.chat > .conversation-main > .send-message-form > .input-container > +.button-container > input{ width: 100%; height: 100%; position: absolute; @@ -229,13 +251,13 @@ input[type="text"]:focus { z-index: 2; opacity: 0; } -.chat > .conversation-main > .send-message-form > -.send-message-button-container > input[type="submit"].disabled { +.chat > .conversation-main > .send-message-form > .input-container > +.button-container > input.disabled { pointer-events: none; } -.chat > .conversation-main > .send-message-form > -.send-message-button-container > .button-icon { +.chat > .conversation-main > .send-message-form > .input-container > +.button-container > .button-icon { --size: 40%; width: var(--size); height: var(--size); @@ -243,3 +265,8 @@ input[type="text"]:focus { color: rgb(90, 90, 90); transition-duration: .3s; } + +.chat > .conversation-main > .send-message-form > .input-container > +.button-container > .button-icon.highlight { + color: dodgerblue; +} diff --git a/src/utils/workers/aws-worker.js b/src/utils/workers/aws-worker.js index d3feb46..6d1eff7 100644 --- a/src/utils/workers/aws-worker.js +++ b/src/utils/workers/aws-worker.js @@ -50,7 +50,7 @@ let abort_signal = false; /** * @typedef ImageMessage * @property {"png" | "jpeg" | "gif" | "webp"} format Format of image - * @property {Buffer} content Content in bytes + * @property {Uint8Array} content Content in bytes */ /** @@ -95,7 +95,11 @@ let abort_signal = false; */ export async function chatCompletions(messages, cb = null) { const { aws_model_id } = getPlatformSettings(); - if(!aws_model_id || (!bedrock_client && !await initBedrockClient())) return null; + if(!aws_model_id || (!bedrock_client && !await initBedrockClient())) { + console.log('no bedrock') + cb && cb("**Cannot Initialize AWS Bedrock Client**", true) + return null; + } const system = []; const normal_messages = []; diff --git a/src/utils/workers/index.js b/src/utils/workers/index.js index 0902406..f5873c4 100644 --- a/src/utils/workers/index.js +++ b/src/utils/workers/index.js @@ -6,7 +6,7 @@ import { chatCompletions as AwsCompletions, abortCompletion as AwsAbort } from " * @typedef CompletionFunctions * @property {Function} completions * @property {Function} abort - * @property {"Wllama" | "AWS"} type + * @property {"Wllama" | "AWS"} platform */ /** @@ -18,9 +18,9 @@ export function getCompletionFunctions() { switch(platform_settings.enabled_platform ) { case 'AWS': - return { completions: AwsCompletions, abort: AwsAbort, type: "AWS" } + return { completions: AwsCompletions, abort: AwsAbort, platform: "AWS" } default: - return { completions: WllamaCompletions, abort: WllamaAbort, type: "Wllama" } + return { completions: WllamaCompletions, abort: WllamaAbort, platform: "Wllama" } } } \ No newline at end of file diff --git a/src/utils/workers/worker.js b/src/utils/workers/worker.js index af32abe..4c6325b 100644 --- a/src/utils/workers/worker.js +++ b/src/utils/workers/worker.js @@ -64,11 +64,16 @@ export async function deleteModel(type = 'completion') { await instance.cacheManager.delete(cacheKey); } -export async function loadModel(type = 'completion') { +export async function loadModel(type = 'completion', cb = null) { // check if model already in cache const { instance, model_src } = engines[type]; try { + // if not downloaded, download first + if(!await isModelDownloaded(type)) { + await downloadModel(type, cb); + } + cb && cb('loading') await instance.loadModelFromUrl(model_src, { n_threads: 6, n_ctx: 4096,