humanvprojectceo commited on
Commit
5533425
·
verified ·
1 Parent(s): cedb3a9

Update customer.html

Browse files
Files changed (1) hide show
  1. customer.html +244 -370
customer.html CHANGED
@@ -3,11 +3,13 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
- <title>Cafe AI سفارش هوشمند</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap" rel="stylesheet">
10
  <script src="https://cdn.tailwindcss.com"></script>
 
 
11
  <script>
12
  tailwind.config = {
13
  theme: {
@@ -41,9 +43,6 @@
41
  'gold-glow': '0 0 30px rgba(212, 175, 55, 0.25)',
42
  'inner-soft': 'inset 0 2px 6px rgba(0,0,0,0.3)',
43
  },
44
- backgroundImage: {
45
- 'persian-geo': "url(\"data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M30 0L60 30L30 60L0 30Z' fill='none' stroke='%230891b2' stroke-width='0.4' opacity='0.08'/%3E%3Ccircle cx='30' cy='30' r='10' fill='none' stroke='%23d4af37' stroke-width='0.4' opacity='0.06'/%3E%3Cpath d='M30 10L40 30L30 50L20 30Z' fill='none' stroke='%23d4af37' stroke-width='0.3' opacity='0.05'/%3E%3C/svg%3E\")",
46
- },
47
  }
48
  }
49
  }
@@ -66,57 +65,40 @@
66
  border-radius: 4px;
67
  }
68
  .glass-card {
69
- background: rgba(15, 31, 56, 0.7);
70
- backdrop-filter: blur(20px);
71
- border: 1px solid rgba(8, 145, 178, 0.15);
72
- }
73
- .glass-chip {
74
- background: rgba(21, 43, 72, 0.6);
75
- backdrop-filter: blur(12px);
76
- border: 1px solid rgba(255,255,255,0.05);
77
- }
78
- .receipt-texture {
79
- background-color: #0f1f38;
80
- background-image: radial-gradient(rgba(212,175,55,0.08) 1px, transparent 0);
81
- background-size: 12px 12px;
82
- }
83
- .jagged-edge {
84
- background-image: linear-gradient(-135deg, #0f1f38 4px, transparent 0), linear-gradient(135deg, #0f1f38 4px, transparent 0);
85
- background-position: left top;
86
- background-repeat: repeat-x;
87
- background-size: 8px 8px;
88
  }
89
- @keyframes wave {
90
- 0%, 100% { height: 4px; }
91
- 50% { height: 18px; }
92
- }
93
- .wave-bar {
94
- animation: wave 1.2s ease-in-out infinite;
95
- }
96
- /* Persian tile divider */
97
- .persian-divider {
98
- height: 6px;
99
- background: repeating-linear-gradient(90deg, #d4af37 0 2px, transparent 2px 6px);
100
- opacity: 0.3;
101
  }
102
  </style>
103
  </head>
104
- <body class="min-h-screen flex flex-col font-sans overflow-x-hidden antialiased bg-gradient-to-br from-tabriz-900 via-[#0a1422] to-[#0d1729] bg-persian-geo bg-repeat selection:bg-gold selection:text-black">
105
 
106
- <!-- Ambient light orbs -->
107
  <div class="hidden lg:block absolute inset-0 overflow-hidden pointer-events-none z-0">
108
- <div class="absolute -top-1/3 -right-1/4 w-[800px] h-[800px] rounded-full bg-turquoise/4 blur-[180px]"></div>
109
- <div class="absolute -bottom-1/3 -left-1/4 w-[800px] h-[800px] rounded-full bg-gold/4 blur-[180px]"></div>
110
  </div>
111
 
112
  <!-- Header -->
113
  <header class="border-b border-tabriz-700/40 bg-tabriz-800/50 backdrop-blur-xl sticky top-0 z-50 px-4 py-3 md:px-8 shadow-lg">
114
- <div class="max-w-7xl mx-auto flex items-center justify-between">
115
  <div class="flex items-center gap-3">
116
  <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-turquoise/20 to-gold/10 border border-turquoise/20 flex items-center justify-center shadow-turquoise-glow">
117
- <!-- Cafe AI Logo (coffee cup with Persian tile motif) -->
118
- <svg class="w-6 h-6 text-gold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
119
- <path d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2z" stroke-dasharray="2 2" opacity="0.5"/>
120
  <path d="M17 8h1a4 4 0 1 1 0 8h-1" />
121
  <path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
122
  <line x1="6" y1="2" x2="6" y2="4" />
@@ -125,140 +107,99 @@
125
  </svg>
126
  </div>
127
  <div>
128
- <h1 class="text-sm md:text-base font-extrabold text-white tracking-wide">
129
- سفارش هوشمند <span class="text-gold">Cafe AI</span>
130
  </h1>
131
- <p class="text-[10px] text-turquoise/70 font-medium">دستیار سفارش‌گیر نیلا در خدمت شماست</p>
 
 
132
  </div>
133
  </div>
134
- <span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
135
- <span class="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></span>
136
- آنلاین
137
- </span>
 
 
138
  </div>
139
  </header>
140
 
141
- <!-- Main content -->
142
- <main class="flex-1 w-full max-w-7xl mx-auto p-4 md:p-6 lg:p-8 z-10 relative flex flex-col items-center justify-center min-h-[80vh]">
143
-
144
- <!-- Table selector screen -->
145
- <div id="tableSelectorScreen" class="w-full max-w-lg flex flex-col items-center space-y-8 transition-all duration-500 ease-out">
146
- <div class="text-center space-y-4">
147
- <div class="relative inline-flex">
148
- <div class="absolute inset-0 bg-gold/10 rounded-2xl blur-xl animate-pulse"></div>
149
- <div class="relative w-20 h-20 rounded-2xl bg-gradient-to-br from-gold/10 to-turquoise/10 border border-gold/30 flex items-center justify-center shadow-gold-glow">
150
- <!-- Persian tile star SVG -->
151
- <svg class="w-10 h-10 text-gold" viewBox="0 0 24 24" fill="currentColor">
152
- <path d="M12 2l1.5 5h5l-4 3 1.5 5-4-3-4 3 1.5-5-4-3h5z"/>
153
- </svg>
154
- </div>
155
- </div>
156
- <h2 class="text-2xl font-extrabold text-white">خوش آمدید</h2>
157
- <p class="text-sm text-turquoise/70 max-w-md mx-auto">لطفاً شماره میز خود را انتخاب کنید تا دستیار هوشمند نیلا فعال شود و منوی امروز را دریافت کنید.</p>
158
- <div class="persian-divider w-24 mx-auto"></div>
159
  </div>
160
-
161
- <div class="grid grid-cols-4 gap-3 w-full max-w-xs mx-auto">
162
- <script>
163
- for (let i = 1; i <= 8; i++) {
164
- document.write(`
165
- <button onclick="selectTable(${i})" id="tableBtn-${i}" class="table-btn aspect-square rounded-xl border border-tabriz-600/40 bg-tabriz-800/60 hover:border-gold/30 text-white font-extrabold text-xs transition-all duration-300 flex flex-col items-center justify-center gap-1 shadow-inner">
166
- <span class="text-[10px] text-turquoise/60">میز</span>
167
- <span class="text-sm">${i}</span>
168
- </button>
169
- `);
170
- }
171
- </script>
172
  </div>
173
-
174
- <button id="confirmTableBtn" disabled onclick="confirmTableSelection()" class="w-full max-w-xs py-4 bg-tabriz-700/40 text-turquoise/50 font-bold rounded-2xl text-xs transition-all duration-300 flex items-center justify-center gap-2 cursor-not-allowed border border-tabriz-600/30">
175
- ثبت موقعیت و شروع گفتگو
176
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
177
- <path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
178
  </svg>
 
179
  </button>
180
  </div>
181
 
182
- <!-- Chat screen -->
183
- <div id="chatScreen" class="hidden w-full max-w-4xl flex flex-col bg-tabriz-800/30 backdrop-blur-xl rounded-3xl border border-turquoise/10 shadow-2xl overflow-hidden transition-all duration-500 opacity-0 transform translate-y-6 h-[80vh]">
 
184
  <!-- Chat header -->
185
- <div class="px-5 py-4 border-b border-tabriz-700/40 flex items-center justify-between bg-tabriz-900/40">
186
- <div class="flex items-center gap-3">
187
- <div class="w-10 h-10 rounded-xl bg-turquoise/10 border border-turquoise/20 flex items-center justify-center">
188
- <span class="text-gold font-extrabold text-sm">N</span>
189
- </div>
190
- <div>
191
- <h2 class="text-sm font-extrabold text-white">دستیار هوشمند نیلا</h2>
192
- <p class="text-[10px] text-turquoise/70 flex items-center gap-1">
193
- میز <span id="activeTableLabel" class="text-gold font-bold">-</span>
194
- </p>
195
- </div>
196
  </div>
197
- <span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 font-bold">
198
- <span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
199
- آنلاین
200
- </span>
201
- </div>
202
-
203
- <!-- Messages container -->
204
- <div id="messagesContainer" class="flex-1 overflow-y-auto p-4 md:p-6 space-y-4 custom-scrollbar"></div>
205
-
206
- <!-- Quick suggestions -->
207
- <div id="suggestionRow" class="px-4 py-2 border-t border-tabriz-700/30 flex gap-2 overflow-x-auto whitespace-nowrap scrollbar-none bg-tabriz-900/30">
208
- <button onclick="sendQuickPrompt('یک فنجان لاته گرم به همراه کیک شکلاتی')" class="glass-chip px-3 py-1.5 rounded-full text-[10px] text-white hover:text-gold hover:border-gold/30 transition-all">☕ لاته + کیک شکلاتی</button>
209
- <button onclick="sendQuickPrompt('منوی کامل نوشیدنی‌های سرد شما چیست؟')" class="glass-chip px-3 py-1.5 rounded-full text-[10px] text-white hover:text-gold hover:border-gold/30 transition-all">🥤 نوشیدنی سرد</button>
210
- <button onclick="sendQuickPrompt('پیشنهاد ویژه نیلا امروز چیست؟')" class="glass-chip px-3 py-1.5 rounded-full text-[10px] text-white hover:text-gold hover:border-gold/30 transition-all">✨ پیشنهاد ویژه</button>
211
- <button onclick="sendQuickPrompt('کروسان ساده به همراه چای ماسالا')" class="glass-chip px-3 py-1.5 rounded-full text-[10px] text-white hover:text-gold hover:border-gold/30 transition-all">🥐 کروسان + چای ماسالا</button>
212
  </div>
213
 
214
- <!-- Draft order receipt -->
215
- <div id="draftOrderContainer" class="hidden border-t border-gold/10 bg-tabriz-800/60 backdrop-blur-xl transition-all duration-500">
216
- <div class="jagged-edge h-2 w-full -mt-2"></div>
217
- <div class="p-4 space-y-3 receipt-texture">
218
- <div class="flex items-center justify-between border-b border-tabriz-700/40 pb-2">
219
- <span class="text-xs font-extrabold text-gold flex items-center gap-1.5">
220
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
221
- <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
222
- </svg>
223
- پیش‌نویس صورت‌حساب
224
- </span>
225
- <span class="text-[9px] font-mono text-turquoise/50">Cafe AI</span>
226
  </div>
227
- <div id="draftItemsList" class="space-y-2 max-h-[140px] overflow-y-auto pr-1"></div>
228
- <div class="pt-2 flex justify-center opacity-30">
229
- <div class="flex space-x-[1.5px] space-x-reverse h-7 w-1/2">
230
- <div class="w-1 h-full bg-zinc-300"></div><div class="w-[1.5px] h-full bg-zinc-300"></div><div class="w-2 h-full bg-zinc-300"></div><div class="w-[1px] h-full bg-zinc-300"></div><div class="w-1.5 h-full bg-zinc-300"></div><div class="w-3 h-full bg-zinc-300"></div><div class="w-[1px] h-full bg-zinc-300"></div><div class="w-1 h-full bg-zinc-300"></div><div class="w-2 h-full bg-zinc-300"></div><div class="w-[1.5px] h-full bg-zinc-300"></div>
231
- </div>
232
  </div>
233
- <button onclick="submitFinalOrder()" class="w-full py-3 bg-emerald-600 hover:bg-emerald-500 text-white font-extrabold rounded-xl text-xs transition-all flex items-center justify-center gap-2 shadow-turquoise-glow">
234
- تایید نهایی و ارسال به آشپزخانه
235
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
236
- <path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" />
237
- </svg>
238
- </button>
239
  </div>
240
  </div>
241
 
242
- <!-- Streaming loader -->
243
- <div id="streamingController" class="hidden px-4 py-2.5 border-t border-tabriz-700/40 bg-tabriz-900/60 flex items-center justify-between text-[10px]">
244
- <div class="flex items-center gap-3">
245
- <div class="flex items-end gap-[2px] h-[18px]">
246
- <div class="wave-bar w-[2px] bg-gold rounded-full" style="animation-delay: 0.1s"></div>
247
- <div class="wave-bar w-[2px] bg-gold rounded-full" style="animation-delay: 0.3s"></div>
248
- <div class="wave-bar w-[2px] bg-gold rounded-full" style="animation-delay: 0.5s"></div>
249
- <div class="wave-bar w-[2px] bg-gold rounded-full" style="animation-delay: 0.2s"></div>
250
  </div>
251
- <span class="text-turquoise/80">نیلا در حال پاسخگویی...</span>
252
  </div>
253
- <button onclick="stopStreaming()" class="px-3 py-1.5 bg-rose-950/30 border border-rose-700/30 hover:bg-rose-900/40 rounded-lg text-rose-400 font-bold transition-all">
254
  توقف
255
  </button>
256
  </div>
257
 
258
- <!-- Chat input -->
259
- <form id="customerChatForm" onsubmit="sendCustomerMessage(event)" class="p-3 border-t border-tabriz-700/40 bg-tabriz-900/40 flex gap-2.5">
260
- <input type="text" id="customerChatInput" placeholder="مایل به سفارش چه مواردی هستید؟..." autocomplete="off" class="flex-1 bg-tabriz-800/80 border border-tabriz-600/40 text-xs text-white placeholder-turquoise/40 rounded-xl px-4 py-3 focus:outline-none focus:border-gold/50 focus:ring-1 focus:ring-gold/30 transition-all">
261
- <button type="submit" id="sendBtn" class="bg-gradient-to-r from-gold to-amber-600 hover:from-amber-400 hover:to-gold text-tabriz-900 font-extrabold px-5 py-3 rounded-xl text-xs transition-all flex items-center justify-center shadow-gold-glow">
 
262
  <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
263
  <path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />
264
  </svg>
@@ -267,298 +208,231 @@
267
  </div>
268
  </main>
269
 
270
- <!-- Success overlay -->
271
- <div id="successOverlay" class="hidden fixed inset-0 bg-tabriz-900/95 z-50 flex flex-col items-center justify-center p-8 text-center space-y-6 transition-all duration-500 opacity-0">
272
- <div class="w-24 h-24 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center animate-pulse shadow-turquoise-glow">
273
- <svg class="w-12 h-12 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
274
- <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
275
- </svg>
276
- </div>
277
- <h2 class="text-2xl font-black text-white">سفارش شما ثبت شد</h2>
278
- <p class="text-sm text-turquoise/60 max-w-md">فاکتور شما با موفقیت به آشپزخانه ارسال گردید. میز شما برای سفارشات بعدی آماده است.</p>
279
- <div class="persian-divider w-16"></div>
280
- <span class="text-[10px] text-gold/60 font-mono uppercase tracking-widest">Cafe AI — Smart Ordering</span>
281
- </div>
282
-
283
- <!-- Footer -->
284
- <footer class="border-t border-tabriz-700/30 py-4 text-center text-[10px] text-turquoise/50 bg-tabriz-900/80 backdrop-blur mt-auto">
285
- طراحی و توسعه برای <span class="text-gold font-bold">Cafe AI</span> — با الهام از فرهنگ تبریز © ۲۰۲۶
286
  </footer>
287
 
288
- <!-- JavaScript logic (unchanged) -->
289
  <script>
290
- const rawTableNumber = "{{ table_number }}";
291
- const initialTableNumber = (rawTableNumber && rawTableNumber !== "None" && rawTableNumber !== "") ? parseInt(rawTableNumber) : null;
292
-
293
- let selectedTableNumber = null;
294
  let abortController = null;
295
  let isGenerating = false;
296
- let currentDraftItems = null;
 
 
 
297
 
 
298
  window.addEventListener('DOMContentLoaded', () => {
299
- if (initialTableNumber && !isNaN(initialTableNumber)) {
300
- selectedTableNumber = initialTableNumber;
301
- bypassTableSelector();
 
302
  }
303
  });
304
 
305
- function selectTable(num) {
306
- document.querySelectorAll('.table-btn').forEach(btn => {
307
- btn.classList.remove('border-gold', 'bg-gold/10', 'text-gold', 'shadow-gold-glow');
308
- btn.classList.add('border-tabriz-600/40', 'bg-tabriz-800/60', 'text-white');
309
- });
310
- const activeBtn = document.getElementById(`tableBtn-${num}`);
311
- activeBtn.classList.remove('border-tabriz-600/40', 'bg-tabriz-800/60', 'text-white');
312
- activeBtn.classList.add('border-gold', 'bg-gold/10', 'text-gold', 'shadow-gold-glow');
313
-
314
- selectedTableNumber = num;
315
- const confirmBtn = document.getElementById('confirmTableBtn');
316
- confirmBtn.disabled = false;
317
- confirmBtn.classList.remove('bg-tabriz-700/40', 'text-turquoise/50', 'cursor-not-allowed');
318
- confirmBtn.classList.add('bg-gold', 'text-tabriz-900', 'hover:bg-amber-400', 'shadow-gold-glow');
319
- }
320
-
321
- function confirmTableSelection() {
322
- if (!selectedTableNumber) return;
323
- window.location.href = `/customer/${selectedTableNumber}`;
324
- }
325
-
326
- function bypassTableSelector() {
327
- const screen1 = document.getElementById('tableSelectorScreen');
328
- const screen2 = document.getElementById('chatScreen');
329
- screen1.classList.add('opacity-0', 'scale-95');
330
- setTimeout(() => {
331
- screen1.classList.add('hidden');
332
- screen2.classList.remove('hidden');
333
- setTimeout(() => {
334
- screen2.classList.remove('opacity-0', 'translate-y-6');
335
- document.getElementById('activeTableLabel').innerText = selectedTableNumber;
336
- triggerNilaGreeting();
337
- }, 50);
338
- }, 300);
339
- }
340
-
341
- async function triggerNilaGreeting() {
342
- await fetchNilaStream("سلام. من روی صندلی خودم نشستم. خوش آمدگویی کن و منوی فعال امروز را معرفی کن.");
343
- }
344
-
345
- async function sendQuickPrompt(text) {
346
- if (isGenerating) return;
347
- appendChatBubble('user', text);
348
- await fetchNilaStream(text);
349
  }
350
 
 
351
  async function sendCustomerMessage(e) {
352
  e.preventDefault();
353
  if (isGenerating) return;
354
- const input = document.getElementById('customerChatInput');
355
- const userText = input.value.trim();
356
- if (!userText) return;
357
- appendChatBubble('user', userText);
 
 
 
 
 
 
358
  input.value = '';
359
- await fetchNilaStream(userText);
360
- }
361
 
362
- async function fetchNilaStream(userText) {
363
- if (isGenerating) return;
364
  isGenerating = true;
365
- toggleInputs(true);
366
-
367
  abortController = new AbortController();
368
- const botBubbleId = appendChatBubble('model', '');
369
- const botBubble = document.getElementById(botBubbleId);
370
-
371
  try {
372
- const response = await fetch(`/api/customer/chat_stream/${selectedTableNumber}`, {
373
  method: 'POST',
374
  headers: { 'Content-Type': 'application/json' },
375
- body: JSON.stringify({ message: userText }),
376
  signal: abortController.signal
377
  });
378
- if (!response.ok) throw new Error("پایگاه داده ارتباط موقت سرور قطع شد.");
379
-
 
 
 
 
 
380
  const reader = response.body.getReader();
381
- const decoder = new TextDecoder("utf-8");
382
- let buffer = "";
383
-
 
 
 
 
384
  while (true) {
385
- const { value, done } = await reader.read();
386
  if (done) break;
387
  buffer += decoder.decode(value, { stream: true });
388
- const lines = buffer.split("\n");
389
- buffer = lines.pop();
390
-
391
  for (const line of lines) {
392
- if (!line.trim() || !line.startsWith("data: ")) continue;
393
-
394
- const jsonStr = line.substring(6).trim();
395
- try {
396
- const data = JSON.parse(jsonStr);
397
-
398
  if (data.type === 'text') {
399
- botBubble.textContent += data.content;
 
 
 
 
400
  } else if (data.type === 'draft') {
401
- showReceipt(data.items);
 
402
  } else if (data.type === 'error') {
403
- botBubble.textContent = data.content;
404
  }
405
- } catch (e) {
406
- console.error("خطا در ساختار استریم:", e);
407
  }
408
  }
409
-
410
- const container = document.getElementById('messagesContainer');
411
- container.scrollTop = container.scrollHeight;
412
  }
413
- finalizeStreamTurn();
 
 
 
 
 
414
  } catch (err) {
415
- if (err.name === 'AbortError') {
416
- botBubble.textContent += ' [مکالمه توسط کاربر لغو گردید]';
417
- } else {
418
- botBubble.textContent = "بروز خطا در اتصال مرکزی: " + err.message;
419
  }
420
- finalizeStreamTurn();
 
 
 
421
  }
422
  }
423
 
424
- async function stopStreaming() {
425
  if (abortController) {
426
  abortController.abort();
 
 
427
  }
 
428
  isGenerating = false;
429
- toggleInputs(false);
430
- try {
431
- await fetch(`/api/customer/stop/${selectedTableNumber}`, { method: 'POST' });
432
- } catch (e) {
433
- console.error(e);
434
- }
435
  }
436
 
437
- function finalizeStreamTurn() {
438
- isGenerating = false;
439
- toggleInputs(false);
440
- }
441
-
442
- function toggleInputs(generating) {
443
- const input = document.getElementById('customerChatInput');
444
  const sendBtn = document.getElementById('sendBtn');
445
- const streamController = document.getElementById('streamingController');
446
- const sugRow = document.getElementById('suggestionRow');
447
-
448
- if (generating) {
449
- input.disabled = true;
450
  sendBtn.disabled = true;
451
- sendBtn.classList.add('opacity-40', 'cursor-not-allowed');
452
- streamController.classList.remove('hidden');
453
- sugRow.classList.add('opacity-40', 'pointer-events-none');
454
  } else {
455
- input.disabled = false;
456
  sendBtn.disabled = false;
457
- sendBtn.classList.remove('opacity-40', 'cursor-not-allowed');
458
- streamController.classList.add('hidden');
459
- sugRow.classList.remove('opacity-40', 'pointer-events-none');
460
- }
461
- }
462
-
463
- function appendChatBubble(role, content) {
464
- const container = document.getElementById('messagesContainer');
465
- const bubbleId = 'bubble-' + Date.now();
466
- let bubbleHtml = '';
467
-
468
- if (role === 'user') {
469
- bubbleHtml = `
470
- <div class="flex justify-end gap-2.5 max-w-[85%] mr-auto animate-fadeIn">
471
- <div class="bg-gold/10 border border-gold/30 text-white text-xs rounded-2xl rounded-tl-none px-4 py-3 leading-relaxed shadow-inner-soft">
472
- ${content}
473
- </div>
474
- </div>
475
- `;
476
- } else {
477
- bubbleHtml = `
478
- <div class="flex gap-2.5 max-w-[88%] animate-fadeIn">
479
- <div class="w-8 h-8 rounded-xl bg-turquoise/10 border border-turquoise/20 flex items-center justify-center flex-shrink-0">
480
- <span class="text-gold font-bold text-xs">AI</span>
481
- </div>
482
- <div id="${bubbleId}" class="bg-tabriz-800/60 border border-tabriz-700/30 text-gray-200 text-xs rounded-2xl rounded-tr-none px-4 py-3 leading-relaxed shadow-inner-soft">
483
- ${content}
484
- </div>
485
- </div>
486
- `;
487
  }
488
- container.insertAdjacentHTML('beforeend', bubbleHtml);
489
- container.scrollTop = container.scrollHeight;
490
- return bubbleId;
491
  }
492
 
493
- function showReceipt(items) {
494
- currentDraftItems = items;
495
- const container = document.getElementById('draftOrderContainer');
496
- const list = document.getElementById('draftItemsList');
497
- list.innerHTML = '';
498
-
499
  items.forEach(item => {
500
- list.insertAdjacentHTML('beforeend', `
501
- <div class="flex items-center justify-between text-xs bg-tabriz-800/60 px-4 py-3 rounded-xl border border-tabriz-700/30">
502
- <span class="text-white font-bold">${item.name}</span>
503
- <span class="text-gold bg-gold/10 px-2.5 py-1 rounded-lg font-black text-[10px]">${item.quantity} عدد</span>
504
  </div>
505
- `);
506
  });
507
- container.classList.remove('hidden');
508
- setTimeout(() => {
509
- const chatContainer = document.getElementById('messagesContainer');
510
- chatContainer.scrollTop = chatContainer.scrollHeight;
511
- }, 100);
512
  }
513
 
514
- async function submitFinalOrder() {
515
- if (!currentDraftItems) return;
 
 
 
516
  try {
517
- const response = await fetch('/api/confirm_order', {
518
  method: 'POST',
519
  headers: { 'Content-Type': 'application/json' },
520
  body: JSON.stringify({
521
- table_number: String(selectedTableNumber),
522
- items: currentDraftItems
523
  })
524
  });
525
- const result = await response.json();
526
- if (response.ok && result.success) {
527
- document.getElementById('draftOrderContainer').classList.add('hidden');
528
- showSuccessTransition();
 
 
529
  } else {
530
- alert(result.error || اییدیه فاکتور ناموفق بود.");
531
  }
532
- } catch (err) {
533
- alert(طع ارتباط زنده پایگاه داده با سیستم مرکزی: " + err.message);
534
  }
535
  }
536
 
537
- function showSuccessTransition() {
538
- const overlay = document.getElementById('successOverlay');
539
- overlay.classList.remove('hidden');
540
- setTimeout(() => {
541
- overlay.classList.remove('opacity-0');
542
- }, 50);
543
-
544
- setTimeout(() => {
545
- overlay.classList.add('opacity-0');
546
- setTimeout(() => {
547
- overlay.classList.add('hidden');
548
- resetSessionAndRestart();
549
- }, 500);
550
- }, 4500);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
551
  }
552
 
553
- function resetSessionAndRestart() {
554
- abortController = null;
555
- isGenerating = false;
556
- currentDraftItems = null;
557
-
558
- document.getElementById('messagesContainer').innerHTML = '';
559
- document.getElementById('draftOrderContainer').classList.add('hidden');
560
-
561
- window.location.href = '/';
562
  }
563
  </script>
564
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
6
+ <title>Cafe AI - سفارش آنلاین</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;500;700;800&display=swap" rel="stylesheet">
10
  <script src="https://cdn.tailwindcss.com"></script>
11
+ <!-- Markdown parser for rich chat messages -->
12
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
13
  <script>
14
  tailwind.config = {
15
  theme: {
 
43
  'gold-glow': '0 0 30px rgba(212, 175, 55, 0.25)',
44
  'inner-soft': 'inset 0 2px 6px rgba(0,0,0,0.3)',
45
  },
 
 
 
46
  }
47
  }
48
  }
 
65
  border-radius: 4px;
66
  }
67
  .glass-card {
68
+ background: rgba(15, 31, 56, 0.65);
69
+ backdrop-filter: blur(16px);
70
+ border: 1px solid rgba(8, 145, 178, 0.12);
71
+ box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
  }
73
+ /* Markdown rendered content inside chat bubbles */
74
+ .chat-markdown p { margin-bottom: 0.3em; }
75
+ .chat-markdown ul { list-style-type: disc; padding-right: 1.2em; margin: 0.2em 0; }
76
+ .chat-markdown ol { list-style-type: decimal; padding-right: 1.2em; margin: 0.2em 0; }
77
+ .chat-markdown li { margin-bottom: 0.1em; }
78
+ .chat-markdown strong { color: #f0d060; }
79
+ .chat-markdown em { color: #22d3ee; }
80
+ .chat-markdown code {
81
+ background: rgba(8,145,178,0.15);
82
+ padding: 0.1em 0.3em;
83
+ border-radius: 4px;
84
+ font-size: 0.9em;
85
  }
86
  </style>
87
  </head>
88
+ <body class="min-h-screen flex flex-col font-sans selection:bg-gold selection:text-black overflow-x-hidden bg-gradient-to-br from-tabriz-900 via-[#0c1426] to-[#0d1729] antialiased">
89
 
90
+ <!-- Ambient lights (desktop) -->
91
  <div class="hidden lg:block absolute inset-0 overflow-hidden pointer-events-none z-0">
92
+ <div class="absolute -top-1/4 -right-1/4 w-[700px] h-[700px] rounded-full bg-turquoise/5 blur-[160px]"></div>
93
+ <div class="absolute -bottom-1/4 -left-1/4 w-[700px] h-[700px] rounded-full bg-gold/5 blur-[160px]"></div>
94
  </div>
95
 
96
  <!-- Header -->
97
  <header class="border-b border-tabriz-700/40 bg-tabriz-800/50 backdrop-blur-xl sticky top-0 z-50 px-4 py-3 md:px-8 shadow-lg">
98
+ <div class="max-w-5xl mx-auto flex items-center justify-between">
99
  <div class="flex items-center gap-3">
100
  <div class="w-10 h-10 rounded-xl bg-gradient-to-br from-turquoise/20 to-gold/10 border border-turquoise/20 flex items-center justify-center shadow-turquoise-glow">
101
+ <svg class="w-6 h-6 text-gold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
 
 
102
  <path d="M17 8h1a4 4 0 1 1 0 8h-1" />
103
  <path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
104
  <line x1="6" y1="2" x2="6" y2="4" />
 
107
  </svg>
108
  </div>
109
  <div>
110
+ <h1 class="text-sm font-extrabold text-white flex items-center gap-2">
111
+ <span class="text-gold">Cafe AI</span> دستیار هوشمند سفارش
112
  </h1>
113
+ <p class="text-[10px] text-turquoise/70 font-medium" id="tableDisplay">
114
+ {% if table_number %} میز شماره {{ table_number }} {% else %} لطفاً شماره میز را وارد کنید {% endif %}
115
+ </p>
116
  </div>
117
  </div>
118
+ <div class="flex items-center gap-3">
119
+ <span class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-[10px] font-bold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">
120
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
121
+ سرویس فعال
122
+ </span>
123
+ </div>
124
  </div>
125
  </header>
126
 
127
+ <!-- Main content: chat + draft panel -->
128
+ <main class="flex-1 max-w-5xl w-full mx-auto p-4 md:p-6 lg:p-8 z-10 relative flex flex-col gap-4">
129
+
130
+ <!-- Draft order panel (hidden until Nila prepares it) -->
131
+ <div id="draftOrderPanel" class="glass-card rounded-2xl p-4 md:p-5 hidden transition-all duration-500">
132
+ <div class="flex items-center justify-between border-b border-tabriz-700/40 pb-3 mb-3">
133
+ <h3 class="text-sm font-extrabold text-gold flex items-center gap-2">
134
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
135
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
136
+ </svg>
137
+ پیش‌نویس سفارش شما
138
+ </h3>
139
+ <span class="text-[10px] text-turquoise/60">جهت تأیید نهایی، دکمه را بزنید</span>
 
 
 
 
 
140
  </div>
141
+ <div id="draftItems" class="space-y-2 mb-4">
142
+ <!-- Dynamically filled -->
 
 
 
 
 
 
 
 
 
 
143
  </div>
144
+ <button id="confirmOrderBtn" onclick="confirmOrder()"
145
+ class="w-full py-3 bg-gradient-to-r from-emerald-600 to-green-500 hover:from-emerald-500 hover:to-green-400 rounded-xl text-white font-extrabold text-sm transition-all shadow-lg flex items-center justify-center gap-2">
146
+ <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
147
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
 
148
  </svg>
149
+ تایید نهایی سفارش
150
  </button>
151
  </div>
152
 
153
+ <!-- Chat card -->
154
+ <div class="flex-1 flex flex-col glass-card rounded-3xl overflow-hidden shadow-xl h-[550px] md:h-[620px]">
155
+
156
  <!-- Chat header -->
157
+ <div class="px-5 py-4 border-b border-tabriz-700/40 flex items-center gap-3 bg-tabriz-900/40">
158
+ <div class="w-10 h-10 rounded-xl bg-turquoise/10 border border-turquoise/20 flex items-center justify-center shadow-turquoise-glow">
159
+ <svg class="w-5 h-5 text-gold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
160
+ <circle cx="12" cy="8" r="4"/>
161
+ <path d="M5 20v-1a7 7 0 0114 0v1"/>
162
+ </svg>
163
+ </div>
164
+ <div>
165
+ <h3 class="text-xs font-extrabold text-white">نیلا، دستیار کافه</h3>
166
+ <p class="text-[9px] text-turquoise/70 font-medium">برای سفارش از منو، با من گفتگو کنید ☕️</p>
 
167
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  </div>
169
 
170
+ <!-- Chat messages area -->
171
+ <div id="chatMessages" class="flex-1 overflow-y-auto p-4 space-y-4">
172
+ <div class="flex gap-2.5 max-w-[85%]">
173
+ <div class="w-8 h-8 rounded-xl bg-turquoise/15 border border-turquoise/20 flex items-center justify-center flex-shrink-0">
174
+ <span class="text-gold font-extrabold text-[10px] font-mono">AI</span>
 
 
 
 
 
 
 
175
  </div>
176
+ <div class="bg-tabriz-900/60 border border-tabriz-700/30 text-zinc-200 text-xs rounded-2xl rounded-tr-none px-3.5 py-2.5 leading-relaxed shadow-inner-soft chat-markdown">
177
+ سلام! من نیلا هستم، دستیار هوشمند کافه AI. به میزتون خوش اومدید 🌸.<br>
178
+ چه نوشیدنی، غذا یا شیرینی‌ای می‌پسندید؟ منوی امروز رو می‌تونم براتون بگم. 😊
 
 
179
  </div>
 
 
 
 
 
 
180
  </div>
181
  </div>
182
 
183
+ <!-- Chat loader (thinking) -->
184
+ <div id="chatLoader" class="hidden px-4 py-2.5 flex items-center justify-between bg-tabriz-800/60 text-[9px] text-turquoise/80 font-extrabold border-t border-tabriz-700/40">
185
+ <div class="flex items-center gap-2">
186
+ <div class="flex space-x-1 space-x-reverse">
187
+ <span class="w-1.5 h-1.5 bg-gold rounded-full animate-bounce" style="animation-delay: 0.1s"></span>
188
+ <span class="w-1.5 h-1.5 bg-gold rounded-full animate-bounce" style="animation-delay: 0.2s"></span>
189
+ <span class="w-1.5 h-1.5 bg-gold rounded-full animate-bounce" style="animation-delay: 0.3s"></span>
 
190
  </div>
191
+ <span>نیلا در حال فکر کردن...</span>
192
  </div>
193
+ <button onclick="stopGeneration()" class="px-2.5 py-1.5 bg-rose-950/10 border border-rose-900/30 hover:bg-rose-900/40 rounded-lg text-[9px] text-rose-400 font-bold transition-all">
194
  توقف
195
  </button>
196
  </div>
197
 
198
+ <!-- Chat input form -->
199
+ <form id="chatForm" onsubmit="sendCustomerMessage(event)" class="p-3 border-t border-tabriz-700/40 bg-tabriz-900/40 flex gap-2">
200
+ <input type="text" id="chatInput" placeholder="پیام خود را بنویسید..." autocomplete="off"
201
+ class="flex-1 bg-tabriz-800/70 border border-tabriz-600/40 text-xs text-white placeholder-turquoise/40 rounded-lg px-3.5 py-2.5 focus:outline-none focus:border-gold/50 transition-all duration-300">
202
+ <button type="submit" id="sendBtn" class="bg-gradient-to-br from-gold to-amber-600 hover:from-amber-400 hover:to-gold text-tabriz-900 font-extrabold px-4 py-2.5 rounded-lg text-xs transition-all duration-300 flex items-center justify-center flex-shrink-0 shadow-gold-glow">
203
  <svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
204
  <path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />
205
  </svg>
 
208
  </div>
209
  </main>
210
 
211
+ <footer class="border-t border-tabriz-700/30 py-4 text-center text-[10px] text-turquoise/50 bg-tabriz-900/80 backdrop-blur">
212
+ طراحی و توسعه توسط الگوریتم داده نسترن | با الهام از فرهنگ تبریز © ۲۰۲۶
 
 
 
 
 
 
 
 
 
 
 
 
 
 
213
  </footer>
214
 
 
215
  <script>
216
+ // Configuration
217
+ const tableNumber = "{{ table_number }}"; // Flask passes this; might be 'None' if not set
218
+ let currentTable = tableNumber !== 'None' ? tableNumber : null;
 
219
  let abortController = null;
220
  let isGenerating = false;
221
+ let currentDraft = null; // stores the latest draft received
222
+
223
+ // Marked.js setup
224
+ marked.setOptions({ sanitize: true, breaks: true });
225
 
226
+ // On load, if table not provided, ask for it.
227
  window.addEventListener('DOMContentLoaded', () => {
228
+ if (!currentTable) {
229
+ askForTableNumber();
230
+ } else {
231
+ document.getElementById('tableDisplay').textContent = `میز شماره ${currentTable}`;
232
  }
233
  });
234
 
235
+ function askForTableNumber() {
236
+ const table = prompt("لطفاً شماره میز خود را وارد کنید:");
237
+ if (table && table.trim() !== '') {
238
+ // Reload to /customer/<table>
239
+ window.location.href = `/customer/${table.trim()}`;
240
+ } else {
241
+ // Ask again or stay (we can show an error)
242
+ document.getElementById('tableDisplay').textContent = "برای شروع شماره میز الزامی است.";
243
+ setTimeout(() => askForTableNumber(), 1000);
244
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
245
  }
246
 
247
+ // Chat sending
248
  async function sendCustomerMessage(e) {
249
  e.preventDefault();
250
  if (isGenerating) return;
251
+ if (!currentTable) {
252
+ alert("شماره میز مشخص نیست. صفحه را بازنشانی کنید.");
253
+ return;
254
+ }
255
+
256
+ const input = document.getElementById('chatInput');
257
+ const text = input.value.trim();
258
+ if (!text) return;
259
+
260
+ appendChatMessage('user', text);
261
  input.value = '';
 
 
262
 
 
 
263
  isGenerating = true;
264
+ toggleChatLoading(true);
 
265
  abortController = new AbortController();
266
+
 
 
267
  try {
268
+ const response = await fetch(`/api/customer/chat_stream/${currentTable}`, {
269
  method: 'POST',
270
  headers: { 'Content-Type': 'application/json' },
271
+ body: JSON.stringify({ message: text }),
272
  signal: abortController.signal
273
  });
274
+
275
+ if (!response.ok) {
276
+ const errData = await response.json();
277
+ throw new Error(errData.error || 'خطای سرور');
278
+ }
279
+
280
+ // Read SSE stream
281
  const reader = response.body.getReader();
282
+ const decoder = new TextDecoder();
283
+ let buffer = '';
284
+ let botBubbleId = appendChatMessage('model', ''); // placeholder
285
+ let botBubble = document.getElementById(botBubbleId);
286
+ let fullText = '';
287
+ let streamCompleted = false;
288
+
289
  while (true) {
290
+ const { done, value } = await reader.read();
291
  if (done) break;
292
  buffer += decoder.decode(value, { stream: true });
293
+ const lines = buffer.split('\n');
294
+ buffer = lines.pop(); // keep incomplete line
295
+
296
  for (const line of lines) {
297
+ if (line.startsWith('data: ')) {
298
+ const data = JSON.parse(line.slice(6));
 
 
 
 
299
  if (data.type === 'text') {
300
+ fullText += data.content;
301
+ // Show plain text during streaming (no markdown conversion yet)
302
+ botBubble.textContent = fullText;
303
+ document.getElementById('chatMessages').scrollTop =
304
+ document.getElementById('chatMessages').scrollHeight;
305
  } else if (data.type === 'draft') {
306
+ currentDraft = data.items;
307
+ showDraftPanel(currentDraft);
308
  } else if (data.type === 'error') {
309
+ botBubble.textContent = '⚠️ ' + data.content;
310
  }
 
 
311
  }
312
  }
 
 
 
313
  }
314
+
315
+ // After stream completed, replace with rendered Markdown
316
+ botBubble.textContent = '';
317
+ botBubble.innerHTML = marked.parse(fullText);
318
+ streamCompleted = true;
319
+
320
  } catch (err) {
321
+ if (err.name !== 'AbortError') {
322
+ appendChatMessage('model', ' عدم دریافت پاسخ: ' + err.message);
 
 
323
  }
324
+ } finally {
325
+ toggleChatLoading(false);
326
+ isGenerating = false;
327
+ abortController = null;
328
  }
329
  }
330
 
331
+ function stopGeneration() {
332
  if (abortController) {
333
  abortController.abort();
334
+ // Also call stop API to clean server state
335
+ fetch(`/api/customer/stop/${currentTable}`, { method: 'POST' }).catch(() => {});
336
  }
337
+ toggleChatLoading(false);
338
  isGenerating = false;
 
 
 
 
 
 
339
  }
340
 
341
+ function toggleChatLoading(loading) {
342
+ const loader = document.getElementById('chatLoader');
 
 
 
 
 
343
  const sendBtn = document.getElementById('sendBtn');
344
+ const input = document.getElementById('chatInput');
345
+ if (loading) {
346
+ loader.classList.remove('hidden');
 
 
347
  sendBtn.disabled = true;
348
+ input.disabled = true;
 
 
349
  } else {
350
+ loader.classList.add('hidden');
351
  sendBtn.disabled = false;
352
+ input.disabled = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  }
 
 
 
354
  }
355
 
356
+ function showDraftPanel(items) {
357
+ const panel = document.getElementById('draftOrderPanel');
358
+ const draftItems = document.getElementById('draftItems');
359
+ draftItems.innerHTML = '';
 
 
360
  items.forEach(item => {
361
+ draftItems.innerHTML += `
362
+ <div class="flex items-center justify-between text-xs bg-tabriz-900/60 px-3 py-2 rounded-lg border border-tabriz-700/30">
363
+ <span class="text-zinc-300 font-bold">${escapeHtml(item.name)}</span>
364
+ <span class="text-gold bg-gold/10 px-2 py-0.5 rounded-md font-extrabold text-[10px]">تعداد: ${item.quantity}</span>
365
  </div>
366
+ `;
367
  });
368
+ panel.classList.remove('hidden');
369
+ // Scroll to top so the draft is visible
370
+ window.scrollTo({ top: 0, behavior: 'smooth' });
 
 
371
  }
372
 
373
+ async function confirmOrder() {
374
+ if (!currentDraft || currentDraft.length === 0) {
375
+ alert("پیش‌نویس سفارشی وجود ندارد.");
376
+ return;
377
+ }
378
  try {
379
+ const res = await fetch('/api/confirm_order', {
380
  method: 'POST',
381
  headers: { 'Content-Type': 'application/json' },
382
  body: JSON.stringify({
383
+ table_number: currentTable,
384
+ items: currentDraft
385
  })
386
  });
387
+ const result = await res.json();
388
+ if (res.ok && result.success) {
389
+ // Hide draft, show success message
390
+ document.getElementById('draftOrderPanel').classList.add('hidden');
391
+ currentDraft = null;
392
+ appendChatMessage('model', '✅ سفارش شما با موفقیت ثبت شد و به آشپزخانه ارسال گردید. از همراهی شما سپاسگزاریم! 🌟');
393
  } else {
394
+ alert(result.error || 'خطا در ثبت سفارش');
395
  }
396
+ } catch (e) {
397
+ alert(طای شبکه: ' + e.message);
398
  }
399
  }
400
 
401
+ // Chat message rendering
402
+ function appendChatMessage(role, content) {
403
+ const container = document.getElementById('chatMessages');
404
+ const bubbleId = 'bubble-' + Date.now();
405
+ let html = '';
406
+
407
+ if (role === 'user') {
408
+ html = `
409
+ <div class="flex justify-end gap-2.5 max-w-[85%] mr-auto">
410
+ <div class="bg-gold/10 border border-gold/25 text-white text-xs rounded-2xl rounded-tl-none px-3.5 py-2.5 leading-relaxed">
411
+ ${escapeHtml(content)}
412
+ </div>
413
+ </div>
414
+ `;
415
+ } else {
416
+ // Model messages may be empty initially (during streaming); they'll be filled later
417
+ html = `
418
+ <div class="flex gap-2.5 max-w-[85%]">
419
+ <div class="w-8 h-8 rounded-xl bg-turquoise/15 border border-turquoise/20 flex items-center justify-center flex-shrink-0">
420
+ <span class="text-gold font-extrabold text-[10px] font-mono">AI</span>
421
+ </div>
422
+ <div id="${bubbleId}" class="bg-tabriz-900/60 border border-tabriz-700/30 text-zinc-200 text-xs rounded-2xl rounded-tr-none px-3.5 py-2.5 leading-relaxed shadow-inner-soft chat-markdown">
423
+ ${content ? marked.parse(content) : ''}
424
+ </div>
425
+ </div>
426
+ `;
427
+ }
428
+
429
+ container.insertAdjacentHTML('beforeend', html);
430
+ container.scrollTop = container.scrollHeight;
431
+ return bubbleId;
432
  }
433
 
434
+ function escapeHtml(text) {
435
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
 
 
 
 
 
 
 
436
  }
437
  </script>
438
  </body>