'frontendadd'

This commit is contained in:
yangqiao
2024-12-30 17:47:28 +08:00
parent 8b4cc37514
commit 34e25633ba
71 changed files with 3699 additions and 0 deletions

12
frontend/src/App.vue Normal file
View File

@@ -0,0 +1,12 @@
<template>
<div>
<SwitchLanguage />
<router-view></router-view>
</div>
</template>
<script setup lang="ts">
import SwitchLanguage from "./components/SwitchLanguage.vue";
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,27 @@
export interface PurchaseListResult {
list: Array<PurchaseInfo>
}
export interface PurchaseInfo {
adminName: string
index: string
pdName: string
pdNum: string
pdType: string
purchaseNum: number
updateTime: Date
}
export interface ProjectListResult {
[x: string]: number
code: any
data: any
msg: any
// list: Array<ProjectInfo>;
}
export interface ProjectInfo {
adminName: string
adminPhone: string
index: number
name: string
updateTime: Date
}

View File

@@ -0,0 +1,26 @@
export interface ListResult {
list: Array<ListModel>;
}
export interface ListModel {
adminName: string;
amount: string;
contractType: number;
index: number;
name: string;
no: string;
paymentType: number;
status: number;
updateTime: Date;
}
export interface CardListResult {
list: Array<CardList>;
}
export interface CardList {
banner: string;
description: string;
index: number;
isSetup: boolean;
name: string;
type: number;
}

View File

@@ -0,0 +1,32 @@
import { defineComponent } from 'vue';
export interface MenuListResult {
list: Array<RouteItem>;
}
export type Component<T = any> =
| ReturnType<typeof defineComponent>
| (() => Promise<typeof import('*.vue')>)
| (() => Promise<T>);
export interface RouteItem {
path: string;
name: string;
component?: Component | string;
components?: Component;
redirect?: string;
meta: RouteMeta;
children?: Array<RouteItem>;
}
export interface RouteMeta {
title: string;
icon?: string;
expanded?: boolean;
orderNo?: number;
hidden?: boolean;
hiddenBreadcrumb?: boolean;
single?: boolean;
keepAlive?: boolean;
frameSrc?: string;
frameBlank?: boolean;
}

22
frontend/src/api/user.ts Normal file
View File

@@ -0,0 +1,22 @@
import type { ProjectListResult } from './model/detailModel'
import { request } from '../utils/request'
// 登录获取Token
export function authLogin(data: any) {
return request.post<ProjectListResult>({
url: `/login`,
data
})
}
// 发送聊天请求
export function chat(data: any) {
return request.post<ProjectListResult>({
url: `/chat`,
data
})
}
// 获取模型列表
export function getModelList() {
return request.get<ProjectListResult>({
url: `/model`
})
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 433 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,125 @@
.reasoning-box {
width: 100%;
.problem-item {
width: 100%;
display: flex;
flex-direction: column;
row-gap: 24px;
.title-box {
width: 100%;
margin-bottom: 24px;
display: flex;
flex-direction: column;
row-gap: 4px;
p {
font-size: 16px;
color: #fff;
font-weight: 600;
}
span {
font-size: 14px;
color: #fff;
}
}
.reasoning-item {
width: 100%;
.title {
width: 100%;
display: flex;
column-gap: 8px;
align-items: center;
margin-bottom: 6px;
img {
width: 34px;
height: 34px;
}
span {
font-size: 16px;
color: #fff;
font-weight: 600;
}
.active_item {
box-sizing: border-box;
padding: 0 8px;
font-size: 14px;
height: 24px;
margin: 0;
color: rgba(73, 252, 255, 0.80);
border-radius: 24px;
border: 1px solid rgba(24, 91, 197, 1);
box-shadow: 0px 0px 6px 0px rgba(0, 213, 250, 1) inset;
display: flex;
align-items: center;
justify-content: center;
column-gap: 3px;
background: #0C3870;
img {
width: 18px;
height: 18px;
}
}
}
.reasoning-content {
width: 100%;
display: flex;
flex-direction: row;
.reasoning-text {
width: calc(100% - 46px);
border-radius: 6px;
background: rgba(3, 52, 90, 0.5);
border: 1px solid #0061A6;
box-shadow: 0px 0px 16px 0px rgba(0, 149, 230, 1) inset;
padding: 16px;
font-size: 14px;
color: #fff;
box-sizing: border-box;
.show-box {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.active {
background: rgba(42, 163, 255, 0.6);
border: 2px solid #0FFBFF;
}
.icon-box {
height: 46px;
display: flex;
align-items: center;
justify-content: center;
.el-icon {
width: 20px;
height: 20px;
background: #093D7C;
border: 1px solid #025389;
cursor: pointer;
}
}
}
}
.complete-box {
font-size: 14px;
color: #2F88BF;
}
}
}

View File

@@ -0,0 +1,153 @@
<template>
<div class="reasoning-box">
<div
class="problem-item"
v-for="(key, num) in props.reasoningList"
:key="num"
:id="`${num}title`"
>
<div class="title-box">
<p>{{generateTitle('问题设定')}}:</p>
<span>{{ key.title }}</span>
</div>
<div
class="reasoning-item"
v-for="(item, index) in key.children"
:key="index"
:id="`${num}${index}content`"
>
<div class="title">
<img src="../../assets/logo.png" alt="" />
<span v-if="item.title">{{generateTitle(item.title =='Planner'?'角色':'模型名称')}}{{ item.title }}</span>
<p
class="active_item"
v-if="reasonStatus.index === index && reasonStatus.show"
>
<img src="../../assets/layout/vector.png" alt="" />
{{generateTitle('推理中')}}...
</p>
</div>
<div class="reasoning-content">
<div
class="reasoning-text"
:class="{
active: reasonStatus.index === index && reasonStatus.show
}"
>
<div
style="font-size: 14px"
:class="{ 'show-box': current[index] }"
>
<MdPreview style="background: linear-gradient(to bottom,#d5e6f4 0%,#afceea 50%,#c2d9ef 100%);" :editorId="id" :modelValue="item.content" />
</div>
</div>
<div class="icon-box">
<el-icon color="#fff" @click="changeShow(index)"
><component :is="getIcon(index)"></component
></el-icon>
</div>
</div>
</div>
<div
class="complete-box"
v-if="completeList[num].show && num === completeList[num].index"
>
{{ generateTitle('推理完成,如果希望了解更多可继续在下面框中提问或新建对话。')}}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineExpose, defineEmits, defineProps } from 'vue'
import { Plus, Minus } from '@element-plus/icons-vue'
import { MdPreview } from 'md-editor-v3'
import {generateTitle} from '../../utils/i18n'
import 'md-editor-v3/lib/style.css'
const props: any = defineProps({
reasoningList: {
type: Array,
default: []
}
})
const id = 'preview-only'
const emits = defineEmits(['completeFun'])
const current = ref<Array<any>>([])
const reasonStatus = ref({
index: 0,
show: false
})
const completeList = ref<Array<any>>([])
const getIcon = (index: number) => {
return current.value[index] ? Plus : Minus
}
const changeShow = (index: number) => {
current.value[index] = !current.value[index]
}
// const reasoningList = ref<Array<any>>([])
const getAnswer = async () => {
emits('completeFun')
}
// function extractSynthesisProcess() {
// talkList.value
// .split(">>>>>>>> USING AUTO REPLY...")
// .forEach((item: any, index: number) => {
// if (index) {
// let data = item.split(
// "\nsynthesis_scientist (to Generate_Group_Admin):\n"
// )[1];
// if (data) {
// reasoningList.value[reasoningList.value.length - 1].content = data;
// }
// } else {
// let data = item.split("Next speaker: ");
// reasoningList.value[
// reasoningList.value.length - 1
// ].title = `模型名称(${data[data.length - 1].replace(/\n/g, "")}`;
// }
// });
// }
// const getData = (e: any) => {
// let regex = /Next speaker: (.*)/;
// let regexnew = /\(to (.*)\)/;
// reasonStatus.value.show = true;
// if (e.includes(">>>>>>>> USING AUTO REPLY...")) {
// reasoningList.value[reasoningList.value.length - 1].title = oldLine.value[oldLine.value.length - 1].match(regex)[1];
// startStatus.value = true;
// } else if (startStatus.value) {
// if (
// e.includes(
// `${reasoningList.value[reasoningList.value.length - 1].title} (to`
// )
// ) {
// reasoningList.value[reasoningList.value.length - 1].title = e.match(regexnew)[1];
// } else {
// console.log(e,e.split("**TERMINATE**"),'111');
// if (e.split("**TERMINATE**")[1]) {
// reasoningList.value[reasoningList.value.length - 1].content +=
// e.split("**TERMINATE**")[0];
// reasonStatus.value.show = false;
// endShow.value = true;
// } else if (e.split("`TERMINATE`")[1]) {
// reasoningList.value[reasoningList.value.length - 1].content +=
// e.split("`TERMINATE`")[0];
// reasonStatus.value.show = false;
// endShow.value = true;
// } else {
// reasoningList.value[reasoningList.value.length - 1].content += e;
// }
// }
// }
// oldLine.value.push(e);
// }
defineExpose({
getAnswer,
reasonStatus,
completeList
})
</script>
<style scoped>
@import './index.less';
</style>

View File

@@ -0,0 +1,177 @@
<template>
<div class="list-box">
<div
class="item-box"
v-for="(item, index) in props.reasoningList"
:key="index"
>
<div class="title" @click="toTitle(index)">
{{ item.title }}
</div>
<div class="ul">
<div
@click="toContent(index, num)"
v-for="(key, num) in item.children"
:key="num"
>
<div v-if="key.title !== 'Planner'" class="li">
<div
class="p-box"
:class="{
active_item:
index === props.reasoningList.length - 1 &&
num === item.children.length - 1,
}"
>
<img
v-if="
index === props.reasoningList.length - 1 &&
num === item.children.length - 1
"
src="../assets/layout/vector.png"
alt=""
/>
<span>{{ key.title }}</span>
</div>
<img
class="next"
v-if="num + 1 != item.children.length"
src="../assets/layout/next.png"
alt=""
/>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps } from "vue";
const props: any = defineProps({
reasoningList: {
type: Array,
default: [],
},
});
const toTitle = (index: number) => {
let id = `${index}title`;
let view: any = document.getElementById(id);
view.scrollIntoView({ behavior: "smooth" });
};
const toContent = (index: number, num: number) => {
let id = `${index}${num}content`;
let view: any = document.getElementById(id);
view.scrollIntoView({ behavior: "smooth" });
};
</script>
<style scoped lang="less">
.list-box {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
row-gap: 18px;
height: calc(100% - 70px);
overflow: auto;
&::-webkit-scrollbar {
width: 0;
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #1a87ca;
}
&::-webkit-scrollbar-thumb:hover {
background: #1a87ca;
}
.item-box {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
box-sizing: border-box;
padding: 12px;
position: relative;
&::before {
content: "";
position: absolute;
bottom: -12px;
left: 50%;
transform: translateX(-50%);
width: 80%;
height: 1px;
background: linear-gradient(
90deg,
rgba(37, 237, 242, 0) 0%,
rgba(98, 250, 248, 0.2) 32.5%,
rgba(98, 250, 248, 0.2) 68%,
rgba(37, 237, 242, 0) 100%
);
}
&:last-child::before {
height: 0;
}
.title {
width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 15px;
color: #fff;
margin-bottom: 12px;
cursor: pointer;
}
.ul {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
row-gap: 8px;
.li {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
row-gap: 8px;
.p-box {
max-width: 90%;
padding: 10px 10px;
font-size: 14px;
min-height: 36px;
color: #fff;
border-radius: 36px;
border: 1px solid rgba(24, 91, 197, 1);
box-shadow: 0px 0px 6px 0px rgba(0, 213, 250, 1) inset;
display: flex;
align-items: center;
justify-content: center;
column-gap: 3px;
box-sizing: border-box;
cursor: pointer;
img {
width: 18px;
height: 18px;
}
span {
width: 100%;
word-wrap: break-word;
text-align: justify;
}
}
.active_item {
background: rgba(25, 240, 255, 0.2);
}
.next {
height: 24px;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,55 @@
<template>
<div>
<el-dropdown class="language-box" placement="bottom-start">
<el-button>{{
state.curLanguage === "en" ? state.curLanguage : "中"
}}</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="zh('zh-cn')">中文</el-dropdown-item>
<el-dropdown-item @click="en('en')">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
<script setup lang='ts'>
import { reactive } from "vue";
import { useI18n } from "vue-i18n";
const { locale } = useI18n();
const state = reactive({
curLanguage: "zh-cn",
});
const zh = (e: any) => {
state.curLanguage = e;
locale.value = state.curLanguage;
};
const en = (e: any) => {
state.curLanguage = e;
locale.value = state.curLanguage;
};
</script>
<style scoped lang='less'>
.language-box {
position: fixed;
top: 6px;
left: 32px;
.el-button {
background: none;
color: #fff;
font-size: 14px;
width: 24px;
padding: 0;
height: 24px;
border-radius: 50%;
&:focus {
outline: none;
}
&:hover {
border: 1px solid #3ba4ff;
color: #3ba4ff;
}
}
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<div class="textarea-content">
<el-input
v-model="chatData.message"
@keydown="handleKeydown"
:placeholder="generateTitle('在此输入您的问题或需求有问必答Shift+Enter换行')"
type="textarea"
:autosize="{ minRows: props.minRows }"
:disabled="disableStatus"
>
</el-input>
<div class="btn-box">
<el-button type="primary" @click="submitFun" :disabled="disableStatus"
><el-icon color="#fff" size="16"><Promotion /></el-icon>{{generateTitle('发送')}}</el-button
>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, defineProps, defineEmits, defineExpose } from 'vue'
import { Promotion } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import {generateTitle} from '../utils/i18n'
// import { chat } from "../api/user";
const handleKeydown = (event: any) => {
if (!event.shiftKey && event.keyCode == 13) {
event.cancelBubble = true
event.stopPropagation()
event.preventDefault()
}
}
const props = defineProps({
minRows: {
type: Number,
default: 6
}
})
const chatData = ref<any>({
chat_id: '',
message: ''
})
//如何在室温条件下合成CsPbBr3
const disableStatus = ref(false)
const emits = defineEmits(['submitFun'])
const submitFun = async () => {
if (!chatData.value.message) {
ElMessage.error('请输入问题')
return
}
if (disableStatus.value) {
ElMessage.error('回答输出中,暂不能再次提问')
return
}
chatData.value.chat_id = new Date().getTime()
emits('submitFun', JSON.stringify(chatData.value))
chatData.value = {
chat_id: '',
message: ''
}
}
defineExpose({
disableStatus
})
</script>
<style scoped lang="less">
.textarea-content {
position: relative;
.btn-box {
position: absolute;
bottom: 12px;
right: 12px;
.el-button {
display: flex;
align-items: center;
justify-content: center;
column-gap: 4px;
}
}
:deep(.el-textarea__inner) {
width: 100%;
border-radius: 6px;
background: rgba(0, 30, 70, 0.5);
border: 1px solid rgba(59, 164, 255, 1);
box-shadow: 0px 0px 16px 0px rgba(0, 149, 230, 1) inset;
padding: 16px;
font-size: 14px;
color: rgba(157, 215, 248, 1);
box-sizing: border-box;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
&:focus {
outline: 1px solid #3ba4ff;
}
&::-webkit-input-placeholder {
color: rgba(157, 215, 248, 1);
}
&:-moz-placeholder {
color: rgba(157, 215, 248, 1);
}
&::-moz-placeholder {
color: rgba(157, 215, 248, 1);
}
&::-ms-input-placeholder {
color: rgba(157, 215, 248, 1);
}
}
}
</style>

View File

@@ -0,0 +1,136 @@
<template>
<div class="CharacterVerification" :style="{'width' : width + 'px','height' : height + 'px'}" ref="CharacterVerification">
<canvas
@click="refresh"
:width="width"
:height="height"
ref="verifyCanvas"
:style="{ cursor: 'pointer' }"
></canvas>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, onMounted } from "vue";
export default defineComponent({
name: "CharacterVerification",
props: {
width: {
type: Number,
default: 150,
},
height: {
type: Number,
default: 40,
},
type: {
type: String,
default: "number", //图形验证码默认类型blend:数字字母混合类型、number:纯数字、letter:纯字母
},
},
setup(props, {expose}) {
const numArr = "0,1,2,3,4,5,6,7,8,9".split(",");
const letterArr =
"a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z".split(
","
);
const code = ref("");
const CharacterVerification = ref<HTMLDivElement | null>(null);
const verifyCanvas = ref<HTMLCanvasElement | null>(null);
let ctx;
onMounted(() => {
refresh();
})
const refresh = () => {
code.value = "";
if (verifyCanvas.value?.getContext) {
ctx = verifyCanvas.value.getContext("2d") as CanvasRenderingContext2D;
} else {
return;
}
ctx.textBaseline = "middle";
ctx.fillStyle = randomColor(180, 240);
ctx.fillRect(0, 0, props.width, props.height);
if (props.type == "blend") {
var txtArr = numArr.concat(letterArr);
} else if (props.type == "number") {
var txtArr = numArr;
} else {
var txtArr = letterArr;
}
for (var i = 1; i <= 4; i++) {
var txt = txtArr[randomNum(0, txtArr.length)];
code.value += txt;
ctx.font = randomNum(props.height / 2, props.height) + "px SimHei"; //随机生成字体大小
ctx.fillStyle = randomColor(50, 160);
ctx.shadowOffsetX = randomNum(-3, 3);
ctx.shadowOffsetY = randomNum(-3, 3);
ctx.shadowBlur = randomNum(-3, 3);
ctx.shadowColor = "rgba(0, 0, 0, 0.3)";
var x = (props.width / 5) * i;
var y = props.height / 2;
var deg = randomNum(-30, 30);
ctx.translate(x, y);
ctx.rotate((deg * Math.PI) / 180);
ctx.fillText(txt, 0, 0);
ctx.rotate((-deg * Math.PI) / 180);
ctx.translate(-x, -y);
}
for (var i = 0; i < 4; i++) {
ctx.strokeStyle = randomColor(40, 180);
ctx.beginPath();
ctx.moveTo(randomNum(0, props.width), randomNum(0, props.height));
ctx.lineTo(randomNum(0, props.width), randomNum(0, props.height));
ctx.stroke();
}
for (var i = 0; i < props.width / 4; i++) {
ctx.fillStyle = randomColor(0, 255);
ctx.beginPath();
ctx.arc(
randomNum(0, props.width),
randomNum(0, props.height),
1,
0,
2 * Math.PI
);
ctx.fill();
}
};
const randomColor = (min: number, max: number) => {
var r = randomNum(min, max);
var g = randomNum(min, max);
var b = randomNum(min, max);
return "rgb(" + r + "," + g + "," + b + ")";
};
const randomNum = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min) + min);
};
const validate = (iptCode: string) => {
var newIptCode = iptCode.toLowerCase();
var v_code = code.value.toLowerCase();
if (newIptCode == v_code) {
return true;
} else {
refresh();
return false;
}
};
expose({ validate });
return {
CharacterVerification,
verifyCanvas,
refresh,
};
},
});
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,610 @@
.is-sidebar-open {
width: 230px;
}
.aside-box {
// height: calc(100vh - 175px);
overflow: inherit;
position: relative;
.collapse-demo {
width: 36px;
height: calc(100% - 260px);
background: #093e7a;
border: 1px solid rgba(2, 83, 137, 1);
padding: 12px;
box-sizing: border-box;
// transition: all .25s ease;
position: fixed;
top: 0;
right: 0;
.collapse-title {
width: 100%;
.title {
width: 100%;
text-align: center;
font-size: 14px;
font-weight: 400;
color: #fff;
letter-spacing: 1px;
}
.icon-box {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 1px solid rgba(2, 83, 137, 1);
background: rgba(9, 61, 124, 1);
position: absolute;
top: 10px;
left: -12px;
z-index: 2;
cursor: pointer;
}
}
}
.active {
width: 230px;
padding: 0;
background: rgba(0, 37, 78, 1);
.collapse-title {
width: 100%;
.title {
width: 100%;
text-align: left;
background: rgba(33, 78, 131, 1);
height: 54px;
line-height: 54px;
box-sizing: border-box;
padding-left: 30px;
font-size: 16px;
font-weight: 600;
}
}
}
.user-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
column-gap: 10px;
font-size: 14px;
color: #fff;
position: absolute;
bottom: 12px;
left: 0;
img {
width: 24px;
height: 24px;
border-radius: 50%;
}
}
.chart-box {
position: fixed;
bottom: 20px;
right: 6px;
width: 230px;
height: 230px;
// background: url('../../assets/chart/图片9.png') no-repeat;
// background-size: cover;
.body-box {
position: relative;
width: 100%;
height: 100%;
.out-round-list {
width: 230px;
height: 230px;
position: relative;
.out-round-item {
display: flex;
align-items: center;
column-gap: 8px;
position: absolute;
// &:nth-child(1) {
// top: 89px;
// left: 117px;
// transform: translateX(-50%) rotate(0deg);
// }
&:nth-child(1) {
top: 0;
left: 88px;
transform: translateX(-50%) rotate(0deg);
}
&:nth-child(2) {
top: 119px;
left: -30px;
transform: translateY(-50%) rotate(-87deg);
.round-arrow-box {
transform: rotate(4deg);
.round-lage-arrow {
top: 10px;
left: -10px;
}
.round-arrow {
top: 26px;
left: 24px;
}
}
.model-box {
transform: rotate(84deg);
}
}
&:nth-child(3) {
bottom: -8px;
left: 85px;
transform: translateX(-50%) rotate(208deg);
.round-arrow-box {
transform: rotate(6deg);
.round-lage-arrow {
top: 24px;
left: -8px;
}
.round-arrow {
top: 40px;
left: 27px;
}
}
.model-box {
transform: rotate(151deg);
}
}
&:nth-child(4) {
bottom: 26px;
right: -83px;
transform: translateX(-50%) rotate(136deg);
.round-arrow-box {
transform: rotate(2deg);
.round-lage-arrow {
top: 21px;
left: -7px;
}
.round-arrow {
top: 38px;
left: 27px;
}
}
.model-box {
transform: rotate(223deg);
}
}
&:nth-child(5) {
top: 62px;
right: -27px;
transform: translateY(-50%) rotate(82deg);
.round-arrow-box {
transform: rotate(-15deg);
.round-lage-arrow {
top: 29px;
left: -11px;
}
.round-arrow {
top: 43px;
left: 22px;
}
}
.model-box {
transform: rotate(-84deg);
}
}
.round-arrow-box {
width: 50px;
height: 50px;
transform: rotate(0deg);
position: relative;
.round-lage-arrow {
width: 61px;
height: 34px;
overflow: hidden;
position: absolute;
top: 19px;
left: -16px;
img {
width: 100%;
}
}
.round-arrow {
width: 50px;
height: 28px;
overflow: hidden;
position: absolute;
top: 33px;
left: 20px;
img {
width: 100%;
}
}
}
.model-box {
width: 56px;
height: 56px;
border-radius: 50%;
background: url('../../assets/chart/model.png') no-repeat;
background-size: cover;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-top: 6px;
box-sizing: border-box;
.type-box {
width: 20px;
height: 20px;
border-radius: 50%;
line-height: 20px;
font-size: 12px;
color: #333;
background: #deebf7;
text-align: center;
}
.model-title {
font-size: 12px;
color: #97d7fb;
transform: scale(0.7);
margin-top: -3px;
}
}
.chart-active {
.type-box {
color: #fff;
background: #133158;
text-align: center;
z-index: 9;
}
.model-title {
color: #333;
}
.chart-bg {
position: absolute;
top: 3px;
left: 2.5px;
width: 51px;
height: 51px;
border-radius: 50%;
background: rgb(181 210 236);
}
}
}
}
.center-list {
width: 180px;
height: 180px;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
.center-round-list {
position: relative;
.center-item {
display: flex;
align-items: center;
justify-content: center;
column-gap: 6px;
position: absolute;
.stand {
width: 12px;
}
.out-arrow {
width: 24px;
}
.in-arrow {
width: 24px;
}
.sit {
width: 14px;
}
}
&:nth-child(1) {
.center-item {
transform: rotate(0deg);
top: 40px;
left: 72px;
}
.sit {
position: absolute;
left: 34px;
top: 0px;
}
.in-arrow {
position: absolute;
left: 15px;
top: 0;
transform: rotate(68deg);
}
.out-arrow {
position: absolute;
left: 9px;
top: 0px;
transform: rotate(70deg);
}
.stand {
position: absolute;
left: 3px;
top: 0;
}
}
&:nth-child(2) {
.center-item {
transform: rotate(-71deg) rotateY(-175deg);
top: 56px;
left: 47px;
}
.sit {
position: absolute;
left: 29px;
top: -3px;
transform: rotate(-70deg) rotateY(166deg);
}
.in-arrow {
position: absolute;
left: 15px;
top: 0;
transform: rotate(68deg);
}
.out-arrow {
position: absolute;
left: 10px;
top: -2px;
transform: rotate(70deg);
}
.stand {
position: absolute;
left: 3px;
top: -1px;
transform: rotate(-71deg) rotateY(172deg);
}
}
&:nth-child(3) {
.center-item {
transform: rotate(-136deg) rotateY(-175deg);
top: 117px;
left: 39px;
}
.sit {
position: absolute;
left: 32px;
top: 0px;
transform: rotate(-141deg) rotateY(166deg);
}
.in-arrow {
position: absolute;
left: 15px;
top: 0;
transform: rotate(68deg);
}
.out-arrow {
position: absolute;
left: 10px;
top: -2px;
transform: rotate(70deg);
}
.stand {
position: absolute;
left: 2px;
top: 4px;
transform: rotate(-112deg) rotateY(172deg);
}
}
&:nth-child(4) {
.center-item {
transform: rotate(-211deg) rotateY(-175deg);
top: 143px;
left: 109px;
}
.sit {
position: absolute;
left: 32px;
top: -3px;
transform: rotate(-225deg) rotateY(166deg);
}
.in-arrow {
position: absolute;
left: 15px;
top: 0;
transform: rotate(68deg);
}
.out-arrow {
position: absolute;
left: 10px;
top: -2px;
transform: rotate(70deg);
}
.stand {
position: absolute;
left: 4px;
top: 0px;
transform: rotate(-200deg) rotateY(172deg);
}
}
&:nth-child(5) {
.center-item {
transform: rotate(-299deg);
top: 56px;
left: 131px;
}
.sit {
position: absolute;
left: 32px;
top: -4px;
transform: rotate(-420deg);
}
.in-arrow {
position: absolute;
left: 15px;
top: 0;
transform: rotate(68deg);
}
.out-arrow {
position: absolute;
left: 10px;
top: -2px;
transform: rotate(70deg);
}
.stand {
position: absolute;
left: 9px;
top: -4px;
transform: rotate(-60deg);
}
}
}
}
.bottom-list {
position: absolute;
top: 89px;
left: 89px;
.model-box {
width: 58px;
height: 58px;
border-radius: 50%;
background: url('../../assets/chart/model.png') no-repeat;
background-size: cover;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding-top: 6px;
.type-box {
width: 20px;
height: 20px;
border-radius: 50%;
line-height: 20px;
font-size: 12px;
color: #333;
background: #deebf7;
text-align: center;
}
.model-title {
font-size: 12px;
color: #97d7fb;
transform: scale(0.8);
}
}
}
.model-box {
position: relative;
}
.chart-active {
.type-box {
color: #fff;
background: #133158;
text-align: center;
z-index: 9;
}
.model-title {
color: #333;
}
.chart-bg {
position: absolute;
top: 3px;
left: 2.5px;
width: 51px;
height: 51px;
border-radius: 50%;
background: rgb(181 210 236);
}
}
}
}
}

View File

@@ -0,0 +1,162 @@
<template>
<div
class="aside-box"
:class="{ 'is-sidebar-open': isCollapse }"
width="230px"
>
<div>
<div class="collapse-demo" :class="{ active: isCollapse }" width="230px">
<div class="collapse-title">
<div class="icon-box" @click="changeStatusFun">
<el-icon size="14" color="rgba(157, 215, 248, 1)"
><component :is="getIcon()"></component
></el-icon>
</div>
<div class="title">{{ generateTitle("模型调用流程") }}</div>
</div>
<div style="height: 100%" v-show="isCollapse">
<steps-view :reasoningList="reasoningList" />
</div>
<!-- <div class="user-box">
<img src="../../assets/logo.png" alt="" />
<span v-show="isCollapse">Jayson</span>
</div> -->
<div class="chart-box" v-show="isCollapse">
<div class="body-box">
<div class="out-round-list">
<div
class="out-round-item"
v-for="(item, index) in chartList.slice(1, 6)"
:key="index"
>
<div class="round-arrow-box">
<div class="round-lage-arrow">
<img src="../../assets/chart/round-lage-arrow.png" alt="" />
</div>
<div class="round-arrow" v-if="index != 4">
<img src="../../assets/chart/round-arrow.png" alt="" />
</div>
</div>
<div class="model-box" :class="{'chart-active': getStatus(item.title)}">
<div
class="chart-bg"
v-if="getStatus(item.title)"
></div>
<div class="type-box">{{ item.type }}</div>
<div class="model-title">{{ item.title }}</div>
</div>
</div>
</div>
<div class="center-list">
<div
class="center-round-list"
v-for="(item, index) in chartList.slice(1, 6)"
:key="item"
>
<div class="center-item">
<img class="sit" src="../../assets/chart/sit.png" alt="" />
<img
class="in-arrow"
src="../../assets/chart/in-arrow.png"
alt=""
/>
<img
class="out-arrow"
src="../../assets/chart/out-arrow.png"
alt=""
v-if="index != 4"
/>
<img
class="stand"
src="../../assets/chart/stand.png"
alt=""
/>
</div>
</div>
</div>
<div class="bottom-list">
<div class="model-box" :class="{'chart-active': getStatus(chartList[0].title)}">
<div
class="chart-bg"
v-if="getStatus(chartList[0].title)"
></div>
<div class="type-box">{{ chartList[0].type }}</div>
<div class="model-title">
{{ chartList[0].title.substr(0, 4) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import StepsView from "../StepsView.vue";
import { ref, defineProps, defineExpose, defineEmits } from "vue";
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import { generateTitle } from "../../utils/i18n";
const getIcon = () => {
return isCollapse.value ? ArrowLeft : ArrowRight;
};
const props = defineProps({
reasoningList: {
type: Array,
default: [],
},
});
const chartList = ref<Array<any>>([
{
type: "G1",
title: "Planner",
},
{
type: "G2",
title: "Retrieval",
},
{
type: "G3",
title: "Generate",
},
{
type: "G4",
title: "Converter",
},
{
type: "G5",
title: "Executor",
},
{
type: "G6",
title: "Optimize",
},
]);
const emits = defineEmits(["changeStatusFun"]);
const getStatus = (val: any) => {
let arr: any = props.reasoningList;
if (arr.length) {
let children: any = arr[arr.length - 1].children;
if (!children.length) {
return false;
}
let title = children[children.length - 1].title.split(":")[0].toLowerCase();
return title === val.toLowerCase();
} else {
return false;
}
};
const changeStatusFun = () => {
isCollapse.value = !isCollapse.value;
emits("changeStatusFun", isCollapse.value);
};
const isCollapse = ref<boolean>(true);
defineExpose({
isCollapse,
});
</script>
<style scoped>
@import "./index.less";
</style>

View File

@@ -0,0 +1,6 @@
// 通用请求头
export enum ContentTypeEnum {
Json = 'application/json;charset=UTF-8',
FormURLEncoded = 'application/x-www-form-urlencoded;charset=UTF-8',
FormData = 'multipart/form-data;charset=UTF-8'
}

31
frontend/src/lang/en.ts Normal file
View File

@@ -0,0 +1,31 @@
export default {
routers:{
logOut:'Log Out',
restore:'Restore data',
'智能协作无界,材料创新无限':'Collaborative Intelligence, Revolutionary Materials',
'问题设定':'Problem Setting',
'模型名称':'Model Name',
'新建对话':'Create New Conversation',
'推理完成,如果希望了解更多可继续在下面框中提问或新建对话。':'The reasoning is complete. If you wish to learn more, you can continue to ask questions or create a new conversation in the box below.',
'推理中':'In Reasoning',
'角色':'Role',
'模型调用流程':'Model Calling Process',
'发送':'send',
'您好多智能体MARS为您服务':'Hello,multi-agent MARS is at your service',
'推荐问题':'Recommended Questions',
'在此输入您的问题或需求有问必答Shift+Enter换行':'Enter your question or requirement here, answer all questions, Shift+Enter line break',
'MARS模型创制系统':'MARS Model Creation System',
'账号':'account',
'密码':'password',
'验证码':'code',
'登录':'login',
'请输入账号':'Please enter your account',
'请输入密码':'Please enter your password',
'请输入验证码':'Please enter your code',
'验证码错误':'code error',
'请输入问题':'Please enter the question',
'回答输出中,暂不能再次提问':'Answer output, cannot be asked again temporarily',
}
}

View File

@@ -0,0 +1,31 @@
import { createI18n } from 'vue-i18n'
// element-plus 中的语言配置
import elementEnLocale from 'element-plus/es/locale/lang/en'
import elementZhLocale from 'element-plus/es/locale/lang/zh-cn'
// 自己的语言配置
import enLocale from './en'
import zhLocale from './zh'
// 语言配置整合
const messages = {
en:{
...enLocale,
...elementEnLocale
},
'zh-cn':{
...zhLocale,
elementZhLocale
}
}
// 创建 i18n
const i18n = createI18n({
legacy: false,
globalInjection:true, // 全局模式,可以直接使用 $t
locale: 'zh-cn',
messages: messages
})
export default i18n

30
frontend/src/lang/zh.ts Normal file
View File

@@ -0,0 +1,30 @@
export default {
routers:{
logOut:'退出登录',
restore:'还原数据',
'智能协作无界,材料创新无限':'智能协作无界,材料创新无限',
'问题设定':'问题设定',
'模型名称':'模型名称',
'新建对话':'新建对话',
'推理完成,如果希望了解更多可继续在下面框中提问或新建对话。':'推理完成,如果希望了解更多可继续在下面框中提问或新建对话。',
'推理中':'推理中',
'角色':'角色',
'模型调用流程':'模型调用流程',
'发送':'发送',
'您好多智能体MARS为您服务':'您好多智能体MARS为您服务',
'推荐问题':'推荐问题',
'在此输入您的问题或需求有问必答Shift+Enter换行':'在此输入您的问题或需求有问必答Shift+Enter换行',
'MARS模型创制系统':'MARS模型创制系统',
'账号':'账号',
'密码':'密码',
'验证码':'验证码',
'登录':'登录',
'请输入账号':'请输入账号',
'请输入密码':'请输入密码',
'请输入验证码':'请输入验证码',
'验证码错误':'验证码错误',
'请输入问题':'请输入问题',
'回答输出中,暂不能再次提问':'回答输出中,暂不能再次提问',
}
}

View File

@@ -0,0 +1,19 @@
.common-layout {
width: 100%;
height: 100vh;
.el-main {
// width: 100vw;
height: calc(100vh - 40px);
}
.feeter-box {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: rgba(47, 136, 191, 1);
}
}

View File

@@ -0,0 +1,18 @@
<template>
<div class="common-layout">
<el-container>
<el-main style="padding: 40px 32px 32px">
<router-view></router-view>
</el-main>
<el-footer style="height: 40px">
<div class="feeter-box">{{ generateTitle('智能协作无界,材料创新无限') }}</div>
</el-footer>
</el-container>
</div>
</template>
<script setup lang="ts">
import {generateTitle} from '../utils/i18n'
</script>
<style scoped>
@import "./index.less";
</style>

19
frontend/src/main.ts Normal file
View File

@@ -0,0 +1,19 @@
import { createApp } from 'vue'
import './style.css'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
import { createPinia } from 'pinia'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import router from "./router"
import i18n from './lang/index'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(router)
app.use(i18n)
app.use(createPinia())
app.use(ElementPlus)
app.mount('#app')

View File

@@ -0,0 +1,58 @@
import { createRouter, createWebHashHistory } from "vue-router"
import Home from '../view/home/index.vue'
import Login from '../view/login/index.vue'
import Reasoning from '../view/reasoning/index.vue'
import LayoutView from '../layout/index.vue'
const routes = [
{
path: '/',
component: Login,
meta: {
title: '登录'
},
},
{
path: '/home',
redirect: '/home',
component: LayoutView,
meta: {
title: '主页'
},
children: [
{
path: '/home',
component: Home,
meta: {
title: '主页'
},
}
]
},
{
path: '/reasoning',
redirect: '/reasoning',
component: LayoutView,
meta: {
title: '推理中'
},
children: [
{
path: '/reasoning',
component: Reasoning,
meta: {
title: '推理中'
},
}
]
}
]
const router = createRouter({
history: createWebHashHistory(),
routes
});
export default router;

View File

@@ -0,0 +1,13 @@
// import { defineStore } from 'pinia'
// export const useUserStore = defineStore('user', {
// state: () => ({
// token: '', // 登录token
// }),
// getters: {},
// actions: {
// },
// persist: {
// key: 'user',
// paths: ['token']
// }
// })

44
frontend/src/style.css Normal file
View File

@@ -0,0 +1,44 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: url('./assets/background.png') no-repeat;
background-size: cover;
}
#app {
padding: 0;
margin: 0;
}
p {
margin: 0;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

99
frontend/src/types/axios.d.ts vendored Normal file
View File

@@ -0,0 +1,99 @@
import type { AxiosRequestConfig } from 'axios'
/**
* Axios请求配置
*/
export interface RequestOptions {
/**
* 接口地址
*
* 例: http://www.baidu.com/api
*/
apiUrl?: string
/**
* 是否自动添加接口前缀
*
* 例: http://www.baidu.com/api
* urlPrefix: 'api'
*/
isJoinPrefix?: boolean
/**
* 接口前缀
*/
urlPrefix?: string
/**
* POST请求的时候添加参数到Url中
*/
joinParamsToUrl?: boolean
/**
* 格式化提交参数时间
*/
formatDate?: boolean
/**
* 是否需要对响应数据进行处理
*/
isTransformResponse?: boolean
/**
* 是否返回原生响应头
*
* 例: 需要获取响应头时使用该属性
*/
isReturnNativeResponse?: boolean
/**
* 是否忽略请求取消令牌
*
* 如果启用,则重复请求时不进行处理
*
* 如果禁用,则重复请求时会取消当前请求
*/
ignoreCancelToken?: boolean
/**
* 自动对请求添加时间戳参数
*/
joinTime?: boolean
/**
* 是否携带Token
*/
withToken?: boolean
externalUrlStatus?: Number
/**
* 重试配置
*/
retry?: {
/**
* 重试次数
*/
count: number
/**
* 隔多久重试
*
* 单位: 毫秒
*/
delay: number
}
/**
* 接口级节流
*
* 单位: 毫秒
*/
throttle?: {
delay: number
}
/**
* 接口级防抖
*
* 单位: 毫秒
*/
debounce?: {
delay: number
}
}
export interface Result<T = any> {
code: number
data: T
}
export interface AxiosRequestConfigRetry extends AxiosRequestConfig {
retryCount?: number
}

5
frontend/src/types/env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
export interface ImportMetaEnv {
readonly VITE_IS_REQUEST_PROXY: string;
readonly VITE_API_URL: string;
readonly VITE_API_URL_PREFIX: string;
}

16
frontend/src/types/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,16 @@
// Vue
declare module '*.vue' {
import { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare type ClassName = { [className: string]: any } | ClassName[] | string
declare module '*.svg' {
const CONTENT: string
export default CONTENT
}
declare type Recordable<T = any> = Record<string, T>

15
frontend/src/types/interface.d.ts vendored Normal file
View File

@@ -0,0 +1,15 @@
export interface MenuRoute {
path: string
title?: string
name?: string
icon?:
| string
| {
render: () => void
}
redirect?: string
children: MenuRoute[]
meta: any
}
export type ClassName = { [className: string]: any } | ClassName[] | string

View File

@@ -0,0 +1,60 @@
import { reactive } from "vue";
interface ViolationBOType {
[key: string]: string;
}
const Agent: ViolationBOType = reactive({
// 1
Converter_Group_Admin: "Converter: Converter_Group_Admin",
scheme_converter: "Converter: scheme_converter",
converter_critic: "Converter: converter_critic",
mergrid_ploter: "Converter: mergrid_ploter",
scheme_code_writer: "Converter: scheme_code_writer",
scheme_code_critic: "Converter: scheme_code_critic",
// 2
expriment_code_writer: "Executor: expriment_code_writer",
data_collector: "Executor: data_collector",
collector_code_writer: "Executor: collector_code_writer",
Inner_Executor_Admin: "Executor: Inner_Executor_Admin",
// Inner_Executor_Admin: "Executor: Executor_Group_Admin",
// 3
Generate_Group_Admin: "Generate: Generate_Group_Admin",
structure_scientist: "Generate: structure_scientist",
property_scientist: "Generate: property_scientist",
application_scientist: "Generate: application_scientist",
synthesis_scientist: "Generate: synthesis_scientist",
scheme_critic: "Generate: scheme_critic",
// 4
analysis_executor: "Optimize: analysis_executor",
analysis_pl_uv: "Optimize: analysis_pl_uv",
analysis_picturer: "Optimize: analysis_picturer",
Experiment_Optimizer: "Optimize: Experiment_Optimizer",
optimizer_critic: "Optimize: optimizer_critic",
Analysis_Group_Admin: "Optimize: Analysis_Group_Admin",
//
Outer_Retrieval_Admin: "Retrieval: Outer_Retrieval_Admin",
Outer_Converter_Admin: "Converter: Outer_Converter_Admin",
Outer_Executor_Admin: "Executor: Outer_Executor_Admin",
experiment_executor: "Executor: experiment_executor",
Outer_Generate_Admin: "Generate: Outer_Generate_Admin",
Outer_Analysis_Admin: "Optimize: Outer_Analysis_Admin",
vector_code_executor: "Retrieval: vector_code_executor",
graphrag_code_executor: "Retrieval: graphrag_code_executor",
web_code_executor: "Retrieval: web_code_executor",
web_summary: "Retrieval: web_summary",
// 5
Inner_Retrieval_Admin: "Retrieval: Inner_Retrieval_Admin",
// Inner_Retrieval_Admin: "Retrieval: Retrieval_Group_Admin",
vector_searcher: "Retrieval: vector_searcher",
graphrag_searcher: "Retrieval: graphrag_searcher",
web_searcher: "Retrieval: web_searcher",
Planer: "Planner",
Planner: "Planner",
})
export function getAgent(data: any) {
let rData = Object.keys(Agent).filter(k => k.toLowerCase() === data.toLowerCase())[0]
return Agent[rData] || ''
}

View File

@@ -0,0 +1,10 @@
import i18n from "../lang/index"
export const generateTitle=(title:string)=>{
//i18n.global.te('routers.' + title) 只能在ts文件中用
const hasKey = i18n.global.te('routers.' + title)
if (hasKey) {
const translatedTitle = i18n.global.t('routers.' + title)
return translatedTitle
}
return title
}

View File

@@ -0,0 +1,324 @@
import axios, {
AxiosError,
AxiosInstance,
AxiosRequestConfig,
AxiosRequestHeaders,
AxiosResponse,
InternalAxiosRequestConfig
} from 'axios'
import cloneDeep from 'lodash/cloneDeep'
import debounce from 'lodash/debounce'
import isFunction from 'lodash/isFunction'
import throttle from 'lodash/throttle'
import { stringify } from 'qs'
import { ContentTypeEnum } from '../../constants'
import { AxiosRequestConfigRetry, RequestOptions, Result } from '../../types/axios'
import { AxiosCanceler } from './AxiosCancel'
import { CreateAxiosOptions } from './AxiosTransform'
// const store = useSettingStore();
// let tokenUpdateing = false;
// // 创建一个 axios 实例
// const service = axios.create({
// timeout: 5000,
// });
// const reqLists = ref<any>([]);
/**
* Axios 模块
*/
export class VAxios {
/**
* Axios实例句柄
* @private
*/
private instance: AxiosInstance
/**
* Axios配置
* @private
*/
private readonly options: CreateAxiosOptions
constructor(options: CreateAxiosOptions) {
this.options = options
this.instance = axios.create(options)
this.setupInterceptors()
}
/**
* 创建Axios实例
* @param config
* @private
*/
private createAxios(config: CreateAxiosOptions): void {
this.instance = axios.create(config)
}
/**
* 获取数据处理类
* @private
*/
private getTransform() {
const { transform } = this.options
return transform
}
/**
* 获取Axios实例
*/
getAxios(): AxiosInstance {
return this.instance
}
/**
* 配置Axios
* @param config
*/
configAxios(config: CreateAxiosOptions) {
if (!this.instance) return
this.createAxios(config)
}
/**
* 设置公共头部信息
* @param headers
*/
setHeader(headers: Record<string, string>): void {
if (!this.instance) return
Object.assign(this.instance.defaults.headers, headers)
}
/**
* 设置拦截器
* @private
*/
private setupInterceptors() {
const transform = this.getTransform()
if (!transform) return
const {
requestInterceptors,
requestInterceptorsCatch,
responseInterceptors,
responseInterceptorsCatch
} = transform
const axiosCanceler = new AxiosCanceler()
this.instance.defaults.withCredentials = false // 解决内网多api跨越问题
// 请求拦截器
this.instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
// 如果忽略取消令牌,则不会取消重复的请求
// @ts-ignore
const { ignoreCancelToken } = config.requestOptions
const ignoreCancel = ignoreCancelToken ?? this.options.requestOptions?.ignoreCancelToken
if (!ignoreCancel) axiosCanceler.addPending(config)
if (requestInterceptors && isFunction(requestInterceptors)) {
config = requestInterceptors(config, this.options) as InternalAxiosRequestConfig
}
return config
}, undefined)
// 请求错误处理
if (requestInterceptorsCatch && isFunction(requestInterceptorsCatch)) {
this.instance.interceptors.request.use(undefined, requestInterceptorsCatch)
}
// 响应结果处理
this.instance.interceptors.response.use(async (res: AxiosResponse) => {
if (res) axiosCanceler.removePending(res.config)
if (
responseInterceptors &&
isFunction(responseInterceptors) &&
responseInterceptors(res).data.code
) {
res = responseInterceptors(res)
}
return res
}, undefined)
// 响应错误处理
if (responseInterceptorsCatch && isFunction(responseInterceptorsCatch)) {
this.instance.interceptors.response.use(undefined, (error) =>
responseInterceptorsCatch(error, this.instance)
)
}
}
/**
* 支持 FormData 请求格式
* @param config
*/
supportFormData(config: AxiosRequestConfig) {
const headers = config.headers || (this.options.headers as AxiosRequestHeaders)
const contentType = headers?.['Content-Type'] || headers?.['content-type']
if (
contentType !== ContentTypeEnum.FormURLEncoded ||
!Reflect.has(config, 'data') ||
config.method?.toUpperCase() === 'GET'
) {
return config
}
return {
...config,
data: stringify(config.data, { arrayFormat: 'brackets' })
}
}
/**
* 支持 params 序列化
* @param config
*/
supportParamsStringify(config: AxiosRequestConfig) {
const headers = config.headers || this.options.headers
const contentType = headers?.['Content-Type'] || headers?.['content-type']
if (contentType === ContentTypeEnum.FormURLEncoded || !Reflect.has(config, 'params')) {
return config
}
return {
...config,
paramsSerializer: (params: any) => stringify(params, { arrayFormat: 'brackets' })
}
}
get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'GET' }, options)
}
post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'POST' }, options)
}
put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PUT' }, options)
}
delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'DELETE' }, options)
}
patch<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
return this.request({ ...config, method: 'PATCH' }, options)
}
/**
* 上传文件封装
* @param key 文件所属的key
* @param file 文件
* @param config 请求配置
* @param options
*/
upload<T = any>(
key: string,
file: File,
config: AxiosRequestConfig,
options?: RequestOptions
): Promise<T> {
const params: FormData = config.params ?? new FormData()
params.append(key, file)
return this.request(
{
...config,
method: 'POST',
headers: {
'Content-Type': ContentTypeEnum.FormData
},
params
},
options
)
}
/**
* 请求封装
* @param config
* @param options
*/
request<T = any>(config: AxiosRequestConfigRetry, options?: RequestOptions): Promise<T> {
const { requestOptions }: any = this.options
if (requestOptions.throttle !== undefined && requestOptions.debounce !== undefined) {
throw new Error('throttle and debounce cannot be set at the same time')
}
if (requestOptions.throttle && requestOptions.throttle.delay !== 0) {
return new Promise((resolve) => {
throttle(
() => resolve(this.synthesisRequest(config, options)),
requestOptions.throttle.delay
)
})
}
if (requestOptions.debounce && requestOptions.debounce.delay !== 0) {
return new Promise((resolve) => {
debounce(
() => resolve(this.synthesisRequest(config, options)),
requestOptions.debounce.delay
)
})
}
return this.synthesisRequest(config, options)
}
/**
* 请求方法
* @private
*/
private async synthesisRequest<T = any>(
config: AxiosRequestConfigRetry,
options?: RequestOptions
): Promise<T> {
let conf: CreateAxiosOptions = cloneDeep(config)
const transform = this.getTransform()
const { requestOptions } = this.options
const opt: RequestOptions = { ...requestOptions, ...options }
const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {}
if (beforeRequestHook && isFunction(beforeRequestHook)) {
conf = beforeRequestHook(conf, opt)
}
conf.requestOptions = opt
conf = this.supportFormData(conf)
// 支持params数组参数格式化因axios默认的toFormData即为brackets方式无需配置paramsSerializer为qs有需要可解除注释参数参考qs文档
// conf = this.supportParamsStringify(conf);
return new Promise((resolve, reject) => {
this.instance
.request<any, AxiosResponse<Result>>(!config.retryCount ? conf : config)
.then((res: AxiosResponse<Result>) => {
if (transformRequestHook && isFunction(transformRequestHook)) {
try {
const ret = transformRequestHook(res, opt)
resolve(ret)
} catch (err) {
reject(err || new Error('请求错误!'))
}
return
}
resolve(res as unknown as Promise<T>)
})
.catch((e: Error | AxiosError) => {
if (requestCatchHook && isFunction(requestCatchHook)) {
reject(requestCatchHook(e, opt))
return
}
if (axios.isAxiosError(e)) {
// 在这里重写Axios的错误信息
}
reject(e)
})
})
}
}

View File

@@ -0,0 +1,67 @@
import type { AxiosRequestConfig, Canceler } from 'axios';
import axios from 'axios';
import isFunction from 'lodash/isFunction';
// 存储请求与取消令牌的键值对列表
let pendingMap = new Map<string, Canceler>();
/**
* 获取请求Url
* @param config
*/
export const getPendingUrl = (config: AxiosRequestConfig) => [config.method, config.url].join('&');
/**
* @description 请求管理器
*/
export class AxiosCanceler {
/**
* 添加请求到列表中
* @param config
*/
addPending(config: AxiosRequestConfig) {
this.removePending(config);
const url = getPendingUrl(config);
config.cancelToken =
config.cancelToken ||
new axios.CancelToken((cancel) => {
if (!pendingMap.has(url)) {
// 如果当前没有相同请求就添加
pendingMap.set(url, cancel);
}
});
}
/**
* 移除现有的所有请求
*/
removeAllPending() {
pendingMap.forEach((cancel) => {
if (cancel && isFunction(cancel)) cancel();
});
pendingMap.clear();
}
/**
* 移除指定请求
* @param config
*/
removePending(config: AxiosRequestConfig) {
const url = getPendingUrl(config);
if (pendingMap.has(url)) {
// If there is a current request identifier in pending,
// the current request needs to be cancelled and removed
const cancel = pendingMap.get(url);
if (cancel) cancel(url);
pendingMap.delete(url);
}
}
/**
* 重置
*/
reset() {
pendingMap = new Map<string, Canceler>();
}
}

View File

@@ -0,0 +1,67 @@
import type { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { AxiosError } from 'axios'
import type { RequestOptions, Result } from '../../types/axios'
/**
* @description 创建Axios实例配置
*/
export interface CreateAxiosOptions extends AxiosRequestConfig {
/**
* 请求验证方案
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
*/
authenticationScheme?: string
/**
* 请求数据处理
*/
transform?: AxiosTransform
/**
* 请求配置
*/
requestOptions?: RequestOptions
}
/**
* Axios请求数据处理 抽象类
*/
export abstract class AxiosTransform {
/**
* 请求前钩子
*/
beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig
/**
* 数据处理前钩子
*/
transformRequestHook?: <T = any>(res: AxiosResponse<Result>, options: RequestOptions) => T
/**
* 请求失败钩子
*/
requestCatchHook?: <T = any>(e: Error | AxiosError, options: RequestOptions) => Promise<T>
/**
* 请求拦截器
*/
requestInterceptors?: (
config: AxiosRequestConfig,
options: CreateAxiosOptions
) => AxiosRequestConfig
/**
* 响应拦截器
*/
responseInterceptors?: (res: AxiosResponse) => AxiosResponse
/**
* 请求拦截器错误处理
*/
requestInterceptorsCatch?: (error: AxiosError) => void
/**
* 响应拦截器错误处理
*/
responseInterceptorsCatch?: (error: AxiosError, instance: AxiosInstance) => void
}

View File

@@ -0,0 +1,222 @@
// axios配置 可自行根据项目进行更改,只需更改该文件即可,其他文件可以不动
// import type { AxiosInstance } from 'axios'
import isString from 'lodash/isString'
import merge from 'lodash/merge'
// import { useRouter } from 'vue-router';
// import { UserRefresh } from '@api/userCenter';
// import { useUserStore } from '../../store'
import { VAxios } from './Axios'
import type { AxiosTransform, CreateAxiosOptions } from './AxiosTransform'
import { formatRequestDate, joinTimestamp, setObjToUrlParams } from './utils'
// 通用请求头
export enum ContentTypeEnum {
Json = 'application/json;charset=UTF-8',
FormURLEncoded = 'application/x-www-form-urlencoded;charset=UTF-8',
FormData = 'multipart/form-data;charset=UTF-8'
}
const env = import.meta.env.MODE || 'development'
// 如果是mock模式 或 没启用直连代理 就不配置host 会走本地Mock拦截 或 Vite 代理
const host =
env === 'mock' || import.meta.env.VITE_IS_REQUEST_PROXY === 'true'
? ''
: import.meta.env.VITE_API_URL
// 数据处理,方便区分多种处理方式
const transform: AxiosTransform = {
// 处理请求数据。如果数据不是预期格式,可直接抛出错误
transformRequestHook: (res, options) => {
const { isTransformResponse, isReturnNativeResponse } = options
// 如果204无内容直接返回
const method:any = res.config.method?.toLowerCase()
if (res.status === 204 && ['put', 'patch', 'delete'].includes(method)) {
return res
}
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
if (isReturnNativeResponse) {
return res
}
// 不进行任何处理,直接返回
// 用于页面代码可能需要直接获取codedatamessage这些信息时开启
if (!isTransformResponse) {
return res.data
}
// 错误的时候返回
const { data } = res
if (!data) {
throw new Error('请求接口错误')
}
// 这里 code为 后台统一的字段,需要在 types.ts内修改为项目自己的接口返回格式
const { code } = data
// 这里逻辑可以根据项目进行修改
const hasSuccess = data && code === 200
if (hasSuccess) {
return data.data
}
throw new Error(`请求接口错误, 错误码: ${code}`)
},
// 请求前处理配置
beforeRequestHook: (config, options) => {
const {
apiUrl,
isJoinPrefix,
urlPrefix,
joinParamsToUrl,
formatDate,
joinTime = true
} = options
// 添加接口前缀
if (isJoinPrefix && urlPrefix && isString(urlPrefix)) {
let u = urlPrefix
config.url = `${u}${config.url}`
}
// 将baseUrl拼接
if (apiUrl && isString(apiUrl)) {
config.url = `${apiUrl}${config.url}`
}
const params = config.params || {}
const data = config.data || false
if (formatDate && data && !isString(data)) {
formatRequestDate(data)
}
if (config.method?.toUpperCase() === 'GET') {
if (!isString(params)) {
// 给 get 请求加上时间戳参数,避免从缓存中拿数据。
config.params = Object.assign(params || {}, joinTimestamp(joinTime, false))
} else {
// 兼容restful风格
config.url = `${config.url + params}${joinTimestamp(joinTime, true)}`
config.params = undefined
}
} else if (!isString(params)) {
if (formatDate) {
formatRequestDate(params)
}
if (
Reflect.has(config, 'data') &&
config.data &&
(Object.keys(config.data).length > 0 || data instanceof FormData)
) {
config.data = data
config.params = params
} else {
// 非GET请求如果没有提供data则将params视为data
config.data = params
config.params = undefined
}
if (joinParamsToUrl) {
config.url = setObjToUrlParams(config.url as string, { ...config.params, ...config.data })
}
} else {
// 兼容restful风格
config.url += params
config.params = undefined
}
return config
},
// 请求拦截器处理
requestInterceptors: (config) => {
// 请求之前处理config
// const userStore = useUserStore()
// const { token } = userStore
const token = localStorage.getItem('token')
if (token && (config as Recordable)?.requestOptions?.withToken !== false) {
;(config as Recordable).headers['x-rtt-login-token'] = token
}
return config
},
// 响应拦截器处理
responseInterceptors: (res) => {
return res
},
// 响应错误处理
responseInterceptorsCatch: (error: any, instance: any) => {
const { config } = error
if (!config || !config.requestOptions.retry) return Promise.reject(error)
config.retryCount = config.retryCount || 0
if (config.retryCount >= config.requestOptions.retry.count) return Promise.reject(error)
config.retryCount += 1
const backoff = new Promise((resolve) => {
setTimeout(() => {
resolve(config)
}, config.requestOptions.retry.delay || 1)
})
config.headers = { ...config.headers, 'Content-Type': ContentTypeEnum.Json }
return backoff.then((config) => instance.request(config))
}
}
function createAxios(opt?: Partial<CreateAxiosOptions>) {
return new VAxios(
merge(
<CreateAxiosOptions>{
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#authentication_schemes
// 例如: authenticationScheme: 'Bearer'
authenticationScheme: '',
// 超时
timeout: 10 * 1000,
// 携带Cookie
withCredentials: true,
// 头信息
headers: {
'Content-Type': ContentTypeEnum.Json,
},
// 数据处理方式
transform,
// 配置项,下面的选项都可以在独立的接口请求中覆盖
requestOptions: {
// 接口地址
apiUrl: host,
// 是否自动添加接口前缀
isJoinPrefix: true,
// 接口前缀
// 例如: https://www.baidu.com/api
// urlPrefix: '/api'
urlPrefix: import.meta.env.VITE_API_URL_PREFIX,
// 是否返回原生响应头 比如:需要获取响应头时使用该属性
isReturnNativeResponse: false,
// 需要对返回数据进行处理
isTransformResponse: false,
// post请求的时候添加参数到url
joinParamsToUrl: false,
// 格式化提交参数时间
formatDate: true,
// 是否加入时间戳
joinTime: true,
// 是否忽略请求取消令牌
// 如果启用,则重复请求时不进行处理
// 如果禁用,则重复请求时会取消当前请求
ignoreCancelToken: true,
// 是否携带token
withToken: true,
externalUrlStatus: 0,
// 重试
retry: {
count: 3,
delay: 1000
}
}
},
opt || {}
)
)
}
export const request = createAxios()

View File

@@ -0,0 +1,57 @@
import isObject from 'lodash/isObject'
import isString from 'lodash/isString'
const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'
export function joinTimestamp<T extends boolean>(
join: boolean,
restful: T
): T extends true ? string : object
export function joinTimestamp(join: boolean, restful = false): string | object {
if (!join) {
return restful ? '' : {}
}
const now = new Date().getTime()
if (restful) {
return `?_t=${now}`
}
return { _t: now }
}
// 格式化提交参数时间
export function formatRequestDate(params: Recordable) {
if (Object.prototype.toString.call(params) !== '[object Object]') {
return
}
for (const key in params) {
// eslint-disable-next-line no-underscore-dangle
if (params[key] && params[key]._isAMomentObject) {
params[key] = params[key].format(DATE_TIME_FORMAT)
}
if (isString(key)) {
const value = params[key]
if (value) {
try {
params[key] = isString(value) ? value.trim() : value
} catch (error: any) {
throw new Error(error)
}
}
}
if (isObject(params[key])) {
formatRequestDate(params[key])
}
}
}
// 将对象转为Url参数
export function setObjToUrlParams(baseUrl: string, obj: { [index: string]: any }): string {
let parameters = ''
for (const key in obj) {
parameters += `${key}=${encodeURIComponent(obj[key])}&`
}
parameters = parameters.replace(/&$/, '')
return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, '?') + parameters
}

View File

@@ -0,0 +1,25 @@
function useWebsocket(handleMessage: any) {
const ws = new WebSocket(import.meta.env.VITE_WB_BASE_URL)
const init = () => {
bindEvent();
}
const bindEvent = () => {
ws.addEventListener('open',handleOpen,false)
ws.addEventListener('close',handleClose,false)
ws.addEventListener('error',handleError,false)
ws.addEventListener('message',handleMessage,false)
}
const handleOpen = (e: any) => {
console.log('websocket open',e);
}
const handleClose = (e: any) => {
console.log('websocket close',e);
}
const handleError = (e: any) => {
console.log('webscoket error',e)
}
init();
return ws
}
export default useWebsocket

View File

@@ -0,0 +1,84 @@
.content {
max-width: 1196px;
// width: 100%;
margin: 0 auto;
padding-top: 98px;
.title-box {
width: 100%;
display: flex;
align-items: center;
column-gap: 8px;
margin-bottom: 16px;
img {
width: 40px;
height: 40px;
}
span {
font-size: 30px;
color: #fff;
font-weight: 600;
}
}
.recommend-box {
width: 100%;
margin-top: 72px;
box-sizing: border-box;
.recommend-title {
display: flex;
align-items: center;
column-gap: 8px;
margin-bottom: 12px;
img {
width: 22px;
height: 24px;
}
span {
font-size: 22px;
font-weight: 600;
color: #fff;
}
}
.recommend-bottm {
width: 100%;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
column-gap: 24px;
row-gap: 24px;
.recommend-list {
width: 100%;
color: #fff;
display: flex;
flex-wrap: wrap;
column-gap: 24px;
row-gap: 12px;
box-sizing: border-box;
.item {
// max-width: 438px;
width: calc(50% - 12px);
padding: 8px 16px;
border: 1px solid rgba(37, 79, 127, 1);
border-radius: 6px;
box-sizing: border-box;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
text-align: justify;
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
<template>
<div class="content">
<div class="title-box">
<img src="../../assets/logo.png" alt="" />
<span>{{ generateTitle('您好多智能体MARS为您服务') }}</span>
</div>
<TextareaView @submitFun="reasoningFun" :minRows="6"></TextareaView>
<div class="recommend-box">
<div class="recommend-title">
<img src="../../assets/home/recommend.png" alt="" />
<span>{{ generateTitle('推荐问题') }}</span>
</div>
<div class="recommend-bottm">
<div class="recommend-list">
<div
class="item"
@click="submitFun(item)"
v-for="(item, index) in recommendList"
:key="index"
>
{{ item }}
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import TextareaView from '../../components/TextareaView.vue'
import { generateTitle } from '../../utils/i18n'
const recommendList = ref<Array<any>>([
'Structure: How can the self-assembly of materials be achieved for nanoparticles by controlling intermolecular interactions?',
'Property: For perovskite nanocrystals, how can blue-shifting of the fluorescence emission wavelength be controlled through structural design?',
'Synthesis: How to synthesize CsPbBr₃ nanocubes at room temperature?',
'Application: How can the photoluminescence quantum efficiency of perovskites in LED devices be improved?'
])
const router = useRouter()
const submitFun = (e: any) => {
let data = {
chat_id: new Date().getTime(),
message: e
}
router.push('/reasoning?message=' + JSON.stringify(data))
}
const reasoningFun = (e: any) => {
router.push('/reasoning?message=' + e)
}
onMounted(() => {})
</script>
<style scoped>
@import './index.less';
</style>

View File

@@ -0,0 +1,62 @@
.login-box {
width: 100vw;
height: 100vh;
background: url('../../assets/login/background.png') no-repeat;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
.content {
width: 450px;
background: rgba(255, 255, 255, 0.8);
box-sizing: border-box;
padding: 40px;
border-radius: 16px;
box-shadow: 0px 15px 15px 0px rgba(0, 0, 0, 0.05);
.title {
width: 100%;
display: flex;
align-items: center;
column-gap: 12px;
margin-bottom: 24px;
img {
width: 40px;
height: 40px;
}
span {
font-size: 32px;
color: #333;
font-weight: 600;
}
}
.code-box {
width: 100%;
display: flex;
align-items: center;
column-gap: 12px;
}
:deep(.el-form) {
margin-bottom: 34px;
.el-input__inner {
height: 56px;
font-size: 16px;
}
}
.btn-box {
width: 100%;
.el-button {
width: 100%;
height: 60px;
}
}
}
}

View File

@@ -0,0 +1,109 @@
<template>
<div class="login-box">
<div v-loading="loading" class="content">
<div class="title">
<img src="/logo.png" alt="" />
<span>{{generateTitle('MARS模型创制系统')}}</span>
</div>
<el-form :model="loginData">
<el-form-item>
<el-input
:placeholder="generateTitle('账号')"
v-model="loginData.user_name"
:prefix-icon="UserFilled"
></el-input>
</el-form-item>
<el-form-item>
<el-input
:placeholder="generateTitle('密码')"
type="password"
v-model="loginData.pass_word"
:prefix-icon="Lock"
show-password
></el-input>
</el-form-item>
<el-form-item>
<div class="code-box">
<el-input
:placeholder="generateTitle('验证码')"
v-model="loginData.code"
:prefix-icon="Lock"
></el-input>
<ValidCode ref="validCodeRef" :height="56"></ValidCode>
</div>
</el-form-item>
</el-form>
<div class="btn-box">
<el-button
@click="submitFun"
type="primary"
element-loading-spinner="el-icon-loading"
element-loading-background="rgba(0, 0, 0, 0.3)"
v-loading.fullscreen.lock="loading"
>{{generateTitle("登录")}}</el-button
>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { UserFilled, Lock } from '@element-plus/icons-vue'
import ValidCode from '../../components/ValidCode.vue'
import { ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { authLogin } from '../../api/user'
import {generateTitle} from '../../utils/i18n'
const router = useRouter()
const loginData = ref<any>({
user_name: '',
pass_word: '',
code: ''
})
const loading = ref(false)
const handleKeyPress = (event: KeyboardEvent) => {
if (event.key === 'Enter') {
submitFun()
}
}
const validCodeRef = ref()
const submitFun = async () => {
if (!loginData.value.user_name) {
ElMessage.error(generateTitle('请输入账号'))
return
}
if (!loginData.value.pass_word) {
ElMessage.error(generateTitle('请输入密码'))
return
}
if (!loginData.value.user_name) {
ElMessage.error(generateTitle('请输入验证码'))
return
}
let code = validCodeRef.value.validate(loginData.value.code)
if (!code) {
ElMessage.error(generateTitle('验证码错误'))
return
}
loading.value = true
try {
const { token }: any = await authLogin(loginData.value)
localStorage.setItem('token', token)
loading.value = false
router.push('/home')
} catch (error: any) {
loading.value = false
ElMessage.error(error.response.data.error)
}
}
onMounted(() => {
window.addEventListener('keypress', handleKeyPress)
})
onUnmounted(() => {
window.removeEventListener('keypress', handleKeyPress)
})
</script>
<style scoped>
@import './index.less';
</style>

View File

@@ -0,0 +1,74 @@
.reasoning-content {
width: 100%;
display: flex;
align-items: flex-start;
box-sizing: border-box;
overflow: hidden;
.content-left {
width: 100%;
.body-box {
width: 100%;
overflow: auto;
height: calc(90vh - 175px);
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-thumb {
background: #1A87CA;
}
&::-webkit-scrollbar-thumb:hover {
background: #1A87CA;
}
}
.message-box {
width: calc(100% - 48px);
padding-top: 30px;
.tip-box {
width: 100%;
display: flex;
align-items: center;
column-gap: 12px;
.tip-text {
color: #fff;
font-size: 16px;
}
}
.active_item {
// width: 104px;
box-sizing: border-box;
padding: 0 8px;
font-size: 14px;
height: 32px;
margin: 0;
color: rgba(73, 252, 255, 0.80);
border-radius: 32px;
border: 1px solid rgba(24, 91, 197, 1);
box-shadow: 0px 0px 6px 0px rgba(0, 213, 250, 1) inset;
display: flex;
align-items: center;
justify-content: center;
column-gap: 4px;
background: #0C3870;
margin-bottom: 8px;
.el-icon {
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(180deg, #62FAF8 0%, #2DC9FE 100%);
}
}
}
}
}

View File

@@ -0,0 +1,213 @@
<template>
<div class="reasoning-content">
<div
class="content-left"
:style="{
width: isCollapse ? 'calc(100% - 200px)' : 'calc(100% - 7px)'
}"
>
<div class="body-box" ref="container">
<reasoning-view
ref="reasoningRef"
@completeFun="completeFun"
:reasoningList="reasoningList"
/>
</div>
<div class="message-box">
<div class="tip-box">
<p class="active_item">
<el-icon size="6" color="#fff"><Plus /></el-icon>
{{ generateTitle('新建对话') }}
</p>
<div class="tip-text" v-show="disableStatus">
{{ generateTitle('回答输出中暂不能再次提问') }}
</div>
</div>
<TextareaView ref="textareaRef" :minRows="1" @submitFun="submitFun" />
</div>
</div>
<collapseView
ref="collapseRef"
:reasoningList="reasoningList"
@changeStatusFun="changeStatusFun"
/>
</div>
</template>
<script setup lang="ts">
import ReasoningView from '../../components/ReasoningView/index.vue'
import TextareaView from '../../components/TextareaView.vue'
import collapseView from '../../components/collapseView/index.vue'
import { Plus } from '@element-plus/icons-vue'
import { ref, onMounted, nextTick } from 'vue'
// import { getModelList } from '../../api/user'
import { useRoute } from 'vue-router'
import useWebSocket from '../../utils/websocket'
import { getAgent } from '../../utils/agent'
import { generateTitle } from '../../utils/i18n'
const route: any = useRoute()
const reasoningRef = ref<any>(null)
const textareaRef = ref<any>(null)
const collapseRef = ref<any>(null)
const isCollapse = ref<boolean>(true)
const changeStatusFun = (val: boolean) => {
isCollapse.value = val
}
const reasoningList = ref<Array<any>>([])
// const getModelListFun = async () => {
// try {
// const { data, code } = await getModelList()
// if (code) {
// reasoningList.value = data
// }
// } catch (error) {
// console.log(error)
// }
// }
const submitFun = (val: any) => {
completeFun()
addMode(JSON.parse(val).message)
ws.send(val)
}
const addMode = (val: any) => {
endStatus.value = false
reasoningList.value.push({
title: val,
children: []
})
reasoningRef.value.completeList.push({
show: false,
index: reasoningRef.value.completeList.length
})
}
const disableStatus = ref(false)
const completeFun = () => {
disableStatus.value = true
textareaRef.value.disableStatus = true
}
const handleMessage = (e: any) => {
getMessage(e.data)
getHeight()
}
const startStatus = ref(false)
const contentStatus = ref(false)
const endStatus = ref(false)
const getMessage = async (e: any) => {
// if (e.split('TERMINATE')[1]) {
// talkList.value += e.split('TERMINATE')[0]
// reasonStatus.value.show = false
// endShow.value = true
// extractSynthesisProcess()
// } else {
// talkList.value += e
// }
reasoningRef.value.reasonStatus.show = true
// talkList.value += e
// console.log(talkList.value);
let list = reasoningList.value
let children = list[list.length - 1].children
// if (
// e.includes(
// '--------------------------------------------------------------------------------'
// )
// ) {
// startStatus.value = true
// } else if (startStatus.value) {
if (parseChatData(e)) {
if (parseChatData(e)?.type) {
children.push({
title: parseChatData(e)?.title,
content: ''
})
reasoningRef.value.reasonStatus.index = children.length - 1
} else {
if (children[children.length - 1].content) {
children[children.length - 1].content += parseChatData(e)?.current
} else {
children[children.length - 1].content = parseChatData(e)?.current
}
}
}
// }
}
function parseChatData(e: any) {
let condition =
!e.includes(
'--------------------------------------------------------------------------------'
) &&
!e.includes(
`********************************************************************************`
) &&
!e.includes('Starting a new chat....') &&
!e.includes('>>>>>>>>')
let title = ''
let current = ''
let regex = /Next speaker: (.*)/
if (condition) {
if (e.includes('Planer') || e.includes('Planner')) {
title = getAgent('Planer')
return {
title,
type: true
}
}
if (e.match(regex) && e.match(regex)[1]) {
// if (!getAgent(e.match(regex)[1])) {
// contentStatus.value = true
// return
// }
// contentStatus.value = false
// title = getAgent(e.match(regex)[1])
title = getAgent(e.match(regex)[1]) || e.match(regex)[1]
return {
title,
type: true
}
}
if (contentStatus.value) {
return
}
if (e.includes('TERMINATE')) {
endStatus.value = true
reasoningRef.value.reasonStatus.show = false
reasoningRef.value.completeList[
reasoningRef.value.completeList.length - 1
].show = true
disableStatus.value = false
textareaRef.value.disableStatus = false
startStatus.value = false
}
current = e.replace(
/(?<!`)TERMINATE(?<!\*\*)|TERMINATE(?!\*\*)|`TERMINATE`/g,
''
)
return {
current,
type: false
}
}
}
const ws = useWebSocket(handleMessage)
const sendFun = () => {
setTimeout(() => {
addMode(JSON.parse(route.query.message).message)
completeFun()
ws.send(JSON.stringify(route.query.message))
}, 300)
}
const container = ref<any>(null)
const getHeight = () => {
container.value.scrollTop = container.value.scrollHeight + 150
collapseRef.value.scrollTop = collapseRef.value.scrollHeight + 50
}
onMounted(() => {
// getModelListFun();
nextTick(() => {
getHeight()
sendFun()
})
})
</script>
<style scoped>
@import './index.less';
</style>

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />