ChatGPT.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. <script setup lang="ts">
  2. import {computed, onMounted, ref, watch} from 'vue'
  3. import {useGettext} from 'vue3-gettext'
  4. import {useUserStore} from '@/pinia'
  5. import {storeToRefs} from 'pinia'
  6. import {urlJoin} from '@/lib/helper'
  7. import {marked} from 'marked'
  8. import hljs from 'highlight.js'
  9. import 'highlight.js/styles/vs2015.css'
  10. import {SendOutlined} from '@ant-design/icons-vue'
  11. import Template from '@/views/template/Template.vue'
  12. import openai from '@/api/openai'
  13. import ChatGPT_logo from '@/assets/svg/ChatGPT_logo.svg'
  14. import Icon from '@ant-design/icons-vue'
  15. const {$gettext} = useGettext()
  16. const props = defineProps(['content', 'path', 'history_messages'])
  17. const emit = defineEmits(['update:history_messages'])
  18. const history_messages = computed(() => props.history_messages)
  19. onMounted(() => {
  20. messages.value = props.history_messages
  21. })
  22. watch(history_messages, () => {
  23. messages.value = props.history_messages
  24. })
  25. const {current} = useGettext()
  26. const messages: any = ref([])
  27. const loading = ref(false)
  28. const ask_buffer = ref('')
  29. async function request() {
  30. loading.value = true
  31. const t = ref({
  32. role: 'assistant',
  33. content: ''
  34. })
  35. const user = useUserStore()
  36. const {token} = storeToRefs(user)
  37. console.log('fetching...')
  38. messages.value.push(t.value)
  39. emit('update:history_messages', messages.value)
  40. let res = await fetch(urlJoin(window.location.pathname, '/api/chat_gpt'), {
  41. method: 'POST',
  42. headers: {'Accept': 'text/event-stream', Authorization: token.value},
  43. body: JSON.stringify({messages: messages.value.slice(0, messages.value?.length - 1)})
  44. })
  45. // read body as stream
  46. console.log('reading...')
  47. let reader = res.body!.getReader()
  48. // read stream
  49. console.log('reading stream...')
  50. let buffer = ''
  51. let hasCodeBlockIndicator = false
  52. while (true) {
  53. let {done, value} = await reader.read()
  54. if (done) {
  55. console.log('done')
  56. loading.value = false
  57. store_record()
  58. break
  59. }
  60. apply(value)
  61. }
  62. function apply(input: any) {
  63. const decoder = new TextDecoder('utf-8')
  64. const raw = decoder.decode(input)
  65. // console.log(input, raw)
  66. const line = raw.split('\n\n')
  67. line?.forEach(v => {
  68. const data = v.slice('event:message\ndata:'.length)
  69. if (!data) {
  70. return
  71. }
  72. const content = JSON.parse(data).content
  73. if (!hasCodeBlockIndicator) {
  74. hasCodeBlockIndicator = content.indexOf('`') > -1
  75. }
  76. for (let c of content) {
  77. buffer += c
  78. if (hasCodeBlockIndicator) {
  79. if (isCodeBlockComplete(buffer)) {
  80. t.value.content = buffer
  81. hasCodeBlockIndicator = false
  82. } else {
  83. t.value.content = buffer + '\n```'
  84. }
  85. } else {
  86. t.value.content = buffer
  87. }
  88. }
  89. })
  90. }
  91. function isCodeBlockComplete(text: string) {
  92. const codeBlockRegex = /```/g
  93. const matches = text.match(codeBlockRegex)
  94. if (matches) {
  95. return matches.length % 2 === 0
  96. } else {
  97. return true
  98. }
  99. }
  100. }
  101. async function send() {
  102. if (!messages.value) {
  103. messages.value = []
  104. }
  105. if (messages.value.length === 0) {
  106. messages.value.push({
  107. role: 'user',
  108. content: props.content + '\n\nCurrent Language Code: ' + current
  109. })
  110. } else {
  111. messages.value.push({
  112. role: 'user',
  113. content: ask_buffer.value
  114. })
  115. ask_buffer.value = ''
  116. }
  117. await request()
  118. }
  119. const renderer = new marked.Renderer()
  120. renderer.code = (code, lang: string) => {
  121. const language = hljs.getLanguage(lang) ? lang : 'nginx'
  122. const highlightedCode = hljs.highlight(code, {language}).value
  123. return `<pre><code class="hljs ${language}">${highlightedCode}</code></pre>`
  124. }
  125. marked.setOptions({
  126. renderer: renderer,
  127. langPrefix: 'hljs language-', // highlight.js css expects a top-level 'hljs' class.
  128. pedantic: false,
  129. gfm: true,
  130. breaks: false,
  131. sanitize: false,
  132. smartypants: true,
  133. xhtml: false
  134. })
  135. function store_record() {
  136. openai.store_record({
  137. file_name: props.path,
  138. messages: messages.value
  139. })
  140. }
  141. function clear_record() {
  142. openai.store_record({
  143. file_name: props.path,
  144. messages: []
  145. })
  146. messages.value = []
  147. emit('update:history_messages', [])
  148. }
  149. async function regenerate(index: number) {
  150. editing_idx.value = -1
  151. messages.value = messages.value.slice(0, index)
  152. await request()
  153. }
  154. const editing_idx = ref(-1)
  155. const show = computed(() => messages?.value?.length > 1)
  156. </script>
  157. <template>
  158. <a-card class="chatgpt" title="ChatGPT" v-if="show">
  159. <div class="chatgpt-container">
  160. <a-list
  161. class="chatgpt-log"
  162. item-layout="horizontal"
  163. :data-source="messages"
  164. >
  165. <template #renderItem="{ item, index }">
  166. <a-list-item>
  167. <a-comment :author="item.role" :avatar="item.avatar">
  168. <template #content>
  169. <div class="content" v-if="item.role==='assistant'||editing_idx!=index"
  170. v-html="marked.parse(item.content)"></div>
  171. <a-input style="padding: 0" v-else v-model:value="item.content"
  172. :bordered="false"/>
  173. </template>
  174. <template #actions>
  175. <span v-if="item.role==='user'&&editing_idx!==index" @click="editing_idx=index">
  176. {{ $gettext('Modify') }}
  177. </span>
  178. <template v-else-if="editing_idx==index">
  179. <span @click="regenerate(index+1)">{{ $gettext('Save') }}</span>
  180. <span @click="editing_idx=-1">{{ $gettext('Cancel') }}</span>
  181. </template>
  182. <span v-else-if="!loading" @click="regenerate(index)" :disabled="loading">
  183. {{ $gettext('Reload') }}
  184. </span>
  185. </template>
  186. </a-comment>
  187. </a-list-item>
  188. </template>
  189. </a-list>
  190. <div class="input-msg">
  191. <div class="control-btn">
  192. <a-space v-show="!loading">
  193. <a-popconfirm
  194. :cancelText="$gettext('No')"
  195. :okText="$gettext('OK')"
  196. :title="$gettext('Are you sure you want to clear the record of chat?')"
  197. @confirm="clear_record">
  198. <a-button type="text">{{ $gettext('Clear') }}</a-button>
  199. </a-popconfirm>
  200. <a-button type="text" @click="regenerate(messages?.length-1)">
  201. {{ $gettext('Regenerate response') }}
  202. </a-button>
  203. </a-space>
  204. </div>
  205. <a-textarea auto-size v-model:value="ask_buffer"/>
  206. <div class="sned-btn">
  207. <a-button size="small" type="text" :loading="loading" @click="send">
  208. <send-outlined/>
  209. </a-button>
  210. </div>
  211. </div>
  212. </div>
  213. </a-card>
  214. <template v-else>
  215. <div class="chat-start">
  216. <a-button size="large" shape="circle" @click="send" :loading="loading">
  217. <Icon v-if="!loading" :component="ChatGPT_logo"/>
  218. </a-button>
  219. </div>
  220. </template>
  221. </template>
  222. <style lang="less" scoped>
  223. .chatgpt {
  224. position: sticky;
  225. top: 78px;
  226. :deep(.ant-card-body) {
  227. max-height: 100vh;
  228. overflow-y: scroll;
  229. }
  230. }
  231. .chat-start {
  232. position: fixed !important;
  233. right: 36px;
  234. bottom: 78px;
  235. }
  236. .chatgpt-container {
  237. margin: 0 auto;
  238. max-width: 800px;
  239. .chatgpt-log {
  240. .content {
  241. width: 100%;
  242. :deep(.hljs) {
  243. border-radius: 5px;
  244. }
  245. }
  246. :deep(.ant-comment-content) {
  247. width: 100%;
  248. }
  249. :deep(.ant-comment) {
  250. width: 100%;
  251. }
  252. :deep(.ant-comment-content-detail) {
  253. width: 100%;
  254. p {
  255. margin-bottom: 10px;
  256. }
  257. }
  258. :deep(.ant-list-item:first-child) {
  259. display: none;
  260. }
  261. }
  262. .input-msg {
  263. position: relative;
  264. .control-btn {
  265. display: flex;
  266. justify-content: center;
  267. }
  268. .sned-btn {
  269. position: absolute;
  270. right: 0;
  271. bottom: 3px;
  272. }
  273. }
  274. }
  275. </style>