FrederickSundeep commited on
Commit
5f4982e
Β·
1 Parent(s): d0833a3

commit 00000023

Browse files
Files changed (3) hide show
  1. app.py +11 -1
  2. static/style.css +246 -0
  3. templates/index.html +291 -0
app.py CHANGED
@@ -4,12 +4,13 @@ import torch
4
  import re
5
  from fastapi import FastAPI, Request
6
  from fastapi.responses import StreamingResponse
 
 
7
  from pydantic import BaseModel
8
  from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
9
  from huggingface_hub import login
10
  from langchain_community.tools import DuckDuckGoSearchRun
11
  from fastapi.middleware.cors import CORSMiddleware
12
- import os
13
  import uvicorn
14
 
15
  # βœ… Safe GPU decorator
@@ -27,6 +28,10 @@ app = FastAPI(
27
  redoc_url="/redoc" # ReDoc at /redoc
28
  )
29
 
 
 
 
 
30
  # Enable CORS (important for browser clients)
31
  app.add_middleware(
32
  CORSMiddleware,
@@ -90,6 +95,11 @@ class ChatRequest(BaseModel):
90
  history: list = []
91
 
92
  # ---------------- FastAPI route ----------------
 
 
 
 
 
93
  @app.post("/chat-stream", summary="Stream assistant reply", tags=["Chat"])
94
  async def chat_stream(body: ChatRequest):
95
  """
 
4
  import re
5
  from fastapi import FastAPI, Request
6
  from fastapi.responses import StreamingResponse
7
+ from fastapi.staticfiles import StaticFiles
8
+ from fastapi.templating import Jinja2Templates
9
  from pydantic import BaseModel
10
  from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
11
  from huggingface_hub import login
12
  from langchain_community.tools import DuckDuckGoSearchRun
13
  from fastapi.middleware.cors import CORSMiddleware
 
14
  import uvicorn
15
 
16
  # βœ… Safe GPU decorator
 
28
  redoc_url="/redoc" # ReDoc at /redoc
29
  )
30
 
31
+ # βœ… Static + templates
32
+ app.mount("/static", StaticFiles(directory="static"), name="static")
33
+ templates = Jinja2Templates(directory="templates")
34
+
35
  # Enable CORS (important for browser clients)
36
  app.add_middleware(
37
  CORSMiddleware,
 
95
  history: list = []
96
 
97
  # ---------------- FastAPI route ----------------
98
+ # ---------------- Routes ----------------
99
+ @app.get("/", summary="Serve homepage")
100
+ async def home(request: Request):
101
+ return templates.TemplateResponse("index.html", {"request": request})
102
+
103
  @app.post("/chat-stream", summary="Stream assistant reply", tags=["Chat"])
104
  async def chat_stream(body: ChatRequest):
105
  """
static/style.css ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ body {
2
+ font-family: 'Roboto', sans-serif;
3
+ background: #121212;
4
+ color: #ffffff;
5
+ margin: 0;
6
+ display: flex;
7
+ justify-content: center;
8
+ align-items: center;
9
+ min-height: 100vh;
10
+ padding: 0 16px;
11
+ }
12
+
13
+ .chat-container {
14
+ background: #1f1f1f;
15
+ border-radius: 12px;
16
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
17
+ padding: 20px;
18
+ max-width: 700px;
19
+ width: 100%;
20
+ display: flex;
21
+ flex-direction: column;
22
+ }
23
+
24
+ h2 {
25
+ text-align: center;
26
+ color: #bb86fc;
27
+ margin-bottom: 20px;
28
+ font-weight: 500;
29
+ }
30
+
31
+ .chat-box {
32
+ flex-grow: 1;
33
+ max-height: 420px;
34
+ overflow-y: auto;
35
+ padding: 12px;
36
+ border: 1px solid #2d2d2d;
37
+ border-radius: 8px;
38
+ background: #2a2a2a;
39
+ margin-bottom: 16px;
40
+ display: flex;
41
+ flex-direction: column;
42
+ }
43
+ .message {
44
+ display: block; /* ensures full-width layout */
45
+ width: fit-content;
46
+ max-width: 85%;
47
+ margin-bottom: 12px;
48
+ padding: 10px 14px;
49
+ border-radius: 16px;
50
+ line-height: 1.5;
51
+ word-wrap: break-word;
52
+ }
53
+
54
+ .message.user {
55
+ align-self: flex-end;
56
+ margin-left: auto;
57
+ background: #333c4d;
58
+ color: #e3f2fd;
59
+ border-bottom-right-radius: 0;
60
+ }
61
+
62
+ .message.assistant {
63
+ align-self: flex-start;
64
+ margin-right: auto;
65
+ background: #383838;
66
+ border-bottom-left-radius: 0;
67
+ }
68
+
69
+ .text-content {
70
+ white-space: pre-wrap;
71
+ }
72
+
73
+ .input-area {
74
+ display: flex;
75
+ gap: 10px;
76
+ }
77
+
78
+ textarea {
79
+ flex-grow: 1;
80
+ padding: 10px 12px;
81
+ resize: none;
82
+ border-radius: 6px;
83
+ background: #2a2a2a;
84
+ color: #fff;
85
+ border: 1px solid #444;
86
+ font-size: 14px;
87
+ }
88
+
89
+ button {
90
+ background: #6200ee;
91
+ color: white;
92
+ border: none;
93
+ border-radius: 6px;
94
+ padding: 10px 16px;
95
+ font-size: 14px;
96
+ font-weight: 500;
97
+ cursor: pointer;
98
+ transition: background 0.3s;
99
+ }
100
+
101
+ button:hover {
102
+ background: #3700b3;
103
+ }
104
+
105
+ .typing-indicator {
106
+ display: flex;
107
+ gap: 6px;
108
+ margin: 6px 0 12px;
109
+ }
110
+
111
+ .dot {
112
+ width: 8px;
113
+ height: 8px;
114
+ background-color: #bb86fc;
115
+ border-radius: 50%;
116
+ animation: blink 1.4s infinite both;
117
+ }
118
+
119
+ .dot:nth-child(2) {
120
+ animation-delay: 0.2s;
121
+ }
122
+
123
+ .dot:nth-child(3) {
124
+ animation-delay: 0.4s;
125
+ }
126
+
127
+ @keyframes blink {
128
+ 0%, 80%, 100% { opacity: 0.2; }
129
+ 40% { opacity: 1; }
130
+ }
131
+
132
+ .code-block {
133
+ position: relative;
134
+ background: #1e1e1e;
135
+ color: #eee;
136
+ padding: 12px;
137
+ border-radius: 8px;
138
+ margin-top: 8px;
139
+ }
140
+
141
+ .copy-btn {
142
+ position: absolute;
143
+ top: 8px;
144
+ right: 12px;
145
+ background: #444;
146
+ color: #fff;
147
+ border: none;
148
+ padding: 4px 8px;
149
+ font-size: 12px;
150
+ border-radius: 4px;
151
+ cursor: pointer;
152
+ }
153
+
154
+ .copy-btn:hover {
155
+ background: #666;
156
+ }
157
+
158
+ @media (max-width: 600px) {
159
+ .chat-box {
160
+ max-height: 60vh;
161
+ }
162
+
163
+ button {
164
+ padding: 8px 14px;
165
+ }
166
+ }
167
+
168
+ .timestamp {
169
+ font-size: 11px;
170
+ color: #aaa;
171
+ margin-top: 4px;
172
+ display: block;
173
+ }
174
+
175
+ .timestamp.user {
176
+ text-align: right;
177
+ }
178
+
179
+ .timestamp.assistant {
180
+ text-align: left;
181
+ }
182
+
183
+ .code-line {
184
+ display: flex;
185
+ }
186
+
187
+ .line-number {
188
+ width: 30px;
189
+ color: #888;
190
+ text-align: right;
191
+ padding-right: 12px;
192
+ user-select: none;
193
+ }
194
+
195
+ .line-content {
196
+ flex-grow: 1;
197
+ white-space: pre-wrap;
198
+ }
199
+
200
+ .code-block-wrapper {
201
+ position: relative;
202
+ background: #2d2d2d;
203
+ border-radius: 8px;
204
+ margin: 10px 0;
205
+ padding-top: 1.5em;
206
+ overflow: auto;
207
+ }
208
+
209
+ .language-label {
210
+ position: absolute;
211
+ top: 0;
212
+ right: 10px;
213
+ background: #444;
214
+ color: #fff;
215
+ padding: 2px 8px;
216
+ font-size: 0.75em;
217
+ border-bottom-left-radius: 4px;
218
+ }
219
+
220
+ .copy-btn {
221
+ position: absolute;
222
+ top: 0;
223
+ left: 10px;
224
+ background: transparent;
225
+ color: white;
226
+ border: none;
227
+ font-size: 0.9em;
228
+ cursor: pointer;
229
+ }
230
+
231
+ pre[class*="language-"] {
232
+ white-space: pre-wrap !important;
233
+ word-break: break-word;
234
+ overflow-x: auto;
235
+ }
236
+
237
+ .chat-image {
238
+ max-width: 100%;
239
+ height: auto;
240
+ margin-top: 10px;
241
+ border-radius: 6px;
242
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
243
+ }
244
+
245
+
246
+
templates/index.html ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>Chat Mate</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <link rel="stylesheet" href="/static/style.css" />
8
+ <link rel="stylesheet" href="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.css">
9
+ <script src="https://unpkg.com/material-components-web@latest/dist/material-components-web.min.js"></script>
10
+
11
+ <!-- Prism -->
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/themes/prism-tomorrow.css">
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-numbers/prism-line-numbers.css">
14
+ <script src="https://cdn.jsdelivr.net/npm/prismjs/prism.js"></script>
15
+ <script src="https://cdn.jsdelivr.net/npm/prismjs/plugins/line-numbers/prism-line-numbers.min.js"></script>
16
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
17
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup.min.js"></script>
18
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup-templating.min.js"></script>
19
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
20
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
21
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
22
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-java.min.js"></script>
23
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-c.min.js"></script>
24
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-cpp.min.js"></script>
25
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
26
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-shell-session.min.js"></script>
27
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-sql.min.js"></script>
28
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
29
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-go.min.js"></script>
30
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-php.min.js"></script>
31
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-ruby.min.js"></script>
32
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-kotlin.min.js"></script>
33
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-swift.min.js"></script>
34
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-rust.min.js"></script>
35
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-scala.min.js"></script>
36
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-dart.min.js"></script>
37
+
38
+ <script src="https://unpkg.com/vue@3"></script>
39
+ </head>
40
+
41
+ <body>
42
+ <div id="app" class="chat-container">
43
+ <h2>πŸ’¬ Chat Mate</h2>
44
+
45
+ <div class="chat-box" ref="chatbox">
46
+ <div v-for="(msg, i) in history" :key="i" class="message" :class="msg.role">
47
+ <template v-if="msg.role === 'assistant' && msg.content.includes('```')">
48
+ <div class="message-content" v-html="renderCode(msg.content)"></div>
49
+ {% raw %}<div class="timestamp" :class="msg.role">{{ msg.time }}</div>{% endraw %}
50
+ </template>
51
+ <template v-else>
52
+ <div class="text-content" v-html="formatText(msg.content)"></div>
53
+ {% raw %}<div class="timestamp" :class="msg.role">{{ msg.time }}</div>{% endraw %}
54
+ </template>
55
+ </div>
56
+
57
+ <div v-if="loading" class="typing-indicator">
58
+ <span class="dot"></span><span class="dot"></span><span class="dot"></span>
59
+ </div>
60
+ </div>
61
+
62
+ <div class="input-area">
63
+ <textarea v-model="message" placeholder="Ask something..." rows="2"></textarea>
64
+ <button @click="sendMessage" :disabled="!message.trim() || loading">Send</button>
65
+ </div>
66
+ </div>
67
+
68
+ <script>
69
+ const { createApp, ref, nextTick } = Vue;
70
+
71
+ createApp({
72
+ setup() {
73
+ const message = ref('');
74
+ const history = ref([]);
75
+ const loading = ref(false);
76
+ const chatbox = ref(null);
77
+
78
+ const scrollToBottom = () => {
79
+ nextTick(() => {
80
+ if (chatbox.value) chatbox.value.scrollTop = chatbox.value.scrollHeight;
81
+ });
82
+ };
83
+
84
+ const formatText = (text) => {
85
+ // βœ… Handle base64 images
86
+ const imageRegex = /\[IMAGE_START\](.*?)\[IMAGE_END\]/gs;
87
+ text = text.replace(imageRegex, (match, base64) => {
88
+ const src = `data:image/png;base64,${base64.trim()}`;
89
+ return `<img src="${src}" alt="Generated Image" class="chat-image"/>`;
90
+ });
91
+
92
+ // βœ… Normalize line endings and remove excessive blank lines
93
+ text = text.replace(/\r\n|\r/g, '\n');
94
+ text = text.replace(/\n{3,}/g, '\n\n');
95
+
96
+ // βœ… Parse fenced code blocks (```code```)
97
+ text = text.replace(/```(\w+)?\n([\s\S]*?)```/g, (match, lang, code) => {
98
+ const language = lang ? ` class="language-${lang}"` : '';
99
+ return `<pre><code${language}>${code.trim().replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre>`;
100
+ });
101
+
102
+ // βœ… Parse blockquotes
103
+ text = text.replace(/^> (.*)$/gm, '<blockquote>$1</blockquote>');
104
+
105
+ // βœ… Headings
106
+ text = text.replace(/^### (.*)$/gm, '<h3>$1</h3>');
107
+
108
+ // βœ… Horizontal rules
109
+ text = text.replace(/^---$/gm, '<hr>');
110
+
111
+ // βœ… Bold (**text**) and italic (*text*)
112
+ text = text.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
113
+ text = text.replace(/\*(.*?)\*/g, '<em>$1</em>');
114
+
115
+ // βœ… Emoji rendering using colon syntax (:smile:)
116
+ const emojiMap = {
117
+ smile: "πŸ˜„",
118
+ sad: "😒",
119
+ heart: "❀️",
120
+ thumbs_up: "πŸ‘",
121
+ fire: "πŸ”₯",
122
+ check: "βœ…",
123
+ x: "❌",
124
+ star: "⭐",
125
+ rocket: "πŸš€",
126
+ warning: "⚠️",
127
+ };
128
+ text = text.replace(/:([a-z0-9_+-]+):/g, (match, name) => emojiMap[name] || match);
129
+
130
+ // βœ… Unordered list (bullets)
131
+ const listify = (lines, tag) =>
132
+ `<${tag}>` +
133
+ lines.map(item => `<li>${item.replace(/^(\-|\d+\.)\s*/, '').trim()}</li>`).join('') +
134
+ `</${tag}>`;
135
+
136
+ text = text.replace(
137
+ /((?:^[-*] .+(?:\n|$))+)/gm,
138
+ (match) => listify(match.trim().split('\n'), 'ul')
139
+ );
140
+
141
+ // βœ… Ordered list (fix separate `1.` items issue)
142
+ text = text.replace(/^(\d+\. .+)$/gm, '__ORDERED__START__$1__ORDERED__END__');
143
+ text = text.replace(
144
+ /__ORDERED__START__(\d+\. .+?)__ORDERED__END__/gs,
145
+ (_, line) => `<ol><li>${line.replace(/^\d+\.\s*/, '')}</li></ol>`
146
+ );
147
+ text = text.replace(/<\/ol>\s*<ol>/g, '');
148
+
149
+ // βœ… Markdown-style tables
150
+ text = text.replace(
151
+ /^\|(.+?)\|\n\|([-:| ]+)\|\n((?:\|.*\|\n?)*)/gm,
152
+ (_, headerRow, dividerRow, bodyRows) => {
153
+ const headers = headerRow.split('|').map(h => `<th>${h.trim()}</th>`).join('');
154
+ const rows = bodyRows.trim().split('\n').map(r =>
155
+ '<tr>' + r.split('|').map(cell => `<td>${cell.trim()}</td>`).join('') + '</tr>'
156
+ ).join('');
157
+ return `<table><thead><tr>${headers}</tr></thead><tbody>${rows}</tbody></table>`;
158
+ }
159
+ );
160
+
161
+ // βœ… Paragraphs and line breaks inside paragraphs
162
+ const blocks = text.split(/\n{2,}/).map(block => {
163
+ if (
164
+ block.startsWith('<h3>') ||
165
+ block.startsWith('<hr>') ||
166
+ block.startsWith('<ul>') ||
167
+ block.startsWith('<ol>') ||
168
+ block.startsWith('<table>') ||
169
+ block.startsWith('<pre>') ||
170
+ block.startsWith('<blockquote>') ||
171
+ block.startsWith('<img')
172
+ ) {
173
+ return block;
174
+ } else {
175
+ return `<p>${block.trim().replace(/\n/g, '<br>')}</p>`;
176
+ }
177
+ });
178
+
179
+ return blocks.join('\n');
180
+ };
181
+
182
+
183
+
184
+
185
+
186
+ const renderCode = (text) => {
187
+ const codeBlocks = text.split(/```/);
188
+ let output = '';
189
+
190
+ for (let i = 0; i < codeBlocks.length; i++) {
191
+ if (i % 2 === 1) {
192
+ const lines = codeBlocks[i].split('\n');
193
+ let langGuess = /^[a-zA-Z]+$/.test(lines[0]) ? lines[0].trim().toLowerCase() : '';
194
+ const codeLines = langGuess ? lines.slice(1) : lines;
195
+ const rawCode = codeLines.join('\n');
196
+
197
+ if (!langGuess) langGuess = detectLanguageByKeywords(rawCode);
198
+
199
+ const escapeHTML = (str) =>
200
+ str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
201
+ .replace(/"/g, "&quot;").replace(/'/g, "&#39;");
202
+
203
+ const escapedCode = escapeHTML(rawCode);
204
+
205
+ output += `
206
+ <div class="code-block-wrapper">
207
+ <div class="language-label">${langGuess.toUpperCase() || 'CODE'}</div>
208
+ <pre class="line-numbers language-${langGuess}"><code class="language-${langGuess}">${escapedCode}</code></pre>
209
+ </div>
210
+ `;
211
+ } else {
212
+ output += `<div class="text-content">${formatText(codeBlocks[i])}</div>`;
213
+ }
214
+ }
215
+
216
+ nextTick(() => setTimeout(() => Prism.highlightAll(), 0));
217
+ return output;
218
+ };
219
+
220
+ const detectLanguageByKeywords = (code) => {
221
+ const keywords = {
222
+ python: ['def ', 'print(', 'import ', 'class '],
223
+ javascript: ['function ', 'console.log(', 'let ', 'const ', 'document.getElementById'],
224
+ typescript: ['interface ', 'type ', 'let ', 'const ', ': string', ': number'],
225
+ java: ['import java.', 'ArrayList<', 'System.out', 'void main(', 'public class', 'new '],
226
+ c: ['#include <stdio.h>', 'printf(', 'scanf(', 'int main('],
227
+ cpp: ['#include', 'std::', 'cout <<', 'cin >>'],
228
+ bash: ['#!/bin/bash', 'echo ', 'cd ', 'ls', 'pwd'],
229
+ shell: ['#!/bin/sh', 'echo ', 'export ', 'fi'],
230
+ sql: ['SELECT ', 'INSERT ', 'UPDATE ', 'FROM ', 'WHERE ', 'JOIN ', 'DELETE '],
231
+ html: ['<!DOCTYPE html>', '<html>', '<div>', '<script>'],
232
+ css: ['color:', 'font-size:', 'margin:', 'padding:'],
233
+ go: ['package main', 'fmt.Println', 'func main()'],
234
+ php: ['<?php', 'echo ', '$_', '->'],
235
+ ruby: ['def ', 'puts ', 'end', 'class '],
236
+ kotlin: ['fun main(', 'val ', 'var ', 'println('],
237
+ swift: ['import SwiftUI', 'struct ', 'var body:', 'Text('],
238
+ rust: ['fn main()', 'println!', 'let mut'],
239
+ scala: ['object ', 'def ', 'val ', 'println('],
240
+ dart: ['void main()', 'print(', 'var ', 'class '],
241
+ };
242
+
243
+ let best = 'plaintext', score = 0;
244
+ for (const [lang, keys] of Object.entries(keywords)) {
245
+ let s = 0;
246
+ for (const k of keys) s += (code.match(new RegExp(k, 'g')) || []).length;
247
+ if (s > score) [score, best] = [s, lang];
248
+ }
249
+ return best;
250
+ };
251
+
252
+ const sendMessage = async () => {
253
+ if (!message.value.trim()) return;
254
+ history.value.push({ role: 'user', content: message.value, time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) });
255
+ scrollToBottom();
256
+
257
+ const assistant = { role: 'assistant', content: '', time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) };
258
+ history.value.push(assistant);
259
+ loading.value = true;
260
+
261
+ const payload = { message: message.value, history: history.value.slice(0, -1) };
262
+ message.value = '';
263
+
264
+ const response = await fetch("/chat-stream", {
265
+ method: "POST",
266
+ headers: { "Content-Type": "application/json" },
267
+ body: JSON.stringify(payload)
268
+ });
269
+
270
+ const reader = response.body.getReader();
271
+ const decoder = new TextDecoder();
272
+ let done = false;
273
+
274
+ while (!done) {
275
+ const { value, done: isDone } = await reader.read();
276
+ if (value) {
277
+ assistant.content += decoder.decode(value);
278
+ scrollToBottom();
279
+ }
280
+ done = isDone;
281
+ }
282
+
283
+ loading.value = false;
284
+ };
285
+
286
+ return { message, history, sendMessage, renderCode, formatText, loading, chatbox };
287
+ }
288
+ }).mount('#app');
289
+ </script>
290
+ </body>
291
+ </html>