当前位置:   article > 正文

ChatAI智能聊天运营版源码基于Vue3搭建GPT前后端及搭建教程_vue3+gpt

vue3+gpt

ChatAI很多,网上很多说的ChatGPT搭建及源码都是假的,无法使用的,自己研究了好几天,搞定了基于Vue3搭建Chat GPT前后端端源码及搭建教程,现在分享出来,有喜欢的朋友自行下载搭建,用的是openAPI官方接口和key。

搭建好的ChatGPT主要可以实现多聊天窗口,聊天记录导出下载,刷新能保持原来的内容。

先说下技术框架及主要步骤:

  • 准备科学上网工具,国外发码虚拟号,申请好key。(注:需要使用KE 上网,地区选择韩国,日本,印度,新加坡,美国);

  • 前端vue3聊天界面交互,后端express转发接口

  • nginx反向代理转发

 

 

 客户端源码:

  1. <script setup lang='ts'>
  2. import { computed, onMounted, onUnmounted, ref } from 'vue'
  3. import { useRoute } from 'vue-router'
  4. import { storeToRefs } from 'pinia'
  5. import { NAutoComplete, NButton, NInput, useDialog, useMessage } from 'naive-ui'
  6. import html2canvas from 'html2canvas'
  7. import { Message } from './components'
  8. import { useScroll } from './hooks/useScroll'
  9. import { useChat } from './hooks/useChat'
  10. import { useCopyCode } from './hooks/useCopyCode'
  11. import { useUsingContext } from './hooks/useUsingContext'
  12. import HeaderComponent from './components/Header/index.vue'
  13. import { HoverButton, SvgIcon } from '@/components/common'
  14. import { useBasicLayout } from '@/hooks/useBasicLayout'
  15. import { useChatStore, usePromptStore } from '@/store'
  16. import { fetchChatAPIProcess } from '@/api'
  17. import { t } from '@/locales'
  18. let controller = new AbortController()
  19. const openLongReply = import.meta.env.VITE_GLOB_OPEN_LONG_REPLY === 'true'
  20. const route = useRoute()
  21. const dialog = useDialog()
  22. const ms = useMessage()
  23. const chatStore = useChatStore()
  24. useCopyCode()
  25. const { isMobile } = useBasicLayout()
  26. const { addChat, updateChat, updateChatSome, getChatByUuidAndIndex } = useChat()
  27. const { scrollRef, scrollToBottom } = useScroll()
  28. const { usingContext, toggleUsingContext } = useUsingContext()
  29. const { uuid } = route.params as { uuid: string }
  30. const dataSources = computed(() => chatStore.getChatByUuid(+uuid))
  31. const conversationList = computed(() => dataSources.value.filter(item => (!item.inversion && !item.error)))
  32. const prompt = ref<string>('')
  33. const loading = ref<boolean>(false)
  34. // 添加PromptStore
  35. const promptStore = usePromptStore()
  36. // 使用storeToRefs,保证store修改后,联想部分能够重新渲染
  37. const { promptList: promptTemplate } = storeToRefs<any>(promptStore)
  38. function handleSubmit() {
  39. onConversation()
  40. }
  41. async function onConversation() {
  42. let message = prompt.value
  43. if (loading.value)
  44. return
  45. if (!message || message.trim() === '')
  46. return
  47. controller = new AbortController()
  48. addChat(
  49. +uuid,
  50. {
  51. dateTime: new Date().toLocaleString(),
  52. text: message,
  53. inversion: true,
  54. error: false,
  55. conversationOptions: null,
  56. requestOptions: { prompt: message, options: null },
  57. },
  58. )
  59. scrollToBottom()
  60. loading.value = true
  61. prompt.value = ''
  62. let options: Chat.ConversationRequest = {}
  63. const lastContext = conversationList.value[conversationList.value.length - 1]?.conversationOptions
  64. if (lastContext && usingContext.value)
  65. options = { ...lastContext }
  66. addChat(
  67. +uuid,
  68. {
  69. dateTime: new Date().toLocaleString(),
  70. text: '',
  71. loading: true,
  72. inversion: false,
  73. error: false,
  74. conversationOptions: null,
  75. requestOptions: { prompt: message, options: { ...options } },
  76. },
  77. )
  78. scrollToBottom()
  79. try {
  80. let lastText = ''
  81. const fetchChatAPIOnce = async () => {
  82. await fetchChatAPIProcess<Chat.ConversationResponse>({
  83. prompt: message,
  84. options,
  85. signal: controller.signal,
  86. onDownloadProgress: ({ event }) => {
  87. const xhr = event.target
  88. const { responseText } = xhr
  89. // Always process the final line
  90. const lastIndex = responseText.lastIndexOf('\n')
  91. let chunk = responseText
  92. if (lastIndex !== -1)
  93. chunk = responseText.substring(lastIndex)
  94. try {
  95. const data = JSON.parse(chunk)
  96. updateChat(
  97. +uuid,
  98. dataSources.value.length - 1,
  99. {
  100. dateTime: new Date().toLocaleString(),
  101. text: lastText + data.text ?? '',
  102. inversion: false,
  103. error: false,
  104. loading: false,
  105. conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
  106. requestOptions: { prompt: message, options: { ...options } },
  107. },
  108. )
  109. if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
  110. options.parentMessageId = data.id
  111. lastText = data.text
  112. message = ''
  113. return fetchChatAPIOnce()
  114. }
  115. scrollToBottom()
  116. }
  117. catch (error) {
  118. //
  119. }
  120. },
  121. })
  122. }
  123. await fetchChatAPIOnce()
  124. }
  125. catch (error: any) {
  126. const errorMessage = error?.message ?? t('common.wrong')
  127. if (error.message === 'canceled') {
  128. updateChatSome(
  129. +uuid,
  130. dataSources.value.length - 1,
  131. {
  132. loading: false,
  133. },
  134. )
  135. scrollToBottom()
  136. return
  137. }
  138. const currentChat = getChatByUuidAndIndex(+uuid, dataSources.value.length - 1)
  139. if (currentChat?.text && currentChat.text !== '') {
  140. updateChatSome(
  141. +uuid,
  142. dataSources.value.length - 1,
  143. {
  144. text: `${currentChat.text}\n[${errorMessage}]`,
  145. error: false,
  146. loading: false,
  147. },
  148. )
  149. return
  150. }
  151. updateChat(
  152. +uuid,
  153. dataSources.value.length - 1,
  154. {
  155. dateTime: new Date().toLocaleString(),
  156. text: errorMessage,
  157. inversion: false,
  158. error: true,
  159. loading: false,
  160. conversationOptions: null,
  161. requestOptions: { prompt: message, options: { ...options } },
  162. },
  163. )
  164. scrollToBottom()
  165. }
  166. finally {
  167. loading.value = false
  168. }
  169. }
  170. async function onRegenerate(index: number) {
  171. if (loading.value)
  172. return
  173. controller = new AbortController()
  174. const { requestOptions } = dataSources.value[index]
  175. let message = requestOptions?.prompt ?? ''
  176. let options: Chat.ConversationRequest = {}
  177. if (requestOptions.options)
  178. options = { ...requestOptions.options }
  179. loading.value = true
  180. updateChat(
  181. +uuid,
  182. index,
  183. {
  184. dateTime: new Date().toLocaleString(),
  185. text: '',
  186. inversion: false,
  187. error: false,
  188. loading: true,
  189. conversationOptions: null,
  190. requestOptions: { prompt: message, ...options },
  191. },
  192. )
  193. try {
  194. let lastText = ''
  195. const fetchChatAPIOnce = async () => {
  196. await fetchChatAPIProcess<Chat.ConversationResponse>({
  197. prompt: message,
  198. options,
  199. signal: controller.signal,
  200. onDownloadProgress: ({ event }) => {
  201. const xhr = event.target
  202. const { responseText } = xhr
  203. // Always process the final line
  204. const lastIndex = responseText.lastIndexOf('\n')
  205. let chunk = responseText
  206. if (lastIndex !== -1)
  207. chunk = responseText.substring(lastIndex)
  208. try {
  209. const data = JSON.parse(chunk)
  210. updateChat(
  211. +uuid,
  212. index,
  213. {
  214. dateTime: new Date().toLocaleString(),
  215. text: lastText + data.text ?? '',
  216. inversion: false,
  217. error: false,
  218. loading: false,
  219. conversationOptions: { conversationId: data.conversationId, parentMessageId: data.id },
  220. requestOptions: { prompt: message, ...options },
  221. },
  222. )
  223. if (openLongReply && data.detail.choices[0].finish_reason === 'length') {
  224. options.parentMessageId = data.id
  225. lastText = data.text
  226. message = ''
  227. return fetchChatAPIOnce()
  228. }
  229. }
  230. catch (error) {
  231. //
  232. }
  233. },
  234. })
  235. }
  236. await fetchChatAPIOnce()
  237. }
  238. catch (error: any) {
  239. if (error.message === 'canceled') {
  240. updateChatSome(
  241. +uuid,
  242. index,
  243. {
  244. loading: false,
  245. },
  246. )
  247. return
  248. }
  249. const errorMessage = error?.message ?? t('common.wrong')
  250. updateChat(
  251. +uuid,
  252. index,
  253. {
  254. dateTime: new Date().toLocaleString(),
  255. text: errorMessage,
  256. inversion: false,
  257. error: true,
  258. loading: false,
  259. conversationOptions: null,
  260. requestOptions: { prompt: message, ...options },
  261. },
  262. )
  263. }
  264. finally {
  265. loading.value = false
  266. }
  267. }
  268. function handleExport() {
  269. if (loading.value)
  270. return
  271. const d = dialog.warning({
  272. title: t('chat.exportImage'),
  273. content: t('chat.exportImageConfirm'),
  274. positiveText: t('common.yes'),
  275. negativeText: t('common.no'),
  276. onPositiveClick: async () => {
  277. try {
  278. d.loading = true
  279. const ele = document.getElementById('image-wrapper')
  280. const canvas = await html2canvas(ele as HTMLDivElement, {
  281. useCORS: true,
  282. })
  283. const imgUrl = canvas.toDataURL('image/png')
  284. const tempLink = document.createElement('a')
  285. tempLink.style.display = 'none'
  286. tempLink.href = imgUrl
  287. tempLink.setAttribute('download', 'chat-shot.png')
  288. if (typeof tempLink.download === 'undefined')
  289. tempLink.setAttribute('target', '_blank')
  290. document.body.appendChild(tempLink)
  291. tempLink.click()
  292. document.body.removeChild(tempLink)
  293. window.URL.revokeObjectURL(imgUrl)
  294. d.loading = false
  295. ms.success(t('chat.exportSuccess'))
  296. Promise.resolve()
  297. }
  298. catch (error: any) {
  299. ms.error(t('chat.exportFailed'))
  300. }
  301. finally {
  302. d.loading = false
  303. }
  304. },
  305. })
  306. }
  307. function handleDelete(index: number) {
  308. if (loading.value)
  309. return
  310. dialog.warning({
  311. title: t('chat.deleteMessage'),
  312. content: t('chat.deleteMessageConfirm'),
  313. positiveText: t('common.yes'),
  314. negativeText: t('common.no'),
  315. onPositiveClick: () => {
  316. chatStore.deleteChatByUuid(+uuid, index)
  317. },
  318. })
  319. }
  320. function handleClear() {
  321. if (loading.value)
  322. return
  323. dialog.warning({
  324. title: t('chat.clearChat'),
  325. content: t('chat.clearChatConfirm'),
  326. positiveText: t('common.yes'),
  327. negativeText: t('common.no'),
  328. onPositiveClick: () => {
  329. chatStore.clearChatByUuid(+uuid)
  330. },
  331. })
  332. }
  333. function handleEnter(event: KeyboardEvent) {
  334. if (!isMobile.value) {
  335. if (event.key === 'Enter' && !event.shiftKey) {
  336. event.preventDefault()
  337. handleSubmit()
  338. }
  339. }
  340. else {
  341. if (event.key === 'Enter' && event.ctrlKey) {
  342. event.preventDefault()
  343. handleSubmit()
  344. }
  345. }
  346. }
  347. function handleStop() {
  348. if (loading.value) {
  349. controller.abort()
  350. loading.value = false
  351. }
  352. }
  353. // 可优化部分
  354. // 搜索选项计算,这里使用value作为索引项,所以当出现重复value时渲染异常(多项同时出现选中效果)
  355. // 理想状态下其实应该是key作为索引项,但官方的renderOption会出现问题,所以就需要value反renderLabel实现
  356. const searchOptions = computed(() => {
  357. if (prompt.value.startsWith('/')) {
  358. return promptTemplate.value.filter((item: { key: string }) => item.key.toLowerCase().includes(prompt.value.substring(1).toLowerCase())).map((obj: { value: any }) => {
  359. return {
  360. label: obj.value,
  361. value: obj.value,
  362. }
  363. })
  364. }
  365. else {
  366. return []
  367. }
  368. })
  369. // value反渲染key
  370. const renderOption = (option: { label: string }) => {
  371. for (const i of promptTemplate.value) {
  372. if (i.value === option.label)
  373. return [i.key]
  374. }
  375. return []
  376. }
  377. const placeholder = computed(() => {
  378. if (isMobile.value)
  379. return t('chat.placeholderMobile')
  380. return t('chat.placeholder')
  381. })
  382. const buttonDisabled = computed(() => {
  383. return loading.value || !prompt.value || prompt.value.trim() === ''
  384. })
  385. const footerClass = computed(() => {
  386. let classes = ['p-4']
  387. if (isMobile.value)
  388. classes = ['sticky', 'left-0', 'bottom-0', 'right-0', 'p-2', 'pr-3', 'overflow-hidden']
  389. return classes
  390. })
  391. onMounted(() => {
  392. scrollToBottom()
  393. })
  394. onUnmounted(() => {
  395. if (loading.value)
  396. controller.abort()
  397. })
  398. </script>
  399. <template>
  400. <div class="flex flex-col w-full h-full">
  401. <HeaderComponent
  402. v-if="isMobile"
  403. :using-context="usingContext"
  404. @export="handleExport"
  405. @toggle-using-context="toggleUsingContext"
  406. />
  407. <main class="flex-1 overflow-hidden">
  408. <div
  409. id="scrollRef"
  410. ref="scrollRef"
  411. class="h-full overflow-hidden overflow-y-auto"
  412. >
  413. <div
  414. id="image-wrapper"
  415. class="w-full max-w-screen-xl m-auto dark:bg-[#101014]"
  416. :class="[isMobile ? 'p-2' : 'p-4']"
  417. >
  418. <template v-if="!dataSources.length">
  419. <div class="flex items-center justify-center mt-4 text-center text-neutral-300">
  420. <SvgIcon icon="ri:bubble-chart-fill" class="mr-2 text-3xl" />
  421. <span>Aha~</span>
  422. </div>
  423. </template>
  424. <template v-else>
  425. <div>
  426. <Message
  427. v-for="(item, index) of dataSources"
  428. :key="index"
  429. :date-time="item.dateTime"
  430. :text="item.text"
  431. :inversion="item.inversion"
  432. :error="item.error"
  433. :loading="item.loading"
  434. @regenerate="onRegenerate(index)"
  435. @delete="handleDelete(index)"
  436. />
  437. <div class="sticky bottom-0 left-0 flex justify-center">
  438. <NButton v-if="loading" type="warning" @click="handleStop">
  439. <template #icon>
  440. <SvgIcon icon="ri:stop-circle-line" />
  441. </template>
  442. 停止回复
  443. </NButton>
  444. </div>
  445. </div>
  446. </template>
  447. </div>
  448. </div>
  449. </main>
  450. <footer :class="footerClass">
  451. <div class="w-full max-w-screen-xl m-auto">
  452. <div class="flex items-center justify-between space-x-2">
  453. <HoverButton @click="handleClear">
  454. <span class="text-xl text-[#4f555e] dark:text-white">
  455. <SvgIcon icon="ri:delete-bin-line" />
  456. </span>
  457. </HoverButton>
  458. <HoverButton v-if="!isMobile" @click="handleExport">
  459. <span class="text-xl text-[#4f555e] dark:text-white">
  460. <SvgIcon icon="ri:download-2-line" />
  461. </span>
  462. </HoverButton>
  463. <HoverButton v-if="!isMobile" @click="toggleUsingContext">
  464. <span class="text-xl" :class="{ 'text-[#4b9e5f]': usingContext, 'text-[#a8071a]': !usingContext }">
  465. <SvgIcon icon="ri:chat-history-line" />
  466. </span>
  467. </HoverButton>
  468. <NAutoComplete v-model:value="prompt" :options="searchOptions" :render-label="renderOption">
  469. <template #default="{ handleInput, handleBlur, handleFocus }">
  470. <NInput
  471. v-model:value="prompt"
  472. type="textarea"
  473. :placeholder="placeholder"
  474. :autosize="{ minRows: 1, maxRows: isMobile ? 4 : 8 }"
  475. @input="handleInput"
  476. @focus="handleFocus"
  477. @blur="handleBlur"
  478. @keypress="handleEnter"
  479. />
  480. </template>
  481. </NAutoComplete>
  482. <NButton type="primary" :disabled="buttonDisabled" @click="handleSubmit">
  483. <template #icon>
  484. <span class="dark:text-black">
  485. <SvgIcon icon="ri:send-plane-fill" />
  486. </span>
  487. </template>
  488. </NButton>
  489. </div>
  490. </div>
  491. </footer>
  492. </div>
  493. </template>

演示及搭建:基于Vue3搭建ChatGPT前后端源码及搭建教程 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/201405?site
推荐阅读
相关标签
  

闽ICP备14008679号