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

Update cafe.html

Browse files
Files changed (1) hide show
  1. cafe.html +288 -69
cafe.html CHANGED
@@ -8,6 +8,8 @@
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: {
@@ -75,6 +77,14 @@
75
  border-color: rgba(212, 175, 55, 0.25);
76
  box-shadow: 0 10px 40px -8px rgba(212, 175, 55, 0.15);
77
  }
 
 
 
 
 
 
 
 
78
  </style>
79
  </head>
80
  <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 bg-persian-geo bg-repeat">
@@ -118,7 +128,7 @@
118
  <!-- Main grid -->
119
  <main class="flex-1 max-w-7xl w-full mx-auto p-4 md:p-6 lg:p-8 grid grid-cols-1 lg:grid-cols-12 gap-6 z-10 relative">
120
 
121
- <!-- Right column: orders + menu upload (7/12) -->
122
  <section class="lg:col-span-7 flex flex-col gap-6">
123
 
124
  <!-- Live orders monitor -->
@@ -140,15 +150,54 @@
140
  بروزرسانی
141
  </button>
142
  </div>
143
-
144
  <div id="ordersContainer" class="space-y-4 max-h-[350px] overflow-y-auto pr-1">
145
- <div class="text-center py-12 text-xs text-turquoise/60">
146
- در حال بارگذاری لیست سفارشات جاری...
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  </div>
148
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  </div>
150
 
151
- <!-- Menu extraction & upload -->
152
  <div class="glass-card rounded-3xl p-5 md:p-6 space-y-4 shadow-xl">
153
  <h2 class="text-sm md:text-base font-extrabold text-white flex items-center gap-2 border-b border-tabriz-700/40 pb-3">
154
  <svg class="w-5 h-5 text-gold" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@@ -156,11 +205,9 @@
156
  </svg>
157
  استخراج خودکار ساختار منو با هوش مصنوعی
158
  </h2>
159
-
160
  <p class="text-[11px] text-turquoise/70 leading-relaxed">
161
  تصویر منو یا فاکتور جدید کافه را بارگذاری کنید. هوش مصنوعی نیلا متون و قیمت‌ها را استخراج کرده و جهت تایید نهایی برای شما ساختاربندی می‌کند.
162
  </p>
163
-
164
  <!-- Drag & drop zone -->
165
  <div id="dropZone"
166
  class="border-2 border-dashed border-tabriz-600/50 hover:border-gold/60 rounded-2xl p-6 md:p-8 text-center cursor-pointer transition-all bg-tabriz-900/40 hover:bg-tabriz-800/30 group"
@@ -168,7 +215,6 @@
168
  ondragover="handleDragOver(event)"
169
  ondragleave="handleDragLeave(event)"
170
  ondrop="handleDrop(event)">
171
-
172
  <input type="file" id="menuFileInput" class="hidden" accept="image/*,application/pdf" onchange="uploadMenuFile(event)">
173
  <div class="flex flex-col items-center gap-3">
174
  <svg id="uploadIcon" class="w-10 h-10 text-turquoise/60 group-hover:text-gold transition-all duration-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
@@ -178,14 +224,10 @@
178
  <span class="text-[9px] text-turquoise/50 uppercase tracking-wider">فرمت‌های تصویری یا PDF</span>
179
  </div>
180
  </div>
181
-
182
- <!-- Upload loader -->
183
  <div id="uploadLoader" class="hidden text-center py-6 bg-tabriz-900/60 rounded-2xl border border-turquoise/20 animate-pulse">
184
  <div class="inline-block animate-spin rounded-full h-7 w-7 border-t-2 border-b-2 border-gold mb-2"></div>
185
  <p class="text-[10px] text-gold font-extrabold">نیلا در حال ساختارسازی و آنالیز متون منوی جدید است...</p>
186
  </div>
187
-
188
- <!-- Menu editor table -->
189
  <div id="menuEditorContainer" class="hidden space-y-4 border-t border-tabriz-700/30 pt-4">
190
  <h3 class="text-xs font-bold text-gold flex items-center gap-1.5">
191
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
@@ -193,7 +235,6 @@
193
  </svg>
194
  بازبینی اقلام منو پیش از انتشار زنده
195
  </h3>
196
-
197
  <div class="overflow-x-auto max-h-[250px] border border-tabriz-700/40 rounded-2xl bg-tabriz-900/30">
198
  <table class="min-w-full divide-y divide-tabriz-700/40 text-[11px]">
199
  <thead class="bg-tabriz-800/50">
@@ -209,14 +250,9 @@
209
  </tbody>
210
  </table>
211
  </div>
212
-
213
  <div class="flex justify-end gap-2">
214
- <button onclick="addEmptyMenuRow()" class="px-4 py-2 border border-turquoise/30 hover:border-gold/50 rounded-xl text-[10px] text-turquoise hover:text-gold transition-all duration-300 bg-tabriz-800/40">
215
- افزودن دستی محصول جدید
216
- </button>
217
- <button onclick="saveAndPublishMenu()" class="px-5 py-2.5 bg-gradient-to-r from-turquoise to-cyan-600 hover:from-cyan-500 hover:to-turquoise rounded-xl text-[11px] font-extrabold text-white transition-all duration-300 flex items-center gap-2 shadow-turquoise-glow">
218
- ثبت و همگام‌سا��ی زنده منو
219
- </button>
220
  </div>
221
  </div>
222
  </div>
@@ -224,8 +260,6 @@
224
 
225
  <!-- Left column: Admin chat assistant (5/12) -->
226
  <section class="lg:col-span-5 flex flex-col glass-card rounded-3xl h-[630px] overflow-hidden shadow-xl">
227
-
228
- <!-- Chat header -->
229
  <div class="px-5 py-4 border-b border-tabriz-700/40 flex items-center justify-between bg-tabriz-900/40">
230
  <div class="flex items-center gap-3">
231
  <div class="w-10 h-10 rounded-xl bg-turquoise/10 border border-turquoise/20 flex items-center justify-center shadow-turquoise-glow">
@@ -241,19 +275,17 @@
241
  </div>
242
  </div>
243
 
244
- <!-- Chat messages area -->
245
  <div id="chatMessages" class="flex-1 overflow-y-auto p-4 space-y-4">
246
  <div class="flex gap-2.5 max-w-[85%]">
247
  <div class="w-8 h-8 rounded-xl bg-turquoise/15 border border-turquoise/20 flex items-center justify-center flex-shrink-0">
248
  <span class="text-gold font-extrabold text-[10px] font-mono">AI</span>
249
  </div>
250
- <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">
251
  سلام همکار گرامی! من نیلا هستم، دستیار مدیریت منوی Cafe AI. شما می‌توانید از همین‌جا اقلام منو را با گفتگوی ساده کنترل کنید؛ برای مثال بنویسید: "کروسان ساده تمام شد" یا "چای ماسالا را موجود کن" تا سریعاً تغییرات روی سیستم مشتریان نیز اعمال گردد.
252
  </div>
253
  </div>
254
  </div>
255
 
256
- <!-- Chat loader (thinking) -->
257
  <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">
258
  <div class="flex items-center gap-2">
259
  <div class="flex space-x-1 space-x-reverse">
@@ -263,12 +295,9 @@
263
  </div>
264
  <span>نیلا در حال پردازش و اعمال دستور مدیریت...</span>
265
  </div>
266
- <button onclick="stopAdminChat()" 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">
267
- توقف فرآیند
268
- </button>
269
  </div>
270
 
271
- <!-- Chat input form -->
272
  <form id="chatForm" onsubmit="sendAdminMessage(event)" class="p-3 border-t border-tabriz-700/40 bg-tabriz-900/40 flex gap-2">
273
  <input type="text" id="chatInput" placeholder="تغییر وضعیت یا ویرایش منو را به صورت متنی بنویسید..." autocomplete="off" 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">
274
  <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">
@@ -278,31 +307,34 @@
278
  </button>
279
  </form>
280
  </section>
281
-
282
  </main>
283
 
284
- <!-- Footer -->
285
  <footer class="border-t border-tabriz-700/30 py-4 text-center text-[10px] text-turquoise/50 bg-tabriz-900/80 backdrop-blur">
286
  طراحی و توسعه توسط الگوریتم داده نسترن | با الهام از فرهنگ تبریز © ۲۰۲۶
287
  </footer>
288
 
289
- <!-- JavaScript logic (completely unchanged) -->
290
  <script>
 
 
 
291
  let chatHistory = [
292
  { role: 'model', content: 'سلام همکار گرامی! من نیلا هستم، دستیار مدیریت منوی Cafe AI. شما می‌توانید از همین‌جا اقلام منو را با گفتگوی ساده کنترل کنید؛ برای مثال بنویسید: "کروسان ساده تمام شد" یا "چای ماسالا را موجود کن" تا سریعاً تغییرات روی سیستم مشتریان نیز اعمال گردد.' }
293
  ];
294
  let abortController = null;
295
  let isGenerating = false;
 
296
  window.addEventListener('DOMContentLoaded', () => {
297
  fetchActiveOrders();
 
298
  setInterval(fetchActiveOrders, 10000);
299
  });
 
 
300
  async function fetchActiveOrders() {
301
  const container = document.getElementById('ordersContainer');
302
  try {
303
  const response = await fetch('/api/admin/orders');
304
  const orders = await response.json();
305
-
306
  if (response.ok) {
307
  if (orders.length === 0) {
308
  container.innerHTML = `
@@ -315,7 +347,6 @@
315
  `;
316
  return;
317
  }
318
-
319
  container.innerHTML = '';
320
  orders.forEach(order => {
321
  let itemsHtml = '';
@@ -327,7 +358,6 @@
327
  </div>
328
  `;
329
  });
330
-
331
  const timeString = new Date(order.timestamp * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' });
332
  const cardHtml = `
333
  <div class="border border-tabriz-700/30 hover:border-gold/30 rounded-2xl p-4 bg-tabriz-900/30 transition-all duration-300 flex flex-col md:flex-row justify-between items-start md:items-center gap-3">
@@ -357,6 +387,7 @@
357
  container.innerHTML = `<div class="text-center py-6 text-rose-500 text-xs">عدم برقراری ارتباط زنده با سرور: ${err.message}</div>`;
358
  }
359
  }
 
360
  async function completeOrder(orderId) {
361
  try {
362
  const response = await fetch('/api/admin/complete_order', {
@@ -364,7 +395,6 @@
364
  headers: { 'Content-Type': 'application/json' },
365
  body: JSON.stringify({ order_id: orderId })
366
  });
367
-
368
  const result = await response.json();
369
  if (response.ok && result.success) {
370
  fetchActiveOrders();
@@ -375,6 +405,200 @@
375
  alert("عدم پاسخگویی در ارتباطات شبکه: " + err.message);
376
  }
377
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  function handleDragOver(e) {
379
  e.preventDefault();
380
  const dropZone = document.getElementById('dropZone');
@@ -392,7 +616,6 @@
392
  function handleDrop(e) {
393
  e.preventDefault();
394
  handleDragLeave(e);
395
-
396
  const files = e.dataTransfer.files;
397
  if (files.length > 0) {
398
  const fileInput = document.getElementById('menuFileInput');
@@ -404,23 +627,18 @@
404
  async function uploadMenuFile(e) {
405
  const file = e.target.files[0];
406
  if (!file) return;
407
-
408
  const loader = document.getElementById('uploadLoader');
409
  const editor = document.getElementById('menuEditorContainer');
410
-
411
  loader.classList.remove('hidden');
412
  editor.classList.add('hidden');
413
-
414
  const formData = new FormData();
415
  formData.append('file', file);
416
-
417
  try {
418
  const response = await fetch('/api/admin/extract_menu', {
419
  method: 'POST',
420
  body: formData
421
  });
422
  const data = await response.json();
423
-
424
  loader.classList.add('hidden');
425
  if (response.ok && data.success) {
426
  renderMenuEditor(data.extracted_menu);
@@ -491,26 +709,18 @@
491
  const tbody = document.getElementById('menuEditorTableBody');
492
  const rows = tbody.querySelectorAll('tr');
493
  const menuData = [];
494
-
495
  rows.forEach(row => {
496
  const name = row.querySelector('.menu-name').value.trim();
497
  const description = row.querySelector('.menu-desc').value.trim();
498
  const price = row.querySelector('.menu-price').value.trim();
499
  if (name) {
500
- menuData.push({
501
- name: name,
502
- description: description,
503
- price: price,
504
- available: true
505
- });
506
  }
507
  });
508
-
509
  if (menuData.length === 0) {
510
  alert("انتشار لیست فاقد محصول امکان‌پذیر نیست.");
511
  return;
512
  }
513
-
514
  try {
515
  const response = await fetch('/api/admin/save_menu', {
516
  method: 'POST',
@@ -518,10 +728,10 @@
518
  body: JSON.stringify({ menu: menuData })
519
  });
520
  const data = await response.json();
521
-
522
  if (response.ok && data.success) {
523
  alert("منوی جدید با موفقیت ثبت و روی وب‌سایت همگام‌سازی شد!");
524
  document.getElementById('menuEditorContainer').classList.add('hidden');
 
525
  appendChatMessage('model', 'منوی زنده همگام‌سازی شد. سیستم سفارش‌گیری مشتریان اکنون بر مبنای آخرین لیست ویرایش‌شده کالیبره شده است.');
526
  } else {
527
  alert(data.error || 'بروز خطا در همگام‌سازی ساختار داده‌های جدید.');
@@ -530,22 +740,23 @@
530
  alert('عدم اتصال به پایگاه داده همگام‌سازی منوها: ' + err.message);
531
  }
532
  }
 
 
533
  async function sendAdminMessage(e) {
534
  e.preventDefault();
535
  if (isGenerating) return;
536
-
537
  const input = document.getElementById('chatInput');
538
  const text = input.value.trim();
539
  if (!text) return;
540
-
541
  appendChatMessage('user', text);
542
  input.value = '';
543
  chatHistory.push({ role: 'user', content: text });
544
-
545
  isGenerating = true;
546
  toggleChatLoading(true);
547
  abortController = new AbortController();
548
-
549
  try {
550
  const response = await fetch('/api/admin/chat', {
551
  method: 'POST',
@@ -554,16 +765,15 @@
554
  signal: abortController.signal
555
  });
556
  const data = await response.json();
557
-
558
  toggleChatLoading(false);
559
  isGenerating = false;
560
-
561
  if (response.ok && data.success) {
562
- const botBubbleId = appendChatMessage('model', '');
563
  const botBubble = document.getElementById(botBubbleId);
564
- let i = 0;
565
  const textResponse = data.response;
566
-
 
567
  function typeText() {
568
  if (i < textResponse.length && !abortController.signal.aborted) {
569
  botBubble.textContent += textResponse.charAt(i);
@@ -571,7 +781,12 @@
571
  document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
572
  setTimeout(typeText, 6);
573
  } else {
 
 
 
574
  chatHistory.push({ role: 'model', content: textResponse });
 
 
575
  }
576
  }
577
  typeText();
@@ -586,6 +801,7 @@
586
  }
587
  }
588
  }
 
589
  function stopAdminChat() {
590
  if (abortController) {
591
  abortController.abort();
@@ -593,11 +809,11 @@
593
  toggleChatLoading(false);
594
  isGenerating = false;
595
  }
 
596
  function toggleChatLoading(loading) {
597
  const loader = document.getElementById('chatLoader');
598
  const sendBtn = document.getElementById('sendBtn');
599
  const input = document.getElementById('chatInput');
600
-
601
  if (loading) {
602
  loader.classList.remove('hidden');
603
  sendBtn.disabled = true;
@@ -608,32 +824,35 @@
608
  input.disabled = false;
609
  }
610
  }
 
611
  function appendChatMessage(role, content) {
612
  const container = document.getElementById('chatMessages');
613
  const bubbleId = 'bubble-admin-' + Date.now();
614
  let html = '';
615
-
616
  if (role === 'user') {
617
  html = `
618
- <div class="flex justify-end gap-2.5 max-w-[85%] mr-auto animate-fadeIn">
619
  <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">
620
- ${content}
621
  </div>
622
  </div>
623
  `;
624
  } else {
 
 
625
  html = `
626
- <div class="flex gap-2.5 max-w-[85%] animate-fadeIn">
627
  <div class="w-8 h-8 rounded-xl bg-turquoise/15 border border-turquoise/20 flex items-center justify-center flex-shrink-0">
628
  <span class="text-gold font-extrabold text-[10px] font-mono">AI</span>
629
  </div>
630
- <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">
631
- ${content}
632
  </div>
633
  </div>
634
  `;
635
  }
636
-
637
  container.insertAdjacentHTML('beforeend', html);
638
  container.scrollTop = container.scrollHeight;
639
  return bubbleId;
 
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 -->
12
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
13
  <script>
14
  tailwind.config = {
15
  theme: {
 
77
  border-color: rgba(212, 175, 55, 0.25);
78
  box-shadow: 0 10px 40px -8px rgba(212, 175, 55, 0.15);
79
  }
80
+ /* Markdown rendered content styles */
81
+ .chat-markdown p { margin-bottom: 0.3em; }
82
+ .chat-markdown ul { list-style-type: disc; padding-right: 1.2em; margin: 0.2em 0; }
83
+ .chat-markdown ol { list-style-type: decimal; padding-right: 1.2em; margin: 0.2em 0; }
84
+ .chat-markdown li { margin-bottom: 0.1em; }
85
+ .chat-markdown strong { color: #f0d060; }
86
+ .chat-markdown em { color: #22d3ee; }
87
+ .chat-markdown code { background: rgba(8,145,178,0.15); padding: 0.1em 0.3em; border-radius: 4px; font-size: 0.9em; }
88
  </style>
89
  </head>
90
  <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 bg-persian-geo bg-repeat">
 
128
  <!-- Main grid -->
129
  <main class="flex-1 max-w-7xl w-full mx-auto p-4 md:p-6 lg:p-8 grid grid-cols-1 lg:grid-cols-12 gap-6 z-10 relative">
130
 
131
+ <!-- Right column: orders + current menu + menu upload (7/12) -->
132
  <section class="lg:col-span-7 flex flex-col gap-6">
133
 
134
  <!-- Live orders monitor -->
 
150
  بروزرسانی
151
  </button>
152
  </div>
 
153
  <div id="ordersContainer" class="space-y-4 max-h-[350px] overflow-y-auto pr-1">
154
+ <div class="text-center py-12 text-xs text-turquoise/60">در حال بارگذاری لیست سفارشات جاری...</div>
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Current Menu (NEW) -->
159
+ <div class="glass-card rounded-3xl p-5 md:p-6 space-y-4 shadow-xl">
160
+ <div class="flex items-center justify-between border-b border-tabriz-700/40 pb-3">
161
+ <h2 class="text-sm md:text-base font-extrabold text-white flex items-center gap-2">
162
+ <svg class="w-5 h-5 text-gold" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
163
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
164
+ </svg>
165
+ مدیریت منوی فعال
166
+ </h2>
167
+ <div class="flex gap-2">
168
+ <button onclick="fetchCurrentMenu()" class="text-[10px] text-turquoise/70 hover:text-gold flex items-center gap-1 transition-all bg-tabriz-800/30 hover:bg-tabriz-700/40 px-3 py-1.5 rounded-xl">
169
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
170
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 1121.21 15H15" />
171
+ </svg>
172
+ بازنشانی
173
+ </button>
174
+ <button onclick="addMenuItemRow()" class="px-4 py-1.5 bg-gradient-to-r from-turquoise to-cyan-600 hover:from-cyan-500 hover:to-turquoise rounded-xl text-[10px] font-extrabold text-white transition-all flex items-center gap-1 shadow-turquoise-glow">
175
+ <span>+ افزودن دستی</span>
176
+ </button>
177
  </div>
178
  </div>
179
+ <p class="text-[11px] text-turquoise/70 leading-relaxed">
180
+ لیست تمام اقلام منوی کافه. می‌توانید هر آیتم را ویرایش، حذف یا آیتم جدید اضافه کنید. تغییرات بلافاصله روی سامانه مشتریان اعمال می‌شود.
181
+ </p>
182
+ <div class="overflow-x-auto max-h-[400px] border border-tabriz-700/40 rounded-2xl bg-tabriz-900/30">
183
+ <table class="min-w-full divide-y divide-tabriz-700/40 text-[11px]" id="currentMenuTable">
184
+ <thead class="bg-tabriz-800/50 sticky top-0">
185
+ <tr>
186
+ <th class="px-3 py-2.5 text-right font-medium text-turquoise/90">نام محصول</th>
187
+ <th class="px-3 py-2.5 text-right font-medium text-turquoise/90 hidden md:table-cell">توضیحات</th>
188
+ <th class="px-3 py-2.5 text-right font-medium text-turquoise/90 w-20">قیمت</th>
189
+ <th class="px-3 py-2.5 text-center font-medium text-turquoise/90 w-16">موجود</th>
190
+ <th class="px-3 py-2.5 text-center font-medium text-turquoise/90 w-20">عملیات</th>
191
+ </tr>
192
+ </thead>
193
+ <tbody id="menuTableBody" class="divide-y divide-tabriz-700/30">
194
+ <!-- filled by JS -->
195
+ </tbody>
196
+ </table>
197
+ </div>
198
  </div>
199
 
200
+ <!-- Menu extraction & upload (existing) -->
201
  <div class="glass-card rounded-3xl p-5 md:p-6 space-y-4 shadow-xl">
202
  <h2 class="text-sm md:text-base font-extrabold text-white flex items-center gap-2 border-b border-tabriz-700/40 pb-3">
203
  <svg class="w-5 h-5 text-gold" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
 
205
  </svg>
206
  استخراج خودکار ساختار منو با هوش مصنوعی
207
  </h2>
 
208
  <p class="text-[11px] text-turquoise/70 leading-relaxed">
209
  تصویر منو یا فاکتور جدید کافه را بارگذاری کنید. هوش مصنوعی نیلا متون و قیمت‌ها را استخراج کرده و جهت تایید نهایی برای شما ساختاربندی می‌کند.
210
  </p>
 
211
  <!-- Drag & drop zone -->
212
  <div id="dropZone"
213
  class="border-2 border-dashed border-tabriz-600/50 hover:border-gold/60 rounded-2xl p-6 md:p-8 text-center cursor-pointer transition-all bg-tabriz-900/40 hover:bg-tabriz-800/30 group"
 
215
  ondragover="handleDragOver(event)"
216
  ondragleave="handleDragLeave(event)"
217
  ondrop="handleDrop(event)">
 
218
  <input type="file" id="menuFileInput" class="hidden" accept="image/*,application/pdf" onchange="uploadMenuFile(event)">
219
  <div class="flex flex-col items-center gap-3">
220
  <svg id="uploadIcon" class="w-10 h-10 text-turquoise/60 group-hover:text-gold transition-all duration-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.5">
 
224
  <span class="text-[9px] text-turquoise/50 uppercase tracking-wider">فرمت‌های تصویری یا PDF</span>
225
  </div>
226
  </div>
 
 
227
  <div id="uploadLoader" class="hidden text-center py-6 bg-tabriz-900/60 rounded-2xl border border-turquoise/20 animate-pulse">
228
  <div class="inline-block animate-spin rounded-full h-7 w-7 border-t-2 border-b-2 border-gold mb-2"></div>
229
  <p class="text-[10px] text-gold font-extrabold">نیلا در حال ساختارسازی و آنالیز متون منوی جدید است...</p>
230
  </div>
 
 
231
  <div id="menuEditorContainer" class="hidden space-y-4 border-t border-tabriz-700/30 pt-4">
232
  <h3 class="text-xs font-bold text-gold flex items-center gap-1.5">
233
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
 
235
  </svg>
236
  بازبینی اقلام منو پیش از انتشار زنده
237
  </h3>
 
238
  <div class="overflow-x-auto max-h-[250px] border border-tabriz-700/40 rounded-2xl bg-tabriz-900/30">
239
  <table class="min-w-full divide-y divide-tabriz-700/40 text-[11px]">
240
  <thead class="bg-tabriz-800/50">
 
250
  </tbody>
251
  </table>
252
  </div>
 
253
  <div class="flex justify-end gap-2">
254
+ <button onclick="addEmptyMenuRow()" class="px-4 py-2 border border-turquoise/30 hover:border-gold/50 rounded-xl text-[10px] text-turquoise hover:text-gold transition-all duration-300 bg-tabriz-800/40">افزودن دستی محصول جدید</button>
255
+ <button onclick="saveAndPublishMenu()" class="px-5 py-2.5 bg-gradient-to-r from-turquoise to-cyan-600 hover:from-cyan-500 hover:to-turquoise rounded-xl text-[11px] font-extrabold text-white transition-all duration-300 flex items-center gap-2 shadow-turquoise-glow">ثبت و همگام‌سازی زنده منو</button>
 
 
 
 
256
  </div>
257
  </div>
258
  </div>
 
260
 
261
  <!-- Left column: Admin chat assistant (5/12) -->
262
  <section class="lg:col-span-5 flex flex-col glass-card rounded-3xl h-[630px] overflow-hidden shadow-xl">
 
 
263
  <div class="px-5 py-4 border-b border-tabriz-700/40 flex items-center justify-between bg-tabriz-900/40">
264
  <div class="flex items-center gap-3">
265
  <div class="w-10 h-10 rounded-xl bg-turquoise/10 border border-turquoise/20 flex items-center justify-center shadow-turquoise-glow">
 
275
  </div>
276
  </div>
277
 
 
278
  <div id="chatMessages" class="flex-1 overflow-y-auto p-4 space-y-4">
279
  <div class="flex gap-2.5 max-w-[85%]">
280
  <div class="w-8 h-8 rounded-xl bg-turquoise/15 border border-turquoise/20 flex items-center justify-center flex-shrink-0">
281
  <span class="text-gold font-extrabold text-[10px] font-mono">AI</span>
282
  </div>
283
+ <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">
284
  سلام همکار گرامی! من نیلا هستم، دستیار مدیریت منوی Cafe AI. شما می‌توانید از همین‌جا اقلام منو را با گفتگوی ساده کنترل کنید؛ برای مثال بنویسید: "کروسان ساده تمام شد" یا "چای ماسالا را موجود کن" تا سریعاً تغییرات روی سیستم مشتریان نیز اعمال گردد.
285
  </div>
286
  </div>
287
  </div>
288
 
 
289
  <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">
290
  <div class="flex items-center gap-2">
291
  <div class="flex space-x-1 space-x-reverse">
 
295
  </div>
296
  <span>نیلا در حال پردازش و اعمال دستور مدیریت...</span>
297
  </div>
298
+ <button onclick="stopAdminChat()" 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">توقف فرآیند</button>
 
 
299
  </div>
300
 
 
301
  <form id="chatForm" onsubmit="sendAdminMessage(event)" class="p-3 border-t border-tabriz-700/40 bg-tabriz-900/40 flex gap-2">
302
  <input type="text" id="chatInput" placeholder="تغییر وضعیت یا ویرایش منو را به صورت متنی بنویسید..." autocomplete="off" 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">
303
  <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">
 
307
  </button>
308
  </form>
309
  </section>
 
310
  </main>
311
 
 
312
  <footer class="border-t border-tabriz-700/30 py-4 text-center text-[10px] text-turquoise/50 bg-tabriz-900/80 backdrop-blur">
313
  طراحی و توسعه توسط الگوریتم داده نسترن | با الهام از فرهنگ تبریز © ۲۰۲۶
314
  </footer>
315
 
 
316
  <script>
317
+ // Marked options (for security and simple rendering)
318
+ marked.setOptions({ sanitize: true, breaks: true });
319
+
320
  let chatHistory = [
321
  { role: 'model', content: 'سلام همکار گرامی! من نیلا هستم، دستیار مدیریت منوی Cafe AI. شما می‌توانید از همین‌جا اقلام منو را با گفتگوی ساده کنترل کنید؛ برای مثال بنویسید: "کروسان ساده تمام شد" یا "چای ماسالا را موجود کن" تا سریعاً تغییرات روی سیستم مشتریان نیز اعمال گردد.' }
322
  ];
323
  let abortController = null;
324
  let isGenerating = false;
325
+
326
  window.addEventListener('DOMContentLoaded', () => {
327
  fetchActiveOrders();
328
+ fetchCurrentMenu();
329
  setInterval(fetchActiveOrders, 10000);
330
  });
331
+
332
+ // ==================== Order functions (unchanged) ====================
333
  async function fetchActiveOrders() {
334
  const container = document.getElementById('ordersContainer');
335
  try {
336
  const response = await fetch('/api/admin/orders');
337
  const orders = await response.json();
 
338
  if (response.ok) {
339
  if (orders.length === 0) {
340
  container.innerHTML = `
 
347
  `;
348
  return;
349
  }
 
350
  container.innerHTML = '';
351
  orders.forEach(order => {
352
  let itemsHtml = '';
 
358
  </div>
359
  `;
360
  });
 
361
  const timeString = new Date(order.timestamp * 1000).toLocaleTimeString('fa-IR', { hour: '2-digit', minute: '2-digit' });
362
  const cardHtml = `
363
  <div class="border border-tabriz-700/30 hover:border-gold/30 rounded-2xl p-4 bg-tabriz-900/30 transition-all duration-300 flex flex-col md:flex-row justify-between items-start md:items-center gap-3">
 
387
  container.innerHTML = `<div class="text-center py-6 text-rose-500 text-xs">عدم برقراری ارتباط زنده با سرور: ${err.message}</div>`;
388
  }
389
  }
390
+
391
  async function completeOrder(orderId) {
392
  try {
393
  const response = await fetch('/api/admin/complete_order', {
 
395
  headers: { 'Content-Type': 'application/json' },
396
  body: JSON.stringify({ order_id: orderId })
397
  });
 
398
  const result = await response.json();
399
  if (response.ok && result.success) {
400
  fetchActiveOrders();
 
405
  alert("عدم پاسخگویی در ارتباطات شبکه: " + err.message);
406
  }
407
  }
408
+
409
+ // ==================== Menu CRUD (NEW) ====================
410
+ async function fetchCurrentMenu() {
411
+ const tbody = document.getElementById('menuTableBody');
412
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8 text-turquoise/60">در حال بارگذاری منو...</td></tr>';
413
+ try {
414
+ const res = await fetch('/api/admin/menu');
415
+ const menu = await res.json();
416
+ if (res.ok && Array.isArray(menu)) {
417
+ renderMenuTable(menu);
418
+ } else {
419
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8 text-rose-400">خطا در بارگذاری منو</td></tr>';
420
+ }
421
+ } catch (e) {
422
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8 text-rose-400">عدم اتصال به سرور</td></tr>';
423
+ }
424
+ }
425
+
426
+ function renderMenuTable(menu) {
427
+ const tbody = document.getElementById('menuTableBody');
428
+ tbody.innerHTML = '';
429
+ if (menu.length === 0) {
430
+ tbody.innerHTML = '<tr><td colspan="5" class="text-center py-8 text-turquoise/60">منو خالی است. آیتم جدید اضافه کنید.</td></tr>';
431
+ return;
432
+ }
433
+ menu.forEach(item => {
434
+ const row = document.createElement('tr');
435
+ row.className = 'hover:bg-tabriz-800/40 transition-colors menu-row';
436
+ row.dataset.id = item.id;
437
+ row.innerHTML = `
438
+ <td class="px-2 py-1.5">
439
+ <span class="view-mode font-bold text-white">${escapeHtml(item.name)}</span>
440
+ <input type="text" class="edit-mode hidden w-full bg-tabriz-800/60 border border-tabriz-600/40 rounded px-2 py-1 text-white text-[11px]" value="${escapeHtml(item.name)}" data-field="name">
441
+ </td>
442
+ <td class="px-2 py-1.5 hidden md:table-cell">
443
+ <span class="view-mode text-zinc-400 text-[10px]">${escapeHtml(item.description || '-')}</span>
444
+ <input type="text" class="edit-mode hidden w-full bg-tabriz-800/60 border border-tabriz-600/40 rounded px-2 py-1 text-white text-[11px]" value="${escapeHtml(item.description || '')}" data-field="description">
445
+ </td>
446
+ <td class="px-2 py-1.5">
447
+ <span class="view-mode text-gold font-medium">${escapeHtml(item.price || '-')}</span>
448
+ <input type="text" class="edit-mode hidden w-full bg-tabriz-800/60 border border-tabriz-600/40 rounded px-2 py-1 text-white text-[11px]" value="${escapeHtml(item.price || '')}" data-field="price">
449
+ </td>
450
+ <td class="px-2 py-1.5 text-center">
451
+ <span class="view-mode text-xs ${item.available ? 'text-emerald-400' : 'text-rose-400'}">${item.available ? 'بله' : 'خیر'}</span>
452
+ <input type="checkbox" class="edit-mode hidden" data-field="available" ${item.available ? 'checked' : ''}>
453
+ </td>
454
+ <td class="px-2 py-1.5 text-center">
455
+ <div class="view-mode flex gap-1 justify-center">
456
+ <button onclick="editMenuItem(this)" class="p-1 text-turquoise/70 hover:text-gold transition-colors" title="ویرایش">
457
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
458
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
459
+ </svg>
460
+ </button>
461
+ <button onclick="deleteMenuItem(${item.id})" class="p-1 text-zinc-600 hover:text-rose-500 transition-colors" title="حذف">
462
+ <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
463
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
464
+ </svg>
465
+ </button>
466
+ </div>
467
+ <div class="edit-mode hidden flex gap-1 justify-center">
468
+ <button onclick="saveMenuItem(this, ${item.id})" class="px-2 py-1 bg-emerald-500/20 border border-emerald-500/40 text-emerald-300 rounded text-[10px]">ذخیره</button>
469
+ <button onclick="cancelEdit(this)" class="px-2 py-1 bg-zinc-800 border border-zinc-600 text-zinc-400 rounded text-[10px]">لغو</button>
470
+ </div>
471
+ </td>
472
+ `;
473
+ tbody.appendChild(row);
474
+ });
475
+ }
476
+
477
+ function escapeHtml(text) {
478
+ return String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#039;');
479
+ }
480
+
481
+ function toggleRowEdit(row, editing) {
482
+ const viewModes = row.querySelectorAll('.view-mode');
483
+ const editModes = row.querySelectorAll('.edit-mode');
484
+ viewModes.forEach(el => el.classList.toggle('hidden', editing));
485
+ editModes.forEach(el => el.classList.toggle('hidden', !editing));
486
+ }
487
+
488
+ function editMenuItem(btn) {
489
+ const row = btn.closest('tr');
490
+ toggleRowEdit(row, true);
491
+ }
492
+
493
+ function cancelEdit(btn) {
494
+ const row = btn.closest('tr');
495
+ // Reset fields to original (re-fetch from data attributes? we can just store original values in dataset)
496
+ // Simple: reload menu
497
+ fetchCurrentMenu();
498
+ }
499
+
500
+ async function saveMenuItem(btn, id) {
501
+ const row = btn.closest('tr');
502
+ const nameInput = row.querySelector('[data-field="name"]');
503
+ const descInput = row.querySelector('[data-field="description"]');
504
+ const priceInput = row.querySelector('[data-field="price"]');
505
+ const availCheck = row.querySelector('[data-field="available"]');
506
+
507
+ const data = {
508
+ name: nameInput.value.trim(),
509
+ description: descInput.value.trim(),
510
+ price: priceInput.value.trim(),
511
+ available: availCheck.checked
512
+ };
513
+ if (!data.name) {
514
+ alert('نام آیتم نمی‌تواند خالی باشد.');
515
+ return;
516
+ }
517
+ try {
518
+ const res = await fetch(`/api/admin/menu/item/${id}`, {
519
+ method: 'PUT',
520
+ headers: { 'Content-Type': 'application/json' },
521
+ body: JSON.stringify(data)
522
+ });
523
+ const result = await res.json();
524
+ if (res.ok && result.success) {
525
+ fetchCurrentMenu();
526
+ } else {
527
+ alert(result.error || 'خطا در بروزرسانی');
528
+ }
529
+ } catch (e) {
530
+ alert('خطای شبکه: ' + e.message);
531
+ }
532
+ }
533
+
534
+ async function deleteMenuItem(id) {
535
+ if (!confirm('آیا از حذف این آیتم اطمینان دارید؟')) return;
536
+ try {
537
+ const res = await fetch(`/api/admin/menu/item/${id}`, { method: 'DELETE' });
538
+ const result = await res.json();
539
+ if (res.ok && result.success) {
540
+ fetchCurrentMenu();
541
+ } else {
542
+ alert(result.error || 'خطا در حذف');
543
+ }
544
+ } catch (e) {
545
+ alert('خطای شبکه: ' + e.message);
546
+ }
547
+ }
548
+
549
+ async function addMenuItemRow() {
550
+ // Create a new empty row with inputs and a save button
551
+ const tbody = document.getElementById('menuTableBody');
552
+ const row = document.createElement('tr');
553
+ row.className = 'bg-tabriz-800/40';
554
+ row.innerHTML = `
555
+ <td class="px-2 py-1.5">
556
+ <input type="text" class="w-full bg-tabriz-800/60 border border-turquoise/40 rounded px-2 py-1 text-white text-[11px]" placeholder="نام محصول" data-field="name">
557
+ </td>
558
+ <td class="px-2 py-1.5 hidden md:table-cell">
559
+ <input type="text" class="w-full bg-tabriz-800/60 border border-tabriz-600/40 rounded px-2 py-1 text-white text-[11px]" placeholder="توضیحات" data-field="description">
560
+ </td>
561
+ <td class="px-2 py-1.5">
562
+ <input type="text" class="w-full bg-tabriz-800/60 border border-tabriz-600/40 rounded px-2 py-1 text-white text-[11px]" placeholder="قیمت" data-field="price">
563
+ </td>
564
+ <td class="px-2 py-1.5 text-center">
565
+ <input type="checkbox" data-field="available" checked>
566
+ </td>
567
+ <td class="px-2 py-1.5 text-center">
568
+ <button onclick="saveNewItem(this)" class="px-3 py-1 bg-emerald-500/20 border border-emerald-500/40 text-emerald-300 rounded text-[10px]">افزودن</button>
569
+ </td>
570
+ `;
571
+ tbody.appendChild(row);
572
+ }
573
+
574
+ async function saveNewItem(btn) {
575
+ const row = btn.closest('tr');
576
+ const name = row.querySelector('[data-field="name"]').value.trim();
577
+ const desc = row.querySelector('[data-field="description"]').value.trim();
578
+ const price = row.querySelector('[data-field="price"]').value.trim();
579
+ const avail = row.querySelector('[data-field="available"]').checked;
580
+ if (!name) {
581
+ alert('نام الزامی است.');
582
+ return;
583
+ }
584
+ try {
585
+ const res = await fetch('/api/admin/menu/item', {
586
+ method: 'POST',
587
+ headers: { 'Content-Type': 'application/json' },
588
+ body: JSON.stringify({ name, description: desc, price, available: avail })
589
+ });
590
+ const result = await res.json();
591
+ if (res.ok && result.success) {
592
+ fetchCurrentMenu();
593
+ } else {
594
+ alert(result.error || 'خطا در افزودن');
595
+ }
596
+ } catch (e) {
597
+ alert('خطای شبکه: ' + e.message);
598
+ }
599
+ }
600
+
601
+ // ==================== Menu extraction (unchanged) ====================
602
  function handleDragOver(e) {
603
  e.preventDefault();
604
  const dropZone = document.getElementById('dropZone');
 
616
  function handleDrop(e) {
617
  e.preventDefault();
618
  handleDragLeave(e);
 
619
  const files = e.dataTransfer.files;
620
  if (files.length > 0) {
621
  const fileInput = document.getElementById('menuFileInput');
 
627
  async function uploadMenuFile(e) {
628
  const file = e.target.files[0];
629
  if (!file) return;
 
630
  const loader = document.getElementById('uploadLoader');
631
  const editor = document.getElementById('menuEditorContainer');
 
632
  loader.classList.remove('hidden');
633
  editor.classList.add('hidden');
 
634
  const formData = new FormData();
635
  formData.append('file', file);
 
636
  try {
637
  const response = await fetch('/api/admin/extract_menu', {
638
  method: 'POST',
639
  body: formData
640
  });
641
  const data = await response.json();
 
642
  loader.classList.add('hidden');
643
  if (response.ok && data.success) {
644
  renderMenuEditor(data.extracted_menu);
 
709
  const tbody = document.getElementById('menuEditorTableBody');
710
  const rows = tbody.querySelectorAll('tr');
711
  const menuData = [];
 
712
  rows.forEach(row => {
713
  const name = row.querySelector('.menu-name').value.trim();
714
  const description = row.querySelector('.menu-desc').value.trim();
715
  const price = row.querySelector('.menu-price').value.trim();
716
  if (name) {
717
+ menuData.push({ name, description, price, available: true });
 
 
 
 
 
718
  }
719
  });
 
720
  if (menuData.length === 0) {
721
  alert("انتشار لیست فاقد محصول امکان‌پذیر نیست.");
722
  return;
723
  }
 
724
  try {
725
  const response = await fetch('/api/admin/save_menu', {
726
  method: 'POST',
 
728
  body: JSON.stringify({ menu: menuData })
729
  });
730
  const data = await response.json();
 
731
  if (response.ok && data.success) {
732
  alert("منوی جدید با موفقیت ثبت و روی وب‌سایت همگام‌سازی شد!");
733
  document.getElementById('menuEditorContainer').classList.add('hidden');
734
+ fetchCurrentMenu(); // refresh main menu table
735
  appendChatMessage('model', 'منوی زنده همگام‌سازی شد. سیستم سفارش‌گیری مشتریان اکنون بر مبنای آخرین لیست ویرایش‌شده کالیبره شده است.');
736
  } else {
737
  alert(data.error || 'بروز خطا در همگام‌سازی ساختار داده‌های جدید.');
 
740
  alert('عدم اتصال به پایگاه داده همگام‌سازی منوها: ' + err.message);
741
  }
742
  }
743
+
744
+ // ==================== Admin Chat (updated with Markdown) ====================
745
  async function sendAdminMessage(e) {
746
  e.preventDefault();
747
  if (isGenerating) return;
 
748
  const input = document.getElementById('chatInput');
749
  const text = input.value.trim();
750
  if (!text) return;
751
+
752
  appendChatMessage('user', text);
753
  input.value = '';
754
  chatHistory.push({ role: 'user', content: text });
755
+
756
  isGenerating = true;
757
  toggleChatLoading(true);
758
  abortController = new AbortController();
759
+
760
  try {
761
  const response = await fetch('/api/admin/chat', {
762
  method: 'POST',
 
765
  signal: abortController.signal
766
  });
767
  const data = await response.json();
 
768
  toggleChatLoading(false);
769
  isGenerating = false;
770
+
771
  if (response.ok && data.success) {
772
+ const botBubbleId = appendChatMessage('model', ''); // empty placeholder
773
  const botBubble = document.getElementById(botBubbleId);
 
774
  const textResponse = data.response;
775
+ let i = 0;
776
+ // Typewriter effect with plain text
777
  function typeText() {
778
  if (i < textResponse.length && !abortController.signal.aborted) {
779
  botBubble.textContent += textResponse.charAt(i);
 
781
  document.getElementById('chatMessages').scrollTop = document.getElementById('chatMessages').scrollHeight;
782
  setTimeout(typeText, 6);
783
  } else {
784
+ // Once done, replace with rendered Markdown
785
+ botBubble.textContent = ''; // clear
786
+ botBubble.innerHTML = marked.parse(textResponse);
787
  chatHistory.push({ role: 'model', content: textResponse });
788
+ // After Nila might have changed menu, refresh the menu list
789
+ fetchCurrentMenu();
790
  }
791
  }
792
  typeText();
 
801
  }
802
  }
803
  }
804
+
805
  function stopAdminChat() {
806
  if (abortController) {
807
  abortController.abort();
 
809
  toggleChatLoading(false);
810
  isGenerating = false;
811
  }
812
+
813
  function toggleChatLoading(loading) {
814
  const loader = document.getElementById('chatLoader');
815
  const sendBtn = document.getElementById('sendBtn');
816
  const input = document.getElementById('chatInput');
 
817
  if (loading) {
818
  loader.classList.remove('hidden');
819
  sendBtn.disabled = true;
 
824
  input.disabled = false;
825
  }
826
  }
827
+
828
  function appendChatMessage(role, content) {
829
  const container = document.getElementById('chatMessages');
830
  const bubbleId = 'bubble-admin-' + Date.now();
831
  let html = '';
832
+
833
  if (role === 'user') {
834
  html = `
835
+ <div class="flex justify-end gap-2.5 max-w-[85%] mr-auto">
836
  <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">
837
+ ${escapeHtml(content)}
838
  </div>
839
  </div>
840
  `;
841
  } else {
842
+ // For model, we use innerHTML with chat-markdown class
843
+ // Note: content will be set later (initially empty for typewriter)
844
  html = `
845
+ <div class="flex gap-2.5 max-w-[85%]">
846
  <div class="w-8 h-8 rounded-xl bg-turquoise/15 border border-turquoise/20 flex items-center justify-center flex-shrink-0">
847
  <span class="text-gold font-extrabold text-[10px] font-mono">AI</span>
848
  </div>
849
+ <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">
850
+ ${content ? marked.parse(content) : ''}
851
  </div>
852
  </div>
853
  `;
854
  }
855
+
856
  container.insertAdjacentHTML('beforeend', html);
857
  container.scrollTop = container.scrollHeight;
858
  return bubbleId;