diff --git a/components/chat-page/chatMain.js b/components/chat-page/chatMain.js index 3daf522..78a2d49 100644 --- a/components/chat-page/chatMain.js +++ b/components/chat-page/chatMain.js @@ -1,7 +1,7 @@ import useConversation from "../../global/useConversation.js"; import useHistory from "../../global/useHistory.js"; import useModelSettings from "../../global/useModelSettings.js"; -import { formatJSON } from "../../tools/conversationFormat.js"; +import { formatJSON, formatMarkdown } from "../../tools/conversationFormat.js"; import showMessage from "../../tools/message.js"; import request from "../../tools/request.js"; import getSVG from "../../tools/svgs.js"; @@ -168,7 +168,7 @@ async function sendMessage(message, send) { top: main_elem.scrollHeight, behavior: 'smooth' }) - const [bot_answer, updateMessage] = createBlock('assistant'); + const [bot_answer, elements] = createBlock('assistant'); main_elem.appendChild(bot_answer); let content = '' @@ -183,15 +183,12 @@ async function sendMessage(message, send) { } }, true) - await send(response, msg=>{ - content = msg; - updateMessage(msg); - }); + await send(response, elements, c=>content = c); } catch(error) { error; if(content) content+=' ...' content += '(Message Abroted)' - updateMessage(content) + elements.main = content; } finally { appendConversationMessage([ { role: 'user', message }, @@ -210,28 +207,43 @@ function sendMessageWaiting(msg) { } async function sendMessageStream(msg) { - return sendMessage(msg, async (response, updateMessage) => { - let resp_content = '' + return sendMessage(msg, async (response, elements, updateContent) => { + const { main, pending, started } = elements; + let msg_started = false; const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); - let pending_content = '' + let resp_content = '', pending_content = '', html_content = '', end_block = null while(true) { const {value, done} = await reader.read(); if(done) break; pending_content += value; - if(pending_content.includes('\n\n')) { - const splitted_content = pending_content.split('\n\n') + if(pending_content.includes('}\n\n')) { + const splitted_content = pending_content.split('}\n\n') try { - const json = JSON.parse(splitted_content.shift().replace('data: ', '')) + if(!msg_started) { + msg_started = true; + started(); + } + const json = JSON.parse(splitted_content.shift().replace('data: ', '')+'}') + pending_content = splitted_content.join('}\n\n') resp_content += json.content; - updateMessage(resp_content); - pending_content = splitted_content.join('') + html_content += json.content; + const parsed_md = formatMarkdown(html_content, main, pending, end_block); + main_elem.scrollTo({ + top: main_elem.scrollHeight, + behavior: 'smooth' + }) + if(parsed_md) { + html_content = parsed_md[0]; + end_block = parsed_md[1]; + } + updateContent(resp_content); if(json.stop) break; - } catch(error) { + } catch(error) { console.error(error); } } } - return resp_content; + formatMarkdown(html_content, main, pending, end_block, true); }) } @@ -262,28 +274,28 @@ function createBlock(role, msg = '') { block.appendChild(message); if(role === 'assistant') { - message.innerHTML = ` - ${getSVG('circle-fill', 'dot-animation dot-1')} - ${getSVG('circle-fill', 'dot-animation dot-2')} - ${getSVG('circle-fill', 'dot-animation dot-3')}` - block.insertAdjacentHTML("afterbegin", ``) + if(!msg) { + message.innerHTML = ` + ${getSVG('circle-fill', 'dot-animation dot-1')} + ${getSVG('circle-fill', 'dot-animation dot-2')} + ${getSVG('circle-fill', 'dot-animation dot-3')}` + } } + const pending_elem = document.createElement('div'); + if(msg) { - message.textContent = msg; + message.appendChild(pending_elem); + formatMarkdown(msg, message, pending_elem, null, true); } return [ block, - (msg) => { - if(msg) { - message.textContent = msg; - main_elem.scrollTo({ - top: main_elem.scrollHeight, - behavior: 'smooth' - }) - } + { + main: message, + pending: pending_elem, + started: ()=>{ message.innerHTML = ''; message.appendChild(pending_elem); } } ] } \ No newline at end of file diff --git a/styles/chat_page.css b/styles/chat_page.css index 9252484..369b654 100644 --- a/styles/chat_page.css +++ b/styles/chat_page.css @@ -247,6 +247,20 @@ animation-delay: .6s; } +.message .code-block { + background-color: #1f1f1f; + color: white; + padding: 20px 15px; + border-radius: 7px; + margin-top: 5px; +} + +.message .inline-code { + background-color: var(--gray-bg); + padding: 3px 7px; + border-radius: 6px; +} + #chat-page #chat-main #submit-chat { position: absolute; width: calc(100% - var(--conversation-main-side-padding)); diff --git a/tools/conversationFormat.js b/tools/conversationFormat.js index 1730b3c..b2e5df5 100644 --- a/tools/conversationFormat.js +++ b/tools/conversationFormat.js @@ -1,8 +1,74 @@ -// export function formatMarkdown(input) { -// input.split('\n').map(line=>{ - -// }) -// } +export function formatMarkdown(str, target_elem, pending_elem, end_special_block = null, force = false) { + if(!str.includes('\n') && !force) { + pending_elem.textContent = str; + return null; + } + const whole_lines = str.split('\n'); + let content_left = '' + if(!force) content_left = whole_lines.pop(); + pending_elem.textContent = content_left; + + function parseSingleLine(pattern_name) { + return (_, group_1, group_2) => { + switch(pattern_name) { + case 'header': + return `${group_2}`; + case 'bold': return `${group_1}`; + case 'italic': return `${group_1}`; + case 'bold-italic': return `${group_1}`; + case 'hr': return ''; + case 'inline-code': return `${group_2||group_1}`; + } + } + } + + function parseLine(line) { + // test if this line is start/end of a code block + const match_code = line.match(/`{3,}/) + if(match_code) { + if(end_special_block) { + if(match_code[0] === end_special_block) { + end_special_block = null; + target_elem.appendChild(pending_elem) + return; + } + } else{ + const pattern = match_code[0] + const elem = document.createElement('div') + elem.className = 'code-block'; + target_elem.appendChild(elem); + + end_special_block = pattern; + elem.appendChild(pending_elem); + return; + } + } + + // replace white spaces, no need for single white space + line.replaceAll(' ', '  '); + if(end_special_block) { + pending_elem.insertAdjacentHTML("beforebegin",line||"
"); + } else { + const parsed_line = !line ? "
" : line + .replace(/(#{6}|#{5}|#{4}|#{3}|#{2}|#{1}) (.*$)/, parseSingleLine('header')) + .replaceAll(/[*_]{3,}(.+?)[*_]{3,}/g, parseSingleLine('bold-italic')) + .replaceAll(/\*\*(.+?)\*\*/g, parseSingleLine('bold')) + .replaceAll(/__(.+?)__/g, parseSingleLine('italic')) + .replaceAll(/^(\*|-){3,}$/g, parseSingleLine('hr')) + .replaceAll(/``(.+?)``|`(.+?)`/g, parseSingleLine('inline-code')) + + pending_elem.insertAdjacentHTML("beforebegin", parsed_line); + } + } + + whole_lines.forEach(line=>{ + parseLine(line) + }) + + force && pending_elem.remove(); + + return [content_left, end_special_block] +} export function formatJSON(conversation, {createdAt, name}) { const json =