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:
XianBW
2026-03-18 14:04:52 +08:00
committed by GitHub
parent 7cd64a26fd
commit 14395488b9
141 changed files with 12852 additions and 279 deletions
+153
View File
@@ -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>
+183
View File
@@ -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>
+529
View File
@@ -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>
+145
View File
@@ -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>
+330
View File
@@ -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>
+94
View File
@@ -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 &amp; 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>
+36
View File
@@ -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>
+130
View File
@@ -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>
+338
View File
@@ -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]
// paramstooltip 的数据
// domtooltip 的 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>
+23
View File
@@ -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>
+35
View File
@@ -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>
+499
View File
@@ -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>
+240
View File
@@ -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>
+51
View File
@@ -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>
+73
View File
@@ -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>
+648
View File
@@ -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>
+39
View File
@@ -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>
+271
View File
@@ -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>
+250
View File
@@ -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>
+87
View File
@@ -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>
+39
View File
@@ -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>
+146
View File
@@ -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>
+436
View File
@@ -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>