finished refactor chat page UI

This commit is contained in:
RockYang 2024-05-17 19:25:38 +08:00
parent f617bde81b
commit dad9254128
20 changed files with 471 additions and 344 deletions

View File

@ -6,4 +6,4 @@ VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123 VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=ChatPLUS_DEV_ VUE_APP_KEY_PREFIX=ChatPLUS_DEV_
VUE_APP_TITLE="Geek-AI 创作系统" VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.0.6 VUE_APP_VERSION=v4.0.7

View File

@ -2,4 +2,4 @@ VUE_APP_API_HOST=
VUE_APP_WS_HOST= VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=ChatPLUS_ VUE_APP_KEY_PREFIX=ChatPLUS_
VUE_APP_TITLE="Geek-AI 创作系统" VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.0.6 VUE_APP_VERSION=v4.0.7

View File

@ -114,10 +114,13 @@ $borderColor = #4676d0;
.tool-box { .tool-box {
display: flex; display: flex;
justify-content: flex-end; justify-content: center;
align-items: center; padding-top 12px
padding 0 20px 10px 20px;
border-top 1px solid #3c3c3c; border-top 1px solid #3c3c3c;
.iconfont {
margin-right 5px
}
} }
} }
@ -126,52 +129,6 @@ $borderColor = #4676d0;
--el-main-padding: 0; --el-main-padding: 0;
margin: 0; margin: 0;
.chat-head {
width: 100%;
height: 50px;
background-color: #28292A
.chat-config {
display flex
flex-direction row
align-items: center;
justify-content center;
padding-top 10px;
.role-select-label {
color #ffffff
}
.el-select {
max-width 150px;
margin-right 10px;
}
.role-select {
max-width 130px;
}
.el-button {
.el-icon {
margin-right 5px;
}
}
}
.iconfont {
margin-right 5px;
}
.is-circle {
margin-left 5px
.iconfont {
margin-right 0
}
}
}
.chat-box { .chat-box {
min-width: 0; min-width: 0;
flex: 1; flex: 1;
@ -207,23 +164,6 @@ $borderColor = #4676d0;
} }
} }
.re-generate {
position: relative;
display: flex;
justify-content: center;
.btn-box {
position: absolute
bottom: 10px;
.el-button {
.el-icon {
margin-right 5px;
}
}
}
}
.input-box { .input-box {
background-color: #ffffff background-color: #ffffff
display: flex; display: flex;
@ -232,6 +172,26 @@ $borderColor = #4676d0;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
padding 0 15px; padding 0 15px;
.tool-item {
margin-right 15px
border-radius: 6px;
color: #19c37d;
display flex
justify-content center
justify-items center
padding 6px
cursor pointer
background #F2F2F2
&:hover {
background #D5FAD3
}
.iconfont {
font-size: 24px;
}
}
.input-container { .input-container {
width 100% width 100%
margin: 0; margin: 0;
@ -242,7 +202,6 @@ $borderColor = #4676d0;
position relative position relative
.el-textarea { .el-textarea {
.el-textarea__inner::-webkit-scrollbar { .el-textarea__inner::-webkit-scrollbar {
width: 0; width: 0;
height: 0; height: 0;
@ -263,8 +222,6 @@ $borderColor = #4676d0;
.el-button { .el-button {
padding 8px 5px; padding 8px 5px;
border-radius 6px; border-radius 6px;
background: rgb(25, 195, 125)
color #ffffff;
font-size 20px; font-size 20px;
} }
} }

View File

@ -35,6 +35,23 @@
} }
.navbar { .navbar {
display flex
flex-flow row
.link-button {
margin-right 15px
color #e1e1e1
padding 0 10px
&:hover {
background-color #414141
}
.iconfont {
font-size 24px
}
}
.user-info { .user-info {
width 100% width 100%
padding 5px 0; padding 5px 0;
@ -153,4 +170,18 @@
background-color #f1f1f1 background-color #f1f1f1
} }
} }
.user-info-menu {
li {
a {
width 100%
justify-content left
&:hover {
text-decoration none !important
color var(--el-primary-text-color)
}
}
}
}
} }

View File

@ -1,8 +1,8 @@
@font-face { @font-face {
font-family: "iconfont"; /* Project id 4125778 */ font-family: "iconfont"; /* Project id 4125778 */
src: url('iconfont.woff2?t=1713766977199') format('woff2'), src: url('iconfont.woff2?t=1715938850931') format('woff2'),
url('iconfont.woff?t=1713766977199') format('woff'), url('iconfont.woff?t=1715938850931') format('woff'),
url('iconfont.ttf?t=1713766977199') format('truetype'); url('iconfont.ttf?t=1715938850931') format('truetype');
} }
.iconfont { .iconfont {
@ -13,6 +13,38 @@
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.icon-mic-bold:before {
content: "\e683";
}
.icon-mic-thin:before {
content: "\e8c2";
}
.icon-attachment-cl:before {
content: "\e66a";
}
.icon-attachment-st:before {
content: "\e63b";
}
.icon-speaker:before {
content: "\e607";
}
.icon-clear:before {
content: "\e900";
}
.icon-bbs:before {
content: "\e623";
}
.icon-license:before {
content: "\e65a";
}
.icon-more:before { .icon-more:before {
content: "\e63c"; content: "\e63c";
} }

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,62 @@
"css_prefix_text": "icon-", "css_prefix_text": "icon-",
"description": "", "description": "",
"glyphs": [ "glyphs": [
{
"icon_id": "6539424",
"name": "麦克风",
"font_class": "mic-bold",
"unicode": "e683",
"unicode_decimal": 59011
},
{
"icon_id": "1727442",
"name": "213麦克风",
"font_class": "mic-thin",
"unicode": "e8c2",
"unicode_decimal": 59586
},
{
"icon_id": "3730725",
"name": "attach-attachment-cl",
"font_class": "attachment-cl",
"unicode": "e66a",
"unicode_decimal": 58986
},
{
"icon_id": "15748474",
"name": "st-attachment",
"font_class": "attachment-st",
"unicode": "e63b",
"unicode_decimal": 58939
},
{
"icon_id": "1010",
"name": "扬声器",
"font_class": "speaker",
"unicode": "e607",
"unicode_decimal": 58887
},
{
"icon_id": "8094805",
"name": "clear",
"font_class": "clear",
"unicode": "e900",
"unicode_decimal": 59648
},
{
"icon_id": "34803640",
"name": "论坛",
"font_class": "bbs",
"unicode": "e623",
"unicode_decimal": 58915
},
{
"icon_id": "310708",
"name": "license",
"font_class": "license",
"unicode": "e65a",
"unicode_decimal": 58970
},
{ {
"icon_id": "1421807", "icon_id": "1421807",
"name": "更多", "name": "更多",

Binary file not shown.

View File

@ -7,9 +7,9 @@
<div class="chat-item"> <div class="chat-item">
<div class="content" v-html="content"></div> <div class="content" v-html="content"></div>
<div class="bar" v-if="createdAt !== ''"> <div class="bar" v-if="createdAt">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span> <span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span>
<span class="bar-item">Tokens: {{ finalTokens }}</span> <!-- <span class="bar-item">Tokens: {{ finalTokens }}</span>-->
</div> </div>
</div> </div>
</div> </div>

View File

@ -2,38 +2,61 @@
<div class="chat-line chat-line-reply"> <div class="chat-line chat-line-reply">
<div class="chat-line-inner"> <div class="chat-line-inner">
<div class="chat-icon"> <div class="chat-icon">
<img :src="icon" alt="ChatGPT"> <img :src="data.icon" alt="ChatGPT">
</div> </div>
<div class="chat-item"> <div class="chat-item">
<div class="content" v-html="content"></div> <div class="content" v-html="data.content"></div>
<div class="bar" v-if="createdAt !== ''"> <div class="bar" v-if="data.created_at">
<span class="bar-item"><el-icon><Clock/></el-icon> {{ createdAt }}</span> <span class="bar-item"><el-icon><Clock/></el-icon> {{ dateFormat(data.created_at) }}</span>
<span class="bar-item">Tokens: {{ tokens }}</span> <!-- <span class="bar-item">Tokens: {{ tokens }}</span>-->
<span class="bar-item"> <span class="bar-item">
<el-tooltip
class="box-item"
effect="dark"
content="复制回答"
placement="bottom"
>
<el-icon class="copy-reply" :data-clipboard-text="data.orgContent">
<DocumentCopy/>
</el-icon>
</el-tooltip>
</span>
<span v-if="!readOnly">
<span class="bar-item" @click="reGenerate(data.prompt)">
<el-tooltip <el-tooltip
class="box-item" class="box-item"
effect="dark" effect="dark"
content="复制回答" content="重新生成"
placement="bottom" placement="bottom"
> >
<el-icon class="copy-reply" :data-clipboard-text="orgContent"> <el-icon><Refresh/></el-icon>
<DocumentCopy/> </el-tooltip>
</el-icon>
</el-tooltip>
</span> </span>
<span class="bar-item">
<el-dropdown trigger="click"> <span class="bar-item" @click="synthesis(data.orgContent)">
<span class="el-dropdown-link"> <el-tooltip
<el-icon><More/></el-icon> class="box-item"
</span> effect="dark"
<template #dropdown> content="生成语音朗读"
<el-dropdown-menu> placement="bottom"
<el-dropdown-item :icon="Headset" @click="synthesis(orgContent)">生成语音</el-dropdown-item> >
</el-dropdown-menu> <i class="iconfont icon-speaker"></i>
</template> </el-tooltip>
</el-dropdown>
</span> </span>
</span>
<!-- <span class="bar-item">-->
<!-- <el-dropdown trigger="click">-->
<!-- <span class="el-dropdown-link">-->
<!-- <el-icon><More/></el-icon>-->
<!-- </span>-->
<!-- <template #dropdown>-->
<!-- <el-dropdown-menu>-->
<!-- <el-dropdown-item :icon="Headset" @click="synthesis(orgContent)">生成语音</el-dropdown-item>-->
<!-- </el-dropdown-menu>-->
<!-- </template>-->
<!-- </el-dropdown>-->
<!-- </span>-->
</div> </div>
</div> </div>
</div> </div>
@ -41,36 +64,36 @@
</template> </template>
<script setup> <script setup>
import {Clock, DocumentCopy, Headset, More} from "@element-plus/icons-vue"; import {Clock, DocumentCopy, Refresh} from "@element-plus/icons-vue";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {dateFormat} from "@/utils/libs";
// eslint-disable-next-line no-undef,no-unused-vars // eslint-disable-next-line no-undef,no-unused-vars
const props = defineProps({ const props = defineProps({
content: { data: {
type: String, type: Object,
default: '', default: {},
}, },
orgContent: { readOnly: {
type: String, type: Boolean,
default: '', default: false
},
createdAt: {
type: String,
default: '',
},
tokens: {
type: Number,
default: 0,
},
icon: {
type: String,
default: 'images/gpt-icon.png',
} }
}) })
const emits = defineEmits(['regen']);
if (!props.data.icon) {
props.data.icon = "images/gpt-icon.png"
}
const synthesis = (text) => { const synthesis = (text) => {
console.log(text) console.log(text)
ElMessage.info("语音合成功能暂不可用") ElMessage.info("语音合成功能暂不可用")
} }
//
const reGenerate = (prompt) => {
emits('regen', prompt)
}
</script> </script>
<style lang="stylus"> <style lang="stylus">
@ -226,6 +249,7 @@ const synthesis = (text) => {
padding 3px 5px; padding 3px 5px;
margin-right 10px; margin-right 10px;
border-radius 5px; border-radius 5px;
cursor pointer
.el-icon { .el-icon {
position relative position relative

View File

@ -1,13 +1,8 @@
<template> <template>
<el-container class="file-list-box"> <el-container class="file-list-box">
<el-tooltip class="box-item" effect="dark" content="打开文件管理中心"> <a class="file-upload-img" @click="fetchFiles">
<el-button class="file-upload-img" @click="fetchFiles"> <i class="iconfont icon-attachment-st"></i>
<el-icon> </a>
<PictureFilled/>
</el-icon>
</el-button>
</el-tooltip>
<el-dialog <el-dialog
v-model="show" v-model="show"
:close-on-click-modal="true" :close-on-click-modal="true"
@ -58,7 +53,7 @@
import {ref} from "vue"; import {ref} from "vue";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {Delete, PictureFilled, Plus} from "@element-plus/icons-vue"; import {Delete, Plus} from "@element-plus/icons-vue";
import {isImage, removeArrayItem} from "@/utils/libs"; import {isImage, removeArrayItem} from "@/utils/libs";
const props = defineProps({ const props = defineProps({
@ -132,11 +127,9 @@ const insertURL = (url) => {
.file-list-box { .file-list-box {
.file-upload-img { .file-upload-img {
padding: 8px 5px; .iconfont {
border-radius: 6px; font-size: 24px;
background: #19c37d; }
color: #fff;
font-size: 20px;
} }
.el-dialog { .el-dialog {

View File

@ -221,7 +221,7 @@
</template> </template>
<script setup> <script setup>
import {computed, ref} from "vue" import {ref, watch} from "vue"
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {setUserToken} from "@/store/session"; import {setUserToken} from "@/store/session";
@ -234,8 +234,9 @@ import {arrayContains} from "@/utils/libs";
const props = defineProps({ const props = defineProps({
show: Boolean, show: Boolean,
}); });
const showDialog = computed(() => { const showDialog = ref(false)
return props.show watch(() => props.show, (newValue) => {
showDialog.value = newValue
}) })
const login = ref(true) const login = ref(true)

View File

@ -8,6 +8,7 @@
import {createApp} from 'vue' import {createApp} from 'vue'
import ElementPlus from "element-plus" import ElementPlus from "element-plus"
import "element-plus/dist/index.css" import "element-plus/dist/index.css"
import '@/assets/iconfont/iconfont.css';
import 'vant/lib/index.css'; import 'vant/lib/index.css';
import App from './App.vue' import App from './App.vue'
import {createPinia} from "pinia"; import {createPinia} from "pinia";

View File

@ -0,0 +1,13 @@
import {defineStore} from 'pinia';
export const useSharedStore = defineStore('shared', {
state: () => ({
showLoginDialog: false
}),
getters: {},
actions: {
setShowLoginDialog(value) {
this.showLoginDialog = value;
}
}
});

View File

@ -3,9 +3,6 @@
<div class="chat-box" id="chat-box"> <div class="chat-box" id="chat-box">
<div class="title"> <div class="title">
<h2>{{ chatTitle }}</h2> <h2>{{ chatTitle }}</h2>
<el-button type="success" @click="exportChat" :icon="Promotion">
导出 PDF 文档
</el-button>
</div> </div>
<div v-for="item in chatData" :key="item.id"> <div v-for="item in chatData" :key="item.id">
@ -14,17 +11,10 @@
:icon="item.icon" :icon="item.icon"
:created-at="dateFormat(item['created_at'])" :created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']" :tokens="item['tokens']"
:model="item['model']"
:content="item.content"/> :content="item.content"/>
<chat-reply v-else-if="item.type==='reply'" <chat-reply v-else-if="item.type==='reply'"
:icon="item.icon" :data="item" :read-only="true"/>
:org-content="item.orgContent"
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:content="item.content"/>
<chat-mid-journey v-else-if="item.type==='mj'"
:content="item.content"
:icon="item.icon"
:created-at="dateFormat(item['created_at'])"/>
</div> </div>
</div><!-- end chat box --> </div><!-- end chat box -->
</div> </div>
@ -34,14 +24,13 @@
import {dateFormat} from "@/utils/libs"; import {dateFormat} from "@/utils/libs";
import ChatReply from "@/components/ChatReply.vue"; import ChatReply from "@/components/ChatReply.vue";
import ChatPrompt from "@/components/ChatPrompt.vue"; import ChatPrompt from "@/components/ChatPrompt.vue";
import {nextTick, ref} from "vue"; import {nextTick, onMounted, ref} from "vue";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import 'highlight.js/styles/a11y-dark.css' import 'highlight.js/styles/a11y-dark.css'
import hl from "highlight.js"; import hl from "highlight.js";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {Promotion} from "@element-plus/icons-vue"; import Clipboard from "clipboard";
import ChatMidJourney from "@/components/ChatMidJourney.vue";
const chatData = ref([]) const chatData = ref([])
const router = useRouter() const router = useRouter()
@ -91,9 +80,16 @@ httpGet('/api/chat/detail?chat_id=' + chatId).then(res => {
ElMessage.error("加载会失败: " + e.message) ElMessage.error("加载会失败: " + e.message)
}) })
const exportChat = () => { onMounted(() => {
window.print() const clipboard = new Clipboard('.copy-reply');
} clipboard.on('success', () => {
ElMessage.success('复制成功!');
})
clipboard.on('error', () => {
ElMessage.error('复制失败!');
})
})
</script> </script>
<style lang="stylus"> <style lang="stylus">
.chat-export { .chat-export {
@ -115,12 +111,15 @@ const exportChat = () => {
} }
.chat-line { .chat-line-prompt {
font-size: 14px; font-size: 14px;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
.chat-line-inner { .chat-line-inner {
.chat-icon {
margin-right: 0
}
.content { .content {
padding-top: 0 padding-top: 0
font-size 16px; font-size 16px;
@ -138,10 +137,6 @@ const exportChat = () => {
.chat-line-inner { .chat-line-inner {
display flex display flex
.copy-reply {
display none
}
.bar-item { .bar-item {
background-color: #f7f7f8; background-color: #f7f7f8;
color: #888; color: #888;

View File

@ -59,92 +59,12 @@
</div> </div>
<div class="tool-box"> <div class="tool-box">
<!-- <el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="isLogin">--> <el-button type="danger" size="small" @click="clearAllChats">
<!-- <span class="el-dropdown-link">--> <i class="iconfont icon-clear"></i> 清空聊天记录
<!-- <el-image :src="loginUser.avatar"/>--> </el-button>
<!-- <span class="username">{{ loginUser.nickname }}</span>-->
<!-- <el-icon><ArrowDown/></el-icon>-->
<!-- </span>-->
<!-- <template #dropdown>-->
<!-- <el-dropdown-menu style="width: 296px;">-->
<!-- <el-dropdown-item @click="showConfig">-->
<!-- <el-icon>-->
<!-- <Tools/>-->
<!-- </el-icon>-->
<!-- <span>账户信息</span>-->
<!-- </el-dropdown-item>-->
<!-- <el-dropdown-item @click="clearAllChats">-->
<!-- <el-icon>-->
<!-- <Delete/>-->
<!-- </el-icon>-->
<!-- <span>清除所有会话</span>-->
<!-- </el-dropdown-item>-->
<!-- <el-dropdown-item @click="logout">-->
<!-- <i class="iconfont icon-logout"></i>-->
<!-- <span>注销</span>-->
<!-- </el-dropdown-item>-->
<!-- <el-dropdown-item>-->
<!-- <i class="iconfont icon-github"></i>-->
<!-- <span>-->
<!-- powered by-->
<!-- <el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">chatgpt-plus-v3</el-link>-->
<!-- </span>-->
<!-- </el-dropdown-item>-->
<!-- </el-dropdown-menu>-->
<!-- </template>-->
<!-- </el-dropdown>-->
</div> </div>
</el-aside> </el-aside>
<el-main v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.3)"> <el-main v-loading="loading" element-loading-background="rgba(122, 122, 122, 0.3)">
<!-- <div class="chat-head">-->
<!-- <div class="chat-config">-->
<!-- <el-select v-model="roleId" filterable placeholder="角色" class="role-select" @change="_newChat"-->
<!-- style="width:150px">-->
<!-- <el-option-->
<!-- v-for="item in roles"-->
<!-- :key="item.id"-->
<!-- :label="item.name"-->
<!-- :value="item.id"-->
<!-- >-->
<!-- <div class="role-option">-->
<!-- <el-image :src="item.icon"></el-image>-->
<!-- <span>{{ item.name }}</span>-->
<!-- </div>-->
<!-- </el-option>-->
<!-- </el-select>-->
<!-- <el-select v-model="modelID" placeholder="模型" @change="_newChat" :disabled="disableModel"-->
<!-- style="width:150px">-->
<!-- <el-option-->
<!-- v-for="item in models"-->
<!-- :key="item.id"-->
<!-- :label="item.name"-->
<!-- :value="item.id"-->
<!-- >-->
<!-- <span>{{ item.name }}</span>-->
<!-- <el-tag style="margin-left: 5px; position: relative; top:-2px" type="info" size="small">{{-->
<!-- item.power-->
<!-- }}算力-->
<!-- </el-tag>-->
<!-- </el-option>-->
<!-- </el-select>-->
<!-- <el-button type="primary" @click="newChat">-->
<!-- <el-icon>-->
<!-- <Plus/>-->
<!-- </el-icon>-->
<!-- 新建对话-->
<!-- </el-button>-->
<!-- <el-button type="success" @click="exportChat" plain>-->
<!-- <i class="iconfont icon-export"></i>-->
<!-- <span>导出会话</span>-->
<!-- </el-button>-->
<!-- </div>-->
<!-- </div>-->
<div class="chat-box" :style="{height: mainWinHeight+'px'}"> <div class="chat-box" :style="{height: mainWinHeight+'px'}">
<div> <div>
<div id="container"> <div id="container">
@ -160,63 +80,102 @@
:tokens="item['tokens']" :tokens="item['tokens']"
:model="getModelValue(modelID)" :model="getModelValue(modelID)"
:content="item.content"/> :content="item.content"/>
<chat-reply v-else-if="item.type==='reply'" <chat-reply v-else-if="item.type==='reply'" :data="item" @regen="reGenerate" :read-only="false"/>
:icon="item.icon"
:org-content="item.orgContent"
:created-at="dateFormat(item['created_at'])"
:tokens="item['tokens']"
:content="item.content"/>
<chat-mid-journey v-else-if="item.type==='mj'"
:content="item.content"
:role-id="item.role_id"
:chat-id="item.chat_id"
:icon="item.icon"
@disable-input="disableInput(true)"
@enable-input="enableInput"
:created-at="dateFormat(item['created_at'])"/>
</div> </div>
</div><!-- end chat box --> </div><!-- end chat box -->
<div class="re-generate"> <el-affix position="bottom" :offset="0">
<div class="btn-box"> <div class="input-box">
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain> <span class="tool-item">
<el-icon> <el-tooltip effect="dark" content="聊天设置">
<VideoPause/> <el-popover
</el-icon> :width="300"
停止生成 trigger="click"
</el-button> placement="top-start"
>
<template #reference>
<i class="iconfont icon-config"></i>
</template>
<el-button type="primary" v-if="showReGenerate" @click="reGenerate" plain> <template #default>
<el-icon> <div class="chat-config">
<RefreshRight/> <el-select v-model="roleId" filterable placeholder="角色" @change="newChat"
</el-icon> class="role-select"
重新生成 style="width:150px">
</el-button> <el-option
</div> v-for="item in roles"
</div> :key="item.id"
:label="item.name"
:value="item.id"
>
<div class="role-option">
<el-image :src="item.icon"></el-image>
<span>{{ item.name }}</span>
</div>
</el-option>
</el-select>
<div class="input-box"> <el-select v-model="modelID" filterable placeholder="模型" @change="newChat"
<div class="input-container"> :disabled="disableModel"
<el-input style="width:150px">
ref="textInput" <el-option
v-model="prompt" v-for="item in models"
v-on:keydown="inputKeyDown" :key="item.id"
autofocus :label="item.name"
type="textarea" :value="item.id"
:rows="2" >
placeholder="按 Enter 键发送消息,使用 Ctrl + Enter 换行" <span>{{ item.name }}</span>
/> <el-tag style="margin-left: 5px; position: relative; top:-2px" type="info" size="small">{{
<span class="select-file"> item.power
<file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertURL"/> }}算力
</el-tag>
</el-option>
</el-select>
</div>
</template>
</el-popover>
</el-tooltip>
</span> </span>
<span class="send-btn">
<el-button @click="sendMessage"> <span class="tool-item" @click="ElMessage.info('暂时不支持语音输入')">
<el-tooltip class="box-item" effect="dark" content="语音输入">
<i class="iconfont icon-mic-bold"></i>
</el-tooltip>
</span>
<span class="tool-item">
<el-tooltip class="box-item" effect="dark" content="上传附件">
<file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertURL"/>
</el-tooltip>
</span>
<div class="input-container">
<el-input
ref="textInput"
v-model="prompt"
v-on:keydown="inputKeyDown"
autofocus
type="textarea"
:rows="2"
style="--el-input-focus-border-color:#21AA93;
border: 1px solid #21AA93;--el-input-border-color:#21AA93;
border-radius: 5px; --el-input-hover-border-color:#21AA93;"
placeholder="按 Enter 键发送消息,使用 Ctrl + Enter 换行"
/>
<span class="send-btn">
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
<el-icon>
<VideoPause/>
</el-icon>
</el-button>
<el-button @click="sendMessage" color="#19c37d" style="color:#ffffff" v-else>
<el-icon><Promotion/></el-icon> <el-icon><Promotion/></el-icon>
</el-button> </el-button>
</span> </span>
</div> </div>
</div><!-- end input box --> </div><!-- end input box -->
</el-affix>
</div><!-- end container --> </div><!-- end container -->
</div><!-- end loading --> </div><!-- end loading -->
</div> </div>
@ -239,10 +198,6 @@
</p> </p>
</div> </div>
</el-dialog> </el-dialog>
<config-dialog v-if="isLogin" :show="showConfigDialog" :models="models" @hide="showConfigDialog = false"/>
<login-dialog :show="showLoginDialog" @hide="showLoginDialog = false" @success="initData"/>
</div> </div>
@ -251,7 +206,7 @@
import {nextTick, onMounted, onUnmounted, ref} from 'vue' import {nextTick, onMounted, onUnmounted, ref} from 'vue'
import ChatPrompt from "@/components/ChatPrompt.vue"; import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue"; import ChatReply from "@/components/ChatReply.vue";
import {Delete, Edit, More, Plus, Promotion, RefreshRight, Search, Share, VideoPause} from '@element-plus/icons-vue' import {Delete, Edit, More, Plus, Promotion, Search, Share, VideoPause} from '@element-plus/icons-vue'
import 'highlight.js/styles/a11y-dark.css' import 'highlight.js/styles/a11y-dark.css'
import {dateFormat, escapeHTML, isMobile, processContent, randString, removeArrayItem, UUID} from "@/utils/libs"; import {dateFormat, escapeHTML, isMobile, processContent, randString, removeArrayItem, UUID} from "@/utils/libs";
import {ElMessage, ElMessageBox} from "element-plus"; import {ElMessage, ElMessageBox} from "element-plus";
@ -260,12 +215,10 @@ import {getSessionId, getUserToken, removeUserToken} from "@/store/session";
import {httpGet, httpPost} from "@/utils/http"; import {httpGet, httpPost} from "@/utils/http";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import Clipboard from "clipboard"; import Clipboard from "clipboard";
import ConfigDialog from "@/components/ConfigDialog.vue";
import {checkSession} from "@/action/session"; import {checkSession} from "@/action/session";
import Welcome from "@/components/Welcome.vue"; import Welcome from "@/components/Welcome.vue";
import ChatMidJourney from "@/components/ChatMidJourney.vue"; import {useSharedStore} from "@/store/sharedata";
import FileSelect from "@/components/FileSelect.vue"; import FileSelect from "@/components/FileSelect.vue";
import LoginDialog from "@/components/LoginDialog.vue";
const title = ref('ChatGPT-智能助手'); const title = ref('ChatGPT-智能助手');
const models = ref([]) const models = ref([])
@ -283,14 +236,13 @@ const roles = ref([]);
const router = useRouter(); const router = useRouter();
const roleId = ref(0) const roleId = ref(0)
const newChatItem = ref(null); const newChatItem = ref(null);
const showConfigDialog = ref(false);
const showLoginDialog = ref(false)
const isLogin = ref(false) const isLogin = ref(false)
const showHello = ref(true) const showHello = ref(true)
const textInput = ref(null) const textInput = ref(null)
const showNotice = ref(false) const showNotice = ref(false)
const notice = ref("") const notice = ref("")
const noticeKey = ref("SYSTEM_NOTICE") const noticeKey = ref("SYSTEM_NOTICE")
const store = useSharedStore();
if (isMobile()) { if (isMobile()) {
router.replace("/mobile/chat") router.replace("/mobile/chat")
@ -426,22 +378,17 @@ const resizeElement = function () {
leftBoxHeight.value = window.innerHeight - 90 - 45 - 82; leftBoxHeight.value = window.innerHeight - 90 - 45 - 82;
}; };
const _newChat = () => {
if (isLogin.value) {
newChat()
}
}
const disableModel = ref(false) const disableModel = ref(false)
// //
const newChat = () => { const newChat = () => {
if (!isLogin.value) { if (!isLogin.value) {
showLoginDialog.value = true store.setShowLoginDialog(true)
return; return;
} }
const role = getRoleById(roleId.value) const role = getRoleById(roleId.value)
showHello.value = role.key === 'gpt'; showHello.value = role.key === 'gpt';
// if the role bind a model, disable model change // if the role bind a model, disable model change
disableModel.value = false
if (role.model_id > 0) { if (role.model_id > 0) {
modelID.value = role.model_id modelID.value = role.model_id
disableModel.value = true disableModel.value = true
@ -469,7 +416,6 @@ const newChat = () => {
}; };
activeChat.value = {} // activeChat.value = {} //
showStopGenerate.value = false; showStopGenerate.value = false;
showReGenerate.value = false;
connect(null, roleId.value) connect(null, roleId.value)
} }
@ -481,7 +427,7 @@ const changeChat = (chat) => {
const loadChat = function (chat) { const loadChat = function (chat) {
if (!isLogin.value) { if (!isLogin.value) {
showLoginDialog.value = true store.setShowLoginDialog(true)
return; return;
} }
@ -494,7 +440,6 @@ const loadChat = function (chat) {
roleId.value = chat.role_id; roleId.value = chat.role_id;
modelID.value = chat.model_id; modelID.value = chat.model_id;
showStopGenerate.value = false; showStopGenerate.value = false;
showReGenerate.value = false;
connect(chat.chat_id, chat.role_id) connect(chat.chat_id, chat.role_id)
} }
@ -600,8 +545,6 @@ md.use(mathjaxPlugin)
// socket // socket
const prompt = ref(''); const prompt = ref('');
const showStopGenerate = ref(false); // const showStopGenerate = ref(false); //
const showReGenerate = ref(false); //
const previousText = ref(''); //
const lineBuffer = ref(''); // const lineBuffer = ref(''); //
const socket = ref(null); const socket = ref(null);
const activelyClose = ref(false); // const activelyClose = ref(false); //
@ -646,7 +589,6 @@ const connect = function (chat_id, role_id) {
const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`); const _socket = new WebSocket(host + `/api/chat/new?session_id=${_sessionId}&role_id=${role_id}&chat_id=${chat_id}&model_id=${modelID.value}&token=${getUserToken()}`);
_socket.addEventListener('open', () => { _socket.addEventListener('open', () => {
chatData.value = []; // chatData.value = []; //
previousText.value = '';
enableInput() enableInput()
activelyClose.value = false; activelyClose.value = false;
@ -686,7 +628,7 @@ const connect = function (chat_id, role_id) {
} else if (data.type === 'end') { // } else if (data.type === 'end') { //
// //
if (isNewChat && newChatItem.value !== null) { if (isNewChat && newChatItem.value !== null) {
newChatItem.value['title'] = previousText.value; newChatItem.value['title'] = tmpChatTitle.value;
newChatItem.value['chat_id'] = chat_id; newChatItem.value['chat_id'] = chat_id;
chatList.value.unshift(newChatItem.value); chatList.value.unshift(newChatItem.value);
activeChat.value = newChatItem.value; activeChat.value = newChatItem.value;
@ -751,13 +693,11 @@ const connect = function (chat_id, role_id) {
const disableInput = (force) => { const disableInput = (force) => {
canSend.value = false; canSend.value = false;
showReGenerate.value = false;
showStopGenerate.value = !force; showStopGenerate.value = !force;
} }
const enableInput = () => { const enableInput = () => {
canSend.value = true; canSend.value = true;
showReGenerate.value = previousText.value !== "";
showStopGenerate.value = false; showStopGenerate.value = false;
} }
@ -782,7 +722,7 @@ const autofillPrompt = (text) => {
// //
const sendMessage = function () { const sendMessage = function () {
if (!isLogin.value) { if (!isLogin.value) {
showLoginDialog.value = true store.setShowLoginDialog(true)
return; return;
} }
@ -800,7 +740,7 @@ const sendMessage = function () {
id: randString(32), id: randString(32),
icon: loginUser.value.avatar, icon: loginUser.value.avatar,
content: md.render(escapeHTML(processContent(prompt.value))), content: md.render(escapeHTML(processContent(prompt.value))),
created_at: new Date().getTime(), created_at: new Date().getTime() / 1000,
}); });
nextTick(() => { nextTick(() => {
@ -810,15 +750,11 @@ const sendMessage = function () {
showHello.value = false showHello.value = false
disableInput(false) disableInput(false)
socket.value.send(JSON.stringify({type: "chat", content: prompt.value})); socket.value.send(JSON.stringify({type: "chat", content: prompt.value}));
previousText.value = prompt.value; tmpChatTitle.value = prompt.value
prompt.value = ''; prompt.value = '';
return true; return true;
} }
const showConfig = function () {
showConfigDialog.value = true;
}
const clearAllChats = function () { const clearAllChats = function () {
ElMessageBox.confirm( ElMessageBox.confirm(
'确认要清空所有的会话历史记录吗?<br/>此操作不可以撤销!', '确认要清空所有的会话历史记录吗?<br/>此操作不可以撤销!',
@ -866,6 +802,9 @@ const loadChatHistory = function (chatId) {
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
data[i].orgContent = data[i].content; data[i].orgContent = data[i].content;
data[i].content = md.render(processContent(data[i].content)) data[i].content = md.render(processContent(data[i].content))
if (i > 0 && data[i].type === 'reply') {
data[i].prompt = data[i - 1].orgContent
}
chatData.value.push(data[i]); chatData.value.push(data[i]);
} }
@ -887,9 +826,9 @@ const stopGenerate = function () {
} }
// //
const reGenerate = function () { const reGenerate = function (prompt) {
disableInput(false) disableInput(false)
const text = '重新生成上述问题的答案:' + previousText.value; const text = '重新生成下面问题的答案:' + prompt;
// //
chatData.value.push({ chatData.value.push({
type: "prompt", type: "prompt",
@ -897,7 +836,7 @@ const reGenerate = function () {
icon: loginUser.value.avatar, icon: loginUser.value.avatar,
content: md.render(text) content: md.render(text)
}); });
socket.value.send(JSON.stringify({type: "chat", content: previousText.value})); socket.value.send(JSON.stringify({type: "chat", content: prompt}));
} }
const chatName = ref('') const chatName = ref('')
@ -961,4 +900,37 @@ const insertURL = (url) => {
padding 0 20px padding 0 20px
} }
} }
.input-container {
.el-textarea {
.el-textarea__inner {
padding-right 40px
}
}
}
.chat-config {
display flex
flex-direction row
padding-top 10px;
.role-select-label {
color #ffffff
}
.el-select {
max-width 150px;
margin-right 10px;
}
.role-select {
max-width 130px;
}
.el-button {
.el-icon {
margin-right 5px;
}
}
}
</style> </style>

View File

@ -11,13 +11,34 @@
</div> </div>
<div class="navbar"> <div class="navbar">
<el-tooltip
class="box-item"
effect="dark"
content="部署文档"
placement="bottom">
<a href="https://ai.r9it.com/docs/install/" class="link-button" target="_blank">
<i class="iconfont icon-book"></i>
</a>
</el-tooltip>
<el-tooltip
class="box-item"
effect="dark"
content="项目源码"
placement="bottom">
<a href="https://github.com/yangjian102621/chatgpt-plus" class="link-button" target="_blank">
<i class="iconfont icon-github"></i>
</a>
</el-tooltip>
<el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="loginUser.id"> <el-dropdown :hide-on-click="true" class="user-info" trigger="click" v-if="loginUser.id">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<el-image :src="loginUser.avatar"/> <el-image :src="loginUser.avatar"/>
</span> </span>
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu class="user-info-menu">
<el-dropdown-item> <el-dropdown-item @click="showConfigDialog = true">
<el-icon> <el-icon>
<UserFilled/> <UserFilled/>
</el-icon> </el-icon>
@ -26,22 +47,18 @@
<el-dropdown-item> <el-dropdown-item>
<i class="iconfont icon-book"></i> <i class="iconfont icon-book"></i>
<span> <a href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank"> 用户手册
用户手册 </a>
</el-link>
</span>
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item> <el-dropdown-item>
<i class="iconfont icon-github"></i> <i class="iconfont icon-github"></i>
<span> <a href="https://ai.r9it.com/docs/" target="_blank">
<el-link type="primary" href="https://ai.r9it.com/docs/" target="_blank"> Geek-AI {{ version }}
Geek-AI {{ version }} </a>
</el-link>
</span>
</el-dropdown-item> </el-dropdown-item>
<el-divider style="margin: 2px 0"/>
<el-dropdown-item @click="logout"> <el-dropdown-item @click="logout">
<i class="iconfont icon-logout"></i> <i class="iconfont icon-logout"></i>
<span>退出登录</span> <span>退出登录</span>
@ -49,6 +66,11 @@
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
<div v-else>
<el-button size="small" color="#21aa93" @click="show = true" round>登录</el-button>
<el-button size="small" @click="router.push('/register')" round>注册</el-button>
</div>
</div> </div>
</div> </div>
<div class="main"> <div class="main">
@ -92,25 +114,31 @@
</div> </div>
<div class="content" :style="{height: mainWinHeight+'px'}"> <div class="content" :style="{height: mainWinHeight+'px'}">
<router-view v-slot="{ Component }"> <router-view :key="routerViewKey" v-slot="{ Component }">
<transition name="move" mode="out-in"> <transition name="move" mode="out-in">
<component :is="Component"></component> <component :is="Component"></component>
</transition> </transition>
</router-view> </router-view>
</div> </div>
</div> </div>
<login-dialog :show="show" @hide="store.setShowLoginDialog(false)" @success="loginCallback"/>
<config-dialog v-if="loginUser.id" :show="showConfigDialog" @hide="showConfigDialog = false"/>
</div> </div>
</template> </template>
<script setup> <script setup>
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {onMounted, ref} from "vue"; import {onMounted, ref, watch} from "vue";
import {httpGet} from "@/utils/http"; import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus"; import {ElMessage} from "element-plus";
import {UserFilled} from "@element-plus/icons-vue"; import {UserFilled} from "@element-plus/icons-vue";
import {checkSession} from "@/action/session"; import {checkSession} from "@/action/session";
import {removeUserToken} from "@/store/session"; import {removeUserToken} from "@/store/session";
import LoginDialog from "@/components/LoginDialog.vue";
import {useSharedStore} from "@/store/sharedata";
import ConfigDialog from "@/components/ConfigDialog.vue";
const router = useRouter(); const router = useRouter();
const logo = ref('/images/logo.png'); const logo = ref('/images/logo.png');
@ -121,6 +149,14 @@ const title = ref("")
const mainWinHeight = window.innerHeight - 50 const mainWinHeight = window.innerHeight - 50
const loginUser = ref({}) const loginUser = ref({})
const version = ref(process.env.VUE_APP_VERSION) const version = ref(process.env.VUE_APP_VERSION)
const routerViewKey = ref(0)
const showConfigDialog = ref(false)
const store = useSharedStore();
const show = ref(false)
watch(() => store.showLoginDialog, (newValue) => {
show.value = newValue
});
if (curPath.value === "/external") { if (curPath.value === "/external") {
curPath.value = router.currentRoute.value.query.url curPath.value = router.currentRoute.value.query.url
@ -154,23 +190,35 @@ onMounted(() => {
ElMessage.error("获取系统菜单失败:" + e.message) ElMessage.error("获取系统菜单失败:" + e.message)
}) })
init()
})
const init = () => {
checkSession().then(user => { checkSession().then(user => {
loginUser.value = user loginUser.value = user
}).catch(() => { }).catch(() => {
}) })
}) }
const logout = function () { const logout = function () {
httpGet('/api/user/logout').then(() => { httpGet('/api/user/logout').then(() => {
removeUserToken() removeUserToken()
router.push("/login") store.setShowLoginDialog(true)
loginUser.value = {}
//
routerViewKey.value += 1
}).catch(() => { }).catch(() => {
ElMessage.error('注销失败!'); ElMessage.error('注销失败!');
}) })
} }
const loginCallback = () => {
init()
//
routerViewKey.value += 1
}
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
@import '@/assets/iconfont/iconfont.css';
@import "@/assets/css/home.styl" @import "@/assets/css/home.styl"
</style> </style>

View File

@ -36,8 +36,12 @@
<el-row class="opt" :gutter="20"> <el-row class="opt" :gutter="20">
<el-col :span="8"><el-link type="primary" @click="router.push('/register')">注册</el-link></el-col> <el-col :span="8"><el-link type="primary" @click="router.push('/register')">注册</el-link></el-col>
<el-col :span="8"><el-link @click="showResetPass = true">重置密码</el-link></el-col> <el-col :span="8">
<el-col :span="8"><el-link @click="router.push('/')">首页</el-link></el-col> <el-link type="info" @click="showResetPass = true">重置密码</el-link>
</el-col>
<el-col :span="8">
<el-link type="info" @click="router.push('/')">首页</el-link>
</el-col>
</el-row> </el-row>
</div> </div>
</div> </div>