核工业西南物理研究院知识库AI客户端
xiangpei
2025-04-18 7789aeaad9032763805da324d743bc664bddd2e8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
<template>
  <div style="height:calc(100vh - 20px);width: 100%;display: flex;justify-content: center">
    <!-- 聊天消息列表 -->
    <div class="chat-messages">
      <div
          v-for="(message, index) in messages"
          :key="message + index"
          :class="['message', message.role]"
          @mouseover="handleMouseEnter(message)"
      >
        <div class="avatar">
          <img :src="getAvatar(message.role)" alt="avatar" />
        </div>
        <div v-if="!message.content && message.role == 'assistant'" v-loading="!message.content && message.role == 'assistant'">
 
        </div>
        <div v-else class="content">
          <div class="text" :ref="'msg' + index" v-html="getHtml(message.content)"></div>
          <div v-if="message.role === 'assistant'">
            <div v-show="msgIndex === message" class="msg-op-list">
              <el-button class="copy" v-show="false"/>
              <el-tooltip class="item" effect="dark" content="复制" placement="top">
                <i class="el-icon-copy-document msg-op msg-copy" @click="copyText(message.content)"/>
              </el-tooltip>
              <el-tooltip class="item" effect="dark" content="重新生成" placement="top">
                <i class="el-icon-refresh msg-op msg-re" @click="reAnswer"/>
              </el-tooltip>
            </div>
          </div>
        </div>
        <div v-if="message.role === 'user'">
          <div v-show="msgIndex === message" style="display: flex">
            <el-tooltip class="item" effect="dark" content="复制" placement="top">
              <i class="el-icon-copy-document msg-op msg-copy" @click="copyText(message.content)"/>
            </el-tooltip>
          </div>
        </div>
      </div>
    </div>
 
    <!-- 输入框 -->
    <div class="chat-input">
      <div style="position: relative;width: 800px;box-sizing: border-box;">
        <el-input
            v-model="inputMessage"
            type="textarea"
            resize="none"
            :autofocus="true"
            :autosize="true"
            placeholder="请输入消息,按下回车发送"
            @keyup.native.enter="sendMessage"
            @keydown.enter.native.prevent="handleEnterKey"
        >
        </el-input>
        <div class="send-op-list">
          <div class="send-warp">
            <div class="add-option">
              <div :class="{'add-but': true, 'add-but-active': netSearchEnable}" @click="changeNetEnable">
                <svg t="1743144032689" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2731" width="18" height="18">
                  <path fill="#575757" d="M909.989 343.281c-21.76-51.446-52.904-97.643-92.568-137.307-39.663-39.664-85.86-70.808-137.306-92.568-53.28-22.536-109.858-33.962-168.164-33.962S397.067 90.87 343.787 113.406c-51.446 21.76-97.643 52.904-137.307 92.568-39.664 39.664-70.808 85.86-92.568 137.307-22.536 53.28-33.962 109.858-33.962 168.164s11.426 114.884 33.962 168.164c21.76 51.445 52.904 97.643 92.568 137.306 39.664 39.664 85.86 70.809 137.307 92.568 53.28 22.535 109.858 33.962 168.164 33.962s114.884-11.427 168.164-33.962c51.445-21.76 97.643-52.904 137.306-92.568 39.664-39.663 70.809-85.86 92.568-137.306 22.535-53.28 33.962-109.858 33.962-168.164s-11.427-114.884-33.962-168.164zM543.951 376.03c17.57-0.243 35.095-0.683 52.536-1.368a1990.762 1990.762 0 0 0 91.131-5.678c10.157 34.378 16.643 71.314 19.389 110.46H543.951V376.03z m0-64.069V163.41c17.617 12.176 39.372 29.845 61.074 54.197 23.42 26.28 43.109 56.138 58.784 89.019-41.937 3.147-82.117 4.783-119.858 5.335z m-64-144.763v144.705a2038.572 2038.572 0 0 1-103.41-4.309 1929.6 1929.6 0 0 1-11.183-0.786c15.195-31.915 34.175-60.991 56.676-86.711 20.284-23.185 40.722-40.49 57.917-52.899zM370.842 371.354a2124.301 2124.301 0 0 0 109.109 4.606v103.484H322.25c2.742-39.079 9.208-75.96 19.335-110.285 9.453 0.776 19.21 1.512 29.257 2.195z m-112.731 108.09H145.342c3.916-45.757 16.208-89.778 36.139-130.276 20.257 3.578 52.836 8.759 95.407 13.641-9.902 36.699-16.182 75.682-18.777 116.635z m-0.859 64c1.975 39.144 9.712 78.242 23.101 116.799-44.093 4.976-77.797 10.319-98.621 13.992-20.08-40.64-32.458-84.841-36.39-130.792h111.91z m64.098 0h158.601v104.048c-36.37 0.602-72.818 2.13-109.109 4.601-8.41 0.573-16.612 1.182-24.609 1.821-14.322-36.523-22.659-73.543-24.883-110.47z m158.601 168.103v131.712c-16.974-15.877-36.574-35.962-56.038-59.735-18.064-22.064-33.892-44.698-47.379-67.668a2038.38 2038.38 0 0 1 103.417-4.309z m64 136.665V711.487c34.387 0.503 70.782 1.903 108.68 4.527-13.467 22.915-29.263 45.496-47.286 67.509-21.547 26.32-43.262 48.119-61.394 64.689z m52.536-199.426a2063.187 2063.187 0 0 0-52.536-1.384V543.444h163.958c-2.227 36.975-10.582 74.045-24.939 110.615a1994.972 1994.972 0 0 0-86.483-5.273z m175.521-105.342H878.56c-3.928 45.898-16.283 90.051-36.322 130.652a1822.84 1822.84 0 0 0-93.476-13.439c13.478-38.691 21.264-77.929 23.246-117.213z m-0.859-64c-2.605-41.111-8.924-80.238-18.891-117.061a1821.491 1821.491 0 0 0 90.233-13.075c19.89 40.46 32.158 84.432 36.07 130.136H771.149z m1.017-228.215a373.188 373.188 0 0 1 34.046 39.155 1772.853 1772.853 0 0 1-75.117 9.98c-19.529-46.785-45.825-88.91-78.291-125.338a423.863 423.863 0 0 0-5.386-5.927c46.185 18.263 88.573 45.955 124.748 82.13zM384.81 165.923a420.788 420.788 0 0 0-8.355 9.102c-32.552 36.525-58.904 78.775-78.449 125.71-32.357-3.484-59.48-7.244-80.226-10.469a373.386 373.386 0 0 1 33.956-39.037c38.338-38.338 83.654-67.152 133.074-85.306zM251.736 771.659a373.444 373.444 0 0 1-33.584-38.548c22.526-3.498 52.529-7.614 88.626-11.328 18.229 35.542 41.31 70.356 68.867 103.808a678.068 678.068 0 0 0 35.727 39.995c-59.757-16.875-114.524-48.814-159.636-93.927z m368.249 91.745a677.032 677.032 0 0 0 33.629-37.814c27.469-33.344 50.487-68.043 68.689-103.466a1780.315 1780.315 0 0 1 83.528 10.88 373.34 373.34 0 0 1-33.665 38.654c-43.23 43.232-95.324 74.373-152.181 91.746z" p-id="2732"></path></svg>
                联网搜索
              </div>
            </div>
            <div class="send-but-warp">
              <div class="send-but" @click="sendMessage">
                <svg t="1743144485359" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1261" width="36" height="36">
                  <path fill="#409eff" d="M512 85.333333c234.666667 0 426.666667 192 426.666667 426.666667s-192 426.666667-426.666667 426.666667S85.333333 746.666667 85.333333 512 277.333333 85.333333 512 85.333333z m-6.4 234.666667c-4.266667 2.133333-6.4 2.133333-12.8 8.533333l-153.6 153.6c-12.8 12.8-12.8 32 0 44.8 12.8 12.8 32 12.8 44.8 0l96-96V682.666667c0 17.066667 14.933333 32 32 32s32-14.933333 32-32V430.933333l96 96c12.8 12.8 32 12.8 44.8 0s12.8-32 0-44.8l-153.6-153.6c-6.4-6.4-8.533333-8.533333-12.8-8.533333s-8.533333-2.133333-12.8 0z" p-id="1262"></path>
                </svg>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
 
<script>
import ClipboardJS from 'clipboard';
import {Message} from "element-ui";
const markdownIt = require('markdown-it')();
 
export default {
  name: 'AiChat',
  data() {
    return {
      chatId: null,
      messages: [], // 用于页面展示的对话列表
      netSearchEnable: false,
      inputMessage: '',
      sendMsgForm: {
        query: "",
        mode: "local_kb",
        kbName: "SouthWest_Neclear_Develepment_KB",
        topK: 3,
        scoreThreshold: 2,
        history: [
 
        ],
        stream: true,
        model: "Qwen25-32B-Instruct",
        temperature: 1.15,
        maxTokens: 512,
        promptName: "default",
        returnDirect: false
      },
      msgIndex: null,
      msgRole: null,
      chunkRef: null, // ai回答临时数据
      haveThinkStart: false, // ai回答响应数据是否有think标签
      haveThinkEnd: false, // ai回答响应数据是否有think标签
      controller: null, // ai回答请求控制器,可以根据业务中断请求
    };
  },
  methods: {
    // md渲染为html
    getHtml(md) {
      return markdownIt.render(md)
    },
    changeNetEnable() {
      this.netSearchEnable = ! this.netSearchEnable
    },
    // 阻止回车换行
    handleEnterKey(e) {
      e.preventDefault(); // 阻止默认换行行为
    },
    handleMouseEnter(msgIndex) {
      this.msgIndex = msgIndex
    },
    handleMouseLeave() {
      this.msgIndex = null
    },
    // 重新生成
    reAnswer() {
    },
    // 复制内容
    copyText(content) {
      const clipboard = new ClipboardJS('.copy', {
        text: () => content
      });
      // 触发复制(需要一个隐藏的按钮)
      document.querySelector('.copy').click();
      clipboard.destroy();
      Message.success("复制成功")
    },
    // 获取角色头像
    getAvatar(role) {
      const avatars = {
        user: 'https://picsum.photos/200',
        assistant: 'https://picsum.photos/200',
      };
      return avatars[role];
    },
 
    // 获取角色名称
    getRoleName(role) {
      const roleNames = {
        user: '用户',
        assistant: 'AI 助手',
      };
      return roleNames[role];
    },
 
    async sendMessage() {
      if (this.inputMessage.trim() === '') return;
 
      // 初始化状态
      this.chunkRef = '';
      this.haveThinkStart = false;
      this.haveThinkEnd = false;
      this.controller = new AbortController();
 
      // 添加用户消息和占位助理消息
      this.messages.push(
          { role: 'user', content: this.inputMessage },
          { role: 'assistant', content: '' }
      );
      this.sendMsgForm.query = this.inputMessage;
      this.inputMessage = '';
      const assistantIndex = this.messages.length - 1;
      try {
        const response = await fetch('/api/chat/send/msg', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
            "Accept": 'text/event-stream',
            'Cache-Control': 'no-cache, no-transform'
          },
          signal: this.controller.signal,
          body: JSON.stringify(this.sendMsgForm)
        });
 
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
 
        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let jsonBuffer = '';
 
        // eslint-disable-next-line no-constant-condition
        while (true) {
          const { done, value } = await reader.read();
          if (done) {
            this.sendMsgForm.history.push({ role: 'user', content: this.inputMessage })
            this.sendMsgForm.history.push({ role: 'assistant', content: this.messages[assistantIndex].content })
            break;
          }
 
          const chunk = decoder.decode(value, { stream: true });
          const lines = chunk.split('\n');
 
          for (const line of lines) {
            if (!line.startsWith('data:')) continue;
 
            const rawData = line.slice(5).trim();
            if (rawData === '[DONE]') continue;
 
            try {
              jsonBuffer += rawData;
              const data = JSON.parse(jsonBuffer);
              jsonBuffer = '';
              let content = '';
              if (data.docs) {
                for (let i = 0; i < data.docs.length; i++) {
                  content += data.docs[i]
                }
              } else {
                content = data?.choices?.[0]?.delta?.content || '';
              }
              if (!content) continue;
              // 实时更新
              this.messages[assistantIndex].content += content;
              this.messages[assistantIndex].content = this.messages[assistantIndex].content.replace(/<\/?think>/g, '');
              await this.$nextTick();
            } catch {
              // JSON不完整,等待下次数据
              continue;
            }
          }
        }
      } catch (error) {
        if (error.name !== 'AbortError') {
          console.error('Request failed:', error);
          this.messages[assistantIndex].content = '请求异常';
        }
      }
    }
 
  },
};
</script>
 
<style scoped>
.add-but {
  width: 90px;
  height: 80%;
  border: 1px solid #E4E7ED;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  background-color: #DCDFE6;
}
.add-but:hover {
  cursor: pointer;
  background-color: #409EFF;
}
.add-but-active {
  background-color: #409EFF;
}
.send-but-warp {
  flex:1;
  display: flex;
  justify-content: flex-end;
  align-items: center;
}
.send-but:hover {
  cursor: pointer;
}
.ai-chat-dialog {
  display: flex;
  flex-direction: column;
  border-radius: 8px;
  overflow: hidden;
}
 
.chat-messages {
  padding: 16px;
  margin-top: 14px;
  width: 800px;
}
 
.message {
  display: flex;
  margin-bottom: 18px;
  min-height: 75px;
}
 
.message.user {
  align-items: center;
  flex-direction: row-reverse;
}
 
.avatar img {
  width: 40px;
  height: 40px;
  border-radius: 50%;
}
 
.content {
  max-width: 70%;
  margin: 0 12px;
  padding: 8px 12px;
  border-radius: 8px;
  background-color: #fff;
  /*box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);*/
}
 
.message.user .content {
  background-color: #409eff;
  color: #fff;
}
 
.name {
  font-size: 12px;
  color: #666;
  margin-bottom: 4px;
}
 
.message.user .name {
  color: #fff;
}
 
.text {
  font-size: 14px;
}
 
.chat-input {
  background-color: #fff;
  width: 100%;
  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;
  position: absolute;
  bottom: 50px;
  left: 50%; /* 将 div 的左边移动到父容器的 50% 位置 */
  transform: translateX(-50%); /* 将 div 向左移动自身宽度的 50% */
}
 
.msg-op-list {
  margin-top: 15px;
}
.msg-copy {
  font-size: 18px;
}
.msg-re {
  font-size: 20px;
}
.msg-op {
  color: gray;
  margin-right: 5px;
}
.msg-op:hover {
  cursor: pointer;
}
 
.send-op-list {
  position: absolute;
  bottom: 0px;
  width: 100%;
  height: 40px;
  display: flex;
  align-items: center;
  user-select: none;
  opacity: 1;
  z-index: 2;
}
 
.send-warp {
  display: flex;
  width: 100%;
  height: 100%;
  padding: 0 15px;
  box-sizing: border-box
}
 
.add-option {
  flex:1;
  display: flex;
  justify-content: flex-start;
}
 
::v-deep(.el-textarea__inner) {
  min-height: 100px !important;
  max-height: 400px !important;
  padding-bottom: 85px !important;
}
</style>