st-comp 0.0.252 → 0.0.254

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "st-comp",
3
3
  "public": true,
4
- "version": "0.0.252",
4
+ "version": "0.0.254",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
@@ -1,10 +1,13 @@
1
1
  <script setup>
2
2
  import dayjs from "dayjs";
3
3
  import { ElMessage } from "element-plus";
4
- import { ref, nextTick, watch, onMounted } from "vue";
4
+ import { getUserData } from "st-func";
5
+ import { inject, ref, nextTick, watch, reactive } from "vue";
5
6
  import { sendToBaiLianAppStreaming } from "../../public/aiTools";
6
7
  import { UserFilled, Service, Promotion } from "@element-plus/icons-vue";
7
8
 
9
+ const stConfig = inject("stConfig");
10
+ const userData = reactive(getUserData());
8
11
  const emit = defineEmits(["callBack"]);
9
12
  const props = defineProps({
10
13
  defaultMessage: {
@@ -14,114 +17,211 @@ const props = defineProps({
14
17
  });
15
18
 
16
19
  const visible = ref(false);
17
- const isSending = ref(false); // 发送按钮
18
- const isThinking = ref(false); // AI思考状态
20
+ const isSending = ref(false);
21
+ const isThinking = ref(false);
19
22
 
20
- // 消息列表
21
23
  const messageListRef = ref(null);
22
- const messages = ref([]);
24
+ const messages = ref([
25
+ {
26
+ role: "assistant", // AI-assistant, 用户-user
27
+ content: props.defaultMessage,
28
+ userContent: null,
29
+ showFeedback: false, // 是否展示反馈按钮(默认信息不用展示)
30
+ hasFeedback: false, // 是否已进行过反馈
31
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"), // 消息发起时间
32
+ resTime: 0, // 响应总耗时
33
+ firstPackageTime: 0, // 首包响应耗时
34
+ },
35
+ ]);
23
36
  const inputMessage = ref("");
24
37
 
25
- // 当前正在接收的AI消息(用于流式更新)
26
- const currentAssistantMessage = ref(null);
27
- const currentAssistantIndex = ref(-1);
38
+ // 反馈弹窗相关
39
+ const feedbackDialogVisible = ref(false);
40
+ const feedbackContent = ref("");
41
+ const feedbackMessageIndex = ref({}); // 当前点击要反馈的message的索引
42
+
43
+ // 反馈弹窗相关函数处理
44
+ const handleFeedbackAction = async (action, messageIndex) => {
45
+ switch (action) {
46
+ // 窗口: 提交(满意)
47
+ case "satisfied": {
48
+ const message = messages.value[messageIndex];
49
+ const params = {
50
+ userName: userData.username,
51
+ userContent: message.userContent,
52
+ aiContent: message.content,
53
+ type: 1,
54
+ createTime: message.createTime,
55
+ resTime: message.resTime,
56
+ firstPackageTime: message.firstPackageTime,
57
+ };
58
+ await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", params);
59
+ ElMessage.success("感谢您的评价!");
60
+ messages.value[messageIndex].hasFeedback = true;
61
+ break;
62
+ }
63
+ // 窗口: 打开
64
+ case "open": {
65
+ feedbackMessageIndex.value = messageIndex;
66
+ feedbackDialogVisible.value = true;
67
+ break;
68
+ }
69
+ // 窗口: 提交(不满意)
70
+ case "unsatisfied": {
71
+ const message = messages.value[messageIndex];
72
+ const params = {
73
+ userName: userData.username,
74
+ userContent: message.userContent,
75
+ userSuggestion: feedbackContent.value,
76
+ aiContent: message.content,
77
+ type: 2,
78
+ resTime: message.resTime,
79
+ createTime: message.createTime,
80
+ firstPackageTime: message.firstPackageTime
81
+ };
82
+ await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", params);
83
+ ElMessage.success("感谢您的反馈!我们将持续跟踪并进行优化");
84
+ feedbackDialogVisible.value = false;
85
+ messages.value[messageIndex].hasFeedback = true;
86
+ break;
87
+ }
88
+ // 自动提交记录跟踪日志
89
+ case "default": {
90
+ const message = messages.value[messageIndex];
91
+ const params = {
92
+ userName: userData.username,
93
+ userContent: message.userContent,
94
+ userSuggestion: feedbackContent.value,
95
+ aiContent: message.content,
96
+ type: null,
97
+ resTime: message.resTime,
98
+ createTime: message.createTime,
99
+ firstPackageTime: message.firstPackageTime
100
+ };
101
+ await stConfig.request.post("/alarm/deliversign/addVarietyAiHelperLog", params);
102
+ }
103
+ }
104
+ };
28
105
 
29
106
  // 发送消息
30
107
  const sendMessage = async () => {
31
- // 校验输入内容是否为空
32
108
  const content = inputMessage.value.trim();
33
109
  if (!content) return ElMessage.warning("请输入消息内容");
34
- // 校验是否正在处理消息
35
110
  if (isSending.value) return;
36
111
 
37
112
  // 记录用户消息
38
113
  messages.value.push({
39
114
  role: "user",
40
- time: dayjs().format("HH:mm"),
41
115
  content: content,
116
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
42
117
  });
43
118
  inputMessage.value = "";
44
119
  await scrollToBottom();
45
120
 
46
- // 创建一个临时的AI消息占位符
47
- const assistantMessage = {
121
+ // 创建AI消息占位符,记录对应的用户输入
122
+ messages.value.push({
48
123
  role: "assistant",
49
- time: dayjs().format("HH:mm"),
50
124
  content: "",
51
- };
52
- messages.value.push(assistantMessage);
53
- currentAssistantIndex.value = messages.value.length - 1;
54
- currentAssistantMessage.value = assistantMessage;
125
+ userContent: content,
126
+ showFeedback: false,
127
+ hasFeedback: false,
128
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
129
+ resTime: 0,
130
+ firstPackageTime: 0,
131
+ });
55
132
  await scrollToBottom();
56
133
 
57
- // 发送请求至百炼应用AI(流式)
58
134
  isSending.value = true;
59
135
  isThinking.value = true;
60
-
136
+
61
137
  let fullResponse = "";
62
-
138
+ let resTime = new Date().getTime();
63
139
  try {
64
140
  const appId = "9e54d112acfe4531bd1fc4fee8827fef";
65
141
  const apiKey = "sk-d995eb26a4334bdeb2ccb4cbfaf51de8";
66
-
67
142
  await sendToBaiLianAppStreaming({
68
143
  appId,
69
144
  apiKey,
70
145
  value: content,
71
146
  callback: (type, data) => {
72
147
  if (type === "message") {
73
- // 实时更新消息内容
74
148
  fullResponse += data;
75
- if (currentAssistantMessage.value) {
76
- currentAssistantMessage.value.content = fullResponse;
77
- // 实时滚动到底部
149
+ // 直接更新最后一条AI消息
150
+ const lastMessage = messages.value[messages.value.length - 1];
151
+ if (lastMessage && lastMessage.role === "assistant") {
152
+ lastMessage.content = fullResponse;
78
153
  scrollToBottom();
79
154
  }
155
+ // 记录首包响应耗时
156
+ if (lastMessage.firstPackageTime === 0) {
157
+ lastMessage.firstPackageTime = new Date().getTime() - resTime;
158
+ }
80
159
  } else if (type === "finish") {
81
- // 流式传输完成
82
160
  isThinking.value = false;
83
161
  isSending.value = false;
84
- console.log(fullResponse)
162
+
163
+ // 显示反馈按钮
164
+ const lastMessage = messages.value[messages.value.length - 1];
165
+ if (lastMessage && lastMessage.role === "assistant") {
166
+ lastMessage.showFeedback = true;
167
+ lastMessage.resTime = new Date().getTime() - resTime;
168
+ handleFeedbackAction("default", messages.value.length - 1);
169
+ }
170
+
85
171
  // 触发回调
86
172
  try {
87
- // 尝试解析完整的响应为JSON
88
173
  const jsonResponse = JSON.parse(fullResponse);
89
174
  emit("callBack", jsonResponse);
90
175
  } catch (error) {
91
- // 如果不是JSON格式,直接返回文本
92
176
  emit("callBack", fullResponse);
93
177
  }
94
-
95
- // 清空当前消息引用
96
- currentAssistantMessage.value = null;
97
- currentAssistantIndex.value = -1;
98
178
  }
99
- }
179
+ },
100
180
  });
101
181
  } catch (error) {
102
182
  ElMessage.error(`AI响应异常: ${error}`);
103
- console.error("AI响应异常:", error);
104
-
105
- // 如果出错,移除占位消息并显示错误
106
- if (currentAssistantIndex.value !== -1) {
107
- messages.value.splice(currentAssistantIndex.value, 1);
108
- currentAssistantMessage.value = null;
109
- currentAssistantIndex.value = -1;
110
- }
111
-
183
+ // 移除占位的AI消息
184
+ messages.value.pop();
112
185
  // 添加错误提示消息
113
186
  messages.value.push({
114
187
  role: "assistant",
115
- time: dayjs().format("HH:mm"),
188
+ userContent: content,
116
189
  content: "抱歉,AI服务响应异常,请稍后重试。",
190
+ showFeedback: true,
191
+ hasFeedback: false,
192
+ createTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
193
+ resTime: new Date().getTime() - resTime,
194
+ firstPackageTime: 0,
117
195
  });
196
+ handleFeedbackAction("default", messages.value.length - 1);
118
197
  await scrollToBottom();
119
-
120
198
  isSending.value = false;
121
199
  isThinking.value = false;
122
200
  }
123
201
  };
124
202
 
203
+ // 处理键盘事件:Ctrl+Enter 换行,Enter 发送
204
+ const handleKeydown = (event) => {
205
+ if (event.key === 'Enter') {
206
+ if (event.ctrlKey) {
207
+ // Ctrl + Enter: 插入换行符
208
+ event.preventDefault();
209
+ const textarea = event.target;
210
+ const start = textarea.selectionStart;
211
+ const end = textarea.selectionEnd;
212
+ inputMessage.value = inputMessage.value.substring(0, start) + '\n' + inputMessage.value.substring(end);
213
+ // 将光标移动到新插入的换行符之后
214
+ nextTick(() => {
215
+ textarea.selectionStart = textarea.selectionEnd = start + 1;
216
+ });
217
+ } else {
218
+ // 单独的 Enter: 发送消息
219
+ event.preventDefault();
220
+ sendMessage();
221
+ }
222
+ }
223
+ };
224
+
125
225
  // 滚动到底部
126
226
  const scrollToBottom = async () => {
127
227
  await nextTick();
@@ -130,14 +230,6 @@ const scrollToBottom = async () => {
130
230
  }
131
231
  };
132
232
 
133
- onMounted(() => {
134
- messages.value.push({
135
- role: "assistant",
136
- time: dayjs().format("HH:mm"),
137
- content: props.defaultMessage,
138
- });
139
- });
140
-
141
233
  watch(
142
234
  () => messages.value,
143
235
  () => {
@@ -145,11 +237,9 @@ watch(
145
237
  },
146
238
  { deep: true },
147
239
  );
148
-
149
240
  defineExpose({
150
- open: (data) => {
241
+ open: () => {
151
242
  visible.value = true;
152
- // 自动滚动到底部
153
243
  nextTick(() => {
154
244
  scrollToBottom();
155
245
  });
@@ -170,7 +260,6 @@ defineExpose({
170
260
  :modal-penetrable="true"
171
261
  >
172
262
  <div class="chat-container">
173
- <!-- 消息列表 -->
174
263
  <div
175
264
  ref="messageListRef"
176
265
  class="message-list"
@@ -178,26 +267,54 @@ defineExpose({
178
267
  <div
179
268
  v-for="(message, index) in messages"
180
269
  :key="index"
181
- class="message-item"
182
- :class="message.role"
270
+ class="message-item-wrapper"
183
271
  >
184
- <template v-if="message.content">
185
- <div class="avatar">
186
- <el-avatar
187
- :size="32"
188
- :icon="message.role === 'user' ? UserFilled : Service"
189
- />
190
- </div>
191
- <div class="message-content">
192
- <div class="message-text">{{ message.content }}</div>
193
- <div class="message-time">{{ message.time }}</div>
194
- </div>
195
- </template>
272
+ <div
273
+ class="message-item"
274
+ :class="message.role"
275
+ >
276
+ <template v-if="message.content">
277
+ <div class="avatar">
278
+ <el-avatar
279
+ :size="32"
280
+ :icon="message.role === 'user' ? UserFilled : Service"
281
+ />
282
+ </div>
283
+ <div class="message-content">
284
+ <div class="message-text">{{ message.content }}</div>
285
+ <div class="message-createTime">{{ message.createTime }}</div>
286
+ <!-- 反馈按钮(仅AI侧展示) -->
287
+ <template v-if="message.role === 'assistant'">
288
+ <template v-if="message.showFeedback && !message.hasFeedback">
289
+ <div class="message-createTime">请问对本轮查询结果是否满意?</div>
290
+ <div class="feedback-buttons">
291
+ <button
292
+ class="feedback-btn satisfied-btn"
293
+ @click="handleFeedbackAction('satisfied', index)"
294
+ >
295
+ <span class="btn-emoji">👍</span>
296
+ <span class="btn-text">满意</span>
297
+ </button>
298
+ <button
299
+ class="feedback-btn unsatisfied-btn"
300
+ @click="handleFeedbackAction('open', index)"
301
+ >
302
+ <span class="btn-emoji">👎</span>
303
+ <span class="btn-text">不满意</span>
304
+ </button>
305
+ </div>
306
+ </template>
307
+ <template v-if="message.showFeedback && message.hasFeedback">
308
+ <div class="message-createTime">感谢您进行的评价反馈</div>
309
+ </template>
310
+ </template>
311
+ </div>
312
+ </template>
313
+ </div>
196
314
  </div>
197
315
 
198
- <!-- AI 思考状态(仅在未开始接收流式消息时显示) -->
199
316
  <div
200
- v-if="isThinking && !currentAssistantMessage?.content"
317
+ v-if="isThinking && !messages[messages.length - 1]?.content"
201
318
  class="message-item assistant"
202
319
  >
203
320
  <div class="avatar">
@@ -215,7 +332,7 @@ defineExpose({
215
332
  </div>
216
333
  </div>
217
334
  </div>
218
- <!-- 输入区域 -->
335
+
219
336
  <div class="input-area">
220
337
  <el-input
221
338
  class="message-input"
@@ -223,12 +340,12 @@ defineExpose({
223
340
  type="textarea"
224
341
  :rows="4"
225
342
  :autosize="{ minRows: 2, maxRows: 4 }"
226
- placeholder="输入您想查询的品种条件..."
227
- @keydown.ctrl.enter="sendMessage"
343
+ placeholder="输入您想查询的品种条件... (Ctrl+Enter换行,Enter发送)"
344
+ @keydown="handleKeydown"
228
345
  />
229
346
  <div class="input-actions">
230
347
  <div class="input-hint">
231
- <span>Ctrl + Enter 发送</span>
348
+ <span>Enter 发送 | Ctrl + Enter 换行</span>
232
349
  </div>
233
350
  <el-button
234
351
  class="send-btn"
@@ -244,9 +361,38 @@ defineExpose({
244
361
  </div>
245
362
  </div>
246
363
  </el-dialog>
364
+
365
+ <!-- 窗口: 反馈意见 -->
366
+ <el-dialog
367
+ v-model="feedbackDialogVisible"
368
+ title="📝 反馈意见"
369
+ width="400px"
370
+ >
371
+ <div class="feedback-dialog-content">
372
+ <div class="feedback-emoji">😟</div>
373
+ <p class="feedback-tip">很抱歉没能帮到您,请告诉我们哪里需要改进:</p>
374
+ <el-input
375
+ v-model="feedbackContent"
376
+ type="textarea"
377
+ :rows="4"
378
+ placeholder="[非必填]例如:回答不够准确、查询结果有误、界面体验不佳..."
379
+ />
380
+ </div>
381
+ <template #footer>
382
+ <span class="dialog-footer">
383
+ <el-button @click="feedbackDialogVisible = false">取消</el-button>
384
+ <el-button
385
+ type="primary"
386
+ @click="handleFeedbackAction('unsatisfied', feedbackMessageIndex)"
387
+ >提交反馈</el-button
388
+ >
389
+ </span>
390
+ </template>
391
+ </el-dialog>
247
392
  </template>
248
393
 
249
394
  <style lang="scss" scoped>
395
+ /* 样式保持不变,和之前一样 */
250
396
  .ai-dialog {
251
397
  :deep(.el-dialog) {
252
398
  border-radius: 24px;
@@ -280,12 +426,14 @@ defineExpose({
280
426
  }
281
427
  }
282
428
  }
429
+
283
430
  .chat-container {
284
431
  display: flex;
285
432
  flex-direction: column;
286
- height: 380px;
433
+ height: 480px;
287
434
  background: transparent;
288
435
  }
436
+
289
437
  .message-list {
290
438
  flex: 1;
291
439
  overflow-y: auto;
@@ -310,9 +458,13 @@ defineExpose({
310
458
  }
311
459
  }
312
460
  }
461
+
462
+ .message-item-wrapper {
463
+ margin-bottom: 20px;
464
+ }
465
+
313
466
  .message-item {
314
467
  display: flex;
315
- margin-bottom: 20px;
316
468
  animation: fadeInUp 0.3s ease-out;
317
469
 
318
470
  &.user {
@@ -332,7 +484,7 @@ defineExpose({
332
484
  border-radius: 18px 18px 4px 18px;
333
485
  }
334
486
 
335
- .message-time {
487
+ .message-createTime {
336
488
  text-align: right;
337
489
  }
338
490
  }
@@ -383,14 +535,80 @@ defineExpose({
383
535
  white-space: pre-wrap;
384
536
  }
385
537
 
386
- .message-time {
538
+ .message-createTime {
387
539
  font-size: 11px;
388
540
  color: #9ca3af;
389
541
  margin-top: 6px;
390
542
  padding: 0 4px;
391
543
  }
544
+
545
+ .feedback-buttons {
546
+ display: flex;
547
+ gap: 12px;
548
+ margin-top: 12px;
549
+ .feedback-btn {
550
+ display: flex;
551
+ align-items: center;
552
+ gap: 6px;
553
+ padding: 4px 10px;
554
+ border: none;
555
+ border-radius: 20px;
556
+ font-size: 13px;
557
+ font-weight: 500;
558
+ cursor: pointer;
559
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
560
+ background: rgba(255, 255, 255, 0.9);
561
+ backdrop-filter: blur(10px);
562
+
563
+ .btn-emoji {
564
+ font-size: 16px;
565
+ transition: transform 0.2s ease;
566
+ }
567
+
568
+ .btn-text {
569
+ font-size: 13px;
570
+ }
571
+
572
+ &:hover {
573
+ transform: translateY(-2px);
574
+
575
+ .btn-emoji {
576
+ transform: scale(1.1);
577
+ }
578
+ }
579
+
580
+ &:active {
581
+ transform: translateY(0);
582
+ }
583
+ }
584
+ .satisfied-btn {
585
+ background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
586
+ color: #1890ff;
587
+ border: 1px solid rgba(24, 144, 255, 0.2);
588
+ box-shadow: 0 2px 8px rgba(24, 144, 255, 0.1);
589
+
590
+ &:hover {
591
+ background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
592
+ border-color: #1890ff;
593
+ box-shadow: 0 4px 12px rgba(24, 144, 255, 0.2);
594
+ }
595
+ }
596
+ .unsatisfied-btn {
597
+ background: linear-gradient(135deg, #fff1f0 0%, #ffe7e5 100%);
598
+ color: #ff4d4f;
599
+ border: 1px solid rgba(255, 77, 79, 0.2);
600
+ box-shadow: 0 2px 8px rgba(255, 77, 79, 0.1);
601
+
602
+ &:hover {
603
+ background: linear-gradient(135deg, #ffe7e5 0%, #ffccc7 100%);
604
+ border-color: #ff4d4f;
605
+ box-shadow: 0 4px 12px rgba(255, 77, 79, 0.2);
606
+ }
607
+ }
608
+ }
392
609
  }
393
610
  }
611
+
394
612
  .typing-indicator {
395
613
  display: flex;
396
614
  gap: 4px;
@@ -416,6 +634,7 @@ defineExpose({
416
634
  }
417
635
  }
418
636
  }
637
+
419
638
  .input-area {
420
639
  padding: 16px 24px 24px;
421
640
  border-top: 1px solid rgba(102, 126, 234, 0.1);
@@ -478,6 +697,36 @@ defineExpose({
478
697
  }
479
698
  }
480
699
  }
700
+
701
+ .feedback-dialog-content {
702
+ text-align: center;
703
+ padding: 12px 0;
704
+
705
+ .feedback-emoji {
706
+ font-size: 48px;
707
+ margin-bottom: 16px;
708
+ animation: shake 0.5s ease-in-out;
709
+ }
710
+
711
+ .feedback-tip {
712
+ font-size: 14px;
713
+ color: #666;
714
+ margin-bottom: 20px;
715
+ line-height: 1.6;
716
+ }
717
+
718
+ :deep(.el-textarea__inner) {
719
+ border-radius: 12px;
720
+ border: 1px solid rgba(102, 126, 234, 0.2);
721
+ font-size: 14px;
722
+
723
+ &:focus {
724
+ border-color: #667eea;
725
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
726
+ }
727
+ }
728
+ }
729
+
481
730
  @keyframes fadeInUp {
482
731
  from {
483
732
  opacity: 0;
@@ -500,4 +749,16 @@ defineExpose({
500
749
  opacity: 1;
501
750
  }
502
751
  }
752
+ @keyframes shake {
753
+ 0%,
754
+ 100% {
755
+ transform: translateX(0);
756
+ }
757
+ 25% {
758
+ transform: translateX(-5px);
759
+ }
760
+ 75% {
761
+ transform: translateX(5px);
762
+ }
763
+ }
503
764
  </style>
@@ -112,10 +112,14 @@ const actionState = (...args) => {
112
112
  };
113
113
  // 品种池参数解析助手的回调
114
114
  const varietyAiHelperCallBack = (data) => {
115
- // 重置
116
- varietySearchRef.value.reset()
117
- // 参数合并
118
- varietySearchData.value = { ...varietySearchData.value, ...data };
115
+ if (typeof data === "string") {
116
+ return;
117
+ } else {
118
+ // 重置
119
+ varietySearchRef.value.reset();
120
+ // 参数合并
121
+ varietySearchData.value = { ...varietySearchData.value, ...data };
122
+ }
119
123
  };
120
124
 
121
125
  onMounted(async () => {
@@ -226,9 +230,9 @@ watch(
226
230
  @actionState="actionState"
227
231
  />
228
232
  <div>
229
- <div style="font-size: 14px; display: flex;">
230
- <pre style="flex: 1;">前端组件参数: {{ varietySearchData }}</pre>
231
- <pre style="flex: 0.5;">后端接口参数: {{ apiParams }}</pre>
233
+ <div style="font-size: 14px; display: flex">
234
+ <pre style="flex: 1">前端组件参数: {{ varietySearchData }}</pre>
235
+ <pre style="flex: 0.5">后端接口参数: {{ apiParams }}</pre>
232
236
  </div>
233
237
  <el-table
234
238
  ref="multipleTableRef"