ChatMateAPI / templates /index.html
FrederickSundeep's picture
commit 00000023
5f4982e
raw
history blame
12.2 kB
<!DOCTYPE html>
<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, '&lt;').replace(/>/g, '&gt;')}</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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
.replace(/"/g, "&quot;").replace(/'/g, "&#39;");
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>