aiChat.vue 29 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193
  1. <template>
  2. <div class="ai-chat-popup" :style="popupStyle" ref="popupRef">
  3. <div class="fullscreen-spin mask2" v-if="agentlistloading">
  4. <div class="spin">
  5. <div class="spinner"></div>
  6. <div class="text">加载中...</div>
  7. </div>
  8. </div>
  9. <!-- 弹窗头部 -->
  10. <div class="ai-chat-header">
  11. <h3>{{ lang.ssClassroomAiAssistant }}</h3>
  12. <button class="close-btn" @click="$emit('close')">
  13. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  14. stroke-linejoin="round">
  15. <line x1="18" y1="6" x2="6" y2="18"></line>
  16. <line x1="6" y1="6" x2="18" y2="18"></line>
  17. </svg>
  18. </button>
  19. </div>
  20. <!-- 聊天区域 v-if="messages.length > 0" -->
  21. <div class="chat-section" ref="chatSection">
  22. <!-- 消息列表 -->
  23. <div v-for="(message, index) in messages" :key="index" class="chat-message">
  24. <div class="message-content user-message chat" v-if="message.content">
  25. <div v-html="message.content"></div>
  26. <!-- 显示上传的文件 -->
  27. <div class="message-files" v-if="message.sourceFiles && message.sourceFiles.length > 0">
  28. <div v-for="(file, index) in message.sourceFiles" :key="index" class="message-file-item">
  29. <span>{{ file.title }}</span>
  30. </div>
  31. </div>
  32. </div>
  33. <div class="message-content ai-message chat" v-if="message.aiContent || message.loading || message?.jsonData?.error || !message.aiContent">
  34. <div v-if="message.aiContent" v-html="message.aiContent"></div>
  35. <svg v-else-if="message.loading" xmlns="http://www.w3.org/2000/svg" width="32" height="32"
  36. viewBox="0 0 24 24"><!-- Icon from SVG Spinners by Utkarsh Verma - https://github.com/n3r4zzurr0/svg-spinners/blob/main/LICENSE -->
  37. <circle cx="4" cy="12" r="3" fill="currentColor">
  38. <animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s"
  39. calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
  40. </circle>
  41. <circle cx="12" cy="12" r="3" fill="currentColor">
  42. <animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline" dur="0.6s"
  43. keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
  44. </circle>
  45. <circle cx="20" cy="12" r="3" fill="currentColor">
  46. <animate id="svgSpinners3DotsBounce1" attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.2s"
  47. calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12" />
  48. </circle>
  49. </svg>
  50. <div v-else-if="message?.jsonData?.error || !message.aiContent" class="error-message">
  51. <div class="error-text">{{ message?.jsonData?.errorMessage || lang.ssRetryMessage }}</div>
  52. <button class="retry-btn" @click="retryMessage(index)">
  53. <IconRefresh class="retry-icon"/>
  54. </button>
  55. </div>
  56. </div>
  57. </div>
  58. </div>
  59. <!-- 输入区域 -->
  60. <div class="input-section">
  61. <div class="input-wrapper">
  62. <div class="file-box" v-show="files.length">
  63. <div v-for="(file, index) in files" :key="index" class="file-item">
  64. <span class="file-name">{{ file.title }}</span>
  65. <button class="remove-file-btn" @click="removeFile(index)">
  66. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  67. stroke-linejoin="round">
  68. <line x1="18" y1="6" x2="6" y2="18"></line>
  69. <line x1="6" y1="6" x2="18" y2="18"></line>
  70. </svg>
  71. </button>
  72. </div>
  73. </div>
  74. <textarea class="ai-input" :placeholder="lang.ssEnterQuestion" v-model="inputText" @keyup.enter.exact="sendMessage"
  75. />
  76. </div>
  77. <div class="input-actions">
  78. <!-- <FileInput accept="*" @change="handleFileUpload" >
  79. <button class="attach-btn">
  80. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
  81. <path
  82. d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48">
  83. </path>
  84. </svg>
  85. </button>
  86. </FileInput> -->
  87. <button class="send-btn" @click="sendMessage" v-if="!chatLoading">
  88. <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
  89. stroke-linejoin="round">
  90. <line x1="22" y1="2" x2="11" y2="13"></line>
  91. <polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
  92. </svg>
  93. </button>
  94. <button class="send-btn stop" @click="stopMessage" v-if="chatLoading">
  95. <svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
  96. <rect width="32" height="32" rx="16" fill="black" fill-opacity="0.4"></rect>
  97. <path
  98. d="M11.3333 12.333C11.3333 11.7807 11.781 11.333 12.3333 11.333H19.6666C20.2189 11.333 20.6666 11.7807 20.6666 12.333V19.6663C20.6666 20.2186 20.2189 20.6663 19.6666 20.6663H12.3333C11.781 20.6663 11.3333 20.2186 11.3333 19.6663V12.333Z"
  99. fill="white" fill-opacity="0.9"></path>
  100. </svg>
  101. </button>
  102. </div>
  103. <!-- 输入时的快捷操作弹出 -->
  104. <div class="quick-actions-popup" v-if="showQuickActions">
  105. <button v-for="(action, index) in quickActions" :key="index" class="quick-action-btn"
  106. @click="sendQuickAction(action)">{{ action }}</button>
  107. </div>
  108. </div>
  109. </div>
  110. </template>
  111. <script lang="ts" setup>
  112. import { ref, onMounted, useTemplateRef, nextTick, watch, computed } from 'vue'
  113. import { chat_no_stream, chat_stream, getAgentModel, chat_no_stream2, getAgentChatList, agentlistloading, insertChat } from '@/tools/aiChat'
  114. import { useSlidesStore } from '@/store'
  115. import { lang } from '@/main'
  116. import MarkdownIt from 'markdown-it'
  117. import { getWorkPageId, getClassById } from '@/services/course'
  118. import FileInput from '@/components/FileInput2.vue'
  119. import axios from '@/services/config'
  120. import message from '@/utils/message'
  121. interface ChatMessage {
  122. uid?: string
  123. role: 'ai' | 'user'
  124. content?: string
  125. aiContent?: string
  126. oldContent?: string
  127. loading?: boolean
  128. chatloading?: boolean
  129. gLoading?: boolean
  130. rawContent?: string
  131. timestamp?: Date
  132. like?: boolean
  133. unlike?: boolean
  134. isTyping?: boolean
  135. AI?: string
  136. isShowSynchronization?: boolean
  137. filename?: string
  138. is_mind_map?: boolean
  139. sourceFiles?: Array<{
  140. title: string
  141. id?: string
  142. url?: string
  143. }>
  144. jsonData?: {
  145. error?: boolean
  146. errorMessage?: string
  147. retryable?: boolean
  148. gType?: string
  149. isGenerate?: boolean
  150. headUrl?: string
  151. assistantName?: string
  152. files?: Array<{
  153. title: string
  154. id?: string
  155. url?: string
  156. }>
  157. sourceArray?: Array<{
  158. text?: string
  159. id?: string
  160. title?: string
  161. }>
  162. }
  163. }
  164. const props = withDefaults(defineProps<{
  165. userid?: string | null
  166. position?: { x: number; y: number }
  167. workJson?: any
  168. visible?: boolean
  169. cid?: string | null
  170. }>(), {
  171. userid: null,
  172. position: () => ({ x: 0, y: 0 }),
  173. workJson: () => ({}),
  174. visible: false,
  175. cid: null
  176. })
  177. const emit = defineEmits(['close'])
  178. const grade = ref('')
  179. watch(() => props.visible, (newVal) => {
  180. if (newVal) {
  181. session_name.value = props.workJson.id || ''
  182. console.log('workJson', props.workJson)
  183. getAgentChatList(props.workJson.id, props.userid || '').then((res) => {
  184. console.log('res', res)
  185. messages.value = res
  186. if (messages.value.length === 0) {
  187. messages.value.push({
  188. role: 'user',
  189. content: '',
  190. })
  191. messages.value.at(-1).loading = true
  192. messages.value.at(-1).chatloading = true
  193. messages.value.at(-1).sourceFiles = files.value.filter(file => file.id !== null).map(file => ({
  194. title: file.title,
  195. id: file.id
  196. }))
  197. chatLoading.value = true
  198. sendAction('')
  199. }
  200. else {
  201. prevChatResult()
  202. }
  203. })
  204. }
  205. })
  206. watch(() => props.cid, (newVal) => {
  207. if (newVal) {
  208. getClassById({
  209. id: newVal
  210. }).then(res => {
  211. console.log('res年级', res)
  212. grade.value = res[0][0].name || ''
  213. })
  214. }
  215. })
  216. // 计算弹窗样式
  217. // 弹窗引用
  218. const popupRef = useTemplateRef<HTMLElement>('popupRef')
  219. // 计算弹窗样式
  220. const popupStyle = computed(() => {
  221. // 获取slideListWrap的尺寸和位置
  222. const slideListWrap = document.querySelector('.slide-list-wrap')
  223. if (!slideListWrap) {
  224. // 如果找不到slideListWrap,使用默认位置
  225. return {
  226. right: `${props.position.x + 10}px`,
  227. bottom: `${props.position.y + 60}px`
  228. }
  229. }
  230. const wrapRect = slideListWrap.getBoundingClientRect()
  231. // 使用实际的弹窗宽高,考虑边距
  232. const popupWidth = popupRef.value?.clientWidth || 320 // 实际弹窗宽度
  233. const popupHeight = popupRef.value?.clientHeight || 400 // 实际弹窗高度
  234. const buttonWidth = 105 // 按钮宽度
  235. const buttonHeight = 50 // 按钮高度
  236. const margin = 10 // 弹窗与屏幕边缘的最小距离
  237. // 计算按钮在slideListWrap内的实际位置
  238. const buttonRight = props.position.x
  239. const buttonBottom = props.position.y
  240. const buttonLeft = wrapRect.width - buttonRight - buttonWidth
  241. const buttonTop = wrapRect.height - buttonBottom - buttonHeight
  242. console.log('按钮位置:', buttonTop, buttonLeft)
  243. console.log('弹窗尺寸:', popupHeight, popupWidth)
  244. // 判断弹窗显示方向
  245. const style: any = {}
  246. // 判断垂直方向
  247. if (buttonTop < popupHeight + margin + buttonHeight) {
  248. // 按钮在上方,弹窗向下显示
  249. style.top = `${wrapRect.top + wrapRect.height - buttonBottom - margin}px`
  250. }
  251. else {
  252. // 按钮在下方,弹窗向上显示
  253. style.bottom = `${(buttonBottom) + buttonHeight + margin}px`
  254. }
  255. // 判断水平方向
  256. if (buttonRight < popupWidth + margin) {
  257. // 按钮在右侧,弹窗向左显示
  258. style.right = `${buttonRight}px`
  259. }
  260. else {
  261. // 按钮在左侧,弹窗向右显示
  262. style.left = `${buttonLeft}px`
  263. }
  264. return style
  265. })
  266. const inputText = ref('')
  267. const messages = ref<ChatMessage[]>([])
  268. const chatSection = useTemplateRef<HTMLElement>('chatSection')
  269. const chatLoading = ref(false)
  270. const showQuickActions = ref(false)
  271. const streamController = ref<{ abort: () => void } | null>(null)
  272. const noStreamController = ref<{ promise: Promise<string>; abort: () => void } | null>(null)
  273. const files = ref<Array<{ title: string; id?: string | null; url?: string; isProcessing?: boolean; cancel?: () => void }>>([])
  274. // 快捷操作短语数组
  275. const quickActions = [
  276. lang.ssAiChatQuickAction1,
  277. lang.ssAiChatQuickAction2,
  278. ]
  279. // 监听输入变化,当输入"/"时显示快捷操作
  280. watch(inputText, (newValue) => {
  281. // if (messages.value.length > 0 && newValue === '/') {
  282. // showQuickActions.value = true
  283. // }
  284. // else if (newValue !== '/') {
  285. // showQuickActions.value = false
  286. // }
  287. })
  288. const sendMessage = () => {
  289. if (chatLoading.value) {
  290. return
  291. }
  292. // 检查是否有文件正在处理中
  293. const hasProcessingFile = files.value.some(file => file.isProcessing)
  294. if (hasProcessingFile) {
  295. message.error(lang.ssAiChatWaitUpload)
  296. return
  297. }
  298. if (inputText.value.trim() || files.value.length > 0) {
  299. // 添加用户消息
  300. messages.value.push({
  301. role: 'user',
  302. content: inputText.value,
  303. })
  304. // 模拟AI回复
  305. // setTimeout(() => {
  306. // setTimeout(() => {
  307. // messages.value.at(-1).aiContent = '课程生成完成!为您创建了5个内容页面和3个互动工具。您可以查看底部课程大纲,或在中央区域开始编辑。',
  308. // messages.value.at(-1).jsonData = {
  309. // isChoice: true
  310. // }
  311. // }, 1000)
  312. // }, 500)
  313. prevChatResult()
  314. messages.value.at(-1).loading = true
  315. messages.value.at(-1).chatloading = true
  316. messages.value.at(-1).sourceFiles = files.value.filter(file => file.id !== null).map(file => ({
  317. title: file.title,
  318. id: file.id
  319. }))
  320. if (!messages.value.at(-1).jsonData) {
  321. messages.value.at(-1).jsonData = {}
  322. }
  323. chatLoading.value = true
  324. sendAction(inputText.value)
  325. inputText.value = ''
  326. files.value = []
  327. }
  328. }
  329. const stopMessage = () => {
  330. if (streamController.value) {
  331. streamController.value.abort()
  332. streamController.value = null
  333. }
  334. if (noStreamController.value) {
  335. noStreamController.value.abort()
  336. noStreamController.value = null
  337. }
  338. chatLoading.value = false
  339. if (messages.value.length > 0) {
  340. messages.value.at(-1).chatloading = false
  341. messages.value.at(-1).loading = false
  342. }
  343. }
  344. // 处理文件上传
  345. const handleFileUpload = async (files2: File[]) => {
  346. const maxSize = 10 * 1024 * 1024 // 10MB
  347. const uploadPromises = []
  348. for (let i = 0; i < files2.length; i++) {
  349. const file = files2[i]
  350. if (file.size > maxSize) {
  351. message.error(lang.ssAiChatFileSizeLimit)
  352. continue
  353. }
  354. // 先添加文件到列表,显示解析中状态
  355. const fileIndex = files.value.length
  356. files.value.push({
  357. title: file.name + ' (' + lang.ssAiChatParsing + ')',
  358. id: null,
  359. isProcessing: true,
  360. cancel: null
  361. })
  362. // 创建取消控制器
  363. const controller = new AbortController()
  364. files.value[fileIndex].cancel = () => {
  365. controller.abort()
  366. files.value.splice(fileIndex, 1)
  367. }
  368. // 创建上传Promise并添加到数组
  369. const uploadPromise = uploadFile2(file, controller.signal).then(res => {
  370. if (!res) {
  371. files.value.splice(fileIndex, 1)
  372. return
  373. }
  374. // 上传成功,更新文件状态
  375. files.value[fileIndex] = {
  376. title: file.name,
  377. id: res.results.document_id,
  378. isProcessing: false
  379. }
  380. }).catch(error => {
  381. if (error.name !== 'AbortError') {
  382. console.error(lang.ssAiChatUploadFailed, error)
  383. files.value.splice(fileIndex, 1)
  384. }
  385. })
  386. uploadPromises.push(uploadPromise)
  387. }
  388. // 等待所有文件上传完成
  389. await Promise.allSettled(uploadPromises)
  390. }
  391. // 移除文件
  392. const removeFile = (index: number) => {
  393. const file = files.value[index]
  394. if (file && file.isProcessing && file.cancel) {
  395. file.cancel()
  396. }
  397. files.value.splice(index, 1)
  398. }
  399. const prevChatResult = () => {
  400. nextTick(() => {
  401. if (chatSection.value) {
  402. chatSection.value.scrollTop = chatSection.value.scrollHeight
  403. }
  404. })
  405. }
  406. const sendQuickAction = (action: string) => {
  407. inputText.value = action
  408. sendMessage()
  409. }
  410. const retryMessage = (index: number) => {
  411. const message = messages.value[index]
  412. if (message && message?.jsonData?.retryable) {
  413. inputText.value = message.content || ''
  414. sendMessage()
  415. }
  416. }
  417. import { v4 as uuidv4 } from 'uuid'
  418. const session_name = ref('')
  419. const slidesStore = useSlidesStore()
  420. const gType = ref('chat')
  421. const sendAction = async (action: string) => {
  422. const md = new MarkdownIt()
  423. let choice = ''
  424. switch (props.workJson.atool) {
  425. case '45':
  426. choice = '选择题'
  427. break
  428. case '15':
  429. choice = '问答题'
  430. break
  431. case '78':
  432. choice = '投票'
  433. break
  434. default:
  435. choice = ''
  436. break
  437. }
  438. let question_content = ''
  439. let correct_answer = ''
  440. let student_answer = ''
  441. const workContent = JSON.parse(decodeURIComponent(props.workJson.content))
  442. if (props.workJson.atool === '45') {
  443. const testJson = workContent.testJson || []
  444. for (let i = 0; i < testJson.length; i++) {
  445. question_content += `第${i + 1}题:${testJson[i].teststitle}\n 选项:${testJson[i].checkList}`
  446. correct_answer += `第${i + 1}题:${testJson[i].answer}` + '\n'
  447. student_answer += `第${i + 1}题:${testJson[i].userAnswer}` + '\n'
  448. }
  449. }
  450. else if (props.workJson.atool === '78') {
  451. const testJson = workContent.testJson || []
  452. for (let i = 0; i < testJson.length; i++) {
  453. question_content += `第${i + 1}题:${testJson[i].teststitle}\n 选项:${testJson[i].checkList}`
  454. student_answer += `第${i + 1}题:${testJson[i].userAnswer}` + '\n'
  455. }
  456. }
  457. else if (props.workJson.atool === '15') {
  458. question_content = workContent.answerQ || ''
  459. correct_answer = workContent.evaluationCriteria || '无'
  460. student_answer = workContent.answer || ''
  461. }
  462. let promptText = ``
  463. if (props.workJson.atool === '45') {
  464. promptText += `
  465. 当前题目信息:
  466. - 题目类型:${choice}
  467. - 题目内容:${question_content}
  468. - 正确答案:${correct_answer}(0,1,3是选项的下标,数组的下标,相当于第一选项是0,第二选项是1,第三选项是2,第四选项是3,学生的答案也是数组的下标)
  469. - 学生提交的回答:${student_answer}
  470. `
  471. }
  472. else if (props.workJson.atool === '78') {
  473. promptText += `
  474. 当前题目信息:
  475. - 题目类型:${choice}
  476. - 投票题目:${question_content}
  477. - 学生提交的投票:${student_answer}(0,1,2是选项的下标,数组的下标,相当于第一选项是0,第二选项是1,第三选项是2)
  478. `
  479. }
  480. else if (props.workJson.atool === '15') {
  481. promptText += `
  482. 当前题目信息:
  483. - 题目类型:${choice}
  484. - 题目标题:${question_content}
  485. - 评分要点:${correct_answer}
  486. - 学生提交的回答:${student_answer}
  487. `
  488. }
  489. const prompt = `
  490. 当前课程信息:
  491. - 课程年级:${grade.value}}
  492. ${promptText}
  493. query:${action}
  494. `
  495. // 设置超时
  496. const timeoutPromise = new Promise<never>((_, reject) => {
  497. setTimeout(() => {
  498. reject(new Error('请求超时'))
  499. }, 30000) // 30秒超时
  500. })
  501. try {
  502. const controller = await Promise.race([
  503. chat_stream(prompt, agentid2.value, props.userid || '', lang.lang, (event) => {
  504. if (event.type === 'message') {
  505. messages.value.at(-1).aiContent = md.render(event.data)
  506. messages.value.at(-1).loading = false
  507. prevChatResult()
  508. }
  509. else if (event.type === 'messageEnd') {
  510. messages.value.at(-1).aiContent = md.render(event.data)
  511. messages.value.at(-1).chatloading = false
  512. chatLoading.value = false
  513. prevChatResult()
  514. addChat()
  515. }
  516. else if (event.type === 'error') {
  517. errorSet()
  518. }
  519. }, session_name.value, messages.value.at(-1)?.sourceFiles?.map(file => file.id).filter(Boolean)),
  520. timeoutPromise
  521. ])
  522. streamController.value = controller
  523. }
  524. catch (err) {
  525. console.log('err', err)
  526. errorSet()
  527. stopMessage()
  528. }
  529. }
  530. const errorSet = () => {
  531. chatLoading.value = false
  532. messages.value.at(-1).chatloading = false
  533. messages.value.at(-1).loading = false
  534. messages.value.at(-1).jsonData.error = true
  535. messages.value.at(-1).jsonData.errorMessage = lang.ssRetryMessage
  536. messages.value.at(-1).jsonData.retryable = true
  537. addChat()
  538. }
  539. const addChat = () => {
  540. insertChat({
  541. answer: messages.value.at(-1).aiContent,
  542. problem: messages.value.at(-1).content,
  543. type: 'chat',
  544. alltext: messages.value.at(-1).aiContent,
  545. assistant_id: props.workJson.id,
  546. jsonData: messages.value.at(-1).jsonData
  547. })
  548. }
  549. import useCreateElement from '@/hooks/useCreateElement'
  550. import useSlideHandler from '@/hooks/useSlideHandler'
  551. const { createSlide } = useSlideHandler()
  552. const { createFrameElement } = useCreateElement()
  553. const setPageId = async (tool: any, json: any) => {
  554. const res = await getWorkPageId({
  555. userid: props.userid || '',
  556. type: tool,
  557. json: json
  558. })
  559. return res[0][0].id
  560. }
  561. // 上传文件
  562. const uploadFile2 = async (file: File, signal?: AbortSignal): Promise<any> => {
  563. try {
  564. const uuid = uuidv4()
  565. const formData = new FormData()
  566. const timestamp = Date.now()
  567. const finalExtension = file.name.split('.').pop()?.toLowerCase() || ''
  568. const baseName = file.name.slice(0, -(finalExtension.length + 1))
  569. formData.append(
  570. 'file',
  571. new File([file], `${baseName}${timestamp}.${finalExtension}`)
  572. )
  573. formData.append('collection_ids', JSON.stringify([]))
  574. formData.append('id', uuid)
  575. formData.append('metadata', JSON.stringify({ title: file.name }))
  576. formData.append('ingestion_mode', 'fast')
  577. formData.append('run_with_orchestration', 'true')
  578. // 同步知识库
  579. const res = await axios.post(
  580. 'https://r2rserver.cocorobo.cn/v3/documents',
  581. formData,
  582. {
  583. headers: {
  584. 'Content-Type': 'multipart/form-data',
  585. },
  586. signal: signal
  587. }
  588. )
  589. console.log(res)
  590. return res
  591. }
  592. catch (error) {
  593. console.log('err', error)
  594. if (error.name === 'AbortError') {
  595. throw error
  596. }
  597. return ''
  598. }
  599. }
  600. // const agentid1 = ref('cbb29b41-2a4a-4453-bf8d-357929ced4bd')// 判断意图
  601. const agentid2 = ref('bdcb2d5b-9dd6-4b1b-8cef-b34ce5579c5e')// 生成内容
  602. onMounted(() => {
  603. // session_name.value = uuidv4()
  604. // getAgentModel(agentid1.value)
  605. getAgentModel(agentid2.value)
  606. if (props.cid) {
  607. getClassById({
  608. id: props.cid
  609. }).then(res => {
  610. console.log('res年级', props.cid, res)
  611. grade.value = res[0][0].name || ''
  612. })
  613. }
  614. })
  615. </script>
  616. <style lang="scss" scoped>
  617. .ai-chat-container {
  618. width: 100%;
  619. height: 100%;
  620. display: flex;
  621. flex-direction: column;
  622. padding: 16px;
  623. gap: 16px;
  624. }
  625. .ai-chat-popup {
  626. position: absolute;
  627. width: 40%;
  628. height: 60%;
  629. max-width: 400px;
  630. max-height: 500px;
  631. background: #fffefa;
  632. display: flex;
  633. flex-direction: column;
  634. // padding: 16px;
  635. gap: 16px;
  636. z-index: 9999;
  637. border-radius: 10px;
  638. border: 2px solid #fcefc6;
  639. overflow: hidden;
  640. }
  641. .ai-chat-header {
  642. display: flex;
  643. align-items: center;
  644. height: 60px;
  645. width: 100%;
  646. padding: 0 16px;
  647. border-bottom: 2px solid #fcefc6;
  648. box-sizing: border-box;
  649. .close-btn {
  650. margin-left: auto;
  651. width: 30px;
  652. height: 30px;
  653. background: none;
  654. border: none;
  655. cursor: pointer;
  656. padding: 4px;
  657. display: flex;
  658. align-items: center;
  659. justify-content: center;
  660. transition: all 0.3s ease;
  661. color: #000;
  662. background: #fff;
  663. border-radius: 5px;
  664. border: 2px solid #fcefc6;
  665. box-sizing: border-box;
  666. svg {
  667. width: 20px;
  668. height: 20px;
  669. }
  670. }
  671. }
  672. .chat-section {
  673. display: flex;
  674. flex-direction: column;
  675. gap: 12px;
  676. position: relative;
  677. }
  678. .input-section {
  679. display: flex;
  680. gap: 10px;
  681. padding: 16px;
  682. width: 100%;
  683. border-top: 2px solid #fcefc6;
  684. }
  685. .input-wrapper {
  686. position: relative;
  687. display: flex;
  688. background: #fff;
  689. border: 2px solid #fcefc6;
  690. border-radius: 8px;
  691. padding: 8px 12px;
  692. min-height: 70px;
  693. flex: 1;
  694. }
  695. .ai-input {
  696. flex: 1;
  697. border: none;
  698. background: transparent;
  699. font-size: 14px;
  700. color: #374151;
  701. outline: none;
  702. resize: none;
  703. // min-height: 80px;
  704. &::placeholder {
  705. color: #9CA3AF;
  706. }
  707. }
  708. .input-actions {
  709. display: flex;
  710. // justify-content: space-between;
  711. // align-items: center;
  712. margin-top: auto;
  713. }
  714. .attach-btn {
  715. width: 32px;
  716. height: 32px;
  717. background: none;
  718. border: none;
  719. cursor: pointer;
  720. padding: 4px;
  721. display: flex;
  722. align-items: center;
  723. justify-content: center;
  724. transition: all 0.3s ease;
  725. color: #6b7280;
  726. svg {
  727. width: 20px;
  728. height: 20px;
  729. }
  730. &:hover {
  731. background: #FFF4E5;
  732. color: #F78B22;
  733. border-radius: 4px;
  734. }
  735. input[type="file"] {
  736. display: none;
  737. }
  738. }
  739. .file-box {
  740. margin-bottom: 8px;
  741. min-height: 24px;
  742. max-height: 70px;
  743. overflow-y: auto;
  744. .file-item {
  745. display: flex;
  746. align-items: center;
  747. background: #f5f5f5;
  748. padding: 4px 8px;
  749. border-radius: 4px;
  750. margin-bottom: 4px;
  751. .file-name {
  752. flex: 1;
  753. font-size: 12px;
  754. color: #374151;
  755. overflow: hidden;
  756. text-overflow: ellipsis;
  757. white-space: nowrap;
  758. }
  759. .remove-file-btn {
  760. background: none;
  761. border: none;
  762. cursor: pointer;
  763. color: #9CA3AF;
  764. font-size: 12px;
  765. padding: 2px;
  766. margin-left: 8px;
  767. display: flex;
  768. align-items: center;
  769. justify-content: center;
  770. svg {
  771. width: 14px;
  772. height: 14px;
  773. }
  774. &:hover {
  775. color: #EF4444;
  776. }
  777. }
  778. }
  779. }
  780. .send-btn {
  781. margin-left: auto;
  782. width: 32px;
  783. height: 32px;
  784. border: none;
  785. background: #FF9300;
  786. color: #fff;
  787. border-radius: 50%;
  788. cursor: pointer;
  789. display: flex;
  790. align-items: center;
  791. justify-content: center;
  792. transition: all 0.3s ease;
  793. svg {
  794. width: 20px;
  795. height: 20px;
  796. }
  797. &:hover {
  798. background: #E68A00;
  799. }
  800. &.stop {
  801. background: unset;
  802. width: auto;
  803. svg {
  804. width: 32px;
  805. height: 32px;
  806. }
  807. }
  808. }
  809. .quick-actions {
  810. // display: flex;
  811. // flex-direction: column;
  812. // gap: 8px;
  813. .quick-action-btn {
  814. padding: 5px 10px;
  815. border: 1px solid #f7c58f;
  816. background: #FFF9F2;
  817. color: #6b4a1f;
  818. border-radius: 16px;
  819. font-size: 12px;
  820. cursor: pointer;
  821. text-align: left;
  822. display: block;
  823. &:hover {
  824. border-color: #F78B22;
  825. background: #FFF4E5;
  826. color: #111827;
  827. }
  828. +.quick-action-btn {
  829. margin-top: 8px;
  830. }
  831. }
  832. }
  833. .chat-section {
  834. // flex: 1;
  835. height: calc(100% - 155px);
  836. overflow-y: auto;
  837. display: flex;
  838. flex-direction: column;
  839. // gap: 16px;
  840. padding: 0 10px;
  841. &::-webkit-scrollbar {
  842. width: 6px;
  843. }
  844. &::-webkit-scrollbar-track {
  845. background: #F3F4F6;
  846. border-radius: 3px;
  847. }
  848. &::-webkit-scrollbar-thumb {
  849. background: #D1D5DB;
  850. border-radius: 3px;
  851. &:hover {
  852. background: #9CA3AF;
  853. }
  854. }
  855. }
  856. .chat-message {
  857. max-width: 100%;
  858. margin-bottom: 10px;
  859. display: flex;
  860. flex-direction: column;
  861. .message-content {
  862. display: flex;
  863. flex-direction: column;
  864. border-radius: 8px;
  865. padding: 8px 10px;
  866. font-size: 14px;
  867. line-height: 1.5;
  868. color: #374151;
  869. width: fit-content;
  870. +.message-content {
  871. margin-top: 10px;
  872. }
  873. }
  874. }
  875. .message-content {
  876. word-break: break-word;
  877. &.ai-message {
  878. align-self: flex-start;
  879. background: #fff;
  880. border: 1.5px solid #fcefc6;
  881. border-bottom-left-radius: 2px;
  882. &>svg {
  883. width: 17px;
  884. height: 17px;
  885. }
  886. }
  887. }
  888. .message-content {
  889. &.user-message {
  890. align-self: flex-end;
  891. background: #FFF4E5;
  892. border: 1.5px solid #F78B22;
  893. border-bottom-right-radius: 2px;
  894. }
  895. }
  896. .error-message {
  897. display: flex;
  898. align-items: center;
  899. gap: 8px;
  900. color: #ef4444;
  901. font-size: 14px;
  902. .error-text {
  903. flex: 1;
  904. }
  905. .retry-btn {
  906. background: none;
  907. border: none;
  908. cursor: pointer;
  909. color: #ff9300;
  910. padding: 4px;
  911. display: flex;
  912. align-items: center;
  913. justify-content: center;
  914. transition: all 0.3s ease;
  915. .retry-icon {
  916. width: 16px;
  917. height: 16px;
  918. }
  919. &:hover {
  920. color: #e68a00;
  921. transform: rotate(180deg);
  922. }
  923. }
  924. }
  925. .initial-state {
  926. display: flex;
  927. flex-direction: column;
  928. justify-content: flex-start;
  929. align-items: flex-start;
  930. // padding: 24px;
  931. gap: 16px;
  932. }
  933. .confirm-btn {
  934. margin-top: 10px;
  935. padding: 6px 15px;
  936. background: #FF9300;
  937. color: white;
  938. border: none;
  939. border-radius: 8px;
  940. font-size: 14px;
  941. cursor: pointer;
  942. margin-left: auto;
  943. transition: all 0.3s ease;
  944. &:hover {
  945. background: #E68A00;
  946. }
  947. &.disabled {
  948. background: #9CA3AF;
  949. cursor: not-allowed;
  950. }
  951. }
  952. ul {
  953. margin: 8px 0;
  954. padding-left: 20px;
  955. li {
  956. margin: 4px 0;
  957. }
  958. }
  959. .quick-actions-popup {
  960. position: absolute;
  961. bottom: 100%;
  962. left: 0;
  963. right: 0;
  964. background: white;
  965. border: 1px solid #E5E7EB;
  966. border-radius: 8px;
  967. box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
  968. padding: 8px;
  969. z-index: 100;
  970. margin-bottom: 8px;
  971. .quick-action-btn {
  972. width: 100%;
  973. text-align: left;
  974. padding: 10px 12px;
  975. background: white;
  976. border: none;
  977. border-radius: 6px;
  978. font-size: 14px;
  979. color: #374151;
  980. cursor: pointer;
  981. transition: all 0.2s ease;
  982. &:hover {
  983. background: #fff4e5;
  984. }
  985. }
  986. }
  987. .fullscreen-spin {
  988. position: absolute;
  989. top: 0;
  990. bottom: 0;
  991. left: 0;
  992. right: 0;
  993. z-index: 100;
  994. display: flex;
  995. justify-content: center;
  996. align-items: center;
  997. &.mask2 {
  998. background-color: rgba($color: #f1f1f1, $alpha: .7);
  999. }
  1000. }
  1001. .spin {
  1002. width: 200px;
  1003. height: 200px;
  1004. position: absolute;
  1005. top: 50%;
  1006. left: 50%;
  1007. margin-top: -100px;
  1008. margin-left: -100px;
  1009. display: flex;
  1010. flex-direction: column;
  1011. justify-content: center;
  1012. align-items: center;
  1013. }
  1014. .spinner {
  1015. width: 36px;
  1016. height: 36px;
  1017. border: 3px solid $themeColor;
  1018. border-top-color: transparent;
  1019. border-radius: 50%;
  1020. animation: spinner .8s linear infinite;
  1021. }
  1022. .text {
  1023. margin-top: 20px;
  1024. color: $themeColor;
  1025. }
  1026. @keyframes spinner {
  1027. 0% {
  1028. transform: rotate(0deg);
  1029. }
  1030. 100% {
  1031. transform: rotate(360deg);
  1032. }
  1033. }
  1034. </style>
  1035. <style>
  1036. .chat table {
  1037. text-align: center;
  1038. border-spacing: 0;
  1039. border-left: 1px solid #000;
  1040. border-bottom: 1px solid #000;
  1041. }
  1042. .chat table td,
  1043. .chat table th {
  1044. border-top: 1px solid #000;
  1045. border-right: 1px solid #000;
  1046. padding: 10px;
  1047. }
  1048. .message-files {
  1049. margin-top: 8px;
  1050. display: flex;
  1051. flex-direction: column;
  1052. gap: 4px;
  1053. }
  1054. .message-file-item {
  1055. display: flex;
  1056. align-items: center;
  1057. gap: 6px;
  1058. font-size: 12px;
  1059. color: #6b7280;
  1060. background: #f3f4f6;
  1061. padding: 4px 8px;
  1062. border-radius: 4px;
  1063. max-width: 200px;
  1064. overflow: hidden;
  1065. }
  1066. .message-file-item span {
  1067. flex: 1;
  1068. white-space: nowrap;
  1069. overflow: hidden;
  1070. text-overflow: ellipsis;
  1071. }
  1072. </style>