diff --git a/src/components/chat/Conversation.jsx b/src/components/chat/Conversation.jsx index 4ad1894..8abdabb 100644 --- a/src/components/chat/Conversation.jsx +++ b/src/components/chat/Conversation.jsx @@ -1,19 +1,21 @@ import { useEffect, useRef, useState } from "react"; import ConversationBubble from "./ConversationBubble"; -import { FileImageFill, FileTextFill, Paperclip, Send, StopCircleFill } from 'react-bootstrap-icons'; +import { CheckCircle, FileImageFill, FileTextFill, Paperclip, Send, StopCircleFill, XCircle } from 'react-bootstrap-icons'; import useIDB from "../../utils/idb"; import { isModelLoaded, loadModel } from '../../utils/workers/worker' import { getCompletionFunctions } from "../../utils/workers"; import { setClient as setAwsClient } from "../../utils/workers/aws-worker"; import { setClient as setOpenaiClient } from "../../utils/workers/openai-worker"; -export default function Conversation({ uid, client, updateClient }) { +export default function Conversation({ uid, title, updateTitle, client, updateClient }) { const [conversation, setConversation] = useState([]); const [message, setMessage] = useState(''); const [pending_message, setPendingMessage] = useState(''); const [hide_pending, setHidePending] = useState(true); const [upload_file, setUploadFile] = useState(null); + const [edit_title, toggleEditTitle] = useState(false); + const [edited_title, setEditedTitle] = useState(title); const chat_functions = useRef(getCompletionFunctions()); const idb = useIDB(); @@ -53,7 +55,6 @@ export default function Conversation({ uid, client, updateClient }) { if(!isFinished) { setPendingMessage(text); } else { - setPendingMessage(''); setConversation([ ...conversation, user_msg, { role: 'assistant', content: text } @@ -67,7 +68,6 @@ export default function Conversation({ uid, client, updateClient }) { idb.updateOne( 'chat-history', {updatedAt: Date.now()}, [{uid}] ) - setHidePending(true); } } @@ -106,13 +106,16 @@ export default function Conversation({ uid, client, updateClient }) { ] } - const result = await chat_functions.current.completions(messages, cb); - if(!result) { - setPendingMessage(''); - setHidePending(true); - } else { - console.log(result) + await chat_functions.current.completions(messages, cb); + setPendingMessage(''); + setHidePending(true); + } + + function submitUpdateTitle() { + if(edited_title && edited_title !== title) { + updateTitle(edited_title); } + toggleEditTitle(false); } useEffect(()=>{ @@ -129,7 +132,11 @@ export default function Conversation({ uid, client, updateClient }) { }, [conversation, pending_message]) useEffect(()=>{ - if(!chat_functions.current) return; + setEditedTitle(title); + }, [title]) + + useEffect(()=>{ + if(!chat_functions.current || !uid) return; const platform = chat_functions.current.platform if(platform) { @@ -145,55 +152,66 @@ export default function Conversation({ uid, client, updateClient }) { })() } // eslint-disable-next-line - }, [client]) + }, [uid]) return (
{ uid ? <> -
- { conversation.map(({role, content}, idx) => { - return ( - - ) - }) } -
-
-
- { - 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 ? - : - +
+ { + edit_title ? + {evt.preventDefault(); submitUpdateTitle()}}> + setEditedTitle(evt.target.value)} /> + + {setEditedTitle(title); toggleEditTitle(false)}} /> + : +
toggleEditTitle(true)}>{ title }
+ } +
+
+ { conversation.map(({role, content}, idx) => { + return ( + + ) + }) } +
+
+
+ { + 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 ? + : + + } +
- +
+ :
Please select a conversation or start a new one.
} diff --git a/src/components/chat/ConversationBubble.jsx b/src/components/chat/ConversationBubble.jsx index ebffa5a..b6fc753 100644 --- a/src/components/chat/ConversationBubble.jsx +++ b/src/components/chat/ConversationBubble.jsx @@ -1,12 +1,12 @@ import { CircleFill } from "react-bootstrap-icons" import Markdown from "react-markdown" -export default function ConversationBubble({role, content, hidden}) { +export default function ConversationBubble({role, content, hidden, special}) { return (
{ - content ? - { content } : + content || !special? + { content || "**[ EMPTY MESSAGE ]**" } : <> diff --git a/src/components/chat/Ticket.jsx b/src/components/chat/Ticket.jsx index dd8afe8..9db8a9f 100644 --- a/src/components/chat/Ticket.jsx +++ b/src/components/chat/Ticket.jsx @@ -1,10 +1,20 @@ -export default function Ticket({ title, info, selectChat, is_selected }) { +import { XLg } from "react-bootstrap-icons"; + +export default function Ticket({ title, info, selectChat, is_selected, deleteHistory }) { + return (
selectChat(info)} > { title } + { + evt.stopPropagation(); + deleteHistory({ uid: info.uid, title }) + }} + />
) } \ No newline at end of file diff --git a/src/components/chat/Tickets.jsx b/src/components/chat/Tickets.jsx index d91491b..2e019de 100644 --- a/src/components/chat/Tickets.jsx +++ b/src/components/chat/Tickets.jsx @@ -3,14 +3,14 @@ import Ticket from "./Ticket"; import useIDB from "../../utils/idb"; import { genRandomID } from "../../utils/tools"; -export default function Tickets({selectChat, current_chat, history, setHistory}) { +export default function Tickets({selectChat, current_chat, history, setHistory, deleteHistory}) { const idb = useIDB(); async function syncHistory() { const history = await idb.getAll('chat-history') history.sort((a, b)=>b.updatedAt - a.updatedAt) - setHistory(history) + setHistory(history.map(e=>{return {...e, client: null}})) } async function startNewConversation() { @@ -27,7 +27,7 @@ export default function Tickets({selectChat, current_chat, history, setHistory}) const new_conv_info = await idb.getByID('chat-history', conv_id); new_conv_info && setHistory([ - new_conv_info, + {...new_conv_info, client: null}, ...history ]) selectChat(new_conv_info) @@ -50,9 +50,10 @@ export default function Tickets({selectChat, current_chat, history, setHistory}) const { title, uid } = elem; return ( ) diff --git a/src/components/chat/index.jsx b/src/components/chat/index.jsx index 51ce95f..288007e 100644 --- a/src/components/chat/index.jsx +++ b/src/components/chat/index.jsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import Tickets from "./Tickets"; import Conversation from "./Conversation"; import useIDB from "../../utils/idb"; @@ -8,6 +8,9 @@ export default function Chat() { const [chat, selectChat] = useState({}); const [history, setHistory] = useState([]); const idb = useIDB(); + const dialogRef = useRef(null); + const [showConfirm, toggleConfirm] = useState(false); + const [conv_to_delete, requestDelete] = useState(null); function updateChatClient(client) { selectChat({ @@ -19,14 +22,74 @@ export default function Chat() { history_cp.findIndex(e=>e.uid === chat.uid) ].client = client; setHistory(history_cp); + } + + function resetRequestDelete() { + requestDelete(null); + toggleConfirm(false); + } + + async function deleteHistory() { + if(!conv_to_delete) return; + + const {uid} = conv_to_delete; + await idb.deleteOne("chat-history", [{uid}]); + await idb.deleteAll("messages", [{'history-uid': uid}]); + setHistory(history.filter(e=>e.uid !== uid)); + uid === chat.uid && selectChat({}); + resetRequestDelete(); + } + + async function updateTitle(title) { + await idb.updateOne("chat-history", {title}, [{uid: chat.uid}]) + + selectChat({ + ...chat, title: title + }) - idb.updateOne('chat-history', {client}, [{uid:chat.uid}]) + let history_cp = [...history]; + history_cp[ + history_cp.findIndex(e=>e.uid === chat.uid) + ].title = title; + setHistory(history_cp); } + useEffect(()=>{ + if(dialogRef.current) { + if(showConfirm) dialogRef.current.showModal(); + else dialogRef.current.close(); + } + }, [showConfirm]) + + useEffect(()=>{ + conv_to_delete && toggleConfirm(true); + }, [conv_to_delete]) + return (
- - + + + +
+ Delete {conv_to_delete && conv_to_delete.title}? +
+
Yes, Delete
+
No, Go Back
+
) } \ No newline at end of file diff --git a/src/styles/chat.css b/src/styles/chat.css index dd9b4dc..7a056eb 100644 --- a/src/styles/chat.css +++ b/src/styles/chat.css @@ -7,6 +7,7 @@ display: block; --tickets-width: 250px; + --ticket-height: 50px; } .chat > .tickets { @@ -35,7 +36,7 @@ background-color: var(--ticket-bg-color); width: 100%; padding: 0px 10px; - height: 50px; + height: var(--ticket-height); align-content: center; font-size: 16px; position: relative; @@ -59,6 +60,46 @@ background-image: linear-gradient(to right, transparent, var(--ticket-bg-color) 70%); } +.chat > .tickets > .ticket > .delete-icon { + position: absolute; + right: 7px; + --size: calc(var(--ticket-height) * 0.3); + top: calc(var(--ticket-height) * 0.35); + width: var(--size); + height: var(--size); + z-index: 3; + color: gray; + display: none; +} +.chat > .tickets > .ticket:hover > .delete-icon { + display: block; +} +.chat > .tickets > .ticket:hover > .delete-icon:hover { + color: black; +} + +.chat > dialog { + border: 2px solid black; + padding: 30px; + border-radius: 10px; + text-align: center; +} +.chat > dialog:focus { + outline: none; +} + +.chat > dialog > .button { + border-bottom: 1px solid; + width: fit-content; + margin: auto; + margin-top: 7px; + padding: 0px 7px; + color: rgb(70, 70, 70); +} +.chat > dialog > .button:hover { + color: black; +} + .chat > .conversation-main { width: calc(100% - var(--tickets-width)); position: absolute; @@ -70,9 +111,10 @@ background-repeat: no-repeat; background-size: cover; + --title-bar-height: 40px; --send-input-height: 60px; --elem-size: 40px; - --bubbles-height: calc(100% - var(--send-input-height) - 10px); + --bubbles-height: calc(100% - var(--send-input-height) - var(--title-bar-height) - 10px); } .chat > .conversation-main::before { @@ -101,6 +143,39 @@ background-image: none; } +.chat > .conversation-main > .title-bar { + background-color: var(--normal-bg-color); + width: 100%; + height: var(--title-bar-height); + padding: 0px 15px; + align-content: center; + font-size: 15px; + font-weight: bold; + color: rgb(50, 50, 50); + + --elem-height: calc(var(--title-bar-height) - 14px); +} + +.chat > .conversation-main > .title-bar > form { + display: flex; +} + +.chat > .conversation-main > .title-bar .edit-title { + border: none; + background-color: transparent; + border-bottom: 1px solid gray; + padding: 0px 5px; + width: 100%; + font-size: 15px; + height: var(--elem-height); +} + +.chat > .conversation-main > .title-bar .btn { + height: var(--elem-height); + width: var(--elem-height); + margin-left: 15px; +} + .chat > .conversation-main > .bubbles { height: var(--bubbles-height); width: 100%; @@ -108,7 +183,7 @@ margin-bottom: 10px; position: absolute; left: 0; - top: 0; + top: var(--title-bar-height); padding: 20px 10px; } diff --git a/src/utils/idb/settings.js b/src/utils/idb/settings.js index 17bf3de..47c3a2b 100644 --- a/src/utils/idb/settings.js +++ b/src/utils/idb/settings.js @@ -31,12 +31,5 @@ export const versions = [ { name: 'json' } ] } - }, - { - 'chat-history': { - columns: [ - { 'name': "client" } - ] - } } ] \ No newline at end of file diff --git a/src/utils/workers/openai-worker.js b/src/utils/workers/openai-worker.js index 0eb0c26..99ff0e1 100644 --- a/src/utils/workers/openai-worker.js +++ b/src/utils/workers/openai-worker.js @@ -96,6 +96,7 @@ export async function chatCompletions(messages, cb = null) { if(chunk.choices[0].finish_reason) break; if(abort_signal) break; } + cb && cb(response_text, true); } catch(error) { console.error(error); cb && cb(`**${error.name}**:\n\`\`\`\n${error.message}\n\`\`\``, true);