CustomExec.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. <script setup lang="ts">
  2. import { ref } from 'vue';
  3. import { v4 as uuidv4 } from 'uuid';
  4. import { ChatbubblesOutline, NavigateOutline, RefreshOutline } from '@vicons/ionicons5';
  5. import { exec as flowExec } from '@/api/aigc/flow';
  6. import { useNotification } from 'naive-ui';
  7. import { useRouter } from 'vue-router';
  8. const emits = defineEmits(['focus-active']);
  9. const middleRef = ref();
  10. const router = useRouter();
  11. const notification = useNotification();
  12. const content = ref('');
  13. const loading = ref(false);
  14. function handleFocus() {
  15. emits('focus-active');
  16. }
  17. const messages = ref<
  18. {
  19. id: string;
  20. role: 'human' | 'ai';
  21. content: string;
  22. type: 'q' | 'a' | 'loading' | 'error';
  23. }[]
  24. >([]);
  25. async function handleSubmit() {
  26. loading.value = true;
  27. try {
  28. let id = uuidv4();
  29. messages.value.push(
  30. {
  31. id: uuidv4(),
  32. role: 'human',
  33. content: content.value,
  34. type: 'q',
  35. },
  36. {
  37. id: id,
  38. role: 'ai',
  39. content: '',
  40. type: 'loading',
  41. }
  42. );
  43. const items = messages.value.filter((i) => i.id == id);
  44. await flowExec(
  45. String(router.currentRoute.value.params.id),
  46. {
  47. content: content.value,
  48. },
  49. ({ event }) => {
  50. console.log('response', event);
  51. const { responseText } = event.target;
  52. try {
  53. const data = JSON.parse(responseText);
  54. notification.error({
  55. duration: 5000,
  56. content: data.message,
  57. meta: '请检查Flow流程设计。',
  58. });
  59. items[0].type = 'error';
  60. items[0].content = 'Error! ';
  61. } catch (e) {
  62. items[0].content = responseText;
  63. items[0].type = 'a';
  64. }
  65. scrollToBottom();
  66. }
  67. );
  68. content.value = '';
  69. loading.value = false;
  70. } finally {
  71. loading.value = false;
  72. scrollToBottom();
  73. }
  74. }
  75. const scrollToBottom = () => {
  76. const middleElement = middleRef.value;
  77. if (middleElement) {
  78. middleElement.scrollTop = middleElement.scrollHeight;
  79. }
  80. };
  81. function handleEnter(event: KeyboardEvent) {
  82. if (event.key === 'Enter' && event.ctrlKey) {
  83. } else if (event.key === 'Enter') {
  84. event.preventDefault();
  85. handleSubmit();
  86. }
  87. }
  88. </script>
  89. <template>
  90. <div class="container relative h-full card-shadow rounded-xl mb-2">
  91. <div class="top absolute top-0 left-0 w-full h-10 z-10 border-b border-1">
  92. <div class="w-full flex justify-between items-center p-2 absolute">
  93. <div>
  94. <n-badge type="success" dot />
  95. <div class="inline-block ml-2">会话测试</div>
  96. </div>
  97. <n-button text class="mr-2">
  98. <n-icon size="16" color="#18a058">
  99. <RefreshOutline />
  100. </n-icon>
  101. </n-button>
  102. </div>
  103. </div>
  104. <div
  105. ref="middleRef"
  106. class="middle absolute top-10 left-0 w-full bottom-[65px] z-0 overflow-y-auto"
  107. >
  108. <div v-if="messages.length == 0" class="flex-1 flex mt-5 h-full justify-center">
  109. <div class="w-1/2 flex flex-col justify-center text-xs items-center gap-2">
  110. <n-icon size="40" color="#e4e4e7">
  111. <ChatbubblesOutline />
  112. </n-icon>
  113. <div class="text-[#69696b]">输入内容开始测试会话...</div>
  114. </div>
  115. </div>
  116. <div v-else class="flex-1 overflow-y-auto mb-1">
  117. <div class="h-full w-full flex flex-col space-y-3 relative p-2 pl-4 pr-4 mt-2">
  118. <template v-for="item in messages" :key="item">
  119. <div
  120. v-if="item.role == 'human'"
  121. class="flex justify-end p-1.5 rounded select-text self-end"
  122. style="background: #d2f9d1"
  123. >{{ item.content }}
  124. </div>
  125. <div
  126. v-if="item.role == 'ai'"
  127. class="flex justify-start items-center rounded self-start min-w-[40px] min-h-[33px]"
  128. :style="
  129. item.type === 'error' ? 'color: #d03050;background:#d0305029' : 'background:#f4f6f8'
  130. "
  131. >
  132. <div v-if="item.type === 'loading'" class="flex justify-center items-center w-[55px]">
  133. <span class="dot-pulse"></span>
  134. </div>
  135. <div class="p-1.5" v-else>{{ item.content }}</div>
  136. </div>
  137. </template>
  138. </div>
  139. </div>
  140. </div>
  141. <div class="bottom absolute bottom-0 left-0 w-full h-[60px] z-10">
  142. <div class="pl-5 pr-5 flex justify-center items-center space-x-2 w-full">
  143. <n-input
  144. @focus="handleFocus"
  145. v-model:value="content"
  146. type="textarea"
  147. size="small"
  148. class="w-full rounded-lg text-xs"
  149. :autosize="{ minRows: 2, maxRows: 5 }"
  150. :disabled="loading"
  151. @keypress="handleEnter"
  152. >
  153. <template #suffix>
  154. <n-button @click="handleSubmit" :loading="loading" size="small" text>
  155. <template #icon>
  156. <n-icon color="#18a058">
  157. <NavigateOutline />
  158. </n-icon>
  159. </template>
  160. </n-button>
  161. </template>
  162. </n-input>
  163. </div>
  164. </div>
  165. </div>
  166. </template>
  167. <style scoped lang="less"></style>