ChatGPT.vue 9.0 KB

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