支持按次收费的 OpenAI 实时语音通话功能

This commit is contained in:
RockYang
2024-12-20 18:21:54 +08:00
24 changed files with 1956 additions and 821 deletions

View File

@@ -6,6 +6,6 @@ VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=GeekAI_DEV_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.1.6
VUE_APP_VERSION=v4.1.7
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai

View File

@@ -1,6 +1,6 @@
VUE_APP_API_HOST=
VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=GeekAI_
VUE_APP_VERSION=v4.1.6
VUE_APP_VERSION=v4.1.7
VUE_APP_DOCS_URL=https://docs.geekai.me
VUE_APP_GIT_URL=https://github.com/yangjian102621/geekai

View File

@@ -1,14 +1,74 @@
.el-form-item__content {
.tip-input {
display flex
width 100%
.form {
.el-form-item__label {
.label-title {
display flex
align-items center
.el-input,.el-select,.el-switch {
margin-right 10px
}
.info {
margin-top 2px
.el-icon {
margin-left 5px
cursor pointer
}
}
}
}
.el-form-item__content {
width 100%
.uploader-icon {
font-size 24px
position relative
top 3px
}
.tip-input-line {
.tip {
margin-top 10px
color #c1c1c1
font-size 12px;
line-height 1.5;
}
}
}
.el-input {
width 100%
}
.text {
font-size 14px
}
.active-info {
line-height 1.5
padding 10px 0 30px 0
}
.el-descriptions {
margin-bottom 20px
.el-icon {
font-size 18px
}
.selected {
color #0bc15f
}
.closed {
color #da0d54
}
.text {
margin-left 10px
font-size 12px
color #999999
position: relative;
top -5px
}
}
.el-alert {
margin-bottom 15px;
}
}

View File

@@ -345,13 +345,14 @@
justify-content: flex-end;
align-items: center;
gap: 8px;
.tool-item-btn{
.iconfont{
.iconfont{
font-size: 19px;
cursor pointer
background-color: var(--chat-content-bg);
padding: 5px;
border-radius: 6px;
}
}
}
.add-new{

View File

@@ -172,6 +172,58 @@ body {
}
}
.w-100 {
width 100%
}
.mr-1 {
margin-right 0.5rem
}
.mr-2 {
margin-right 1rem
}
.ml-1 {
margin-left 0.5rem
}
.ml-2 {
margin-left 1rem
}
.d-flex {
display flex !important
}
.justify-center {
justify-content center
}
.justify-between {
justify-content space-between
}
.justify-end {
justify-content flex-end
}
.align-center {
align-items center
}
.p-1 {
padding 0.5rem
}
.p-2 {
padding 1rem
}
.m-1 {
margin 0.5rem
}
.m-2 {
margin 1rem
}

View File

@@ -6,23 +6,14 @@
</div>
<div class="chat-item">
<div
class="content"
v-html="md.render(processContent(data.content))"
></div>
<div class="content" v-html="md.render(processContent(data.content))"></div>
<div class="bar" v-if="data.created_at">
<span class="bar-item"
><el-icon><Clock /></el-icon>
{{ dateFormat(data.created_at) }}</span
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<span class="bar-item">tokens: {{ data.tokens }}</span>
<span class="bar-item">
<el-tooltip
class="box-item"
effect="dark"
content="复制回答"
placement="bottom"
>
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy />
</el-icon>
@@ -30,23 +21,13 @@
</span>
<span v-if="!readOnly">
<span class="bar-item" @click="reGenerate(data.prompt)">
<el-tooltip
class="box-item"
effect="dark"
content="重新生成"
placement="bottom"
>
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
<el-icon><Refresh /></el-icon>
</el-tooltip>
</span>
<span class="bar-item" @click="synthesis(data.content)">
<el-tooltip
class="box-item"
effect="dark"
content="生成语音朗读"
placement="bottom"
>
<el-tooltip class="box-item" effect="dark" content="生成语音朗读" placement="bottom">
<i class="iconfont icon-speaker"></i>
</el-tooltip>
</span>
@@ -75,24 +56,15 @@
</div>
<div class="chat-item">
<div class="content-wrapper">
<div
class="content"
v-html="md.render(processContent(data.content))"
></div>
<div class="content" v-html="md.render(processContent(data.content))"></div>
</div>
<div class="bar" v-if="data.created_at">
<span class="bar-item"
><el-icon><Clock /></el-icon>
{{ dateFormat(data.created_at) }}</span
><el-icon><Clock /></el-icon> {{ dateFormat(data.created_at) }}</span
>
<!-- <span class="bar-item">tokens: {{ data.tokens }}</span>-->
<span class="bar-item bg">
<el-tooltip
class="box-item"
effect="dark"
content="复制回答"
placement="bottom"
>
<el-tooltip class="box-item" effect="dark" content="复制回答" placement="bottom">
<el-icon class="copy-reply" :data-clipboard-text="data.content">
<DocumentCopy />
</el-icon>
@@ -100,23 +72,13 @@
</span>
<span v-if="!readOnly">
<span class="bar-item bg" @click="reGenerate(data.prompt)">
<el-tooltip
class="box-item"
effect="dark"
content="重新生成"
placement="bottom"
>
<el-tooltip class="box-item" effect="dark" content="重新生成" placement="bottom">
<el-icon><Refresh /></el-icon>
</el-tooltip>
</span>
<span class="bar-item bg" @click="synthesis(data.content)">
<el-tooltip
class="box-item"
effect="dark"
content="生成语音朗读"
placement="bottom"
>
<el-tooltip class="box-item" effect="dark" content="生成语音朗读" placement="bottom">
<i class="iconfont icon-speaker"></i>
</el-tooltip>
</span>
@@ -140,17 +102,17 @@ const props = defineProps({
icon: "",
content: "",
created_at: "",
tokens: 0
}
tokens: 0,
},
},
readOnly: {
type: Boolean,
default: false
default: false,
},
listStyle: {
type: String,
default: "list"
}
default: "list",
},
});
const mathjaxPlugin = require("markdown-it-mathjax3");
@@ -160,8 +122,7 @@ const md = require("markdown-it")({
linkify: true,
typographer: true,
highlight: function (str, lang) {
const codeIndex =
parseInt(Date.now()) + Math.floor(Math.random() * 10000000);
const codeIndex = parseInt(Date.now()) + Math.floor(Math.random() * 10000000);
// 显示复制代码按钮
const copyBtn = `<span class="copy-code-btn" data-clipboard-action="copy" data-clipboard-target="#copy-target-${codeIndex}">复制</span>
<textarea style="position: absolute;top: -9999px;left: -9999px;z-index: -9999;" id="copy-target-${codeIndex}">${str.replace(
@@ -180,7 +141,7 @@ const md = require("markdown-it")({
const preCode = md.utils.escapeHtml(str);
// 将代码包裹在 pre 中
return `<pre class="code-container"><code class="language-${lang} hljs">${preCode}</code>${copyBtn}</pre>`;
}
},
});
md.use(mathjaxPlugin);
@@ -257,7 +218,7 @@ const reGenerate = (prompt) => {
code {
color:var(--theme-text-color-primary);
background-color var(--el-color-primary-light-3)
padding 0 3px;
padding 3px 5px;
border-radius 5px;
}
}
@@ -349,7 +310,6 @@ const reGenerate = (prompt) => {
.bar-item {
background-color var( --little-btn-bg);
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;
@@ -434,7 +394,8 @@ const reGenerate = (prompt) => {
code {
color:var(--theme-text-color-primary);
background-color var( --little-btn-bg)
padding 0 3px;
padding 3px 5px;
font-weight 600;
border-radius 5px;
}
}
@@ -526,7 +487,6 @@ const reGenerate = (prompt) => {
padding 10px 10px 10px 0;
.bar-item {
color #888
padding 3px 5px;
margin-right 10px;
border-radius 5px;

View File

@@ -1,7 +1,7 @@
/**
* Util lib functions
*/
import {showConfirmDialog, showFailToast, showSuccessToast, showToast} from "vant";
import {showConfirmDialog, showFailToast, showSuccessToast, showToast, showLoadingToast, closeToast} from "vant";
import {isMobile} from "@/utils/libs";
import {ElMessage} from "element-plus";
@@ -41,3 +41,11 @@ export function showMessageError(message) {
ElMessage.error(message)
}
}
export function showLoading(message = '正在处理...') {
showLoadingToast({ message: message, forbidClick: true, duration: 0 })
}
export function closeLoading() {
closeToast()
}

View File

@@ -6,12 +6,12 @@
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import axios from 'axios'
import {getAdminToken, getSessionId, getUserToken, removeAdminToken, removeUserToken} from "@/store/session";
import {getAdminToken, getUserToken, removeAdminToken, removeUserToken} from "@/store/session";
axios.defaults.timeout = 180000
axios.defaults.baseURL = process.env.VUE_APP_API_HOST
axios.defaults.withCredentials = true;
axios.defaults.headers.post['Content-Type'] = 'application/json'
//axios.defaults.headers.post['Content-Type'] = 'application/json'
// HTTP拦截器
axios.interceptors.request.use(
@@ -81,4 +81,19 @@ export function httpDownload(url) {
reject(err)
})
})
}
export function httpPostDownload(url, data) {
return new Promise((resolve, reject) => {
axios({
method: 'POST',
url: url,
data: data,
responseType: 'blob' // 将响应类型设置为 `blob`
}).then(response => {
resolve(response)
}).catch(err => {
reject(err)
})
})
}

View File

@@ -3,21 +3,15 @@
<el-container>
<el-aside>
<div class="media-page">
<!-- <el-button @click="_newChat" color="#21aa93">
<el-button @click="_newChat" type="primary" style="margin-bottom: 10px">
<el-icon style="margin-right: 5px">
<Plus/>
<Plus />
</el-icon>
新建对话
</el-button> -->
</el-button>
<div class="search-box">
<el-input
v-model="chatName"
placeholder="搜索会话"
@keyup="searchChat($event)"
style=""
class="search-input"
>
<el-input v-model="chatName" placeholder="搜索会话" @keyup="searchChat($event)" style="" class="search-input">
<template #prefix>
<el-icon class="el-input__icon">
<Search />
@@ -28,14 +22,7 @@
<div class="content" :style="{ height: leftBoxHeight + 'px' }">
<el-row v-for="chat in chatList" :key="chat.chat_id">
<div
:class="
chat.chat_id === chatId
? 'chat-list-item active'
: 'chat-list-item'
"
@click="loadChat(chat)"
>
<div :class="chat.chat_id === chatId ? 'chat-list-item active' : 'chat-list-item'" @click="loadChat(chat)">
<el-image :src="chat.icon" class="avatar" />
<span class="chat-title-input" v-if="chat.edit">
<el-input
@@ -52,34 +39,23 @@
<span class="chat-opt">
<el-dropdown trigger="click">
<span
class="el-dropdown-link"
@click="stopPropagation($event)"
>
<span class="el-dropdown-link" @click="stopPropagation($event)">
<el-icon><More /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item
:icon="Edit"
@click="editChatTitle(chat)"
>重命名</el-dropdown-item
>
<el-dropdown-item :icon="Edit" @click="editChatTitle(chat)">重命名</el-dropdown-item>
<el-dropdown-item
:icon="Delete"
style="
--el-text-color-regular: var(--el-color-danger);
--el-dropdown-menuItem-hover-fill: #f8e1de;
--el-dropdown-menuItem-hover-color: var(
--el-color-danger
);
--el-dropdown-menuItem-hover-color: var(--el-color-danger);
"
@click="removeChat(chat)"
>删除</el-dropdown-item
>
<el-dropdown-item :icon="Share" @click="shareChat(chat)"
>分享</el-dropdown-item
>
<el-dropdown-item :icon="Share" @click="shareChat(chat)">分享</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
@@ -90,31 +66,14 @@
</div>
<div class="tool-box">
<el-button type="primary" size="small" @click="clearAllChats">
<i class="iconfont icon-clear"></i> 清除所有对话
</el-button>
<el-button type="primary" size="small" @click="clearAllChats"> <i class="iconfont icon-clear"></i> 清除所有对话 </el-button>
</div>
</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-container">
<div class="chat-config">
<el-select
v-model="roleId"
filterable
placeholder="角色"
@change="_newChat"
class="role-select"
style="width: 150px"
>
<el-option
v-for="item in roles"
:key="item.id"
:label="item.name"
:value="item.id"
>
<el-select v-model="roleId" filterable placeholder="角色" @change="_newChat" class="role-select" 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>
@@ -122,43 +81,21 @@
</el-option>
</el-select>
<el-select
v-model="modelID"
filterable
placeholder="模型"
@change="_newChat"
:disabled="disableModel"
style="width: 150px"
>
<el-option
v-for="item in models"
:key="item.id"
:label="item.name"
:value="item.id"
>
<el-select v-model="modelID" filterable 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-tag style="margin-left: 5px; position: relative; top: -2px" type="info" size="small">{{ item.power }}算力 </el-tag>
</el-option>
</el-select>
<div class="flex-center">
<el-dropdown :hide-on-click="false" trigger="click">
<span class="setting"
><i class="iconfont icon-plugin"></i
></span>
<span class="setting"><i class="iconfont icon-plugin"></i></span>
<template #dropdown>
<el-dropdown-menu class="tools-dropdown">
<el-checkbox-group v-model="toolSelected">
<el-dropdown-item v-for="item in tools" :key="item.id">
<el-checkbox :value="item.id" :label="item.label" />
<el-tooltip
:content="item.description"
placement="right"
>
<el-tooltip :content="item.description" placement="right">
<el-icon><InfoFilled /></el-icon>
</el-tooltip>
</el-dropdown-item>
@@ -175,27 +112,13 @@
<div>
<div id="container" :style="{ height: mainWinHeight + 'px' }">
<div
class="chat-box"
id="chat-box"
:style="{ height: chatBoxHeight + 'px' }"
>
<div class="chat-box" id="chat-box" :style="{ height: chatBoxHeight + 'px' }">
<div v-if="showHello">
<welcome @send="autofillPrompt" />
</div>
<div v-for="item in chatData" :key="item.id" v-else>
<chat-prompt
v-if="item.type === 'prompt'"
:data="item"
:list-style="listStyle"
/>
<chat-reply
v-else-if="item.type === 'reply'"
:data="item"
@regen="reGenerate"
:read-only="false"
:list-style="listStyle"
/>
<chat-prompt v-if="item.type === 'prompt'" :data="item" :list-style="listStyle" />
<chat-reply v-else-if="item.type === 'reply'" :data="item" @regen="reGenerate" :read-only="false" :list-style="listStyle" />
</div>
<back-top :right="30" :bottom="155" />
@@ -248,66 +171,34 @@
</textarea>
</div>
<div class="flex-between">
<div @click="_newChat" class="flex-center add-new">
<el-tooltip
class="box-item"
effect="dark"
content="新建会话"
>
<el-icon><CirclePlusFilled /></el-icon>
</el-tooltip>
</div>
<div class="flex little-btns">
<span class="send-btn tool-item-btn">
<!-- showStopGenerate -->
<el-button
type="info"
v-if="showStopGenerate"
@click="stopGenerate"
plain
>
<el-icon>
<VideoPause />
</el-icon>
</el-button>
<el-button
@click="sendMessage"
style="color: #754ff6"
v-else
>
<el-tooltip
class="box-item"
effect="dark"
content="发送"
>
<el-icon><Promotion /></el-icon>
</el-tooltip>
</el-button>
</span>
<div class="flex-center little-btns">
<span class="tool-item-btn" @click="realtimeChat">
<el-tooltip
class="box-item"
effect="dark"
content="实时语音对话"
>
<el-tooltip class="box-item" effect="dark" :content="'实时语音对话,每次消耗' + config.advance_voice_power + '算力'">
<i class="iconfont icon-mic-bold"></i>
</el-tooltip>
</span>
<span class="tool-item-btn" v-if="isLogin">
<el-tooltip
class="box-item"
effect="dark"
content="上传附件"
>
<file-select
v-if="isLogin"
:user-id="loginUser.id"
@selected="insertFile"
/>
<el-tooltip class="box-item" effect="dark" content="上传附件">
<file-select v-if="isLogin" :user-id="loginUser.id" @selected="insertFile" />
</el-tooltip>
</span>
</div>
<div class="flex little-btns">
<span class="send-btn tool-item-btn">
<!-- showStopGenerate -->
<el-button type="info" v-if="showStopGenerate" @click="stopGenerate" plain>
<el-icon>
<VideoPause />
</el-icon>
</el-button>
<el-button @click="sendMessage" style="color: #754ff6" v-else>
<el-tooltip class="box-item" effect="dark" content="发送">
<el-icon><Promotion /></el-icon>
</el-tooltip>
</el-button>
</span>
</div>
</div>
</div>
</div>
@@ -322,28 +213,21 @@
</el-main>
</el-container>
<el-dialog
v-model="showNotice"
:show-close="true"
class="notice-dialog"
title="网站公告"
>
<el-dialog v-model="showNotice" :show-close="true" class="notice-dialog" title="网站公告">
<div class="notice">
<div v-html="notice"></div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="notShow" type="primary"
>我知道了不再显示</el-button
>
<el-button @click="notShow" type="primary">我知道了不再显示</el-button>
</span>
</template>
</el-dialog>
<ChatSetting :show="showChatSetting" @hide="showChatSetting = false" />
<el-dialog
<!-- <el-dialog
v-model="showConversationDialog"
title="实时语音通话"
:before-close="hangUp"
@@ -353,6 +237,17 @@
ref="conversationRef"
:height="dialogHeight + 'px'"
/>
</el-dialog> -->
<el-dialog v-model="showConversationDialog" title="实时语音通话" :fullscreen="true">
<div v-loading="!frameLoaded">
<iframe
style="width: 100%; height: calc(100vh - 100px); border: none"
:src="voiceChatUrl"
@load="frameLoaded = true"
allow="microphone *;camera *;"
></iframe>
</div>
</el-dialog>
</div>
</template>
@@ -360,18 +255,7 @@
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import ChatPrompt from "@/components/ChatPrompt.vue";
import ChatReply from "@/components/ChatReply.vue";
import {
Delete,
Edit,
InfoFilled,
More,
Plus,
CirclePlusFilled,
Promotion,
Search,
Share,
VideoPause
} from "@element-plus/icons-vue";
import { Delete, Edit, InfoFilled, More, Plus, Promotion, Search, Share, VideoPause } from "@element-plus/icons-vue";
import "highlight.js/styles/a11y-dark.css";
import { isMobile, randString, removeArrayItem, UUID } from "@/utils/libs";
import { ElMessage, ElMessageBox } from "element-plus";
@@ -385,8 +269,7 @@ import FileSelect from "@/components/FileSelect.vue";
import FileList from "@/components/FileList.vue";
import ChatSetting from "@/components/ChatSetting.vue";
import BackTop from "@/components/BackTop.vue";
import { showMessageError } from "@/utils/dialog";
import RealtimeConversation from "@/components/RealtimeConversation.vue";
import { closeLoading, showLoading, showMessageError } from "@/utils/dialog";
const title = ref("GeekAI-智能助手");
const models = ref([]);
@@ -415,6 +298,8 @@ const store = useSharedStore();
const row = ref(1);
const showChatSetting = ref(false);
const listStyle = ref(store.chatListStyle);
const config = ref({ advance_voice_power: 0 });
const voiceChatUrl = ref("");
watch(
() => store.chatListStyle,
(newValue) => {
@@ -460,7 +345,8 @@ if (!chatId.value) {
// 获取系统配置
getSystemInfo()
.then((res) => {
title.value = res.data.title;
config.value = res.data;
title.value = config.value.title;
})
.catch((e) => {
ElMessage.error("获取系统配置失败:" + e.message);
@@ -470,7 +356,7 @@ const md = require("markdown-it")({
breaks: true,
html: true,
linkify: true,
typographer: true
typographer: true,
});
// 获取系统公告
httpGet("/api/config/get?key=notice")
@@ -539,7 +425,7 @@ onMounted(() => {
id: randString(32),
icon: chatRole["icon"],
prompt: prePrompt,
content: data.body
content: data.body,
});
isNewMsg.value = false;
lineBuffer.value = data.body;
@@ -561,16 +447,14 @@ onMounted(() => {
httpPost("/api/chat/tokens", {
text: "",
model: getModelValue(modelID.value),
chat_id: chatId.value
chat_id: chatId.value,
})
.then((res) => {
reply["created_at"] = new Date().getTime();
reply["tokens"] = res.data;
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
document.getElementById("chat-box").scrollTo(0, document.getElementById("chat-box").scrollHeight);
});
})
.catch(() => {});
@@ -584,9 +468,7 @@ onMounted(() => {
}
// 将聊天框的滚动条滑动到最底部
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
document.getElementById("chat-box").scrollTo(0, document.getElementById("chat-box").scrollHeight);
localStorage.setItem("chat_id", chatId.value);
});
});
@@ -687,8 +569,8 @@ const resizeElement = function () {
// mainWinHeight.value = window.innerHeight;
// leftBoxHeight.value = window.innerHeight - 90 - 45 - 82;
// leftBoxHeight.value = window.innerHeight - 90 - 82;
leftBoxHeight.value = window.innerHeight - 90 - 50;
leftBoxHeight.value = window.innerHeight - 90 - 90;
// leftBoxHeight.value = window.innerHeight - 90 - 50;
};
const _newChat = () => {
@@ -714,10 +596,7 @@ const newChat = () => {
disableModel.value = true;
}
// 已有新开的会话
if (
newChatItem.value !== null &&
newChatItem.value["role_id"] === roles.value[0]["role_id"]
) {
if (newChatItem.value !== null && newChatItem.value["role_id"] === roles.value[0]["role_id"]) {
return;
}
@@ -735,7 +614,7 @@ const newChat = () => {
model_id: modelID.value,
title: "",
edit: false,
removing: false
removing: false,
};
showStopGenerate.value = false;
loadChatHistory(chatId.value);
@@ -796,7 +675,7 @@ const editConfirm = function (chat) {
httpPost("/api/chat/update", {
chat_id: chat.chat_id,
title: tmpChatTitle.value
title: tmpChatTitle.value,
})
.then(() => {
chat.title = tmpChatTitle.value;
@@ -811,18 +690,14 @@ const removeChat = function (chat) {
ElMessageBox.confirm(`该操作会删除"${chat.title}"`, "删除聊天", {
confirmButtonText: "删除",
cancelButtonText: "取消",
type: "warning"
type: "warning",
})
.then(() => {
httpGet("/api/chat/remove?chat_id=" + chat.chat_id)
.then(() => {
chatList.value = removeArrayItem(
chatList.value,
chat,
function (e1, e2) {
return e1.id === e2.id;
}
);
chatList.value = removeArrayItem(chatList.value, chat, function (e1, e2) {
return e1.id === e2.id;
});
// 重置会话
_newChat();
})
@@ -845,9 +720,7 @@ const enableInput = () => {
const onInput = (e) => {
// 根据输入的内容自动计算输入框的行数
const lineHeight = parseFloat(
window.getComputedStyle(inputRef.value).lineHeight
);
const lineHeight = parseFloat(window.getComputedStyle(inputRef.value).lineHeight);
textHeightRef.value.style.width = inputRef.value.clientWidth + "px"; // 设定宽度和 textarea 相同
const lines = Math.floor(textHeightRef.value.clientHeight / lineHeight);
inputRef.value.scrollTo(0, inputRef.value.scrollHeight);
@@ -914,13 +787,11 @@ const sendMessage = function () {
icon: loginUser.value.avatar,
content: content,
model: getModelValue(modelID.value),
created_at: new Date().getTime() / 1000
created_at: new Date().getTime() / 1000,
});
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
document.getElementById("chat-box").scrollTo(0, document.getElementById("chat-box").scrollHeight);
});
showHello.value = false;
@@ -935,8 +806,8 @@ const sendMessage = function () {
chat_id: chatId.value,
content: content,
tools: toolSelected.value,
stream: stream.value
}
stream: stream.value,
},
})
);
tmpChatTitle.value = content;
@@ -954,7 +825,7 @@ const clearAllChats = function () {
dangerouslyUseHTMLString: true,
showClose: true,
closeOnClickModal: false,
center: false
center: false,
})
.then(() => {
httpGet("/api/chat/clear")
@@ -987,7 +858,7 @@ const loadChatHistory = function (chatId) {
type: "reply",
id: randString(32),
icon: _role["icon"],
content: _role["hello_msg"]
content: _role["hello_msg"],
});
return;
}
@@ -1000,9 +871,7 @@ const loadChatHistory = function (chatId) {
}
nextTick(() => {
document
.getElementById("chat-box")
.scrollTo(0, document.getElementById("chat-box").scrollHeight);
document.getElementById("chat-box").scrollTo(0, document.getElementById("chat-box").scrollHeight);
});
})
.catch((e) => {
@@ -1027,7 +896,7 @@ const reGenerate = function (prompt) {
type: "prompt",
id: randString(32),
icon: loginUser.value.avatar,
content: text
content: text,
});
store.socket.conn.send(
JSON.stringify({
@@ -1039,8 +908,8 @@ const reGenerate = function (prompt) {
chat_id: chatId.value,
content: text,
tools: toolSelected.value,
stream: stream.value
}
stream: stream.value,
},
})
);
};
@@ -1055,11 +924,7 @@ const searchChat = function (e) {
if (e.keyCode === 13) {
const items = [];
for (let i = 0; i < allChats.value.length; i++) {
if (
allChats.value[i].title
.toLowerCase()
.indexOf(chatName.value.toLowerCase()) !== -1
) {
if (allChats.value[i].title.toLowerCase().indexOf(chatName.value.toLowerCase()) !== -1) {
items.push(allChats.value[i]);
}
}
@@ -1073,12 +938,7 @@ const shareChat = (chat) => {
return ElMessage.error("请先选中一个会话");
}
const url =
location.protocol +
"//" +
location.host +
"/chat/export?chat_id=" +
chat.chat_id;
const url = location.protocol + "//" + location.host + "/chat/export?chat_id=" + chat.chat_id;
window.open(url, "_blank");
};
@@ -1102,31 +962,36 @@ const insertFile = (file) => {
files.value.push(file);
};
const removeFile = (file) => {
files.value = removeArrayItem(
files.value,
file,
(v1, v2) => v1.url === v2.url
);
files.value = removeArrayItem(files.value, file, (v1, v2) => v1.url === v2.url);
};
// 实时语音对话
const showConversationDialog = ref(false);
const conversationRef = ref(null);
const dialogHeight = ref(window.innerHeight - 75);
// const conversationRef = ref(null);
// const dialogHeight = ref(window.innerHeight - 75);
const frameLoaded = ref(false);
const realtimeChat = () => {
if (!isLogin.value) {
store.setShowLoginDialog(true);
return;
}
showConversationDialog.value = true;
nextTick(() => {
conversationRef.value.connect();
});
};
const hangUp = () => {
showConversationDialog.value = false;
conversationRef.value.hangUp();
showLoading("正在连接...");
httpPost("/api/realtime/voice")
.then((res) => {
voiceChatUrl.value = res.data;
showConversationDialog.value = true;
closeLoading();
})
.catch((e) => {
showMessageError("连接失败:" + e.message);
closeLoading();
});
};
// const hangUp = () => {
// showConversationDialog.value = false;
// conversationRef.value.hangUp();
// };
</script>
<style scoped lang="stylus">

View File

@@ -393,23 +393,23 @@
<script setup>
import nodata from "@/assets/img/no-data.png";
import { nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import { Delete, InfoFilled } from "@element-plus/icons-vue";
import {nextTick, onMounted, onUnmounted, ref, watch} from "vue";
import {Delete, InfoFilled} from "@element-plus/icons-vue";
import BlackSelect from "@/components/ui/BlackSelect.vue";
import BlackSwitch from "@/components/ui/BlackSwitch.vue";
import BlackInput from "@/components/ui/BlackInput.vue";
import MusicPlayer from "@/components/MusicPlayer.vue";
import { compact } from "lodash";
import { httpDownload, httpGet, httpPost } from "@/utils/http";
import { showMessageError, showMessageOK } from "@/utils/dialog";
import { checkSession, getClientId } from "@/store/cache";
import { ElMessage, ElMessageBox } from "element-plus";
import { formatTime, replaceImg } from "@/utils/libs";
import {compact} from "lodash";
import {httpDownload, httpGet, httpPost} from "@/utils/http";
import {showMessageError, showMessageOK} from "@/utils/dialog";
import {checkSession, getClientId} from "@/store/cache";
import {ElMessage, ElMessageBox} from "element-plus";
import {formatTime, replaceImg} from "@/utils/libs";
import Clipboard from "clipboard";
import BlackDialog from "@/components/ui/BlackDialog.vue";
import Compressor from "compressorjs";
import Generating from "@/components/ui/Generating.vue";
import { useSharedStore } from "@/store/sharedata";
import {useSharedStore} from "@/store/sharedata";
// const winHeight = ref(window.innerHeight - 50);
const winHeight = ref(window.innerHeight - 20);
@@ -417,7 +417,8 @@ const winHeight = ref(window.innerHeight - 20);
const custom = ref(false);
const models = ref([
{ label: "v3.0", value: "chirp-v3-0" },
{ label: "v3.5", value: "chirp-v3-5" }
{label: "v3.5", value: "chirp-v3-5"},
{label: "v4.0", value: "chirp-v4"}
]);
const tags = ref([
{ label: "女声", value: "female vocals" },

View File

@@ -12,10 +12,14 @@
</el-select>
<el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button>
<el-button type="success" :icon="Plus" @click="add">添加兑换码</el-button>
<el-button type="primary" @click="exportItems" :loading="exporting"><i class="iconfont icon-export mr-1"></i> 导出
</el-button>
</div>
<el-row>
<el-table :data="items" :row-key="row => row.id">
<el-table :data="items" :row-key="row => row.id"
@selection-change="handleSelectionChange" table-layout="auto">
<el-table-column type="selection" width="38"></el-table-column>
<el-table-column prop="name" label="名称"/>
<el-table-column prop="code" label="兑换码">
<template #default="scope">
@@ -48,7 +52,8 @@
<el-table-column prop="enabled" label="启用状态">
<template #default="scope">
<el-switch v-model="scope.row['enabled']" @change="set('enabled',scope.row)" :disabled="scope.row['redeemed_at']>0"/>
<el-switch v-model="scope.row['enabled']" @change="set('enabled',scope.row)"
:disabled="scope.row['redeemed_at']>0"/>
</template>
</el-table-column>
@@ -90,7 +95,7 @@
</el-form-item>
<el-form-item label="生成数量" prop="num">
<el-input v-model.number="item.num" />
<el-input v-model.number="item.num"/>
</el-form-item>
</el-form>
</template>
@@ -107,17 +112,17 @@
<script setup>
import {onMounted, onUnmounted, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {httpGet, httpPost, httpPostDownload} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat, removeArrayItem, substr} from "@/utils/libs";
import {Delete, DocumentCopy, Plus, Search, UploadFilled} from "@element-plus/icons-vue";
import {dateFormat, substr, UUID} from "@/utils/libs";
import {DocumentCopy, Plus, Search} from "@element-plus/icons-vue";
import {showMessageError} from "@/utils/dialog";
import ClipboardJS from "clipboard";
// 变量定义
const items = ref([])
const loading = ref(true)
const query = ref({code:"",status:-1})
const query = ref({code: "", status: -1})
const redeemStatus = ref([
{value: -1, label: "全部"},
{value: 0, label: "未核销"},
@@ -126,6 +131,8 @@ const redeemStatus = ref([
const showDialog = ref(false)
const dialogLoading = ref(false)
const item = ref({name: "", power: 0, num: 1})
const itemIds = ref([])
const exporting = ref(false)
const clipboard = ref(null)
onMounted(() => {
@@ -152,10 +159,10 @@ const add = () => {
}
const save = () => {
if (item.value.name ===""){
if (item.value.name === "") {
return showMessageError("请输入兑换码名称")
}
if (item.value.power === 0){
if (item.value.power === 0) {
return showMessageError("请输入算力额度")
}
if (item.value.num <= 0) {
@@ -207,12 +214,39 @@ const remove = function (row) {
ElMessage.error("删除失败" + e.message)
})
}
const handleSelectionChange = (items) => {
itemIds.value = items.map(item => item.id)
}
const exportItems = () => {
query.value.ids = itemIds.value
exporting.value = true
httpPostDownload("/api/admin/redeem/export", query.value).then(response => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', UUID() + ".csv"); // 设置下载文件的名称
document.body.appendChild(link);
link.click();
// 移除 <a> 标签
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
exporting.value = false
}).catch(() => {
exporting.value = false
showMessageError("下载失败")
})
}
</script>
<style lang="stylus" scoped>
.list {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;

View File

@@ -1,5 +1,5 @@
<template>
<div class="system-config" v-loading="loading">
<div class="system-config form" v-loading="loading">
<el-tabs v-model="activeName" class="sys-tabs">
<el-tab-pane label="系统配置" name="basic">
@@ -33,8 +33,23 @@
</el-input>
</el-form-item>
<el-form-item label="首页背景图" prop="logo">
<div class="tip-input">
<el-form-item>
<template #label>
<div class="label-title">
首页背景图
<el-tooltip
effect="dark"
content="网站首页背景图片"
raw-content
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
<div class="d-flex justify-between w-100">
<el-input v-model="system['index_bg_url']" placeholder="网站首页背景图片">
<template #append>
<el-upload
@@ -49,14 +64,28 @@
</el-upload>
</template>
</el-input>
<el-button type="primary" @click="system.index_bg_url = 'https://api.dujin.org/bing/1920.php'">使用动态背景</el-button>
<el-button @click="system.index_bg_url = 'color'">使用纯色背景</el-button>
<el-button class="ml-1" type="primary" @click="system.index_bg_url = 'https://api.dujin.org/bing/1920.php'">使用动态背景</el-button>
<el-button class="ml-1" @click="system.index_bg_url = 'color'">使用纯色背景</el-button>
</div>
</el-form-item>
<el-form-item label="首页导航菜单" prop="index_navs">
<div class="tip-input">
<el-select
<el-form-item>
<template #label>
<div class="label-title">
首页导航菜单
<el-tooltip
effect="dark"
content="被选中的菜单将会在首页导航栏显示"
raw-content
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
<el-select
v-model="system['index_navs']"
multiple
:filterable="true"
@@ -70,29 +99,16 @@
:value="item.id"
/>
</el-select>
<div class="info">
<el-tooltip
class="box-item"
effect="dark"
content="被选中的菜单将会在首页导航栏显示"
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</el-form-item>
<el-form-item label="版权信息" prop="copyright">
<el-input v-model="system['copyright']" placeholder="更改此选项需要获取 License 授权"/>
</el-form-item>
<el-form-item label="开放注册" prop="enabled_register">
<div class="tip-input">
<el-switch v-model="system['enabled_register']"/>
<div class="info">
<el-form-item>
<template #label>
<div class="label-title">
开放注册
<el-tooltip
effect="dark"
content="关闭注册之后只能通过管理后台添加用户"
@@ -104,13 +120,14 @@
</el-icon>
</el-tooltip>
</div>
</div>
</template>
<el-switch v-model="system['enabled_register']"/>
</el-form-item>
<el-form-item label="启用验证码" prop="enabled_verify">
<div class="tip-input">
<el-switch v-model="system['enabled_verify']"/>
<div class="info">
<el-form-item>
<template #label>
<div class="label-title">
启用验证码
<el-tooltip
effect="dark"
content="启用验证码之后,注册登录都会加载行为验证码,增加安全性。此功能需要购买验证码服务才会生效。"
@@ -122,7 +139,8 @@
</el-icon>
</el-tooltip>
</div>
</div>
</template>
<el-switch v-model="system['enabled_verify']"/>
</el-form-item>
<el-form-item label="注册方式" prop="register_ways">
@@ -153,36 +171,35 @@
</template>
</el-input>
</el-form-item>
<el-form-item label="默认翻译模型">
<template #default>
<div class="tip-input">
<el-select
<el-form-item>
<template #label>
<div class="label-title">
默认翻译模型
<el-tooltip
effect="dark"
content="选择一个默认模型来翻译提示词"
raw-content
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
<el-select
v-model.number="system['translate_model_id']"
:filterable="true"
placeholder="选择一个默认模型来翻译提示词"
style="width: 100%"
>
<el-option
v-for="item in models"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
<div class="info">
<el-tooltip
class="box-item"
effect="dark"
content="新用户注册默认开通的 AI 模型"
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</template>
<el-option
v-for="item in models"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="开启聊天上下文">
@@ -193,15 +210,15 @@
<el-input-number v-model="system['context_deep']" :min="0" :max="10"/>
<div class="tip">会话上下文深度在老会话中继续会话默认加载多少条聊天记录作为上下文如果设置为
0
则不加载聊天记录仅仅使用当前角色的上下文该配置参数最好设置需要为偶数否则将无法兼容百度的 API
则不加载聊天记录仅仅使用当前角色的上下文该配置参数必须设置需要为偶数
</div>
</div>
</el-form-item>
<el-form-item label="SD反向提示词" prop="sd_neg_prompt">
<div class="tip-input">
<el-input v-model="system['sd_neg_prompt']" placeholder=""/>
<div class="info">
<el-form-item>
<template #label>
<div class="label-title">
SD反向提示词
<el-tooltip
effect="dark"
content="Stable-Diffusion 绘画默认反向提示词"
@@ -213,13 +230,14 @@
</el-icon>
</el-tooltip>
</div>
</div>
</template>
<el-input type="textarea" :rows="2" v-model="system['sd_neg_prompt']" placeholder=""/>
</el-form-item>
<el-form-item label="会员充值说明" prop="order_pay_timeout">
<div class="tip-input">
<el-input v-model="system['vip_info_text']" placeholder=""/>
<div class="info">
<template #label>
<div class="label-title">
会员充值说明
<el-tooltip
effect="dark"
content="会员充值页面的充值说明文字"
@@ -231,7 +249,8 @@
</el-icon>
</el-tooltip>
</div>
</div>
</template>
<el-input type="textarea" :rows="2" v-model="system['vip_info_text']" placeholder=""/>
</el-form-item>
<el-form-item label="MJ默认API模式" prop="mj_mode">
@@ -255,17 +274,16 @@
<el-input v-model.number="system['vip_month_power']" placeholder="VIP用户每月赠送算力"/>
</el-form-item>
<el-form-item label="每日赠送算力" prop="daily_power">
<div class="tip-input-line">
<el-input v-model.number="system['daily_power']" placeholder="默认值0"/>
<div class="tip">
如果设置0表示不赠送用户享受完免费算力额度之后就不能再发起对话了如果设置为N则系统每天将算力值小于N的用户自动补充到N注意此功能要配合XXL-JOB启用
</div>
</div>
<el-text type="info">
如果设置0表示不赠送用户享受完免费算力额度之后就不能再发起对话了如果设置为N则系统每天将算力值小于N的用户自动补充到N注意此功能要配合XXL-JOB启用
</el-text>
</el-form-item>
<el-form-item label="MJ绘图算力" prop="mj_power">
<div class="tip-input">
<el-input v-model.number="system['mj_power']" placeholder=""/>
<div class="info">
<el-form-item>
<template #label>
<div class="label-title">
MJ绘图算力
<el-tooltip
effect="dark"
content="使用MidJourney画一张图消耗算力"
@@ -277,12 +295,13 @@
</el-icon>
</el-tooltip>
</div>
</div>
</template>
<el-input v-model.number="system['mj_power']" placeholder=""/>
</el-form-item>
<el-form-item label="MJ操作算力" prop="mj_action_power">
<div class="tip-input">
<el-input v-model.number="system['mj_action_power']" placeholder=""/>
<div class="info">
<el-form-item>
<template #label>
<div class="label-title">
MJ操作算力
<el-tooltip
effect="dark"
content="放大,变换,重绘操作一次消耗的算力"
@@ -294,7 +313,8 @@
</el-icon>
</el-tooltip>
</div>
</div>
</template>
<el-input v-model.number="system['mj_action_power']" placeholder=""/>
</el-form-item>
<el-form-item label="Stable-Diffusion算力" prop="sd_power">
<el-input v-model.number="system['sd_power']" placeholder="使用Stable-Diffusion画一张图消耗算力"/>
@@ -308,6 +328,24 @@
<el-form-item label="Luma 算力" prop="luma_power">
<el-input v-model.number="system['luma_power']" placeholder="使用 Luma 生成一段视频消耗算力"/>
</el-form-item>
<el-form-item>
<template #label>
<div class="label-title">
高级语音算力
<el-tooltip
effect="dark"
content="使用一次 OpenAI 高级语音对话消耗的算力"
raw-content
placement="right"
>
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
<el-input v-model.number="system['advance_voice_power']" placeholder=""/>
</el-form-item>
</el-tab-pane>
</el-tabs>
@@ -420,7 +458,7 @@ import {onMounted, reactive, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import Compressor from "compressorjs";
import {ElMessage, ElMessageBox} from "element-plus";
import {InfoFilled, UploadFilled,Select,CloseBold} from "@element-plus/icons-vue";
import {CloseBold, InfoFilled, Select, UploadFilled} from "@element-plus/icons-vue";
import MdEditor from "md-editor-v3";
import 'md-editor-v3/lib/style.css';
import Menu from "@/views/admin/Menu.vue";
@@ -594,6 +632,7 @@ const fixData = () => {
<style lang="stylus" scoped>
@import "@/assets/css/admin/form.styl"
@import "@/assets/css/main.styl"
.system-config {
display flex
justify-content center
@@ -603,76 +642,6 @@ const fixData = () => {
background-color var(--el-bg-color)
padding 10px 20px 40px 20px
//border: 1px solid var(--el-border-color);
.container {
.el-form {
.el-form-item__content {
.tip-text {
padding-left 10px;
}
.el-icon {
font-size 16px
cursor pointer
}
.uploader-icon {
font-size 24px
position relative
top 3px
}
.tip-input-line {
.tip {
margin-top 10px
color #c1c1c1
font-size 12px;
line-height 1.5;
}
}
}
.el-input {
width 100%
}
}
.text {
font-size 14px
}
.active-info {
line-height 1.5
padding 10px 0 30px 0
}
.el-descriptions {
margin-bottom 20px
.el-icon {
font-size 18px
}
.selected {
color #0bc15f
}
.closed {
color #da0d54
}
.text {
margin-left 10px
font-size 12px
color #999999
position: relative;
top -5px
}
}
.el-alert {
margin-bottom 15px;
}
}
}
}
</style>