Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac9536ab87 | ||
|
|
938c91f635 | ||
|
|
4ab9f709de | ||
|
|
cb90d81c69 |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,3 +1,21 @@
|
||||
## v2.5.2
|
||||
|
||||
`2023-02-21`
|
||||
### Feature
|
||||
- 增加对 `markdown` 格式的支持 [Demo](https://github.com/Chanzhaoyu/chatgpt-web/pull/77)
|
||||
### BugFix
|
||||
- 重载会话时滚动条保持
|
||||
|
||||
## v2.5.1
|
||||
|
||||
`2023-02-21`
|
||||
|
||||
### Enhancement
|
||||
- 调整路由模式为 `hash`
|
||||
- 调整新增会话添加到
|
||||
- 调整移动端样式
|
||||
|
||||
|
||||
## v2.5.0
|
||||
|
||||
`2023-02-20`
|
||||
|
||||
29
README.md
29
README.md
@@ -2,7 +2,8 @@
|
||||
|
||||
使用 express 和 vue3 搭建的 ChartGPT 演示网页
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
> 提示:目前 `OpenAI` 开放的模型最高只有 `GPT-3`,和现在网页所使用的 `GPT-3.5` 或 `GPT-4` 有很大差距,需要等官方开放最新的模型接口。
|
||||
|
||||
@@ -11,8 +12,6 @@
|
||||
|
||||
[✓] 对代码等消息类型的格式化美化处理
|
||||
|
||||
[✗] 用户模块(注册、登录、个人中心)
|
||||
|
||||
[✗] 界面多语言
|
||||
|
||||
[✗] 界面主题
|
||||
@@ -61,7 +60,6 @@ pnpm install
|
||||
pnpm bootstrap
|
||||
```
|
||||
|
||||
|
||||
## 运行
|
||||
### 后端服务
|
||||
|
||||
@@ -78,15 +76,24 @@ pnpm dev
|
||||
```
|
||||
|
||||
## 打包
|
||||
## Docker build
|
||||
|
||||
[参考信息](https://github.com/Chanzhaoyu/chatgpt-web/pull/42)
|
||||
### 使用 Docker
|
||||
### Docker build & Run
|
||||
|
||||
```bash
|
||||
docker build -t chatgpt-web .
|
||||
|
||||
# 前台运行
|
||||
docker run --name chatgpt-web --rm -it -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web
|
||||
|
||||
# 后台运行
|
||||
docker run --name chatgpt-web -d -p 3002:3002 --env OPENAI_API_KEY=your_api_key chatgpt-web
|
||||
|
||||
# 运行地址
|
||||
http://localhost:3002/
|
||||
```
|
||||
|
||||
## Docker compose
|
||||
### Docker compose
|
||||
|
||||
[Hub 地址](https://hub.docker.com/repository/docker/chenzhaoyu94/chatgpt-web/general)
|
||||
|
||||
@@ -102,10 +109,12 @@ services:
|
||||
OPENAI_API_KEY: xxxxxx
|
||||
```
|
||||
|
||||
|
||||
## 手动打包
|
||||
### 后端服务
|
||||
> 如果你不需要本项目的 `node` 接口,可以省略如下操作
|
||||
|
||||
复制 `service` 文件夹到你有 `node` 服务环境的服务器上。(搜索关键字:`express部署`)
|
||||
复制 `service` 文件夹到你有 `node` 服务环境的服务器上。
|
||||
|
||||
```shell
|
||||
# 安装
|
||||
@@ -122,7 +131,9 @@ PS: 不进行打包,直接在服务器上运行 `pnpm start` 也可
|
||||
|
||||
### 前端打包
|
||||
|
||||
根目录下运行以下命令,然后将 `dist` 文件夹复制到你的托管服务器上
|
||||
1、修改根目录下 `.env` 内 `VITE_APP_API_BASE_URL` 为你的实际后端接口地址
|
||||
|
||||
2、根目录下运行以下命令,然后将 `dist` 文件夹内的文件复制到你网站服务的根目录下
|
||||
|
||||
[参考信息](https://cn.vitejs.dev/guide/static-deploy.html#building-the-app)
|
||||
|
||||
|
||||
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",
|
||||
"version": "2.5.0",
|
||||
"version": "2.5.2",
|
||||
"private": false,
|
||||
"description": "ChatGPT Web",
|
||||
"author": "ChenZhaoYu <chenzhaoyu1994@gmail.com>",
|
||||
@@ -25,6 +25,7 @@
|
||||
"dependencies": {
|
||||
"@vueuse/core": "^9.13.0",
|
||||
"highlight.js": "^11.7.0",
|
||||
"marked": "^4.2.12",
|
||||
"naive-ui": "^2.34.3",
|
||||
"pinia": "^2.0.30",
|
||||
"vue": "^3.2.47",
|
||||
@@ -36,6 +37,7 @@
|
||||
"@commitlint/config-conventional": "^17.4.4",
|
||||
"@iconify/vue": "^4.1.0",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@types/marked": "^4.0.8",
|
||||
"@types/node": "^18.14.0",
|
||||
"@types/web-bluetooth": "^0.0.16",
|
||||
"@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
|
||||
'@iconify/vue': ^4.1.0
|
||||
'@types/crypto-js': ^4.1.1
|
||||
'@types/marked': ^4.0.8
|
||||
'@types/node': ^18.14.0
|
||||
'@types/web-bluetooth': ^0.0.16
|
||||
'@vitejs/plugin-vue': ^4.0.0
|
||||
@@ -18,6 +19,7 @@ specifiers:
|
||||
husky: ^8.0.3
|
||||
less: ^4.1.3
|
||||
lint-staged: ^13.1.2
|
||||
marked: ^4.2.12
|
||||
naive-ui: ^2.34.3
|
||||
npm-run-all: ^4.1.5
|
||||
pinia: ^2.0.30
|
||||
@@ -33,6 +35,7 @@ specifiers:
|
||||
dependencies:
|
||||
'@vueuse/core': 9.13.0_vue@3.2.47
|
||||
highlight.js: 11.7.0
|
||||
marked: 4.2.12
|
||||
naive-ui: 2.34.3_vue@3.2.47
|
||||
pinia: 2.0.30_hmuptsblhheur2tugfgucj7gc4
|
||||
vue: 3.2.47
|
||||
@@ -44,6 +47,7 @@ devDependencies:
|
||||
'@commitlint/config-conventional': 17.4.4
|
||||
'@iconify/vue': 4.1.0_vue@3.2.47
|
||||
'@types/crypto-js': 4.1.1
|
||||
'@types/marked': 4.0.8
|
||||
'@types/node': 18.14.0
|
||||
'@types/web-bluetooth': 0.0.16
|
||||
'@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==}
|
||||
dev: false
|
||||
|
||||
/@types/marked/4.0.8:
|
||||
resolution: {integrity: sha512-HVNzMT5QlWCOdeuBsgXP8EZzKUf0+AXzN+sLmjvaB3ZlLqO+e4u0uXrdw9ub69wBKFs+c6/pA4r9sy6cCDvImw==}
|
||||
dev: true
|
||||
|
||||
/@types/mdast/3.0.10:
|
||||
resolution: {integrity: sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==}
|
||||
dependencies:
|
||||
@@ -3229,6 +3237,12 @@ packages:
|
||||
engines: {node: '>=8'}
|
||||
dev: true
|
||||
|
||||
/marked/4.2.12:
|
||||
resolution: {integrity: sha512-yr8hSKa3Fv4D3jdZmtMMPghgVt6TWbk86WQaWhDloQjRSQhMMYCAro7jP7VDJrjjdV8pxVxMssXS8B8Y5DZ5aw==}
|
||||
engines: {node: '>= 12'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/mdast-util-from-markdown/0.8.5:
|
||||
resolution: {integrity: sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==}
|
||||
dependencies:
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { App, Directive } from 'vue'
|
||||
import hljs from 'highlight.js'
|
||||
import includeCode from '@/utils/functions/includeCode'
|
||||
|
||||
function highlightCode(el: HTMLElement) {
|
||||
const regexp = /^(?:\s{4}|\t).+/gm
|
||||
if (el.textContent?.indexOf(' = ') !== -1 || el.textContent.match(regexp))
|
||||
if (includeCode(el.textContent))
|
||||
hljs.highlightBlock(el)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { App } from 'vue'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import { ChatLayout } from '@/views/chat/layout'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
@@ -20,7 +20,7 @@ const routes: RouteRecordRaw[] = [
|
||||
]
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHashHistory(),
|
||||
routes,
|
||||
scrollBehavior: () => ({ left: 0, top: 0 }),
|
||||
})
|
||||
|
||||
@@ -17,8 +17,8 @@ export const useChatStore = defineStore('chat-store', {
|
||||
|
||||
actions: {
|
||||
addHistory(history: Chat.History, chatData: Chat.Chat[] = []) {
|
||||
this.history.push(history)
|
||||
this.chat.push({ uuid: history.uuid, data: chatData })
|
||||
this.history.unshift(history)
|
||||
this.chat.unshift({ uuid: history.uuid, data: chatData })
|
||||
this.active = history.uuid
|
||||
this.reloadRoute(history.uuid)
|
||||
},
|
||||
@@ -63,9 +63,9 @@ export const useChatStore = defineStore('chat-store', {
|
||||
}
|
||||
},
|
||||
|
||||
setActive(uuid: number) {
|
||||
async setActive(uuid: number) {
|
||||
this.active = uuid
|
||||
this.reloadRoute(uuid)
|
||||
return await this.reloadRoute(uuid)
|
||||
},
|
||||
|
||||
addChatByUuid(uuid: number, chat: Chat.Chat) {
|
||||
|
||||
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>
|
||||
import { computed } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import includeCode from '@/utils/functions/includeCode'
|
||||
|
||||
interface Props {
|
||||
inversion?: 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>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="min-w-[20px] p-2 rounded-md"
|
||||
:class="[inversion ? 'bg-[#d2f9d1]' : 'bg-[#f4f6f8]', { 'text-red-500': error }]"
|
||||
>
|
||||
<span
|
||||
v-highlight
|
||||
class="leading-relaxed whitespace-pre-wrap"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
<div :class="wrapClass">
|
||||
<template v-if="loading">
|
||||
<span class="w-[3px] h-[20px] block animate-blink" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<code v-if="includeCode(text)" v-highlight class="leading-relaxed" v-text="text" />
|
||||
<div v-else class="leading-relaxed break-all" v-html="text" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
<style lang="less">
|
||||
.text-wrap{
|
||||
img{
|
||||
max-width: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.hljs {
|
||||
background-color: #fff0 !important;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -37,10 +37,7 @@ function handleRegenerate() {
|
||||
{{ dateTime }}
|
||||
</span>
|
||||
<div class="flex items-end mt-2">
|
||||
<Text :inversion="inversion" :error="error">
|
||||
<span v-if="loading" class="w-[3px] h-[20px] block animate-blink" />
|
||||
<span v-else>{{ text }}</span>
|
||||
</Text>
|
||||
<Text :inversion="inversion" :error="error" :text="text" :loading="loading" />
|
||||
<button
|
||||
v-if="!inversion && !loading"
|
||||
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 },
|
||||
},
|
||||
)
|
||||
scrollToBottom()
|
||||
|
||||
try {
|
||||
const { data } = await fetchChatAPI<Chat.ConversationResponse>(message, options, controller.signal)
|
||||
@@ -170,7 +169,6 @@ async function onRegenerate(index: number) {
|
||||
requestOptions: { prompt: message, ...options },
|
||||
},
|
||||
)
|
||||
scrollToBottom()
|
||||
}
|
||||
catch (error: any) {
|
||||
let errorMessage = 'Something went wrong, please try again later.'
|
||||
@@ -191,7 +189,6 @@ async function onRegenerate(index: number) {
|
||||
requestOptions: { prompt: message, ...options },
|
||||
},
|
||||
)
|
||||
scrollToBottom()
|
||||
}
|
||||
finally {
|
||||
loading.value = false
|
||||
@@ -224,10 +221,17 @@ const buttonDisabled = computed(() => {
|
||||
return loading.value || !prompt.value || prompt.value.trim() === ''
|
||||
})
|
||||
|
||||
const wrapClass = computed(() => {
|
||||
if (isMobile.value)
|
||||
return ['pt-14', 'pb-14']
|
||||
|
||||
return []
|
||||
})
|
||||
|
||||
const footerClass = computed(() => {
|
||||
let classes = ['p-4']
|
||||
if (isMobile.value)
|
||||
classes = [...classes, 'pl-2', 'pt-2', 'pb-2', 'fixed', 'bottom-0', 'left-0', 'right-0', 'z-30']
|
||||
classes = ['p-2', 'pr-4', 'fixed', 'bottom-4', 'left-0', 'right-0', 'z-30', 'h-14', 'overflow-hidden']
|
||||
return classes
|
||||
})
|
||||
|
||||
@@ -242,7 +246,7 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex flex-col h-full" :class="wrapClass">
|
||||
<main class="flex-1 overflow-hidden">
|
||||
<div ref="scrollRef" class="h-full p-4 overflow-hidden overflow-y-auto" :class="[{ 'p-2': isMobile }]">
|
||||
<template v-if="!dataSources.length">
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
<script setup lang='ts'>
|
||||
import { computed } from 'vue'
|
||||
import { NLayout, NLayoutContent } from 'naive-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Sider from './sider/index.vue'
|
||||
import Header from './header/index.vue'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
import { useAppStore } from '@/store'
|
||||
import { useAppStore, useChatStore } from '@/store'
|
||||
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
router.replace({ name: 'Chat', params: { uuid: chatStore.active } })
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
@@ -21,14 +26,13 @@ const getMobileClass = computed(() => {
|
||||
const getContainerClass = computed(() => {
|
||||
return [
|
||||
'h-full',
|
||||
{ 'pt-14': isMobile.value },
|
||||
{ 'pl-[260px]': !isMobile.value && !collapsed.value },
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen" :class="[isMobile ? 'p-0' : 'p-4']">
|
||||
<div class="h-full" :class="[isMobile ? 'p-0' : 'p-4']">
|
||||
<div class="h-full overflow-hidden" :class="getMobileClass">
|
||||
<NLayout class="z-40 transition" :class="getContainerClass" has-sider>
|
||||
<Sider />
|
||||
|
||||
@@ -18,7 +18,7 @@ function handleUpdateCollapsed() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="fixed top-0 left-0 right-0 z-50 border-b bg-white/80 backdrop-blur">
|
||||
<header class="fixed top-0 left-0 right-0 z-30 border-b bg-white/80 backdrop-blur">
|
||||
<div class="relative flex items-center justify-between h-14">
|
||||
<button class="flex items-center justify-center w-11 h-11" @click="handleUpdateCollapsed">
|
||||
<SvgIcon v-if="collapsed" class="text-2xl" icon="ri:align-justify" />
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
import { computed } from 'vue'
|
||||
import { NInput, NPopconfirm, NScrollbar } from 'naive-ui'
|
||||
import { SvgIcon } from '@/components/common'
|
||||
import { useChatStore } from '@/store'
|
||||
import { useAppStore, useChatStore } from '@/store'
|
||||
import { useBasicLayout } from '@/hooks/useBasicLayout'
|
||||
|
||||
const { isMobile } = useBasicLayout()
|
||||
|
||||
const appStore = useAppStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
const dataSources = computed(() => chatStore.history)
|
||||
@@ -12,7 +16,10 @@ async function handleSelect({ uuid }: Chat.History) {
|
||||
if (isActive(uuid))
|
||||
return
|
||||
|
||||
chatStore.setActive(uuid)
|
||||
await chatStore.setActive(uuid)
|
||||
|
||||
if (isMobile.value)
|
||||
appStore.setSiderCollapsed(true)
|
||||
}
|
||||
|
||||
function handleEdit({ uuid }: Chat.History, isEdit: boolean, event?: MouseEvent) {
|
||||
|
||||
@@ -56,14 +56,16 @@ watch(
|
||||
:style="getMobileClass"
|
||||
@update-collapsed="handleUpdateCollapsed"
|
||||
>
|
||||
<div class="flex flex-col h-full" :class="[{ 'pt-14': isMobile }]">
|
||||
<main class="flex-1 min-h-0 overflow-hidden">
|
||||
<div class="flex flex-col h-full">
|
||||
<main class="flex flex-col flex-1 min-h-0">
|
||||
<div class="p-4">
|
||||
<NButton dashed block @click="handleAdd">
|
||||
New chat
|
||||
</NButton>
|
||||
</div>
|
||||
<List />
|
||||
<div class="flex-1 min-h-0 pb-4 overflow-hidden">
|
||||
<List />
|
||||
</div>
|
||||
</main>
|
||||
<footer class="flex items-center justify-between min-w-0 p-4 overflow-hidden border-t">
|
||||
<UserAvatar />
|
||||
|
||||
Reference in New Issue
Block a user