Opera8 commited on
Commit
01098f1
·
verified ·
1 Parent(s): 30181ae

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +176 -382
app.py CHANGED
@@ -5,12 +5,42 @@ import uvicorn
5
  from fastapi import FastAPI, Form, File, UploadFile, HTTPException
6
  from fastapi.responses import HTMLResponse, JSONResponse
7
  from playwright.async_api import async_playwright
 
8
 
9
- app = FastAPI()
10
  UPLOAD_DIR = "temp_uploads"
11
  os.makedirs(UPLOAD_DIR, exist_ok=True)
12
 
13
- # لایه فرانت‌اند چت‌باکس با استایل شیشه‌ای (Glassmorphism)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  HTML_CHAT_INTERFACE = """
15
  <!DOCTYPE html>
16
  <html lang="fa" dir="rtl">
@@ -31,217 +61,48 @@ HTML_CHAT_INTERFACE = """
31
 
32
  body {
33
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Tahoma, sans-serif;
34
- margin: 0;
35
- padding: 0;
36
- background: var(--bg-gradient);
37
- color: var(--text-main);
38
- height: 100vh;
39
- display: flex;
40
- align-items: center;
41
- justify-content: center;
42
- overflow: hidden;
43
  }
44
 
45
  .chat-container {
46
- width: 100%;
47
- max-width: 850px;
48
- height: 85vh;
49
- background: var(--glass-bg);
50
- backdrop-filter: blur(16px);
51
- -webkit-backdrop-filter: blur(16px);
52
- border: 1px solid var(--glass-border);
53
- border-radius: 24px;
54
- display: flex;
55
- flex-direction: column;
56
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
57
- margin: 20px;
58
- }
59
-
60
- .chat-header {
61
- padding: 20px 24px;
62
- border-bottom: 1px solid var(--glass-border);
63
- display: flex;
64
- align-items: center;
65
- gap: 12px;
66
- }
67
-
68
- .chat-header .status-dot {
69
- width: 10px;
70
- height: 10px;
71
- background: #4ade80;
72
- border-radius: 50%;
73
- box-shadow: 0 0 10px #4ade80;
74
- }
75
-
76
- .chat-header h2 {
77
- margin: 0;
78
- font-size: 18px;
79
- font-weight: 600;
80
- color: var(--accent-color);
81
- }
82
-
83
- .messages-box {
84
- flex: 1;
85
- padding: 24px;
86
- overflow-y: auto;
87
- display: flex;
88
- flex-direction: column;
89
- gap: 16px;
90
  }
91
 
 
 
 
 
92
  .messages-box::-webkit-scrollbar { width: 6px; }
93
  .messages-box::-webkit-scrollbar-track { background: transparent; }
94
  .messages-box::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 10px; }
95
 
96
- .message {
97
- max-width: 75%;
98
- padding: 14px 18px;
99
- border-radius: 18px;
100
- font-size: 15px;
101
- line-height: 1.6;
102
- word-wrap: break-word;
103
- animation: fadeIn 0.3s ease forwards;
104
- }
105
-
106
- .message.user {
107
- background: var(--user-bubble);
108
- align-self: flex-start;
109
- border-bottom-right-radius: 4px;
110
- box-shadow: 0 4px 12px rgba(2, 132, 199, 0.2);
111
- }
112
-
113
- .message.ai {
114
- background: var(--ai-bubble);
115
- border: 1px solid var(--glass-border);
116
- align-self: flex-end;
117
- border-bottom-left-radius: 4px;
118
- white-space: pre-wrap;
119
- }
120
-
121
- .input-wrapper {
122
- border-top: 1px solid var(--glass-border);
123
- background: rgba(15, 23, 42, 0.4);
124
- border-bottom-left-radius: 24px;
125
- border-bottom-right-radius: 24px;
126
- padding: 15px 24px;
127
- display: flex;
128
- flex-direction: column;
129
- gap: 10px;
130
- }
131
-
132
- .preview-container {
133
- display: none;
134
- align-items: center;
135
- gap: 10px;
136
- background: rgba(255, 255, 255, 0.05);
137
- padding: 8px 12px;
138
- border-radius: 10px;
139
- width: fit-content;
140
- border: 1px solid var(--glass-border);
141
- }
142
-
143
- .preview-container img {
144
- width: 40px;
145
- height: 40px;
146
- object-fit: cover;
147
- border-radius: 6px;
148
- }
149
-
150
- .preview-container .remove-btn {
151
- color: #ef4444;
152
- cursor: pointer;
153
- font-weight: bold;
154
- font-size: 14px;
155
- }
156
-
157
- .input-area {
158
- display: flex;
159
- gap: 12px;
160
- align-items: center;
161
- }
162
-
163
- .input-area input[type="text"] {
164
- flex: 1;
165
- background: rgba(255, 255, 255, 0.05);
166
- border: 1px solid var(--glass-border);
167
- padding: 14px 20px;
168
- border-radius: 14px;
169
- color: white;
170
- font-size: 15px;
171
- outline: none;
172
- transition: all 0.2s;
173
- }
174
-
175
- .input-area input[type="text"]:focus {
176
- border-color: var(--accent-color);
177
- background: rgba(255, 255, 255, 0.08);
178
- box-shadow: 0 0 12px rgba(56, 189, 248, 0.15);
179
- }
180
-
181
- .upload-label {
182
- background: rgba(255, 255, 255, 0.05);
183
- border: 1px solid var(--glass-border);
184
- padding: 12px;
185
- border-radius: 14px;
186
- cursor: pointer;
187
- display: flex;
188
- align-items: center;
189
- justify-content: center;
190
- transition: all 0.2s;
191
- }
192
-
193
- .upload-label:hover {
194
- background: rgba(255, 255, 255, 0.1);
195
- border-color: var(--accent-color);
196
- }
197
-
198
- .upload-label svg {
199
- width: 22px;
200
- height: 22px;
201
- fill: var(--text-main);
202
- }
203
-
204
- .input-area button {
205
- background: var(--accent-color);
206
- color: #0f172a;
207
- border: none;
208
- padding: 14px 24px;
209
- border-radius: 14px;
210
- font-size: 15px;
211
- font-weight: bold;
212
- cursor: pointer;
213
- transition: all 0.2s;
214
- }
215
 
216
- .input-area button:hover {
217
- background: #7dd3fc;
218
- transform: translateY(-1px);
219
- }
220
 
221
- .input-area button:disabled {
222
- background: #64748b;
223
- color: #1e293b;
224
- cursor: not-allowed;
225
- transform: none;
226
- }
 
227
 
228
- .typing-indicator {
229
- display: flex;
230
- gap: 5px;
231
- padding: 12px 18px;
232
- background: var(--ai-bubble);
233
- border-radius: 12px;
234
- align-self: flex-end;
235
- border: 1px solid var(--glass-border);
236
- }
237
 
238
- .typing-indicator span {
239
- width: 8px;
240
- height: 8px;
241
- background: var(--accent-color);
242
- border-radius: 50%;
243
- animation: bounce 1.4s infinite ease-in-out both;
244
- }
245
  .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
246
  .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
247
 
@@ -286,27 +147,18 @@ HTML_CHAT_INTERFACE = """
286
  const previewContainer = document.getElementById('previewContainer');
287
  const imagePreview = document.getElementById('imagePreview');
288
 
289
- function handleKeyPress(e) {
290
- if (e.key === 'Enter') sendMessage();
291
- }
292
 
293
  function previewImage(event) {
294
  const file = event.target.files[0];
295
  if (file) {
296
  const reader = new FileReader();
297
- reader.onload = function(e) {
298
- imagePreview.src = e.target.result;
299
- previewContainer.style.display = 'flex';
300
- }
301
  reader.readAsDataURL(file);
302
  }
303
  }
304
 
305
- function removeSelectedImage() {
306
- fileInput.value = '';
307
- previewContainer.style.display = 'none';
308
- imagePreview.src = '';
309
- }
310
 
311
  async function sendMessage() {
312
  const text = userInput.value.trim();
@@ -327,7 +179,6 @@ HTML_CHAT_INTERFACE = """
327
  try {
328
  const response = await fetch('/api/chat', { method: 'POST', body: formData });
329
  const data = await response.json();
330
-
331
  if (response.ok) {
332
  appendMessage(data.response, 'ai');
333
  } else {
@@ -341,44 +192,24 @@ HTML_CHAT_INTERFACE = """
341
  }
342
 
343
  function appendMessage(text, sender) {
344
- const msgDiv = document.createElement('div');
345
- msgDiv.classList.add('message', sender);
346
- msgDiv.innerText = text;
347
- messagesBox.appendChild(msgDiv);
348
- messagesBox.scrollTop = messagesBox.scrollHeight;
349
  }
350
 
351
  function appendImageMessage(src, sender) {
352
- const msgDiv = document.createElement('div');
353
- msgDiv.classList.add('message', sender);
354
- msgDiv.style.padding = '8px';
355
- const img = document.createElement('img');
356
- img.src = src;
357
- img.style.maxWidth = '200px';
358
- img.style.maxHeight = '200px';
359
- img.style.borderRadius = '12px';
360
- img.style.display = 'block';
361
- msgDiv.appendChild(img);
362
- messagesBox.appendChild(msgDiv);
363
- messagesBox.scrollTop = messagesBox.scrollHeight;
364
  }
365
 
366
  function setLoading(isLoading) {
367
  if (isLoading) {
368
- userInput.disabled = true;
369
- sendBtn.disabled = true;
370
- const indicator = document.createElement('div');
371
- indicator.classList.add('typing-indicator');
372
- indicator.id = 'typingIndicator';
373
- indicator.innerHTML = '<span></span><span></span><span></span>';
374
- messagesBox.appendChild(indicator);
375
- messagesBox.scrollTop = messagesBox.scrollHeight;
376
  } else {
377
- userInput.disabled = false;
378
- sendBtn.disabled = false;
379
- const indicator = document.getElementById('typingIndicator');
380
- if (indicator) indicator.remove();
381
- userInput.focus();
382
  }
383
  }
384
  </script>
@@ -392,8 +223,11 @@ async def chat_interface():
392
 
393
  @app.post("/api/chat")
394
  async def chat_endpoint(message: str = Form(...), image: UploadFile = File(None)):
 
 
 
 
395
  saved_image_path = None
396
-
397
  if image:
398
  try:
399
  saved_image_path = os.path.join(UPLOAD_DIR, f"{int(time.time())}_{image.filename}")
@@ -403,155 +237,115 @@ async def chat_endpoint(message: str = Form(...), image: UploadFile = File(None)
403
  raise HTTPException(status_code=500, detail=f"خطا در ذخیره‌سازی تصویر: {str(e)}")
404
 
405
  url = "https://www.google.com/search?q=سلام&udm=50"
406
- ai_response = ""
407
 
408
- async with async_playwright() as p:
409
- try:
410
- browser = await p.chromium.launch(
411
- headless=True,
412
- args=[
413
- "--no-sandbox",
414
- "--disable-setuid-sandbox",
415
- "--disable-dev-shm-usage",
416
- "--disable-blink-features=AutomationControlled"
417
- ]
418
- )
419
-
420
- context = await browser.new_context(
421
- user_agent="Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
422
- viewport={"width": 412, "height": 915},
423
- locale="fa-IR",
424
- timezone_id="Asia/Tehran"
425
- )
426
-
427
- page = await context.new_page()
428
- # لود اولیه سبک domcontentloaded به جای networkidle جهت افزایش سرعت
429
- await page.goto(url, wait_until="domcontentloaded", timeout=30000)
430
-
431
- # پیدا کردن کادر متن فرم برای آمادگی تزریق
432
  try:
433
- await page.wait_for_selector("textarea, input[type='text']", timeout=4000)
434
- except:
435
- pass
436
-
437
- # پروسه مالتی‌مدال خطا‌پذیر بدون قفل شدن مرورگر
438
- if saved_image_path and os.path.exists(saved_image_path):
439
- uploaded = False
440
- try:
441
- file_input = await page.query_selector("input[type='file']")
442
- if file_input:
443
- await file_input.set_input_files(saved_image_path)
444
- await page.wait_for_timeout(1500) # زمان کوتا�� فقط برای متصل شدن فایل به فرم گوگل
445
- uploaded = True
446
- except Exception as e:
447
- print(f"Direct injection warning: {e}")
448
-
449
- if not uploaded:
450
- try:
451
- elements = await page.query_selector_all("button, div[role='button'], svg")
452
- for el in elements:
453
- try:
454
- aria = (await el.get_attribute("aria-label") or "").lower()
455
- html = (await el.inner_html() or "").lower()
456
- combined = aria + html
457
- if any(k in combined for k in ["+", "plus", "camera", "lens", "دوربین", "تصویر"]):
458
- async with page.expect_file_chooser(timeout=2000) as fc_info:
459
- await el.click(timeout=1500)
460
- file_chooser = await fc_info.value
461
- await file_chooser.set_files(saved_image_path)
462
- await page.wait_for_timeout(1500)
463
- break
464
- except:
465
- continue
466
- except Exception as e:
467
- print(f"UI upload sequence warning: {e}")
468
-
469
- # درج و ارسال نهایی پیام کاربر
470
- input_box = None
471
- candidates = await page.query_selector_all("textarea, input, [contenteditable='true']")
472
- for el in candidates:
473
- try:
474
  placeholder = await el.get_attribute("placeholder") or ""
475
  if any(w in placeholder for w in ["بپرسید", "ask", "پیام", "چطور", "هرچه"]):
476
  input_box = el
477
  break
478
- except:
479
- continue
 
 
 
 
 
 
 
 
 
480
 
481
- if not input_box:
482
- input_box = await page.query_selector("textarea") or await page.query_selector("input[type='text']")
483
-
484
- if input_box:
485
- await input_box.focus()
486
- await input_box.fill(message)
487
- await page.wait_for_timeout(300)
488
- await input_box.press("Enter")
 
 
 
489
 
490
- # 🚀 الگوریتم پایش داینامیک قد کشیدن متن (حذف ۹ ثانیه خواب اجباری)
491
- await page.wait_for_timeout(2500) # مهلت اولیه برای استارت خوردن استریم جواب توسط گوگل
492
-
493
- last_len = 0
494
- stable_count = 0
495
- for _ in range(20): # حداکثر ۱۰ ثانیه تلاش مجدد در گام‌های ۵۰۰ میلی‌ثانیه‌ای
496
- current_text = await page.evaluate("() => document.body.innerText")
497
- current_len = len(current_text or "")
498
-
499
- if current_len == last_len and current_len > 0:
500
- stable_count += 1
501
- if stable_count >= 2: # اگر طول متن در ۲ دوره متوالی ثابت ماند، استریم به پایان رسیده است
502
- break
503
- else:
504
- stable_count = 0
505
- last_len = current_len
506
- await page.wait_for_timeout(500)
507
-
508
  full_text = await page.evaluate("() => document.body.innerText")
509
- await browser.close()
510
-
511
- # 🎯 الگوریتم پاکسازی کدهای اضافی و جداسازی پاسخ خالص هوش مصنوعی
512
- lines = full_text.split('\n')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
513
  cleaned_lines = []
 
514
 
515
- exclude_keywords = [
516
- "حالت هوشواره‌ای", "ویدئوها", "نقشه‌ها", "اخبار", "تصاویر", "خرید کردن", "کتاب‌ها", "مالی",
517
- "All items removed from input context", "ارسال", "هرچه می‌خواهید بپرسید", "بیشتر بدانید",
518
- "اگر امکان دارد آن را به فارسی ایمیل کنید", "پاسخ «حالت هوشواره‌ای» آماده است", "همه"
519
- ]
520
-
521
- for line in lines:
522
- line_str = line.strip()
523
- if not line_str:
524
- continue
525
-
526
- # حذف خط اکو شده‌ی خود کلمه سرچ شده کاربر در ابتدای بدنه
527
- if line_str == message.strip():
528
- continue
529
-
530
- # نقطه قطع سابمیت: رسیدن به دکمه‌های لایک/دیس‌لایک یا کادر کپی‌رایت انتهای چت گوگل
531
- if any(stop_word in line_str for stop_word in ["در پاسخ‌های هوشواره", "هوشواره ممکن است اشتباه", "پاسخ‌های مربوط به افراد"]):
532
- break
533
-
534
- if any(ex in line_str for ex in exclude_keywords):
535
- continue
536
-
537
- cleaned_lines.append(line_str)
538
-
539
- ai_response = "\n".join(cleaned_lines)
540
- if not ai_response:
541
- ai_response = full_text
542
-
543
- except Exception as e:
544
- if 'browser' in locals():
545
- await browser.close()
546
- raise HTTPException(status_code=500, detail=f"خطا در رندر سرور هوش مصنوعی: {str(e)}")
547
- finally:
548
- if saved_image_path and os.path.exists(saved_image_path):
549
- try:
550
- os.remove(saved_image_path)
551
- except:
552
- pass
553
-
554
- return JSONResponse(content={"response": ai_response})
555
 
556
  if __name__ == "__main__":
557
  uvicorn.run(app, host="0.0.0.0", port=7860)
 
5
  from fastapi import FastAPI, Form, File, UploadFile, HTTPException
6
  from fastapi.responses import HTMLResponse, JSONResponse
7
  from playwright.async_api import async_playwright
8
+ from contextlib import asynccontextmanager
9
 
 
10
  UPLOAD_DIR = "temp_uploads"
11
  os.makedirs(UPLOAD_DIR, exist_ok=True)
12
 
13
+ # تعریف متغیرهای سراسری برای زنده نگه داشتن مرورگر در حافظه سرور
14
+ global_browser = None
15
+ pw_instance = None
16
+
17
+ @asynccontextmanager
18
+ async def lifespan(app: FastAPI):
19
+ # گام اول: لود کردن مرورگر کروم فقط برای یک بار در زمان استارت آپ سرور
20
+ global global_browser, pw_instance
21
+ print("⚡ در حال راه‌اندازی مرورگر سراسری کروم...")
22
+ pw_instance = await async_playwright().start()
23
+ global_browser = await pw_instance.chromium.launch(
24
+ headless=True,
25
+ args=[
26
+ "--no-sandbox",
27
+ "--disable-setuid-sandbox",
28
+ "--disable-dev-shm-usage",
29
+ "--disable-blink-features=AutomationControlled"
30
+ ]
31
+ )
32
+ print("🚀 مرورگر سراسری با موفقیت لود شد و آماده خدمت‌رسانی است.")
33
+ yield
34
+ # گام پایانی: بستن شکیل مرورگر در زمان خاموش شدن سرور هاگینگ فیس
35
+ print("🛑 در حال بستن مرورگر سراسری...")
36
+ if global_browser:
37
+ await global_browser.close()
38
+ if pw_instance:
39
+ await pw_instance.stop()
40
+
41
+ # تزریق لایف‌اسپن به هسته FastAPI
42
+ app = FastAPI(lifespan=lifespan)
43
+
44
  HTML_CHAT_INTERFACE = """
45
  <!DOCTYPE html>
46
  <html lang="fa" dir="rtl">
 
61
 
62
  body {
63
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Tahoma, sans-serif;
64
+ margin: 0; padding: 0; background: var(--bg-gradient); color: var(--text-main);
65
+ height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden;
 
 
 
 
 
 
 
66
  }
67
 
68
  .chat-container {
69
+ width: 100%; max-width: 850px; height: 85vh; background: var(--glass-bg);
70
+ backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px);
71
+ border: 1px solid var(--glass-border); border-radius: 24px;
72
+ display: flex; flex-direction: column; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3); margin: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73
  }
74
 
75
+ .chat-header { padding: 20px 24px; border-bottom: 1px solid var(--glass-border); display: flex; align-items: center; gap: 12px; }
76
+ .chat-header .status-dot { width: 10px; height: 10px; background: #4ade80; border-radius: 50%; box-shadow: 0 0 10px #4ade80; }
77
+ .chat-header h2 { margin: 0; font-size: 18px; font-weight: 600; color: var(--accent-color); }
78
+ .messages-box { flex: 1; padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
79
  .messages-box::-webkit-scrollbar { width: 6px; }
80
  .messages-box::-webkit-scrollbar-track { background: transparent; }
81
  .messages-box::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.1); border-radius: 10px; }
82
 
83
+ .message { max-width: 75%; padding: 14px 18px; border-radius: 18px; font-size: 15px; line-height: 1.6; word-wrap: break-word; animation: fadeIn 0.3s ease forwards; }
84
+ .message.user { background: var(--user-bubble); align-self: flex-start; border-bottom-right-radius: 4px; box-shadow: 0 4px 12px rgba(2, 132, 199, 0.2); }
85
+ .message.ai { background: var(--ai-bubble); border: 1px solid var(--glass-border); align-self: flex-end; border-bottom-left-radius: 4px; white-space: pre-wrap; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
+ .input-wrapper { border-top: 1px solid var(--glass-border); background: rgba(15, 23, 42, 0.4); border-bottom-left-radius: 24px; border-bottom-right-radius: 24px; padding: 15px 24px; display: flex; flex-direction: column; gap: 10px; }
88
+ .preview-container { display: none; align-items: center; gap: 10px; background: rgba(255, 255, 255, 0.05); padding: 8px 12px; border-radius: 10px; width: fit-content; border: 1px solid var(--glass-border); }
89
+ .preview-container img { width: 40px; height: 40px; object-fit: cover; border-radius: 6px; }
90
+ .preview-container .remove-btn { color: #ef4444; cursor: pointer; font-weight: bold; font-size: 14px; }
91
 
92
+ .input-area { display: flex; gap: 12px; align-items: center; }
93
+ .input-area input[type="text"] { flex: 1; background: rgba(255, 255, 255, 0.05); border: 1px solid var(--glass-border); padding: 14px 20px; border-radius: 14px; color: white; font-size: 15px; outline: none; transition: all 0.2s; }
94
+ .input-area input[type="text"]:focus { border-color: var(--accent-color); background: rgba(255, 255, 255, 0.08); box-shadow: 0 0 12px rgba(56, 189, 248, 0.15); }
95
+
96
+ .upload-label { background: rgba(255, 255, 255, 0.05); border: 1px solid var(--glass-border); padding: 12px; border-radius: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.2s; }
97
+ .upload-label:hover { background: rgba(255, 255, 255, 0.1); border-color: var(--accent-color); }
98
+ .upload-label svg { width: 22px; height: 22px; fill: var(--text-main); }
99
 
100
+ .input-area button { background: var(--accent-color); color: #0f172a; border: none; padding: 14px 24px; border-radius: 14px; font-size: 15px; font-weight: bold; cursor: pointer; transition: all 0.2s; }
101
+ .input-area button:hover { background: #7dd3fc; transform: translateY(-1px); }
102
+ .input-area button:disabled { background: #64748b; color: #1e293b; cursor: not-allowed; transform: none; }
 
 
 
 
 
 
103
 
104
+ .typing-indicator { display: flex; gap: 5px; padding: 12px 18px; background: var(--ai-bubble); border-radius: 12px; align-self: flex-end; border: 1px solid var(--glass-border); }
105
+ .typing-indicator span { width: 8px; height: 8px; background: var(--accent-color); border-radius: 50%; animation: bounce 1.4s infinite ease-in-out both; }
 
 
 
 
 
106
  .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
107
  .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
108
 
 
147
  const previewContainer = document.getElementById('previewContainer');
148
  const imagePreview = document.getElementById('imagePreview');
149
 
150
+ function handleKeyPress(e) { if (e.key === 'Enter') sendMessage(); }
 
 
151
 
152
  function previewImage(event) {
153
  const file = event.target.files[0];
154
  if (file) {
155
  const reader = new FileReader();
156
+ reader.onload = function(e) { imagePreview.src = e.target.result; previewContainer.style.display = 'flex'; }
 
 
 
157
  reader.readAsDataURL(file);
158
  }
159
  }
160
 
161
+ function removeSelectedImage() { fileInput.value = ''; previewContainer.style.display = 'none'; imagePreview.src = ''; }
 
 
 
 
162
 
163
  async function sendMessage() {
164
  const text = userInput.value.trim();
 
179
  try {
180
  const response = await fetch('/api/chat', { method: 'POST', body: formData });
181
  const data = await response.json();
 
182
  if (response.ok) {
183
  appendMessage(data.response, 'ai');
184
  } else {
 
192
  }
193
 
194
  function appendMessage(text, sender) {
195
+ const msgDiv = document.createElement('div'); msgDiv.classList.add('message', sender);
196
+ msgDiv.innerText = text; messagesBox.appendChild(msgDiv); messagesBox.scrollTop = messagesBox.scrollHeight;
 
 
 
197
  }
198
 
199
  function appendImageMessage(src, sender) {
200
+ const msgDiv = document.createElement('div'); msgDiv.classList.add('message', sender); msgDiv.style.padding = '8px';
201
+ const img = document.createElement('img'); img.src = src; img.style.maxWidth = '200px'; img.style.maxHeight = '200px'; img.style.borderRadius = '12px'; img.style.display = 'block';
202
+ msgDiv.appendChild(img); messagesBox.appendChild(msgDiv); messagesBox.scrollTop = messagesBox.scrollHeight;
 
 
 
 
 
 
 
 
 
203
  }
204
 
205
  function setLoading(isLoading) {
206
  if (isLoading) {
207
+ userInput.disabled = true; sendBtn.disabled = true;
208
+ const indicator = document.createElement('div'); indicator.classList.add('typing-indicator'); indicator.id = 'typingIndicator';
209
+ indicator.innerHTML = '<span></span><span></span><span></span>'; messagesBox.appendChild(indicator); messagesBox.scrollTop = messagesBox.scrollHeight;
 
 
 
 
 
210
  } else {
211
+ userInput.disabled = false; sendBtn.disabled = false;
212
+ const indicator = document.getElementById('typingIndicator'); if (indicator) indicator.remove(); userInput.focus();
 
 
 
213
  }
214
  }
215
  </script>
 
223
 
224
  @app.post("/api/chat")
225
  async def chat_endpoint(message: str = Form(...), image: UploadFile = File(None)):
226
+ global global_browser
227
+ if not global_browser:
228
+ raise HTTPException(status_code=500, detail="وب سرور کروم هنوز آماده نشده است.")
229
+
230
  saved_image_path = None
 
231
  if image:
232
  try:
233
  saved_image_path = os.path.join(UPLOAD_DIR, f"{int(time.time())}_{image.filename}")
 
237
  raise HTTPException(status_code=500, detail=f"خطا در ذخیره‌سازی تصویر: {str(e)}")
238
 
239
  url = "https://www.google.com/search?q=سلام&udm=50"
240
+ full_text = ""
241
 
242
+ # ساخت یک Context و Page جدید در کسر از ثانیه روی مرورگر زنده سراسری
243
+ context = await global_browser.new_context(
244
+ user_agent="Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Mobile Safari/537.36",
245
+ viewport={"width": 412, "height": 915},
246
+ locale="fa-IR", timezone_id="Asia/Tehran"
247
+ )
248
+
249
+ page = await context.new_page()
250
+
251
+ try:
252
+ # لود اولیه با سرعت بالا
253
+ await page.goto(url, wait_until="commit", timeout=30000)
254
+ await page.wait_for_timeout(500)
255
+
256
+ # فرآیند مستقل و خطا ناپذیر آپلود تصویر مستقیم به لایه Input گوگل
257
+ if saved_image_path and os.path.exists(saved_image_path):
 
 
 
 
 
 
 
 
258
  try:
259
+ file_input = await page.query_selector("input[type='file']")
260
+ if file_input:
261
+ await file_input.set_input_files(saved_image_path)
262
+ await page.wait_for_timeout(1000) # زمان کوتاه ۱ ثانیه‌ای برای کش شدن فایل در مرورگر
263
+ except Exception as e:
264
+ print(f"Bypassed image file injection: {e}")
265
+
266
+ # پیدا کردن فیلد متنی و تایپ کوئری جدید
267
+ input_box = None
268
+ candidates = await page.query_selector_all("textarea, input, [contenteditable='true']")
269
+ for el in candidates:
270
+ try:
271
+ if await el.is_visible():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  placeholder = await el.get_attribute("placeholder") or ""
273
  if any(w in placeholder for w in ["بپرسید", "ask", "پیام", "چطور", "هرچه"]):
274
  input_box = el
275
  break
276
+ except:
277
+ continue
278
+
279
+ if not input_box:
280
+ input_box = await page.query_selector("textarea") or await page.query_selector("input[type='text']")
281
+
282
+ if input_box:
283
+ await input_box.focus()
284
+ await input_box.fill(message)
285
+ await page.wait_for_timeout(200)
286
+ await input_box.press("Enter")
287
 
288
+ # کلیک ضربتی روی دکمه ارسال در صورت عدم فعال شدن اینتر
289
+ try:
290
+ send_buttons = await page.query_selector_all("button, [role='button']")
291
+ for btn in send_buttons:
292
+ if await btn.is_visible():
293
+ aria = (await btn.get_attribute("aria-label") or "").lower()
294
+ if any(w in aria for w in ["send", "ارسال", "arrow", "up"]):
295
+ await btn.click(timeout=1000)
296
+ break
297
+ except:
298
+ pass
299
 
300
+ # 🪐 مانیتورینگ هوشمند (Smart Pooling Loop): چک کردن هر ۴۰۰ میلی‌ثانیه برای دریافت در لحظه پاسخ
301
+ for _ in range(30): # حداکثر ۱۲ ثانیه نگه می‌دارد اما معمولاً در ۲ الی ۳ ثانیه اول پاسخ کامل می‌شود
302
+ await page.wait_for_timeout(400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
303
  full_text = await page.evaluate("() => document.body.innerText")
304
+ if "هوشواره ممکن است اشتباه" in full_text or "بیشتر بدانید" in full_text or "پاسخ‌های مربوط به افراد" in full_text:
305
+ break
306
+
307
+ except Exception as e:
308
+ await context.close()
309
+ raise HTTPException(status_code=500, detail=f"خطا در رندر سرور هوش مصنوعی: {str(e)}")
310
+ finally:
311
+ await context.close() # بستن فوری پیج و کانتکست جهت آزاد سازی رم
312
+ if saved_image_path and os.path.exists(saved_image_path):
313
+ try: os.remove(saved_image_path)
314
+ except: pass
315
+
316
+ # 🧼 پورت تصفیه متن (Text Purifying Algorithm)
317
+ lines = full_text.split('\n')
318
+ cleaned_lines = []
319
+
320
+ # واژه‌های ممنوعه لایه‌های کاربری سرچ گوگل که باید کلاً نادیده گرفته شوند
321
+ global_junk = ["تصاویر", "ویدیوها", "اخبار", "نقشه‌ها", "خرید کردن", "کتاب‌ها", "مالی", "حالت موضوع‌محور", "ورود", "همه", "ویدئوها", "ارسال", "پاسخ «حالت هوشواره‌ای» آماده است", "All items removed"]
322
+
323
+ for line in lines:
324
+ line_str = line.strip()
325
+ if not line_str:
326
+ continue
327
+ # حذف خطوط ابزارها
328
+ if any(junk == line_str for junk in global_junk):
329
+ continue
330
+ # به محض رسیدن به کادر هشدار سلب مسئولیت گوگل، فوتور را قطع کن
331
+ if "هوشواره ممکن است اشتباه" in line_str or "در پاسخ‌های" in line_str or "بیشتر بدانید" in line_str:
332
+ break
333
+
334
+ # ترفند طلایی: اگر خط جاری دقیقاً برابر با پیام ارسالی کاربر بود، تمام زباله‌های بالای صفحه را تخلیه کن
335
+ if line_str == message:
336
  cleaned_lines = []
337
+ continue
338
 
339
+ cleaned_lines.append(line_str)
340
+
341
+ # بازسازی نهایی متن
342
+ final_response = "\n".join(cleaned_lines).strip()
343
+
344
+ # لایه پشتیبان در صورت عدم تطابق متون
345
+ if not final_response:
346
+ final_response = "پاسخی دریافت نشد یا ساختار خروجی تغییر کرده است."
347
+
348
+ return JSONResponse(content={"response": final_response})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
 
350
  if __name__ == "__main__":
351
  uvicorn.run(app, host="0.0.0.0", port=7860)