KennyOry commited on
Commit
2678c19
·
verified ·
1 Parent(s): 9c09266

Update templates/index.html

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