humanvprojectceo commited on
Commit
6c8b745
·
verified ·
1 Parent(s): 0f3a3d7

Update customer.html

Browse files
Files changed (1) hide show
  1. customer.html +186 -160
customer.html CHANGED
@@ -3,12 +3,12 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Cafe AI - سفارش هوشمند با نیلا</title>
7
- <!-- لود فونت فارسی وزیر -->
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;700&display=swap" rel="stylesheet">
11
- <!-- لود فریمورک Tailwind CSS -->
12
  <script src="https://cdn.tailwindcss.com"></script>
13
  <script>
14
  tailwind.config = {
@@ -18,10 +18,10 @@
18
  sans: ['Vazirmatn', 'sans-serif'],
19
  },
20
  colors: {
21
- cafeDark: '#09090b', /* مشکی بسیار تیره */
22
- cafeCard: '#18181b', /* خاکستری تیره کارت‌ها */
23
- cafeBorder: '#27272a', /* رنگ خطوط حاشیه */
24
- cafeGold: '#d4af37', /* طلایی سنتی کافه */
25
  cafeGoldHover: '#f3cd5c', /* طلایی روشن */
26
  }
27
  }
@@ -33,7 +33,7 @@
33
  background-color: #09090b;
34
  color: #f4f4f5;
35
  }
36
- /* طراحی اسکرول‌بار تاریک */
37
  ::-webkit-scrollbar {
38
  width: 4px;
39
  }
@@ -48,13 +48,23 @@
48
  </head>
49
  <body class="min-h-screen flex flex-col font-sans selection:bg-amber-500 selection:text-black overflow-x-hidden">
50
 
51
- <!-- بخش اول: انتخاب شماره میز (انیمیشن شروع) -->
 
 
 
 
 
 
 
 
 
 
 
52
  <div id="tableSelectorScreen" class="flex-1 flex flex-col items-center justify-center p-6 transition-all duration-500 ease-out">
53
  <div class="max-w-md w-full bg-cafeCard border border-cafeBorder rounded-2xl p-6 shadow-2xl text-center space-y-6">
54
 
55
- <!-- لوگوی قهوه هوشمند -->
56
  <div class="flex justify-center">
57
- <div class="w-16 h-16 rounded-full bg-cafeGold/10 border border-cafeGold/20 flex items-center justify-center animate-pulse">
58
  <svg class="w-8 h-8 text-cafeGold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
59
  <path d="M17 8h1a4 4 0 1 1 0 8h-1" />
60
  <path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
@@ -66,15 +76,14 @@
66
  </div>
67
 
68
  <div class="space-y-2">
69
- <h1 class="text-xl font-bold text-white">خوش آمدید به Cafe AI</h1>
70
- <p class="text-xs text-zinc-400 leading-relaxed">لطفاً برای شروع فرآیند مشاوره و سفارش، شماره میز خود را انتخاب کنید.</p>
71
  </div>
72
 
73
- <!-- گرید دکمه‌های شماره میز -->
74
- <div class="grid grid-cols-4 gap-3 py-2">
75
  <script>
76
- // تولید داینامیک ۱۲ شماره میز با استایل شیک
77
- for (let i = 1; i <= 12; i++) {
78
  document.write(`
79
  <button onclick="selectTable(${i})" id="tableBtn-${i}" class="table-btn aspect-square rounded-xl border border-cafeBorder bg-cafeDark hover:border-cafeGold/40 hover:bg-cafeGold/5 text-zinc-300 font-bold text-sm transition-all flex items-center justify-center">
80
  ${i}
@@ -84,21 +93,17 @@
84
  </script>
85
  </div>
86
 
87
- <!-- دکمه تایید ورود -->
88
  <button id="confirmTableBtn" disabled onclick="confirmTableSelection()" class="w-full py-3 bg-zinc-800 text-zinc-500 font-bold rounded-xl text-sm transition-all flex items-center justify-center gap-2 cursor-not-allowed">
89
- <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
90
- <path stroke-linecap="round" stroke-linejoin="round" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1" />
91
- </svg>
92
- ورود و آغاز گفتگو با نیلا
93
  </button>
94
 
95
  </div>
96
  </div>
97
 
98
- <!-- بخش دوم: چت باکس هوشمند مشتری با نیلا (در ابتدا مخفی) -->
99
  <div id="chatScreen" class="hidden flex-1 flex flex-col h-screen max-w-2xl w-full mx-auto border-x border-cafeBorder bg-cafeDark transition-all duration-500 opacity-0 transform translate-y-4">
100
 
101
- <!-- هدر چت مشتری -->
102
  <header class="px-4 py-3 border-b border-cafeBorder bg-cafeCard/60 backdrop-blur flex items-center justify-between">
103
  <div class="flex items-center gap-3">
104
  <div class="w-9 h-9 rounded-full bg-cafeGold/10 border border-cafeGold/30 flex items-center justify-center">
@@ -108,41 +113,39 @@
108
  </svg>
109
  </div>
110
  <div>
111
- <h2 class="text-sm font-bold text-white">دستیار سفارشات (Nila)</h2>
112
  <p class="text-[9px] text-zinc-400">میز شماره <span id="activeTableLabel" class="text-cafeGold font-bold">-</span></p>
113
  </div>
114
  </div>
115
 
116
  <div class="flex items-center gap-2">
117
- <span class="w-2 h-2 rounded-full bg-emerald-500 animate-pulse"></span>
118
- <span class="text-[10px] text-zinc-500">آنلاین</span>
119
  </div>
120
  </header>
121
 
122
- <!-- باکس پیام‌های چت -->
123
  <div id="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-4">
124
  <!-- پیام‌ها به صورت داینامیک اضافه می‌شوند -->
125
  </div>
126
 
127
- <!-- کارت معلق فاکتور تایید نهایی سفارش (فقط در زمان آماده شدن پیش‌نویس ظاهر می‌شود) -->
128
- <div id="draftOrderContainer" class="hidden px-4 py-3 border-t border-cafeBorder bg-cafeCard/90 backdrop-blur space-y-3">
129
  <div class="flex items-center justify-between">
130
- <span class="text-xs font-bold text-amber-400 flex items-center gap-1">
131
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
132
  <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" />
133
  </svg>
134
- پیشنویس نهایی سفارش شما
135
  </span>
136
- <span class="text-[10px] text-zinc-500">بررسی و ارسال به آشپزخانه</span>
137
  </div>
138
 
139
- <!-- آیتم‌های پیش‌نویس شده -->
140
  <div id="draftItemsList" class="space-y-1.5 max-h-[100px] overflow-y-auto">
141
- <!-- ساختار داینامیک اضافه می‌شود -->
142
  </div>
143
 
144
- <!-- دکمه تایید نهایی سفارش و بسته شدن گفتگو -->
145
- <button onclick="submitFinalOrder()" class="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded-lg text-xs transition-all flex items-center justify-center gap-2 shadow-lg shadow-emerald-950/20">
146
  <svg class="w-4 h-4" 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>
@@ -150,29 +153,28 @@
150
  </button>
151
  </div>
152
 
153
- <!-- لودر استریمینگ و دکمه توقف -->
154
  <div id="streamingController" class="hidden px-4 py-2 border-t border-cafeBorder/40 bg-cafeCard/30 flex items-center justify-between">
155
- <div class="flex items-center gap-2 text-[11px] text-zinc-400">
156
  <div class="flex space-x-1 space-x-reverse">
157
  <span class="w-1.5 h-1.5 bg-cafeGold rounded-full animate-bounce" style="animation-delay: 0.1s"></span>
158
  <span class="w-1.5 h-1.5 bg-cafeGold rounded-full animate-bounce" style="animation-delay: 0.2s"></span>
159
  <span class="w-1.5 h-1.5 bg-cafeGold rounded-full animate-bounce" style="animation-delay: 0.3s"></span>
160
  </div>
161
- <span>نیلا در حال نوشتن...</span>
162
  </div>
163
 
164
- <!-- دکمه استاپ با SVG قرمز -->
165
- <button onclick="stopStreaming()" class="flex items-center gap-1.5 px-3 py-1 bg-rose-950/40 border border-rose-800/40 hover:bg-rose-900/60 rounded text-[10px] text-rose-400 font-bold transition-all">
166
- <svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="currentColor">
167
  <rect x="4" y="4" width="16" height="16" rx="2" />
168
  </svg>
169
- توقف پاسخ
170
  </button>
171
  </div>
172
 
173
- <!-- فرم ارسال پیام مشتری -->
174
  <form id="customerChatForm" onsubmit="sendCustomerMessage(event)" class="p-3 border-t border-cafeBorder bg-cafeCard/20 flex gap-2">
175
- <input type="text" id="customerChatInput" placeholder="به نیلا بگویید چه سلیقه‌ای دارید..." autocomplete="off" class="flex-1 bg-cafeCard border border-cafeBorder text-xs text-zinc-200 placeholder-zinc-500 rounded-xl px-4 py-3 focus:outline-none focus:border-cafeGold/50 transition-all">
176
  <button type="submit" id="sendBtn" class="bg-cafeGold hover:bg-cafeGoldHover text-zinc-950 font-bold px-5 py-3 rounded-xl text-xs transition-all flex items-center justify-center flex-shrink-0">
177
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
178
  <path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />
@@ -182,31 +184,36 @@
182
 
183
  </div>
184
 
185
- <!-- اسکریپت‌های کلاینت سفارشات -->
186
  <script>
 
 
 
187
  let selectedTableNumber = null;
188
- let chatHistory = [];
189
  let abortController = null;
190
  let isGenerating = false;
191
- let currentDraftOrder = null;
192
 
193
- // --- مدیریت شماره میز ---
 
 
 
 
 
 
194
 
195
  function selectTable(num) {
196
- // حذف کلاس‌های اکتیو از سایر دکمه‌های میز
197
  document.querySelectorAll('.table-btn').forEach(btn => {
198
- btn.classList.remove('border-cafeGold', 'bg-cafeGold/10', 'text-cafeGold', 'shadow-lg', 'shadow-amber-500/10');
199
  btn.classList.add('border-cafeBorder', 'bg-cafeDark', 'text-zinc-300');
200
  });
201
 
202
- // اعمال استایل طلایی روی دکمه میز انتخابی
203
  const activeBtn = document.getElementById(`tableBtn-${num}`);
204
  activeBtn.classList.remove('border-cafeBorder', 'bg-cafeDark', 'text-zinc-300');
205
- activeBtn.classList.add('border-cafeGold', 'bg-cafeGold/10', 'text-cafeGold', 'shadow-lg', 'shadow-amber-500/10');
206
 
207
  selectedTableNumber = num;
208
 
209
- // فعال کردن دکمه ورود
210
  const confirmBtn = document.getElementById('confirmTableBtn');
211
  confirmBtn.disabled = false;
212
  confirmBtn.classList.remove('bg-zinc-800', 'text-zinc-500', 'cursor-not-allowed');
@@ -215,8 +222,10 @@
215
 
216
  function confirmTableSelection() {
217
  if (!selectedTableNumber) return;
 
 
218
 
219
- // انیمیشن جابجایی نرم بین صفحات
220
  const screen1 = document.getElementById('tableSelectorScreen');
221
  const screen2 = document.getElementById('chatScreen');
222
 
@@ -226,22 +235,18 @@
226
  screen2.classList.remove('hidden');
227
  setTimeout(() => {
228
  screen2.classList.remove('opacity-0', 'translate-y-4');
229
- // مقداردهی هدر صفحه چت
230
  document.getElementById('activeTableLabel').innerText = selectedTableNumber;
231
- // شروع به جریان انداختن پیام خوش‌آمدگویی نیلا
232
  triggerNilaGreeting();
233
  }, 50);
234
  }, 300);
235
  }
236
 
237
 
238
- // --- هندل کردن چت با نیلا (بخش استریمینگ) ---
239
 
240
  async function triggerNilaGreeting() {
241
- // برای خوش‌آمدگویی طبیعی و هماهنگ با استریم، سیگنال ورودی پنهان به نیلا می‌فرستیم
242
- const initialPrompt = "سلام. من سر میز مستقر شدم. لطفا خوش آمدگویی کن و بگو چطور میتونی کمکم کنی.";
243
- chatHistory.push({ role: 'user', content: initialPrompt });
244
- await fetchNilaStream();
245
  }
246
 
247
  async function sendCustomerMessage(e) {
@@ -255,92 +260,100 @@
255
  appendChatBubble('user', userText);
256
  input.value = '';
257
 
258
- chatHistory.push({ role: 'user', content: userText });
259
- await fetchNilaStream();
260
  }
261
 
262
- async function fetchNilaStream() {
263
  if (isGenerating) return;
264
-
265
  isGenerating = true;
266
  toggleInputs(true);
267
 
268
- // ایجاد آبورت کنترلر برای لغو عملیات استریمینگ در صورت درخواست کاربر
269
  abortController = new AbortController();
270
 
271
- // ساخت حباب پیام خام برای نیلا که قرار است پر شود
272
  const botBubbleId = appendChatBubble('model', '');
273
- const botMessageContainer = document.getElementById(botBubbleId);
274
 
275
  try {
276
- const response = await fetch('/api/customer/chat', { // از مسیر استریم اتمیک چت استفاده می‌شود
 
277
  method: 'POST',
278
  headers: { 'Content-Type': 'application/json' },
279
- body: JSON.stringify({
280
- table_number: String(selectedTableNumber),
281
- messages: chatHistory
282
- }),
283
  signal: abortController.signal
284
  });
285
 
286
- if (!response.ok) {
287
- throw new Error("اختلال در پاسخ سرور");
288
- }
289
-
290
- // بدست آوردن متن پاسخ مستقیم
291
- const result = await response.json();
292
-
293
- if (result.success) {
294
- // شبیه‌سازی افکت تایپ حروف (Streaming Effect) برای حس کاربری مدرن‌تر
295
- let i = 0;
296
- const responseText = result.response;
297
-
298
- function typeLetter() {
299
- if (i < responseText.length && isGenerating) {
300
- botMessageContainer.innerText += responseText.charAt(i);
301
- i++;
302
- document.getElementById('messagesContainer').scrollTop = document.getElementById('messagesContainer').scrollHeight;
303
- setTimeout(typeLetter, 10); // سرعت تایپ حروف
304
- } else {
305
- finalizeTurn(responseText, result.draft_order);
 
 
 
 
 
 
 
 
 
 
 
 
306
  }
307
  }
308
- typeLetter();
309
-
310
- } else {
311
- botMessageContainer.innerText = result.error || "نیلا در حال حاضر پاسخگو نیست.";
312
- finalizeTurn(botMessageContainer.innerText, null);
313
  }
314
 
 
 
315
  } catch (err) {
316
  if (err.name === 'AbortError') {
317
- botMessageContainer.innerText += ' [تولید پاسخ توسط کاربر متوقف شد]';
318
  } else {
319
- botMessageContainer.innerText = "متاسفانه مشکلی در ارتباط به وجود آمد: " + err.message;
320
  }
321
- finalizeTurn(botMessageContainer.innerText, null);
322
  }
323
  }
324
 
325
- function stopStreaming() {
326
  if (abortController) {
327
  abortController.abort();
328
  }
329
  isGenerating = false;
330
  toggleInputs(false);
 
 
 
 
 
 
 
331
  }
332
 
333
- function finalizeTurn(fullResponse, draftOrder) {
334
  isGenerating = false;
335
  toggleInputs(false);
336
-
337
- // ذخیره پاسخ نهایی در تاریخچه به عنوان کانتکست گفتگوهای بعد
338
- chatHistory.push({ role: 'model', content: fullResponse });
339
-
340
- // مدیریت نمایش پیش‌نویس سفارش
341
- if (draftOrder && draftOrder.items && draftOrder.items.length > 0) {
342
- showDraftCard(draftOrder);
343
- }
344
  }
345
 
346
  function toggleInputs(generating) {
@@ -351,22 +364,19 @@
351
  if (generating) {
352
  input.disabled = true;
353
  sendBtn.disabled = true;
354
- sendBtn.classList.add('opacity-50', 'cursor-not-allowed');
355
  streamController.classList.remove('hidden');
356
  } else {
357
  input.disabled = false;
358
  sendBtn.disabled = false;
359
- sendBtn.classList.remove('opacity-50', 'cursor-not-allowed');
360
  streamController.classList.add('hidden');
361
  }
362
  }
363
 
364
-
365
- // --- نمایش حباب پیام‌ها در بدنه چت باکس ---
366
-
367
  function appendChatBubble(role, content) {
368
  const container = document.getElementById('messagesContainer');
369
- const bubbleId = 'bubble-' + Date.now() + '-' + Math.floor(Math.random() * 1000);
370
 
371
  let bubbleHtml = '';
372
  if (role === 'user') {
@@ -399,17 +409,17 @@
399
  }
400
 
401
 
402
- // --- مدیریت کارت پیشنویس فاکتور و تایید نهایی ---
403
 
404
- function showDraftCard(draft) {
405
- currentDraftOrder = draft;
406
  const container = document.getElementById('draftOrderContainer');
407
  const list = document.getElementById('draftItemsList');
408
 
409
  list.innerHTML = '';
410
- draft.items.forEach(item => {
411
  list.insertAdjacentHTML('beforeend', `
412
- <div class="flex items-center justify-between text-[11px] bg-cafeDark/80 border border-cafeBorder/40 px-3 py-1.5 rounded-lg">
413
  <span class="text-zinc-200 font-bold">${item.name}</span>
414
  <span class="text-cafeGold font-bold">${item.quantity} عدد</span>
415
  </div>
@@ -420,7 +430,7 @@
420
  }
421
 
422
  async function submitFinalOrder() {
423
- if (!currentDraftOrder) return;
424
 
425
  try {
426
  const response = await fetch('/api/confirm_order', {
@@ -428,61 +438,77 @@
428
  headers: { 'Content-Type': 'application/json' },
429
  body: JSON.stringify({
430
  table_number: String(selectedTableNumber),
431
- items: currentDraftOrder.items
432
  })
433
  });
434
 
435
  const result = await response.json();
436
  if (response.ok && result.success) {
437
- // بستن فاکتور
438
  document.getElementById('draftOrderContainer').classList.add('hidden');
439
-
440
- // پاکسازی نهایی چت و برگشت به حالت انتخاب میز جهت خالی شدن منابع سرور
441
- alert("سفارش شما با موفقیت ثبت شد و به آشپزخانه ارسال گردید. گفتگو بسته خواهد شد.");
442
-
443
- resetSessionAndExit();
444
  } else {
445
- alert(result.error || "خطا در تایید سفارش.");
446
  }
447
  } catch (err) {
448
- alert("خطا در برقراری ارتباط با سرور: " + err.message);
449
  }
450
  }
451
 
452
- function resetSessionAndExit() {
453
- // حذف کامل متغیرها
454
- selectedTableNumber = null;
455
- chatHistory = [];
456
- currentDraftOrder = null;
457
- if (abortController) abortController.abort();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
458
  isGenerating = false;
 
459
 
460
- // تخلیه صفحه چت باکس
461
  document.getElementById('messagesContainer').innerHTML = '';
462
  document.getElementById('draftOrderContainer').classList.add('hidden');
463
 
464
- // انیمیشن جابجایی معکوس به صفحه انتخاب میز
465
- const screen1 = document.getElementById('tableSelectorScreen');
466
- const screen2 = document.getElementById('chatScreen');
 
 
 
 
467
 
468
- // ریست دکمه‌های شماره میز
469
- document.querySelectorAll('.table-btn').forEach(btn => {
470
- btn.classList.remove('border-cafeGold', 'bg-cafeGold/10', 'text-cafeGold', 'shadow-lg', 'shadow-amber-500/10');
471
- btn.classList.add('border-cafeBorder', 'bg-cafeDark', 'text-zinc-300');
472
- });
473
- const confirmBtn = document.getElementById('confirmTableBtn');
474
- confirmBtn.disabled = true;
475
- confirmBtn.classList.remove('bg-cafeGold', 'text-zinc-950', 'hover:bg-goldHover');
476
- confirmBtn.classList.add('bg-zinc-800', 'text-zinc-500', 'cursor-not-allowed');
477
 
478
- screen2.classList.add('opacity-0', 'translate-y-4');
479
- setTimeout(() => {
480
- screen2.classList.add('hidden');
481
- screen1.classList.remove('hidden');
 
 
482
  setTimeout(() => {
483
- screen1.classList.remove('opacity-0', 'scale-95');
484
- }, 50);
485
- }, 300);
 
 
 
 
486
  }
487
  </script>
488
  </body>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Cafe AI - سفارش هوشمند</title>
7
+ <!-- بارگذاری قلم فارسی Vazirmatn -->
8
  <link rel="preconnect" href="https://fonts.googleapis.com">
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
  <link href="https://fonts.googleapis.com/css2?family=Vazirmatn:wght@300;400;700&display=swap" rel="stylesheet">
11
+ <!-- بارگذاری Tailwind CSS -->
12
  <script src="https://cdn.tailwindcss.com"></script>
13
  <script>
14
  tailwind.config = {
 
18
  sans: ['Vazirmatn', 'sans-serif'],
19
  },
20
  colors: {
21
+ cafeDark: '#09090b', /* مشکی مطلق برای پس‌زمینه */
22
+ cafeCard: '#18181b', /* خاکستری تیره برای کارت‌ها */
23
+ cafeBorder: '#27272a', /* خطوط راهنما */
24
+ cafeGold: '#d4af37', /* طلایی کلاسیک کافه */
25
  cafeGoldHover: '#f3cd5c', /* طلایی روشن */
26
  }
27
  }
 
33
  background-color: #09090b;
34
  color: #f4f4f5;
35
  }
36
+ /* کاستومایز کردن نوار اسکرول */
37
  ::-webkit-scrollbar {
38
  width: 4px;
39
  }
 
48
  </head>
49
  <body class="min-h-screen flex flex-col font-sans selection:bg-amber-500 selection:text-black overflow-x-hidden">
50
 
51
+ <!-- لایه موفقیت آمیز اتمام سفارش (Overlay) -->
52
+ <div id="successOverlay" class="hidden fixed inset-0 bg-cafeDark/95 z-50 flex flex-col items-center justify-center p-6 text-center space-y-4 transition-all duration-500 opacity-0">
53
+ <div class="w-16 h-16 rounded-full bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center animate-bounce">
54
+ <svg class="w-8 h-8 text-emerald-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
55
+ <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
56
+ </svg>
57
+ </div>
58
+ <h2 class="text-base font-bold text-white">سفارش شما با موفقیت ثبت شد!</h2>
59
+ <p class="text-xs text-zinc-400 max-w-xs leading-relaxed">فاکتور نهایی شما به آشپزخانه ارسال گردید. جهت خالی شدن منابع سرور، تاریخچه گفتگوی شما پاک‌سازی و ریست شد.</p>
60
+ </div>
61
+
62
+ <!-- بخش اول: گام انتخاب دستی شماره میز (در صورت عدم دسترسی از آدرس مستقیم) -->
63
  <div id="tableSelectorScreen" class="flex-1 flex flex-col items-center justify-center p-6 transition-all duration-500 ease-out">
64
  <div class="max-w-md w-full bg-cafeCard border border-cafeBorder rounded-2xl p-6 shadow-2xl text-center space-y-6">
65
 
 
66
  <div class="flex justify-center">
67
+ <div class="w-16 h-16 rounded-full bg-cafeGold/10 border border-cafeGold/20 flex items-center justify-center">
68
  <svg class="w-8 h-8 text-cafeGold" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
69
  <path d="M17 8h1a4 4 0 1 1 0 8h-1" />
70
  <path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4Z" />
 
76
  </div>
77
 
78
  <div class="space-y-2">
79
+ <h1 class="text-lg font-bold text-white">سامانه سفارشات Cafe AI</h1>
80
+ <p class="text-xs text-zinc-400 leading-relaxed">لطفاً برای اتصال به دستیار صوتی و متنی، شماره میز خود را انتخاب فرمایید.</p>
81
  </div>
82
 
83
+ <!-- گرید انتخاب شماره میز -->
84
+ <div class="grid grid-cols-4 gap-3 py-1">
85
  <script>
86
+ for (let i = 1; i <= 5; i++) {
 
87
  document.write(`
88
  <button onclick="selectTable(${i})" id="tableBtn-${i}" class="table-btn aspect-square rounded-xl border border-cafeBorder bg-cafeDark hover:border-cafeGold/40 hover:bg-cafeGold/5 text-zinc-300 font-bold text-sm transition-all flex items-center justify-center">
89
  ${i}
 
93
  </script>
94
  </div>
95
 
 
96
  <button id="confirmTableBtn" disabled onclick="confirmTableSelection()" class="w-full py-3 bg-zinc-800 text-zinc-500 font-bold rounded-xl text-sm transition-all flex items-center justify-center gap-2 cursor-not-allowed">
97
+ ورود به سامانه گفتگو و سفارش
 
 
 
98
  </button>
99
 
100
  </div>
101
  </div>
102
 
103
+ <!-- بخش دوم: چت باکس هوشمند مشتری با نیلا -->
104
  <div id="chatScreen" class="hidden flex-1 flex flex-col h-screen max-w-2xl w-full mx-auto border-x border-cafeBorder bg-cafeDark transition-all duration-500 opacity-0 transform translate-y-4">
105
 
106
+ <!-- هدر چت -->
107
  <header class="px-4 py-3 border-b border-cafeBorder bg-cafeCard/60 backdrop-blur flex items-center justify-between">
108
  <div class="flex items-center gap-3">
109
  <div class="w-9 h-9 rounded-full bg-cafeGold/10 border border-cafeGold/30 flex items-center justify-center">
 
113
  </svg>
114
  </div>
115
  <div>
116
+ <h2 class="text-sm font-bold text-white">دستیار هوشمند نیلا (Nila)</h2>
117
  <p class="text-[9px] text-zinc-400">میز شماره <span id="activeTableLabel" class="text-cafeGold font-bold">-</span></p>
118
  </div>
119
  </div>
120
 
121
  <div class="flex items-center gap-2">
122
+ <span class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse"></span>
123
+ <span class="text-[9px] text-zinc-500">جریان متصل</span>
124
  </div>
125
  </header>
126
 
127
+ <!-- باکس نمایش پیام‌های گفتگو -->
128
  <div id="messagesContainer" class="flex-1 overflow-y-auto p-4 space-y-4">
129
  <!-- پیام‌ها به صورت داینامیک اضافه می‌شوند -->
130
  </div>
131
 
132
+ <!-- فاکتور موقت جهت ثبت نهایی سفارش -->
133
+ <div id="draftOrderContainer" class="hidden px-4 py-3 border-t border-cafeBorder bg-cafeCard/95 backdrop-blur space-y-3">
134
  <div class="flex items-center justify-between">
135
+ <span class="text-xs font-bold text-amber-400 flex items-center gap-1.5">
136
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
137
  <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" />
138
  </svg>
139
+ فاکتور پیشنهادی جهت تایید نهایی
140
  </span>
141
+ <span class="text-[9px] text-zinc-500">قابل بازبینی پیش از ثبت</span>
142
  </div>
143
 
 
144
  <div id="draftItemsList" class="space-y-1.5 max-h-[100px] overflow-y-auto">
145
+ <!-- محصولات سفارش فرضی رندر می‌شوند -->
146
  </div>
147
 
148
+ <button onclick="submitFinalOrder()" class="w-full py-2.5 bg-emerald-600 hover:bg-emerald-500 text-white font-bold rounded-lg text-xs transition-all flex items-center justify-center gap-2 shadow-lg">
 
149
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
150
  <path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
151
  </svg>
 
153
  </button>
154
  </div>
155
 
156
+ <!-- بخش لودینگ استریمینگ و کنترل توقف پاسخ -->
157
  <div id="streamingController" class="hidden px-4 py-2 border-t border-cafeBorder/40 bg-cafeCard/30 flex items-center justify-between">
158
+ <div class="flex items-center gap-2 text-[10px] text-zinc-400">
159
  <div class="flex space-x-1 space-x-reverse">
160
  <span class="w-1.5 h-1.5 bg-cafeGold rounded-full animate-bounce" style="animation-delay: 0.1s"></span>
161
  <span class="w-1.5 h-1.5 bg-cafeGold rounded-full animate-bounce" style="animation-delay: 0.2s"></span>
162
  <span class="w-1.5 h-1.5 bg-cafeGold rounded-full animate-bounce" style="animation-delay: 0.3s"></span>
163
  </div>
164
+ <span>نیلا در حال پردازش کلمات...</span>
165
  </div>
166
 
167
+ <button onclick="stopStreaming()" class="flex items-center gap-1 px-2.5 py-1 bg-rose-950/40 border border-rose-800/40 hover:bg-rose-900/60 rounded text-[9px] text-rose-400 font-bold transition-all">
168
+ <svg class="w-3 h-3" viewBox="0 0 24 24" fill="currentColor">
 
169
  <rect x="4" y="4" width="16" height="16" rx="2" />
170
  </svg>
171
+ توقف تولید پاسخ
172
  </button>
173
  </div>
174
 
175
+ <!-- فرم پایینی نوشتن و ارسال پیام -->
176
  <form id="customerChatForm" onsubmit="sendCustomerMessage(event)" class="p-3 border-t border-cafeBorder bg-cafeCard/20 flex gap-2">
177
+ <input type="text" id="customerChatInput" placeholder="پیام خود را به نیلا بگویید..." autocomplete="off" class="flex-1 bg-cafeCard border border-cafeBorder text-xs text-zinc-200 placeholder-zinc-500 rounded-xl px-4 py-3 focus:outline-none focus:border-cafeGold/50 transition-all">
178
  <button type="submit" id="sendBtn" class="bg-cafeGold hover:bg-cafeGoldHover text-zinc-950 font-bold px-5 py-3 rounded-xl text-xs transition-all flex items-center justify-center flex-shrink-0">
179
  <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5">
180
  <path stroke-linecap="round" stroke-linejoin="round" d="M14 5l7 7m0 0l-7 7m7-7H3" />
 
184
 
185
  </div>
186
 
187
+ <!-- منطق کلاینت و پردازش‌های استریمینگ کلمات -->
188
  <script>
189
+ // تزریق هوشمند شماره میز دریافتی از سرور از طریق آدرس صفحه
190
+ const initialTableNumber = "{{ table_number }}";
191
+
192
  let selectedTableNumber = null;
 
193
  let abortController = null;
194
  let isGenerating = false;
195
+ let currentDraftItems = null;
196
 
197
+ // بررسی در زمان لود صفحه: اگر آدرس شامل شماره میز معتبر بود، صفحه انتخاب میز را رد کند
198
+ window.addEventListener('DOMContentLoaded', () => {
199
+ if (initialTableNumber && initialTableNumber !== "" && initialTableNumber !== "None" && !isNaN(initialTableNumber)) {
200
+ selectedTableNumber = parseInt(initialTableNumber);
201
+ bypassTableSelector();
202
+ }
203
+ });
204
 
205
  function selectTable(num) {
 
206
  document.querySelectorAll('.table-btn').forEach(btn => {
207
+ btn.classList.remove('border-cafeGold', 'bg-cafeGold/10', 'text-cafeGold');
208
  btn.classList.add('border-cafeBorder', 'bg-cafeDark', 'text-zinc-300');
209
  });
210
 
 
211
  const activeBtn = document.getElementById(`tableBtn-${num}`);
212
  activeBtn.classList.remove('border-cafeBorder', 'bg-cafeDark', 'text-zinc-300');
213
+ activeBtn.classList.add('border-cafeGold', 'bg-cafeGold/10', 'text-cafeGold');
214
 
215
  selectedTableNumber = num;
216
 
 
217
  const confirmBtn = document.getElementById('confirmTableBtn');
218
  confirmBtn.disabled = false;
219
  confirmBtn.classList.remove('bg-zinc-800', 'text-zinc-500', 'cursor-not-allowed');
 
222
 
223
  function confirmTableSelection() {
224
  if (!selectedTableNumber) return;
225
+ bypassTableSelector();
226
+ }
227
 
228
+ function bypassTableSelector() {
229
  const screen1 = document.getElementById('tableSelectorScreen');
230
  const screen2 = document.getElementById('chatScreen');
231
 
 
235
  screen2.classList.remove('hidden');
236
  setTimeout(() => {
237
  screen2.classList.remove('opacity-0', 'translate-y-4');
 
238
  document.getElementById('activeTableLabel').innerText = selectedTableNumber;
 
239
  triggerNilaGreeting();
240
  }, 50);
241
  }, 300);
242
  }
243
 
244
 
245
+ // --- سیستم استریمینگ زنده چت (Server-Sent Events Parser) ---
246
 
247
  async function triggerNilaGreeting() {
248
+ // آغاز گفتگو با یک سیگنال پنهان جهت خوش‌آمدگویی استریمینگ نیلا
249
+ await fetchNilaStream("سلام من سر میز نشستم. لطفاً خودتو معرفی کن و خوشآمد بگو.");
 
 
250
  }
251
 
252
  async function sendCustomerMessage(e) {
 
260
  appendChatBubble('user', userText);
261
  input.value = '';
262
 
263
+ await fetchNilaStream(userText);
 
264
  }
265
 
266
+ async function fetchNilaStream(userText) {
267
  if (isGenerating) return;
268
+
269
  isGenerating = true;
270
  toggleInputs(true);
271
 
272
+ // مقداردهی آبورت کنترلر جهت امکان لغو درخواست
273
  abortController = new AbortController();
274
 
275
+ // ایجاد حباب موقت خالی برای شروع جریان کلمات نیلا
276
  const botBubbleId = appendChatBubble('model', '');
277
+ const botBubble = document.getElementById(botBubbleId);
278
 
279
  try {
280
+ // ارسال درخواست POST برای برقراری ارتباط جریان استریمینگ با متد SSE
281
+ const response = await fetch(`/api/customer/chat_stream/${selectedTableNumber}`, {
282
  method: 'POST',
283
  headers: { 'Content-Type': 'application/json' },
284
+ body: JSON.stringify({ message: userText }),
 
 
 
285
  signal: abortController.signal
286
  });
287
 
288
+ if (!response.ok) throw new Error("اتصال شبکه ناموفق بود.");
289
+
290
+ const reader = response.body.getReader();
291
+ const decoder = new TextDecoder("utf-8");
292
+ let buffer = "";
293
+
294
+ while (true) {
295
+ const { value, done } = await reader.read();
296
+ if (done) break;
297
+
298
+ buffer += decoder.decode(value, { stream: true });
299
+ const lines = buffer.split("\n");
300
+ buffer = lines.pop(); // باقیمانده ناخوانده را در بافر نگه دار
301
+
302
+ for (const line of lines) {
303
+ if (!line.trim() || !line.startsWith("data: ")) continue;
304
+
305
+ const jsonStr = line.substring(6).trim();
306
+ try {
307
+ const data = JSON.parse(jsonStr);
308
+
309
+ if (data.type === 'text') {
310
+ // اضافه شدن لحظه‌ای کلمات به حباب چت
311
+ botBubble.innerText += data.content;
312
+ } else if (data.type === 'draft') {
313
+ // نمایش فاکتور نهایی
314
+ showDraftCard(data.items);
315
+ } else if (data.type === 'error') {
316
+ botBubble.innerText = data.content;
317
+ }
318
+ } catch (e) {
319
+ console.error("خطا در پارس فریمورک استریم:", e);
320
  }
321
  }
322
+
323
+ // اسکرول نرم به پایین حین لود شدن کلمات
324
+ document.getElementById('messagesContainer').scrollTop = document.getElementById('messagesContainer').scrollHeight;
 
 
325
  }
326
 
327
+ finalizeStreamTurn();
328
+
329
  } catch (err) {
330
  if (err.name === 'AbortError') {
331
+ botBubble.innerText += ' [فرآیند تولید کلمات متوقف شد]';
332
  } else {
333
+ botBubble.innerText = "بروز خطا در برقراری ارتباط با دستیار: " + err.message;
334
  }
335
+ finalizeStreamTurn();
336
  }
337
  }
338
 
339
+ async function stopStreaming() {
340
  if (abortController) {
341
  abortController.abort();
342
  }
343
  isGenerating = false;
344
  toggleInputs(false);
345
+
346
+ // ارسال فرستنده توقف به بک‌اند اتمیک جهت توقف پردازش سرور
347
+ try {
348
+ await fetch(`/api/customer/stop/${selectedTableNumber}`, { method: 'POST' });
349
+ } catch (e) {
350
+ console.error(e);
351
+ }
352
  }
353
 
354
+ function finalizeStreamTurn() {
355
  isGenerating = false;
356
  toggleInputs(false);
 
 
 
 
 
 
 
 
357
  }
358
 
359
  function toggleInputs(generating) {
 
364
  if (generating) {
365
  input.disabled = true;
366
  sendBtn.disabled = true;
367
+ sendBtn.classList.add('opacity-40', 'cursor-not-allowed');
368
  streamController.classList.remove('hidden');
369
  } else {
370
  input.disabled = false;
371
  sendBtn.disabled = false;
372
+ sendBtn.classList.remove('opacity-40', 'cursor-not-allowed');
373
  streamController.classList.add('hidden');
374
  }
375
  }
376
 
 
 
 
377
  function appendChatBubble(role, content) {
378
  const container = document.getElementById('messagesContainer');
379
+ const bubbleId = 'bubble-' + Date.now();
380
 
381
  let bubbleHtml = '';
382
  if (role === 'user') {
 
409
  }
410
 
411
 
412
+ // --- تایید دو مرحلهای سفارشات موقت ---
413
 
414
+ function showDraftCard(items) {
415
+ currentDraftItems = items;
416
  const container = document.getElementById('draftOrderContainer');
417
  const list = document.getElementById('draftItemsList');
418
 
419
  list.innerHTML = '';
420
+ items.forEach(item => {
421
  list.insertAdjacentHTML('beforeend', `
422
+ <div class="flex items-center justify-between text-[11px] bg-cafeDark border border-cafeBorder px-3 py-2 rounded-lg">
423
  <span class="text-zinc-200 font-bold">${item.name}</span>
424
  <span class="text-cafeGold font-bold">${item.quantity} عدد</span>
425
  </div>
 
430
  }
431
 
432
  async function submitFinalOrder() {
433
+ if (!currentDraftItems) return;
434
 
435
  try {
436
  const response = await fetch('/api/confirm_order', {
 
438
  headers: { 'Content-Type': 'application/json' },
439
  body: JSON.stringify({
440
  table_number: String(selectedTableNumber),
441
+ items: currentDraftItems
442
  })
443
  });
444
 
445
  const result = await response.json();
446
  if (response.ok && result.success) {
447
+ // مخفی کردن فاکتور و لود موقت اورلی خروج جهت پاک‌سازی نشست‌ها
448
  document.getElementById('draftOrderContainer').classList.add('hidden');
449
+ showSuccessOverlay();
 
 
 
 
450
  } else {
451
+ alert(result.error || "تایید نهایی با خطا مواجه شد.");
452
  }
453
  } catch (err) {
454
+ alert("خطای سیستم در برقراری ارتباط: " + err.message);
455
  }
456
  }
457
 
458
+ function showSuccessOverlay() {
459
+ const overlay = document.getElementById('successOverlay');
460
+ overlay.classList.remove('hidden');
461
+ setTimeout(() => {
462
+ overlay.classList.remove('opacity-0');
463
+ }, 50);
464
+
465
+ // پاک‌سازی کامل نشست و بازگشت به صفحه اول پس از ۴ ثانیه
466
+ setTimeout(() => {
467
+ overlay.classList.add('opacity-0');
468
+ setTimeout(() => {
469
+ overlay.classList.add('hidden');
470
+ resetSessionAndReturn();
471
+ }, 500);
472
+ }, 4000);
473
+ }
474
+
475
+ function resetSessionAndReturn() {
476
+ // ریست کل متغیرها
477
+ abortController = null;
478
  isGenerating = false;
479
+ currentDraftItems = null;
480
 
481
+ // تخلیه چتباکس
482
  document.getElementById('messagesContainer').innerHTML = '';
483
  document.getElementById('draftOrderContainer').classList.add('hidden');
484
 
485
+ // اگر کاربر از ابتدا مستقیماً از طریق روت آدرس وارد شده بود، گفتگو را مجدد لود کند، در غیر این صورت به سلکتور برود
486
+ if (initialTableNumber && initialTableNumber !== "" && initialTableNumber !== "None" && !isNaN(initialTableNumber)) {
487
+ triggerNilaGreeting();
488
+ } else {
489
+ selectedTableNumber = null;
490
+ const screen1 = document.getElementById('tableSelectorScreen');
491
+ const screen2 = document.getElementById('chatScreen');
492
 
493
+ document.querySelectorAll('.table-btn').forEach(btn => {
494
+ btn.classList.remove('border-cafeGold', 'bg-cafeGold/10', 'text-cafeGold');
495
+ btn.classList.add('border-cafeBorder', 'bg-cafeDark', 'text-zinc-300');
496
+ });
 
 
 
 
 
497
 
498
+ const confirmBtn = document.getElementById('confirmTableBtn');
499
+ confirmBtn.disabled = true;
500
+ confirmBtn.classList.add('bg-zinc-800', 'text-zinc-500', 'cursor-not-allowed');
501
+ confirmBtn.classList.remove('bg-cafeGold', 'text-zinc-950', 'hover:bg-cafeGoldHover');
502
+
503
+ screen2.classList.add('opacity-0', 'translate-y-4');
504
  setTimeout(() => {
505
+ screen2.classList.add('hidden');
506
+ screen1.classList.remove('hidden');
507
+ setTimeout(() => {
508
+ screen1.classList.remove('opacity-0', 'scale-95');
509
+ }, 50);
510
+ }, 300);
511
+ }
512
  }
513
  </script>
514
  </body>