KennyOry commited on
Commit
601e79d
·
verified ·
1 Parent(s): 2678c19

Update templates/index.html

Browse files
Files changed (1) hide show
  1. templates/index.html +934 -363
templates/index.html CHANGED
@@ -3,506 +3,1077 @@
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>PrintMaster Assistant</title>
7
- <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
 
8
  <style>
9
- body {
10
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
11
- background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
 
 
 
 
 
 
 
 
 
 
 
12
  margin: 0;
13
- padding: 20px;
14
- color: #333;
 
 
 
 
 
 
15
  min-height: 100vh;
 
 
 
16
  }
17
 
18
- .container {
19
- max-width: 1000px;
20
  margin: 0 auto;
21
- background: white;
22
- border-radius: 15px;
23
- box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
24
- overflow: hidden;
25
  }
26
 
27
- header {
28
- background: linear-gradient(90deg, #2c3e50 0%, #1a2530 100%);
29
- color: white;
30
- padding: 20px;
31
- text-align: center;
32
- position: relative;
 
 
33
  }
34
 
35
- h1 {
36
- margin: 0;
37
- font-size: 28px;
38
- font-weight: 600;
39
  }
40
 
41
- .tagline {
42
- margin-top: 5px;
43
- opacity: 0.8;
44
- font-size: 16px;
 
 
 
 
45
  }
46
 
47
- .main-content {
48
- display: flex;
49
- min-height: 70vh;
50
  }
51
 
52
  .chat-container {
53
- flex: 3;
54
- padding: 20px;
55
- border-right: 1px solid #eee;
56
  display: flex;
57
  flex-direction: column;
 
 
58
  }
59
 
60
- .chat-box {
61
- flex: 1;
62
- overflow-y: auto;
63
- padding: 15px;
64
- background: #fafafa;
65
- border-radius: 10px;
66
- margin-bottom: 15px;
67
- border: 1px solid #eee;
68
- min-height: 400px;
69
- max-height: 60vh;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  }
71
 
72
  .input-area {
 
 
 
 
 
 
 
 
 
 
 
 
73
  display: flex;
74
  gap: 10px;
75
  }
76
 
77
  #user-input {
78
  flex: 1;
79
- padding: 12px 15px;
80
- border: 1px solid #ddd;
81
- border-radius: 25px;
82
  font-size: 16px;
83
- outline: none;
84
- transition: border 0.3s;
85
  }
86
 
87
  #user-input:focus {
88
- border-color: #3498db;
89
- box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
 
90
  }
91
 
92
- button {
93
- background: #3498db;
94
  color: white;
95
  border: none;
96
- padding: 12px 20px;
97
- border-radius: 25px;
98
- cursor: pointer;
99
- font-size: 16px;
100
- transition: all 0.3s;
 
 
 
101
  }
102
 
103
- button:hover {
104
- background: #2980b9;
105
- transform: translateY(-2px);
106
  }
107
 
108
- button:active {
109
- transform: translateY(0);
110
  }
111
 
112
- .controls {
113
  display: flex;
114
- gap: 10px;
115
- margin-top: 10px;
 
116
  }
117
 
118
- .controls button {
119
- flex: 1;
120
- background: #7f8c8d;
 
121
  }
122
 
123
- .controls button#new-dialog {
124
- background: #e74c3c;
 
 
 
 
125
  }
126
 
127
- .controls button#repeat {
128
- background: #f39c12;
 
 
 
 
129
  }
130
 
131
- .info-panel {
132
- flex: 1;
133
- padding: 20px;
134
- background: #f8f9fa;
135
- display: flex;
136
- flex-direction: column;
137
  }
138
 
139
- .status-box {
140
- flex: 1;
141
- background: white;
142
- border-radius: 10px;
143
- padding: 15px;
144
- border: 1px solid #eee;
145
- overflow-y: auto;
146
- margin-bottom: 15px;
147
  }
148
 
149
- .sources-box {
150
- background: white;
151
- border-radius: 10px;
152
- padding: 15px;
153
- border: 1px solid #eee;
154
- max-height: 200px;
155
- overflow-y: auto;
156
  }
157
 
158
- .message {
159
- margin-bottom: 15px;
160
- padding: 12px 15px;
161
- border-radius: 10px;
162
- animation: fadeIn 0.3s;
 
 
 
 
 
 
 
 
 
 
163
  }
164
 
165
- .user-message {
166
- background: #e3f2fd;
167
- margin-left: 40px;
168
- border-bottom-right-radius: 0;
169
  }
170
 
171
- .assistant-message {
172
- background: #f0f7ff;
173
- margin-right: 40px;
174
- border-bottom-left-radius: 0;
 
 
175
  }
176
 
177
- .system-message {
178
- background: #e8f5e9;
179
- text-align: center;
180
- padding: 8px;
 
 
 
 
181
  font-size: 14px;
182
- margin: 10px 0;
 
 
183
  }
184
 
185
- .log-message {
186
- background: #fff8e1;
187
- padding: 8px 12px;
188
- border-radius: 5px;
189
- margin: 5px 0;
190
- font-size: 14px;
191
- border-left: 3px solid #ffc107;
192
  }
193
 
194
- .message-header {
195
- font-weight: bold;
196
- margin-bottom: 5px;
197
- display: flex;
198
  align-items: center;
 
 
 
 
 
 
199
  }
200
 
201
- .message-header .role {
202
- color: #2c3e50;
 
203
  }
204
 
205
- .message-header .time {
206
- margin-left: auto;
207
- font-size: 12px;
208
- color: #7f8c8d;
 
209
  }
210
 
211
- .typing-indicator {
212
- display: inline-flex;
213
- align-items: center;
214
- padding: 8px 15px;
215
- background: #f0f7ff;
216
- border-radius: 18px;
217
- font-size: 14px;
218
  }
219
 
220
- .typing-dots {
221
  display: flex;
222
- margin-left: 8px;
 
 
 
 
223
  }
224
 
225
- .typing-dot {
226
- width: 6px;
227
- height: 6px;
228
- background: #3498db;
229
- border-radius: 50%;
230
- margin: 0 2px;
231
- animation: pulse 1.5s infinite;
 
 
232
  }
233
 
234
- .typing-dot:nth-child(2) {
235
- animation-delay: 0.5s;
 
 
236
  }
237
 
238
- .typing-dot:nth-child(3) {
239
- animation-delay: 1s;
240
  }
241
 
242
- @keyframes pulse {
243
- 0%, 100% { opacity: 0.4; transform: scale(0.8); }
244
- 50% { opacity: 1; transform: scale(1); }
 
245
  }
246
 
247
- @keyframes fadeIn {
248
- from { opacity: 0; transform: translateY(10px); }
249
- to { opacity: 1; transform: translateY(0); }
 
 
 
 
250
  }
251
 
252
- .problem {
253
- color: #e74c3c;
254
- font-weight: bold;
 
 
 
 
 
255
  }
256
 
257
- .solution {
258
- color: #27ae60;
 
 
259
  }
260
 
261
- .sources {
262
- color: #3498db;
 
 
263
  }
264
 
265
- .source-item {
266
- margin: 8px 0;
267
- padding: 8px;
268
- background: #f8f9fa;
269
- border-radius: 5px;
 
 
270
  font-size: 14px;
 
271
  }
272
 
273
- .source-title {
274
- font-weight: bold;
 
275
  }
276
 
277
- .source-url {
278
- color: #7f8c8d;
279
- font-size: 12px;
280
- word-break: break-all;
 
281
  }
282
 
283
- .error {
284
- color: #c0392b;
285
- background: #fadbd8;
286
- padding: 10px;
287
- border-radius: 5px;
288
- margin: 10px 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
  }
290
  </style>
291
  </head>
292
  <body>
293
- <div class="container">
294
- <header>
295
- <h1>PrintMaster Assistant</h1>
296
- <div class="tagline">Ваш эксперт по ремонту печатной техники</div>
297
- </header>
298
-
299
- <div class="main-content">
300
- <div class="chat-container">
301
- <div class="chat-box" id="chatbox">
302
- <!-- Сообщения будут добавляться здесь -->
303
- </div>
304
-
305
- <div class="input-area">
306
- <input type="text" id="user-input" placeholder="Опишите проблему с принтером..." autocomplete="off">
307
- <button id="send-btn">Отправить</button>
308
  </div>
309
-
310
- <div class="controls">
311
- <button id="repeat">Повторить ответ</button>
312
- <button id="new-dialog">🆕 Новый диалог</button>
313
  </div>
314
  </div>
315
-
316
- <div class="info-panel">
317
- <h3>Ход выполнения</h3>
318
- <div class="status-box" id="status-box">
319
- <!-- Логи процесса будут добавляться здесь -->
 
 
 
 
 
 
 
320
  </div>
321
-
322
- <h3>Источники информации</h3>
323
- <div class="sources-box" id="sources-box">
324
- <!-- Источники будут добавляться здесь -->
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  </div>
326
  </div>
327
  </div>
328
  </div>
329
 
330
  <script>
331
- $(document).ready(function() {
332
- const chatbox = $('#chatbox');
333
- const statusBox = $('#status-box');
334
- const sourcesBox = $('#sources-box');
335
- const userInput = $('#user-input');
336
- const sendBtn = $('#send-btn');
337
- const repeatBtn = $('#repeat');
338
- const newDialogBtn = $('#new-dialog');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
339
 
340
- let isProcessing = false;
 
341
 
342
- // Функция добавления сообщения в чат
343
- function addMessage(role, content, type = '') {
344
- const timestamp = new Date().toLocaleTimeString();
345
- const messageClass = type ? type : role + '-message';
346
-
347
- let formattedContent = content;
348
 
349
- // Форматирование ответа ассистента
350
- if (role === 'assistant') {
351
- formattedContent = formattedContent
352
- .replace(/\*\*Проблема:\*\*/g, '<div class="problem">Проблема:</div>')
353
- .replace(/\*\*Решение:\*\*/g, '<div class="solution">Решение:</div>')
354
- .replace(/\*\*Примечания:\*\*/g, '<div class="notes">Примечания:</div>')
355
- .replace(/\*\*Источники:\*\*/g, '<div class="sources">Источники информации:</div>')
356
- .replace(/(\d+\.\s.+)/g, '<div class="step">$1</div>')
357
- .replace(/(-\s.+)/g, '<div class="substep">$1</div>');
358
- }
359
-
360
- const messageElement = `
361
- <div class="message ${messageClass}">
362
- <div class="message-header">
363
- <span class="role">${role === 'user' ? 'Вы' : 'PrintMaster'}</span>
364
- <span class="time">${timestamp}</span>
365
- </div>
366
- <div class="message-content">${formattedContent}</div>
367
- </div>
368
- `;
369
 
370
- chatbox.append(messageElement);
371
- chatbox.scrollTop(chatbox[0].scrollHeight);
372
- }
373
 
374
- // Функция добавления лога
375
- function addLog(content) {
376
- const logElement = `<div class="log-message">${content}</div>`;
377
- statusBox.append(logElement);
378
- statusBox.scrollTop(statusBox[0].scrollHeight);
379
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
- // Функция отображения индикатора набора
382
- function showTypingIndicator() {
383
- const indicator = `
384
- <div class="message assistant-message">
385
- <div class="message-header">
386
- <span class="role">PrintMaster</span>
387
- </div>
388
- <div class="typing-indicator">
389
- Генерация ответа
390
- <div class="typing-dots">
391
- <div class="typing-dot"></div>
392
- <div class="typing-dot"></div>
393
- <div class="typing-dot"></div>
394
- </div>
395
- </div>
396
  </div>
397
- `;
398
- chatbox.append(indicator);
399
- chatbox.scrollTop(chatbox[0].scrollHeight);
400
- return indicator;
401
- }
 
 
 
 
 
 
 
 
 
 
 
402
 
403
- // Функция отправки сообщения
404
- function sendMessage() {
405
- const message = userInput.val().trim();
406
- if (message === '') return;
407
- if (isProcessing) return;
408
-
409
- userInput.val('');
410
- addMessage('user', message);
411
-
412
- // Показываем индикатор набора
413
- const indicator = showTypingIndicator();
414
- isProcessing = true;
415
-
416
- // Отправляем запрос на сервер
417
- $.post('/ask', {message: message}, function(response) {
418
- if (response.status !== 'processing') {
419
- console.error('Ошибка обработки запроса');
420
- isProcessing = false;
421
- }
422
- });
423
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
424
 
425
- // Обработчик отправки сообщения
426
- sendBtn.click(sendMessage);
427
- userInput.keypress(function(e) {
428
- if (e.which === 13) {
429
- sendMessage();
430
  }
431
  });
432
-
433
- // Обработчик кнопки "Повторить"
434
- repeatBtn.click(function() {
435
- if (isProcessing) return;
436
- isProcessing = true;
437
- statusBox.empty();
438
- sourcesBox.empty();
439
- showTypingIndicator();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
440
 
441
- $.post('/repeat', function(response) {
442
- if (response.status !== 'repeating') {
443
- console.error('Ошибка повтора');
444
- isProcessing = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
445
  }
446
- });
447
- });
448
-
449
- // Обработчик кнопки "Новый диалог"
450
- newDialogBtn.click(function() {
451
- if (!confirm('Начать новый диалог? Текущая история будет очищена.')) return;
452
-
453
- $.post('/new', function() {
454
- chatbox.empty();
455
- statusBox.empty();
456
- sourcesBox.empty();
457
- addMessage('system', 'Диалог начат заново. Опишите вашу проблему с принтером.', 'system-message');
458
- });
459
- });
 
 
 
 
 
 
 
 
 
 
 
 
460
 
461
- // Инициализация SSE
462
- const eventSource = new EventSource('/stream');
 
 
 
 
 
 
 
 
 
 
 
 
463
 
464
- eventSource.onmessage = function(event) {
465
- const data = JSON.parse(event.data);
466
-
467
- switch(data.type) {
468
- case 'log':
469
- addLog(data.content);
470
- break;
471
-
472
- case 'response':
473
- // Удаляем индикатор набора
474
- chatbox.find('.typing-indicator').closest('.message').remove();
475
- addMessage('assistant', data.content);
476
- isProcessing = false;
477
- break;
478
-
479
- case 'sources':
480
- sourcesBox.empty();
481
- try {
482
- const sources = JSON.parse(data.content);
483
- sources.forEach((source, index) => {
484
- const sourceElement = `
485
- <div class="source-item">
486
- <div class="source-title">${index + 1}. ${source.title}</div>
487
- <div class="source-url"><a href="${source.url}" target="_blank">${source.url}</a></div>
488
- </div>
489
- `;
490
- sourcesBox.append(sourceElement);
491
- });
492
- } catch (e) {
493
- sourcesBox.html('<div class="error">Ошибка загрузки источников</div>');
494
- }
495
- break;
496
-
497
- case 'done':
498
- isProcessing = false;
499
- break;
500
  }
501
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
502
 
503
- // Инициализируем приветственное сообщение
504
- addMessage('system', 'Здравствуйте! Я PrintMaster, ваш помощник по ремонту принтеров. Опишите вашу проблему, например: "Canon iR ADV C5540i выдает ошибку E000025-04-00".', 'system-message');
 
 
 
 
 
505
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  </script>
507
  </body>
508
  </html>
 
3
  <head>
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>EngiPrintAI | Сервисный ассистент</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
  <style>
10
+ :root {
11
+ --primary: #4361ee;
12
+ --primary-light: #4895ef;
13
+ --secondary: #3f37c9;
14
+ --dark: #0e1424;
15
+ --light: #f8f9fa;
16
+ --gray: #e9ecef;
17
+ --success: #2ecc71;
18
+ --warning: #f39c12;
19
+ --danger: #e74c3c;
20
+ --transition: all 0.3s ease;
21
+ }
22
+
23
+ * {
24
  margin: 0;
25
+ padding: 0;
26
+ box-sizing: border-box;
27
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
28
+ }
29
+
30
+ body {
31
+ background: linear-gradient(135deg, #f5f7fa 0%, #e4e7f1 100%);
32
+ color: var(--dark);
33
  min-height: 100vh;
34
+ padding: 20px;
35
+
36
+
37
  }
38
 
39
+ .app-container {
40
+ max-width: 1200px;
41
  margin: 0 auto;
42
+ display: grid;
43
+ grid-template-columns: 1fr 300px;
44
+ gap: 20px;
 
45
  }
46
 
47
+ .card {
48
+ background: rgba(255, 255, 255, 0.92);
49
+ border-radius: 16px;
50
+ border: none;
51
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.1);
52
+ backdrop-filter: blur(4px);
53
+ overflow: hidden;
54
+ transition: var(--transition);
55
  }
56
 
57
+ .card:hover {
58
+ box-shadow: 0 12px 40px rgba(31, 38, 135, 0.15);
 
 
59
  }
60
 
61
+ .card-header {
62
+ background: white;
63
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
64
+ padding: 20px;
65
+ font-weight: 600;
66
+ display: flex;
67
+ align-items: center;
68
+ gap: 12px;
69
  }
70
 
71
+ .card-body {
72
+ padding: 0;
 
73
  }
74
 
75
  .chat-container {
76
+ height: calc(100vh - 200px);
 
 
77
  display: flex;
78
  flex-direction: column;
79
+ padding: 20px;
80
+ overflow-y: auto;
81
  }
82
 
83
+ .message {
84
+ max-width: 80%;
85
+ padding: 16px 20px;
86
+ margin-bottom: 16px;
87
+ border-radius: 18px;
88
+ line-height: 1.5;
89
+ position: relative;
90
+ animation: fadeIn 0.3s ease;
91
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
92
+ will-change: transform, opacity;
93
+ }
94
+
95
+ @keyframes fadeIn {
96
+ from { opacity: 0; transform: translateY(10px); }
97
+ to { opacity: 1; transform: translateY(0); }
98
+
99
+ }
100
+
101
+ .user-message {
102
+ background: var(--primary);
103
+ color: white;
104
+ align-self: flex-end;
105
+ border-bottom-right-radius: 4px;
106
+ }
107
+
108
+ .bot-message {
109
+ background: white;
110
+ border: 1px solid var(--gray);
111
+ align-self: flex-start;
112
+ border-bottom-left-radius: 4px;
113
+ }
114
+
115
+ .message-header {
116
+ font-size: 12px;
117
+ opacity: 0.8;
118
+ margin-bottom: 8px;
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 6px;
122
  }
123
 
124
  .input-area {
125
+ padding: 20px;
126
+ border-top: 1px solid rgba(0, 0, 0, 0.05);
127
+ background: white;
128
+
129
+
130
+
131
+
132
+
133
+
134
+ }
135
+
136
+ .input-group {
137
  display: flex;
138
  gap: 10px;
139
  }
140
 
141
  #user-input {
142
  flex: 1;
143
+ border: 1px solid var(--gray);
144
+ border-radius: 12px;
145
+ padding: 14px 20px;
146
  font-size: 16px;
147
+ transition: var(--transition);
148
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.03);
149
  }
150
 
151
  #user-input:focus {
152
+ border-color: var(--primary-light);
153
+ box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15);
154
+ outline: none;
155
  }
156
 
157
+ #send-btn {
158
+ background: var(--primary);
159
  color: white;
160
  border: none;
161
+ border-radius: 12px;
162
+ padding: 0 24px;
163
+ font-weight: 600;
164
+ transition: var(--transition);
165
+ display: flex;
166
+ align-items: center;
167
+ justify-content: center;
168
+ box-shadow: 0 4px 6px rgba(67, 97, 238, 0.2);
169
  }
170
 
171
+ #send-btn:hover {
172
+ background: var(--secondary);
173
+ transform: translateY(-1px);
174
  }
175
 
176
+ #send-btn:active {
177
+ transform: translateY(1px);
178
  }
179
 
180
+ .typing-indicator {
181
  display: flex;
182
+ gap: 6px;
183
+ padding: 20px;
184
+ align-items: center;
185
  }
186
 
187
+ .typing-text {
188
+ margin-left: 10px;
189
+ font-style: italic;
190
+ color: #6c757d;
191
  }
192
 
193
+ .typing-dot {
194
+ width: 10px;
195
+ height: 10px;
196
+ background: var(--primary-light);
197
+ border-radius: 50%;
198
+ animation: bounce 1.4s infinite ease-in-out both;
199
  }
200
 
201
+ .typing-dot:nth-child(1) { animation-delay: -0.32s; }
202
+ .typing-dot:nth-child(2) { animation-delay: -0.16s; }
203
+
204
+ @keyframes bounce {
205
+ 0%, 80%, 100% { transform: scale(0); }
206
+ 40% { transform: scale(1); }
207
  }
208
 
209
+ .log-entry {
210
+ padding: 14px 20px;
211
+ border-bottom: 1px solid rgba(0, 0, 0, 0.03);
212
+ font-size: 14px;
213
+ line-height: 1.5;
214
+ animation: fadeIn 0.3s ease;
215
  }
216
 
217
+ .log-entry:last-child {
218
+ border-bottom: none;
219
+
220
+
221
+
222
+
223
+
224
+
225
  }
226
 
227
+ .log-timestamp {
228
+ font-size: 11px;
229
+ opacity: 0.6;
230
+ margin-right: 8px;
231
+
232
+
233
+
234
  }
235
 
236
+ .log-info { color: var(--primary); }
237
+ .log-success { color: var(--success); }
238
+ .log-warning { color: var(--warning); }
239
+ .log-error { color: var(--danger); }
240
+
241
+ .problem-text {
242
+ font-weight: 600;
243
+ color: var(--primary);
244
+ margin-bottom: 8px;
245
+ }
246
+
247
+ .solution-text {
248
+ font-weight: 600;
249
+ color: var(--secondary);
250
+ margin-bottom: 8px;
251
  }
252
 
253
+ .solution-step {
254
+ padding-left: 20px;
255
+ position: relative;
256
+ margin-bottom: 6px;
257
  }
258
 
259
+ .solution-step:before {
260
+ content: "•";
261
+ position: absolute;
262
+ left: 8px;
263
+ color: var(--primary);
264
+ font-weight: bold;
265
  }
266
 
267
+ .sources-badge {
268
+ display: inline-flex;
269
+ align-items: center;
270
+ gap: 6px;
271
+ background: rgba(67, 97, 238, 0.1);
272
+ color: var(--primary);
273
+ padding: 6px 12px;
274
+ border-radius: 20px;
275
  font-size: 14px;
276
+ margin-top: 15px;
277
+ cursor: pointer;
278
+ transition: var(--transition);
279
  }
280
 
281
+ .sources-badge:hover {
282
+ background: rgba(67, 97, 238, 0.15);
283
+
284
+
285
+
286
+
287
+
288
  }
289
 
290
+ .status-indicator {
291
+ display: inline-flex;
292
+
293
+
294
  align-items: center;
295
+ gap: 6px;
296
+ font-size: 14px;
297
+ padding: 4px 12px;
298
+ border-radius: 20px;
299
+ background: rgba(46, 204, 113, 0.1);
300
+ color: var(--success);
301
  }
302
 
303
+ .status-indicator.offline {
304
+ background: rgba(231, 76, 60, 0.1);
305
+ color: var(--danger);
306
  }
307
 
308
+ .status-dot {
309
+ width: 8px;
310
+ height: 8px;
311
+ border-radius: 50%;
312
+ background: var(--success);
313
  }
314
 
315
+ .status-indicator.offline .status-dot {
316
+ background: var(--danger);
317
+
318
+
319
+
320
+
321
+
322
  }
323
 
324
+ .logo {
325
  display: flex;
326
+ align-items: center;
327
+ gap: 10px;
328
+ font-weight: 700;
329
+ font-size: 20px;
330
+ color: var(--dark);
331
  }
332
 
333
+ .logo-icon {
334
+ width: 36px;
335
+ height: 36px;
336
+ background: var(--primary);
337
+ border-radius: 10px;
338
+ display: flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ color: white;
342
  }
343
 
344
+ .source-item {
345
+ padding: 12px 16px;
346
+ border-bottom: 1px solid rgba(0, 0, 0, 0.05);
347
+ transition: var(--transition);
348
  }
349
 
350
+ .source-item:hover {
351
+ background: rgba(67, 97, 238, 0.03);
352
  }
353
 
354
+ .source-title {
355
+ font-weight: 500;
356
+ margin-bottom: 4px;
357
+ display: block;
358
  }
359
 
360
+ .source-url {
361
+ font-size: 13px;
362
+ color: #6c757d;
363
+ display: block;
364
+ white-space: nowrap;
365
+ overflow: hidden;
366
+ text-overflow: ellipsis;
367
  }
368
 
369
+ .empty-state {
370
+ display: flex;
371
+ flex-direction: column;
372
+ align-items: center;
373
+ justify-content: center;
374
+ text-align: center;
375
+ padding: 40px 20px;
376
+ color: #6c757d;
377
  }
378
 
379
+ .empty-state i {
380
+ font-size: 48px;
381
+ margin-bottom: 20px;
382
+ color: #dee2e6;
383
  }
384
 
385
+ .empty-state h4 {
386
+ font-weight: 500;
387
+ margin-bottom: 10px;
388
+ color: #495057;
389
  }
390
 
391
+ .example-badge {
392
+ display: inline-block;
393
+ background: white;
394
+ border: 1px solid var(--gray);
395
+ border-radius: 8px;
396
+ padding: 8px 16px;
397
+ margin: 6px;
398
  font-size: 14px;
399
+ box-shadow: 0 2px 4px rgba(0,0,0,0.03);
400
  }
401
 
402
+ .log-container {
403
+ height: calc(100vh - 200px);
404
+ overflow-y: auto;
405
  }
406
 
407
+ .highlight {
408
+ background-color: #fff9c4;
409
+ padding: 2px 4px;
410
+ border-radius: 4px;
411
+ font-weight: 600;
412
  }
413
 
414
+ .message-content {
415
+ white-space: pre-wrap;
416
+ word-wrap: break-word;
417
+ line-height: 1.6;
418
+ }
419
+
420
+ /* В разделе стилей */
421
+ .notes-text {
422
+ font-weight: 600;
423
+ color: #f39c12; /* оранжевый для примечаний */
424
+ margin-top: 15px;
425
+ margin-bottom: 8px;
426
+ border-left: 3px solid #f39c12;
427
+ padding-left: 10px;
428
+ }
429
+
430
+ .sources-title {
431
+ font-weight: 600;
432
+ color: #3f37c9;
433
+ margin-top: 15px;
434
+ margin-bottom: 8px;
435
+ }
436
+
437
+ .note-item {
438
+ padding-left: 20px;
439
+ position: relative;
440
+ margin-bottom: 6px;
441
+ font-style: italic;
442
+ }
443
+
444
+ .note-item:before {
445
+ content: "•";
446
+ position: absolute;
447
+ left: 8px;
448
+ color: #f39c12;
449
+ }
450
+
451
+ .source-reference {
452
+ display: inline-block;
453
+ background: rgba(67, 97, 238, 0.1);
454
+ color: var(--primary);
455
+ padding: 2px 8px;
456
+ border-radius: 4px;
457
+ font-size: 0.9em;
458
+ margin-right: 5px;
459
+ }
460
+
461
+ @media (max-width: 768px) {
462
+ .app-container {
463
+ grid-template-columns: 1fr;
464
+ }
465
+
466
+ .chat-container {
467
+ height: 60vh;
468
+ }
469
+
470
+ .log-container {
471
+ height: 30vh;
472
+ }
473
  }
474
  </style>
475
  </head>
476
  <body>
477
+ <div class="app-container">
478
+ <div class="card main-card">
479
+ <div class="card-header">
480
+ <div class="logo">
481
+ <div class="logo-icon">
482
+ <i class="fas fa-print"></i>
483
+ </div>
484
+ <span>EngiPrintAI</span>
 
 
 
 
 
 
 
485
  </div>
486
+ <div class="ms-auto status-indicator" id="status-indicator">
487
+ <div class="status-dot"></div>
488
+ <span>Подключено</span>
 
489
  </div>
490
  </div>
491
+ <div class="card-body">
492
+ <div class="chat-container" id="chat-container">
493
+ <div class="empty-state">
494
+ <i class="fas fa-comments"></i>
495
+ <h4>Добро пожаловать в EngiPrintAI</h4>
496
+ <p>Опишите проблему с вашим принтером или МФУ, и я помогу найти решение</p>
497
+ <div class="mt-3">
498
+ <div class="example-badge">HP LaserJet 1020 не печатает</div>
499
+ <div class="example-badge">Konica Minolta C258 ошибка C-2557</div>
500
+ <div class="example-badge">Canon i-SENSYS MF644Cdw зажевывает бумагу</div>
501
+ </div>
502
+ </div>
503
  </div>
504
+
505
+ <div class="input-area">
506
+ <div class="input-group">
507
+ <input type="text" id="user-input" class="form-control" placeholder="Опишите проблему с принтером..." autocomplete="off">
508
+ <button id="send-btn" class="btn">
509
+ <i class="fas fa-paper-plane"></i>
510
+ </button>
511
+ </div>
512
+
513
+ </div>
514
+ </div>
515
+ </div>
516
+
517
+ <div class="card logs-card">
518
+ <div class="card-header">
519
+ <i class="fas fa-terminal"></i>
520
+ <span>Журнал обработки</span>
521
+ </div>
522
+ <div class="card-body">
523
+ <div class="log-container" id="logs-container">
524
+ <div class="log-entry log-info">
525
+
526
+ </div>
527
  </div>
528
  </div>
529
  </div>
530
  </div>
531
 
532
  <script>
533
+ document.addEventListener('DOMContentLoaded', () => {
534
+ const chatContainer = document.getElementById('chat-container');
535
+ const userInput = document.getElementById('user-input');
536
+ const sendBtn = document.getElementById('send-btn');
537
+ const logsContainer = document.getElementById('logs-container');
538
+ const statusIndicator = document.getElementById('status-indicator');
539
+ const statusDot = statusIndicator.querySelector('.status-dot');
540
+ const statusText = statusIndicator.querySelector('span');
541
+ const welcomeState = document.querySelector('.empty-state');
542
+
543
+ let currentSources = [];
544
+ let isProcessing = false;
545
+ let eventSource = null;
546
+ let currentBotMessage = null;
547
+ let processingLog = [];
548
+ let responseBuffer = '';
549
+
550
+ // Технические термины для подсветки
551
+ const technicalTerms = [
552
+ 'фузер', 'термистор', 'микроконтроллер', 'перепрошивка',
553
+ 'нагревательный вал', 'контакты', 'разъем', 'плата', 'картридж',
554
+ 'вал', 'дозирующее лезвие', 'чистящее лезвие', 'мусорный бункер',
555
+ 'лоток', 'печка', 'бумагопровод', 'сенсор', 'датчик', 'лазер'
556
+ ];
557
+
558
+ // Функция добавления временной метки
559
+ function getTimestamp() {
560
+ const now = new Date();
561
+ return now.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit', second:'2-digit'});
562
+ }
563
+
564
+ // Функция добавления сообщения в журнал
565
+ function addLog(message, type = 'info') {
566
+ const logEntry = document.createElement('div');
567
+ logEntry.className = `log-entry log-${type}`;
568
+ logEntry.innerHTML = `
569
+ <span class="log-timestamp">${getTimestamp()}</span>
570
+ ${message}
571
+ `;
572
+ logsContainer.appendChild(logEntry);
573
+ logsContainer.scrollTop = logsContainer.scrollHeight;
574
+ processingLog.push(`[${getTimestamp()}] ${message}`);
575
+ }
576
+
577
+ // Добавление сообщения пользователя
578
+ function addUserMessage(message) {
579
+ if (welcomeState && welcomeState.parentElement === chatContainer) {
580
+ chatContainer.innerHTML = '';
581
+ }
582
+
583
+ const messageDiv = document.createElement('div');
584
+ messageDiv.className = 'message user-message';
585
+ messageDiv.innerHTML = `
586
+ <div class="message-header">
587
+ <i class="fas fa-user"></i>
588
+ <span>Вы</span>
589
+ <span class="ms-auto">${getTimestamp()}</span>
590
+ </div>
591
+ <div>${escapeHtml(message)}</div>
592
+ `;
593
+ chatContainer.appendChild(messageDiv);
594
+ chatContainer.scrollTop = chatContainer.scrollHeight;
595
+ }
596
+
597
+ // Создание сообщения бота с индикатором набора
598
+ function createBotThinkingMessage() {
599
+ if (welcomeState && welcomeState.parentElement === chatContainer) {
600
+ chatContainer.innerHTML = '';
601
+ }
602
+
603
+ const messageDiv = document.createElement('div');
604
+ messageDiv.className = 'message bot-message';
605
+ messageDiv.id = 'bot-thinking';
606
+ messageDiv.innerHTML = `
607
+ <div class="message-header">
608
+ <i class="fas fa-robot"></i>
609
+ <span>PrintMaster</span>
610
+ <span class="ms-auto">${getTimestamp()}</span>
611
+ </div>
612
+ <div class="typing-indicator">
613
+ <div class="typing-dot"></div>
614
+ <div class="typing-dot"></div>
615
+ <div class="typing-dot"></div>
616
+ <span class="typing-text">Думаю над ответом...</span>
617
+ </div>
618
+ `;
619
+
620
+ chatContainer.appendChild(messageDiv);
621
+ chatContainer.scrollTop = chatContainer.scrollHeight;
622
+ return messageDiv;
623
+ }
624
+
625
+ // Преобразование индикатора в полноценное сообщение
626
+ function convertToMessage(content) {
627
+ const thinkingElement = document.getElementById('bot-thinking');
628
+ if (thinkingElement) {
629
+ thinkingElement.innerHTML = `
630
+ <div class="message-header">
631
+ <i class="fas fa-robot"></i>
632
+ <span>PrintMaster</span>
633
+ <span class="ms-auto">${getTimestamp()}</span>
634
+ </div>
635
+ <div class="message-content">${formatContent(content)}</div>
636
+ `;
637
+ thinkingElement.id = 'bot-message-' + Date.now();
638
+ return thinkingElement;
639
+ }
640
+
641
+ // Если индикатора нет, создаем новое сообщение
642
+ const messageDiv = document.createElement('div');
643
+ messageDiv.className = 'message bot-message';
644
+ messageDiv.id = 'bot-message-' + Date.now();
645
+ messageDiv.innerHTML = `
646
+ <div class="message-header">
647
+ <i class="fas fa-robot"></i>
648
+ <span>PrintMaster</span>
649
+ <span class="ms-auto">${getTimestamp()}</span>
650
+ </div>
651
+ <div class="message-content">${formatContent(content)}</div>
652
+ `;
653
+
654
+ chatContainer.appendChild(messageDiv);
655
+ chatContainer.scrollTop = chatContainer.scrollHeight;
656
+ return messageDiv;
657
+ }
658
+
659
+ // Обновление сообщения бота
660
+ function updateBotMessage(content) {
661
+ if (!currentBotMessage) {
662
+ currentBotMessage = convertToMessage(content);
663
+ } else {
664
+ const contentDiv = currentBotMessage.querySelector('.message-content');
665
+ if (contentDiv) {
666
+ contentDiv.textContent = formatContent(content);
667
+ }
668
+ }
669
+ }
670
+
671
+ // Форматирование финального ответа
672
+ function finalizeBotMessage(content) {
673
+ if (!currentBotMessage) {
674
+ currentBotMessage = convertToMessage(content);
675
+ } else {
676
+ const contentDiv = currentBotMessage.querySelector('.message-content');
677
+ if (contentDiv) {
678
+ contentDiv.innerHTML = formatContent(content);
679
+ }
680
+ }
681
+ return currentBotMessage;
682
+ }
683
+
684
+ // Форматирование контента
685
+ function formatContent(content) {
686
+ // Заменяем разделы на стилизованные блоки
687
+ content = content.replace(/\*\*Проблема:\*\*/g,
688
+ '<div class="problem-text">Проблема:</div>');
689
+ content = content.replace(/\*\*Решение:\*\*/g,
690
+ '<div class="solution-text">Решение:</div>');
691
+ content = content.replace(/\*\*Примечания:\*\*/g,
692
+ '<div class="notes-text">Примечания:</div>');
693
+ content = content.replace(/\*\*Источники:\*\*/g,
694
+ '<div class="sources-title">Источники:</div>');
695
+ content = content.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>');
696
+
697
+ // Обработка ссылок на источники
698
+ content = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g,
699
+ '<a href="$2" target="_blank" class="source-link">$1</a>');
700
+
701
+ return content;
702
+ }
703
+
704
+ // Добавление ссылок на источники
705
+ function addSourceLinks() {
706
+ if (!currentBotMessage || currentSources.length === 0) return;
707
+
708
+ const messageDiv = currentBotMessage;
709
+ const existingSources = messageDiv.querySelector('.sources-container');
710
+ if (existingSources) existingSources.remove();
711
+
712
+ if (currentSources.length > 0) {
713
+ const sourcesContainer = document.createElement('div');
714
+ sourcesContainer.className = 'sources-container mt-3';
715
+ sourcesContainer.innerHTML = '<div class="font-bold text-gray-700 mb-2 flex items-center"><i class="fas fa-link mr-2"></i> Источники информации</div>';
716
 
717
+ const linksContainer = document.createElement('div');
718
+ linksContainer.className = 'flex flex-wrap gap-2';
719
 
720
+ currentSources.slice(0, 5).forEach((source, index) => {
721
+ const link = document.createElement('span');
722
+ link.className = 'sources-badge';
723
+ link.innerHTML = `<i class="fas fa-external-link-alt mr-1"></i> Источник ${index + 1}`;
724
+ link.dataset.sourceIndex = index;
 
725
 
726
+ link.addEventListener('click', function(e) {
727
+ e.preventDefault();
728
+ const sourceIndex = parseInt(this.dataset.sourceIndex);
729
+ showSourceDetails(sourceIndex);
730
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
 
732
+ linksContainer.appendChild(link);
733
+ });
 
734
 
735
+ sourcesContainer.appendChild(linksContainer);
736
+ messageDiv.appendChild(sourcesContainer);
737
+ chatContainer.scrollTop = chatContainer.scrollHeight;
738
+ }
739
+ }
740
+
741
+ // Показ деталей источника
742
+ function showSourceDetails(sourceIndex) {
743
+ const source = currentSources[sourceIndex];
744
+ if (!source) return;
745
+
746
+ // Создаем модальное окно
747
+ let modal = document.getElementById('source-modal');
748
+ if (!modal) {
749
+ modal = document.createElement('div');
750
+ modal.id = 'source-modal';
751
+ modal.style.cssText = `
752
+ position: fixed;
753
+ top: 0;
754
+ left: 0;
755
+ width: 100%;
756
+ height: 100%;
757
+ background: rgba(0,0,0,0.5);
758
+ display: flex;
759
+ align-items: center;
760
+ justify-content: center;
761
+ z-index: 1000;
762
+ `;
763
 
764
+ modal.innerHTML = `
765
+ <div class="modal-content" style="background: white; border-radius: 12px; width: 90%; max-width: 600px; max-height: 80vh; overflow: auto;">
766
+ <div class="modal-header" style="padding: 15px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center;">
767
+ <h5 style="margin: 0; font-weight: 600;">Источни�� информации</h5>
768
+ <button id="close-modal" style="background: none; border: none; font-size: 1.5rem; cursor: pointer;">&times;</button>
 
 
 
 
 
 
 
 
 
 
769
  </div>
770
+ <div class="modal-body" style="padding: 15px;">
771
+ <h6 style="font-weight: 600; margin-bottom: 5px;">Название:</h6>
772
+ <p id="source-title" style="margin-bottom: 15px;"></p>
773
+
774
+ <h6 style="font-weight: 600; margin-bottom: 5px;">URL:</h6>
775
+ <a id="source-url" href="#" target="_blank" style="color: #4361ee; text-decoration: underline; display: block; margin-bottom: 15px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"></a>
776
+ </div>
777
+ <div class="modal-footer" style="padding: 15px; border-top: 1px solid #eee; text-align: right;">
778
+ <button id="open-source" class="btn btn-primary" style="background: #4361ee; color: white; border: none; padding: 8px 15px; border-radius: 6px; cursor: pointer; margin-right: 10px;">Открыть источник</button>
779
+ <button id="close-modal-footer" class="btn btn-secondary" style="background: #e9ecef; color: #495057; border: none; padding: 8px 15px; border-radius: 6px; cursor: pointer;">Закрыть</button>
780
+ </div>
781
+ </div>
782
+ `;
783
+
784
+
785
+
786
 
787
+ document.body.appendChild(modal);
788
+
789
+
790
+
791
+
792
+
793
+
794
+ // Обработчики закрытия
795
+ const closeModal = () => modal.remove();
796
+
797
+
798
+
799
+
800
+
801
+
802
+
803
+
804
+
805
+
806
+
807
+
808
+
809
+
810
+
811
+
812
+
813
+
814
+
815
+
816
+ document.getElementById('close-modal').addEventListener('click', closeModal);
817
+ document.getElementById('close-modal-footer').addEventListener('click', closeModal);
818
+
819
+
820
+
821
+
822
+
823
+
824
+
825
+
826
+
827
+
828
+
829
+
830
+
831
+
832
+
833
+
834
+
835
+
836
+
837
 
838
+ modal.addEventListener('click', (e) => {
839
+ if (e.target === modal || e.target.closest('#close-modal, #close-modal-footer')) {
840
+ closeModal();
841
+
842
+
843
  }
844
  });
845
+ }
846
+
847
+ // Заполняем данные
848
+ document.getElementById('source-title').textContent = source.title;
849
+ const sourceUrl = document.getElementById('source-url');
850
+ sourceUrl.textContent = source.url;
851
+ sourceUrl.href = source.url;
852
+
853
+ // Обработчик открытия источника
854
+ document.getElementById('open-source').onclick = () => {
855
+ window.open(source.url, '_blank');
856
+ };
857
+
858
+ // Показываем модальное окно
859
+ document.body.appendChild(modal);
860
+ }
861
+
862
+ // Функция для экранирования HTML
863
+ function escapeHtml(unsafe) {
864
+ return unsafe
865
+ .replace(/&/g, "&amp;")
866
+ .replace(/</g, "&lt;")
867
+ .replace(/>/g, "&gt;")
868
+ .replace(/"/g, "&quot;")
869
+ .replace(/'/g, "&#039;");
870
+ }
871
+
872
+ // Подключение потока данных
873
+ function connectStream() {
874
+ if (eventSource) eventSource.close();
875
+
876
+ // Обновляем статус
877
+ statusText.textContent = 'Обработка запроса...';
878
+ statusDot.style.background = 'var(--warning)';
879
+ statusIndicator.classList.add('warning');
880
+ statusIndicator.classList.remove('success');
881
+
882
+ eventSource = new EventSource('/stream');
883
+
884
+ eventSource.onmessage = function(event) {
885
+ try {
886
+ const data = JSON.parse(event.data);
887
+ const { type, content } = data;
888
 
889
+ if (type === 'log' || type === 'processing_log') {
890
+ addLog(content, 'info');
891
+ }
892
+ else if (type === 'response_start') {
893
+ // Начало ответа - создаем индикатор набора
894
+ createBotThinkingMessage();
895
+ responseBuffer = '';
896
+ }
897
+ else if (type === 'response_chunk') {
898
+ // Порция ответа
899
+ responseBuffer += content;
900
+ updateBotMessage(responseBuffer);
901
+ }
902
+ else if (type === 'response_end') {
903
+ // Конец ответа
904
+ currentBotMessage = finalizeBotMessage(content);
905
+ addSourceLinks();
906
+ }
907
+ else if (type === 'sources') {
908
+ // Получение источников
909
+ try {
910
+ currentSources = JSON.parse(content);
911
+ addSourceLinks();
912
+ } catch (e) {
913
+ console.error('Error parsing sources:', e);
914
+ addLog('Ошибка обработки источников', 'error');
915
  }
916
+ }
917
+ else if (type === 'done') {
918
+ // Завершение обработки
919
+ isProcessing = false;
920
+ statusText.textContent = 'Подключено';
921
+ statusDot.style.background = 'var(--success)';
922
+ statusIndicator.classList.add('success');
923
+ statusIndicator.classList.remove('warning');
924
+
925
+ if (eventSource) {
926
+ eventSource.close();
927
+ eventSource = null;
928
+ }
929
+ }
930
+ } catch (e) {
931
+ console.error('Error processing event:', e);
932
+ addLog(`Ошибка обработки данных: ${e.message}`, 'error');
933
+ }
934
+ };
935
+
936
+ eventSource.onerror = function() {
937
+ addLog("⚠️ Поток данных прерван", 'error');
938
+ statusText.textContent = 'Ошибка соединения';
939
+ statusDot.style.background = 'var(--danger)';
940
+ statusIndicator.classList.add('danger');
941
+ statusIndicator.classList.remove('success', 'warning');
942
 
943
+ if (eventSource) {
944
+ eventSource.close();
945
+ eventSource = null;
946
+ }
947
+
948
+
949
+
950
+
951
+
952
+
953
+
954
+
955
+
956
+
957
 
958
+ // Попытка восстановления через 2 секунды
959
+ setTimeout(() => {
960
+ if (!isProcessing) {
961
+ statusText.textContent = 'Подключено';
962
+ statusDot.style.background = 'var(--success)';
963
+ statusIndicator.classList.add('success');
964
+ statusIndicator.classList.remove('danger');
965
+
966
+
967
+
968
+
969
+
970
+
971
+
972
+
973
+
974
+
975
+
976
+
977
+
978
+
979
+
980
+
981
+
982
+
983
+
984
+
985
+
986
+
987
+
988
+
989
+
990
+
991
+
992
+
993
+
994
  }
995
+ }, 2000);
996
+ };
997
+ }
998
+
999
+ // Отправка сообщения
1000
+ function sendMessage() {
1001
+ const message = userInput.value.trim();
1002
+ if (!message || isProcessing) return;
1003
+
1004
+ // Добавляем сообщение пользователя
1005
+ addUserMessage(message);
1006
+ userInput.value = '';
1007
+
1008
+ // Обновляем статус
1009
+ isProcessing = true;
1010
+ statusText.textContent = 'Обработка запроса...';
1011
+ statusDot.style.background = 'var(--warning)';
1012
+ statusIndicator.classList.add('warning');
1013
+ statusIndicator.classList.remove('success');
1014
+
1015
+ // Скрываем примеры запросов
1016
+ const examples = document.querySelector('.example-badge');
1017
+ if (examples) {
1018
+ examples.parentElement.style.display = 'none';
1019
+ }
1020
+
1021
+ // Очищаем предыдущие данные
1022
+ currentSources = [];
1023
+ processingLog = [];
1024
+ responseBuffer = '';
1025
+ currentBotMessage = null;
1026
+
1027
+ // Подключаемся к потоку данных
1028
+ connectStream();
1029
+
1030
+ // Отправляем запрос на сервер
1031
+ fetch('/ask', {
1032
+ method: 'POST',
1033
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1034
+ body: `message=${encodeURIComponent(message)}`
1035
+ })
1036
+ .then(res => res.json())
1037
+ .then(data => {
1038
+ if (data.status === 'processing') {
1039
+ addLog("⚙️ Запрос отправлен на сервер...", 'info');
1040
+ }
1041
+ })
1042
+ .catch(err => {
1043
+ addLog(`❌ Ошибка запроса: ${err.message}`, 'error');
1044
+ isProcessing = false;
1045
+ statusText.textContent = 'Ошибка соединения';
1046
+ statusDot.style.background = 'var(--danger)';
1047
+ statusIndicator.classList.add('danger');
1048
+ statusIndicator.classList.remove('warning');
1049
 
1050
+ // Восстанавливаем статус
1051
+ setTimeout(() => {
1052
+ statusText.textContent = 'Подключено';
1053
+ statusDot.style.background = 'var(--success)';
1054
+ statusIndicator.classList.add('success');
1055
+ statusIndicator.classList.remove('danger');
1056
+ }, 2000);
1057
  });
1058
+ }
1059
+
1060
+ // Обработчики событий
1061
+ sendBtn.addEventListener('click', sendMessage);
1062
+ userInput.addEventListener('keypress', (e) => {
1063
+ if (e.key === 'Enter') sendMessage();
1064
+ });
1065
+
1066
+ // Закрытие соединений при уходе со страницы
1067
+ window.addEventListener('beforeunload', () => {
1068
+ if (eventSource) {
1069
+ eventSource.close();
1070
+ eventSource = null;
1071
+ }
1072
+ });
1073
+
1074
+ // Инициализация
1075
+ addLog("Система инициализирована. Ожидание запроса...", 'info');
1076
+ });
1077
  </script>
1078
  </body>
1079
  </html>