|
|
|
<!DOCTYPE html> |
|
<html lang="zh-CN"> |
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> |
|
<title>教育AI助手开发平台</title> |
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"> |
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css"> |
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.6/js/jsplumb.min.js"></script> |
|
<style> |
|
:root { |
|
--primary-color: #4361ee; |
|
--secondary-color: #3f37c9; |
|
--accent-color: #4cc9f0; |
|
--success-color: #4caf50; |
|
--warning-color: #ff9800; |
|
--danger-color: #f44336; |
|
--light-color: #f8f9fa; |
|
--dark-color: #212529; |
|
--border-color: #dee2e6; |
|
--border-radius: 0.375rem; |
|
} |
|
|
|
body { |
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
|
display: flex; |
|
margin: 0; |
|
padding: 0; |
|
height: 100vh; |
|
background-color: #f5f7fa; |
|
color: #333; |
|
} |
|
|
|
.sidebar { |
|
width: 280px; |
|
background-color: #fff; |
|
border-right: 1px solid var(--border-color); |
|
box-shadow: 2px 0 5px rgba(0, 0, 0, 0.05); |
|
display: flex; |
|
flex-direction: column; |
|
transition: all 0.3s ease; |
|
z-index: 1000; |
|
} |
|
|
|
.sidebar-header { |
|
padding: 1.5rem; |
|
border-bottom: 1px solid var(--border-color); |
|
} |
|
|
|
.sidebar-header h2 { |
|
margin: 0; |
|
font-size: 1.5rem; |
|
font-weight: 600; |
|
color: var(--primary-color); |
|
} |
|
|
|
.sidebar-menu { |
|
list-style: none; |
|
padding: 0; |
|
margin: 0; |
|
flex: 1; |
|
overflow-y: auto; |
|
} |
|
|
|
.sidebar-menu li { |
|
padding: 1rem 1.5rem; |
|
cursor: pointer; |
|
transition: background-color 0.2s ease; |
|
display: flex; |
|
align-items: center; |
|
position: relative; |
|
} |
|
|
|
.sidebar-menu li i { |
|
margin-right: 0.75rem; |
|
font-size: 1.1rem; |
|
} |
|
|
|
.sidebar-menu li:hover { |
|
background-color: rgba(67, 97, 238, 0.05); |
|
color: var(--primary-color); |
|
} |
|
|
|
.sidebar-menu li.active { |
|
background-color: rgba(67, 97, 238, 0.1); |
|
color: var(--primary-color); |
|
font-weight: 500; |
|
} |
|
|
|
.sidebar-menu li.active::before { |
|
content: ''; |
|
position: absolute; |
|
left: 0; |
|
top: 0; |
|
bottom: 0; |
|
width: 4px; |
|
background-color: var(--primary-color); |
|
} |
|
|
|
.main-content { |
|
flex: 1; |
|
padding: 2rem; |
|
overflow-y: auto; |
|
} |
|
|
|
.content-header { |
|
margin-bottom: 2rem; |
|
} |
|
|
|
.content-header h1 { |
|
margin: 0; |
|
font-size: 1.75rem; |
|
font-weight: 600; |
|
color: #333; |
|
} |
|
|
|
.content-section { |
|
display: none; |
|
} |
|
|
|
.content-section.active { |
|
display: block; |
|
} |
|
|
|
.card { |
|
background-color: #fff; |
|
border-radius: var(--border-radius); |
|
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05); |
|
margin-bottom: 1.5rem; |
|
border: 1px solid var(--border-color); |
|
overflow: hidden; |
|
} |
|
|
|
.card-header { |
|
background-color: #fff; |
|
border-bottom: 1px solid var(--border-color); |
|
padding: 1rem 1.5rem; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
|
|
.card-header h3 { |
|
margin: 0; |
|
font-size: 1.2rem; |
|
font-weight: 600; |
|
} |
|
|
|
.card-body { |
|
padding: 1.5rem; |
|
} |
|
|
|
.form-group { |
|
margin-bottom: 1.5rem; |
|
} |
|
|
|
.form-label { |
|
font-weight: 500; |
|
margin-bottom: 0.5rem; |
|
display: block; |
|
} |
|
|
|
.form-control { |
|
border-radius: var(--border-radius); |
|
} |
|
|
|
.btn { |
|
border-radius: var(--border-radius); |
|
font-weight: 500; |
|
} |
|
|
|
.btn-primary { |
|
background-color: var(--primary-color); |
|
border-color: var(--primary-color); |
|
} |
|
|
|
.btn-primary:hover { |
|
background-color: var(--secondary-color); |
|
border-color: var(--secondary-color); |
|
} |
|
|
|
.workflow-editor { |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--border-radius); |
|
min-height: 400px; |
|
background-color: #f8f9fa; |
|
position: relative; |
|
} |
|
|
|
.knowledge-item { |
|
padding: 1rem; |
|
margin-bottom: 1rem; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--border-radius); |
|
background-color: #fff; |
|
transition: all 0.2s ease; |
|
} |
|
|
|
.knowledge-item:hover { |
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08); |
|
} |
|
|
|
.knowledge-item .header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.agent-item { |
|
padding: 1.25rem; |
|
margin-bottom: 1.5rem; |
|
border-radius: var(--border-radius); |
|
background-color: #fff; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); |
|
transition: all 0.2s ease; |
|
border-left: 4px solid var(--primary-color); |
|
} |
|
|
|
.agent-item:hover { |
|
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1); |
|
transform: translateY(-2px); |
|
} |
|
|
|
.agent-item .header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.agent-item .title { |
|
font-size: 1.2rem; |
|
font-weight: 600; |
|
margin: 0; |
|
color: #333; |
|
} |
|
|
|
.agent-item .description { |
|
color: #666; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.agent-item .meta { |
|
display: flex; |
|
font-size: 0.85rem; |
|
color: #777; |
|
margin-top: 1rem; |
|
} |
|
|
|
.agent-item .meta div { |
|
margin-right: 1.5rem; |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.agent-item .meta i { |
|
margin-right: 0.5rem; |
|
} |
|
|
|
.nav-tabs { |
|
border-bottom: 1px solid var(--border-color); |
|
margin-bottom: 1.5rem; |
|
} |
|
|
|
.nav-tabs .nav-link { |
|
margin-bottom: -1px; |
|
border: 1px solid transparent; |
|
border-top-left-radius: 0.375rem; |
|
border-top-right-radius: 0.375rem; |
|
padding: 0.75rem 1.25rem; |
|
color: #495057; |
|
} |
|
|
|
.nav-tabs .nav-link.active { |
|
color: var(--primary-color); |
|
background-color: #fff; |
|
border-color: var(--border-color) var(--border-color) #fff; |
|
font-weight: 500; |
|
} |
|
|
|
.nav-tabs .nav-link:hover { |
|
border-color: #e9ecef #e9ecef var(--border-color); |
|
} |
|
|
|
.spinner-border { |
|
width: 1.5rem; |
|
height: 1.5rem; |
|
} |
|
|
|
.distribution-link { |
|
padding: 0.75rem 1rem; |
|
background-color: #f8f9fa; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--border-radius); |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.badge { |
|
display: inline-block; |
|
padding: 0.35em 0.65em; |
|
font-size: 0.75em; |
|
font-weight: 500; |
|
line-height: 1; |
|
text-align: center; |
|
white-space: nowrap; |
|
vertical-align: baseline; |
|
border-radius: 50rem; |
|
margin-left: 0.5rem; |
|
} |
|
|
|
.badge-primary { |
|
background-color: var(--primary-color); |
|
color: #fff; |
|
} |
|
|
|
.badge-success { |
|
background-color: var(--success-color); |
|
color: #fff; |
|
} |
|
|
|
.badge-warning { |
|
background-color: var(--warning-color); |
|
color: #fff; |
|
} |
|
|
|
.plugin-option { |
|
padding: 1rem; |
|
margin-bottom: 1rem; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--border-radius); |
|
display: flex; |
|
align-items: center; |
|
cursor: pointer; |
|
transition: all 0.2s ease; |
|
} |
|
|
|
.plugin-option:hover { |
|
background-color: rgba(67, 97, 238, 0.05); |
|
border-color: var(--primary-color); |
|
} |
|
|
|
.plugin-option.selected { |
|
background-color: rgba(67, 97, 238, 0.1); |
|
border-color: var(--primary-color); |
|
} |
|
|
|
.plugin-option .icon { |
|
font-size: 1.5rem; |
|
margin-right: 1rem; |
|
color: var(--primary-color); |
|
} |
|
|
|
.plugin-option .content { |
|
flex: 1; |
|
} |
|
|
|
.plugin-option .title { |
|
font-weight: 500; |
|
margin-bottom: 0.25rem; |
|
} |
|
|
|
.plugin-option .description { |
|
font-size: 0.9rem; |
|
color: #666; |
|
} |
|
|
|
|
|
.workflow-canvas { |
|
height: 400px; |
|
background-color: #f9fbfd; |
|
border: 1px solid var(--border-color); |
|
border-radius: var(--border-radius); |
|
position: relative; |
|
} |
|
|
|
|
|
.workflow-node { |
|
position: absolute; |
|
width: 150px; |
|
height: 50px; |
|
background-color: white; |
|
border-radius: 4px; |
|
border: 1px solid #ccc; |
|
box-shadow: 0 2px 6px rgba(0,0,0,0.1); |
|
display: flex; |
|
overflow: hidden; |
|
user-select: none; |
|
z-index: 10; |
|
} |
|
|
|
.node-icon { |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
width: 30px; |
|
height: 100%; |
|
color: white; |
|
font-weight: bold; |
|
} |
|
|
|
.node-content { |
|
flex: 1; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
padding: 0 10px; |
|
font-size: 14px; |
|
color: #333; |
|
} |
|
|
|
|
|
.node-type-intent_recognition .node-icon { |
|
background-color: #673AB7; |
|
} |
|
.node-type-intent_recognition { |
|
border-color: #673AB7; |
|
background-color: #EDE7F6; |
|
} |
|
|
|
.node-type-knowledge_query .node-icon { |
|
background-color: #009688; |
|
} |
|
.node-type-knowledge_query { |
|
border-color: #009688; |
|
background-color: #E0F2F1; |
|
} |
|
|
|
.node-type-plugin_call .node-icon { |
|
background-color: #FF9800; |
|
} |
|
.node-type-plugin_call { |
|
border-color: #FF9800; |
|
background-color: #FFF3E0; |
|
} |
|
|
|
.node-type-generate_response .node-icon { |
|
background-color: #2196F3; |
|
} |
|
.node-type-generate_response { |
|
border-color: #2196F3; |
|
background-color: #E3F2FD; |
|
} |
|
|
|
|
|
.connection-label { |
|
background-color: white; |
|
padding: 3px 6px; |
|
border-radius: 3px; |
|
border: 1px solid #eee; |
|
font-size: 12px; |
|
color: #666; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<div class="sidebar"> |
|
<div class="sidebar-header"> |
|
<h2>AI助教开发平台</h2> |
|
</div> |
|
<ul class="sidebar-menu"> |
|
<li class="active" data-section="dashboard"> |
|
<i class="bi bi-grid-1x2"></i> |
|
<span>仪表盘</span> |
|
</li> |
|
<li data-section="create-agent"> |
|
<i class="bi bi-plus-circle"></i> |
|
<span>创建Agent</span> |
|
</li> |
|
<li data-section="knowledge-base"> |
|
<i class="bi bi-database"></i> |
|
<span>知识库管理</span> |
|
</li> |
|
<li data-section="agent-list"> |
|
<i class="bi bi-list-ul"></i> |
|
<span>Agent列表</span> |
|
</li> |
|
</ul> |
|
</div> |
|
|
|
<div class="main-content"> |
|
|
|
<section id="dashboard" class="content-section active"> |
|
<div class="content-header"> |
|
<h1>仪表盘</h1> |
|
<p class="text-muted mt-2">欢迎使用教育AI助手开发平台</p> |
|
</div> |
|
|
|
<div class="row"> |
|
<div class="col-md-4"> |
|
<div class="card"> |
|
<div class="card-body text-center"> |
|
<div class="display-4 mb-3"> |
|
<i class="bi bi-robot text-primary"></i> |
|
</div> |
|
<h3 id="agent-count">0</h3> |
|
<p class="text-muted">AI助手</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="col-md-4"> |
|
<div class="card"> |
|
<div class="card-body text-center"> |
|
<div class="display-4 mb-3"> |
|
<i class="bi bi-database text-success"></i> |
|
</div> |
|
<h3 id="knowledge-count">0</h3> |
|
<p class="text-muted">知识库</p> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="col-md-4"> |
|
<div class="card"> |
|
<div class="card-body text-center"> |
|
<div class="display-4 mb-3"> |
|
<i class="bi bi-share text-warning"></i> |
|
</div> |
|
<h3 id="distribution-count">0</h3> |
|
<p class="text-muted">分发链接</p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="row mt-4"> |
|
<div class="col-md-6"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h3>最近创建的Agent</h3> |
|
</div> |
|
<div class="card-body" id="recent-agents"> |
|
<div class="text-center py-4"> |
|
<div class="spinner-border text-primary" role="status"> |
|
<span class="visually-hidden">加载中...</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="col-md-6"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h3>快速入门</h3> |
|
</div> |
|
<div class="card-body"> |
|
<div class="quick-start-item mb-3 p-3 border-bottom"> |
|
<h5><i class="bi bi-1-circle me-2"></i> 创建知识库</h5> |
|
<p>上传PDF、文本等文档,构建AI助手的知识基础。</p> |
|
<button class="btn btn-sm btn-outline-primary" onclick="switchSection('knowledge-base')">开始创建</button> |
|
</div> |
|
<div class="quick-start-item mb-3 p-3 border-bottom"> |
|
<h5><i class="bi bi-2-circle me-2"></i> 创建Agent</h5> |
|
<p>设计您的AI助手,选择插件和知识库。</p> |
|
<button class="btn btn-sm btn-outline-primary" onclick="switchSection('create-agent')">开始创建</button> |
|
</div> |
|
<div class="quick-start-item p-3"> |
|
<h5><i class="bi bi-3-circle me-2"></i> 分发给学生</h5> |
|
<p>生成访问链接,分享给您的学生。</p> |
|
<button class="btn btn-sm btn-outline-primary" onclick="switchSection('agent-list')">查看Agent</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="create-agent" class="content-section"> |
|
<div class="content-header"> |
|
<h1>创建新Agent</h1> |
|
<p class="text-muted mt-2">设计您的AI助教Agent</p> |
|
</div> |
|
|
|
<div class="card"> |
|
<div class="card-body"> |
|
<ul class="nav nav-tabs" id="agentCreationTabs" role="tablist"> |
|
<li class="nav-item" role="presentation"> |
|
<button class="nav-link active" id="basic-tab" data-bs-toggle="tab" data-bs-target="#basic" type="button" role="tab" aria-controls="basic" aria-selected="true">基本信息</button> |
|
</li> |
|
<li class="nav-item" role="presentation"> |
|
<button class="nav-link" id="plugins-tab" data-bs-toggle="tab" data-bs-target="#plugins" type="button" role="tab" aria-controls="plugins" aria-selected="false">插件选择</button> |
|
</li> |
|
<li class="nav-item" role="presentation"> |
|
<button class="nav-link" id="knowledge-tab" data-bs-toggle="tab" data-bs-target="#knowledge" type="button" role="tab" aria-controls="knowledge" aria-selected="false">知识库</button> |
|
</li> |
|
<li class="nav-item" role="presentation"> |
|
<button class="nav-link" id="workflow-tab" data-bs-toggle="tab" data-bs-target="#workflow" type="button" role="tab" aria-controls="workflow" aria-selected="false">工作流设计</button> |
|
</li> |
|
</ul> |
|
|
|
<div class="tab-content" id="agentCreationTabContent"> |
|
|
|
<div class="tab-pane fade show active" id="basic" role="tabpanel" aria-labelledby="basic-tab"> |
|
<div class="mt-4"> |
|
<div class="form-group"> |
|
<label for="agent-name" class="form-label">Agent名称</label> |
|
<input type="text" class="form-control" id="agent-name" placeholder="例如:计算机组成原理助教"> |
|
</div> |
|
<div class="form-group"> |
|
<label for="agent-description" class="form-label">描述</label> |
|
<textarea class="form-control" id="agent-description" rows="3" placeholder="描述这个Agent的功能和用途..."></textarea> |
|
</div> |
|
<div class="form-group"> |
|
<label for="agent-subject" class="form-label">学科/课程名称</label> |
|
<input type="text" class="form-control" id="agent-subject" placeholder="例如:计算机组成原理"> |
|
<div class="form-text">这将影响AI助教的知识领域定位</div> |
|
</div> |
|
<div class="form-group"> |
|
<label for="agent-instructor" class="form-label">指导教师</label> |
|
<input type="text" class="form-control" id="agent-instructor" placeholder="例如:李志刚教授"> |
|
</div> |
|
<div class="form-group"> |
|
<label for="agent-type" class="form-label">Agent类型</label> |
|
<select class="form-select" id="agent-type"> |
|
<option value="educational">教育辅导</option> |
|
<option value="programming">编程辅助</option> |
|
<option value="math">数学辅导</option> |
|
<option value="general">通用助手</option> |
|
</select> |
|
</div> |
|
<button class="btn btn-primary" onclick="nextTab('plugins-tab')">下一步</button> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="tab-pane fade" id="plugins" role="tabpanel" aria-labelledby="plugins-tab"> |
|
<div class="mt-4"> |
|
<div class="mb-3"> |
|
<h5>选择Agent所需的插件</h5> |
|
<p class="text-muted">插件可以扩展Agent的能力,使其更加强大</p> |
|
</div> |
|
|
|
<div class="plugin-option" data-plugin="code" onclick="togglePluginSelection(this)"> |
|
<div class="icon"> |
|
<i class="bi bi-code-square"></i> |
|
</div> |
|
<div class="content"> |
|
<div class="title">代码执行</div> |
|
<div class="description">允许学生编写和执行Python代码,实时获取结果</div> |
|
</div> |
|
<div class="checkbox"> |
|
<input type="checkbox" class="form-check-input"> |
|
</div> |
|
</div> |
|
|
|
<div class="plugin-option" data-plugin="visualization" onclick="togglePluginSelection(this)"> |
|
<div class="icon"> |
|
<i class="bi bi-bar-chart"></i> |
|
</div> |
|
<div class="content"> |
|
<div class="title">3D可视化</div> |
|
<div class="description">生成交互式3D图形,帮助学生理解数学概念</div> |
|
</div> |
|
<div class="checkbox"> |
|
<input type="checkbox" class="form-check-input"> |
|
</div> |
|
</div> |
|
|
|
<div class="plugin-option" data-plugin="mindmap" onclick="togglePluginSelection(this)"> |
|
<div class="icon"> |
|
<i class="bi bi-diagram-3"></i> |
|
</div> |
|
<div class="content"> |
|
<div class="title">思维导图</div> |
|
<div class="description">创建结构化的思维导图,帮助学生梳理知识点</div> |
|
</div> |
|
<div class="checkbox"> |
|
<input type="checkbox" class="form-check-input"> |
|
</div> |
|
</div> |
|
|
|
<div class="d-flex justify-content-between mt-4"> |
|
<button class="btn btn-outline-secondary" onclick="nextTab('basic-tab')">上一步</button> |
|
<button class="btn btn-primary" onclick="nextTab('knowledge-tab')">下一步</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="tab-pane fade" id="knowledge" role="tabpanel" aria-labelledby="knowledge-tab"> |
|
<div class="mt-4"> |
|
<div class="mb-3"> |
|
<h5>选择Agent使用的知识库</h5> |
|
<p class="text-muted">知识库提供Agent回答问题的专业知识</p> |
|
</div> |
|
|
|
<div class="mb-3"> |
|
<div class="input-group"> |
|
<input type="text" class="form-control" placeholder="搜索知识库..." id="knowledge-search"> |
|
<button class="btn btn-outline-secondary" type="button"> |
|
<i class="bi bi-search"></i> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div id="knowledge-list" class="mb-4"> |
|
<div class="text-center py-4"> |
|
<div class="spinner-border text-primary" role="status"> |
|
<span class="visually-hidden">加载中...</span> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="d-flex justify-content-between mt-4"> |
|
<button class="btn btn-outline-secondary" onclick="nextTab('plugins-tab')">上一步</button> |
|
<button class="btn btn-primary" onclick="nextTab('workflow-tab')">下一步</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="tab-pane fade" id="workflow" role="tabpanel" aria-labelledby="workflow-tab"> |
|
<div class="mt-4"> |
|
<div class="mb-3"> |
|
<h5>设计Agent工作流</h5> |
|
<p class="text-muted">定义Agent如何处理用户的请求</p> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<button class="btn btn-primary" id="ai-workflow-btn"> |
|
<i class="bi bi-magic"></i> 使用AI自动设计工作流 |
|
</button> |
|
</div> |
|
|
|
<div class="card mb-4"> |
|
<div class="card-body p-0"> |
|
<ul class="nav nav-tabs" id="workflowViewTabs" role="tablist"> |
|
<li class="nav-item" role="presentation"> |
|
<button class="nav-link active" id="visual-tab" data-bs-toggle="tab" data-bs-target="#visual-view" type="button" role="tab">可视化视图</button> |
|
</li> |
|
<li class="nav-item" role="presentation"> |
|
<button class="nav-link" id="code-tab" data-bs-toggle="tab" data-bs-target="#code-view" type="button" role="tab">代码视图</button> |
|
</li> |
|
</ul> |
|
<div class="tab-content" id="workflowViewContent"> |
|
<div class="tab-pane fade show active" id="visual-view" role="tabpanel"> |
|
<div id="workflow-canvas" class="workflow-canvas"> |
|
<div class="p-4 text-center text-muted"> |
|
点击"使用AI自动设计工作流"按钮,或手动设计工作流 |
|
</div> |
|
</div> |
|
</div> |
|
<div class="tab-pane fade" id="code-view" role="tabpanel"> |
|
<div class="p-4"> |
|
<pre id="workflow-code" class="mb-0 p-3 bg-light border rounded">{}</pre> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="d-flex justify-content-between"> |
|
<button class="btn btn-outline-secondary" onclick="nextTab('knowledge-tab')">上一步</button> |
|
<button class="btn btn-success" id="create-agent-btn">创建Agent</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="knowledge-base" class="content-section"> |
|
<div class="content-header"> |
|
<h1>知识库管理</h1> |
|
<p class="text-muted mt-2">上传和管理Agent的知识源</p> |
|
</div> |
|
|
|
<div class="row"> |
|
<div class="col-md-4"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h3>创建知识库</h3> |
|
</div> |
|
<div class="card-body"> |
|
<form id="knowledge-form"> |
|
<div class="form-group"> |
|
<label for="knowledge-name" class="form-label">知识库名称</label> |
|
<input type="text" class="form-control" id="knowledge-name" placeholder="例如:计算机组成原理" required> |
|
</div> |
|
|
|
<div class="form-group"> |
|
<label for="knowledge-file" class="form-label">上传文件</label> |
|
<input type="file" class="form-control" id="knowledge-file" required> |
|
<div class="form-text">支持PDF、TXT、MD等格式</div> |
|
</div> |
|
|
|
<div class="progress mt-3 mb-2" style="display: none;" id="upload-progress"> |
|
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div> |
|
</div> |
|
|
|
<button type="submit" class="btn btn-primary mt-3 w-100">创建知识库</button> |
|
</form> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="col-md-8"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h3>现有知识库</h3> |
|
</div> |
|
<div class="card-body"> |
|
<div id="existing-knowledge-list"> |
|
<div class="text-center py-4"> |
|
<div class="spinner-border text-primary" role="status"> |
|
<span class="visually-hidden">加载中...</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="agent-list" class="content-section"> |
|
<div class="content-header d-flex justify-content-between align-items-center"> |
|
<div> |
|
<h1>Agent列表</h1> |
|
<p class="text-muted mt-2">管理和分发您创建的AI助手</p> |
|
</div> |
|
<div> |
|
<button class="btn btn-primary" onclick="switchSection('create-agent')"> |
|
<i class="bi bi-plus-lg"></i> 创建新Agent |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="row"> |
|
<div class="col-12"> |
|
<div class="card"> |
|
<div class="card-body"> |
|
<div id="agent-list-container"> |
|
<div class="text-center py-4"> |
|
<div class="spinner-border text-primary" role="status"> |
|
<span class="visually-hidden">加载中...</span> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
</div> |
|
|
|
|
|
<div class="modal fade" id="agentDetailModal" tabindex="-1" aria-hidden="true"> |
|
<div class="modal-dialog modal-lg"> |
|
<div class="modal-content"> |
|
<div class="modal-header"> |
|
<h5 class="modal-title" id="agent-detail-title">Agent详情</h5> |
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|
</div> |
|
<div class="modal-body" id="agent-detail-body"> |
|
|
|
</div> |
|
<div class="modal-footer"> |
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="modal fade" id="createDistributionModal" tabindex="-1" aria-hidden="true"> |
|
<div class="modal-dialog"> |
|
<div class="modal-content"> |
|
<div class="modal-header"> |
|
<h5 class="modal-title">创建分发链接</h5> |
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|
</div> |
|
<div class="modal-body"> |
|
<form id="distribution-form"> |
|
<input type="hidden" id="distribution-agent-id"> |
|
<div class="form-group mb-3"> |
|
<label for="distribution-expires" class="form-label">链接有效期</label> |
|
<select class="form-select" id="distribution-expires"> |
|
<option value="0">永不过期</option> |
|
<option value="86400">1天</option> |
|
<option value="604800">7天</option> |
|
<option value="2592000">30天</option> |
|
</select> |
|
</div> |
|
</form> |
|
</div> |
|
<div class="modal-footer"> |
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button> |
|
<button type="button" class="btn btn-primary" id="create-distribution-btn">创建链接</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="modal fade" id="showDistributionModal" tabindex="-1" aria-hidden="true"> |
|
<div class="modal-dialog"> |
|
<div class="modal-content"> |
|
<div class="modal-header"> |
|
<h5 class="modal-title">分发链接</h5> |
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
|
</div> |
|
<div class="modal-body"> |
|
<div class="alert alert-success"> |
|
链接创建成功!请复制以下链接分享给学生 |
|
</div> |
|
<div class="input-group mb-3"> |
|
<input type="text" class="form-control" id="distribution-link" readonly> |
|
<button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('distribution-link')"> |
|
<i class="bi bi-clipboard"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="modal-footer"> |
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<script> |
|
|
|
let selectedPlugins = []; |
|
let selectedKnowledgeBases = []; |
|
let currentWorkflow = { nodes: [], edges: [] }; |
|
let allKnowledgeBases = []; |
|
let allAgents = []; |
|
let jsPlumbWorkflowInstance = null; |
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
|
document.querySelectorAll('.sidebar-menu li').forEach(item => { |
|
item.addEventListener('click', function() { |
|
const section = this.getAttribute('data-section'); |
|
switchSection(section); |
|
}); |
|
}); |
|
|
|
|
|
document.getElementById('knowledge-form').addEventListener('submit', createKnowledgeBase); |
|
|
|
|
|
document.getElementById('ai-workflow-btn').addEventListener('click', generateAIWorkflow); |
|
|
|
|
|
document.getElementById('create-agent-btn').addEventListener('click', createAgent); |
|
|
|
|
|
document.getElementById('create-distribution-btn').addEventListener('click', createDistribution); |
|
|
|
|
|
initWorkflowTabs(); |
|
|
|
|
|
loadKnowledgeBases(); |
|
|
|
|
|
loadAgents(); |
|
}); |
|
|
|
|
|
function initWorkflowTabs() { |
|
|
|
document.querySelectorAll('#workflowViewTabs .nav-link, #detailWorkflowTabs .nav-link').forEach(tab => { |
|
tab.addEventListener('click', function(event) { |
|
event.preventDefault(); |
|
|
|
|
|
const targetId = this.getAttribute('data-bs-target'); |
|
const targetPane = document.querySelector(targetId); |
|
|
|
if (!targetPane) return; |
|
|
|
|
|
const tabs = this.closest('.nav-tabs').querySelectorAll('.nav-link'); |
|
tabs.forEach(t => t.classList.remove('active')); |
|
|
|
|
|
this.classList.add('active'); |
|
|
|
|
|
const tabContent = targetPane.closest('.tab-content'); |
|
if (tabContent) { |
|
tabContent.querySelectorAll('.tab-pane').forEach(pane => { |
|
pane.classList.remove('show', 'active'); |
|
}); |
|
} |
|
|
|
|
|
targetPane.classList.add('show', 'active'); |
|
|
|
|
|
if (targetId === '#visual-view' || targetId === '#detail-visual-view') { |
|
const canvasId = targetId === '#visual-view' ? 'workflow-canvas' : 'detail-workflow-canvas'; |
|
|
|
|
|
let workflowData = currentWorkflow; |
|
if (targetId === '#detail-visual-view') { |
|
const modalBody = document.getElementById('agent-detail-body'); |
|
if (modalBody && modalBody.dataset.agentWorkflow) { |
|
try { |
|
workflowData = JSON.parse(modalBody.dataset.agentWorkflow); |
|
} catch (e) { |
|
console.error('解析工作流数据出错:', e); |
|
} |
|
} |
|
} |
|
|
|
|
|
setTimeout(() => { |
|
renderWorkflowVisualization(workflowData, canvasId); |
|
}, 200); |
|
} |
|
}); |
|
}); |
|
} |
|
|
|
|
|
function switchSection(section) { |
|
|
|
document.querySelectorAll('.content-section').forEach(item => { |
|
item.classList.remove('active'); |
|
}); |
|
|
|
|
|
document.getElementById(section).classList.add('active'); |
|
|
|
|
|
document.querySelectorAll('.sidebar-menu li').forEach(item => { |
|
item.classList.remove('active'); |
|
if (item.getAttribute('data-section') === section) { |
|
item.classList.add('active'); |
|
} |
|
}); |
|
|
|
|
|
if (section === 'create-agent') { |
|
|
|
const activeTab = document.querySelector('#agentCreationTabs .nav-link.active'); |
|
if (activeTab && activeTab.id === 'workflow-tab') { |
|
|
|
const activeView = document.querySelector('#workflowViewContent .tab-pane.active'); |
|
if (activeView && activeView.id === 'visual-view') { |
|
|
|
setTimeout(() => { |
|
renderWorkflowVisualization(currentWorkflow, 'workflow-canvas'); |
|
}, 200); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
function nextTab(tabId) { |
|
const tab = document.getElementById(tabId); |
|
const bsTab = new bootstrap.Tab(tab); |
|
bsTab.show(); |
|
} |
|
|
|
|
|
function togglePluginSelection(element) { |
|
const pluginId = element.getAttribute('data-plugin'); |
|
const checkbox = element.querySelector('input[type="checkbox"]'); |
|
|
|
if (element.classList.contains('selected')) { |
|
element.classList.remove('selected'); |
|
checkbox.checked = false; |
|
selectedPlugins = selectedPlugins.filter(id => id !== pluginId); |
|
} else { |
|
element.classList.add('selected'); |
|
checkbox.checked = true; |
|
selectedPlugins.push(pluginId); |
|
} |
|
} |
|
|
|
|
|
function toggleKnowledgeSelection(element) { |
|
const knowledgeId = element.getAttribute('data-id'); |
|
|
|
if (element.classList.contains('selected')) { |
|
element.classList.remove('selected'); |
|
element.querySelector('input[type="checkbox"]').checked = false; |
|
selectedKnowledgeBases = selectedKnowledgeBases.filter(id => id !== knowledgeId); |
|
} else { |
|
element.classList.add('selected'); |
|
element.querySelector('input[type="checkbox"]').checked = true; |
|
selectedKnowledgeBases.push(knowledgeId); |
|
} |
|
} |
|
|
|
|
|
async function loadKnowledgeBases() { |
|
try { |
|
const response = await fetch('/api/knowledge'); |
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
allKnowledgeBases = result.data || []; |
|
|
|
|
|
document.getElementById('knowledge-count').textContent = allKnowledgeBases.length; |
|
|
|
|
|
const knowledgeListElement = document.getElementById('knowledge-list'); |
|
if (knowledgeListElement) { |
|
if (allKnowledgeBases.length === 0) { |
|
knowledgeListElement.innerHTML = ` |
|
<div class="alert alert-info"> |
|
<i class="bi bi-info-circle me-2"></i> |
|
暂无知识库,请先创建知识库 |
|
</div> |
|
<div class="text-center"> |
|
<button class="btn btn-primary" onclick="switchSection('knowledge-base')"> |
|
创建知识库 |
|
</button> |
|
</div> |
|
`; |
|
} else { |
|
knowledgeListElement.innerHTML = ''; |
|
|
|
allKnowledgeBases.forEach(knowledge => { |
|
const item = document.createElement('div'); |
|
item.className = 'plugin-option'; |
|
item.setAttribute('data-id', knowledge.id); |
|
item.setAttribute('onclick', 'toggleKnowledgeSelection(this)'); |
|
|
|
item.innerHTML = ` |
|
<div class="icon"> |
|
<i class="bi bi-journal-text"></i> |
|
</div> |
|
<div class="content"> |
|
<div class="title">${knowledge.name}</div> |
|
<div class="description"> |
|
<small>${knowledge.fileCount}个文件</small> |
|
</div> |
|
</div> |
|
<div class="checkbox"> |
|
<input type="checkbox" class="form-check-input"> |
|
</div> |
|
`; |
|
|
|
knowledgeListElement.appendChild(item); |
|
}); |
|
} |
|
} |
|
|
|
|
|
const existingKnowledgeList = document.getElementById('existing-knowledge-list'); |
|
if (existingKnowledgeList) { |
|
if (allKnowledgeBases.length === 0) { |
|
existingKnowledgeList.innerHTML = ` |
|
<div class="alert alert-info"> |
|
<i class="bi bi-info-circle me-2"></i> |
|
暂无知识库,请创建第一个知识库 |
|
</div> |
|
`; |
|
} else { |
|
existingKnowledgeList.innerHTML = ''; |
|
|
|
allKnowledgeBases.forEach(knowledge => { |
|
const item = document.createElement('div'); |
|
item.className = 'knowledge-item'; |
|
|
|
let fileListHtml = ''; |
|
if (knowledge.files && knowledge.files.length > 0) { |
|
fileListHtml = '<div class="mt-3"><small class="text-muted">包含文件:</small><ul class="list-unstyled ms-3">'; |
|
knowledge.files.forEach(file => { |
|
fileListHtml += `<li><small><i class="bi bi-file-text me-1"></i>${file}</small></li>`; |
|
}); |
|
fileListHtml += '</ul></div>'; |
|
} |
|
|
|
item.innerHTML = ` |
|
<div class="header"> |
|
<h5>${knowledge.name}</h5> |
|
<div> |
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteKnowledgeBase('${knowledge.id}')"> |
|
<i class="bi bi-trash"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div> |
|
<span class="badge bg-primary">${knowledge.fileCount}个文件</span> |
|
</div> |
|
${fileListHtml} |
|
<div class="mt-3"> |
|
<form class="upload-form" data-id="${knowledge.id}"> |
|
<div class="input-group"> |
|
<input type="file" class="form-control form-control-sm" required> |
|
<button type="submit" class="btn btn-sm btn-outline-primary">添加文件</button> |
|
</div> |
|
</form> |
|
</div> |
|
`; |
|
|
|
existingKnowledgeList.appendChild(item); |
|
}); |
|
|
|
|
|
document.querySelectorAll('.upload-form').forEach(form => { |
|
form.addEventListener('submit', function(event) { |
|
event.preventDefault(); |
|
addFileToKnowledgeBase(form); |
|
}); |
|
}); |
|
} |
|
} |
|
} else { |
|
console.error('加载知识库失败:', result.message); |
|
} |
|
} catch (error) { |
|
console.error('加载知识库出错:', error); |
|
} |
|
} |
|
|
|
|
|
async function createKnowledgeBase(event) { |
|
event.preventDefault(); |
|
|
|
const nameInput = document.getElementById('knowledge-name'); |
|
const fileInput = document.getElementById('knowledge-file'); |
|
const progressBar = document.getElementById('upload-progress'); |
|
|
|
if (!nameInput.value || !fileInput.files[0]) { |
|
alert('请填写知识库名称并选择文件'); |
|
return; |
|
} |
|
|
|
|
|
progressBar.style.display = 'block'; |
|
progressBar.querySelector('.progress-bar').style.width = '0%'; |
|
|
|
const formData = new FormData(); |
|
formData.append('name', nameInput.value); |
|
formData.append('file', fileInput.files[0]); |
|
|
|
try { |
|
const response = await fetch('/api/knowledge', { |
|
method: 'POST', |
|
body: formData |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
|
|
pollProgress(result.task_id, progressBar); |
|
} else { |
|
alert('创建知识库失败: ' + result.message); |
|
progressBar.style.display = 'none'; |
|
} |
|
} catch (error) { |
|
console.error('创建知识库出错:', error); |
|
alert('创建知识库出错,请查看控制台日志'); |
|
progressBar.style.display = 'none'; |
|
} |
|
} |
|
|
|
|
|
async function addFileToKnowledgeBase(form) { |
|
const knowledgeId = form.getAttribute('data-id'); |
|
const fileInput = form.querySelector('input[type="file"]'); |
|
const submitBtn = form.querySelector('button[type="submit"]'); |
|
|
|
if (!fileInput.files[0]) { |
|
alert('请选择文件'); |
|
return; |
|
} |
|
|
|
|
|
submitBtn.disabled = true; |
|
submitBtn.textContent = '上传中...'; |
|
|
|
const formData = new FormData(); |
|
formData.append('file', fileInput.files[0]); |
|
|
|
try { |
|
const response = await fetch(`/api/knowledge/${knowledgeId}/documents`, { |
|
method: 'POST', |
|
body: formData |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
alert('文件上传成功,开始处理'); |
|
|
|
pollProgressSimple(result.task_id, submitBtn); |
|
} else { |
|
alert('添加文件失败: ' + result.message); |
|
submitBtn.disabled = false; |
|
submitBtn.textContent = '添加文件'; |
|
} |
|
} catch (error) { |
|
console.error('添加文件出错:', error); |
|
alert('添加文件出错,请查看控制台日志'); |
|
submitBtn.disabled = false; |
|
submitBtn.textContent = '添加文件'; |
|
} |
|
} |
|
|
|
|
|
async function deleteKnowledgeBase(knowledgeId) { |
|
if (!confirm('确定要删除这个知识库吗?此操作不可恢复。')) { |
|
return; |
|
} |
|
|
|
try { |
|
const response = await fetch(`/api/knowledge/${knowledgeId}`, { |
|
method: 'DELETE' |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
alert('知识库删除成功!'); |
|
loadKnowledgeBases(); |
|
} else { |
|
alert('删除知识库失败: ' + result.message); |
|
} |
|
} catch (error) { |
|
console.error('删除知识库出错:', error); |
|
alert('删除知识库出错,请查看控制台日志'); |
|
} |
|
} |
|
|
|
|
|
function pollProgress(taskId, progressElement) { |
|
const progressBar = progressElement.querySelector('.progress-bar'); |
|
|
|
const interval = setInterval(async () => { |
|
try { |
|
const response = await fetch(`/api/progress/${taskId}`); |
|
const data = await response.json(); |
|
|
|
if (data.success) { |
|
const progressData = data.data; |
|
|
|
|
|
progressBar.style.width = progressData.progress + '%'; |
|
|
|
|
|
if (progressData.progress >= 100 || progressData.error) { |
|
clearInterval(interval); |
|
|
|
if (progressData.error) { |
|
alert('处理失败: ' + progressData.status); |
|
} else { |
|
alert(`知识库处理成功! 已处理${progressData.docCount}个文档片段`); |
|
} |
|
|
|
|
|
progressElement.style.display = 'none'; |
|
|
|
|
|
document.getElementById('knowledge-name').value = ''; |
|
document.getElementById('knowledge-file').value = ''; |
|
|
|
|
|
loadKnowledgeBases(); |
|
} |
|
} |
|
} catch (error) { |
|
console.error('获取进度信息出错:', error); |
|
} |
|
}, 1000); |
|
} |
|
|
|
|
|
function pollProgressSimple(taskId, buttonElement) { |
|
const originalText = buttonElement.textContent; |
|
|
|
const interval = setInterval(async () => { |
|
try { |
|
const response = await fetch(`/api/progress/${taskId}`); |
|
const data = await response.json(); |
|
|
|
if (data.success) { |
|
const progressData = data.data; |
|
|
|
|
|
if (progressData.progress >= 100 || progressData.error) { |
|
clearInterval(interval); |
|
|
|
if (progressData.error) { |
|
alert('处理失败: ' + progressData.status); |
|
} else { |
|
alert(`处理成功! 已处理${progressData.docCount}个文档片段`); |
|
} |
|
|
|
|
|
buttonElement.disabled = false; |
|
buttonElement.textContent = originalText; |
|
|
|
|
|
loadKnowledgeBases(); |
|
} |
|
} |
|
} catch (error) { |
|
console.error('获取进度信息出错:', error); |
|
} |
|
}, 1000); |
|
} |
|
|
|
|
|
async function generateAIWorkflow() { |
|
const description = document.getElementById('agent-description').value; |
|
const subject = document.getElementById('agent-subject').value || document.getElementById('agent-name').value; |
|
|
|
if (!description) { |
|
alert('请先填写Agent描述,以便AI生成工作流'); |
|
nextTab('basic-tab'); |
|
return; |
|
} |
|
|
|
|
|
const workflowCanvas = document.getElementById('workflow-canvas'); |
|
workflowCanvas.innerHTML = ` |
|
<div class="text-center py-5"> |
|
<div class="spinner-border text-primary" role="status"> |
|
<span class="visually-hidden">生成中...</span> |
|
</div> |
|
<p class="mt-3">AI正在设计工作流,请稍候...</p> |
|
</div> |
|
`; |
|
|
|
try { |
|
const response = await fetch('/api/agent/ai-assist', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ |
|
description: description, |
|
subject: subject, |
|
plugins: selectedPlugins, |
|
knowledge_bases: selectedKnowledgeBases |
|
}) |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
|
|
currentWorkflow = result.workflow; |
|
|
|
|
|
updateWorkflowCodeView(currentWorkflow); |
|
|
|
|
|
let recommendationHtml = ''; |
|
let recommendationsApplied = false; |
|
|
|
|
|
if (result.recommended_knowledge_bases && result.recommended_knowledge_bases.length > 0) { |
|
|
|
selectedKnowledgeBases = result.recommended_knowledge_bases; |
|
|
|
|
|
document.querySelectorAll('#knowledge-list .plugin-option').forEach(item => { |
|
const kbId = item.getAttribute('data-id'); |
|
const selected = selectedKnowledgeBases.includes(kbId); |
|
|
|
item.classList.toggle('selected', selected); |
|
const checkbox = item.querySelector('input[type="checkbox"]'); |
|
if (checkbox) checkbox.checked = selected; |
|
}); |
|
|
|
recommendationsApplied = true; |
|
recommendationHtml += ` |
|
<div class="mb-3"> |
|
<h6>AI推荐的知识库:</h6> |
|
<ul> |
|
${selectedKnowledgeBases.map(kb => `<li>${kb}</li>`).join('')} |
|
</ul> |
|
</div> |
|
`; |
|
} |
|
|
|
|
|
if (result.recommended_plugins && result.recommended_plugins.length > 0) { |
|
|
|
selectedPlugins = result.recommended_plugins; |
|
|
|
|
|
document.querySelectorAll('[data-plugin]').forEach(item => { |
|
const pluginId = item.getAttribute('data-plugin'); |
|
const selected = selectedPlugins.includes(pluginId); |
|
|
|
item.classList.toggle('selected', selected); |
|
const checkbox = item.querySelector('input[type="checkbox"]'); |
|
if (checkbox) checkbox.checked = selected; |
|
}); |
|
|
|
recommendationsApplied = true; |
|
recommendationHtml += ` |
|
<div class="mb-3"> |
|
<h6>AI推荐的插件:</h6> |
|
<ul> |
|
${selectedPlugins.map(plugin => { |
|
let pluginName = plugin; |
|
if (plugin === 'code') pluginName = '代码执行'; |
|
if (plugin === 'visualization') pluginName = '3D可视化'; |
|
if (plugin === 'mindmap') pluginName = '思维导图'; |
|
return `<li>${pluginName}</li>`; |
|
}).join('')} |
|
</ul> |
|
</div> |
|
`; |
|
} |
|
|
|
|
|
if (recommendationsApplied) { |
|
workflowCanvas.innerHTML = ` |
|
<div class="alert alert-success mb-3"> |
|
<i class="bi bi-check-circle-fill me-2"></i> |
|
AI已成功设计工作流并推荐了知识库和插件 |
|
</div> |
|
${recommendationHtml} |
|
<div id="workflow-visualization-container"></div> |
|
`; |
|
} else { |
|
workflowCanvas.innerHTML = ` |
|
<div class="alert alert-info mb-3"> |
|
<i class="bi bi-info-circle-fill me-2"></i> |
|
AI已设计工作流但未推荐额外的知识库或插件 |
|
</div> |
|
<div id="workflow-visualization-container"></div> |
|
`; |
|
} |
|
|
|
|
|
setTimeout(() => { |
|
renderWorkflowVisualization(currentWorkflow, 'workflow-canvas'); |
|
}, 200); |
|
|
|
} else { |
|
workflowCanvas.innerHTML = ` |
|
<div class="alert alert-danger m-3"> |
|
<i class="bi bi-exclamation-triangle-fill me-2"></i> |
|
生成工作流失败: ${result.message} |
|
</div> |
|
`; |
|
} |
|
} catch (error) { |
|
console.error('生成工作流出错:', error); |
|
workflowCanvas.innerHTML = ` |
|
<div class="alert alert-danger m-3"> |
|
<i class="bi bi-exclamation-triangle-fill me-2"></i> |
|
生成工作流时发生错误,请重试 |
|
</div> |
|
`; |
|
} |
|
} |
|
|
|
|
|
function updateWorkflowCodeView(workflow) { |
|
const workflowCodeElement = document.getElementById('workflow-code'); |
|
if (workflowCodeElement) { |
|
workflowCodeElement.textContent = JSON.stringify(workflow, null, 2); |
|
} |
|
} |
|
|
|
|
|
async function createAgent() { |
|
const name = document.getElementById('agent-name').value; |
|
const description = document.getElementById('agent-description').value; |
|
const subject = document.getElementById('agent-subject').value || name; |
|
const instructor = document.getElementById('agent-instructor').value || '教师'; |
|
const type = document.getElementById('agent-type').value; |
|
|
|
if (!name) { |
|
alert('请填写Agent名称'); |
|
nextTab('basic-tab'); |
|
return; |
|
} |
|
|
|
|
|
const agentData = { |
|
name: name, |
|
description: description, |
|
subject: subject, |
|
instructor: instructor, |
|
type: type, |
|
plugins: selectedPlugins, |
|
knowledge_bases: selectedKnowledgeBases, |
|
workflow: currentWorkflow |
|
}; |
|
|
|
try { |
|
const response = await fetch('/api/agent/create', { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify(agentData) |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
alert(`Agent '${name}' 创建成功!`); |
|
|
|
|
|
document.getElementById('agent-name').value = ''; |
|
document.getElementById('agent-description').value = ''; |
|
document.getElementById('agent-subject').value = ''; |
|
document.getElementById('agent-instructor').value = ''; |
|
document.getElementById('agent-type').value = 'educational'; |
|
selectedPlugins = []; |
|
selectedKnowledgeBases = []; |
|
currentWorkflow = { nodes: [], edges: [] }; |
|
|
|
|
|
document.querySelectorAll('.plugin-option').forEach(item => { |
|
item.classList.remove('selected'); |
|
item.querySelector('input[type="checkbox"]').checked = false; |
|
}); |
|
|
|
|
|
document.getElementById('workflow-canvas').innerHTML = ` |
|
<div class="p-4 text-center text-muted"> |
|
点击"使用AI自动设计工作流"按钮,或手动设计工作流 |
|
</div> |
|
`; |
|
|
|
|
|
updateWorkflowCodeView(currentWorkflow); |
|
|
|
|
|
switchSection('agent-list'); |
|
|
|
|
|
loadAgents(); |
|
} else { |
|
alert('创建Agent失败: ' + result.message); |
|
} |
|
} catch (error) { |
|
console.error('创建Agent出错:', error); |
|
alert('创建Agent出错,请查看控制台日志'); |
|
} |
|
} |
|
|
|
|
|
async function loadAgents() { |
|
try { |
|
const response = await fetch('/api/agent/list'); |
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
allAgents = result.agents || []; |
|
|
|
|
|
document.getElementById('agent-count').textContent = allAgents.length; |
|
|
|
|
|
let distributionCount = 0; |
|
allAgents.forEach(agent => { |
|
distributionCount += agent.distribution_count || 0; |
|
}); |
|
document.getElementById('distribution-count').textContent = distributionCount; |
|
|
|
|
|
updateRecentAgents(); |
|
|
|
|
|
updateAgentListPage(); |
|
} else { |
|
console.error('加载Agent列表失败:', result.message); |
|
} |
|
} catch (error) { |
|
console.error('加载Agent列表出错:', error); |
|
} |
|
} |
|
|
|
|
|
function updateRecentAgents() { |
|
const recentAgentsElement = document.getElementById('recent-agents'); |
|
|
|
if (recentAgentsElement) { |
|
if (allAgents.length === 0) { |
|
recentAgentsElement.innerHTML = ` |
|
<div class="alert alert-info"> |
|
<i class="bi bi-info-circle me-2"></i> |
|
您还没有创建Agent |
|
</div> |
|
<div class="text-center"> |
|
<button class="btn btn-primary" onclick="switchSection('create-agent')"> |
|
创建第一个Agent |
|
</button> |
|
</div> |
|
`; |
|
} else { |
|
|
|
const recentAgents = allAgents.slice(0, 3); |
|
|
|
recentAgentsElement.innerHTML = ''; |
|
|
|
recentAgents.forEach(agent => { |
|
const item = document.createElement('div'); |
|
item.className = 'agent-item'; |
|
|
|
const createdDate = new Date(agent.created_at * 1000); |
|
const formattedDate = createdDate.toLocaleDateString(); |
|
|
|
|
|
const subjectInfo = agent.subject ? `<small class="text-muted me-3"><i class="bi bi-book me-1"></i>${agent.subject}</small>` : ''; |
|
const instructorInfo = agent.instructor ? `<small class="text-muted"><i class="bi bi-person me-1"></i>${agent.instructor}</small>` : ''; |
|
|
|
item.innerHTML = ` |
|
<div class="header"> |
|
<h5 class="title">${agent.name}</h5> |
|
<span class="badge badge-primary">${agent.type || 'educational'}</span> |
|
</div> |
|
<div class="description">${agent.description || '暂无描述'}</div> |
|
<div class="mt-2"> |
|
${subjectInfo} |
|
${instructorInfo} |
|
</div> |
|
<div class="meta"> |
|
<div> |
|
<i class="bi bi-calendar3"></i> |
|
${formattedDate} |
|
</div> |
|
<div> |
|
<i class="bi bi-share"></i> |
|
${agent.distribution_count || 0}个分发 |
|
</div> |
|
</div> |
|
`; |
|
|
|
recentAgentsElement.appendChild(item); |
|
}); |
|
} |
|
} |
|
} |
|
|
|
|
|
function updateAgentListPage() { |
|
const agentListContainer = document.getElementById('agent-list-container'); |
|
|
|
if (agentListContainer) { |
|
if (allAgents.length === 0) { |
|
agentListContainer.innerHTML = ` |
|
<div class="alert alert-info"> |
|
<i class="bi bi-info-circle me-2"></i> |
|
您还没有创建Agent |
|
</div> |
|
<div class="text-center"> |
|
<button class="btn btn-primary" onclick="switchSection('create-agent')"> |
|
创建第一个Agent |
|
</button> |
|
</div> |
|
`; |
|
} else { |
|
agentListContainer.innerHTML = ''; |
|
|
|
allAgents.forEach(agent => { |
|
const item = document.createElement('div'); |
|
item.className = 'agent-item'; |
|
|
|
const createdDate = new Date(agent.created_at * 1000); |
|
const formattedDate = createdDate.toLocaleDateString(); |
|
|
|
|
|
let pluginsHtml = ''; |
|
if (agent.plugins && agent.plugins.length > 0) { |
|
pluginsHtml = '<div class="mt-2"><small class="text-muted me-2">插件:</small>'; |
|
agent.plugins.forEach(plugin => { |
|
let pluginName = '未知'; |
|
let pluginIcon = 'puzzle'; |
|
|
|
if (plugin === 'code') { |
|
pluginName = '代码执行'; |
|
pluginIcon = 'code-square'; |
|
} else if (plugin === 'visualization') { |
|
pluginName = '3D可视化'; |
|
pluginIcon = 'bar-chart'; |
|
} else if (plugin === 'mindmap') { |
|
pluginName = '思维导图'; |
|
pluginIcon = 'diagram-3'; |
|
} |
|
|
|
pluginsHtml += ` |
|
<span class="badge bg-light text-dark me-1"> |
|
<i class="bi bi-${pluginIcon} me-1"></i> |
|
${pluginName} |
|
</span> |
|
`; |
|
}); |
|
pluginsHtml += '</div>'; |
|
} |
|
|
|
let knowledgeHtml = ''; |
|
if (agent.knowledge_bases && agent.knowledge_bases.length > 0) { |
|
knowledgeHtml = '<div class="mt-2"><small class="text-muted me-2">知识库:</small>'; |
|
agent.knowledge_bases.forEach(kb => { |
|
|
|
const knowledge = allKnowledgeBases.find(k => k.id === kb); |
|
const knowledgeName = knowledge ? knowledge.name : kb; |
|
|
|
knowledgeHtml += ` |
|
<span class="badge bg-light text-dark me-1"> |
|
<i class="bi bi-journal-text me-1"></i> |
|
${knowledgeName} |
|
</span> |
|
`; |
|
}); |
|
knowledgeHtml += '</div>'; |
|
} |
|
|
|
|
|
const subjectInfo = agent.subject ? `<small class="text-muted me-3"><i class="bi bi-book me-1"></i>${agent.subject}</small>` : ''; |
|
const instructorInfo = agent.instructor ? `<small class="text-muted"><i class="bi bi-person me-1"></i>${agent.instructor}</small>` : ''; |
|
|
|
item.innerHTML = ` |
|
<div class="header"> |
|
<h5 class="title">${agent.name}</h5> |
|
<div> |
|
<button class="btn btn-sm btn-primary me-1" onclick="showCreateDistributionModal('${agent.id}')"> |
|
<i class="bi bi-share"></i> 分发 |
|
</button> |
|
<button class="btn btn-sm btn-outline-secondary me-1" onclick="showAgentDetail('${agent.id}')"> |
|
<i class="bi bi-info-circle"></i> 详情 |
|
</button> |
|
<button class="btn btn-sm btn-outline-danger" onclick="deleteAgent('${agent.id}')"> |
|
<i class="bi bi-trash"></i> |
|
</button> |
|
</div> |
|
</div> |
|
<div class="description">${agent.description || '暂无描述'}</div> |
|
<div class="mt-2"> |
|
${subjectInfo} |
|
${instructorInfo} |
|
</div> |
|
${pluginsHtml} |
|
${knowledgeHtml} |
|
<div class="meta"> |
|
<div> |
|
<i class="bi bi-calendar3"></i> |
|
${formattedDate} |
|
</div> |
|
<div> |
|
<i class="bi bi-share"></i> |
|
${agent.distribution_count || 0}个分发 |
|
</div> |
|
<div> |
|
<i class="bi bi-eye"></i> |
|
${agent.usage_count || 0}次使用 |
|
</div> |
|
</div> |
|
`; |
|
|
|
agentListContainer.appendChild(item); |
|
}); |
|
} |
|
} |
|
} |
|
|
|
|
|
async function showAgentDetail(agentId) { |
|
try { |
|
|
|
const response = await fetch(`/api/agent/${agentId}`); |
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
const agent = result.agent; |
|
|
|
|
|
const modalTitle = document.getElementById('agent-detail-title'); |
|
const modalBody = document.getElementById('agent-detail-body'); |
|
|
|
modalTitle.textContent = agent.name; |
|
|
|
|
|
const createdDate = new Date(agent.created_at * 1000); |
|
const formattedDate = createdDate.toLocaleString(); |
|
|
|
|
|
modalBody.innerHTML = ` |
|
<div class="mb-3"> |
|
<h6>描述</h6> |
|
<p>${agent.description || '暂无描述'}</p> |
|
</div> |
|
|
|
<div class="row mb-3"> |
|
<div class="col-md-6"> |
|
<h6>学科/课程</h6> |
|
<p>${agent.subject || agent.name}</p> |
|
</div> |
|
<div class="col-md-6"> |
|
<h6>指导教师</h6> |
|
<p>${agent.instructor || '教师'}</p> |
|
</div> |
|
</div> |
|
|
|
<div class="row mb-3"> |
|
<div class="col-md-6"> |
|
<h6>创建时间</h6> |
|
<p>${formattedDate}</p> |
|
</div> |
|
<div class="col-md-6"> |
|
<h6>使用次数</h6> |
|
<p>${agent.stats ? agent.stats.usage_count || 0 : 0}次</p> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-3"> |
|
<h6>插件</h6> |
|
${getPluginsHtml(agent.plugins)} |
|
</div> |
|
|
|
<div class="mb-3"> |
|
<h6>知识库</h6> |
|
${getKnowledgeBasesHtml(agent.knowledge_bases)} |
|
</div> |
|
|
|
<div class="mb-3"> |
|
<h6>分发链接</h6> |
|
${getDistributionsHtml(agent.distributions, agent.id)} |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<h6>工作流配置</h6> |
|
<ul class="nav nav-tabs mb-3" id="detailWorkflowTabs" role="tablist"> |
|
<li class="nav-item" role="presentation"> |
|
<button class="nav-link active" id="detail-visual-tab" data-bs-toggle="tab" data-bs-target="#detail-visual-view" type="button" role="tab">可视化视图</button> |
|
</li> |
|
<li class="nav-item" role="presentation"> |
|
<button class="nav-link" id="detail-code-tab" data-bs-toggle="tab" data-bs-target="#detail-code-view" type="button" role="tab">代码视图</button> |
|
</li> |
|
</ul> |
|
<div class="tab-content" id="detailWorkflowContent"> |
|
<div class="tab-pane fade show active" id="detail-visual-view" role="tabpanel"> |
|
<div id="detail-workflow-canvas" class="workflow-canvas"></div> |
|
</div> |
|
<div class="tab-pane fade" id="detail-code-view" role="tabpanel"> |
|
<div class="p-3 bg-light border rounded"> |
|
<pre class="mb-0">${JSON.stringify(agent.workflow, null, 2)}</pre> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
|
|
|
|
modalBody.dataset.agentWorkflow = JSON.stringify(agent.workflow); |
|
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('agentDetailModal')); |
|
modal.show(); |
|
|
|
|
|
document.getElementById('agentDetailModal').addEventListener('shown.bs.modal', function() { |
|
|
|
document.querySelectorAll('#detailWorkflowTabs .nav-link').forEach(tab => { |
|
tab.addEventListener('click', function(event) { |
|
event.preventDefault(); |
|
|
|
const targetId = this.getAttribute('data-bs-target'); |
|
const targetPane = document.querySelector(targetId); |
|
|
|
|
|
document.querySelectorAll('#detailWorkflowTabs .nav-link').forEach(t => |
|
t.classList.remove('active')); |
|
this.classList.add('active'); |
|
|
|
|
|
document.querySelectorAll('#detailWorkflowContent .tab-pane').forEach(p => |
|
p.classList.remove('show', 'active')); |
|
targetPane.classList.add('show', 'active'); |
|
|
|
|
|
if (targetId === '#detail-visual-view') { |
|
setTimeout(() => { |
|
const workflow = JSON.parse(modalBody.dataset.agentWorkflow); |
|
renderWorkflowVisualization(workflow, 'detail-workflow-canvas'); |
|
}, 200); |
|
} |
|
}); |
|
}); |
|
|
|
|
|
setTimeout(() => { |
|
const workflow = JSON.parse(modalBody.dataset.agentWorkflow); |
|
renderWorkflowVisualization(workflow, 'detail-workflow-canvas'); |
|
}, 200); |
|
}); |
|
} else { |
|
alert('获取Agent详情失败: ' + result.message); |
|
} |
|
} catch (error) { |
|
console.error('获取Agent详情出错:', error); |
|
alert('获取Agent详情出错,请查看控制台日志'); |
|
} |
|
} |
|
|
|
|
|
function getPluginsHtml(plugins) { |
|
if (!plugins || plugins.length === 0) { |
|
return '<p>无</p>'; |
|
} |
|
|
|
let html = '<ul>'; |
|
plugins.forEach(plugin => { |
|
let pluginName = '未知'; |
|
|
|
if (plugin === 'code') { |
|
pluginName = '代码执行'; |
|
} else if (plugin === 'visualization') { |
|
pluginName = '3D可视化'; |
|
} else if (plugin === 'mindmap') { |
|
pluginName = '思维导图'; |
|
} |
|
|
|
html += `<li>${pluginName}</li>`; |
|
}); |
|
html += '</ul>'; |
|
|
|
return html; |
|
} |
|
|
|
|
|
function getKnowledgeBasesHtml(knowledgeBases) { |
|
if (!knowledgeBases || knowledgeBases.length === 0) { |
|
return '<p>无</p>'; |
|
} |
|
|
|
let html = '<ul>'; |
|
knowledgeBases.forEach(kb => { |
|
|
|
const knowledge = allKnowledgeBases.find(k => k.id === kb); |
|
const knowledgeName = knowledge ? knowledge.name : kb; |
|
|
|
html += `<li>${knowledgeName}</li>`; |
|
}); |
|
html += '</ul>'; |
|
|
|
return html; |
|
} |
|
|
|
|
|
function getDistributionsHtml(distributions, agentId) { |
|
if (!distributions || distributions.length === 0) { |
|
return '<p>无</p>'; |
|
} |
|
|
|
let html = '<div class="list-group">'; |
|
distributions.forEach(dist => { |
|
const expiryText = dist.expires_at > 0 |
|
? `过期时间: ${new Date(dist.expires_at * 1000).toLocaleString()}` |
|
: '永不过期'; |
|
|
|
const url = `/student/${agentId}?token=${dist.token}`; |
|
|
|
html += ` |
|
<div class="distribution-link"> |
|
<div> |
|
<div class="mb-1">${url}</div> |
|
<small class="text-muted">${expiryText} | 使用次数: ${dist.usage_count || 0}</small> |
|
</div> |
|
<button class="btn btn-sm btn-outline-primary" onclick="copyToClipboard('${url}')"> |
|
<i class="bi bi-clipboard"></i> 复制 |
|
</button> |
|
</div> |
|
`; |
|
}); |
|
html += '</div>'; |
|
|
|
return html; |
|
} |
|
|
|
|
|
async function deleteAgent(agentId) { |
|
if (!confirm('确定要删除这个Agent吗?此操作不可恢复。')) { |
|
return; |
|
} |
|
|
|
try { |
|
const response = await fetch(`/api/agent/${agentId}`, { |
|
method: 'DELETE' |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
alert('Agent删除成功!'); |
|
loadAgents(); |
|
} else { |
|
alert('删除Agent失败: ' + result.message); |
|
} |
|
} catch (error) { |
|
console.error('删除Agent出错:', error); |
|
alert('删除Agent出错,请查看控制台日志'); |
|
} |
|
} |
|
|
|
|
|
function showCreateDistributionModal(agentId) { |
|
document.getElementById('distribution-agent-id').value = agentId; |
|
|
|
|
|
const modal = new bootstrap.Modal(document.getElementById('createDistributionModal')); |
|
modal.show(); |
|
} |
|
|
|
|
|
async function createDistribution() { |
|
const agentId = document.getElementById('distribution-agent-id').value; |
|
const expiresIn = parseInt(document.getElementById('distribution-expires').value); |
|
|
|
if (!agentId) { |
|
alert('Agent ID不能为空'); |
|
return; |
|
} |
|
|
|
try { |
|
const response = await fetch(`/api/agent/${agentId}/distribute`, { |
|
method: 'POST', |
|
headers: { 'Content-Type': 'application/json' }, |
|
body: JSON.stringify({ expires_in: expiresIn }) |
|
}); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
|
|
const createModal = bootstrap.Modal.getInstance(document.getElementById('createDistributionModal')); |
|
createModal.hide(); |
|
|
|
|
|
const fullUrl = window.location.origin + result.distribution.link; |
|
document.getElementById('distribution-link').value = fullUrl; |
|
|
|
|
|
const showModal = new bootstrap.Modal(document.getElementById('showDistributionModal')); |
|
showModal.show(); |
|
|
|
|
|
loadAgents(); |
|
} else { |
|
alert('创建分发链接失败: ' + result.message); |
|
} |
|
} catch (error) { |
|
console.error('创建分发链接出错:', error); |
|
alert('创建分发链接出错,请查看控制台日志'); |
|
} |
|
} |
|
|
|
|
|
function copyToClipboard(textOrElementId) { |
|
let text; |
|
|
|
if (textOrElementId.startsWith('/') || textOrElementId.startsWith('http')) { |
|
|
|
text = textOrElementId; |
|
} else { |
|
|
|
const element = document.getElementById(textOrElementId); |
|
text = element.value || element.textContent; |
|
} |
|
|
|
navigator.clipboard.writeText(text).then(() => { |
|
alert('已复制到剪贴板!'); |
|
}).catch(err => { |
|
console.error('复制失败:', err); |
|
}); |
|
} |
|
|
|
|
|
function getNodeIcon(type) { |
|
switch (type) { |
|
case 'intent_recognition': |
|
return 'AI'; |
|
case 'knowledge_query': |
|
return 'KB'; |
|
case 'plugin_call': |
|
return 'API'; |
|
case 'generate_response': |
|
return 'AI'; |
|
default: |
|
return ''; |
|
} |
|
} |
|
|
|
|
|
function renderWorkflowVisualization(workflow, containerId) { |
|
const container = document.getElementById(containerId); |
|
if (!container) return; |
|
|
|
|
|
container.innerHTML = ''; |
|
|
|
|
|
if (!workflow || !workflow.nodes || workflow.nodes.length === 0) { |
|
container.innerHTML = ` |
|
<div class="p-4 text-center text-muted"> |
|
暂无工作流数据 |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
|
|
if (jsPlumbWorkflowInstance) { |
|
jsPlumbWorkflowInstance.reset(); |
|
} |
|
|
|
jsPlumbWorkflowInstance = jsPlumb.getInstance({ |
|
Endpoint: ["Dot", { radius: 4 }], |
|
Connector: ["Bezier", { curviness: 50 }], |
|
PaintStyle: { stroke: "#4361ee", strokeWidth: 2 }, |
|
HoverPaintStyle: { stroke: "#3a56e4", strokeWidth: 3 }, |
|
ConnectionOverlays: [ |
|
["Arrow", { location: 1, width: 10, length: 10, foldback: 0.7 }] |
|
], |
|
Container: containerId |
|
}); |
|
|
|
|
|
workflow.nodes.forEach(node => { |
|
const nodeEl = document.createElement('div'); |
|
nodeEl.id = node.id; |
|
nodeEl.className = `workflow-node node-type-${node.type}`; |
|
nodeEl.innerHTML = ` |
|
<div class="node-icon">${getNodeIcon(node.type)}</div> |
|
<div class="node-content">${node.data.name}</div> |
|
`; |
|
|
|
container.appendChild(nodeEl); |
|
}); |
|
|
|
|
|
const dependencies = {}; |
|
const dependents = {}; |
|
|
|
workflow.nodes.forEach(node => { |
|
dependencies[node.id] = []; |
|
dependents[node.id] = []; |
|
}); |
|
|
|
workflow.edges.forEach(edge => { |
|
dependencies[edge.target].push(edge.source); |
|
dependents[edge.source].push(edge.target); |
|
}); |
|
|
|
|
|
const nodeLevels = {}; |
|
const queue = []; |
|
|
|
|
|
workflow.nodes.forEach(node => { |
|
if (dependencies[node.id].length === 0) { |
|
nodeLevels[node.id] = 0; |
|
queue.push(node.id); |
|
} |
|
}); |
|
|
|
|
|
while (queue.length > 0) { |
|
const currentId = queue.shift(); |
|
const currentLevel = nodeLevels[currentId]; |
|
|
|
dependents[currentId].forEach(dependentId => { |
|
const allDependenciesProcessed = dependencies[dependentId].every( |
|
depId => nodeLevels[depId] !== undefined |
|
); |
|
|
|
if (allDependenciesProcessed) { |
|
const maxDependencyLevel = Math.max( |
|
...dependencies[dependentId].map(depId => nodeLevels[depId]) |
|
); |
|
nodeLevels[dependentId] = maxDependencyLevel + 1; |
|
queue.push(dependentId); |
|
} |
|
}); |
|
} |
|
|
|
|
|
const levelGroups = {}; |
|
Object.keys(nodeLevels).forEach(nodeId => { |
|
const level = nodeLevels[nodeId]; |
|
if (!levelGroups[level]) { |
|
levelGroups[level] = []; |
|
} |
|
levelGroups[level].push(nodeId); |
|
}); |
|
|
|
const containerWidth = container.clientWidth; |
|
const containerHeight = container.clientHeight; |
|
const levelCount = Object.keys(levelGroups).length; |
|
const levelHeight = containerHeight / (levelCount > 1 ? levelCount : 2); |
|
|
|
Object.entries(levelGroups).forEach(([level, nodeIds]) => { |
|
const levelIndex = parseInt(level); |
|
const nodeCount = nodeIds.length; |
|
const nodeWidth = containerWidth / (nodeCount + 1); |
|
|
|
nodeIds.forEach((nodeId, index) => { |
|
const node = document.getElementById(nodeId); |
|
if (node) { |
|
node.style.top = `${levelIndex * levelHeight + 50}px`; |
|
node.style.left = `${(index + 1) * nodeWidth - (node.clientWidth / 2)}px`; |
|
} |
|
}); |
|
}); |
|
|
|
|
|
workflow.nodes.forEach(node => { |
|
const nodeEl = document.getElementById(node.id); |
|
if (nodeEl) { |
|
jsPlumbWorkflowInstance.draggable(nodeEl, { |
|
containment: container |
|
}); |
|
} |
|
}); |
|
|
|
|
|
setTimeout(() => { |
|
workflow.edges.forEach(edge => { |
|
const sourceEl = document.getElementById(edge.source); |
|
const targetEl = document.getElementById(edge.target); |
|
|
|
if (sourceEl && targetEl) { |
|
const connection = jsPlumbWorkflowInstance.connect({ |
|
source: sourceEl, |
|
target: targetEl, |
|
anchors: ["Bottom", "Top"], |
|
connector: ["Bezier", { curviness: 50 }], |
|
paintStyle: { stroke: "#4361ee", strokeWidth: 2 }, |
|
endpoint: "Blank", |
|
overlays: [ |
|
["Arrow", { location: 1, width: 10, length: 10, foldback: 0.7 }] |
|
] |
|
}); |
|
|
|
|
|
if (edge.condition) { |
|
connection.addOverlay([ |
|
"Label", { |
|
label: edge.condition, |
|
cssClass: "connection-label", |
|
location: 0.5 |
|
} |
|
]); |
|
} |
|
} |
|
}); |
|
|
|
|
|
jsPlumbWorkflowInstance.repaintEverything(); |
|
}, 100); |
|
} |
|
</script> |
|
</body> |
|
</html> |