centaline-data-driven-v3 0.0.77 → 0.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,753 @@
1
+ <template>
2
+ <div class="chat-container">
3
+ <!-- 头部 -->
4
+ <div class="header">
5
+ <div class="header-item">
6
+ <el-icon class="ico-button" @click="hideAI()">
7
+ <DArrowLeft />
8
+ </el-icon>
9
+ </div>
10
+ <div class="header-item" v-if="model.controlLabel">
11
+ <h3 style="line-height: 25px;">
12
+ {{ model.controlLabel }}</h3>
13
+ </div>
14
+ </div>
15
+ <!-- 消息列表 -->
16
+ <div class="messages" ref="messagesContainer">
17
+ <template v-for="message in model.messages">
18
+ <div :key="message.id" class="message" v-if="message.content"
19
+ :class="{ 'user-message': message.sender === 'user', 'ai-message': message.sender === 'ai' }">
20
+ <div class="chat-message" v-html="htmlContent(message)">
21
+ </div>
22
+ </div>
23
+ <div v-else class="loading-container">
24
+ <div class="loading-circle"></div>
25
+ </div>
26
+ </template>
27
+ </div>
28
+
29
+ <!-- 输入区域 -->
30
+ <div class="chat-editor" v-if="showAI">
31
+ <div class="chat-input-editor-container">
32
+ <textarea ref="textareaRef" class="chat-input" v-model="inputMessage" @keydown="handleKeydown"
33
+ placeholder="输入你的消息..."></textarea>
34
+
35
+
36
+ </div>
37
+ <div class="chat-editor-action">
38
+
39
+ <div class="left-area"></div>
40
+ <div class="right-area">
41
+ <!-- <label class="attachment-button">
42
+ <input data-v-6da69357="" class="hidden-input" type="file" multiple="false"
43
+ accept=".doc,.docx,.ppt,.pptx,.xls,.xlsx,.py,.js,.java,.go,.c,.cpp,.html,.json,.h,.py3,.css,.ts,.tsx,.yaml,.yml,.jsp,.php,.asp,.plain,.plaintext,.text,.txt,.clike,.arduino,.ino,.bash,.sh,.shell,.csharp,.cs,.dotnet,.markup,.mathml,.svg,.xml,.ssml,.atom,.rss,.diff,.ini,.regex,.javascript,.webmanifest,.kotlin,.kt,.kts,.less,.lua,.makefile,.markdown,.md,.objectivec,.objc,.perl,.markup-templating,.python,.r,.ruby,.rb,.rust,.sass,.scss,.sql,.swift,.typescript,.basic,.vbnet,.pdf,.jpg,.jpeg,.png,.apng,.bmp,.gif,.webp,.avif,.csv,.mobi,.log,.cxx,.cc,.conf,.epub">
44
+ </label> -->
45
+ <div v-if="!afoot" :class="['send-button', inputMessage && !afoot ? '' : 'disabled']"
46
+ @click="sendMessage" :disabled="!inputMessage"></div>
47
+ <button v-else class="stop-button" @click="stopRequest">
48
+ <span class="stop-icon"></span>
49
+ </button>
50
+ </div>
51
+ </div>
52
+ </div>
53
+ </div>
54
+ </template>
55
+
56
+
57
+ <script setup lang="ts">
58
+ import { ref, reactive, nextTick, watch, computed } from 'vue'
59
+ import common from '../../utils/common'
60
+ import { marked } from 'marked';
61
+ import DOMPurify from 'dompurify'
62
+
63
+
64
+ const emit = defineEmits(['loaded', "change", "click", "hideAI"])
65
+ const props = defineProps({
66
+ api: String,
67
+ vmodel: Object,
68
+ actionRouter: Array,
69
+ form: Object,
70
+ })
71
+ interface StreamEventData {
72
+ event: string
73
+ conversation_id?: string
74
+ answer?: string
75
+ task_id?: string
76
+ }
77
+
78
+ const model = ref({});
79
+ const textareaRef = ref(null);//消息输入 框
80
+ const inputMessage = ref('')//消息输入框
81
+ const outtext = ref('')//当前返回的信息
82
+ const messagesContainer = ref(null)//内容展示区域
83
+ const conversationId = ref('')//
84
+ const task_id = ref('')//任务ID,用于停止
85
+ const showAI = ref(false)//是否显示AI输入区域
86
+ const afoot = ref(false)//是否正在请求数据
87
+ let isStopped = false; // 标志是否停止读取
88
+ let scrollPending = false;//滚动输入
89
+ const headers = computed(() => {
90
+ return {
91
+ 'Content-Type': 'application/json',
92
+ Authorization: "Bearer " + props.vmodel?.code2,
93
+ };
94
+ })
95
+
96
+ init()
97
+ //初始化数据
98
+ function init() {
99
+ nextTick(function () {
100
+ if (props.vmodel) {
101
+ load(props.vmodel);
102
+ emit("loaded");
103
+ }
104
+ });
105
+ }
106
+
107
+ function action() {
108
+ let rtn = '';
109
+ showAI.value = false;
110
+ if (model.value.action) {
111
+ showAI.value = true;
112
+ rtn = model.value.action;
113
+ }
114
+ else if (props.actionRouter) {
115
+ var router = props.actionRouter.find((v1) => {
116
+ return v1.key === model.value.fieldName1;
117
+ });
118
+ rtn = router.action;
119
+ }
120
+ if (rtn != '') {
121
+ showAI.value = true;
122
+ }
123
+ else {
124
+ common.message('请配置AI路由', 'error')
125
+ return rtn;
126
+ }
127
+ rtn = rtn.endsWith('/') ? rtn : rtn + '/'
128
+ return rtn;
129
+ }
130
+ function load(data) {
131
+ if (data.code2 == "") {
132
+ common.message('请配置Key', 'error')
133
+ afoot.value = true;
134
+ return;
135
+ }
136
+ model.value = data;
137
+ action();
138
+ }
139
+
140
+ // Marked 配置
141
+ marked.setOptions({
142
+ breaks: true,
143
+ gfm: true,
144
+ })
145
+
146
+ // 安全渲染内容
147
+ function htmlContent(message) {
148
+ return DOMPurify.sanitize(marked(message.content) + '')
149
+ };
150
+ // 监听内容变化,调整 textarea 高度
151
+ watch(inputMessage, () => {
152
+ nextTick(() => {
153
+ adjustTextareaHeight();
154
+ });
155
+ });
156
+
157
+ // 调整 textarea 高度
158
+ const adjustTextareaHeight = () => {
159
+ const textarea = textareaRef.value;
160
+ textarea.style.height = 'auto';
161
+ textarea.style.height = `${Math.min(textarea.scrollHeight, 200)}px`; // 限制最大高度为 200px
162
+ };
163
+
164
+ // 处理按键事件
165
+ const handleKeydown = (event) => {
166
+ if (event.keyCode === 13) {
167
+ if (!event.shiftKey) {
168
+ event.preventDefault(); // 阻止回车换行
169
+ sendMessage();
170
+ } else {
171
+ // Shift+Enter 换行
172
+ }
173
+ }
174
+ };
175
+
176
+ // 发送消息
177
+ const sendMessage = async () => {
178
+ if (!inputMessage.value.trim()) return
179
+
180
+ if (afoot.value) {
181
+ return;
182
+ }
183
+
184
+ // 添加用户消息
185
+ addMessage({
186
+ content: inputMessage.value.trim(),
187
+ sender: 'user'
188
+ })
189
+ // 添加AI消息
190
+ addMessage({
191
+ content: "",
192
+ sender: 'ai'
193
+ })
194
+
195
+ // AI回复
196
+ await fetchAIResponse({
197
+ "query": inputMessage.value
198
+ })
199
+ }
200
+
201
+ // 添加消息通用方法
202
+ const addMessage = (message) => {
203
+ model.value.messages.push({
204
+ ...message,
205
+ timestamp: new Date()
206
+ })
207
+ scheduleScroll()
208
+ }
209
+ // 添加消息通用方法
210
+ const appendMessage = (message) => {
211
+ model.value.messages[model.value.messages.length - 1].content = model.value.messages[model.value.messages.length - 1].content + message;
212
+ scheduleScroll()
213
+ }
214
+
215
+ // 防抖滚动
216
+ const scheduleScroll = () => {
217
+ if (!scrollPending) {
218
+ if (messagesContainer.value) {
219
+ const container = messagesContainer.value;
220
+
221
+ const distanceToBottom = container.scrollHeight - (container.scrollTop + container.clientHeight);
222
+ if (distanceToBottom <= 20) {
223
+ scrollPending = true
224
+ nextTick(() => {
225
+ if (messagesContainer.value) {
226
+ messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
227
+ }
228
+ scrollPending = false
229
+ })
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ //获取关联列返回对象
236
+ function getRefFieldJson() {
237
+ let rtn = {};
238
+ if (props.form?.AIRouter.refFieldName) {
239
+ let refFieldName = props.form.AIRouter.refFieldName.split(',');
240
+ if (refFieldName.length > 0) {
241
+ rtn = props.form.getRefFieldPara(refFieldName);
242
+ }
243
+ }
244
+ return JSON.stringify(rtn);
245
+ }
246
+
247
+ //获取Token
248
+ function getToken() {
249
+ let token = null;
250
+ try {
251
+ token = common.getDataDrivenOpts().handler.getToken();
252
+
253
+ } catch (e) {
254
+
255
+ }
256
+ return token;
257
+ }
258
+
259
+
260
+ // API请求处理
261
+ const fetchAIResponse = async (params) => {
262
+ afoot.value = true;
263
+ inputMessage.value = '';
264
+ task_id.value = '';
265
+ outtext.value = '';
266
+ isStopped = false;
267
+
268
+ // 发起请求
269
+ fetch(action() + "chat-messages", {
270
+ method: 'POST',
271
+ headers: {
272
+ ...headers.value,
273
+ 'Accept': 'text/event-stream'
274
+ },
275
+ body: JSON.stringify({
276
+ ...{
277
+ inputs: { UserData: getRefFieldJson() },
278
+ query: "",
279
+ response_mode: "streaming",
280
+ conversation_id: conversationId.value,
281
+ files: [],
282
+ user: getToken(),
283
+ }, ...params
284
+ })
285
+ }).then(response => {
286
+ if (!response.ok) {
287
+ throw new Error('Network response was not ok');
288
+ }
289
+
290
+ // 获取响应的 ReadableStream
291
+ const reader = response.body.getReader();
292
+ const decoder = new TextDecoder('utf-8');
293
+ let currentLine = '';
294
+
295
+ // 创建一个 Promise,用于等待流结束或停止
296
+ return new Promise((resolve) => {
297
+ // 读取流数据
298
+ function read() {
299
+ if (isStopped) {
300
+ // 如果停止标志为 true,直接返回
301
+ outtext.value += "用户停止"
302
+ startTypingEffect(outtext.value);
303
+ outtext.value = '';
304
+ resolve();
305
+ return;
306
+ }
307
+
308
+ reader.read().then(({ done, value }) => {
309
+ if (done) {
310
+ // 流结束
311
+ resolve();
312
+ return;
313
+ }
314
+
315
+ // 解码二进制数据为字符串
316
+ const chunk = decoder.decode(value, { stream: true });
317
+ currentLine += chunk;
318
+
319
+ // 按行分割数据
320
+ const lines = currentLine.split('\n');
321
+ currentLine = lines.pop(); // 保留未完成的行
322
+
323
+ // 处理每一行
324
+ lines.forEach(line => {
325
+ processLine(line);
326
+ });
327
+
328
+ read(); // 继续读取
329
+ });
330
+ }
331
+
332
+ read();
333
+ }).finally(() => {
334
+ // 确保在流结束或中止时关闭 reader
335
+ return reader.cancel();
336
+ });
337
+ }).then(() => {
338
+ // 在所有行处理完毕后执行 startTypingEffect
339
+ startTypingEffect(outtext.value);
340
+ outtext.value = '';
341
+ }).catch(error => {
342
+ if (error.name === 'AbortError') {
343
+ console.log('Request aborted');
344
+ } else {
345
+ common.message('请求失败', 'error');
346
+ }
347
+ });
348
+ }
349
+
350
+ // 提供一个方法来停止请求
351
+ function stopRequest() {
352
+ isStopped = true; // 设置停止标志
353
+
354
+ fetch(action() + "chat-messages/" + task_id.value + "/stop", {
355
+ method: 'POST', // 请求方法
356
+ headers: headers.value,
357
+ body: JSON.stringify({
358
+ user: getToken(),
359
+ }), // 请求体,发送的数据
360
+ }).then(response => response.json()) // 将响应转换为 JSON
361
+ .then(data => {
362
+ if (data.result == 'success') {
363
+ afoot.value = true;
364
+ }
365
+
366
+ // 如果服务器返回 { result: "success" },这里会输出这个对象
367
+ }).catch(error => {
368
+ if (error.name === 'AbortError') {
369
+ console.log('Request aborted');
370
+ } else {
371
+ common.message('请求失败', 'error');
372
+ }
373
+ });
374
+
375
+ }
376
+
377
+ // 处理每一行数据的函数
378
+ function processLine(line) {
379
+ // 去掉多余的空格和换行符
380
+ line = line.trim();
381
+ // 跳过空行
382
+ if (line === '') return;
383
+
384
+ // 解析 SSE 数据
385
+ if (line.startsWith('data:')) {
386
+ try {
387
+ const eventData: StreamEventData = JSON.parse(line.slice(5).trim())
388
+ task_id.value = eventData.task_id;
389
+ handleStreamEvent(eventData)
390
+ } catch (e) {
391
+ console.error('Error parsing event:', e)
392
+ }
393
+ } else if (line.startsWith('event:')) {
394
+ //const event = line.substring(6).trim(); // 去掉 'event:' 前缀
395
+ } else if (line.startsWith('id:')) {
396
+ // const id = line.substring(3).trim(); // 去掉 'id:' 前缀
397
+ } else {
398
+ }
399
+ }
400
+
401
+
402
+
403
+ // 处理流事件
404
+ const handleStreamEvent = (eventData: StreamEventData) => {
405
+ if (eventData.conversation_id) {
406
+ conversationId.value = eventData.conversation_id
407
+ }
408
+
409
+ if (eventData.answer) {
410
+ const lastMessage = model.value.messages[model.value.messages.length - 1]
411
+ if (lastMessage.sender === 'ai') {
412
+ // if(outtext.value !== '') {
413
+ // outtext.value = outtext.value +"\n\n";
414
+ // }
415
+ outtext.value += eventData.answer
416
+
417
+ }
418
+ }
419
+ }
420
+
421
+
422
+
423
+ function startTypingEffect(fullText) {
424
+ let index = 0;
425
+ let timer = setInterval(() => {
426
+ // 检查索引是否超出字符串长度
427
+ if (index >= fullText.length) {
428
+ clearInterval(timer); // 清除定时器
429
+ return;
430
+ }
431
+
432
+ // 随机生成3到10之间的字符数
433
+ const randomChars = Math.floor(Math.random() * 8) + 3; // 3到10(包括3和10)
434
+ const charsToLoad = Math.min(randomChars, fullText.length - index);
435
+
436
+ // 获取当前要加载的字符
437
+ const currentChars = fullText.substring(index, index + charsToLoad);
438
+ // 显示字符
439
+ appendMessage(currentChars);
440
+ // 更新索引
441
+ index += charsToLoad;
442
+
443
+ // 检查是否已经加载完成
444
+ if (index >= fullText.length) {
445
+ clearInterval(timer); // 清除定时器
446
+ }
447
+ }, 100); // 每100毫秒加载10个字符
448
+ afoot.value = false;
449
+ }
450
+
451
+ function hideAI() {
452
+ emit("hideAI", false);
453
+
454
+ }
455
+ </script>
456
+
457
+ <style scoped>
458
+ .chat-container {
459
+ display: flex;
460
+ flex-direction: column;
461
+ height: 100vh;
462
+ max-width: 800px;
463
+ margin: 0 auto;
464
+ background-color: #f5f5f5;
465
+ /* border-radius: 10px */
466
+ }
467
+
468
+ .header {
469
+ border-bottom: 1px solid #e0e0e0;
470
+ background-color: #fff;
471
+ display: flex;
472
+ flex-direction: row;
473
+ /* 子元素水平排列 */
474
+ gap: 5px;
475
+
476
+ /* 子元素之间的间距为5像素 */
477
+ .header-item {
478
+ padding: 10px;
479
+ }
480
+ }
481
+
482
+ .messages {
483
+ flex: 1;
484
+ overflow-y: auto;
485
+ /* 确保子级超出时可以滚动 */
486
+ padding: 1rem;
487
+ display: flex;
488
+ flex-direction: column;
489
+ gap: 1rem;
490
+ }
491
+
492
+ /* 隐藏滚动条 */
493
+ .messages::-webkit-scrollbar {
494
+ display: none;
495
+ /* 隐藏滚动条 */
496
+ }
497
+
498
+ /* Firefox 的滚动条宽度 */
499
+ .messages {
500
+ scrollbar-width: none;
501
+ /* Firefox 隐藏滚动条 */
502
+ }
503
+
504
+ .message {
505
+ padding: 10px;
506
+ animation: fadeIn 0.3s ease-in;
507
+ min-height: auto;
508
+ /* 子级高度由内容决定 */
509
+ max-height: none;
510
+ /* 不限制子级高度 */
511
+ border-radius: 10px;
512
+
513
+ }
514
+
515
+ .user-message {
516
+ align-self: flex-end;
517
+ background-color: rgb(225, 239, 254);
518
+
519
+ }
520
+
521
+ .ai-message {
522
+ align-self: flex-start;
523
+ background-color: white;
524
+ border: 1px solid #e0e0e0;
525
+ }
526
+
527
+ .chat-message {
528
+ word-break: break-word;
529
+
530
+ }
531
+
532
+ .chat-message>:first-child {
533
+ margin-top: 0 !important
534
+ }
535
+
536
+ .chat-message>:last-child {
537
+ margin-bottom: 0 !important
538
+ }
539
+
540
+
541
+ .chat-message ::v-deep(ul) {
542
+ list-style: disc outside !important;
543
+ /* 同时设置项目符号类型和位置 */
544
+ margin: 1em 0;
545
+ /* 默认的上下外边距 */
546
+ padding-left: 30px;
547
+ /* 默认的左内边距 */
548
+ }
549
+
550
+ .timestamp {
551
+ font-size: 0.75rem;
552
+ color: #666;
553
+ margin-top: 0.5rem;
554
+ text-align: right;
555
+ }
556
+
557
+ .chat-editor {
558
+ padding: 5px;
559
+ background-color: white;
560
+ border-top: 1px solid #e0e0e0;
561
+ display: flex;
562
+ flex-direction: column;
563
+ gap: 0.5rem;
564
+ box-shadow: 0px 0px 0px .5px #dce0e9;
565
+ border-radius: 5px 5px 0px 0px;
566
+ }
567
+
568
+ .chat-input-editor-container {
569
+ width: 100%;
570
+ margin: 0 4px;
571
+ position: relative;
572
+ cursor: text;
573
+ box-sizing: border-box;
574
+ width: 100%;
575
+ transition: box-shadow 0.2s cubic-bezier(.4, 0, .2, 1);
576
+ box-shadow: none;
577
+ background-color: transparent;
578
+ background-color: rgb(255 255 255);
579
+ border-radius: 24px;
580
+ flex-direction: column;
581
+ display: flex;
582
+
583
+ }
584
+
585
+ .chat-input {
586
+ font-size: inherit;
587
+ line-height: inherit;
588
+ word-break: break-word;
589
+ white-space: pre-wrap;
590
+ border: none;
591
+ width: 100%;
592
+ margin: 0;
593
+ padding: 0;
594
+ font-family: inherit;
595
+ display: block;
596
+ top: 0;
597
+ bottom: 0;
598
+ left: 0;
599
+ right: 0;
600
+ overflow: auto;
601
+ resize: none;
602
+ /* 禁用手动调整大小 */
603
+ min-height: 20px;
604
+ /* 设置最大高度 */
605
+ max-height: 55px;
606
+ /* 设置最大高度 */
607
+ }
608
+
609
+ /* 去掉焦点边框 */
610
+ .chat-input:focus {
611
+ outline: none;
612
+ }
613
+
614
+ .chat-editor-action {
615
+ display: flex;
616
+ justify-content: space-between;
617
+ align-items: flex-end;
618
+ margin-top: 2px;
619
+ position: relative;
620
+ }
621
+
622
+ .chat-editor-action .left-area,
623
+ .chat-editor-action .right-area {
624
+ display: flex;
625
+ gap: 8px;
626
+ align-items: center;
627
+ }
628
+
629
+ .chat-editor-action .right-area {
630
+ display: flex;
631
+ gap: 8px;
632
+ align-items: center;
633
+ }
634
+
635
+ .attachment-button {
636
+ overflow: hidden;
637
+ display: block;
638
+ width: 36px;
639
+ height: 36px;
640
+ background-image: var(--bg-image);
641
+ background-position: center;
642
+ background-size: 26px;
643
+ background-repeat: no-repeat;
644
+ border-radius: 8px;
645
+ transition: background-color .15s cubic-bezier(.4, 0, .2, 1);
646
+ cursor: pointer;
647
+ --hover-bg: rgba(0, 0, 0, .04);
648
+ --bg-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAMAAADypuvZAAAB+1BMVEUAAAAAAACAgIBVVVVAgIBmZmZVVYBmZmZVampbbW1gYHBaaXhjY3Fea2tZZnNganViYnZeaHFbZHZhanJeZm9aa3NiaXFcaW9danBbZ3NdaHRgZnFcZ3JcZnBfaXNgZXNeaHFgaXJcZXJdZXFgaHRdaHRfZ3JeZXFdZ3JfZnReaHJdZ3FeZ3FeaHFdZ3NdZnJfaHFeZ3JdaHFeZnFeZ3JdaHNfZ3JeZnFdaHJdZ3FeZnNdaHJeaHJdZ3FfZnJdZnJeaHFeZ3NfZnJeZ3NdZnJeZ3JdZnFfZ3JeZnFdaHNeZ3JeZnNfZ3JeZnNfZ3FeZnJdZ3FeaHJfZ3NeaHJdZ3NfZnJeaHFeZ3JdZnJeZ3JeZ3JeZ3FeZ3JeZnJeZ3NeaHJfZ3NdZ3FeZ3JeaHJfZ3JeZnFeZ3JdZ3JeZ3JfZnNdZnJeZ3FeZnJeZ3JeZ3FdZ3JeZ3JeZ3JfZ3JeZ3JeZ3FdZ3FeaHJfZ3JeZ3JeZ3JeaHJeZ3NeZ3FeZ3JdaHJeZ3JfZ3JeZ3NeZ3JeZnFeZ3JeZ3JfZ3JeZnJeZ3NeZ3FeZnJdZ3JeZ3JeZnJeZ3JeZ3JeZ3FeZ3JeZnJeZ3JeZ3JeaHJeZ3JeZ3NeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3JeZ3L///8OKrtSAAAAp3RSTlMAAQIDBAUGCgwOEBESExQYGhscHR4fIicpKiwtLzIzNTY4Oj9AQkNESktMTU9RUlVWV1haXF1eX2BjZGVnaGlrbG1ub3Byc3R1dnd4eXp8fX6AgYKDhIWIiYuNkJKTlZaXmZqbnKKjpKanrK2xs7S3uLm6vL2/wMLDxMXGyMnKzM3P0NHT1NXW2Nrb3d7g4eLj5OXm6err7O7v8vP09fb3+Pn6+/z9/qMjBEIAAAABYktHRKhQCDaSAAACTklEQVRIx9XW6V9MURzH8e+UGBEJUWRfkkZR2ZdUIktRhLILWbKUtUgKRbbINplMk7mff9ODZtLpNbczHvZ99jv3vu95ve659/yONNkyLXvv8fKCtP8hC64PAuA8y42XeKqGGU1zSlwmsXHk9l8OAL3z45nnEsDzolmasvRMEOj02tFmYLjEM1Jk9gL19tfWB2wfLRcGYCjdhjYCLWPqaqDWhhqBVZKUXFaUKCklBG9sqAu+JkhSG5ySpCfATElSmuta90ObJHmBF5J0DVghSWVOuNQF+eG+JM0AXknSZSBbkj5Dnzt6YKCLwPrIFb87emSg80CuHbUa6CywwY6eGqgeyLOjdgPVAZvsqMNAp4ECO+o00EmgyI5eGqgW2GpHrw1UA2yzox4DHQN22NFbA1UBu+zonYGOAHvs6L2BDgIldvTJQPuBMjv6YqB9QLkd9RuoFKiwo+8GKgYO29EPA+0GKu3IL0nJQLck7QSOStKAO/oI4SRJCSEYmiOpASiWNNWJLEaMtAOpktQB3F48+0AQWClpHpHfM0ZuRTefkn/t5kOSpHzghguqAM5Jkqc+asI+SboS/TJiJAP4Nl2SVBkGYLBAklIDgGsn6AFORB5Q97i7pXquJOlCdMeNmUIgtHb8aG4Y8LmihC7g5xJzMMsPtHvc+8byIBDIHzu0bgAILZuo22wBcG5mROv0q2EiKzxBKh2AP62HcrKycioeDgM4VbbOlhdgXH4X2lv8ombT3MuM6wiy5m4wKoJ3Vsd9QPL6ahqamhpqfF5NwvwFB+eT0/aKvPQAAAAASUVORK5CYII=);
649
+ --disabled-bg-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADQAAAA0CAYAAADFeBvrAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAN6SURBVHgB7VpdTttAEB4HkICncIL6COQFCXioc4JyA8IJSk6Q5ATACUhPAD1BUomfx6QnqHuCpi8kivLTb1IHrYd1kvFPiFp/kmV7PLveb2d2155Zohw51gqHUkKn0yn2+31vMpm4juMUZ5U7TpeP4+Njn9aExISen5+96XRaw6W3QK0LnZvT09MmZYxEhJ6enq5wulQUacNiF1laLDYhWOYWvV6xPYO8N3c7C3w8K2dFqkAxwJaxkGlDdrG3t3cA1zo4OTlxcF+C/IvQcyFv8ZijDKC20OPjYwU9fCvEVRC4jioDa85I4NI1xNcoU6WUobYQyNSEaCEZBrsXuxm7oiG+ZKKUMlSE2Dpk9DIa2VxGZo5gzNyYMhDUTCgrQUUIBD4JkRwfs/Xo4eHhMiAfwnA4vBZWkvUlhtblXOPaR6+3pQIW106hULjicQZSdfNZuVzu4VnXrC/K7VgexyVVhNC7rnHry+etVotnrlcdkDq31PFTiFypAyI16P3gAzOqyi21Lrdwqt3d3U1lKsbnk0miRgrEWoeyhug4VSe9OyG4VaoLbE4oKcS0nTrew0L/HKFMkYSQSxuI3EKbjpzQpiMntOnYBEIupYjcQinApwyRW2jT8X9/yy37l7HEq4sy5Iuoz0fzHr/by34nVL8b2iBJqPKI+LQZpuKc0S2Ho1g3iOZURJ2m/jxy9Aro+6SA1uVCLx8MBocWHRl8PONwFIj9wrkunr2J7SFyFKpzBQuGoHW57+Y9wk2e1OHQMPQatALQWJveuXjnN1JARQj+36Zwgz7b3A7plPoiUsFYrMI6TVMeZCk8U7azs3NPmjZqlNk9xMRQfHl5sUY2mdT29jbnh25AvE1/F1Qu30DHlCKC/Gwd17j3j46OuqRAnPxQXaRUemh4WftiS72HqLdjyoL0ZZMUUK9DnEGg8OdLcTQa3SXJ9XBZNP5OiH0tGYaaEGcQuOeEmH2/g8D6GSkRZNFldo+tU6YYiJ00trjevCFNnBrLksLBvgYu/2YM8jjjMUgxkCitH0UqQBvT+tetra0ujt54PO6hoYe8MQOTAie6PFuhJGQYiTdeLCGlRXXVFGcUUtkaE5Hl1oCn8yosk2imZKS214cBYhWczuXiuAC8s6RhS23GRaqE5ggsxt9kHo4PRgLLh/w3ju7+/v59qVTKNHCfI8ca8AeLM6GG6K5VFwAAAABJRU5ErkJggg==);
650
+ }
651
+
652
+ .attachment-button:hover {
653
+
654
+ background-color: rgba(219, 221, 225, 0.6);
655
+ }
656
+
657
+ .ico-button {
658
+ cursor: pointer;
659
+ width: 25px;
660
+ height: 25px;
661
+ background-color: #DBEAFE;
662
+ }
663
+
664
+
665
+ .hidden-input {
666
+ display: none;
667
+ }
668
+
669
+ .send-button {
670
+ overflow: hidden;
671
+ display: block;
672
+ width: 36px;
673
+ height: 36px;
674
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/AP+gvaeTAAADO0lEQVRoge2ZS0gVURjHv6OZoZVJhFgGQlKblCSJCFdtgkJoYYs2BUXRC6KghRERbYTARVBmtAiMIoiIiFZCuCiKyChBbSUmgZWSpeEr9dfijHYY59x53Ln3Esx/dZn5zv9x59zznTNXJEGCBAkSpAEVtBDYKCKHRWSHc6lXRB4ppboyYSxWAGeACbzRCezJtUcrgJMW4250AY1AXq49LwIoA8ZdRvuBHmDeEuQTcARYnmv/ApxwmbsCKOdeNXAfmLUEGQTOAsW5DHDDMDQK5HvUbALagClLkGHgElCaiwC3DCNDPrXlwDVgzBJkzLlfni3/Alw1DMwFmdfAamfqDFmCTAPtwOZsBDjuEq8MMbbYCTJoCTIHPAO2ZzLAXpdofQSOAuAQ0GcJAtAB7MpEgGqX0ME0uPKAA8D7FEHibYpAqUvgQky89c70seGD89SWrHpRxH47pPPA9Rj8m9y7nCC2pviRdH8j6M66EOBxTN7dGjXAA7yb4gywOx3yDuMbehujby8tW1McBlZ5jfHdTgN3RW+jlYj8EJGmkL5mROSpUmo06AB0s2sVkf3G5dNKqdaQ2kuaWVR0R9AtBL4ZHO1edUG2vj/DintgK1AUZoBSalpEBoxLa73qlgXgWh9G2APzItKilJoIMwjYIiK1xqUBr7ogASqMz/0iUhfGiIhMKaUmgxY7T+qoiFwWkQLj1pOQuouEr4x52BmJJJjOGuCia94v4AXOOSQK8WeD6F7MvhdOfc3AL8sC8A7wnP9ByPOBPwZZc4zGK4Gb2F8WjABNQIE/m12kwkV6KgbjVcBtdIf1wlf00bUkXS0BdrrIG9LgqkUfZGxn6H70+WFF2sYN0UaXSK3/qCUcfjvPbvTOM8iKGFr8nEtsXcBxCmgAXqcw/tKpiba6BDTSYghO+onx7/TV42c8Y6ZdhtoM4cEUdUXo148DFtOzwENgW1aMG8bMJzADlLnul2BvPqC3xXeAqqwaNwzucxl6g/5R1pC6+Yw74TfkxLgRoAB9rAuKEfQaHq1zZgLoNxPffYx/Ac4DK3Pt1xPo7vncw3gfcAwozJW3UGsw+l+aOtHb8F6lVE9GXCVIkCDBf4O/rcmOoo4lpkIAAAAASUVORK5CYII=);
675
+ background-position: center;
676
+ background-size: 26px;
677
+ background-repeat: no-repeat;
678
+ background-color: #356bfd;
679
+ cursor: pointer;
680
+ }
681
+
682
+ .send-button:hover {
683
+ background-color: rgba(53, 107, 253, .6);
684
+ }
685
+
686
+ .disabled {
687
+ background-color: #ddd !important;
688
+ }
689
+
690
+ @keyframes fadeIn {
691
+ from {
692
+ opacity: 0;
693
+ transform: translateY(10px);
694
+ }
695
+
696
+ to {
697
+ opacity: 1;
698
+ transform: translateY(0);
699
+ }
700
+ }
701
+
702
+
703
+
704
+ .stop-button {
705
+ background-color: #f8f9fa;
706
+ border: 1px solid #dee2e6;
707
+ /* border-radius: 50%; */
708
+ width: 36px;
709
+ height: 36px;
710
+ display: flex;
711
+ justify-content: center;
712
+ align-items: center;
713
+ cursor: pointer;
714
+ transition: all 0.3s ease;
715
+ background-color: #007bff;
716
+ }
717
+
718
+
719
+
720
+ .stop-icon {
721
+
722
+ width: 14px;
723
+ height: 14px;
724
+ border-radius: 2px;
725
+ display: inline-block;
726
+ background-color: #e9ecef;
727
+ }
728
+
729
+ .loading-container {
730
+ width: 20px;
731
+ height: 20px;
732
+ background-color: #f5f5f5;
733
+ }
734
+
735
+ .loading-circle {
736
+ width: 20px;
737
+ height: 20px;
738
+ border: 5px solid #dbdbdb;
739
+ border-top: 5px solid #999494;
740
+ border-radius: 50%;
741
+ animation: spin 1s linear infinite;
742
+ }
743
+
744
+ @keyframes spin {
745
+ 0% {
746
+ transform: rotate(0deg);
747
+ }
748
+
749
+ 100% {
750
+ transform: rotate(360deg);
751
+ }
752
+ }
753
+ </style>