Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Chat Mate</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
| <link rel="stylesheet" href="/static/style.css" /> | |
| <link rel="stylesheet" href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css"> | |
| <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script> | |
| <!-- Prism --> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism-tomorrow.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-numbers/prism-line-numbers.css"> | |
| <script src="https://cdn.jsdelivr.net/npm/prismjs/prism.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-numbers/prism-line-numbers.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup-templating.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-java.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-c.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-cpp.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-shell-session.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-go.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-php.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-ruby.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-kotlin.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-swift.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-rust.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-scala.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-dart.min.js"></script> | |
| <script src="https://unpkg.com/vue@3"></script> | |
| </head> | |
| <body> | |
| <div id="app" class="chat-container"> | |
| <h2>💬 Chat Mate</h2> | |
| <div class="chat-box" ref="chatbox"> | |
| <div v-for="(msg, i) in history" :key="i" class="message" :class="msg.role"> | |
| <template v-if="msg.role === 'assistant' && msg.content.includes('```')"> | |
| <div class="message-content" v-html="renderCode(msg.content)"></div> | |
| {% raw %}<div class="timestamp" :class="msg.role">{{ msg.time }}</div>{% endraw %} | |
| </template> | |
| <template v-else> | |
| <div class="text-content" v-html="formatText(msg.content)"></div> | |
| {% raw %}<div class="timestamp" :class="msg.role">{{ msg.time }}</div>{% endraw %} | |
| </template> | |
| </div> | |
| <div v-if="loading" class="typing-indicator"> | |
| <span class="dot"></span><span class="dot"></span><span class="dot"></span> | |
| </div> | |
| </div> | |
| <div class="input-area"> | |
| <textarea v-model="message" placeholder="Ask something..." rows="2"></textarea> | |
| <button @click="sendMessage" :disabled="!message.trim() || loading">Send</button> | |
| </div> | |
| </div> | |
| <script> | |
| const { createApp, ref, nextTick } = Vue; | |
| createApp({ | |
| setup() { | |
| const message = ref(''); | |
| const history = ref([]); | |
| const loading = ref(false); | |
| const chatbox = ref(null); | |
| const scrollToBottom = () => { | |
| nextTick(() => { | |
| if (chatbox.value) chatbox.value.scrollTop = chatbox.value.scrollHeight; | |
| }); | |
| }; | |
| const formatText = (text) => { | |
| // ✅ Handle base64 images | |
| const imageRegex = /\[IMAGE_START\](.*?)\[IMAGE_END\]/gs; | |
| text = text.replace(imageRegex, (match, base64) => { | |
| const src = `data:image/png;base64,${base64.trim()}`; | |
| return `<img src="${src}" alt="Generated Image" class="chat-image"/>`; | |
| }); | |
| // ✅ Normalize line endings and remove excessive blank lines | |
| text = text.replace(/\r\n|\r/g, '\n'); | |
| text = text.replace(/\n{3,}/g, '\n\n'); | |
| // ✅ Parse fenced code blocks (```code```) | |
| text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => { | |
| const language = lang ? ` class="language-${lang}"` : ''; | |
| return `<pre><code${language}>${code.trim().replace(/</g, '<').replace(/>/g, '>')}</code></pre>`; | |
| }); | |
| // ✅ Parse blockquotes | |
| text = text.replace(/^> (.*)$/gm, '<blockquote>$1</blockquote>'); | |
| // ✅ Headings | |
| text = text.replace(/^### (.*)$/gm, '<h3>$1</h3>'); | |
| // ✅ Horizontal rules | |
| text = text.replace(/^---$/gm, '<hr>'); | |
| // ✅ Bold (**text**) and italic (*text*) | |
| text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>'); | |
| text = text.replace(/\*(.*?)\*/g, '<em>$1</em>'); | |
| // ✅ Emoji rendering using colon syntax (:smile:) | |
| const emojiMap = { | |
| smile: "😄", | |
| sad: "😢", | |
| heart: "❤️", | |
| thumbs_up: "👍", | |
| fire: "🔥", | |
| check: "✅", | |
| x: "❌", | |
| star: "⭐", | |
| rocket: "🚀", | |
| warning: "⚠️", | |
| }; | |
| text = text.replace(/:([a-z0-9_+-]+):/g, (match, name) => emojiMap[name] || match); | |
| // ✅ Unordered list (bullets) | |
| const listify = (lines, tag) => | |
| `<${tag}>` + | |
| lines.map(item => `<li>${item.replace(/^(\-|\d+\.)\s*/, '').trim()}</li>`).join('') + | |
| `</${tag}>`; | |
| text = text.replace( | |
| /((?:^[-*] .+(?:\n|$))+)/gm, | |
| (match) => listify(match.trim().split('\n'), 'ul') | |
| ); | |
| // ✅ Ordered list (fix separate `1.` items issue) | |
| text = text.replace(/^(\d+\. .+)$/gm, '__ORDERED__START__$1__ORDERED__END__'); | |
| text = text.replace( | |
| /__ORDERED__START__(\d+\. .+?)__ORDERED__END__/gs, | |
| (_, line) => `<ol><li>${line.replace(/^\d+\.\s*/, '')}</li></ol>` | |
| ); | |
| text = text.replace(/<\/ol>\s*<ol>/g, ''); | |
| // ✅ Markdown-style tables | |
| text = text.replace( | |
| /^\|(.+?)\|\n\|([-:| ]+)\|\n((?:\|.*\|\n?)*)/gm, | |
| (_, headerRow, dividerRow, bodyRows) => { | |
| const headers = headerRow.split('|').map(h => `<th>${h.trim()}</th>`).join(''); | |
| const rows = bodyRows.trim().split('\n').map(r => | |
| '<tr>' + r.split('|').map(cell => `<td>${cell.trim()}</td>`).join('') + '</tr>' | |
| ).join(''); | |
| return `<table><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`; | |
| } | |
| ); | |
| // ✅ Paragraphs and line breaks inside paragraphs | |
| const blocks = text.split(/\n{2,}/).map(block => { | |
| if ( | |
| block.startsWith('<h3>') || | |
| block.startsWith('<hr>') || | |
| block.startsWith('<ul>') || | |
| block.startsWith('<ol>') || | |
| block.startsWith('<table>') || | |
| block.startsWith('<pre>') || | |
| block.startsWith('<blockquote>') || | |
| block.startsWith('<img') | |
| ) { | |
| return block; | |
| } else { | |
| return `<p>${block.trim().replace(/\n/g, '<br>')}</p>`; | |
| } | |
| }); | |
| return blocks.join('\n'); | |
| }; | |
| const renderCode = (text) => { | |
| const codeBlocks = text.split(/```/); | |
| let output = ''; | |
| for (let i = 0; i < codeBlocks.length; i++) { | |
| if (i % 2 === 1) { | |
| const lines = codeBlocks[i].split('\n'); | |
| let langGuess = /^[a-zA-Z]+$/.test(lines[0]) ? lines[0].trim().toLowerCase() : ''; | |
| const codeLines = langGuess ? lines.slice(1) : lines; | |
| const rawCode = codeLines.join('\n'); | |
| if (!langGuess) langGuess = detectLanguageByKeywords(rawCode); | |
| const escapeHTML = (str) => | |
| str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">") | |
| .replace(/"/g, """).replace(/'/g, "'"); | |
| const escapedCode = escapeHTML(rawCode); | |
| output += ` | |
| <div class="code-block-wrapper"> | |
| <div class="language-label">${langGuess.toUpperCase() || 'CODE'}</div> | |
| <pre class="line-numbers language-${langGuess}"><code class="language-${langGuess}">${escapedCode}</code></pre> | |
| </div> | |
| `; | |
| } else { | |
| output += `<div class="text-content">${formatText(codeBlocks[i])}</div>`; | |
| } | |
| } | |
| nextTick(() => setTimeout(() => Prism.highlightAll(), 0)); | |
| return output; | |
| }; | |
| const detectLanguageByKeywords = (code) => { | |
| const keywords = { | |
| python: ['def ', 'print(', 'import ', 'class '], | |
| javascript: ['function ', 'console.log(', 'let ', 'const ', 'document.getElementById'], | |
| typescript: ['interface ', 'type ', 'let ', 'const ', ': string', ': number'], | |
| java: ['import java.', 'ArrayList<', 'System.out', 'void main(', 'public class', 'new '], | |
| c: ['#include <stdio.h>', 'printf(', 'scanf(', 'int main('], | |
| cpp: ['#include', 'std::', 'cout <<', 'cin >>'], | |
| bash: ['#!/bin/bash', 'echo ', 'cd ', 'ls', 'pwd'], | |
| shell: ['#!/bin/sh', 'echo ', 'export ', 'fi'], | |
| sql: ['SELECT ', 'INSERT ', 'UPDATE ', 'FROM ', 'WHERE ', 'JOIN ', 'DELETE '], | |
| html: ['<!DOCTYPE html>', '<html>', '<div>', '<script>'], | |
| css: ['color:', 'font-size:', 'margin:', 'padding:'], | |
| go: ['package main', 'fmt.Println', 'func main()'], | |
| php: ['<?php', 'echo ', '$_', '->'], | |
| ruby: ['def ', 'puts ', 'end', 'class '], | |
| kotlin: ['fun main(', 'val ', 'var ', 'println('], | |
| swift: ['import SwiftUI', 'struct ', 'var body:', 'Text('], | |
| rust: ['fn main()', 'println!', 'let mut'], | |
| scala: ['object ', 'def ', 'val ', 'println('], | |
| dart: ['void main()', 'print(', 'var ', 'class '], | |
| }; | |
| let best = 'plaintext', score = 0; | |
| for (const [lang, keys] of Object.entries(keywords)) { | |
| let s = 0; | |
| for (const k of keys) s += (code.match(new RegExp(k, 'g')) || []).length; | |
| if (s > score) [score, best] = [s, lang]; | |
| } | |
| return best; | |
| }; | |
| const sendMessage = async () => { | |
| if (!message.value.trim()) return; | |
| history.value.push({ role: 'user', content: message.value, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }); | |
| scrollToBottom(); | |
| const assistant = { role: 'assistant', content: '', time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) }; | |
| history.value.push(assistant); | |
| loading.value = true; | |
| const payload = { message: message.value, history: history.value.slice(0, -1) }; | |
| message.value = ''; | |
| const response = await fetch("/chat-stream", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(payload) | |
| }); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let done = false; | |
| while (!done) { | |
| const { value, done: isDone } = await reader.read(); | |
| if (value) { | |
| assistant.content += decoder.decode(value); | |
| scrollToBottom(); | |
| } | |
| done = isDone; | |
| } | |
| loading.value = false; | |
| }; | |
| return { message, history, sendMessage, renderCode, formatText, loading, chatbox }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |