新增历史任务详情页

This commit is contained in:
aylvn 2025-03-16 16:02:03 +08:00
parent 94cf2be101
commit d08db8a76a
7 changed files with 97 additions and 384 deletions

View File

@ -13,9 +13,12 @@ export default {
copy: "Copy",
paste: "Paste",
cut: "Cut",
baseInfo: "Base Info",
createdDt: "Created Date",
updatedDt: "Updated Date",
noData: "No Data",
menu: {
task: "Task",
history: "History",

View File

@ -13,9 +13,12 @@ export default {
copy: "复制",
paste: "粘贴",
cut: "剪切",
baseInfo: "基本信息",
createdDt: "创建时间",
updatedDt: "更新时间",
noData: "暂无数据",
menu: {
task: "任务",
history: "历史记录",

View File

@ -32,7 +32,7 @@ const router = createRouter({
},
{
path: 'history',
component: () => import('@/views/task/History.vue'),
component: () => import('@/views/task/HistoryIndex.vue'),
meta: {
keepAlive: false,
title: "历史记录",

View File

@ -1,4 +1,4 @@
<template :lang="i18n.locale">
<template>
<div class="main-content">
<el-card>
<template #header>
@ -54,11 +54,11 @@
<script setup>
import { ref, reactive, inject, computed, onMounted, onBeforeUnmount, onUnmounted, watch } from 'vue'
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
import { useRouter } from 'vue-router'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
import i18n from '@/locales/i18n'
const router = useRouter()
const utils = inject('utils')
const config = useConfig()
const { t } = useI18n()
@ -171,7 +171,6 @@ function search() {
}
const handleSelectionChange = (val) => {
selectedRows.value = val
}
@ -229,120 +228,10 @@ function resetSearch() {
function toTaskInfo(taskId) {
console.log("toTaskInfo:", taskId)
router.push("/task/"+taskId)
}
</script>
<style scoped>
.output-area {
flex-grow: 1;
}
.dialog-user {
display: flex;
justify-content: center;
align-items: space-between;
margin-bottom: 16px;
}
.dialog-user .blank {
flex-grow: 1;
}
.dialog-user .content {
display: flex;
flex-direction: column;
justify-content: center;
align-items: end;
border-radius: 12px;
background-color: var(--el-fg-color);
}
.dialog-user .title {
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
align-self: flex-end;
margin: 6px 16px;
font-size: 15px;
}
.dialog-user .prompt {
/** 防止子元素宽度被设置为100%, 子元素的align-self设置除auto和stretch之外的值 */
align-self: flex-end;
margin: 0px 16px 6px 16px;
}
.dialog {
width: 100%;
}
.dialog-ai {
margin-bottom: 16px;
background-color: var(--el-fg-color);
border-radius: 12px;
}
.dialog-ai .title {
margin: 6px 12px;
font-size: 15px;
}
.input-area {
flex-grow: 0;
width: 100%;
max-height: 180px;
padding-left: 80px;
padding-right: 80px;
padding-top: 12px;
padding-bottom: 12px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.input-box {
width: 100%;
border-radius: 16px;
background-color: var(--el-fg-color);
display: flex;
justify-content: center;
align-items: center;
}
.input-style {
width: 100%;
padding-top: 12px;
padding-bottom: 12px;
}
.input-style :deep(.el-textarea__inner) {
outline: none;
border: none;
resize: none;
box-shadow: none;
}
.add-file-area {
margin-left: 16px;
margin-right: 8px;
}
.send-area {
margin-left: 8px;
margin-right: 16px;
}
.tips {
color: var(--el-text-color-secondary);
font-size: 12px;
padding-top: 10px;
}
.sub-step-time {
color: var(--el-text-color-secondary);
font-size: 12px;
}
</style>
<style scoped></style>

View File

@ -1,4 +1,4 @@
<template :lang="i18n.locale">
<template>
<div class="main-content fc">
<el-scrollbar ref="scrollRef" style="width: 100%;">
<div class="output-area" v-show="taskInfo.taskId != null">

View File

@ -1,4 +1,4 @@
<template :lang="i18n.locale">
<template>
<div class="main-content fc">
<el-scrollbar ref="scrollRef">
<div class="output-area" v-show="taskInfo.taskId != null">
@ -6,9 +6,12 @@
<div class="dialog-user">
<div class="blank"></div>
<div class="content">
<el-text class="title">
<div class="title fxc">
<img src="@/assets/img/user.png" class="user-img"/>
<el-text>
{{ t('user') }}
</el-text>
</div>
<el-text class="prompt">
{{ taskInfo.prompt }}
</el-text>
@ -84,7 +87,7 @@
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
import { FolderAdd, Promotion, User, CircleClose } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
@ -296,6 +299,7 @@ function stop() {
<style scoped>
.output-area {
flex-grow: 1;
margin-top: 10px;
}
.dialog-user {
@ -331,12 +335,17 @@ function stop() {
margin: 0px 16px 6px 16px;
}
.dialog-user .user-img{
width: 20px;
height: 20px;
margin-right: 2px;
margin-bottom: 4px;
}
.dialog {
width: 100%;
}
.dialog-ai {
margin-bottom: 16px;
background-color: var(--el-fg-color);

View File

@ -1,14 +1,20 @@
<template :lang="i18n.locale">
<template>
<div class="main-content fc">
<el-scrollbar ref="scrollRef">
<div class="output-area" v-show="taskInfo.taskId != null">
<!-- 展示模块-暂无数据 -->
<div class="no-data" v-show="baseNoData">{{ t('noData') }}</div>
<!-- 展示模块 -->
<div class="output-area" v-show="baseShow">
<div class="dialog-user">
<div class="blank"></div>
<div class="content">
<el-text class="title">
<div class="title fxc">
<img src="@/assets/img/user.png" class="user-img" />
<el-text>
{{ t('user') }}
</el-text>
</div>
<el-text class="prompt">
{{ taskInfo.prompt }}
</el-text>
@ -47,256 +53,53 @@
</div>
</div>
</div>
<div>
<el-text class="pr-10">{{ t('taskStatus.name') }}:</el-text>
<el-text>{{ taskInfo.status }}</el-text>
</div>
</div>
</el-scrollbar>
<div class="input-area">
<div class="input-box">
<el-icon @click="uploadFile" class="add-file-area" :size="24">
<FolderAdd />
</el-icon>
<el-input ref="promptEle" type="textarea" v-model="prompt" class="input-style" style="border: none;"
:autosize="{ minRows: 1, maxRows: 4 }" autofocus :placeholder="t('promptInputPlaceHolder')"
@keydown.enter="handleInputEnter" />
<el-link class="send-area">
<el-icon @click="sendPrompt" :size="24" v-show="!loading">
<Promotion />
</el-icon>
<el-icon @click="stop" :size="24" v-show="loading">
<CircleClose />
</el-icon>
</el-link>
</div>
<div>
<el-text class="tips">{{ t('openManusAgiTips') }}</el-text>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, inject, computed, onMounted, onUnmounted } from 'vue'
import { FolderAdd, Promotion, Eleme, CircleClose } from '@element-plus/icons-vue'
import { User } from '@element-plus/icons-vue'
import { useConfig } from '@/store/config'
import { useI18n } from 'vue-i18n'
import i18n from '@/locales/i18n'
const utils = inject('utils')
const config = useConfig()
const { t } = useI18n()
const prompt = ref('')
const promptEle = ref(null)
//
const viewModel = reactive({
base: 'show'
})
const eventTypes = ['think', 'tool', 'act', 'log', 'run', 'message']
const eventSource = ref(null)
const baseShow = computed(() => {
return viewModel.base == 'show' || viewModel.base == 'showMore'
})
const baseNoData = computed(() => {
return baseShow && taskInfo.value == null
})
const taskInfo = computed(() => {
return config.getCurrTask()
})
const loading = ref(false)
const scrollRef = ref(null)
// EventSource
const buildEventSource = (taskId) => {
loading.value = true
eventSource.value = new EventSource('http://localhost:5172/tasks/' + taskId + '/events')
eventSource.value.onmessage = (event) => {
console.log('Received data:', event.data)
//
}
eventTypes.forEach(type => {
eventSource.value.addEventListener(type, (event) => handleEvent(event, type))
})
eventSource.value.onerror = (error) => {
console.error('EventSource failed:', error)
//
loading.value = false
eventSource.value.close()
taskInfo.value.status = "taskStatus.failed"
utils.pop("任务执行失败", "error")
}
}
const handleEvent = (event, type) => {
console.log('Received event, type:', type, event.data)
// clearInterval(heartbeatTimer);
try {
const data = JSON.parse(event.data);
console.log("type:", type, "data:", data)
if (eventSource.value.readyState === EventSource.CLOSED) {
console.log('Connection is closed');
}
if (type == "complete" || data.status == "completed") {
console.log('task completed');
loading.value = false
eventSource.value.close()
taskInfo.value.status = "taskStatus.success"
utils.pop("任务已完成", "success")
return
}
// autoScroll(stepContainer);
buildOutput(taskInfo.value.taskId)
} catch (e) {
console.error(`Error handling ${type} event:`, e);
}
}
async function buildOutput(taskId) {
// ,
await utils.awaitGet('http://localhost:5172/tasks/' + taskId).then(data => {
console.log("task info resp:", data)
buildStepList(data.steps)
console.log("stepList:", taskInfo.value.stepList)
//
setTimeout(() => {
scrollToBottom()
}, 100)
})
}
// stepList
const buildStepList = (steps) => {
// stepList
steps.forEach((step, idx) => {
//
if (step.type == "log" && step.result.startsWith("Executing step")) {
const stepStr = step.result.replace("Executing step ", "").replace("\n", "")
const stepNo = stepStr.split("/")[0]
if (taskInfo.value.stepList.length < stepNo) {
// stepstepList
const parentStep = {
type: "log",
idx: idx,
stepNo: stepNo,
result: stepStr,
subList: [],
createdDt: utils.dateFormat(new Date())
}
taskInfo.value.stepList.push(parentStep)
return
}
} else {
//
const subStep = {
type: step.type,
idx: idx,
result: step.result,
createdDt: utils.dateFormat(new Date())
}
// stepListsubList
console.log("stepList:", taskInfo.value.stepList, "idx:", idx)
let parentStep = null
const pStepIndex = taskInfo.value.stepList.findIndex(parentStep => parentStep.idx > idx)
console.log("pStepIndex:", pStepIndex)
if (pStepIndex != -1) {
// pStep
parentStep = taskInfo.value.stepList[pStepIndex - 1]
} else {
// , stepList
parentStep = taskInfo.value.stepList[taskInfo.value.stepList.length - 1]
}
console.log("parentStep:", parentStep)
const existSubStep = parentStep.subList.find(existSubStep => existSubStep.idx == idx)
if (!existSubStep) {
// ,
parentStep.subList.push(subStep)
return
}
}
})
}
onUnmounted(() => {
// EventSource
if (eventSource.value) {
eventSource.value.close()
}
})
function handleInputEnter(event) {
console.log("handleInputEnter:", event)
event.preventDefault()
sendPrompt()
}
function uploadFile() {
utils.pop("暂不支持,开发中", "warning")
}
const scrollToBottom = () => {
if (scrollRef.value) {
console.log("scrollRef:", scrollRef.value, scrollRef.value.wrapRef)
const container = scrollRef.value.wrapRef
if (container) {
container.scrollTop = container.scrollHeight
}
}
}
//
function sendPrompt() {
//
if (eventSource.value != null) {
eventSource.value.close()
}
if (utils.isBlank(prompt.value)) {
utils.pop("Please enter a valid prompt", "error")
promptEle.value.focus()
return
}
utils.post('http://localhost:5172/tasks', { prompt: prompt.value }).then(data => {
if (!data.task_id) {
throw new Error('Invalid task ID')
}
const newTask = {
taskId: data.task_id,
prompt: prompt.value,
status: "running",
createdDt: utils.dateFormat(new Date()),
stepList: []
}
//
config.addTaskHistory(newTask)
//
prompt.value = ''
// EventSource
buildEventSource(data.task_id)
console.log("new task created:", newTask)
}).catch(error => {
console.error('Failed to create task:', error)
})
}
function stop() {
console.log("stop")
loading.value = false
eventSource.value.close()
taskInfo.value.status = "taskStatus.terminated"
utils.pop("用户终止任务", "error")
}
</script>
<style scoped>
.output-area {
flex-grow: 1;
margin-top: 10px;
margin-bottom: 10px;
}
.dialog-user {
@ -332,6 +135,12 @@ function stop() {
margin: 0px 16px 6px 16px;
}
.dialog-user .user-img {
width: 20px;
height: 20px;
margin-right: 2px;
margin-bottom: 4px;
}
.dialog {
width: 100%;