mirror of
https://github.com/Chanzhaoyu/chatgpt-web.git
synced 2025-07-21 11:57:47 +00:00
feat: 支持 markdown 格式 (#77)
* feat: 支持 markdown 格式和图片 * perf: 重载的时候滚动条保持 * chore: version 2.5.2 * feat: 添加文字换行 * chore: 添加新封面 * chore: 更新 cover
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -1,11 +1,21 @@
|
|||||||
|
## v2.5.2
|
||||||
|
|
||||||
|
`2023-02-21`
|
||||||
|
### Feature
|
||||||
|
- 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77)
|
||||||
|
### BugFix
|
||||||
|
- 重载会话时滚动条保持
|
||||||
|
|
||||||
## v2.5.1
|
## v2.5.1
|
||||||
|
|
||||||
`2023-02-21`
|
`2023-02-21`
|
||||||
|
|
||||||
### Enhancement
|
### Enhancement
|
||||||
- 调整路由模式为 `hash`
|
- 调整路由模式为 `hash`
|
||||||
- 调整新增会话添加到列表最前
|
- 调整新增会话添加到
|
||||||
- 调整移动端样式
|
- 调整移动端样式
|
||||||
|
|
||||||
|
|
||||||
## v2.5.0
|
## v2.5.0
|
||||||
|
|
||||||
`2023-02-20`
|
`2023-02-20`
|
||||||
|
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
使用 express 和 vue3 搭建的 ChartGPT 演示网页
|
使用 express 和 vue3 搭建的 ChartGPT 演示网页
|
||||||
|
|
||||||

|

|
||||||
|

|
||||||
|
|
||||||
> 提示:目前 `OpenAI` 开放的模型最高只有 `GPT-3`,和现在网页所使用的 `GPT-3.5` 或 `GPT-4` 有很大差距,需要等官方开放最新的模型接口。
|
> 提示:目前 `OpenAI` 开放的模型最高只有 `GPT-3`,和现在网页所使用的 `GPT-3.5` 或 `GPT-4` 有很大差距,需要等官方开放最新的模型接口。
|
||||||
|
|
||||||
|
BIN
docs/cover.png
BIN
docs/cover.png
Binary file not shown.
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 96 KiB |
BIN
docs/cover2.png
Normal file
BIN
docs/cover2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 518 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "chatgpt-web",
|
"name": "chatgpt-web",
|
||||||
"version": "2.5.1",
|
"version": "2.5.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "ChatGPT Web",
|
"description": "ChatGPT Web",
|
||||||
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vueuse/core": "^9.13.0",
|
"@vueuse/core": "^9.13.0",
|
||||||
"highlight.js": "^11.7.0",
|
"highlight.js": "^11.7.0",
|
||||||
|
"marked": "^4.2.12",
|
||||||
"naive-ui": "^2.34.3",
|
"naive-ui": "^2.34.3",
|
||||||
"pinia": "^2.0.30",
|
"pinia": "^2.0.30",
|
||||||
"vue": "^3.2.47",
|
"vue": "^3.2.47",
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
"@commitlint/config-conventional": "^17.4.4",
|
"@commitlint/config-conventional": "^17.4.4",
|
||||||
"@iconify/vue": "^4.1.0",
|
"@iconify/vue": "^4.1.0",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/marked": "^4.0.8",
|
||||||
"@types/node": "^18.14.0",
|
"@types/node": "^18.14.0",
|
||||||
"@types/web-bluetooth": "^0.0.16",
|
"@types/web-bluetooth": "^0.0.16",
|
||||||
"@vitejs/plugin-vue": "^4.0.0",
|
"@vitejs/plugin-vue": "^4.0.0",
|
||||||
|
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -6,6 +6,7 @@ specifiers:
|
|||||||
'@commitlint/config-conventional': ^17.4.4
|
'@commitlint/config-conventional': ^17.4.4
|
||||||
'@iconify/vue': ^4.1.0
|
'@iconify/vue': ^4.1.0
|
||||||
'@types/crypto-js': ^4.1.1
|
'@types/crypto-js': ^4.1.1
|
||||||
|
'@types/marked': ^4.0.8
|
||||||
'@types/node': ^18.14.0
|
'@types/node': ^18.14.0
|
||||||
'@types/web-bluetooth': ^0.0.16
|
'@types/web-bluetooth': ^0.0.16
|
||||||
'@vitejs/plugin-vue': ^4.0.0
|
'@vitejs/plugin-vue': ^4.0.0
|
||||||
@@ -18,6 +19,7 @@ specifiers:
|
|||||||
husky: ^8.0.3
|
husky: ^8.0.3
|
||||||
less: ^4.1.3
|
less: ^4.1.3
|
||||||
lint-staged: ^13.1.2
|
lint-staged: ^13.1.2
|
||||||
|
marked: ^4.2.12
|
||||||
naive-ui: ^2.34.3
|
naive-ui: ^2.34.3
|
||||||
npm-run-all: ^4.1.5
|
npm-run-all: ^4.1.5
|
||||||
pinia: ^2.0.30
|
pinia: ^2.0.30
|
||||||
@@ -33,6 +35,7 @@ specifiers:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@vueuse/core': 9.13.0_vue@3.2.47
|
'@vueuse/core': 9.13.0_vue@3.2.47
|
||||||
highlight.js: 11.7.0
|
highlight.js: 11.7.0
|
||||||
|
marked: 4.2.12
|
||||||
naive-ui: 2.34.3_vue@3.2.47
|
naive-ui: 2.34.3_vue@3.2.47
|
||||||
pinia: 2.0.30_hmuptsblhheur2tugfgucj7gc4
|
pinia: 2.0.30_hmuptsblhheur2tugfgucj7gc4
|
||||||
vue: 3.2.47
|
vue: 3.2.47
|
||||||
@@ -44,6 +47,7 @@ devDependencies:
|
|||||||
'@commitlint/config-conventional': 17.4.4
|
'@commitlint/config-conventional': 17.4.4
|
||||||
'@iconify/vue': 4.1.0_vue@3.2.47
|
'@iconify/vue': 4.1.0_vue@3.2.47
|
||||||
'@types/crypto-js': 4.1.1
|
'@types/crypto-js': 4.1.1
|
||||||
|
'@types/marked': 4.0.8
|
||||||
'@types/node': 18.14.0
|
'@types/node': 18.14.0
|
||||||
'@types/web-bluetooth': 0.0.16
|
'@types/web-bluetooth': 0.0.16
|
||||||
'@vitejs/plugin-vue': 4.0.0_vite@4.1.2+vue@3.2.47
|
'@vitejs/plugin-vue': 4.0.0_vite@4.1.2+vue@3.2.47
|
||||||
@@ -735,6 +739,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
|
resolution: {integrity: sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@types/marked/4.0.8:
|
||||||
|
resolution: {integrity: sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/mdast/3.0.10:
|
/@types/mdast/3.0.10:
|
||||||
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
|
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -3229,6 +3237,12 @@ packages:
|
|||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/marked/4.2.12:
|
||||||
|
resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
hasBin: true
|
||||||
|
dev: false
|
||||||
|
|
||||||
/mdast-util-from-markdown/0.8.5:
|
/mdast-util-from-markdown/0.8.5:
|
||||||
resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==}
|
resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
import type { App, Directive } from 'vue'
|
import type { App, Directive } from 'vue'
|
||||||
import hljs from 'highlight.js'
|
import hljs from 'highlight.js'
|
||||||
|
import includeCode from '@/utils/functions/includeCode'
|
||||||
|
|
||||||
function highlightCode(el: HTMLElement) {
|
function highlightCode(el: HTMLElement) {
|
||||||
const regexp = /^(?:\s{4}|\t).+/gm
|
if (includeCode(el.textContent))
|
||||||
if (el.textContent?.indexOf(' = ') !== -1 || el.textContent.match(regexp))
|
|
||||||
hljs.highlightBlock(el)
|
hljs.highlightBlock(el)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
8
src/utils/functions/includeCode.ts
Normal file
8
src/utils/functions/includeCode.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
function includeCode(text: string | null | undefined) {
|
||||||
|
const regexp = /^(?:\s{4}|\t).+/gm
|
||||||
|
if (text?.includes(' = ') || text?.match(regexp))
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export default includeCode
|
@@ -1,28 +1,60 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
import includeCode from '@/utils/functions/includeCode'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
inversion?: boolean
|
inversion?: boolean
|
||||||
error?: boolean
|
error?: boolean
|
||||||
|
text?: string
|
||||||
|
loading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const wrapClass = computed(() => {
|
||||||
|
return [
|
||||||
|
'text-wrap',
|
||||||
|
'p-2',
|
||||||
|
'min-w-[20px]',
|
||||||
|
'rounded-md',
|
||||||
|
props.inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]',
|
||||||
|
{ 'text-red-500': props.error },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
if (props.text) {
|
||||||
|
if (!includeCode(props.text))
|
||||||
|
return marked.parse(props.text)
|
||||||
|
return props.text
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div :class="wrapClass">
|
||||||
class="min-w-[20px] p-2 rounded-md"
|
<template v-if="loading">
|
||||||
:class="[inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]', { 'text-red-500': error }]"
|
<span class="w-[3px] h-[20px] block animate-blink" />
|
||||||
>
|
</template>
|
||||||
<span
|
<template v-else>
|
||||||
v-highlight
|
<code v-if="includeCode(text)" v-highlight class="leading-relaxed" v-text="text" />
|
||||||
class="leading-relaxed whitespace-pre-wrap"
|
<div v-else class="leading-relaxed break-all" v-html="text" />
|
||||||
>
|
</template>
|
||||||
<slot />
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style lang="less">
|
||||||
|
.text-wrap{
|
||||||
|
img{
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hljs {
|
.hljs {
|
||||||
background-color: #fff0 !important;
|
background-color: #fff0 !important;
|
||||||
|
white-space: break-spaces;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -37,10 +37,7 @@ function handleRegenerate() {
|
|||||||
{{ dateTime }}
|
{{ dateTime }}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex items-end mt-2">
|
<div class="flex items-end mt-2">
|
||||||
<Text :inversion="inversion" :error="error">
|
<Text :inversion="inversion" :error="error" :text="text" :loading="loading" />
|
||||||
<span v-if="loading" class="w-[3px] h-[20px] block animate-blink" />
|
|
||||||
<span v-else>{{ text }}</span>
|
|
||||||
</Text>
|
|
||||||
<button
|
<button
|
||||||
v-if="!inversion && !loading"
|
v-if="!inversion && !loading"
|
||||||
class="mb-2 ml-2 transition text-neutral-400 hover:text-neutral-800"
|
class="mb-2 ml-2 transition text-neutral-400 hover:text-neutral-800"
|
||||||
|
@@ -153,7 +153,6 @@ async function onRegenerate(index: number) {
|
|||||||
requestOptions: { prompt: message, ...options },
|
requestOptions: { prompt: message, ...options },
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
scrollToBottom()
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await fetchChatAPI<Chat.ConversationResponse>(message, options, controller.signal)
|
const { data } = await fetchChatAPI<Chat.ConversationResponse>(message, options, controller.signal)
|
||||||
@@ -170,7 +169,6 @@ async function onRegenerate(index: number) {
|
|||||||
requestOptions: { prompt: message, ...options },
|
requestOptions: { prompt: message, ...options },
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
scrollToBottom()
|
|
||||||
}
|
}
|
||||||
catch (error: any) {
|
catch (error: any) {
|
||||||
let errorMessage = 'Something went wrong, please try again later.'
|
let errorMessage = 'Something went wrong, please try again later.'
|
||||||
@@ -191,7 +189,6 @@ async function onRegenerate(index: number) {
|
|||||||
requestOptions: { prompt: message, ...options },
|
requestOptions: { prompt: message, ...options },
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
scrollToBottom()
|
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
|
Reference in New Issue
Block a user