新增 Plus 黑色风格模板

This commit is contained in:
RockYang 2023-04-07 17:58:11 +08:00
parent b6d8465127
commit 5a6f070f92
9 changed files with 842 additions and 11 deletions

View File

@ -57,11 +57,13 @@ func (s *Server) ChatHandle(c *gin.Context) {
}
logger.Info("Receive a message: ", string(message))
replyMessage(client, "当前 TOKEN 无效,请使用合法的 TOKEN 登录!", false)
replyMessage(client, "![](images/wx.png)", true)
// TODO: 当前只保持当前会话的上下文,部保存用户的所有的聊天历史记录,后期要考虑保存所有的历史记录
err = s.sendMessage(session, chatRole, string(message), client, false)
if err != nil {
logger.Error(err)
}
//err = s.sendMessage(session, chatRole, string(message), client, false)
//if err != nil {
// logger.Error(err)
//}
}
}()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -62,6 +62,7 @@ export default defineComponent({
}
.content {
min-height 20px;
word-break break-word;
padding: 8px 10px;
color var(--content-color)

View File

@ -0,0 +1,85 @@
<template>
<div class="chat-line chat-line-right">
<div class="chat-item">
<div class="content">{{ content }}</div>
<div class="triangle"></div>
</div>
<div class="chat-icon">
<img :src="icon" alt="User"/>
</div>
</div>
</template>
<script>
import {defineComponent} from "vue"
export default defineComponent({
name: 'ChatPrompt',
props: {
content: {
type: String,
default: '',
},
icon: {
type: String,
default: 'images/user-icon.png',
}
},
data() {
return {}
},
})
</script>
<style lang="stylus">
.chat-line-right {
justify-content: flex-end;
.chat-icon {
margin-left 5px;
img {
border-radius 5px;
}
}
.chat-item {
position: relative;
padding: 0 5px 0 0;
overflow: hidden;
.triangle {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 5px solid #223A34;
position: absolute;
right: 0;
top: 10px;
}
.content {
word-break break-word;
padding: 12px 15px;
background-color: #223A34;
color var(--content-color);
font-size: var(--content-font-size);
border-radius: 5px;
p {
line-height 1.5
}
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
}
}
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div class="chat-line chat-line-left">
<div class="chat-icon">
<img :src="icon" alt="ChatGPT">
</div>
<div class="chat-item">
<div class="triangle"></div>
<div class="content" v-html="content"></div>
</div>
</div>
</template>
<script>
import {defineComponent} from "vue"
export default defineComponent({
name: 'ChatReply',
props: {
content: {
type: String,
default: '',
},
icon: {
type: String,
default: 'images/gpt-icon.png',
}
},
data() {
return {}
},
})
</script>
<style lang="stylus">
.chat-line-left {
justify-content: flex-start;
.chat-icon {
margin-right 5px;
img {
border-radius 5px;
}
}
.chat-item {
display: inline-block;
position: relative;
padding: 0 0 0 5px;
overflow: hidden;
.triangle {
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-right: 5px solid #404042;
position: absolute;
left: 0;
top: 13px;
}
.content {
min-height 20px;
word-break break-word;
padding: 12px 15px;
color var(--content-color)
background-color: #404042;
font-size: var(--content-font-size);
border-radius: 5px;
p {
line-height 1.5
}
p:last-child {
margin-bottom: 0
}
p:first-child {
margin-top 0
}
p > code {
color #cc0000
background-color #f1f1f1
}
}
}
}
</style>

View File

@ -3,17 +3,14 @@ import {createApp} from 'vue'
import ElementPlus from "element-plus"
import "element-plus/dist/index.css"
import App from './App.vue'
import Chat from './views/Chat.vue'
import ChatPlus from "@/views/ChatPlus.vue";
import NotFound from './views/404.vue'
import TestPage from './views/Test.vue'
import './utils/prototype'
import {Global} from "@/utils/storage";
Global['Chat'] = Chat
const routes = [
{
name: 'home', path: '/', component: Chat, meta: {
name: 'chat-plus', path: '/', component: ChatPlus, meta: {
title: 'ChatGPT-Plus'
}
},

View File

@ -132,7 +132,7 @@ export default defineComponent({
connectingMessageBox: null, //
errorMessage: null, //
socket: null,
toolBoxHeight: 61 + 42, //
toolBoxHeight: 61 + 52, //
inputBoxWidth: window.innerWidth - 20,
sending: true,
loading: true
@ -486,6 +486,8 @@ export default defineComponent({
.body {
background-color: rgba(247, 247, 248, 1);
background-image url("~@/assets/img/bg_01.jpeg")
display flex;
//justify-content center;
align-items flex-start;
@ -544,7 +546,7 @@ export default defineComponent({
.input-box {
padding 10px;
width 100%;
background #ffffff;
position: absolute;
bottom: 0

651
web/src/views/ChatPlus.vue Normal file
View File

@ -0,0 +1,651 @@
<template>
<div class="body">
<el-row>
<div class="chat-head">
<el-row class="row-center">
<el-col :span="12">
<div class="title-box">
<el-image :src="logo" class="logo"/>
<span>ChatGPT-Plus</span>
</div>
</el-col>
<el-col :span="12">
<div class="tool-box">
<el-button type="danger" class="clear-history" size="small" circle @click="clearChatHistory">
<el-icon>
<Delete/>
</el-icon>
</el-button>
<el-button type="info" size="small" class="config" ref="send-btn" circle
@click="showConnectDialog = true">
<el-icon>
<Tools/>
</el-icon>
</el-button>
</div>
</el-col>
</el-row>
</div>
</el-row>
<el-row>
<div class="left-box">
<div class="grid-content">
</div>
</div>
<div class="right-box" :style="{height: mainWinHeight+'px'}">
<div v-loading="loading">
<div id="container">
<div class="chat-box" id="chat-box" :style="{height: chatBoxHeight+'px'}">
<div v-for="chat in chatData" :key="chat.id">
<chat-prompt
v-if="chat.type==='prompt'"
:icon="chat.icon"
:content="chat.content"/>
<chat-reply v-else-if="chat.type==='reply'"
:icon="chat.icon"
:content="chat.content"/>
</div>
</div><!-- end chat box -->
<el-row class="chat-tool-box">
<el-tooltip
class="box-item"
effect="dark"
content="进入AI绘画模式"
placement="top"
>
<el-icon @click="drawImage">
<Picture/>
</el-icon>
</el-tooltip>
</el-row>
<div class="input-box">
<div class="input-container">
<el-input
ref="text-input"
v-model="inputValue"
:autosize="{ minRows: 5, maxRows: 10 }"
v-on:keydown="inputKeyDown"
v-on:focus="focus"
autofocus
type="textarea"
placeholder="先聊五毛钱吧..."
/>
</div>
</div><!-- end input box -->
</div><!-- end container -->
</div><!-- end loading -->
</div>
</el-row>
<config-dialog v-model:show="showConnectDialog"></config-dialog>
<div class="token-dialog">
<el-dialog
v-model="showLoginDialog"
:show-close="false"
:close-on-click-modal="false"
title="请输入口令继续访问"
>
<el-row>
<el-input v-model="token" placeholder="在此输入口令" @keyup="loginInputKeyup">
<template #prefix>
<el-icon class="el-input__icon">
<Lock/>
</el-icon>
</template>
</el-input>
<el-button type="primary" @click="submitToken">提交</el-button>
</el-row>
<el-row class="row-center">
<p>打开微信扫下面二维码免费领取口令</p>
</el-row>
<el-row class="row-center">
<el-image src="images/wx.png" fit="cover"/>
</el-row>
</el-dialog>
</div> <!--end token dialog-->
</div>
</template>
<script>
import {defineComponent, nextTick} from 'vue'
import ChatPrompt from "@/components/plus/ChatPrompt.vue";
import ChatReply from "@/components/plus/ChatReply.vue";
import {randString} from "@/utils/libs";
import {ElMessage, ElMessageBox} from 'element-plus'
import {Tools, Lock, Delete, Picture} from '@element-plus/icons-vue'
import ConfigDialog from '@/components/ConfigDialog.vue'
import {httpPost, httpGet} from "@/utils/http";
import {getSessionId, setSessionId} from "@/utils/storage";
import hl from 'highlight.js'
import 'highlight.js/styles/a11y-dark.css'
export default defineComponent({
name: "ChatPlus",
components: {ChatPrompt, ChatReply, Tools, Lock, Delete, Picture, ConfigDialog},
data() {
return {
title: 'ChatGPT 控制台',
logo: 'images/logo.png',
chatData: [],
chatRoles: [],
role: 'gpt',
inputValue: '', //
showConnectDialog: false,
showLoginDialog: false,
token: '', // token
replyIcon: 'images/avatar/gpt.png', //
lineBuffer: '', //
connectingMessageBox: null, //
errorMessage: null, //
socket: null,
mainWinHeight: 0, //
chatBoxHeight: 0, //
sending: true,
loading: true
}
},
mounted: function () {
nextTick(() => {
this.resizeElement();
})
window.addEventListener("resize", () => {
this.resizeElement();
});
this.connect();
},
methods: {
resizeElement: function () {
this.chatBoxHeight = window.innerHeight - 61 - 115 - 38;
this.mainWinHeight = window.innerHeight - 61;
},
// socket
connect: function () {
// WebSocket
const sessionId = getSessionId();
const socket = new WebSocket(process.env.VUE_APP_WS_HOST + `/api/chat?sessionId=${sessionId}&role=${this.role}`);
socket.addEventListener('open', () => {
//
if (this.chatRoles.length === 0) {
httpGet("/api/config/chat-roles/get").then((res) => {
// ElMessage.success('');
this.chatRoles = res.data;
this.loading = false
}).catch(() => {
ElMessage.error("获取聊天角色失败");
})
} else {
this.loading = false
}
this.sending = false; //
if (this.errorMessage !== null) {
this.errorMessage.close(); //
}
//
this.fetchChatHistory();
});
socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
const reader = new FileReader();
reader.readAsText(event.data, "UTF-8");
reader.onload = () => {
const data = JSON.parse(String(reader.result));
if (data.type === 'start') {
this.chatData.push({
type: "reply",
id: randString(32),
icon: this.replyIcon,
content: "",
cursor: true
});
} else if (data.type === 'end') {
this.sending = false;
this.lineBuffer = ''; //
} else {
this.lineBuffer += data.content;
let md = require('markdown-it')();
this.chatData[this.chatData.length - 1]["content"] = md.render(this.lineBuffer);
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const lines = document.querySelectorAll('.chat-line');
const blocks = lines[lines.length - 1].querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
}
//
nextTick(() => {
document.getElementById('container').scrollTo(0, document.getElementById('container').scrollHeight)
})
};
}
});
socket.addEventListener('close', () => {
//
this.sending = true;
this.checkSession();
});
this.socket = socket;
},
checkSession: function () {
//
httpGet("/api/session/get").then(() => {
//
this.connect();
}).catch((res) => {
if (res.code === 400) {
this.showLoginDialog = true;
if (this.errorMessage !== null) {
this.errorMessage.close();
}
} else {
if (this.errorMessage === null) {
this.errorMessage = ElMessage({
message: '当前无法连接服务器,可检查网络设置是否正常',
type: 'error',
duration: 0,
showClose: false
});
}
// 3
setTimeout(() => this.checkSession(), 3000)
}
})
},
drawImage: function () {
ElMessage({
message: '客观别急AI 绘画服服务正在紧锣密鼓搭建中...',
type: 'info',
})
},
//
changeRole: function () {
this.loading = true
//
this.chatData = [];
this.connect();
for (const key in this.chatRoles) {
if (this.chatRoles[key].key === this.role) {
this.replyIcon = this.chatRoles[key].icon;
break;
}
}
},
//
fetchChatHistory: function () {
httpPost("/api/chat/history", {role: this.role}).then((res) => {
if (this.chatData.length > 0) { //
return
}
const data = res.data
const md = require('markdown-it')();
for (let i = 0; i < data.length; i++) {
if (data[i].type === "prompt") {
this.chatData.push(data[i]);
continue;
}
data[i].content = md.render(data[i].content);
this.chatData.push(data[i]);
}
nextTick(() => {
hl.configure({ignoreUnescapedHTML: true})
const blocks = document.querySelector("#chat-box").querySelectorAll('pre code');
blocks.forEach((block) => {
hl.highlightElement(block)
})
})
}).catch(() => {
// console.error(e.message)
})
},
inputKeyDown: function (e) {
if (e.keyCode === 13) {
if (this.sending) {
e.preventDefault();
} else {
this.sendMessage();
}
}
},
//
sendMessage: function (e) {
//
if (e) {
let target = e.target;
if (target.nodeName === "SPAN") {
target = e.target.parentNode;
}
target.blur();
}
if (this.sending || this.inputValue.trim().length === 0) {
return false;
}
//
this.chatData.push({
type: "prompt",
id: randString(32),
icon: 'images/avatar/user.png',
content: this.inputValue
});
this.sending = true;
this.socket.send(this.inputValue);
this.$refs["text-input"].blur();
this.inputValue = '';
// textarea
setTimeout(() => this.$refs["text-input"].focus(), 100);
return true;
},
//
focus: function () {
setTimeout(function () {
document.getElementById('chat-box').scrollTo(0, document.getElementById('chat-box').scrollHeight)
}, 200)
},
// Token
submitToken: function () {
this.showLoginDialog = false;
this.loading = true
//
httpPost("/api/login", {
token: this.token
}).then((res) => {
setSessionId(res.data)
this.connect();
this.loading = false;
}).catch(() => {
ElMessage.error("口令错误");
this.token = '';
this.showLoginDialog = true;
this.loading = false;
})
},
//
loginInputKeyup: function (e) {
if (e.keyCode === 13) {
this.submitToken();
}
},
//
clearChatHistory: function () {
ElMessageBox.confirm(
'确认要清空当前角色聊天历史记录吗?<br/>此操作不可以撤销!',
'操作提示:',
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
dangerouslyUseHTMLString: true,
showClose: true,
closeOnClickModal: false,
center: true,
}
).then(() => {
httpPost("/api/chat/history/clear", {role: this.role}).then(() => {
ElMessage.success("当前角色会话已清空");
this.chatData = [];
}).catch(() => {
ElMessage.error("删除失败")
})
}).catch(() => {
})
}
},
})
</script>
<style lang="stylus">
#app {
height: 100%;
.body {
height 100%;
.chat-head {
width 100%;
height 60px;
background-color: #28292A
border-bottom 1px solid #4f4f4f;
.title-box {
padding-top 6px;
display flex
color #ffffff;
font-size 20px;
.logo {
background-color #ffffff
border-radius 50%;
width 45px;
height 45px;
}
span {
padding-top: 12px;
padding-left: 10px;
}
}
.tool-box {
padding-top 16px;
padding-right 20px;
display flex;
justify-content flex-end;
align-items center;
.el-image {
margin-right 5px;
}
.clear-history, .config {
margin-left 5px;
}
}
}
.el-row {
overflow hidden;
display: flex;
.left-box {
display flex
min-width 220px;
max-width 250px;
background-color: #28292A
border-top: 1px solid #2F3032
border-right: 1px solid #2F3032
}
.right-box {
min-width: 0;
flex: 1;
background-color #232425
border-left 1px solid #4f4f4f
}
}
#container {
overflow hidden;
width 100%;
::-webkit-scrollbar {
width: 0;
height: 0;
background-color: transparent;
}
.chat-box {
overflow-y: scroll;
border-bottom 1px solid #4f4f4f
//
--content-font-size: 16px;
--content-color: #c1c1c1;
font-family 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
padding: 10px;
.chat-line {
padding 10px 5px;
font-size 14px;
display: flex;
align-items: flex-start;
.chat-icon {
img {
width 45px;
height 45px;
border 1px solid #666;
border-radius 50%;
padding 1px;
}
}
}
}
.chat-tool-box {
padding 10px;
border-top: 1px solid #2F3032
.el-icon svg {
color #cccccc
width 1em;
background-color #232425
cursor pointer
}
}
.input-box {
background-color #232425
display: flex;
justify-content: start;
align-items: center;
.input-container {
width: 100%
margin: 0;
border: none;
border-radius: 6px;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
background-color #232425
padding: 5px 10px;
.el-textarea__inner {
box-shadow: none
padding 5px 0
background-color #232425
color #B5B7B8
}
.el-textarea__inner::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.btn-container {
margin-left 10px;
.el-row {
flex-wrap nowrap
//width 106px;
align-items center
}
.send {
width 60px;
height 40px;
background-color: var(--el-color-success)
}
.is-disabled {
background-color: var(--el-button-disabled-bg-color);
border-color: var(--el-button-disabled-border-color);
}
}
}
}
#container::-webkit-scrollbar {
width: 0;
height: 0;
}
.row-center {
justify-content center
}
}
}
.el-message-box {
width 90%;
max-width 420px;
}
.el-message {
min-width: 100px;
max-width 600px;
}
.token-dialog {
.el-dialog {
--el-dialog-width 90%;
max-width 400px;
.el-dialog__body {
padding 10px 10px 20px 10px;
}
.el-row {
flex-wrap nowrap
button {
margin-left 5px;
}
}
}
}
</style>