Skip to content

Commit

Permalink
wip: ChatGPT assistant
Browse files Browse the repository at this point in the history
  • Loading branch information
0xJacky committed Mar 20, 2023
1 parent ba87f02 commit 4cd77f2
Show file tree
Hide file tree
Showing 27 changed files with 3,269 additions and 601 deletions.
6 changes: 6 additions & 0 deletions app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,9 @@ NginxConfigDir =
[nginx_log]
AccessLogPath = /var/log/nginx/access.log
ErrorLogPath = /var/log/nginx/error.log

[openai]
Model =
BaseUrl =
Proxy =
Token =
3 changes: 3 additions & 0 deletions frontend/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ declare module '@vue/runtime-core' {
ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
AComment: typeof import('ant-design-vue/es')['Comment']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADivider: typeof import('ant-design-vue/es')['Divider']
ADrawer: typeof import('ant-design-vue/es')['Drawer']
Expand Down Expand Up @@ -55,9 +56,11 @@ declare module '@vue/runtime-core' {
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
BreadcrumbBreadcrumb: typeof import('./src/components/Breadcrumb/Breadcrumb.vue')['default']
ChartAreaChart: typeof import('./src/components/Chart/AreaChart.vue')['default']
ChartRadialBarChart: typeof import('./src/components/Chart/RadialBarChart.vue')['default']
ChatGPTChatGPT: typeof import('./src/components/ChatGPT/ChatGPT.vue')['default']
CodeEditorCodeEditor: typeof import('./src/components/CodeEditor/CodeEditor.vue')['default']
FooterToolbarFooterToolBar: typeof import('./src/components/FooterToolbar/FooterToolBar.vue')['default']
LogoLogo: typeof import('./src/components/Logo/Logo.vue')['default']
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"apexcharts": "^3.36.3",
"axios": "^1.2.2",
"dayjs": "^1.11.7",
"highlight.js": "^11.7.0",
"marked": "^4.2.5",
"nprogress": "^0.2.0",
"pinia": "^2.0.28",
Expand Down
9 changes: 9 additions & 0 deletions frontend/src/api/openai.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import http from '@/lib/http'

const openai = {
store_record(data: any) {
return http.post('/chat_gpt_record', data)
}
}

export default openai
219 changes: 219 additions & 0 deletions frontend/src/components/ChatGPT/ChatGPT.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
<script setup lang="ts">
import {computed, ref, watch} from 'vue'
import {useGettext} from 'vue3-gettext'
import {useUserStore} from '@/pinia'
import {storeToRefs} from 'pinia'
import {urlJoin} from '@/lib/helper'
import {marked} from 'marked'
import hljs from 'highlight.js'
import 'highlight.js/styles/vs2015.css'
import {SendOutlined} from '@ant-design/icons-vue'
import Template from '@/views/template/Template.vue'
import openai from '@/api/openai'
const {$gettext} = useGettext()
const props = defineProps(['content', 'path', 'history_messages'])
watch(computed(() => props.history_messages), () => {
messages.value = props.history_messages
})
const {current} = useGettext()
const messages: any = ref([])
const loading = ref(false)
const ask_buffer = ref('')
async function send() {
if (messages.value.length === 0) {
messages.value.push({
role: 'user',
content: props.content + '\n\nCurrent Language Code: ' + current
})
} else {
messages.value.push({
role: 'user',
content: ask_buffer.value
})
ask_buffer.value = ''
}
loading.value = true
const t = ref({
role: 'assistant',
content: ''
})
const user = useUserStore()
const {token} = storeToRefs(user)
console.log('fetching...')
let res = await fetch(urlJoin(window.location.pathname, '/api/chat_gpt'), {
method: 'POST',
headers: {'Accept': 'text/event-stream', Authorization: token.value},
body: JSON.stringify({messages: messages.value})
})
messages.value.push(t.value)
// read body as stream
console.log('reading...')
let reader = res.body!.getReader()
// read stream
console.log('reading stream...')
let buffer = ''
while (true) {
let {done, value} = await reader.read()
if (done) {
console.log('done')
loading.value = false
store_record()
break
}
apply(value)
}
function apply(input: any) {
const decoder = new TextDecoder('utf-8')
const raw = decoder.decode(input)
const regex = /{"content":"(.+?)"}/g
const matches = raw.match(regex)
matches?.forEach(v => {
const content = JSON.parse(v).content
for (let c of content) {
buffer += c
if (isCodeBlockComplete(buffer)) {
t.value.content = buffer
} else {
t.value.content = buffer + '\n```'
}
}
})
}
function isCodeBlockComplete(text: string) {
const codeBlockRegex = /```/g
const matches = text.match(codeBlockRegex)
if (matches) {
return matches.length % 2 === 0
} else {
return true
}
}
}
const renderer = new marked.Renderer()
renderer.code = (code, lang: string) => {
const language = hljs.getLanguage(lang) ? lang : 'nginx'
const highlightedCode = hljs.highlight(code, {language}).value
return `<pre><code class="hljs ${language}">${highlightedCode}</code></pre>`
}
marked.setOptions({
renderer: renderer,
langPrefix: 'hljs language-', // highlight.js css expects a top-level 'hljs' class.
pedantic: false,
gfm: true,
breaks: false,
sanitize: false,
smartypants: true,
xhtml: false
})
function store_record() {
openai.store_record({
file_name: props.path,
messages: messages.value
})
}
</script>

<template>
<a-card title="ChatGPT">
<div class="chatgpt-container">
<template v-if="messages?.length>0">
<a-list
class="chatgpt-log"
item-layout="horizontal"
:data-source="messages"
>
<template #renderItem="{ item }">
<a-list-item>
<a-comment :author="item.role" :avatar="item.avatar">
<template #content>
<div class="content" v-html="marked.parse(item.content)"></div>
</template>
</a-comment>
</a-list-item>
</template>
</a-list>
<div class="input-msg">
<a-textarea auto-size v-model:value="ask_buffer"/>
<div class="sned-btn">
<a-button size="small" type="text" :loading="loading" @click="send">
<send-outlined/>
</a-button>
</div>
</div>
</template>
<template v-else>
<a-button @click="send">{{ $gettext('Chat with ChatGPT') }}</a-button>
</template>
</div>
</a-card>
</template>

<style lang="less" scoped>
.chatgpt-container {
margin: 0 auto;
max-width: 800px;
.chatgpt-log {
.content {
width: 100%;
:deep(.hljs) {
border-radius: 5px;
}
}
:deep(.ant-comment-content) {
width: 100%;
}
:deep(.ant-comment) {
width: 100%;
}
:deep(.ant-comment-content-detail) {
width: 100%;
p {
margin-bottom: 10px;
}
}
:deep(.ant-list-item:first-child) {
display: none;
}
}
.input-msg {
position: relative;
.sned-btn {
position: absolute;
right: 0;
bottom: 3px;
}
}
}
</style>
Loading

0 comments on commit 4cd77f2

Please sign in to comment.