mirror of
https://github.com/NicolasBohn/NexQuant.git
synced 2026-08-04 02:37:44 +00:00
feat: add a web UI server (#1345)
* update rdagent cmd * fix log error message * use multiProcessing.Process instead of subprocess.Popen * add traces to gitignore * add user interactor in RDLoop (finance scenarios) * add interactor (feedback, hypothesis) for quant scens * fix the test_end in qlib conf * add features init config, general instruction to qlib scenarios * set base features for based exp * fix bug when combine factors * move traces folder to git_ignore_folder * fix bug in features init * fix quant interact bug * fix logger warning error * bug fixes * modify rdagent logger, now it can set file output * adjust cli functions and fix logger bug * fix server port transport problem * update server_ui in cli * add web code * fix CI problem * black fix * update web ui README * update README * update readme
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div class="chart-box">
|
||||
<div
|
||||
class="chart-item"
|
||||
v-for="(item, index) in keyList"
|
||||
:key="item"
|
||||
:style="{ width: 100 / keyList.length + '%' }"
|
||||
>
|
||||
<div
|
||||
class="zoom"
|
||||
@click="zoom(colors[index], metricData[item], item)"
|
||||
></div>
|
||||
<lineChart
|
||||
:color="colors[index]"
|
||||
:data="metricData[item]"
|
||||
:chartName="item"
|
||||
:smallSize="true"
|
||||
></lineChart>
|
||||
</div>
|
||||
<div class="dialog-box" v-if="showDialog">
|
||||
<div class="dialog-content gradient-border">
|
||||
<div class="close" @click="close"></div>
|
||||
<lineChart
|
||||
:color="dialogColor"
|
||||
:data="dialogData"
|
||||
:chartName="dialogName"
|
||||
:smallSize="false"
|
||||
></lineChart>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, defineProps, watch, ref } from "vue";
|
||||
import lineChart from "../components/lineChartOne.vue";
|
||||
|
||||
const props = defineProps({
|
||||
metricData: Object,
|
||||
});
|
||||
const metricData = ref(props.metricData);
|
||||
const colors = ["red", "blue", "orange", "green"];
|
||||
const keyList = ref([]);
|
||||
const showDialog = ref(false);
|
||||
const updateData = () => {
|
||||
keyList.value = Object.keys(metricData.value);
|
||||
};
|
||||
const dialogColor = ref("");
|
||||
const dialogData = ref(null);
|
||||
const dialogName = ref("");
|
||||
const zoom = (color, data, name) => {
|
||||
dialogColor.value = color;
|
||||
dialogData.value = data;
|
||||
showDialog.value = true;
|
||||
dialogName.value = name;
|
||||
};
|
||||
const close = () => {
|
||||
showDialog.value = false;
|
||||
dialogColor.value = "";
|
||||
dialogData.value = null;
|
||||
dialogName.value = "";
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.metricData,
|
||||
(newValue, oldValue) => {
|
||||
metricData.value = newValue;
|
||||
updateData();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
updateData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chart-box {
|
||||
display: flex;
|
||||
gap: 1.8em;
|
||||
margin-bottom: 1.8em;
|
||||
.chart-item {
|
||||
background-color: var(--bg-white);
|
||||
max-width: 500px;
|
||||
min-width: 0;
|
||||
border-radius: 35.5px;
|
||||
position: relative;
|
||||
box-shadow: 1px 1px 2px 0px rgba(255, 255, 255, 0.3) inset,
|
||||
-1px -1px 2px 0px rgba(221, 221, 221, 0.5) inset,
|
||||
-10px 10px 20px 0px rgba(221, 221, 221, 0.2),
|
||||
10px -10px 20px 0px rgba(221, 221, 221, 0.2),
|
||||
-10px -10px 20px 0px rgba(255, 255, 255, 0.9),
|
||||
10px 10px 25px 0px rgba(221, 221, 221, 0.9);
|
||||
.zoom {
|
||||
position: absolute;
|
||||
right: 1.125em;
|
||||
top: 0.8em;
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/zoom.svg) no-repeat;
|
||||
background-size: contain;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
.dialog-box {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: rgba(255, 255, 255, 0.29);
|
||||
backdrop-filter: blur(4.599999904632568px);
|
||||
z-index: 999999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.dialog-content {
|
||||
width: 60%;
|
||||
height: 498px;
|
||||
background-color: #fff;
|
||||
border-radius: 18px;
|
||||
--border-radius: 20px;
|
||||
--border-width: 2px;
|
||||
// padding: 3em 4em;
|
||||
padding-bottom: 2em;
|
||||
margin-top: -4em;
|
||||
position: relative;
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 1.35em;
|
||||
top: 0.9em;
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/close.svg) no-repeat;
|
||||
background-size: contain;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<div class="code">
|
||||
<div class="code-content">
|
||||
<SvgIcon
|
||||
@click="fullScreen"
|
||||
class="expand-icon"
|
||||
color="#2b2b2b"
|
||||
name="fullscreen"
|
||||
></SvgIcon>
|
||||
<SvgIcon
|
||||
@click="copy"
|
||||
class="copy-icon"
|
||||
color="#2b2b2b"
|
||||
name="copy"
|
||||
></SvgIcon>
|
||||
<div
|
||||
class="md-code"
|
||||
:class="{
|
||||
'full-dev': fullScreenFlag && developer,
|
||||
'full-no-dev': fullScreenFlag && !developer,
|
||||
'no-full-dev': !fullScreenFlag && developer,
|
||||
'no-full-no-dev': !fullScreenFlag && !developer,
|
||||
}"
|
||||
>
|
||||
<pre
|
||||
class="code-display language-python"
|
||||
><code v-html="highlightedCode"></code>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, defineProps, defineEmits, nextTick } from "vue";
|
||||
// main.js 或 Vue 组件内部
|
||||
import "prismjs";
|
||||
import "prismjs/components/prism-python.min.js"; // 导入Python的语言支持
|
||||
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
markdown: String,
|
||||
developer: Boolean,
|
||||
fullscreen: Boolean,
|
||||
});
|
||||
|
||||
const emit = defineEmits(["fullScreen"]);
|
||||
const markdown = ref(props.markdown);
|
||||
const developer = ref(props.developer);
|
||||
const highlightedCode = ref("");
|
||||
const fullScreenFlag = ref(props.fullscreen);
|
||||
watch(
|
||||
() => [props.markdown, props.developer, props.fullscreen],
|
||||
(newValue, oldValue) => {
|
||||
markdown.value = newValue[0];
|
||||
developer.value = newValue[1];
|
||||
fullScreenFlag.value = newValue[2];
|
||||
highlightCode();
|
||||
}
|
||||
);
|
||||
|
||||
const highlightCode = () => {
|
||||
// 使用 PrismJS 对 Python 代码进行高亮
|
||||
highlightedCode.value = Prism.highlight(
|
||||
markdown.value,
|
||||
Prism.languages.python,
|
||||
"python"
|
||||
);
|
||||
};
|
||||
|
||||
const copy = () => {
|
||||
navigator.clipboard.writeText(markdown.value);
|
||||
ElMessage({
|
||||
message: "Copy Success.",
|
||||
type: "success",
|
||||
plain: true,
|
||||
});
|
||||
};
|
||||
|
||||
const fullScreen = () => {
|
||||
fullScreenFlag.value = !fullScreenFlag.value;
|
||||
emit("fullScreen", fullScreenFlag.value);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
// const codeBlock = document.querySelector("pre code");
|
||||
// hljs.highlightElement(codeBlock);
|
||||
highlightCode();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.code {
|
||||
width: 100%;
|
||||
.code-content {
|
||||
border-radius: 11px;
|
||||
background: var(--bg-white);
|
||||
padding: 1.35em 0 0 0.9em;
|
||||
box-sizing: border-box;
|
||||
overflow-y: hidden;
|
||||
position: relative;
|
||||
|
||||
.expand-icon {
|
||||
position: absolute;
|
||||
right: 3.6em;
|
||||
top: 0.45em;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
width: 1.24em;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.copy-icon {
|
||||
position: absolute;
|
||||
right: 1.35em;
|
||||
top: 0.45em;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
width: 1.24em;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
.md-code {
|
||||
height: calc(100vh - 28.35em);
|
||||
max-width: 100%;
|
||||
font-size: 0.9em;
|
||||
line-height: 140%;
|
||||
overflow: auto;
|
||||
// &:hover {
|
||||
// overflow: auto;
|
||||
// }
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.full-dev {
|
||||
height: calc(100vh - 26.13em);
|
||||
}
|
||||
&.full-no-dev {
|
||||
height: calc(100vh - 19.98em);
|
||||
}
|
||||
&.no-full-dev {
|
||||
height: calc(100vh - 32.8em);
|
||||
}
|
||||
&.no-full-no-dev {
|
||||
height: calc(100vh - 26.1em);
|
||||
}
|
||||
pre {
|
||||
background: transparent;
|
||||
border: 0px;
|
||||
display: inline;
|
||||
font-size: 0.9em;
|
||||
margin: 0px;
|
||||
overflow: auto;
|
||||
padding: 0px;
|
||||
white-space: pre;
|
||||
word-break: normal;
|
||||
overflow-wrap: normal;
|
||||
font-family: "consolas", monospace;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
code {
|
||||
font-family: "consolas", monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,529 @@
|
||||
<template>
|
||||
<div class="research-component" v-if="evolvingCodes.length != 0">
|
||||
<div
|
||||
class="content-box sm-7-size"
|
||||
:style="{ width: fullScreenFlag ? '100%' : 'calc(70% - 1.89em)' }"
|
||||
>
|
||||
<div v-show="!fullScreenFlag">
|
||||
<h2>
|
||||
Evolving process
|
||||
<img
|
||||
v-if="allData && allData.length == 0 && !updateEnd"
|
||||
src="@/assets/playground-images/loading-tab.gif"
|
||||
alt="loading"
|
||||
/>
|
||||
</h2>
|
||||
<div class="process">
|
||||
<span
|
||||
class="down-arrow"
|
||||
:class="{ rotate: showProcessFlag }"
|
||||
@click="showAllProcess"
|
||||
></span>
|
||||
<div class="process-content">
|
||||
<ul ref="panel">
|
||||
<li
|
||||
v-for="(item, index) in allData"
|
||||
:key="'p_' + index"
|
||||
:class="{ active: currentLoop == index }"
|
||||
@click="updateLoop(index, 0)"
|
||||
>
|
||||
<span style="margin-right: 1.5em"
|
||||
>Round
|
||||
{{ String(allData.length - index).padStart(2, "0") }}</span
|
||||
>
|
||||
<span
|
||||
v-for="(child, n) in item"
|
||||
:key="'c_' + n"
|
||||
class="span"
|
||||
:class="{
|
||||
active: currentLoop == index && scenarioCheckedIndex == n,
|
||||
}"
|
||||
@click.stop="updateLoop(index, n)"
|
||||
>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
popper-class="process-popper"
|
||||
:content="child.name"
|
||||
placement="bottom"
|
||||
>
|
||||
<span
|
||||
:class="{
|
||||
success: child.decision,
|
||||
fail: !child.decision,
|
||||
checked:
|
||||
currentLoop == index && scenarioCheckedIndex == n,
|
||||
}"
|
||||
></span>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="scenarioChecked" style="width: 100%">
|
||||
<h2 style="margin: 1em 0 0; font-size: 1.125em">Implementation</h2>
|
||||
<div>
|
||||
<el-tabs
|
||||
v-model="activeName"
|
||||
class="demo-tabs"
|
||||
@tab-click="handleClick"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="item in codeNavList"
|
||||
:key="item"
|
||||
:label="item"
|
||||
:name="item"
|
||||
>
|
||||
<codeComponent
|
||||
:markdown="scenarioChecked.workspace[activeName]"
|
||||
:developer="developer"
|
||||
:fullscreen="fullScreenFlag"
|
||||
@fullScreen="fullScreen"
|
||||
></codeComponent>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-box sm-3-size" v-show="!fullScreenFlag">
|
||||
<h2 style="font-size: 1.25em; margin-bottom: 0.8em">Tasks</h2>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
raw-content
|
||||
:content="
|
||||
`<div style='width: 500px;font-size: 14px;padding: 0.5em 0.5em 0.7em;line-height:160%; '>` +
|
||||
modelTaskDesc +
|
||||
'</div>'
|
||||
"
|
||||
placement="left"
|
||||
>
|
||||
<selectComponent
|
||||
:scenarioList="currentLoopData"
|
||||
:scenarioIndex="scenarioCheckedIndex"
|
||||
:showStatus="true"
|
||||
@scenarioCheckedItem="scenarioCheckedItem"
|
||||
></selectComponent>
|
||||
</el-tooltip>
|
||||
<h2 style="margin: 1.2em 0 0; font-size: 1.125em">Feedback</h2>
|
||||
<div
|
||||
class="code-nav"
|
||||
:style="{
|
||||
width: developer
|
||||
? 'calc(calc(100vw - 19.35em) * 0.3)'
|
||||
: 'calc(calc(100vw - 5.49em) * 0.3)',
|
||||
}"
|
||||
>
|
||||
<el-tabs
|
||||
v-model="feedbackName"
|
||||
class="demo-tabs"
|
||||
@tab-click="handleClick"
|
||||
>
|
||||
<el-tab-pane
|
||||
v-for="item in feedbackList"
|
||||
:key="item.abridgeName"
|
||||
:label="item.abridgeName"
|
||||
:name="item.abridgeName"
|
||||
>
|
||||
<div class="deduction" v-if="item.content">
|
||||
<div
|
||||
class="deduction-content"
|
||||
:style="{
|
||||
height: developer
|
||||
? 'calc(100vh - 28.58em)'
|
||||
: 'calc(100vh - 27.5em)',
|
||||
}"
|
||||
>
|
||||
<p>
|
||||
{{ item.content }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="research-component" v-else-if="updateEnd">
|
||||
<p>No code generated due to some errors happened in previous steps.</p>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, computed, defineProps, nextTick } from "vue";
|
||||
import selectComponent from "../components/sm-select-component.vue";
|
||||
import codeComponent from "../components/code.vue";
|
||||
import { marked } from "marked"; // 用于解析Markdown
|
||||
import hljs from "highlight.js"; // 用于代码高亮
|
||||
import "highlight.js/styles/1c-light.css"; // 引入你想要的代码高亮样式
|
||||
|
||||
const props = defineProps({
|
||||
evolvingCodes: Array,
|
||||
evolvingFeedbacks: Array,
|
||||
updateEnd: Boolean,
|
||||
developer: Boolean,
|
||||
currentData: Object,
|
||||
});
|
||||
|
||||
const fullScreenFlag = ref(false);
|
||||
const evolvingCodes = ref(props.evolvingCodes);
|
||||
const evolvingFeedbacks = ref(props.evolvingFeedbacks);
|
||||
const updateEnd = ref(props.updateEnd);
|
||||
const developer = ref(props.developer);
|
||||
const currentData = ref(props.currentData);
|
||||
const allData = ref(null);
|
||||
const currentLoop = ref(0);
|
||||
const currentLoopData = ref([]);
|
||||
const modelTaskDescObj = ref(null);
|
||||
const modelTaskDesc = ref("");
|
||||
const modelTask = ref(null);
|
||||
const scenarioChecked = ref(null);
|
||||
const scenarioCheckedIndex = ref(0);
|
||||
const feedbackList = ref(null);
|
||||
const codeNavList = ref(["ad_data.py"]);
|
||||
const activeName = ref("ad_data.py");
|
||||
|
||||
const feedbackName = ref("");
|
||||
|
||||
const handleClick = (tab, event) => {
|
||||
console.log(tab, event);
|
||||
};
|
||||
|
||||
const getLoopdata = (codes, feedbacks) => {
|
||||
const data = [];
|
||||
const tempObj = {};
|
||||
for (let i = 0; i < codes.length; i++) {
|
||||
for (let j = 0; j < codes[i].content.length; j++) {
|
||||
if (tempObj[codes[i].content[j].evo_id]) {
|
||||
tempObj[codes[i].content[j].evo_id].push({
|
||||
name: codes[i].content[j].target_task_name,
|
||||
workspace: codes[i].content[j].workspace,
|
||||
decision: feedbacks[i].content[j].final_decision,
|
||||
feedback: [
|
||||
{
|
||||
name: "Execution Feedback🖥️",
|
||||
abridgeName: "Execution",
|
||||
content: feedbacks[i].content[j].execution,
|
||||
},
|
||||
{
|
||||
name: "Code Feedback📄",
|
||||
abridgeName: "Code",
|
||||
content: feedbacks[i].content[j].code,
|
||||
},
|
||||
{
|
||||
name: "Return Checking",
|
||||
abridgeName: "Return Checking",
|
||||
content: feedbacks[i].content[j].return_checking,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
tempObj[codes[i].content[j].evo_id] = [
|
||||
{
|
||||
name: codes[i].content[j].target_task_name,
|
||||
workspace: codes[i].content[j].workspace,
|
||||
decision: feedbacks[i].content[j].final_decision,
|
||||
feedback: [
|
||||
{
|
||||
name: "Execution Feedback🖥️",
|
||||
abridgeName: "Execution",
|
||||
content: feedbacks[i].content[j].execution,
|
||||
},
|
||||
{
|
||||
name: "Code Feedback📄",
|
||||
abridgeName: "Code",
|
||||
content: feedbacks[i].content[j].code,
|
||||
},
|
||||
{
|
||||
name: "Return Checking",
|
||||
abridgeName: "Return Checking",
|
||||
content: feedbacks[i].content[j].return_checking,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
let sortedKeys = Object.keys(tempObj).sort((a, b) => b - a);
|
||||
|
||||
// 根据倒序的键名获取值
|
||||
let result = sortedKeys.map((key) => tempObj[key]);
|
||||
return result;
|
||||
};
|
||||
|
||||
const updatData = () => {
|
||||
if (evolvingCodes.value.length > 0 && evolvingFeedbacks.value.length > 0) {
|
||||
allData.value = getLoopdata(evolvingCodes.value, evolvingFeedbacks.value);
|
||||
modelTaskDescObj.value = currentData.value.researcTasks.reduce(
|
||||
(acc, currentValue, index) => {
|
||||
acc[currentValue.name] = currentValue.description;
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
currentLoop.value = 0;
|
||||
currentLoopData.value = allData.value[currentLoop.value];
|
||||
scenarioCheckedIndex.value = 0;
|
||||
scenarioChecked.value = currentLoopData.value[scenarioCheckedIndex.value];
|
||||
modelTaskDesc.value = modelTaskDescObj.value[scenarioChecked.value.name];
|
||||
codeNavList.value = Object.keys(scenarioChecked.value.workspace);
|
||||
activeName.value = codeNavList.value[0];
|
||||
feedbackList.value = scenarioChecked.value.feedback.filter((item) => {
|
||||
return item.content;
|
||||
});
|
||||
feedbackName.value = feedbackList.value[0].abridgeName;
|
||||
}
|
||||
};
|
||||
|
||||
const updateLoop = (index, n) => {
|
||||
currentLoop.value = index;
|
||||
currentLoopData.value = allData.value[currentLoop.value];
|
||||
scenarioCheckedIndex.value = n;
|
||||
scenarioChecked.value = currentLoopData.value[scenarioCheckedIndex.value];
|
||||
codeNavList.value = Object.keys(scenarioChecked.value.workspace);
|
||||
activeName.value = codeNavList.value[0];
|
||||
feedbackList.value = scenarioChecked.value.feedback.filter((item) => {
|
||||
return item.content;
|
||||
});
|
||||
feedbackName.value = feedbackList.value[0].abridgeName;
|
||||
};
|
||||
|
||||
const scenarioCheckedItem = (data) => {
|
||||
scenarioCheckedIndex.value = data.scenarioCheckedIndex;
|
||||
scenarioChecked.value = data.scenarioChecked;
|
||||
codeNavList.value = Object.keys(scenarioChecked.value.workspace);
|
||||
activeName.value = codeNavList.value[0];
|
||||
modelTaskDesc.value = modelTaskDescObj.value[scenarioChecked.value.name];
|
||||
feedbackList.value = scenarioChecked.value.feedback.filter((item) => {
|
||||
return item.content;
|
||||
});
|
||||
feedbackName.value = feedbackList.value[0].abridgeName;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.currentData, props.updateEnd, props.developer],
|
||||
(newValue, oldValue) => {
|
||||
currentData.value = newValue[0];
|
||||
evolvingCodes.value = currentData.value.evolvingCodes;
|
||||
evolvingFeedbacks.value = currentData.value.evolvingFeedbacks;
|
||||
updateEnd.value = newValue[1];
|
||||
developer.value = newValue[2];
|
||||
|
||||
updatData();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const panel = ref(null);
|
||||
const showProcessFlag = ref(false);
|
||||
const showAllProcess = () => {
|
||||
showProcessFlag.value = !showProcessFlag.value;
|
||||
if (!showProcessFlag.value) {
|
||||
panel.value.style.maxHeight = "2.925em";
|
||||
} else {
|
||||
panel.value.style.maxHeight = panel.value.scrollHeight + "px";
|
||||
}
|
||||
};
|
||||
|
||||
const fullScreen = (flag) => {
|
||||
fullScreenFlag.value = flag;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
updatData();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.research-component {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 1.89em;
|
||||
.content-box {
|
||||
// width: 50%;
|
||||
height: 100%;
|
||||
color: var(--text-color);
|
||||
|
||||
&.sm-7-size {
|
||||
width: calc(70% - 1.89em);
|
||||
}
|
||||
&.sm-3-size {
|
||||
width: 30%;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.26em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
margin-bottom: 0.45em;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: flex-start;
|
||||
padding-right: 0.18em;
|
||||
position: relative;
|
||||
img {
|
||||
width: 2.25em;
|
||||
height: 2.25em;
|
||||
margin-left: 0.45em;
|
||||
position: absolute;
|
||||
top: -0.18em;
|
||||
}
|
||||
}
|
||||
|
||||
.process {
|
||||
background: var(--bg-white);
|
||||
border-radius: 11px;
|
||||
position: relative;
|
||||
.down-arrow {
|
||||
width: 0.9em;
|
||||
height: 0.9em;
|
||||
background: url(@/assets/images/down-arrow.svg) no-repeat;
|
||||
background-size: contain;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease-out;
|
||||
position: absolute;
|
||||
right: 0.9em;
|
||||
top: 1.035em;
|
||||
|
||||
&.rotate {
|
||||
transform: rotate(-180deg);
|
||||
transition: transform 0.3s ease-out;
|
||||
}
|
||||
}
|
||||
.process-content {
|
||||
ul {
|
||||
overflow: hidden;
|
||||
max-height: 2.925em;
|
||||
transition: max-height 0.3s ease-out;
|
||||
li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0.72em 1.9125em;
|
||||
height: 2.925em;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 0.1em;
|
||||
gap: 1.08em;
|
||||
|
||||
&:last-child {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border-radius: 11px;
|
||||
background: var(--card-bg-hover-color);
|
||||
}
|
||||
span {
|
||||
display: inline-block;
|
||||
}
|
||||
.span {
|
||||
padding: 0.18em 0.225em;
|
||||
border-radius: 4px;
|
||||
&:hover,
|
||||
&.active {
|
||||
background: rgba(178, 159, 255, 0.4);
|
||||
}
|
||||
}
|
||||
.success {
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/process-success.svg)
|
||||
no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
|
||||
&.checked {
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/process-checked.svg)
|
||||
no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
.fail {
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/process-fail.svg)
|
||||
no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
|
||||
&.checked {
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/process-fail-checked.svg)
|
||||
no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.deduction {
|
||||
border-radius: 11px;
|
||||
background: var(--bg-white);
|
||||
box-sizing: border-box;
|
||||
overflow-y: hidden;
|
||||
.deduction-content {
|
||||
height: calc(100vh - 26.955em);
|
||||
padding: 1.35em 1.6875em 0.9em;
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.1475em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
margin-bottom: 0.45em;
|
||||
margin-top: 0.9em;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 0.9em;
|
||||
line-height: 180%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.el-tabs__item.is-active),
|
||||
:deep(.el-tabs__item:hover) {
|
||||
background: linear-gradient(90deg, #2667ff 0%, #9d41ff 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
// font-weight: 700;
|
||||
}
|
||||
:deep(.el-tabs__active-bar) {
|
||||
background: linear-gradient(to right, #2667ff, #9d41ff);
|
||||
}
|
||||
:deep(.el-tabs__item) {
|
||||
padding: 0 12px;
|
||||
color: #ababab;
|
||||
}
|
||||
:deep(.code-nav .el-tabs__nav) {
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.code-nav .el-tabs__item) {
|
||||
min-width: calc(100% / 5);
|
||||
max-width: calc(100% / 3);
|
||||
width: calc(100% / 3);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="dialog-box" v-if="uniShowDialog">
|
||||
<div class="dialog-content gradient-border">
|
||||
<h1>Increase Loop Count</h1>
|
||||
<p>
|
||||
You can increase the number of loops. Please enter the desired number
|
||||
below.
|
||||
</p>
|
||||
<el-radio-group v-model="radio1">
|
||||
<el-radio value="5">5 Loops</el-radio>
|
||||
<el-radio value="10">10 Loops</el-radio>
|
||||
<el-radio value="20">20 Loops</el-radio>
|
||||
<el-radio value="num"
|
||||
><el-input-number
|
||||
class="number-input"
|
||||
v-model="num"
|
||||
:controls="false"
|
||||
:min="1"
|
||||
:max="100"
|
||||
@change="handleChange"
|
||||
/>
|
||||
Loops</el-radio
|
||||
>
|
||||
</el-radio-group>
|
||||
<div class="btn-box">
|
||||
<button class="gradient-border back" @click="close">BACK</button>
|
||||
<button class="add-loops active">Add Loops</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, defineProps, defineEmits, nextTick } from "vue";
|
||||
const props = defineProps({
|
||||
showDialog: Number,
|
||||
});
|
||||
const uniShowDialog = ref(false);
|
||||
const radio1 = ref("");
|
||||
const num = ref();
|
||||
const emit = defineEmits(["addLoop"]);
|
||||
|
||||
watch(
|
||||
() => props.showDialog,
|
||||
(newValue, oldValue) => {
|
||||
uniShowDialog.value = newValue > 0 ? true : false;
|
||||
}
|
||||
);
|
||||
|
||||
const handleChange = (value) => {
|
||||
console.log(value);
|
||||
};
|
||||
const close = () => {
|
||||
uniShowDialog.value = false;
|
||||
};
|
||||
onMounted(() => {});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dialog-box {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: rgba(255, 255, 255, 0.29);
|
||||
backdrop-filter: blur(4.599999904632568px);
|
||||
z-index: 999999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.dialog-content {
|
||||
background-color: #fff;
|
||||
border-radius: 18px;
|
||||
--border-radius: 20px;
|
||||
--border-width: 2px;
|
||||
padding: 3em 4em;
|
||||
margin-top: -4em;
|
||||
h1 {
|
||||
color: var(--text-color);
|
||||
text-shadow: 8px 11px 30px #edf0ff;
|
||||
font-size: 1.5em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
}
|
||||
p {
|
||||
color: var(--text-color);
|
||||
font-size: 1.2em;
|
||||
line-height: 150%;
|
||||
margin: 1.25em 0;
|
||||
}
|
||||
.number-input {
|
||||
width: 80px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
border: 2px solid #c5d2e6;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
.btn-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 0.25em;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-top: 4em;
|
||||
button {
|
||||
width: 12em;
|
||||
height: 3.78em;
|
||||
color: var(--text-color);
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
line-height: 150%;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
--border-radius: 999px;
|
||||
--border-width: 2px;
|
||||
&.disable {
|
||||
border-radius: 37.5px;
|
||||
background: #c4c4c4;
|
||||
box-shadow: 8px 11px 30px 0px var(--wg-shadow-color);
|
||||
color: var(--bg-white);
|
||||
}
|
||||
&.active {
|
||||
border-radius: 37.5px;
|
||||
background: linear-gradient(90deg, #2667ff 0%, #9d41ff 100%), #979797;
|
||||
box-shadow: 8px 11px 30px 0px var(--wg-shadow-color);
|
||||
color: #fff;
|
||||
}
|
||||
&.back:hover {
|
||||
background-color: var(--card-bg-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.el-radio) {
|
||||
--el-radio-text-color: var(--text-color);
|
||||
}
|
||||
:deep(.el-radio__label) {
|
||||
font-size: 16px;
|
||||
}
|
||||
:deep(.el-radio__inner) {
|
||||
border-color: var(--text-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,330 @@
|
||||
<template>
|
||||
<div class="research-component">
|
||||
<div class="content-box hypothesis-box">
|
||||
<h2>For Hypothesis</h2>
|
||||
<div class="deduction">
|
||||
<div class="deduction-content" v-if="feedbackHypothesis">
|
||||
<div v-if="feedbackHypothesis.observations">
|
||||
<h3>Observations</h3>
|
||||
<p>
|
||||
{{ feedbackHypothesis.observations }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="feedbackHypothesis.hypothesis_evaluation">
|
||||
<h3>Hypothesis Evaluation</h3>
|
||||
<p>
|
||||
{{ feedbackHypothesis.hypothesis_evaluation }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="feedbackHypothesis.new_hypothesis">
|
||||
<h3>New Hypothesis</h3>
|
||||
<p>
|
||||
{{ feedbackHypothesis.new_hypothesis }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="feedbackHypothesis.exception">
|
||||
<h3>Exception</h3>
|
||||
<p>{{ feedbackHypothesis.exception }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h3>Decision</h3>
|
||||
<p>
|
||||
{{ feedbackHypothesis.decision }}
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="feedbackHypothesis.reason">
|
||||
<h3>Reason</h3>
|
||||
<p>
|
||||
{{ feedbackHypothesis.reason }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="deduction-content" v-else>
|
||||
<p>
|
||||
No feedback generated due to some errors happened in previous steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-box returns-box">
|
||||
<h2>For Returns</h2>
|
||||
<div class="deduction" style="margin-top: 0.5em">
|
||||
<div class="deduction-chart" v-if="feedbackCharts">
|
||||
<div class="chart-toolbar">
|
||||
<button class="chart-enlarge-btn" @click="openChartModal">
|
||||
Enlarge
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="!chartModalVisible" v-html="feedbackCharts.chart_html"></div>
|
||||
</div>
|
||||
<div class="deduction-chart" v-else>
|
||||
<p style="padding-left: 1.875em">
|
||||
No feedback generated due to some errors happened in previous steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-section">
|
||||
<h2 style="margin-top: 1em">Configuration</h2>
|
||||
<div v-if="feedbackConfig && feedbackConfig.config">
|
||||
<markdownToHtml :markdown="feedbackConfig.config"></markdownToHtml>
|
||||
</div>
|
||||
<div v-else>
|
||||
<p style="padding-left: 1.875em">
|
||||
No feedback generated due to some errors happened in previous steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-modal" v-if="chartModalVisible">
|
||||
<div class="chart-modal-content gradient-border">
|
||||
<div class="chart-modal-header">
|
||||
<h3>Returns Chart</h3>
|
||||
<button class="chart-modal-close" @click="closeChartModal">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div class="chart-modal-body" v-if="feedbackCharts">
|
||||
<div v-html="feedbackCharts.chart_html"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, defineProps, nextTick } from "vue";
|
||||
import markdownToHtml from "../components/markdownToHtml.vue";
|
||||
const props = defineProps({
|
||||
currentData: Object,
|
||||
updateEnd: Boolean,
|
||||
});
|
||||
const currentData = ref(props.currentData);
|
||||
const updateEnd = ref(props.updateEnd);
|
||||
const feedbackCharts = ref(currentData.value.feedbackCharts);
|
||||
const feedbackConfig = ref(currentData.value.feedbackConfig);
|
||||
const feedbackHypothesis = ref(currentData.value.feedbackHypothesis);
|
||||
const chartModalVisible = ref(false);
|
||||
|
||||
const openChartModal = () => {
|
||||
chartModalVisible.value = true;
|
||||
};
|
||||
|
||||
const closeChartModal = () => {
|
||||
chartModalVisible.value = false;
|
||||
};
|
||||
|
||||
const executeScripts = () => {
|
||||
// 获取 HTML 中所有的 <script> 标签
|
||||
const scripts = document.querySelectorAll("script");
|
||||
scripts.forEach((script) => {
|
||||
const newScript = document.createElement("script");
|
||||
if (script.src) {
|
||||
newScript.src = script.src; // 如果有 src 属性,加载外部脚本
|
||||
} else {
|
||||
newScript.innerHTML = script.innerHTML; // 否则执行内联脚本
|
||||
}
|
||||
document.body.appendChild(newScript); // 将新脚本标签插入到 body 中
|
||||
document.body.removeChild(newScript); // 执行完毕后移除它
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.currentData, props.updateEnd],
|
||||
(newValue, oldValue) => {
|
||||
currentData.value = newValue[0];
|
||||
updateEnd.value = newValue[1];
|
||||
feedbackCharts.value = currentData.value.feedbackCharts;
|
||||
feedbackConfig.value = currentData.value.feedbackConfig;
|
||||
feedbackHypothesis.value = currentData.value.feedbackHypothesis;
|
||||
if (feedbackCharts.value) {
|
||||
nextTick(() => {
|
||||
executeScripts();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => chartModalVisible.value,
|
||||
() => {
|
||||
if (feedbackCharts.value) {
|
||||
nextTick(() => {
|
||||
executeScripts();
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
// 执行嵌入的 JavaScript
|
||||
if (feedbackCharts.value) {
|
||||
nextTick(() => {
|
||||
executeScripts();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.research-component {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 1.89em;
|
||||
.content-box {
|
||||
width: 54%;
|
||||
height: 100%;
|
||||
color: var(--text-color);
|
||||
overflow: auto;
|
||||
h2 {
|
||||
font-size: 1.26em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
margin-bottom: 0.45em;
|
||||
}
|
||||
.deduction {
|
||||
border-radius: 11px;
|
||||
background: var(--bg-white);
|
||||
padding: 0.9em 0;
|
||||
box-sizing: border-box;
|
||||
overflow-y: hidden;
|
||||
.deduction-content {
|
||||
height: calc(100vh - 21.2em);
|
||||
padding: 0.9em 1.6875em 0;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.1475em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
margin-bottom: 0.45em;
|
||||
margin-top: 0.9em;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 0.9em;
|
||||
line-height: 180%;
|
||||
margin-bottom: 0.9em;
|
||||
}
|
||||
}
|
||||
.deduction-chart {
|
||||
max-height: none;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
.chart-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 0 1.2em 0.6em;
|
||||
}
|
||||
.chart-enlarge-btn {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #2667ff 0%, #9d41ff 100%);
|
||||
box-shadow: 0 6px 16px rgba(38, 103, 255, 0.2);
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.hypothesis-box {
|
||||
width: 46%;
|
||||
}
|
||||
|
||||
.returns-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.returns-box .deduction {
|
||||
flex: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.returns-box .deduction-chart {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.returns-box .config-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.returns-box .config-section :deep(.markdown-body) {
|
||||
flex: 1;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.chart-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(6px);
|
||||
z-index: 999999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chart-modal-content {
|
||||
width: min(92vw, 1200px);
|
||||
height: min(85vh, 900px);
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
padding: 1.6em 2em 2em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1em;
|
||||
h3 {
|
||||
font-size: 1.2em;
|
||||
font-weight: 700;
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
|
||||
.chart-modal-close {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.95em;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
padding: 0.5em 1.1em;
|
||||
border-radius: 999px;
|
||||
background: #b0b7c3;
|
||||
}
|
||||
|
||||
.chart-modal-body {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 0 0.4em 0.4em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<footer>
|
||||
<div class="footer" :style="{ background: color }">
|
||||
<p>
|
||||
<a target="_blank" aria-label="Contact us" href="#">Contact us</a>
|
||||
<span class="line">|</span>
|
||||
<a
|
||||
target="_blank"
|
||||
aria-label="Privacy & Cookies"
|
||||
href="https://go.microsoft.com/fwlink/?LinkId=521839"
|
||||
>Privacy & Cookies</a
|
||||
>
|
||||
<span class="line">|</span>
|
||||
<a
|
||||
target="_blank"
|
||||
aria-label="Terms of Use"
|
||||
href="https://go.microsoft.com/fwlink/?LinkID=206977"
|
||||
>Terms of Use</a
|
||||
>
|
||||
<span class="line">|</span>
|
||||
<a
|
||||
target="_blank"
|
||||
aria-label="Trademarks"
|
||||
href="https://www.microsoft.com/trademarks"
|
||||
>Trademarks</a
|
||||
>
|
||||
<span class="line">|</span>
|
||||
<span style="">© Microsoft 2024</span>
|
||||
<span class="line">|</span>
|
||||
<span
|
||||
>This content is AI-generated and may not be fully accurate or
|
||||
up-to-date; please verify with a professional for critical
|
||||
matters.</span
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {},
|
||||
props: {
|
||||
color: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.footer {
|
||||
padding: 1.75em 4.375em;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
color: var(--footer-text-color);
|
||||
// background: var(--bg-grey);
|
||||
// background: var(--bg-white);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
position: relative;
|
||||
a {
|
||||
color: var(--footer-text-color);
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
line-height: 21px;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
.line {
|
||||
margin: 0 16px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
p {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
|
||||
&:nth-child(2) a {
|
||||
display: inline-block;
|
||||
img {
|
||||
height: 1.375em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="math-box" ref="katexContainer"></div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, defineProps } from "vue";
|
||||
import "katex/dist/katex.min.css";
|
||||
import katex from "katex";
|
||||
|
||||
const props = defineProps({
|
||||
formula: String,
|
||||
});
|
||||
const katexContainer = ref(null);
|
||||
watch(
|
||||
() => props.formula,
|
||||
(newValue, oldValue) => {
|
||||
katex.render(newValue, katexContainer.value, {
|
||||
throwOnError: false, // 避免在公式错误时抛出异常
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
if (katexContainer.value) {
|
||||
katex.render(props.formula, katexContainer.value, {
|
||||
throwOnError: false, // 避免在公式错误时抛出异常
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.math-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<div ref="chart" style="width: 100%; height: 600px"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const chart = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
let base = +new Date(1968, 9, 3);
|
||||
let oneDay = 24 * 3600 * 1000;
|
||||
let dateList = [];
|
||||
for (let i = 1; i < 20000; i++) {
|
||||
var now = new Date((base += oneDay));
|
||||
dateList.push(
|
||||
[now.getFullYear(), now.getMonth() + 1, now.getDate()].join("/")
|
||||
);
|
||||
}
|
||||
function getData() {
|
||||
let data = [Math.random() * 300];
|
||||
for (let i = 1; i < 20000; i++) {
|
||||
data.push(Math.round((Math.random() - 0.5) * 20 + data[i - 1]));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
toolbox: {
|
||||
feature: {
|
||||
dataZoom: {
|
||||
yAxisIndex: "none",
|
||||
},
|
||||
restore: {},
|
||||
saveAsImage: {},
|
||||
},
|
||||
},
|
||||
xAxis: [
|
||||
{
|
||||
data: dateList,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
data: dateList,
|
||||
gridIndex: 1,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
data: dateList,
|
||||
gridIndex: 2,
|
||||
show: false,
|
||||
},
|
||||
{
|
||||
data: dateList,
|
||||
gridIndex: 3,
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{},
|
||||
{
|
||||
gridIndex: 1,
|
||||
},
|
||||
{
|
||||
gridIndex: 2,
|
||||
},
|
||||
{
|
||||
gridIndex: 3,
|
||||
},
|
||||
],
|
||||
grid: [
|
||||
{
|
||||
bottom: "75%",
|
||||
top: "5%",
|
||||
},
|
||||
{
|
||||
top: "25%",
|
||||
bottom: "50%",
|
||||
},
|
||||
{
|
||||
bottom: "25%",
|
||||
top: "50%",
|
||||
},
|
||||
{
|
||||
top: "75%",
|
||||
},
|
||||
],
|
||||
series: [
|
||||
{
|
||||
type: "line",
|
||||
showSymbol: false,
|
||||
data: getData(),
|
||||
},
|
||||
{
|
||||
type: "line",
|
||||
showSymbol: false,
|
||||
data: getData(),
|
||||
xAxisIndex: 1,
|
||||
yAxisIndex: 1,
|
||||
},
|
||||
|
||||
{
|
||||
type: "line",
|
||||
showSymbol: false,
|
||||
data: getData(),
|
||||
xAxisIndex: 2,
|
||||
yAxisIndex: 2,
|
||||
},
|
||||
|
||||
{
|
||||
type: "line",
|
||||
showSymbol: false,
|
||||
data: getData(),
|
||||
xAxisIndex: 3,
|
||||
yAxisIndex: 3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const chartInstance = echarts.init(chart.value);
|
||||
chartInstance.setOption(option);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 样式内容 */
|
||||
</style>
|
||||
@@ -0,0 +1,338 @@
|
||||
<template>
|
||||
<div ref="chart" style="width: 100%; height: 200px"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onBeforeUnmount, defineProps, watch, ref, nextTick } from "vue";
|
||||
import * as echarts from "echarts";
|
||||
|
||||
const props = defineProps({
|
||||
color: String,
|
||||
data: Object,
|
||||
chartName: String,
|
||||
smallSize: Boolean,
|
||||
});
|
||||
const color = ref(props.color);
|
||||
const data = ref(props.data);
|
||||
const chartName = ref(props.chartName);
|
||||
const smallSize = ref(props.smallSize);
|
||||
const chart = ref(null);
|
||||
|
||||
let chartInstance = null;
|
||||
let resizeObserver = null;
|
||||
let textMeasureCanvas = null;
|
||||
|
||||
const getTextMeasureContext = () => {
|
||||
if (typeof document === "undefined") {
|
||||
return null;
|
||||
}
|
||||
if (!textMeasureCanvas) {
|
||||
textMeasureCanvas = document.createElement("canvas");
|
||||
}
|
||||
return textMeasureCanvas.getContext("2d");
|
||||
};
|
||||
|
||||
const getAxisLabelLayout = (xLabels = []) => {
|
||||
const labels = Array.isArray(xLabels) ? xLabels : [];
|
||||
const width = chart.value?.clientWidth || 0;
|
||||
const count = labels.length;
|
||||
if (!width || count <= 1) {
|
||||
return {
|
||||
rotate: 0,
|
||||
bottom: smallSize.value ? "20%" : "18%",
|
||||
};
|
||||
}
|
||||
|
||||
const fontSize = smallSize.value ? 11 : 12;
|
||||
const fontFamily =
|
||||
chart.value && typeof window !== "undefined"
|
||||
? window.getComputedStyle(chart.value).fontFamily || "sans-serif"
|
||||
: "sans-serif";
|
||||
const measureContext = getTextMeasureContext();
|
||||
if (measureContext) {
|
||||
measureContext.font = `${fontSize}px ${fontFamily}`;
|
||||
}
|
||||
|
||||
const maxLabelWidth = labels.reduce((max, label) => {
|
||||
const text = String(label == null ? "" : label);
|
||||
const measured = measureContext
|
||||
? measureContext.measureText(text).width
|
||||
: text.length * fontSize * 0.62;
|
||||
return Math.max(max, measured);
|
||||
}, 0);
|
||||
|
||||
// Keep this aligned with grid left/right (10% + 5%).
|
||||
const plotWidth = width * 0.85;
|
||||
const slotWidth = plotWidth / count;
|
||||
const minGap = 8;
|
||||
const useVertical = maxLabelWidth + minGap > slotWidth;
|
||||
|
||||
return {
|
||||
rotate: useVertical ? -90 : 0,
|
||||
bottom: useVertical ? (smallSize.value ? "32%" : "30%") : smallSize.value ? "20%" : "18%",
|
||||
};
|
||||
};
|
||||
|
||||
const updateContainerSize = () => {
|
||||
if (!chart.value) return;
|
||||
const width = chart.value.offsetWidth;
|
||||
// When mounted under v-show/display:none, width can be 0.
|
||||
if (!width) return;
|
||||
chart.value.style.height = width / 2 + "px";
|
||||
};
|
||||
|
||||
const canInitChart = () => {
|
||||
if (!chart.value) return false;
|
||||
// Prefer client sizes (what ECharts checks internally)
|
||||
const w = chart.value.clientWidth;
|
||||
const h = chart.value.clientHeight;
|
||||
return w > 0 && h > 0;
|
||||
};
|
||||
|
||||
const ensureChartInitialized = () => {
|
||||
if (!chart.value) return;
|
||||
if (chartInstance) return;
|
||||
|
||||
updateContainerSize();
|
||||
if (!canInitChart()) {
|
||||
// Still hidden / size not ready; defer.
|
||||
return;
|
||||
}
|
||||
|
||||
// Avoid double-init if something else already initialized it.
|
||||
chartInstance = echarts.getInstanceByDom(chart.value) || echarts.init(chart.value);
|
||||
};
|
||||
|
||||
const updatData = () => {
|
||||
const tooltip = {};
|
||||
|
||||
const ydatas = {};
|
||||
let minValue = Infinity;
|
||||
let maxValue = -Infinity;
|
||||
const series = [];
|
||||
const legend = [];
|
||||
let flag = false;
|
||||
(data.value || []).forEach((item) => {
|
||||
if (!item || item.name == null) return;
|
||||
tooltip[item.name] = item;
|
||||
|
||||
const itemValue = item.value;
|
||||
// Single-series numeric chart: value can be number OR null/undefined.
|
||||
if (typeof itemValue === "number" || itemValue == null) {
|
||||
flag = true;
|
||||
const yKey = chartName.value;
|
||||
ydatas[yKey] = ydatas[yKey] || [];
|
||||
ydatas[yKey].push([item.name, itemValue ?? null]);
|
||||
if (Number.isFinite(itemValue)) {
|
||||
maxValue = Math.max(maxValue, itemValue);
|
||||
minValue = Math.min(minValue, itemValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Multi-series chart: value should be an object; each key can still be null.
|
||||
if (typeof itemValue === "object") {
|
||||
Object.keys(itemValue).forEach((yKey) => {
|
||||
const yVal = itemValue[yKey];
|
||||
if (!ydatas[yKey]) {
|
||||
ydatas[yKey] = [[item.name, yVal ?? null]];
|
||||
} else {
|
||||
ydatas[yKey].push([item.name, yVal ?? null]);
|
||||
}
|
||||
if (Number.isFinite(yVal)) {
|
||||
maxValue = Math.max(maxValue, yVal);
|
||||
minValue = Math.min(minValue, yVal);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
const keys = Object.keys(ydatas);
|
||||
let index = keys.indexOf("ensemble");
|
||||
if (index !== -1) {
|
||||
// 移除找到的元素
|
||||
keys.splice(index, 1);
|
||||
// 将元素添加到数组的第二个位置
|
||||
keys.unshift("ensemble");
|
||||
}
|
||||
keys.forEach((item) => {
|
||||
if (!flag) {
|
||||
legend.push(item);
|
||||
}
|
||||
series.push({
|
||||
name: item,
|
||||
data: ydatas[item],
|
||||
type: "line",
|
||||
symbol: "circle",
|
||||
symbolSize: smallSize.value ? 6 : 10,
|
||||
});
|
||||
});
|
||||
|
||||
const xLabels = (data.value || [])
|
||||
.map((item) => item?.name)
|
||||
.filter((name) => name != null);
|
||||
const axisLayout = getAxisLabelLayout(xLabels);
|
||||
|
||||
const option = {
|
||||
color: [
|
||||
"#5470c6",
|
||||
"#73c0de",
|
||||
"#ee6666",
|
||||
"#91cc75",
|
||||
"#fac858",
|
||||
"#3ba272",
|
||||
"#fc8452",
|
||||
"#9a60b4",
|
||||
"#ea7ccc",
|
||||
],
|
||||
title: {
|
||||
text: chartName.value,
|
||||
textStyle: {
|
||||
fontSize: smallSize.value ? 12 : 18,
|
||||
},
|
||||
padding: [10, 20],
|
||||
top: smallSize.value ? "2%" : "5%",
|
||||
left: smallSize.value ? "1%" : "6%",
|
||||
},
|
||||
grid: {
|
||||
top: "20%",
|
||||
bottom: axisLayout.bottom,
|
||||
left: "10%",
|
||||
right: "5%",
|
||||
},
|
||||
legend: {
|
||||
show: !smallSize.value,
|
||||
data: legend,
|
||||
right: "8%",
|
||||
top: "5%",
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis", // 可选项:'item','axis','none'
|
||||
appendToBody: true,
|
||||
position: function (point, params, dom, rect, size) {
|
||||
// point:鼠标位置,[x, y]
|
||||
// params:tooltip 的数据
|
||||
// dom:tooltip 的 DOM 元素
|
||||
// rect:坐标轴的位置等信息
|
||||
// size:图表的尺寸信息
|
||||
|
||||
return [point[0], point[1]];
|
||||
},
|
||||
// confine: true,
|
||||
formatter: function (params) {
|
||||
if (!params || params.length === 0) return "";
|
||||
|
||||
const axisValue = params[0]?.axisValue;
|
||||
let tooltipContent = `<div><div><strong>${axisValue} Value: <br>`;
|
||||
params.forEach((item) => {
|
||||
const v = Array.isArray(item.value) ? item.value[1] : item.value;
|
||||
tooltipContent += `<span style="color: blue">${
|
||||
item.seriesName + ": " + (v ?? "null")
|
||||
}</span><br>
|
||||
`;
|
||||
});
|
||||
|
||||
const desc = tooltip?.[axisValue]?.desc ?? "";
|
||||
tooltipContent += ` </strong></div>`;
|
||||
if (desc) {
|
||||
tooltipContent += `<p style="margin-top: 10px;">${desc}</p>`;
|
||||
}
|
||||
tooltipContent += `</div>`;
|
||||
return tooltipContent;
|
||||
},
|
||||
extraCssText:
|
||||
"max-width: 400px; white-space: normal; word-wrap: break-word;",
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
axisLabel: {
|
||||
show: true,
|
||||
rotate: axisLayout.rotate,
|
||||
interval: 0,
|
||||
margin: 10,
|
||||
},
|
||||
},
|
||||
yAxis: (() => {
|
||||
const yAxis = {
|
||||
type: "value",
|
||||
axisLabel: {
|
||||
formatter: function (value, index) {
|
||||
if (value % 1 === 0) {
|
||||
return value.toFixed(0); // 没有小数部分时,显示整数
|
||||
} else if ((value * 10) % 1 === 0) {
|
||||
return value.toFixed(1); // 如果小数部分只有一位,则保留一位小数
|
||||
} else {
|
||||
return value.toFixed(2); // 其他情况保留两位小数
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Only set explicit bounds when there is at least one finite value.
|
||||
if (Number.isFinite(minValue) && Number.isFinite(maxValue)) {
|
||||
yAxis.min = Math.floor(minValue * 1000) / 1000;
|
||||
yAxis.max = Math.ceil(maxValue * 1000) / 1000;
|
||||
}
|
||||
return yAxis;
|
||||
})(),
|
||||
series: series,
|
||||
};
|
||||
|
||||
if (chartInstance) {
|
||||
chartInstance.setOption(option);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
(newValue, oldValue) => {
|
||||
data.value = newValue;
|
||||
ensureChartInitialized();
|
||||
if (chartInstance) {
|
||||
updatData();
|
||||
chartInstance.resize();
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
ensureChartInitialized();
|
||||
if (chartInstance) {
|
||||
updatData();
|
||||
chartInstance.resize();
|
||||
}
|
||||
|
||||
// Ensure charts render when the container becomes visible / resizes.
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
updateContainerSize();
|
||||
ensureChartInitialized();
|
||||
if (chartInstance) {
|
||||
updatData();
|
||||
chartInstance.resize();
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(chart.value);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (resizeObserver && chart.value) {
|
||||
resizeObserver.unobserve(chart.value);
|
||||
}
|
||||
resizeObserver = null;
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
chartInstance = null;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 样式内容 */
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<span class="spinner"></span>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
height: 35px;
|
||||
width: 35px;
|
||||
margin-top: 3px;
|
||||
// background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
border-top: 2px solid #fff;
|
||||
border-right: 2px solid transparent;
|
||||
animation: spinner6 700ms linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner6 {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,35 @@
|
||||
<template>
|
||||
<div class="loading">
|
||||
<svg class="loading-svg" width="205" height="120">
|
||||
<path
|
||||
class="loading-path"
|
||||
d="M 100.16623,51.415329 C 106.74946,45.082085 113.08279,39.707091 119.16623,35.290329 C 125.24945,30.790433 130.37444,27.790436 134.54123,26.290329 C 138.70777,24.790439 143.24943,24.04044 148.16623,24.040329 C 158.49941,24.04044 166.83274,27.498769 173.16623,34.415329 C 179.58273,41.248756 182.79106,49.540414 182.79123,59.290329 C 182.79106,65.957064 181.37439,72.123725 178.54123,77.790329 C 175.70773,83.457047 171.66607,87.748709 166.41623,90.665329 C 161.24941,93.582037 155.29108,95.040369 148.54123,95.040329 C 139.7911,95.040369 132.12444,93.16537 125.54123,89.415329 C 119.04112,85.665378 110.58279,78.582052 100.16623,68.165329 C 89.332815,78.915385 80.707824,86.082044 74.291229,89.665329 C 67.874504,93.248704 60.416178,95.040369 51.916229,95.040329 C 41.082864,95.040369 32.624539,91.665372 26.541229,84.915329 C 20.541218,78.165385 17.541221,69.623727 17.541229,59.290329 C 17.541221,49.623747 20.707884,41.332089 27.041229,34.415329 C 33.457871,27.498769 41.832863,24.04044 52.166229,24.040329 C 57.166181,24.04044 61.74951,24.790439 65.916229,26.290329 C 70.082835,27.790436 75.166163,30.790433 81.166229,35.290329 C 87.249484,39.707091 93.582811,45.082085 100.16623,51.415329 M 108.29123,59.165329 C 117.12445,67.915396 124.37445,73.873723 130.04123,77.040329 C 135.7911,80.123717 141.49943,81.665382 147.16623,81.665329 C 154.24942,81.665382 159.79108,79.582051 163.79123,75.415329 C 167.79107,71.165392 169.79107,66.040398 169.79123,60.040329 C 169.79107,53.457077 167.79107,48.040416 163.79123,43.790329 C 159.87441,39.457091 154.66608,37.290426 148.16623,37.290329 C 144.49943,37.290426 140.95776,37.957092 137.54123,39.290329 C 134.12444,40.540423 130.04111,42.790421 125.29123,46.040329 C 120.54112,49.207081 114.87446,53.582077 108.29123,59.165329 M 92.041229,59.165329 C 86.041152,54.082076 80.666157,49.915414 75.916229,46.665329 C 71.166167,43.332087 66.999504,40.957089 63.416229,39.540329 C 59.832845,38.123759 55.916182,37.415426 51.666229,37.415329 C 45.582859,37.415426 40.541198,39.540424 36.541229,43.790329 C 32.541206,48.040416 30.541208,53.457077 30.541229,60.040329 C 30.541208,64.623732 31.582873,68.498728 33.666229,71.665329 C 35.749536,74.832055 38.2912,77.290386 41.291229,79.040329 C 44.374527,80.790383 48.207857,81.665382 52.791229,81.665329 C 58.791179,81.665382 64.624507,80.08205 70.291229,76.915329 C 75.957829,73.748723 83.207822,67.832062 92.041229,59.165329"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.loading {
|
||||
}
|
||||
.loading-svg {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.loading-path {
|
||||
stroke: #fff;
|
||||
stroke-width: 6;
|
||||
stroke-linejoin: round;
|
||||
stroke-linecap: round;
|
||||
stroke-dasharray: 193.904983521;
|
||||
fill: none;
|
||||
animation: load 4s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes load {
|
||||
from {
|
||||
stroke-dashoffset: 775.6199340820312;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,499 @@
|
||||
<template>
|
||||
<div class="loop-box">
|
||||
<div class="loop-box-header">
|
||||
<div
|
||||
class="trace-name-text"
|
||||
v-if="traceName"
|
||||
:title="`Trace name: ${traceName}`"
|
||||
>
|
||||
<span class="trace-name-value">{{ traceName }}</span>
|
||||
</div>
|
||||
<span class="loop-title">Loops</span>
|
||||
</div>
|
||||
<div class="loop-box-list" ref="loops">
|
||||
<div class="loop-length">
|
||||
<div class="loop-item" v-for="index in loopNumber" :key="index">
|
||||
<div
|
||||
class="loop-item-content"
|
||||
@click="clickLoop(index, true)"
|
||||
v-if="!isCompleted(index) && loadingIndex == index"
|
||||
>
|
||||
<div class="loop-item-icon">
|
||||
<img
|
||||
src="@/assets/playground-images/loop-loading.gif"
|
||||
alt="loading"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="loop-item-label"
|
||||
:class="{ active: currentIndex == index }"
|
||||
>
|
||||
<span>{{ index < 10 ? "0" + index : index }}</span> Loop
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="loop-item-content"
|
||||
@click="clickLoop(index, false)"
|
||||
v-if="isCompleted(index)"
|
||||
>
|
||||
<div class="loop-item-icon">
|
||||
<img
|
||||
v-if="statusList[index - 1]"
|
||||
src="@/assets/playground-images/loop-Sucess.svg"
|
||||
alt="loading"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="@/assets/playground-images/loop-error.svg"
|
||||
alt="loading"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="loop-item-label"
|
||||
:class="{ active: currentIndex == index }"
|
||||
>
|
||||
<span>{{ index < 10 ? "0" + index : index }}</span> Loop
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="loop-item-content"
|
||||
v-if="!isCompleted(index) && loadingIndex !== index"
|
||||
>
|
||||
<div class="loop-item-icon">
|
||||
<img
|
||||
src="@/assets/playground-images/loop-default.svg"
|
||||
alt="loading"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="loop-item-label"
|
||||
:class="{ active: currentIndex == index }"
|
||||
>
|
||||
<span>{{ index < 10 ? "0" + index : index }}</span> Loop
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="default-line"></div>
|
||||
<div class="line" :style="{ height: height + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loop-box-btn">
|
||||
<button
|
||||
:class="{
|
||||
active: !isDone,
|
||||
disable: isDone,
|
||||
}"
|
||||
:disabled="stopFlag || isDone"
|
||||
@click="stopClick"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
<div class="auto-skip-toggle" v-if="editLoop">
|
||||
<label class="toggle-label">
|
||||
<span class="toggle-text">Auto Skip Interaction</span>
|
||||
<span class="toggle-switch">
|
||||
<input type="checkbox" v-model="autoSkip" @change="emitAutoSkip" />
|
||||
<span class="toggle-slider"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, defineProps, defineEmits, nextTick } from "vue";
|
||||
const props = defineProps({
|
||||
loadingIndex: Number,
|
||||
loopNumber: Number,
|
||||
editLoop: Boolean,
|
||||
currentData: Array,
|
||||
updateEnd: Boolean,
|
||||
traceName: String,
|
||||
});
|
||||
const loadingIndex = ref(props.loadingIndex);
|
||||
const loopNumber = ref(props.loopNumber);
|
||||
const editLoop = ref(props.editLoop);
|
||||
const currentData = ref(props.currentData);
|
||||
const traceName = ref(props.traceName);
|
||||
const statusList = ref([]);
|
||||
const emit = defineEmits(["addLoop", "clickIndex", "clickStop", "toggleAutoSkip"]);
|
||||
const loops = ref(null);
|
||||
const stopFlag = ref(false);
|
||||
const isDone = ref(props.updateEnd);
|
||||
const autoSkip = ref(false);
|
||||
|
||||
const currentIndex = ref(loadingIndex.value);
|
||||
|
||||
const isCompleted = (index) => {
|
||||
// return loadingIndex.value > index;
|
||||
return currentData.value.length >= index;
|
||||
};
|
||||
|
||||
const scrollTo = () => {
|
||||
const el = loops.value;
|
||||
if (el) {
|
||||
if (loopNumber.value - loadingIndex.value < 3) {
|
||||
el.scrollTo({
|
||||
top: 4.66 * 16 * (loadingIndex.value + 6),
|
||||
behavior: "smooth",
|
||||
});
|
||||
} else {
|
||||
el.scrollTo({
|
||||
top: 4.66 * 16 * (loadingIndex.value - 6),
|
||||
behavior: "smooth",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
const height = ref(0);
|
||||
const getHeight = () => {
|
||||
if (loadingIndex.value >= loopNumber.value) {
|
||||
height.value = 100;
|
||||
} else {
|
||||
height.value =
|
||||
((2.975 + 4.075 * (loadingIndex.value - 1)) /
|
||||
(4.075 * loopNumber.value)) *
|
||||
100;
|
||||
}
|
||||
};
|
||||
const updateData = () => {
|
||||
statusList.value = currentData.value.map((item) => {
|
||||
return item.feedbackHypothesis ? item.feedbackHypothesis.decision : false;
|
||||
});
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [
|
||||
props.loadingIndex,
|
||||
props.loopNumber,
|
||||
props.currentData,
|
||||
props.updateEnd,
|
||||
props.traceName,
|
||||
],
|
||||
(newValue, oldValue) => {
|
||||
loadingIndex.value = newValue[0];
|
||||
loopNumber.value = newValue[1];
|
||||
currentData.value = newValue[2];
|
||||
isDone.value = newValue[3];
|
||||
traceName.value = newValue[4];
|
||||
if (!isDone.value) {
|
||||
stopFlag.value = false;
|
||||
}
|
||||
updateData();
|
||||
currentIndex.value = loadingIndex.value;
|
||||
nextTick(() => {
|
||||
getHeight();
|
||||
scrollTo();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
const addLoop = () => {
|
||||
emit("addLoop", true);
|
||||
};
|
||||
const stopClick = () => {
|
||||
if (isDone.value && !stopFlag.value) {
|
||||
return;
|
||||
}
|
||||
stopFlag.value = true;
|
||||
emit("clickStop", stopFlag.value);
|
||||
};
|
||||
|
||||
const emitAutoSkip = () => {
|
||||
emit("toggleAutoSkip", autoSkip.value);
|
||||
};
|
||||
|
||||
const clickLoop = (index, flag) => {
|
||||
currentIndex.value = index;
|
||||
emit("clickIndex", {
|
||||
index: index,
|
||||
loading: flag,
|
||||
});
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getHeight();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.loop-box {
|
||||
width: 17.5em;
|
||||
width: 15.75em;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
padding-top: 0.5em;
|
||||
.loop-box-header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.1em;
|
||||
padding: 0 0.9em;
|
||||
box-sizing: border-box;
|
||||
|
||||
.loop-title {
|
||||
color: var(--text-color);
|
||||
font-size: 1.4em;
|
||||
font-size: 1.26em;
|
||||
font-weight: 700;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.01em;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.trace-name-text {
|
||||
width: fit-content;
|
||||
max-width: calc(100% - 1.8em);
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 0;
|
||||
|
||||
.trace-name-value {
|
||||
font-size: 1.06875em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
text-shadow: 8px 11px 30px var(--wg-shadow-color);
|
||||
background: linear-gradient(90deg, #2667ff 0%, #9d41ff 100%);
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.loop-box-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: auto;
|
||||
padding: 0 3em 0 4.3em;
|
||||
padding: 0 2.7em 0 3.87em;
|
||||
margin-top: 0.9em;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
&.no-btn {
|
||||
height: calc(100vh - 17em);
|
||||
height: calc(100vh - 15.3em);
|
||||
}
|
||||
.loop-length {
|
||||
position: relative;
|
||||
.line {
|
||||
position: absolute;
|
||||
height: 0;
|
||||
width: 3px;
|
||||
background: linear-gradient(to bottom, #2667ff 0%, #9d41ff 100%);
|
||||
background: linear-gradient(to bottom, #ffffff 0%, #ffffff 100%);
|
||||
opacity: 0.3;
|
||||
opacity: 1;
|
||||
top: 0;
|
||||
left: 0.8125em;
|
||||
left: 0.73125em;
|
||||
transition: 0.75s ease;
|
||||
}
|
||||
.default-line {
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
background-color: #c5d2e6;
|
||||
bottom: 0;
|
||||
left: 0.8125em;
|
||||
left: 0.73125em;
|
||||
}
|
||||
}
|
||||
.loop-item {
|
||||
padding: 1.1em 0;
|
||||
padding: 0.99em 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
.loop-item-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
.loop-item-icon {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// .line {
|
||||
// height: 2.625em;
|
||||
// width: 4px;
|
||||
// background-color: #c5d2e6;
|
||||
// &.active {
|
||||
// background: linear-gradient(to bottom, #2667ff 0%, #9d41ff 100%),
|
||||
// #fefefe;
|
||||
// }
|
||||
// }
|
||||
img {
|
||||
width: 1.875em;
|
||||
height: 1.875em;
|
||||
width: 1.6875em;
|
||||
height: 1.6875em;
|
||||
}
|
||||
}
|
||||
.loop-item-label {
|
||||
width: 4.5em;
|
||||
color: var(--text-color);
|
||||
font-size: 1.125em;
|
||||
font-size: 1.0125em;
|
||||
font-weight: 700;
|
||||
line-height: 160%;
|
||||
margin-left: 1.08em;
|
||||
padding: 0.3em 0.4em 0.3em 0.6em;
|
||||
padding: 0.27em 0.36em 0.27em 0.54em;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
border-radius: 999px;
|
||||
background: var(--card-bg-hover-color);
|
||||
}
|
||||
span {
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.loop-box-btn {
|
||||
// padding-left: 2.5em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 0.9em 0 0.2em;
|
||||
background: #fff;
|
||||
button {
|
||||
width: 9.1em;
|
||||
height: 3em;
|
||||
padding: 0.56em 1.1em;
|
||||
padding: 0.504em 0.99em;
|
||||
border-radius: 24px;
|
||||
background: var(---bg-white);
|
||||
// box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.25);
|
||||
margin-top: 1.1em;
|
||||
margin-top: 0.99em;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
text-align: center;
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 1.125em;
|
||||
font-size: 1.0125em;
|
||||
text-transform: capitalize;
|
||||
cursor: pointer;
|
||||
|
||||
&.disable {
|
||||
background: #d9d9d9;
|
||||
color: var(--text-white-color);
|
||||
pointer-events: none;
|
||||
}
|
||||
&.active-black {
|
||||
background: var(--text-color);
|
||||
// box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.25);
|
||||
color: var(--text-white-color);
|
||||
&:hover {
|
||||
box-shadow: 0px 0px 10px 0px rgba(142, 62, 255, 0.74);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(90deg, #2667ff 0%, #9d41ff 100%), #fefefe;
|
||||
box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.25);
|
||||
color: var(--text-white-color);
|
||||
&:hover {
|
||||
background: linear-gradient(
|
||||
0deg,
|
||||
rgba(255, 255, 255, 0.1) 0%,
|
||||
rgba(255, 255, 255, 0.1) 100%
|
||||
),
|
||||
linear-gradient(90deg, #2667ff 0%, #9d41ff 100%);
|
||||
box-shadow: 0px 0px 10px 0px rgba(142, 62, 255, 0.68);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.loop-box-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75em;
|
||||
}
|
||||
.auto-skip-toggle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 0 1.2em;
|
||||
}
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8em;
|
||||
font-size: 1em;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.toggle-text {
|
||||
font-weight: 600;
|
||||
}
|
||||
.toggle-switch {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 2.8em;
|
||||
height: 1.6em;
|
||||
}
|
||||
.toggle-switch input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
.toggle-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #d6dbe7;
|
||||
transition: 0.2s ease;
|
||||
border-radius: 999px;
|
||||
box-shadow: inset 0 0 0 2px #c5d2e6;
|
||||
}
|
||||
.toggle-slider:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 1.2em;
|
||||
width: 1.2em;
|
||||
left: 0.2em;
|
||||
top: 0.2em;
|
||||
background-color: #fff;
|
||||
transition: 0.2s ease;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.toggle-switch input:checked + .toggle-slider {
|
||||
background: linear-gradient(90deg, #2667ff 0%, #9d41ff 100%);
|
||||
box-shadow: none;
|
||||
}
|
||||
.toggle-switch input:checked + .toggle-slider:before {
|
||||
transform: translateX(1.2em);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div class="markdown-body" v-html="renderedHtml"></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from "vue";
|
||||
import "katex/dist/katex.min.css";
|
||||
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const renderedHtml = ref("");
|
||||
let md = null;
|
||||
let katexEngine = null;
|
||||
|
||||
const normalizeMathBlockInnerContent = (content) => {
|
||||
if (!content || typeof content !== "string") {
|
||||
return content;
|
||||
}
|
||||
|
||||
return content
|
||||
.replace(/\r\n?/g, "\n")
|
||||
.replace(/\\text\{([\s\S]*?)\}/g, (match, inner) => {
|
||||
return `\\text{${inner.replace(/\s*\n\s*/g, " ").trim()}}`;
|
||||
})
|
||||
.replace(/\s*\n+\s*/g, " ")
|
||||
.trim();
|
||||
};
|
||||
|
||||
const wrapBareLatexBlocks = (content) => {
|
||||
if (!content || !content.includes("\\begin{")) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return content.replace(
|
||||
/(^|\n)(\s*)(\\begin\{([a-zA-Z*]+)\}[\s\S]*?\\end\{\4\})(?=\s*(?:\n|$))/g,
|
||||
(match, lineStart, indent, block) => {
|
||||
const trimmedBlock = block.trim();
|
||||
|
||||
if (
|
||||
trimmedBlock.startsWith("$$") ||
|
||||
trimmedBlock.startsWith("\\[") ||
|
||||
trimmedBlock.startsWith("\\(")
|
||||
) {
|
||||
return match;
|
||||
}
|
||||
|
||||
return `${lineStart}${indent}$$\n${trimmedBlock}\n$$`;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const normalizeMathDelimiters = (content) => {
|
||||
if (!content || typeof content !== "string") {
|
||||
return content;
|
||||
}
|
||||
|
||||
return content
|
||||
.replace(/\$\$([\s\S]+?)\$\$/g, (match, inner) => {
|
||||
return `$$\n${normalizeMathBlockInnerContent(inner)}\n$$`;
|
||||
})
|
||||
.replace(/\\\[([\s\S]+?)\\\]/g, (match, inner) => {
|
||||
return `\\[${normalizeMathBlockInnerContent(inner)}\\]`;
|
||||
});
|
||||
};
|
||||
|
||||
const preprocessMathContent = (content) => {
|
||||
return normalizeMathDelimiters(wrapBareLatexBlocks(content));
|
||||
};
|
||||
|
||||
const extractStandaloneMath = (content) => {
|
||||
if (!content || typeof content !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmedContent = preprocessMathContent(content).trim();
|
||||
const dollarBlockMatch = trimmedContent.match(/^\$\$([\s\S]+)\$\$$/);
|
||||
|
||||
if (dollarBlockMatch) {
|
||||
return {
|
||||
displayMode: true,
|
||||
formula: normalizeMathBlockInnerContent(dollarBlockMatch[1]),
|
||||
};
|
||||
}
|
||||
|
||||
const bracketBlockMatch = trimmedContent.match(/^\\\[([\s\S]+)\\\]$/);
|
||||
|
||||
if (bracketBlockMatch) {
|
||||
return {
|
||||
displayMode: true,
|
||||
formula: normalizeMathBlockInnerContent(bracketBlockMatch[1]),
|
||||
};
|
||||
}
|
||||
|
||||
const inlineMatch = trimmedContent.match(/^\\\(([\s\S]+)\\\)$/);
|
||||
|
||||
if (inlineMatch) {
|
||||
return {
|
||||
displayMode: false,
|
||||
formula: normalizeMathBlockInnerContent(inlineMatch[1]),
|
||||
};
|
||||
}
|
||||
|
||||
if (/^\\begin\{([a-zA-Z*]+)\}[\s\S]*\\end\{\1\}$/.test(trimmedContent)) {
|
||||
return {
|
||||
displayMode: true,
|
||||
formula: normalizeMathBlockInnerContent(trimmedContent),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderStandaloneMath = (content) => {
|
||||
if (!katexEngine) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const standaloneMath = extractStandaloneMath(content);
|
||||
|
||||
if (!standaloneMath || !standaloneMath.formula) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return katexEngine.renderToString(standaloneMath.formula, {
|
||||
displayMode: standaloneMath.displayMode,
|
||||
throwOnError: false,
|
||||
strict: "ignore",
|
||||
macros: {
|
||||
"\\RR": "\\mathbb{R}",
|
||||
},
|
||||
});
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = (content) => {
|
||||
if (!md) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const standaloneMathHtml = renderStandaloneMath(content);
|
||||
|
||||
if (standaloneMathHtml) {
|
||||
return standaloneMathHtml;
|
||||
}
|
||||
|
||||
return md.render(preprocessMathContent(content));
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const [{ default: markdownit }, hljsModule, katex, texmathModule] =
|
||||
await Promise.all([
|
||||
import("markdown-it"),
|
||||
import("highlight.js"),
|
||||
import("katex"),
|
||||
import("markdown-it-texmath"),
|
||||
]);
|
||||
|
||||
const hljs = hljsModule.default || hljsModule;
|
||||
const texmath = texmathModule.default || texmathModule;
|
||||
katexEngine = katex.default || katex;
|
||||
|
||||
md = markdownit({
|
||||
highlight: function (str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
const highlighted = hljs.highlight(str, { language: lang }).value;
|
||||
return `<pre><code class="hljs language-${lang}">${highlighted}</code></pre>`;
|
||||
} catch (_) {}
|
||||
}
|
||||
return `<pre><code class="hljs">${md.utils.escapeHtml(
|
||||
str
|
||||
)}</code></pre>`;
|
||||
},
|
||||
});
|
||||
|
||||
// 修复列表和段落渲染逻辑
|
||||
md.renderer.rules.list_item_open = () => "<li>";
|
||||
md.renderer.rules.list_item_close = () => "</li>";
|
||||
md.renderer.rules.paragraph_open = (tokens, idx) => {
|
||||
const parentToken = tokens[idx - 1];
|
||||
return parentToken && parentToken.type === "list_item_open" ? "" : "<p>";
|
||||
};
|
||||
md.renderer.rules.paragraph_close = (tokens, idx) => {
|
||||
const parentToken = tokens[idx - 1];
|
||||
return parentToken && parentToken.type === "list_item_close"
|
||||
? ""
|
||||
: "</p>";
|
||||
};
|
||||
|
||||
md.use(texmath, {
|
||||
engine: katexEngine,
|
||||
delimiters: ["dollars", "brackets"],
|
||||
katexOptions: {
|
||||
throwOnError: false,
|
||||
strict: "ignore",
|
||||
macros: {
|
||||
"\\RR": "\\mathbb{R}",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
renderedHtml.value = renderContent(props.content);
|
||||
} catch (e) {
|
||||
console.error("MarkdownPreview 初始化失败:", e);
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
(newVal) => {
|
||||
if (md) {
|
||||
renderedHtml.value = renderContent(newVal);
|
||||
}
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.markdown-body {
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 1em;
|
||||
line-height: 180%;
|
||||
background-color: #fff;
|
||||
max-height: unset;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.markdown-body li {
|
||||
list-style: unset;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="markdown-body" v-html="renderedHtml"></div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, computed, onMounted, defineProps } from "vue";
|
||||
import { marked } from "marked";
|
||||
import "github-markdown-css/github-markdown.css";
|
||||
|
||||
const props = defineProps({
|
||||
markdown: String,
|
||||
});
|
||||
const markdown = ref(props.markdown);
|
||||
watch(
|
||||
() => props.markdown,
|
||||
(newValue, oldValue) => {
|
||||
markdown.value = newValue;
|
||||
}
|
||||
);
|
||||
|
||||
// 通过 computed 属性来动态计算渲染后的 HTML 内容
|
||||
const renderedHtml = computed(() => {
|
||||
return marked(markdown.value); // 使用 marked 转换为 HTML
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.markdown-body {
|
||||
padding: 0 1.35em;
|
||||
background-color: var(--bg-white-blue-color);
|
||||
border-radius: 8px;
|
||||
font-family: "Segoe UI";
|
||||
max-height: 8.505em;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
table {
|
||||
// margin: 0 auto;
|
||||
width: 100%;
|
||||
display: inline-table;
|
||||
tr {
|
||||
background-color: var(--bg-white-blue-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<div class="header-component">
|
||||
<div class="nav">
|
||||
<router-link to="/">
|
||||
<div class="logo-container">
|
||||
<img src="@/assets/images/RDAgent-logo.png" alt="R&D-Agent logo" />
|
||||
</div>
|
||||
</router-link>
|
||||
<ul>
|
||||
<li>
|
||||
<router-link to="/">Homepage</router-link>
|
||||
</li>
|
||||
<li>
|
||||
<router-link to="/Playground">Playground</router-link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
export default {
|
||||
components: {},
|
||||
setup() {},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.header-component {
|
||||
padding: 1.75em 4.375em 0;
|
||||
// height: 6.125em;
|
||||
height: 5.15em;
|
||||
box-sizing: border-box;
|
||||
& > .nav {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
.logo-container {
|
||||
margin-right: 4.375em;
|
||||
img {
|
||||
display: inline-block;
|
||||
height: 2.25em;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
li {
|
||||
margin-right: 2.25em;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
a {
|
||||
display: inline-block;
|
||||
color: var(--nav-default-color);
|
||||
font-size: 1.25em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
&:hover {
|
||||
color: var(--nav-hover-color);
|
||||
}
|
||||
}
|
||||
.router-link-exact-active {
|
||||
color: var(--text-color);
|
||||
|
||||
&:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,648 @@
|
||||
<template>
|
||||
<div class="research-component">
|
||||
<div class="content-box">
|
||||
<h2>
|
||||
Hypothesis
|
||||
<img
|
||||
v-if="isWaitingForHypothesis"
|
||||
src="@/assets/playground-images/loading-tab.gif"
|
||||
alt="loading"
|
||||
/>
|
||||
</h2>
|
||||
<div class="deduction">
|
||||
<div
|
||||
class="deduction-content"
|
||||
:class="{ 'deduction-content--pdf-only': isPdfOnlyHypothesis }"
|
||||
:style="{
|
||||
height: developer ? 'calc(100vh - 20.3em)' : 'calc(100vh - 17.9em)',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="pdf-content"
|
||||
:class="{ 'pdf-content--full': isPdfOnlyHypothesis }"
|
||||
v-if="researchPdfImage"
|
||||
>
|
||||
<img
|
||||
:src="researchPdfImage"
|
||||
alt="pdf image"
|
||||
:class="{ 'pdf-image--full': isPdfOnlyHypothesis }"
|
||||
/>
|
||||
<div class="pdf-full" @click="zoom">
|
||||
<span class="fullscreen"></span> Full Screen
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="researchHypothesis">
|
||||
<h3>Hypothesis</h3>
|
||||
<div>
|
||||
<p v-if="researchHypothesis.hypothesis">
|
||||
{{ researchHypothesis.hypothesis }}
|
||||
</p>
|
||||
<p v-else>
|
||||
{{ researchHypothesis.name_map["no_hypothesis"] }}
|
||||
</p>
|
||||
</div>
|
||||
<h3>Component</h3>
|
||||
<p>{{ researchHypothesis.component }}</p>
|
||||
<h3>Reason</h3>
|
||||
<div>
|
||||
<p v-if="researchHypothesis.reason">
|
||||
{{ researchHypothesis.reason }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isWaitingForHypothesis && !researchHypothesis && !researchPdfImage">
|
||||
<p style="padding-left: 1em">
|
||||
No hypothesis generated due to some errors happened in previous
|
||||
steps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content-box">
|
||||
<h2>
|
||||
Tasks<img
|
||||
v-if="!researcTasks && !updateEnd"
|
||||
src="@/assets/playground-images/loading-tab.gif"
|
||||
alt="loading"
|
||||
/>
|
||||
</h2>
|
||||
<div v-if="researcTasks">
|
||||
<selectComponent
|
||||
:scenarioList="researcTasks"
|
||||
:scenarioIndex="scenarioCheckedIndex"
|
||||
:showStatus="false"
|
||||
@scenarioCheckedItem="scenarioCheckedItem"
|
||||
></selectComponent>
|
||||
<div class="deduction" style="margin-top: 1em">
|
||||
<div
|
||||
class="deduction-content modelTask"
|
||||
v-if="scenarioChecked"
|
||||
:style="{
|
||||
height: developer ? 'calc(100vh - 24em)' : 'calc(100vh - 21.5em)',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="field in taskFields"
|
||||
:key="field.key"
|
||||
class="task-field"
|
||||
>
|
||||
<h3>{{ field.label }}</h3>
|
||||
<div v-if="field.key === 'description' || field.key === 'formulation'">
|
||||
<markdown :content="toTaskFieldMarkdownContent(field)"></markdown>
|
||||
</div>
|
||||
<div v-else-if="field.key === 'variables'" class="task-table-wrap">
|
||||
<table class="task-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Variable</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="row in toVariablesRows(field.value)"
|
||||
:key="`${field.key}-${row.name}`"
|
||||
>
|
||||
<td><markdown :content="toVariableNameCellContent(row.name)"></markdown></td>
|
||||
<td><markdown :content="toVariableValueCellContent(row.value)"></markdown></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p v-else class="task-field-text">
|
||||
{{ toDisplayText(field.value) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-box" v-if="showDialog">
|
||||
<div class="dialog-content gradient-border">
|
||||
<div class="close" @click="close"></div>
|
||||
<div class="dialog-pdf-box">
|
||||
<img :src="researchPdfImage" alt="pdf image" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, computed, defineProps } from "vue";
|
||||
import selectComponent from "../components/sm-select-component.vue";
|
||||
import markdown from "../components/markdown.vue";
|
||||
const props = defineProps({
|
||||
currentData: Object,
|
||||
updateEnd: Boolean,
|
||||
developer: Boolean,
|
||||
});
|
||||
const currentData = ref(props.currentData);
|
||||
const updateEnd = ref(props.updateEnd);
|
||||
const developer = ref(props.developer);
|
||||
const researchHypothesis = ref(null);
|
||||
const researcTasks = ref(null);
|
||||
const researchPdfImage = ref("");
|
||||
const scenarioChecked = ref(null);
|
||||
const scenarioCheckedIndex = ref(0);
|
||||
const showDialog = ref(false);
|
||||
|
||||
const isPdfOnlyHypothesis = computed(() => {
|
||||
return !developer.value && Boolean(researchPdfImage.value) && !researchHypothesis.value;
|
||||
});
|
||||
|
||||
const isWaitingForHypothesis = computed(() => {
|
||||
return !updateEnd.value && !researchHypothesis.value && !researchPdfImage.value;
|
||||
});
|
||||
|
||||
const isEmptyTaskField = (value) => {
|
||||
if (value == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value.trim() === "";
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.length === 0;
|
||||
}
|
||||
|
||||
if (typeof value === "object") {
|
||||
return Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const toFieldLabel = (key) => {
|
||||
return key
|
||||
.split("_")
|
||||
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const toMarkdownContent = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join("\n\n");
|
||||
}
|
||||
|
||||
if (typeof value === "string") {
|
||||
return value;
|
||||
}
|
||||
|
||||
return JSON.stringify(value, null, 2);
|
||||
};
|
||||
|
||||
const hasLatexDelimiters = (content) => {
|
||||
if (!content || typeof content !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const trimmedContent = content.trim();
|
||||
|
||||
return (
|
||||
trimmedContent.includes("$$") ||
|
||||
trimmedContent.includes("\\(") ||
|
||||
trimmedContent.includes("\\[") ||
|
||||
trimmedContent.includes("\\begin{")
|
||||
);
|
||||
};
|
||||
|
||||
const looksLikeLatexFormula = (content, { includeOperators = true } = {}) => {
|
||||
if (!content || typeof content !== "string") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const trimmedContent = content.trim();
|
||||
|
||||
if (trimmedContent === "") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const hasLatexCommand = /\\[a-zA-Z]+/.test(trimmedContent);
|
||||
const hasSubOrSuperscript = /[A-Za-z][A-Za-z0-9]*(?:[_^](?:\{[^{}]+\}|[A-Za-z0-9]+))/.test(
|
||||
trimmedContent
|
||||
);
|
||||
const hasLatexGrouping = /[_^{}]/.test(trimmedContent);
|
||||
const hasMathOperators = /[=<>+\-*/]/.test(trimmedContent);
|
||||
|
||||
return (
|
||||
hasLatexCommand ||
|
||||
hasSubOrSuperscript ||
|
||||
hasLatexGrouping ||
|
||||
(includeOperators && hasMathOperators)
|
||||
);
|
||||
};
|
||||
|
||||
const wrapStandaloneLatexContent = (
|
||||
value,
|
||||
{ displayMode = false, allowSentencePunctuation = true, includeOperators = true } = {}
|
||||
) => {
|
||||
const content = toMarkdownContent(value);
|
||||
|
||||
if (typeof content !== "string") {
|
||||
return content;
|
||||
}
|
||||
|
||||
const trimmedContent = content.trim();
|
||||
|
||||
if (
|
||||
!trimmedContent ||
|
||||
hasLatexDelimiters(trimmedContent) ||
|
||||
!looksLikeLatexFormula(trimmedContent, { includeOperators })
|
||||
) {
|
||||
return content;
|
||||
}
|
||||
|
||||
const hasPlainSentencePunctuation = /[.!?]|:\s+[A-Za-z]/.test(trimmedContent);
|
||||
|
||||
if (!allowSentencePunctuation && hasPlainSentencePunctuation) {
|
||||
return content;
|
||||
}
|
||||
|
||||
if (displayMode) {
|
||||
return `$$\n${trimmedContent}\n$$`;
|
||||
}
|
||||
|
||||
return `$${trimmedContent}$`;
|
||||
};
|
||||
|
||||
const formatFormulationContent = (value) => {
|
||||
return wrapStandaloneLatexContent(value, {
|
||||
displayMode: true,
|
||||
allowSentencePunctuation: true,
|
||||
includeOperators: true,
|
||||
});
|
||||
};
|
||||
|
||||
const toTaskFieldMarkdownContent = (field) => {
|
||||
if (field.key === "formulation") {
|
||||
return formatFormulationContent(field.value);
|
||||
}
|
||||
|
||||
return toMarkdownContent(field.value);
|
||||
};
|
||||
|
||||
const toDisplayText = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(", ");
|
||||
}
|
||||
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const wrapBareInlineLatex = (content) => {
|
||||
if (!content || typeof content !== "string") {
|
||||
return content;
|
||||
}
|
||||
|
||||
const protectedSegments = [];
|
||||
const protectedContent = content.replace(
|
||||
/(\$\$[\s\S]+?\$\$|\$[^$\n]+\$|\\\([\s\S]+?\\\)|\\\[[\s\S]+?\\\])/g,
|
||||
(match) => {
|
||||
const marker = `@@LATEX_${protectedSegments.length}@@`;
|
||||
protectedSegments.push(match);
|
||||
return marker;
|
||||
}
|
||||
);
|
||||
|
||||
const withCommandLatex = protectedContent.replace(
|
||||
/\\[a-zA-Z]+(?:\s*\[[^\]]*\]|\s*\{[^{}]*\}|[_^](?:\{[^{}]*\}|[A-Za-z0-9]))*/g,
|
||||
(match) => `$${match}$`
|
||||
);
|
||||
|
||||
const withInlineLatex = withCommandLatex.replace(
|
||||
/\b[A-Za-z][A-Za-z0-9]*(?:[_^](?:\{[^{}]+\}|[A-Za-z0-9]+))+/g,
|
||||
(match) => `$${match}$`
|
||||
);
|
||||
|
||||
return withInlineLatex.replace(/@@LATEX_(\d+)@@/g, (_, index) => {
|
||||
return protectedSegments[Number(index)];
|
||||
});
|
||||
};
|
||||
|
||||
const toVariableCellContent = (value) => {
|
||||
return wrapStandaloneLatexContent(value, {
|
||||
displayMode: false,
|
||||
allowSentencePunctuation: false,
|
||||
includeOperators: false,
|
||||
});
|
||||
};
|
||||
|
||||
const toVariableNameCellContent = (value) => {
|
||||
return toVariableCellContent(value);
|
||||
};
|
||||
|
||||
const toVariableValueCellContent = (value) => {
|
||||
return wrapBareInlineLatex(toVariableCellContent(value));
|
||||
};
|
||||
|
||||
const toVariablesRows = (variables) => {
|
||||
if (Array.isArray(variables)) {
|
||||
return variables.map((item, index) => {
|
||||
if (item && typeof item === "object" && !Array.isArray(item)) {
|
||||
return {
|
||||
name: item.name || item.key || `item_${index + 1}`,
|
||||
value: toMarkdownContent(item.value ?? item.expression ?? item),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: `item_${index + 1}`,
|
||||
value: toMarkdownContent(item),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
if (variables && typeof variables === "object") {
|
||||
return Object.entries(variables).map(([name, value]) => ({
|
||||
name,
|
||||
value: toMarkdownContent(value),
|
||||
}));
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
const taskFields = computed(() => {
|
||||
if (!scenarioChecked.value) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Object.entries(scenarioChecked.value)
|
||||
.filter(([key, value]) => key !== "name" && !isEmptyTaskField(value))
|
||||
.map(([key, value]) => ({
|
||||
key,
|
||||
label: toFieldLabel(key),
|
||||
value,
|
||||
}))
|
||||
.sort((left, right) => {
|
||||
if (left.key === "model_type") {
|
||||
return -1;
|
||||
}
|
||||
if (right.key === "model_type") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
|
||||
const setScenarioChecked = (task) => {
|
||||
scenarioChecked.value = task;
|
||||
};
|
||||
|
||||
const updateData = () => {
|
||||
if (currentData.value) {
|
||||
researchHypothesis.value = currentData.value.researchHypothesis;
|
||||
researcTasks.value = currentData.value.researcTasks;
|
||||
researchPdfImage.value = currentData.value.researchPdfImage;
|
||||
scenarioCheckedIndex.value = 0;
|
||||
if (researcTasks.value) {
|
||||
setScenarioChecked(researcTasks.value[scenarioCheckedIndex.value]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const zoom = (color, data, name) => {
|
||||
showDialog.value = true;
|
||||
};
|
||||
const close = () => {
|
||||
showDialog.value = false;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [props.currentData, props.updateEnd, props.developer],
|
||||
(newValue, oldValue) => {
|
||||
currentData.value = newValue[0];
|
||||
updateEnd.value = newValue[1];
|
||||
developer.value = newValue[2];
|
||||
updateData();
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const scenarioCheckedItem = (data) => {
|
||||
scenarioCheckedIndex.value = data.scenarioCheckedIndex;
|
||||
setScenarioChecked(data.scenarioChecked);
|
||||
};
|
||||
onMounted(() => {
|
||||
if (currentData.value) {
|
||||
updateData();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.research-component {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 1.89em;
|
||||
.content-box {
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
color: var(--text-color);
|
||||
h2 {
|
||||
font-size: 1.26em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
margin-bottom: 0.45em;
|
||||
position: relative;
|
||||
|
||||
img {
|
||||
width: 2.25em;
|
||||
height: 2.25em;
|
||||
margin-left: 0.405em;
|
||||
position: absolute;
|
||||
top: -0.18em;
|
||||
}
|
||||
}
|
||||
.deduction {
|
||||
border-radius: 11px;
|
||||
background: var(--bg-white);
|
||||
padding: 0.9em 0;
|
||||
box-sizing: border-box;
|
||||
overflow-y: hidden;
|
||||
.deduction-content {
|
||||
height: calc(100vh - 19.8em);
|
||||
padding: 0 1.6875em;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
|
||||
.pdf-content {
|
||||
text-align: center;
|
||||
&.pdf-content--full {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
img {
|
||||
height: 18em;
|
||||
&.pdf-image--full {
|
||||
width: 100%;
|
||||
height: calc(100% - 2.4em);
|
||||
object-fit: contain;
|
||||
object-position: center top;
|
||||
}
|
||||
}
|
||||
.pdf-full {
|
||||
font-weight: 700;
|
||||
font-size: 0.9em;
|
||||
line-height: 1.8em;
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
max-width: 7.5em;
|
||||
margin: 0 auto;
|
||||
|
||||
.fullscreen {
|
||||
display: inline-block;
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/fullscreen.svg)
|
||||
no-repeat;
|
||||
background-size: contain;
|
||||
margin-right: 0.45em;
|
||||
}
|
||||
}
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.1475em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
margin-bottom: 0.45em;
|
||||
margin-top: 0.9em;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
p {
|
||||
font-family: "Microsoft YaHei";
|
||||
font-size: 0.9em;
|
||||
line-height: 180%;
|
||||
}
|
||||
&.deduction-content--pdf-only {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
.modelTask {
|
||||
height: calc(100vh - 23.4em);
|
||||
.task-field {
|
||||
margin-top: 1em;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.task-field-text {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.task-table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.task-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.9em;
|
||||
line-height: 180%;
|
||||
th,
|
||||
td {
|
||||
padding: 0.65em 0.8em;
|
||||
border: 1px solid #d9e2f2;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
}
|
||||
th {
|
||||
background: #f5f8ff;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background: rgba(255, 255, 255, 0.29);
|
||||
backdrop-filter: blur(4.599999904632568px);
|
||||
z-index: 999999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
.dialog-content {
|
||||
width: 800px;
|
||||
height: 80vh;
|
||||
background-color: #fff;
|
||||
border-radius: 18px;
|
||||
--border-radius: 20px;
|
||||
--border-width: 2px;
|
||||
padding: 2em 0;
|
||||
margin-top: -4em;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
.dialog-pdf-box {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
right: 1.5em;
|
||||
top: 1em;
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/close.svg) no-repeat;
|
||||
background-size: contain;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
&:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
:deep(.el-table) {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
:deep(.el-table thead) {
|
||||
color: var(--text-color);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="capture" ref="capture">
|
||||
<!-- 这里是你想要保存为图片的HTML内容 -->
|
||||
<h1>Hello World</h1>
|
||||
</div>
|
||||
<button @click="saveAsImage">保存为图片</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref } from "vue";
|
||||
import html2canvas from "html2canvas";
|
||||
|
||||
export default {
|
||||
setup() {
|
||||
const capture = ref(null);
|
||||
|
||||
const saveAsImage = async () => {
|
||||
try {
|
||||
const canvas = await html2canvas(capture.value);
|
||||
const img = canvas.toDataURL("image/png");
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = img;
|
||||
link.download = "capture.png";
|
||||
link.click();
|
||||
} catch (error) {
|
||||
console.error("Error capturing the image:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
capture,
|
||||
saveAsImage,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="select-box">
|
||||
<div class="select-div gradient-border" @click.stop="changePopover">
|
||||
<div>
|
||||
<div class="checked-item" v-if="scenarioChecked">
|
||||
<SvgIcon
|
||||
class="select-item-icon"
|
||||
:name="scenarioChecked.icon"
|
||||
></SvgIcon>
|
||||
<span>{{
|
||||
scenarioChecked.checkedName
|
||||
? scenarioChecked.checkedName
|
||||
: scenarioChecked.name
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="down-arrow" :class="{ active: showPopover }"></span>
|
||||
</div>
|
||||
<div
|
||||
class="select-drop-panel gradient-border"
|
||||
v-show="showPopover"
|
||||
:style="{
|
||||
'--height':
|
||||
scenarioList.length <= 4
|
||||
? scenarioList.length * 3.375 * 16 +
|
||||
(scenarioList.length - 1) +
|
||||
'px'
|
||||
: '16em',
|
||||
}"
|
||||
>
|
||||
<div class="select-drop-list">
|
||||
<div
|
||||
class="select-drop-item"
|
||||
@click.stop="choiceScenario(item, index)"
|
||||
v-for="(item, index) in scenarioList"
|
||||
:key="index"
|
||||
:style="{ 'border-color': item.color }"
|
||||
>
|
||||
<div
|
||||
class="drop-item-one"
|
||||
:class="{ active: scenarioCheckedIndex == index && !item.child }"
|
||||
>
|
||||
<SvgIcon
|
||||
v-if="item.icon"
|
||||
class="select-item-icon"
|
||||
:name="item.icon"
|
||||
></SvgIcon>
|
||||
<span>{{ item.name }}</span>
|
||||
<span
|
||||
class="down-arrow"
|
||||
:class="{ active: showChild }"
|
||||
v-if="item.child"
|
||||
></span>
|
||||
</div>
|
||||
<div v-if="item.child && showChild">
|
||||
<div
|
||||
class="drop-child-item"
|
||||
@click.stop="choiceScenario(item, index, child, index2)"
|
||||
v-for="(child, index2) in item.child"
|
||||
:key="child.name"
|
||||
:style="{ 'border-color': item.color }"
|
||||
:class="{ active: scenarioChildCheckedIndex == index2 }"
|
||||
>
|
||||
<span>{{ child.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
nextTick,
|
||||
onUnmounted,
|
||||
} from "vue";
|
||||
const props = defineProps({
|
||||
scenarioList: Array,
|
||||
scenarioIndex: Number,
|
||||
});
|
||||
const emit = defineEmits(["scenarioCheckedItem"]);
|
||||
const scenarioList = ref(props.scenarioList);
|
||||
const scenarioCheckedIndex = ref(props.scenarioIndex);
|
||||
const scenarioChildCheckedIndex = ref(-1);
|
||||
const scenarioChecked = ref(null);
|
||||
const showChild = ref(false);
|
||||
|
||||
const showPopover = ref(false);
|
||||
|
||||
watch(
|
||||
() => [props.scenarioList, props.scenarioIndex],
|
||||
(newValue, oldValue) => {
|
||||
scenarioList.value = newValue[0];
|
||||
scenarioCheckedIndex.value = newValue[1];
|
||||
|
||||
if (scenarioList.value && scenarioCheckedIndex.value >= 0) {
|
||||
scenarioChecked.value = scenarioList.value[scenarioCheckedIndex.value];
|
||||
} else {
|
||||
scenarioChecked.value = null;
|
||||
}
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
|
||||
const changePopover = () => {
|
||||
if (showPopover.value) {
|
||||
showPopover.value = false;
|
||||
} else {
|
||||
showPopover.value = true;
|
||||
}
|
||||
};
|
||||
const choiceScenario = (item, index, child, index2) => {
|
||||
if (item.child && !child) {
|
||||
showChild.value = !showChild.value;
|
||||
return;
|
||||
}
|
||||
|
||||
scenarioCheckedIndex.value = index;
|
||||
scenarioChecked.value = item;
|
||||
|
||||
scenarioChildCheckedIndex.value = -1;
|
||||
if (child) {
|
||||
scenarioChildCheckedIndex.value = index2;
|
||||
scenarioChecked.value.checkedName = child.name;
|
||||
}
|
||||
showPopover.value = false;
|
||||
emit("scenarioCheckedItem", {
|
||||
scenarioCheckedIndex: scenarioCheckedIndex.value,
|
||||
scenarioChecked: scenarioChecked.value,
|
||||
});
|
||||
};
|
||||
const globalClickHandler = (e) => {
|
||||
e.stopPropagation();
|
||||
showPopover.value = false;
|
||||
};
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", globalClickHandler);
|
||||
});
|
||||
|
||||
// 在组件被卸载前移除全局点击事件监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", globalClickHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.select-box {
|
||||
margin-top: 1.62em;
|
||||
margin-bottom: 2.52em;
|
||||
position: relative;
|
||||
.select-div {
|
||||
display: flex;
|
||||
height: 3.375em;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
--border-radius: 11px;
|
||||
--border-width: 2px;
|
||||
cursor: pointer;
|
||||
|
||||
.checked-item {
|
||||
padding: 0.5625em 1.98em 0.5625em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.select-item-icon {
|
||||
margin-right: 0.9em;
|
||||
}
|
||||
span {
|
||||
color: var(--text-color);
|
||||
font-size: 1.17em;
|
||||
line-height: 200%;
|
||||
margin-top: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.select-drop-panel {
|
||||
--height: 16em;
|
||||
--border-width: 2px;
|
||||
--border-radius: 11px;
|
||||
width: 100%;
|
||||
height: calc(var(--height) + 4px);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 3.375em;
|
||||
cursor: pointer;
|
||||
background-color: var(--bg-white);
|
||||
border-radius: 13px;
|
||||
z-index: 99;
|
||||
overflow: hidden;
|
||||
box-shadow: 8px 11px 30px 0px var(--wg-shadow-color);
|
||||
.select-drop-list {
|
||||
width: calc(100% - 4px);
|
||||
height: var(--height);
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
z-index: 1;
|
||||
background-color: var(--bg-white);
|
||||
border-radius: 11px;
|
||||
overflow: auto;
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #fff;
|
||||
}
|
||||
&:hover {
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: #e4e7ff;
|
||||
}
|
||||
}
|
||||
}
|
||||
.select-drop-item {
|
||||
border-bottom: 2px solid #2e65ff;
|
||||
|
||||
.drop-item-one {
|
||||
padding: 0.5625em 1.98em 0.5625em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--card-bg-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
.drop-child-item {
|
||||
padding: 0.5625em 1.98em 0.5625em;
|
||||
padding-left: 4.3em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-top: 2px solid #2e65ff;
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--card-bg-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.select-item-icon {
|
||||
margin-right: 0.9em;
|
||||
}
|
||||
span {
|
||||
color: var(--text-color);
|
||||
font-size: 1.17em;
|
||||
line-height: 200%;
|
||||
margin-top: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.down-arrow {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background: url(@/assets/images/down-arrow.svg) no-repeat;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
|
||||
&.active {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,250 @@
|
||||
<template>
|
||||
<div class="select-box">
|
||||
<div class="select-div gradient-border" @click.stop="changePopover">
|
||||
<div>
|
||||
<div class="checked-item" v-if="scenarioChecked">
|
||||
<span
|
||||
v-if="showStatus"
|
||||
:class="{
|
||||
success: scenarioChecked.decision,
|
||||
fail: !scenarioChecked.decision,
|
||||
}"
|
||||
></span>
|
||||
<span :class="{ omit: showStatus }">{{ scenarioChecked.name }}</span>
|
||||
</div>
|
||||
<div class="checked-item checked-placeholder" v-else>
|
||||
<span>{{ placeholder }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="down-arrow"></span>
|
||||
</div>
|
||||
<div
|
||||
class="select-drop-panel gradient-border"
|
||||
:style="{
|
||||
'--height':
|
||||
optionCount <= 4
|
||||
? optionCount * 3.15 * 16 +
|
||||
Math.max(optionCount - 2, 0) * 2 +
|
||||
'px'
|
||||
: '16em',
|
||||
}"
|
||||
v-show="showPopover"
|
||||
>
|
||||
<div class="select-drop-list">
|
||||
<div
|
||||
class="select-drop-item"
|
||||
@click="choiceScenario(item, index)"
|
||||
v-for="(item, index) in scenarioList"
|
||||
:key="index"
|
||||
:style="{ 'border-color': item.color }"
|
||||
:class="{ active: scenarioCheckedIndex == index }"
|
||||
>
|
||||
<span
|
||||
v-if="showStatus"
|
||||
:class="{
|
||||
success: item.decision,
|
||||
fail: !item.decision,
|
||||
}"
|
||||
></span>
|
||||
<span :class="{ omit: showStatus }">{{ item.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
computed,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
defineProps,
|
||||
defineEmits,
|
||||
nextTick,
|
||||
onUnmounted,
|
||||
} from "vue";
|
||||
const props = defineProps({
|
||||
scenarioList: Array,
|
||||
scenarioIndex: Number,
|
||||
showStatus: Boolean,
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["scenarioCheckedItem"]);
|
||||
const scenarioCheckedIndex = ref(props.scenarioIndex);
|
||||
const scenarioList = ref(props.scenarioList);
|
||||
const scenarioChecked = ref(null);
|
||||
const optionCount = computed(() => scenarioList.value?.length || 0);
|
||||
if (scenarioList.value) {
|
||||
scenarioChecked.value = scenarioList.value[scenarioCheckedIndex.value];
|
||||
}
|
||||
const showStatus = ref(props.showStatus);
|
||||
const placeholder = ref(props.placeholder);
|
||||
watch(
|
||||
() => [props.scenarioList, props.scenarioIndex, props.showStatus, props.placeholder],
|
||||
(newValue, oldValue) => {
|
||||
scenarioList.value = newValue[0];
|
||||
scenarioCheckedIndex.value = newValue[1];
|
||||
showStatus.value = newValue[2];
|
||||
placeholder.value = newValue[3];
|
||||
if (scenarioList.value) {
|
||||
scenarioChecked.value = scenarioList.value[scenarioCheckedIndex.value];
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const showPopover = ref(false);
|
||||
const changePopover = () => {
|
||||
if (showPopover.value) {
|
||||
showPopover.value = false;
|
||||
} else {
|
||||
showPopover.value = true;
|
||||
}
|
||||
};
|
||||
const choiceScenario = (item, index) => {
|
||||
scenarioCheckedIndex.value = index;
|
||||
scenarioChecked.value = item;
|
||||
showPopover.value = false;
|
||||
emit("scenarioCheckedItem", {
|
||||
scenarioCheckedIndex: scenarioCheckedIndex.value,
|
||||
scenarioChecked: scenarioChecked.value,
|
||||
});
|
||||
};
|
||||
const globalClickHandler = () => {
|
||||
showPopover.value = false;
|
||||
};
|
||||
onMounted(() => {
|
||||
document.addEventListener("click", globalClickHandler);
|
||||
});
|
||||
|
||||
// 在组件被卸载前移除全局点击事件监听
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("click", globalClickHandler);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.select-box {
|
||||
position: relative;
|
||||
.select-div {
|
||||
display: flex;
|
||||
height: 2.7em;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-radius: 9px;
|
||||
--border-radius: 11px;
|
||||
--border-width: 2px;
|
||||
cursor: pointer;
|
||||
.down-arrow {
|
||||
width: 1.35em;
|
||||
height: 1.35em;
|
||||
background: url(@/assets/images/down-arrow.svg) no-repeat;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
right: 1.35em;
|
||||
}
|
||||
.checked-item {
|
||||
box-sizing: border-box;
|
||||
padding: 0.5625em 1.98em 0.5625em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.select-item-icon {
|
||||
margin-right: 0.9em;
|
||||
}
|
||||
span {
|
||||
color: var(--text-color);
|
||||
font-size: 1.0125em;
|
||||
line-height: 200%;
|
||||
margin-top: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.select-drop-panel {
|
||||
--height: 16em;
|
||||
width: 100%;
|
||||
height: calc(var(--height) + 4px);
|
||||
max-height: calc(20em + 4px);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 2.745em;
|
||||
cursor: pointer;
|
||||
background-color: var(--bg-white);
|
||||
border-radius: 11px;
|
||||
z-index: 99;
|
||||
overflow: hidden;
|
||||
// box-shadow: 8px 11px 30px 0px var(--wg-shadow-color);
|
||||
.select-drop-list {
|
||||
width: calc(100% - 4px);
|
||||
height: var(--height);
|
||||
max-height: 20em;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
left: 2px;
|
||||
top: 2px;
|
||||
z-index: 1;
|
||||
background-color: var(--bg-white);
|
||||
border-radius: 11px;
|
||||
}
|
||||
.select-drop-item {
|
||||
padding: 0.5625em 1.98em 0.5625em;
|
||||
border-bottom: 2px solid #2e65ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3.15em;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:last-child {
|
||||
|
||||
&.checked-placeholder {
|
||||
span {
|
||||
color: #868ca5;
|
||||
}
|
||||
}
|
||||
border-bottom: none;
|
||||
}
|
||||
.select-item-icon {
|
||||
margin-right: 0.9em;
|
||||
}
|
||||
span {
|
||||
color: var(--text-color);
|
||||
font-size: 1.0125em;
|
||||
line-height: 200%;
|
||||
margin-top: -2px;
|
||||
}
|
||||
&:hover,
|
||||
&.active {
|
||||
background-color: var(--card-bg-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
.success {
|
||||
display: inline-block;
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/process-checked.svg) no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.45em;
|
||||
}
|
||||
.fail {
|
||||
display: inline-block;
|
||||
width: 1.125em;
|
||||
height: 1.125em;
|
||||
background: url(@/assets/playground-images/process-fail-checked.svg)
|
||||
no-repeat;
|
||||
background-size: contain;
|
||||
vertical-align: middle;
|
||||
margin-right: 0.45em;
|
||||
}
|
||||
.omit {
|
||||
display: inline-block;
|
||||
width: 410px;
|
||||
white-space: nowrap; /* 不换行 */
|
||||
overflow: hidden; /* 超出部分隐藏 */
|
||||
text-overflow: ellipsis; /* 显示省略号 */
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="step-box">
|
||||
<div
|
||||
v-for="(item, index) in stepList"
|
||||
:key="index"
|
||||
class="step-card"
|
||||
:class="{
|
||||
'default-color': currentIndex < index,
|
||||
'current-color': currentIndex == index,
|
||||
'active-color': currentIndex > index,
|
||||
}"
|
||||
>
|
||||
<SvgIcon v-if="index != 0" class="step-icon" name="right-arrow"></SvgIcon>
|
||||
<span class="step-label"
|
||||
><i>{{ index + 1 }}.</i>{{ item }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, watch, defineProps, nextTick } from "vue";
|
||||
const props = defineProps({
|
||||
activeIndex: Number,
|
||||
});
|
||||
const currentIndex = ref(props.activeIndex);
|
||||
const stepList = ["Start", "Input Information", "Summary"];
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.step-box {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
.step-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.step-label {
|
||||
display: flex;
|
||||
padding: 0.65em 1em;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
line-height: 200%;
|
||||
border-radius: 999px;
|
||||
color: var(--step-default-color);
|
||||
border: 2px solid var(--step-default-color);
|
||||
i {
|
||||
margin-right: 0.75em;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
.step-icon {
|
||||
width: 2.15em;
|
||||
height: 1.25em;
|
||||
margin: 0 1.25em;
|
||||
}
|
||||
.default-color {
|
||||
.step-label {
|
||||
border-color: var(--step-default-color);
|
||||
color: var(--step-default-color);
|
||||
}
|
||||
.step-icon {
|
||||
color: var(--step-default-color) !important;
|
||||
}
|
||||
}
|
||||
.current-color {
|
||||
.step-label {
|
||||
color: var(--step-current-color);
|
||||
border-color: var(--step-current-color);
|
||||
}
|
||||
.step-icon {
|
||||
color: var(--step-current-color) !important;
|
||||
}
|
||||
}
|
||||
.active-color {
|
||||
.step-label {
|
||||
color: var(--step-active-color);
|
||||
border-color: var(--step-active-color);
|
||||
}
|
||||
.step-icon {
|
||||
color: var(--step-active-color) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<svg aria-hidden="true" class="svg-icon" :style="{ color: color }">
|
||||
<use :xlink:href="symbolId" rel="external nofollow" />
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, computed } from "vue";
|
||||
|
||||
export default defineComponent({
|
||||
name: "SvgIcon",
|
||||
props: {
|
||||
// 使用的svg图标名称,也就是svg文件名
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
prefix: {
|
||||
type: String,
|
||||
default: "icon",
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "#000",
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const symbolId = computed(() => `#${props.prefix}-${props.name}`);
|
||||
return { symbolId };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
<style scope>
|
||||
.svg-icon {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
fill: currentColor;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<swiper
|
||||
:modules="modules"
|
||||
:navigation="true"
|
||||
:pagination="{ clickable: true }"
|
||||
>
|
||||
<swiper-slide
|
||||
class="swiper-slide"
|
||||
v-for="(item, index) in caseData"
|
||||
:key="item.model + index"
|
||||
>
|
||||
<div class="swiper-main">
|
||||
<p class="title">
|
||||
<span>Care/Harm Score:</span>{{ item.score.toFixed(4) }}
|
||||
</p>
|
||||
<p class="title">{{ "Case" + (index + 1) + "-" + item.label }} Score</p>
|
||||
<div class="chart-content-desc">
|
||||
<img src="@/assets/images/Avatar-Q.png" alt="Q" />
|
||||
<p>
|
||||
{{ item.prompt }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="chart-content-desc">
|
||||
<img src="@/assets/images/Avatar-A.png" alt="A" />
|
||||
<p class="highlight" v-html="item.highlight"></p>
|
||||
</div>
|
||||
</div>
|
||||
</swiper-slide>
|
||||
</swiper>
|
||||
</template>
|
||||
<script>
|
||||
import { ref, watch } from "vue";
|
||||
// import Swiper core and required modules
|
||||
import { Pagination, Navigation, A11y, Autoplay } from "swiper/modules";
|
||||
|
||||
// Import Swiper Vue.js components
|
||||
import { Swiper, SwiperSlide } from "swiper/vue";
|
||||
|
||||
// Import Swiper styles
|
||||
import "swiper/css";
|
||||
import "swiper/css/navigation";
|
||||
import "swiper/css/pagination";
|
||||
|
||||
// Import Swiper styles
|
||||
export default {
|
||||
components: {
|
||||
Swiper,
|
||||
SwiperSlide,
|
||||
},
|
||||
props: {
|
||||
data: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
console.log("接收到的消息:", this.data); // 打印接收到的消息
|
||||
},
|
||||
setup(props) {
|
||||
const onSwiper = (swiper) => {
|
||||
console.log(swiper);
|
||||
};
|
||||
const onSlideChange = () => {
|
||||
console.log("slide change");
|
||||
};
|
||||
const caseData = ref(props.data);
|
||||
watch(
|
||||
() => props.data,
|
||||
(newMessage, oldMessage) => {
|
||||
caseData.value = newMessage;
|
||||
console.log(caseData.value);
|
||||
}
|
||||
);
|
||||
return {
|
||||
onSwiper,
|
||||
onSlideChange,
|
||||
modules: [Pagination, Navigation, A11y, Autoplay],
|
||||
caseData,
|
||||
};
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.swiper-slide {
|
||||
width: 100%;
|
||||
padding-bottom: 3.25em;
|
||||
box-sizing: border-box;
|
||||
padding: 2.5em 0 2.5em 0;
|
||||
.swiper-main {
|
||||
padding: 0 4em;
|
||||
height: 21em;
|
||||
overflow: auto;
|
||||
}
|
||||
.title {
|
||||
font-size: 1em;
|
||||
color: #fff;
|
||||
line-height: 2em;
|
||||
span {
|
||||
color: #ffd000;
|
||||
}
|
||||
}
|
||||
.chart-content-desc {
|
||||
display: flex;
|
||||
margin-top: 1.625em;
|
||||
img {
|
||||
display: block;
|
||||
width: 1.875em;
|
||||
height: 1.875em;
|
||||
margin-right: 1.625em;
|
||||
}
|
||||
p {
|
||||
width: 31.43em;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
line-height: 1.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.swiper-pagination-bullet) {
|
||||
width: 1em;
|
||||
height: 3px;
|
||||
background-color: #fff;
|
||||
opacity: 0.3;
|
||||
border-radius: 1px;
|
||||
}
|
||||
:deep(.swiper-pagination-bullet-active) {
|
||||
width: 1.25em;
|
||||
opacity: 1;
|
||||
}
|
||||
:deep(.swiper-button-prev),
|
||||
:deep(.swiper-button-next) {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
color: #fff;
|
||||
}
|
||||
:deep(.swiper-button-prev:after),
|
||||
:deep(.swiper-button-next:after) {
|
||||
font-size: 1em;
|
||||
}
|
||||
:deep(.highlight i) {
|
||||
color: #90e0ef;
|
||||
font-weight: 500;
|
||||
line-height: 1.8em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,436 @@
|
||||
<template>
|
||||
<div
|
||||
class="upload"
|
||||
:class="{ finished: displayProgress >= 100 }"
|
||||
:style="{ '--percent': displayProgress }"
|
||||
>
|
||||
<div class="text">
|
||||
<strong><span>{{ displayProgress >= 100 ? "Uploaded" : "Uploading" }}</span> files</strong>
|
||||
<div>
|
||||
<small>%</small>
|
||||
<div>
|
||||
<small>
|
||||
<span>{{ secondsLeftDisplay }}</span> seconds left
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="javascript:;" class="btn cancel" @click="cancelUpload"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="percent">
|
||||
<span></span>
|
||||
<div>
|
||||
<svg preserveAspectRatio="none" viewBox="0 0 600 12">
|
||||
<path
|
||||
d="M0,1 L200,1 C300,1 300,11 400,11 L600,11"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, defineProps, defineEmits, ref, watch } from "vue";
|
||||
const props = defineProps({
|
||||
progress: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
});
|
||||
const emit = defineEmits(["cancelUpload"]);
|
||||
const displayProgress = ref(0);
|
||||
const startedAt = ref(0);
|
||||
const secondsLeft = ref(0);
|
||||
|
||||
const secondsLeftDisplay = computed(() => {
|
||||
if (displayProgress.value >= 100) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, secondsLeft.value);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.progress,
|
||||
(value) => {
|
||||
const safeValue = Number.isFinite(value) ? value : 0;
|
||||
const next = Math.max(0, Math.min(100, Math.floor(safeValue)));
|
||||
if (displayProgress.value === 0 && next > 0) {
|
||||
startedAt.value = Date.now();
|
||||
}
|
||||
displayProgress.value = next;
|
||||
|
||||
if (next <= 0 || !startedAt.value) {
|
||||
secondsLeft.value = 0;
|
||||
return;
|
||||
}
|
||||
if (next >= 100) {
|
||||
secondsLeft.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const elapsedMs = Date.now() - startedAt.value;
|
||||
const estimatedTotalMs = (elapsedMs * 100) / next;
|
||||
const remainMs = Math.max(0, estimatedTotalMs - elapsedMs);
|
||||
secondsLeft.value = Math.ceil(remainMs / 1000);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const cancelUpload = () => {
|
||||
emit("cancelUpload", true);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload {
|
||||
--percent: 0;
|
||||
counter-increment: percent var(--percent);
|
||||
background: #fff;
|
||||
border-radius: 40px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 0 4px 16px -1px rgba(18, 22, 33, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0 3em 2.3em;
|
||||
box-sizing: border-box;
|
||||
font-family: Roboto, Arial;
|
||||
//Safari fix
|
||||
-webkit-mask-image: -webkit-radial-gradient(white, black);
|
||||
.percent {
|
||||
background: #eeefff;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
transform-origin: 0 50%;
|
||||
overflow: hidden;
|
||||
transition: background 0.6s ease, transform 0.16s ease;
|
||||
transform: scaleX(calc(var(--percent) / 100));
|
||||
span {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 3em;
|
||||
width: 100%;
|
||||
top: 54%;
|
||||
// top: 3em;
|
||||
// height: 3em;
|
||||
opacity: 0;
|
||||
transform: translateY(0.5px);
|
||||
transition: transform 0.8s ease;
|
||||
&:before,
|
||||
&:after {
|
||||
--r: 0;
|
||||
--s: 0.5;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 3px;
|
||||
border-radius: 1px;
|
||||
background: #5628ee;
|
||||
transition: background 0.8s ease, transform 0.8s ease, height 0.3s ease;
|
||||
transform: rotate(var(--r)) scaleY(var(--s));
|
||||
}
|
||||
&:before {
|
||||
right: 0;
|
||||
width: 64%;
|
||||
transform-origin: 0 50%;
|
||||
}
|
||||
&:after {
|
||||
left: 0;
|
||||
width: 38%;
|
||||
transform-origin: 100% 50%;
|
||||
}
|
||||
}
|
||||
div {
|
||||
--x: 0;
|
||||
transform: translateX(var(--x));
|
||||
transition: transform 1s ease;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 20%;
|
||||
width: 300%;
|
||||
}
|
||||
svg {
|
||||
display: block;
|
||||
height: 12px;
|
||||
width: 100%;
|
||||
stroke-width: 4px;
|
||||
color: #5628ee;
|
||||
color: linear-gradient(
|
||||
90deg,
|
||||
#3563ff -15.99%,
|
||||
#6b52ff 43.81%,
|
||||
#9146ff 101.25%
|
||||
),
|
||||
#000;
|
||||
transition: color 0.5s ease;
|
||||
}
|
||||
}
|
||||
&.paused {
|
||||
&:not(.finished) {
|
||||
.percent {
|
||||
div {
|
||||
--x: -66.66%;
|
||||
svg {
|
||||
color: #cdd9ed;
|
||||
animation: down 0.8s linear forwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
.text {
|
||||
& > div {
|
||||
div {
|
||||
small {
|
||||
&:first-child {
|
||||
opacity: 0;
|
||||
}
|
||||
&:last-child {
|
||||
opacity: 1;
|
||||
transition-delay: 0.4s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.finished {
|
||||
.percent {
|
||||
background: #fff;
|
||||
span {
|
||||
opacity: 1;
|
||||
transform: translate(-20px, -19px);
|
||||
&:before,
|
||||
&:after {
|
||||
--s: 1;
|
||||
background: #99a3ba;
|
||||
transition: background 0.6s ease, transform 0.6s ease 0.45s;
|
||||
animation: check 0.4s linear forwards 0.6s;
|
||||
}
|
||||
&:before {
|
||||
--r: -50deg;
|
||||
}
|
||||
&:after {
|
||||
--r: 38deg;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
--y: 0;
|
||||
& > div {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
nav {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
.text {
|
||||
--y: -18px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
transform: translateY(var(--y));
|
||||
transition: transform 0.6s ease;
|
||||
strong {
|
||||
display: block;
|
||||
color: #7209b7;
|
||||
font-size: 1.3em;
|
||||
font-weight: 700;
|
||||
line-height: 200%;
|
||||
}
|
||||
& > div {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 100%;
|
||||
transform: translateY(6px);
|
||||
line-height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
transition: opacity 0.4s ease;
|
||||
small {
|
||||
white-space: nowrap;
|
||||
vertical-align: top;
|
||||
display: block;
|
||||
color: #7209b7;
|
||||
font-size: 1em;
|
||||
}
|
||||
& > small {
|
||||
width: 30px;
|
||||
text-align: center;
|
||||
&:before {
|
||||
content: counter(percent);
|
||||
}
|
||||
}
|
||||
div {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
margin-left: 4px;
|
||||
&:before {
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
display: block;
|
||||
border-radius: 50%;
|
||||
background: #99a3ba;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
margin-top: 9px;
|
||||
}
|
||||
small {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 8px;
|
||||
transition: opacity 0.3s ease;
|
||||
&:first-child {
|
||||
transition-delay: 0.4s;
|
||||
}
|
||||
&:last-child {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
nav {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
transition: opacity 0.4s ease;
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
&:not(:last-child) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
&:first-child {
|
||||
--y: 8px;
|
||||
opacity: 0;
|
||||
transform: translateY(var(--y));
|
||||
transition: opacity 0.3s ease, transform 0.4s ease;
|
||||
}
|
||||
li {
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
a {
|
||||
--r: 0deg;
|
||||
--s: 1.01;
|
||||
display: block;
|
||||
transform: rotate(var(--r)) scale(var(--s)) translateZ(0);
|
||||
transition: transform 0.6s ease, background 0.4s ease;
|
||||
svg {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: #99a3ba;
|
||||
color: #919090;
|
||||
}
|
||||
&:active {
|
||||
--s: 0.84;
|
||||
transition: transform 0.3s ease, background 0.4s ease;
|
||||
}
|
||||
&.dots {
|
||||
--r: 90deg;
|
||||
}
|
||||
&.btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
background: rgba(170, 166, 166, 0.27);
|
||||
svg {
|
||||
position: absolute;
|
||||
left: 9px;
|
||||
top: 9px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
&:hover {
|
||||
background: #e4ecfa;
|
||||
}
|
||||
&.play {
|
||||
--r: 90deg;
|
||||
svg {
|
||||
&:last-child {
|
||||
transform: scale(-1) translateZ(0);
|
||||
}
|
||||
}
|
||||
&.active {
|
||||
--r: 0;
|
||||
}
|
||||
}
|
||||
&.cancel {
|
||||
&:before,
|
||||
&:after {
|
||||
--r: -45deg;
|
||||
content: "";
|
||||
display: block;
|
||||
width: 3px;
|
||||
border-radius: 1px;
|
||||
height: 18px;
|
||||
background: #919090;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
margin: -9px 0 0 -2px;
|
||||
transform: rotate(var(--r)) scale(0.9) translateZ(0);
|
||||
}
|
||||
&:after {
|
||||
--r: 45deg;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
nav {
|
||||
ul {
|
||||
&:first-child {
|
||||
--y: 0;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes down {
|
||||
40% {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes check {
|
||||
100% {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
#3563ff -15.99%,
|
||||
#6b52ff 43.81%,
|
||||
#9146ff 101.25%
|
||||
),
|
||||
var(--Color, #000);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user