ai-chat-bot-interface 1.0.9 → 1.1.1

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.
package/src/ChatUi.vue CHANGED
@@ -3,94 +3,129 @@
3
3
  <div class="cui_wrap">
4
4
  <div class="cui_header">
5
5
  <div class="title" @click.stop="queryHistoryList">
6
- <img class="logo" :src="logo" alt="logo" style="width: 24px; height: 24px;"/>
7
- {{ name }}
6
+ <div class="back" @click.stop="handleBack">
7
+ <back-icon />
8
+ </div>
9
+ <img
10
+ class="logo"
11
+ :src="logo"
12
+ alt="logo"
13
+ style="width: 24px; height: 24px"
14
+ />
15
+ <div class="name_box">
16
+ <div class="name">{{ name }}</div>
17
+ <div class="name_sub">nutribite.com</div>
18
+ </div>
8
19
  </div>
9
20
  <div class="btn_group">
21
+ <button class="btn" @click.stop="createConv">
22
+ <new-chat class="icon" />
23
+ </button>
10
24
  <template v-show="false">
11
25
  <button class="btn">
12
- <clear-icon/>
26
+ <clear-icon />
13
27
  </button>
14
28
  <button class="btn">
15
- <close-icon/>
29
+ <close-icon />
16
30
  </button>
17
31
  </template>
18
32
  </div>
19
33
  </div>
20
34
  <div class="cui_content">
21
- <div v-if="botInfo && botInfo.onboarding_info" style="text-align: left; margin-top: 50px;">
35
+ <div
36
+ v-if="botInfo && botInfo.onboarding_info"
37
+ style="text-align: left; margin-top: 50px"
38
+ >
22
39
  <div style="text-align: center">
23
- <img :src="botInfo.icon_url" alt="icon" width="64" height="64"
24
- style="border-radius: 15px;"
40
+ <img
41
+ :src="botInfo.icon_url"
42
+ alt="icon"
43
+ width="64"
44
+ height="64"
45
+ style="border-radius: 15px"
25
46
  />
26
47
  <p class="board_name">{{ botInfo.name }}</p>
27
48
  </div>
28
49
 
29
50
  <div class="board_desc">{{ botInfo.onboarding_info.prologue }}</div>
30
51
  <div class="flexcss">
31
- <span v-for="(item, idx) in botInfo.onboarding_info.suggested_questions"
32
- :key="idx"
33
- class="board_sug"
34
- @click.stop="chatConv({code: item, text: item});"
35
- >{{ item }}</span>
52
+ <span
53
+ v-for="(item, idx) in botInfo.onboarding_info.suggested_questions"
54
+ :key="idx"
55
+ class="board_sug"
56
+ @click.stop="chatConv({ code: item, text: item })"
57
+ >{{ item }}</span
58
+ >
36
59
  </div>
37
60
  </div>
38
61
  <template v-for="(conv, index) in historyList" :key="index">
39
62
  <div v-if="conv.role === 'assistant'" class="replay role_sys">
40
- <img class="avatar" :src="logo" alt="avatar">
41
63
  <div class="replay_content">
42
- <div class="name">{{ name }} <!--<span class="time">12:30</span>--></div>
64
+ <div class="name">
65
+ <img class="avatar" :src="logo" alt="avatar" />
66
+ {{ name }}
67
+ <!--<span class="time">12:30</span>-->
68
+ </div>
43
69
  <div class="box">
44
- <p class="text" v-html="conv.content"></p>
45
- <div v-if="conv.extra.length && !isAnswering">
46
- <template v-for="(comp, idx) in conv.extra" :key="idx">
47
- <dishes-list v-if="comp.showType === 'card'" :sku-list="comp.skuList"
48
- @select="(data) => handleCardTap(data, comp)"/>
49
- <plan-card v-if="comp.showType === 'plan'" @select="handleCardTap({ type: 'match'}, comp)"/>
50
- </template>
51
- </div>
70
+ <template v-if="conv.content">
71
+ <p class="text" v-html="conv.content"></p>
72
+ <div v-if="conv.extra.length && !isAnswering">
73
+ <template v-for="(comp, idx) in conv.extra" :key="idx">
74
+ <dishes-list
75
+ v-if="comp.showType === 'card'"
76
+ :sku-list="comp.skuList"
77
+ @select="(data) => handleCardTap(data, comp)"
78
+ />
79
+ <plan-card
80
+ v-if="comp.showType === 'plan'"
81
+ @select="handleCardTap({ type: 'match' }, comp)"
82
+ />
83
+ </template>
84
+ </div>
85
+ </template>
86
+ <loading-icon2 v-else />
52
87
  </div>
53
88
  </div>
54
89
  </div>
55
90
  <div v-else class="replay role_user">
56
91
  <div class="replay_content">
57
- <div class="name">User_{{ uid }} <!--<span class="time">12:30</span>--></div>
92
+ <div class="name">
93
+ User_{{ uid }}
94
+ <!--<span class="time">12:30</span>-->
95
+ <img class="avatar" :src="avatar" alt="avatar" />
96
+ </div>
58
97
  <div class="box">
59
98
  <p class="text" v-html="conv.content"></p>
60
99
  </div>
61
100
  </div>
62
- <img class="avatar" :src="avatar" alt="avatar">
63
101
  </div>
64
102
  </template>
65
- <div ref="endTarget" style="height: 100px;"/>
103
+ <div ref="endTarget" style="height: 100px" />
66
104
  </div>
67
- <div class="cui_operate">
68
- <button class="btn" @click.stop="createConv">
69
- <new-session-icon/>
70
- </button>
71
- <div class="input_group">
72
- <input v-model="inputText" class="input" @keyup.enter="chatConv"/>
73
- <div class="send" @click.stop="chatConv">
74
- <send-icon :style="{ color: !isAnswering && inputText ? '#333': '#ccc'}"/>
75
- </div>
76
- </div>
77
- </div>
78
- <div class="cui_footer">Powered by DeepSeek</div>
105
+ <operate-module
106
+ v-model="inputText"
107
+ @send="chatConv"
108
+ @tag="handleTagSel"
109
+ />
79
110
  </div>
80
111
  </div>
81
-
82
112
  </template>
83
113
 
84
114
  <script setup>
85
- import {computed, nextTick, onMounted, ref} from 'vue';
115
+ import { computed, nextTick, onMounted, ref } from 'vue';
86
116
  import ClearIcon from './components/icons/ClearIcon.vue';
87
117
  import CloseIcon from './components/icons/CloseIcon.vue';
88
118
  import NewSessionIcon from './components/icons/NewSessionIcon.vue';
89
119
  import SendIcon from './components/icons/SendIcon.vue';
90
- import {get, post} from './utils/request';
120
+ import { get, post } from './utils/request';
91
121
  import DishesCard from './components/DishesCard.vue';
92
122
  import DishesList from './components/DishesList.vue';
93
123
  import PlanCard from './components/PlanCard.vue';
124
+ import OperateModule from './components/OperateModule.vue';
125
+ import BackIcon from './components/icons/BackIcon.vue';
126
+ import NewChat from './components/icons/newChat.vue';
127
+ import LoadingIcon from './components/icons/loadingIcon.vue';
128
+ import LoadingIcon2 from './components/icons/loadingIcon2.vue';
94
129
 
95
130
  const chatOptions = computed(() => {
96
131
  return {
@@ -111,7 +146,8 @@ const props = defineProps({
111
146
  },
112
147
  avatar: {
113
148
  type: String,
114
- default: 'https://prodstatic.weis1606.cn/api/smartFood/Nutribite/icons/home/icon_3.png',
149
+ default:
150
+ 'https://prodstatic.weis1606.cn/api/smartFood/Nutribite/icons/home/icon_3.png',
115
151
  },
116
152
  name: {
117
153
  type: String,
@@ -130,12 +166,11 @@ const props = defineProps({
130
166
  type: String,
131
167
  required: true,
132
168
  },
133
-
134
169
  });
135
170
 
136
171
  const Emits = defineEmits(['call']);
137
172
 
138
- // const endTarget = ref(null);
173
+ const endTarget = ref(null);
139
174
  const inputText = ref('');
140
175
  const botInfo = ref({});
141
176
 
@@ -152,9 +187,9 @@ onMounted(async () => {
152
187
 
153
188
  const createConv = async () => {
154
189
  const res = await post(
155
- 'https://api.coze.cn/v1/conversation/create',
156
- {'bot_id': props.botId, 'connector_id': '999'},
157
- {...chatOptions.value},
190
+ 'https://api.coze.cn/v1/conversation/create',
191
+ { bot_id: props.botId, connector_id: '999' },
192
+ { ...chatOptions.value },
158
193
  );
159
194
  console.log(res);
160
195
  if (res.code === 0 && res.data) {
@@ -217,32 +252,33 @@ const chatConv = async (data) => {
217
252
  extra: [],
218
253
  });
219
254
  const res = await fetch(
220
- `https://api.coze.cn/v3/chat?conversation_id=${conversationId.value}`,
221
- {
222
- method: 'POST',
223
- headers: {
224
- ...chatOptions.value.headers,
225
- // 'Content-Type': 'text/event-stream',
226
- },
227
- body: JSON.stringify({
228
- bot_id: props.botId,
229
- user_id: props.uid,
230
- stream: true,
231
- connector_id: '999',
232
- additional_messages: [
233
- {
234
- 'role': 'user',
235
- 'content_type': 'text',
236
- 'content': isInCode ? data.code : inText, // '配餐1600kcal,身高173,体重60kg,生成一天的套餐',
237
- }],
238
- custom_variables: {
239
- 'uid': props.uid,
240
- },
241
- }),
242
- credentials: 'same-origin', // 默认同源策略
243
- mode: 'cors', // 默认跨域模式
244
- cache: 'default', // 默认缓存策略
255
+ `https://api.coze.cn/v3/chat?conversation_id=${conversationId.value}`,
256
+ {
257
+ method: 'POST',
258
+ headers: {
259
+ ...chatOptions.value.headers,
260
+ // 'Content-Type': 'text/event-stream',
245
261
  },
262
+ body: JSON.stringify({
263
+ bot_id: props.botId,
264
+ user_id: props.uid,
265
+ stream: true,
266
+ connector_id: '999',
267
+ additional_messages: [
268
+ {
269
+ role: 'user',
270
+ content_type: 'text',
271
+ content: isInCode ? data.code : inText, // '配餐1600kcal,身高173,体重60kg,生成一天的套餐',
272
+ },
273
+ ],
274
+ custom_variables: {
275
+ uid: props.uid,
276
+ },
277
+ }),
278
+ credentials: 'same-origin', // 默认同源策略
279
+ mode: 'cors', // 默认跨域模式
280
+ cache: 'default', // 默认缓存策略
281
+ },
246
282
  );
247
283
  historyList.value.push({
248
284
  conversation_id: '',
@@ -260,7 +296,7 @@ const chatConv = async (data) => {
260
296
  let buffer = '';
261
297
  // 逐块读取数据
262
298
  while (true) {
263
- const {done, value} = await reader.read();
299
+ const { done, value } = await reader.read();
264
300
  if (done) {
265
301
  console.log('Stream has ended.');
266
302
  isAnswering.value = false;
@@ -270,14 +306,14 @@ const chatConv = async (data) => {
270
306
  }
271
307
 
272
308
  // 解码数据块
273
- const chunk = decoder.decode(value, {stream: true});
309
+ const chunk = decoder.decode(value, { stream: true });
274
310
  buffer += chunk;
275
311
 
276
312
  // 处理数据块
277
313
  const lines = buffer.split('\n');
278
314
  buffer = lines.pop(); // 保留未处理的片段
279
315
 
280
- lines.forEach(line => {
316
+ lines.forEach((line) => {
281
317
  if (line.trim().length > 0 && line.trim().startsWith('data:')) {
282
318
  const str = line.replace(/^data:\s*/, '');
283
319
  const strObj = JSON.parse(str);
@@ -290,22 +326,28 @@ const chatConv = async (data) => {
290
326
  if (!historyList.value[idx].conversation_id && strObj.conversation_id) {
291
327
  historyList.value[idx].conversation_id = strObj.conversation_id;
292
328
  }
293
- if (strObj.hasOwnProperty('content')
294
- && strObj.hasOwnProperty('content_type')
295
- && strObj.content_type === 'text'
296
- && strObj.hasOwnProperty('type')
297
- && strObj.type === 'answer'
298
- && !strObj.hasOwnProperty('created_at')) {
299
- historyList.value[idx].content = handleText(historyList.value[idx].content + strObj.content);
300
- } else if (strObj.hasOwnProperty('type')
301
- && strObj.type === 'tool_response') {
329
+ if (
330
+ strObj.hasOwnProperty('content') &&
331
+ strObj.hasOwnProperty('content_type') &&
332
+ strObj.content_type === 'text' &&
333
+ strObj.hasOwnProperty('type') &&
334
+ strObj.type === 'answer' &&
335
+ !strObj.hasOwnProperty('created_at')
336
+ ) {
337
+ historyList.value[idx].content = handleText(
338
+ historyList.value[idx].content + strObj.content,
339
+ );
340
+ } else if (
341
+ strObj.hasOwnProperty('type') &&
342
+ strObj.type === 'tool_response'
343
+ ) {
302
344
  const extraObj = JSON.parse(strObj.content);
303
345
  if (extraObj.hasOwnProperty('showType')) {
304
- historyList.value[idx].extra.push({...extraObj});
346
+ historyList.value[idx].extra.push({ ...extraObj });
305
347
  } else if (extraObj.hasOwnProperty('response_for_model')) {
306
348
  const modelObj = JSON.parse(extraObj.response_for_model);
307
349
  if (modelObj.hasOwnProperty('showType')) {
308
- historyList.value[idx].extra.push({...modelObj});
350
+ historyList.value[idx].extra.push({ ...modelObj });
309
351
  }
310
352
  }
311
353
  }
@@ -317,14 +359,14 @@ const chatConv = async (data) => {
317
359
 
318
360
  const queryHistoryList = async () => {
319
361
  const res = await post(
320
- `https://api.coze.cn/v1/conversation/message/list?conversation_id=${conversationId.value}`,
321
- {order: 'asc'},
322
- {...chatOptions.value},
362
+ `https://api.coze.cn/v1/conversation/message/list?conversation_id=${conversationId.value}`,
363
+ { order: 'asc' },
364
+ { ...chatOptions.value },
323
365
  );
324
366
  if (res.code === 0 && res.data && res.data.length) {
325
367
  historyList.value = [];
326
368
  const cardList = [];
327
- res.data.forEach(row => {
369
+ res.data.forEach((row) => {
328
370
  if (row.hasOwnProperty('content_type')) {
329
371
  if (row.content_type === 'text') {
330
372
  historyList.value.push({
@@ -345,7 +387,7 @@ const queryHistoryList = async () => {
345
387
  conversation_id: row.conversation_id,
346
388
  bot_id: row.bot_id,
347
389
  role: row.role,
348
- extra: [{...cardObj}],
390
+ extra: [{ ...cardObj }],
349
391
  });
350
392
  } else if (cardObj.hasOwnProperty('response_for_model')) {
351
393
  const modelObj = JSON.parse(cardObj.response_for_model);
@@ -355,7 +397,7 @@ const queryHistoryList = async () => {
355
397
  conversation_id: row.conversation_id,
356
398
  bot_id: row.bot_id,
357
399
  role: row.role,
358
- extra: [{...modelObj}],
400
+ extra: [{ ...modelObj }],
359
401
  });
360
402
  }
361
403
  }
@@ -365,8 +407,10 @@ const queryHistoryList = async () => {
365
407
  }
366
408
  }
367
409
  });
368
- cardList.forEach(card => {
369
- const idx = historyList.value.findIndex(c => c.chat_id === card.chat_id && c.role === 'assistant');
410
+ cardList.forEach((card) => {
411
+ const idx = historyList.value.findIndex(
412
+ (c) => c.chat_id === card.chat_id && c.role === 'assistant',
413
+ );
370
414
  if (idx > -1) {
371
415
  historyList.value[idx].extra = [...card.extra];
372
416
  }
@@ -377,40 +421,57 @@ const queryHistoryList = async () => {
377
421
 
378
422
  const queryBotInfo = async () => {
379
423
  const res = await get(
380
- `https://api.coze.cn/v1/bot/get_online_info?bot_id=${props.botId}`,
381
- {...chatOptions.value},
424
+ `https://api.coze.cn/v1/bot/get_online_info?bot_id=${props.botId}`,
425
+ { ...chatOptions.value },
382
426
  );
383
427
  console.log(res);
384
428
  if (res.code === 0 && res.data) {
385
- botInfo.value = {...res.data};
429
+ botInfo.value = { ...res.data };
386
430
  }
387
431
  };
388
432
 
389
433
  const handleText = (str) => {
390
434
  // console.log(str);
391
- return str.replaceAll(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/ig, '<a href="$2" target="_blank">[$1]</a>');
435
+ return str.replaceAll(
436
+ /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/gi,
437
+ '<a href="$2" target="_blank">[$1]</a>',
438
+ );
439
+ };
440
+ const handleBack = () => {
441
+ Emits('call', { type: 'back' });
392
442
  };
393
443
 
394
- const handleCardTap = ({type}, info) => {
444
+ const handleTagSel = (info) => {
445
+ switch (info.type) {
446
+ case 'chat':
447
+ chatConv({ code: info.msg, text: info.msg });
448
+ break;
449
+ case 'call':
450
+ Emits('call', { ...info });
451
+ break;
452
+ }
453
+ };
454
+
455
+ const handleCardTap = ({ type }, info) => {
395
456
  switch (type) {
396
457
  case 'change':
397
- chatConv({code: '換一套菜品', text: '換一套菜品'});
458
+ chatConv({ code: '換一套菜品', text: '換一套菜品' });
398
459
  break;
399
460
  case 'match':
400
- chatConv({code: '請用以上方案為我配餐', text: '請用以上方案為我配餐'});
461
+ chatConv({ code: '請用以上方案為我配餐', text: '請用以上方案為我配餐' });
401
462
  break;
402
463
  default:
403
- Emits('call', {type, info});
464
+ Emits('call', { type, info });
404
465
  }
405
466
  };
406
467
 
407
468
  const scrollToEnd = () => {
408
- // nextTick(() => {
409
- // endTarget.value.scrollIntoView();
410
- // });
469
+ nextTick(() => {
470
+ endTarget.value.scrollIntoView();
471
+ });
411
472
  };
412
473
  </script>
413
474
 
414
475
  <style scoped lang="less">
415
- @import "./ChatUi";
476
+ @import './ChatUi';
416
477
  </style>
@@ -1,8 +1,11 @@
1
1
  @border-color: rgba(68, 83, 130, 0.25);
2
- @border-radius: 8px;
2
+ @border-radius: 10px;
3
3
  @box-max: 290px;
4
4
  @max-width: 630px;
5
- @bg-color: #F3F4F5;
5
+ @bg-color: #fff;
6
+ @sys-bg: #f3f4f5;
7
+ @user-bg: #28d465;
8
+ @primary-color: #039938;
6
9
 
7
10
  .flexr {
8
11
  display: flex;
@@ -3,28 +3,27 @@
3
3
  <img class="img" :src="info.primaryImgUrl" alt="img" />
4
4
  <div>
5
5
  <div class="name">
6
- <span>{{info.skuname}}</span>
6
+ <span>{{ info.skuname }}</span>
7
7
  <span>×1</span>
8
8
  </div>
9
- <div class="sub">{{subStr}}</div>
9
+ <div class="sub">{{ subStr }}</div>
10
10
  </div>
11
11
  </div>
12
12
  </template>
13
13
 
14
14
  <script setup>
15
- import {computed} from 'vue';
15
+ import { computed } from 'vue';
16
16
 
17
17
  const props = defineProps({
18
18
  info: {
19
19
  type: Object,
20
- required: true
21
- }
22
- })
20
+ required: true,
21
+ },
22
+ });
23
23
 
24
24
  const subStr = computed(() => {
25
- return `热量${props.info.energy}kcal,蛋白质${props.info.protein}g,脂肪${props.info.fat}g,碳水${props.info.carbonwater}g`
26
- })
27
-
25
+ return `热量${props.info.energy}kcal,蛋白质${props.info.protein}g,脂肪${props.info.fat}g,碳水${props.info.carbonwater}g`;
26
+ });
28
27
  </script>
29
28
 
30
29
  <style scoped lang="less">
@@ -5,18 +5,24 @@
5
5
  <p class="title">{{ cate.categoryType }}</p>
6
6
  <div>
7
7
  <template v-for="dList in cate.list" :key="dList.id">
8
- <dishes-card v-for="(info, idx) in dList.getCategoryList" :key="idx" :info="info"/>
8
+ <dishes-card
9
+ v-for="(info, idx) in dList.getCategoryList"
10
+ :key="idx"
11
+ :info="info"
12
+ />
9
13
  </template>
10
14
  </div>
11
15
  </div>
12
16
  </template>
13
17
  <div class="btn_group">
14
- <div class="btn btn_1" @click.stop="handleBtn('ship_order')">配送下单</div>
15
- <div class="btn btn_2" @click.stop="handleBtn('pick_order')">自取下单</div>
18
+ <div class="btn btn_1" @click.stop="handleBtn('ship_order')">
19
+ 配送下单
20
+ </div>
21
+ <div class="btn btn_2" @click.stop="handleBtn('pick_order')">
22
+ 自取下单
23
+ </div>
16
24
  </div>
17
25
  </div>
18
-
19
-
20
26
  </template>
21
27
 
22
28
  <script setup>
@@ -26,14 +32,13 @@ const props = defineProps({
26
32
  skuList: {
27
33
  type: Array,
28
34
  required: true,
29
- default: () => ([]),
35
+ default: () => [],
30
36
  },
31
37
  });
32
38
  const Emits = defineEmits(['select']);
33
39
 
34
-
35
40
  const handleBtn = (type) => {
36
- Emits('select', {type});
41
+ Emits('select', { type });
37
42
  };
38
43
  </script>
39
44
 
@@ -55,7 +60,7 @@ const handleBtn = (type) => {
55
60
  width: 100%;
56
61
  height: 38px;
57
62
  line-height: 38px;
58
- background-color: #E6F5EB;
63
+ background-color: #e6f5eb;
59
64
  border-radius: 19px;
60
65
  text-align: center;
61
66
  font-weight: 600;
@@ -76,5 +81,4 @@ const handleBtn = (type) => {
76
81
  }
77
82
  }
78
83
  }
79
-
80
84
  </style>