|
|
|
<!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> |
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); |
|
|
|
:root { |
|
|
|
--primary-color: #0f2d49; |
|
--primary-light: #234a70; |
|
--secondary-color: #4a6cfd; |
|
--secondary-light: #7b91ff; |
|
--tertiary-color: #f7f9fe; |
|
--success-color: #10b981; |
|
--success-light: rgba(16, 185, 129, 0.1); |
|
--warning-color: #f59e0b; |
|
--warning-light: rgba(245, 158, 11, 0.1); |
|
--info-color: #0ea5e9; |
|
--info-light: rgba(14, 165, 233, 0.1); |
|
--danger-color: #ef4444; |
|
--danger-light: rgba(239, 68, 68, 0.1); |
|
--neutral-50: #f9fafb; |
|
--neutral-100: #f3f4f6; |
|
--neutral-200: #e5e7eb; |
|
--neutral-300: #d1d5db; |
|
--neutral-400: #9ca3af; |
|
--neutral-500: #6b7280; |
|
--neutral-600: #4b5563; |
|
--neutral-700: #374151; |
|
--neutral-800: #1f2937; |
|
--neutral-900: #111827; |
|
|
|
|
|
--border-radius-sm: 0.25rem; |
|
--border-radius: 0.375rem; |
|
--border-radius-lg: 0.5rem; |
|
--border-radius-xl: 0.75rem; |
|
--border-radius-2xl: 1rem; |
|
--card-shadow: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.1); |
|
--card-shadow-hover: 0 10px 20px rgba(0, 0, 0, 0.05), 0 6px 6px rgba(0, 0, 0, 0.1); |
|
--sidebar-shadow: 0 0 20px rgba(0, 0, 0, 0.05); |
|
--transition-base: all 0.2s ease-in-out; |
|
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
|
--font-family: 'Inter', 'PingFang SC', 'Helvetica Neue', 'Microsoft YaHei', sans-serif; |
|
} |
|
|
|
|
|
body { |
|
font-family: var(--font-family); |
|
background-color: var(--neutral-50); |
|
color: var(--neutral-800); |
|
display: flex; |
|
margin: 0; |
|
padding: 0; |
|
height: 100vh; |
|
overflow: hidden; |
|
-webkit-font-smoothing: antialiased; |
|
-moz-osx-font-smoothing: grayscale; |
|
} |
|
|
|
h1, h2, h3, h4, h5, h6 { |
|
font-weight: 600; |
|
color: var(--neutral-900); |
|
} |
|
|
|
.text-gradient { |
|
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light)); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
background-clip: text; |
|
color: transparent; |
|
} |
|
|
|
|
|
::-webkit-scrollbar { |
|
width: 6px; |
|
height: 6px; |
|
} |
|
|
|
::-webkit-scrollbar-track { |
|
background: var(--neutral-100); |
|
border-radius: 10px; |
|
} |
|
|
|
::-webkit-scrollbar-thumb { |
|
background: var(--neutral-300); |
|
border-radius: 10px; |
|
} |
|
|
|
::-webkit-scrollbar-thumb:hover { |
|
background: var(--neutral-400); |
|
} |
|
|
|
|
|
.sidebar { |
|
width: 260px; |
|
background-color: white; |
|
display: flex; |
|
flex-direction: column; |
|
box-shadow: var(--sidebar-shadow); |
|
z-index: 1000; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.sidebar::before { |
|
content: ''; |
|
position: absolute; |
|
top: 0; |
|
right: 0; |
|
bottom: 0; |
|
width: 1px; |
|
background: linear-gradient(to bottom, |
|
var(--neutral-100) 0%, |
|
var(--neutral-200) 50%, |
|
var(--neutral-100) 100%); |
|
} |
|
|
|
.sidebar-logo { |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.sidebar-logo-icon { |
|
width: 38px; |
|
height: 38px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light)); |
|
color: white; |
|
border-radius: 12px; |
|
font-size: 20px; |
|
margin-right: 12px; |
|
box-shadow: 0 4px 12px rgba(74, 108, 253, 0.3); |
|
} |
|
|
|
.sidebar-header { |
|
padding: 1.75rem 1.5rem; |
|
display: flex; |
|
align-items: center; |
|
border-bottom: 1px solid var(--neutral-100); |
|
} |
|
|
|
.sidebar-header h2 { |
|
margin: 0; |
|
font-size: 1.25rem; |
|
font-weight: 700; |
|
background: linear-gradient(45deg, var(--primary-color), var(--secondary-color)); |
|
-webkit-background-clip: text; |
|
-webkit-text-fill-color: transparent; |
|
letter-spacing: -0.01em; |
|
} |
|
|
|
.sidebar-menu { |
|
list-style: none; |
|
padding: 1.25rem 0.75rem; |
|
margin: 0; |
|
flex: 1; |
|
overflow-y: auto; |
|
} |
|
|
|
.sidebar-menu li { |
|
margin-bottom: 0.5rem; |
|
border-radius: var(--border-radius-lg); |
|
transition: var(--transition-base); |
|
} |
|
|
|
.sidebar-menu li a { |
|
display: flex; |
|
align-items: center; |
|
padding: 0.85rem 1rem; |
|
color: var(--neutral-700); |
|
text-decoration: none; |
|
font-weight: 500; |
|
border-radius: var(--border-radius-lg); |
|
transition: var(--transition-base); |
|
} |
|
|
|
.sidebar-menu li:hover a { |
|
color: var(--primary-color); |
|
background-color: var(--neutral-100); |
|
} |
|
|
|
.sidebar-menu li.active { |
|
background: linear-gradient(to right, var(--secondary-light), var(--secondary-color)); |
|
} |
|
|
|
.sidebar-menu li.active a { |
|
color: white; |
|
font-weight: 600; |
|
} |
|
|
|
.sidebar-menu li i { |
|
margin-right: 0.85rem; |
|
font-size: 1.15rem; |
|
opacity: 0.85; |
|
} |
|
|
|
|
|
.main-content { |
|
flex: 1; |
|
padding: 2rem 2.5rem; |
|
overflow-y: auto; |
|
background-color: var(--neutral-50); |
|
} |
|
|
|
|
|
.page-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 2rem; |
|
} |
|
|
|
.content-header { |
|
margin-bottom: 0.5rem; |
|
} |
|
|
|
.content-header h1 { |
|
margin: 0; |
|
font-size: 1.75rem; |
|
font-weight: 700; |
|
color: var(--primary-color); |
|
letter-spacing: -0.01em; |
|
} |
|
|
|
.content-header p { |
|
margin-top: 0.5rem; |
|
color: var(--neutral-600); |
|
font-size: 0.95rem; |
|
} |
|
|
|
.content-section { |
|
display: none; |
|
} |
|
|
|
.content-section.active { |
|
display: block; |
|
} |
|
|
|
|
|
.card { |
|
background-color: white; |
|
border-radius: var(--border-radius-xl); |
|
box-shadow: var(--card-shadow); |
|
border: none; |
|
transition: var(--transition-smooth); |
|
overflow: hidden; |
|
margin-bottom: 1.75rem; |
|
} |
|
|
|
.card:hover { |
|
box-shadow: var(--card-shadow-hover); |
|
} |
|
|
|
.card-header { |
|
background-color: white; |
|
border-bottom: 1px solid var(--neutral-100); |
|
padding: 1.25rem 1.5rem; |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
} |
|
|
|
.card-header h3 { |
|
margin: 0; |
|
font-size: 1.1rem; |
|
font-weight: 600; |
|
color: var(--neutral-900); |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.card-header h3 i { |
|
margin-right: 0.5rem; |
|
font-size: 1.2rem; |
|
color: var(--secondary-color); |
|
opacity: 0.85; |
|
} |
|
|
|
.card-body { |
|
padding: 1.5rem; |
|
} |
|
|
|
|
|
.stat-container { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); |
|
gap: 1.5rem; |
|
margin-bottom: 2rem; |
|
} |
|
|
|
.stat-card { |
|
background: white; |
|
border-radius: var(--border-radius-xl); |
|
padding: 1.5rem; |
|
box-shadow: var(--card-shadow); |
|
transition: var(--transition-smooth); |
|
display: flex; |
|
align-items: center; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.stat-card:hover { |
|
transform: translateY(-5px); |
|
box-shadow: var(--card-shadow-hover); |
|
} |
|
|
|
.stat-card::after { |
|
content: ''; |
|
position: absolute; |
|
bottom: 0; |
|
left: 0; |
|
right: 0; |
|
height: 3px; |
|
border-radius: 3px 3px 0 0; |
|
} |
|
|
|
.stat-card.primary::after { |
|
background: linear-gradient(to right, var(--secondary-color), var(--secondary-light)); |
|
} |
|
|
|
.stat-card.success::after { |
|
background: linear-gradient(to right, var(--success-color), #34d399); |
|
} |
|
|
|
.stat-card.warning::after { |
|
background: linear-gradient(to right, var(--warning-color), #fbbf24); |
|
} |
|
|
|
.stat-icon { |
|
width: 50px; |
|
height: 50px; |
|
border-radius: 12px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
margin-right: 1rem; |
|
font-size: 1.5rem; |
|
flex-shrink: 0; |
|
} |
|
|
|
.stat-icon.primary { |
|
background-color: var(--secondary-light); |
|
color: white; |
|
} |
|
|
|
.stat-icon.success { |
|
background-color: var(--success-color); |
|
color: white; |
|
} |
|
|
|
.stat-icon.warning { |
|
background-color: var(--warning-color); |
|
color: white; |
|
} |
|
|
|
.stat-content { |
|
flex: 1; |
|
} |
|
|
|
.stat-label { |
|
font-size: 0.9rem; |
|
color: var(--neutral-600); |
|
margin: 0; |
|
} |
|
|
|
.stat-value { |
|
font-size: 1.75rem; |
|
font-weight: 700; |
|
color: var(--primary-color); |
|
margin: 0.25rem 0 0 0; |
|
line-height: 1; |
|
} |
|
|
|
|
|
.agent-item { |
|
background: white; |
|
border-radius: var(--border-radius-xl); |
|
padding: 1.5rem; |
|
margin-bottom: 1.25rem; |
|
box-shadow: var(--card-shadow); |
|
transition: var(--transition-smooth); |
|
border: none; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.agent-item::before { |
|
content: ''; |
|
position: absolute; |
|
top: 0; |
|
bottom: 0; |
|
left: 0; |
|
width: 4px; |
|
background: linear-gradient(to bottom, var(--secondary-color), var(--secondary-light)); |
|
border-radius: 20px; |
|
} |
|
|
|
.agent-item:hover { |
|
transform: translateY(-5px); |
|
box-shadow: var(--card-shadow-hover); |
|
} |
|
|
|
.agent-content { |
|
padding-left: 0.5rem; |
|
} |
|
|
|
.agent-item .header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.agent-item .title { |
|
font-size: 1.15rem; |
|
font-weight: 600; |
|
margin: 0; |
|
color: var(--primary-color); |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.agent-item .title i { |
|
margin-right: 0.5rem; |
|
font-size: 1.1rem; |
|
color: var(--secondary-color); |
|
} |
|
|
|
.agent-badges { |
|
display: flex; |
|
align-items: center; |
|
gap: 0.5rem; |
|
} |
|
|
|
.agent-item .description { |
|
color: var(--neutral-700); |
|
margin-bottom: 1rem; |
|
line-height: 1.5; |
|
} |
|
|
|
.agent-stats { |
|
display: flex; |
|
align-items: center; |
|
gap: 1.5rem; |
|
margin-top: 1.25rem; |
|
padding-top: 1rem; |
|
border-top: 1px solid var(--neutral-100); |
|
} |
|
|
|
.agent-stat { |
|
display: flex; |
|
flex-direction: column; |
|
} |
|
|
|
.agent-stat-value { |
|
font-size: 1.1rem; |
|
font-weight: 600; |
|
color: var(--neutral-900); |
|
} |
|
|
|
.agent-stat-label { |
|
font-size: 0.8rem; |
|
color: var(--neutral-500); |
|
margin-top: 0.25rem; |
|
} |
|
|
|
.agent-tags { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 0.5rem; |
|
margin-top: 0.5rem; |
|
} |
|
|
|
.agent-tag { |
|
display: inline-flex; |
|
align-items: center; |
|
padding: 0.35rem 0.75rem; |
|
background-color: var(--neutral-100); |
|
border-radius: 20px; |
|
font-size: 0.8rem; |
|
color: var(--neutral-700); |
|
} |
|
|
|
.agent-tag i { |
|
margin-right: 0.3rem; |
|
font-size: 0.85rem; |
|
} |
|
|
|
.agent-actions { |
|
margin-top: 1.25rem; |
|
display: flex; |
|
gap: 0.75rem; |
|
} |
|
|
|
|
|
.quick-start-item { |
|
background-color: white; |
|
border-radius: var(--border-radius-xl); |
|
padding: 1.5rem; |
|
margin-bottom: 1.25rem; |
|
box-shadow: var(--card-shadow); |
|
transition: var(--transition-smooth); |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.quick-start-item:hover { |
|
transform: translateY(-5px); |
|
box-shadow: var(--card-shadow-hover); |
|
} |
|
|
|
.quick-start-num { |
|
position: absolute; |
|
top: 1.25rem; |
|
left: 1.25rem; |
|
width: 32px; |
|
height: 32px; |
|
border-radius: 50%; |
|
background: linear-gradient(135deg, var(--secondary-color), var(--secondary-light)); |
|
color: white; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-weight: 600; |
|
font-size: 0.9rem; |
|
} |
|
|
|
.quick-start-content { |
|
padding-left: 3rem; |
|
} |
|
|
|
.quick-start-item h5 { |
|
font-size: 1.05rem; |
|
font-weight: 600; |
|
margin-bottom: 0.5rem; |
|
color: var(--primary-color); |
|
} |
|
|
|
.quick-start-item p { |
|
color: var(--neutral-600); |
|
margin-bottom: 1rem; |
|
font-size: 0.95rem; |
|
} |
|
|
|
|
|
.plugin-option { |
|
background-color: white; |
|
border-radius: var(--border-radius-xl); |
|
padding: 1.25rem; |
|
margin-bottom: 1rem; |
|
box-shadow: var(--card-shadow); |
|
transition: var(--transition-smooth); |
|
display: flex; |
|
align-items: center; |
|
cursor: pointer; |
|
border: 1px solid var(--neutral-100); |
|
} |
|
|
|
.plugin-option:hover { |
|
transform: translateY(-3px); |
|
box-shadow: var(--card-shadow-hover); |
|
border-color: var(--neutral-200); |
|
} |
|
|
|
.plugin-option.selected { |
|
border-color: var(--secondary-color); |
|
background-color: rgba(74, 108, 253, 0.05); |
|
} |
|
|
|
.plugin-icon { |
|
width: 48px; |
|
height: 48px; |
|
border-radius: 12px; |
|
background-color: var(--neutral-100); |
|
color: var(--secondary-color); |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
font-size: 1.5rem; |
|
margin-right: 1rem; |
|
flex-shrink: 0; |
|
} |
|
|
|
.plugin-content { |
|
flex: 1; |
|
} |
|
|
|
.plugin-title { |
|
font-weight: 600; |
|
margin-bottom: 0.25rem; |
|
color: var(--neutral-900); |
|
} |
|
|
|
.plugin-description { |
|
color: var(--neutral-600); |
|
font-size: 0.9rem; |
|
margin: 0; |
|
} |
|
|
|
|
|
.workflow-canvas { |
|
background-color: white; |
|
border-radius: var(--border-radius-xl); |
|
border: 1px solid var(--neutral-200); |
|
min-height: 400px; |
|
position: relative; |
|
overflow: hidden; |
|
} |
|
|
|
.workflow-node { |
|
position: absolute; |
|
width: 180px; |
|
background-color: white; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1); |
|
user-select: none; |
|
overflow: hidden; |
|
transition: box-shadow 0.2s ease; |
|
} |
|
|
|
.workflow-node:hover { |
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15); |
|
} |
|
|
|
.node-header { |
|
padding: 0.5rem; |
|
display: flex; |
|
align-items: center; |
|
border-bottom: 1px solid var(--neutral-100); |
|
} |
|
|
|
.node-icon { |
|
width: 24px; |
|
height: 24px; |
|
border-radius: 6px; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
color: white; |
|
font-weight: bold; |
|
font-size: 0.75rem; |
|
margin-right: 0.5rem; |
|
} |
|
|
|
.node-title { |
|
font-size: 0.85rem; |
|
font-weight: 500; |
|
color: var(--neutral-800); |
|
} |
|
|
|
.node-content { |
|
padding: 0.75rem; |
|
font-size: 0.8rem; |
|
color: var(--neutral-600); |
|
} |
|
|
|
|
|
.node-type-intent_recognition .node-icon { |
|
background-color: var(--secondary-color); |
|
} |
|
|
|
.node-type-knowledge_query .node-icon { |
|
background-color: var(--success-color); |
|
} |
|
|
|
.node-type-plugin_call .node-icon { |
|
background-color: var(--warning-color); |
|
} |
|
|
|
.node-type-generate_response .node-icon { |
|
background-color: var(--info-color); |
|
} |
|
|
|
|
|
.knowledge-item { |
|
background-color: white; |
|
border-radius: var(--border-radius-xl); |
|
padding: 1.25rem; |
|
margin-bottom: 1rem; |
|
box-shadow: var(--card-shadow); |
|
transition: var(--transition-smooth); |
|
border: 1px solid var(--neutral-100); |
|
} |
|
|
|
.knowledge-item:hover { |
|
transform: translateY(-3px); |
|
box-shadow: var(--card-shadow-hover); |
|
} |
|
|
|
.knowledge-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
margin-bottom: 1rem; |
|
} |
|
|
|
.knowledge-title { |
|
font-weight: 600; |
|
margin: 0; |
|
color: var(--neutral-900); |
|
font-size: 1.05rem; |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.knowledge-title i { |
|
margin-right: 0.5rem; |
|
color: var(--secondary-color); |
|
} |
|
|
|
.knowledge-meta { |
|
margin-top: 1rem; |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 1rem; |
|
} |
|
|
|
.knowledge-meta-item { |
|
display: flex; |
|
align-items: center; |
|
font-size: 0.85rem; |
|
color: var(--neutral-600); |
|
} |
|
|
|
.knowledge-meta-item i { |
|
margin-right: 0.4rem; |
|
font-size: 0.9rem; |
|
} |
|
|
|
|
|
.btn { |
|
font-weight: 500; |
|
padding: 0.65rem 1.25rem; |
|
border-radius: var(--border-radius-lg); |
|
transition: var(--transition-base); |
|
box-shadow: none; |
|
} |
|
|
|
.btn-icon { |
|
width: 36px; |
|
height: 36px; |
|
padding: 0; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
border-radius: 8px; |
|
} |
|
|
|
.btn-xs { |
|
padding: 0.25rem 0.5rem; |
|
font-size: 0.75rem; |
|
border-radius: 4px; |
|
} |
|
|
|
.btn-sm { |
|
padding: 0.35rem 0.85rem; |
|
font-size: 0.875rem; |
|
} |
|
|
|
.btn-primary { |
|
background: linear-gradient(to right, var(--secondary-color), var(--secondary-light)); |
|
border: none; |
|
} |
|
|
|
.btn-primary:hover, .btn-primary:focus { |
|
background: linear-gradient(to right, var(--secondary-light), var(--secondary-color)); |
|
box-shadow: 0 4px 10px rgba(74, 108, 253, 0.3); |
|
transform: translateY(-1px); |
|
} |
|
|
|
.btn-outline-primary { |
|
color: var(--secondary-color); |
|
border-color: var(--secondary-color); |
|
background-color: transparent; |
|
} |
|
|
|
.btn-outline-primary:hover, .btn-outline-primary:focus { |
|
background-color: var(--secondary-color); |
|
border-color: var(--secondary-color); |
|
color: white; |
|
box-shadow: 0 4px 10px rgba(74, 108, 253, 0.2); |
|
} |
|
|
|
.btn-light { |
|
background-color: var(--neutral-100); |
|
color: var(--neutral-700); |
|
border: none; |
|
} |
|
|
|
.btn-light:hover, .btn-light:focus { |
|
background-color: var(--neutral-200); |
|
color: var(--neutral-900); |
|
} |
|
|
|
.btn-success { |
|
background-color: var(--success-color); |
|
border-color: var(--success-color); |
|
} |
|
|
|
.btn-success:hover, .btn-success:focus { |
|
background-color: var(--success-color); |
|
border-color: var(--success-color); |
|
opacity: 0.9; |
|
box-shadow: 0 4px 10px rgba(16, 185, 129, 0.3); |
|
} |
|
|
|
|
|
.badge { |
|
font-weight: 500; |
|
padding: 0.35em 0.65em; |
|
border-radius: 6px; |
|
font-size: 0.75rem; |
|
} |
|
|
|
.badge-primary { |
|
background-color: rgba(74, 108, 253, 0.15); |
|
color: var(--secondary-color); |
|
} |
|
|
|
.badge-success { |
|
background-color: rgba(16, 185, 129, 0.15); |
|
color: var(--success-color); |
|
} |
|
|
|
.badge-warning { |
|
background-color: rgba(245, 158, 11, 0.15); |
|
color: var(--warning-color); |
|
} |
|
|
|
|
|
.form-control, .form-select { |
|
padding: 0.65rem 1rem; |
|
border-radius: var(--border-radius-lg); |
|
border: 1px solid var(--neutral-200); |
|
font-size: 0.95rem; |
|
box-shadow: none; |
|
transition: var(--transition-base); |
|
} |
|
|
|
.form-control:focus, .form-select:focus { |
|
border-color: var(--secondary-color); |
|
box-shadow: 0 0 0 3px rgba(74, 108, 253, 0.1); |
|
} |
|
|
|
.form-label { |
|
font-weight: 500; |
|
margin-bottom: 0.5rem; |
|
color: var(--neutral-800); |
|
} |
|
|
|
.input-group-text { |
|
background-color: var(--neutral-100); |
|
border-color: var(--neutral-200); |
|
color: var(--neutral-700); |
|
} |
|
|
|
|
|
.nav-tabs { |
|
border-bottom: 1px solid var(--neutral-200); |
|
margin-bottom: 1.5rem; |
|
} |
|
|
|
.nav-tabs .nav-link { |
|
border: none; |
|
color: var(--neutral-600); |
|
padding: 0.75rem 1rem; |
|
margin-right: 1rem; |
|
font-weight: 500; |
|
position: relative; |
|
} |
|
|
|
.nav-tabs .nav-link:after { |
|
content: ''; |
|
position: absolute; |
|
left: 0; |
|
right: 0; |
|
bottom: -1px; |
|
height: 2px; |
|
background-color: transparent; |
|
transition: var(--transition-base); |
|
} |
|
|
|
.nav-tabs .nav-link:hover { |
|
color: var(--neutral-900); |
|
border: none; |
|
} |
|
|
|
.nav-tabs .nav-link.active { |
|
color: var(--secondary-color); |
|
background-color: transparent; |
|
border: none; |
|
} |
|
|
|
.nav-tabs .nav-link.active:after { |
|
background-color: var(--secondary-color); |
|
} |
|
|
|
|
|
@keyframes fadeIn { |
|
from { opacity: 0; transform: translateY(8px); } |
|
to { opacity: 1; transform: translateY(0); } |
|
} |
|
|
|
.fade-in { |
|
opacity: 0; |
|
animation: fadeIn 0.4s forwards; |
|
} |
|
|
|
.animate-delay-1 { animation-delay: 0.1s; } |
|
.animate-delay-2 { animation-delay: 0.2s; } |
|
.animate-delay-3 { animation-delay: 0.3s; } |
|
.animate-delay-4 { animation-delay: 0.4s; } |
|
.animate-delay-5 { animation-delay: 0.5s; } |
|
|
|
</style> |
|
</head> |
|
<body> |
|
|
|
<div class="sidebar"> |
|
<div class="sidebar-header"> |
|
<div class="sidebar-logo"> |
|
<div class="sidebar-logo-icon"> |
|
<i class="bi bi-robot"></i> |
|
</div> |
|
<h2>AI助教开发平台</h2> |
|
</div> |
|
</div> |
|
<ul class="sidebar-menu"> |
|
<li class="active"> |
|
<a href="#" data-section="dashboard"> |
|
<i class="bi bi-grid-1x2"></i> |
|
<span>仪表盘</span> |
|
</a> |
|
</li> |
|
<li> |
|
<a href="#" data-section="create-agent"> |
|
<i class="bi bi-plus-circle"></i> |
|
<span>创建 Agent</span> |
|
</a> |
|
</li> |
|
<li> |
|
<a href="#" data-section="knowledge-base"> |
|
<i class="bi bi-database"></i> |
|
<span>知识库管理</span> |
|
</a> |
|
</li> |
|
<li> |
|
<a href="#" data-section="agent-list"> |
|
<i class="bi bi-list-ul"></i> |
|
<span>Agent 列表</span> |
|
</a> |
|
</li> |
|
<button class="btn btn-sm btn-outline-primary" id="logout-btn">登出</button> |
|
</ul> |
|
</div> |
|
|
|
|
|
<div class="main-content"> |
|
|
|
<section id="dashboard" class="content-section active"> |
|
<div class="page-header"> |
|
<div class="content-header"> |
|
<h1>仪表盘</h1> |
|
<p>轻松创建和管理您的教育 AI 助手</p> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="stat-container"> |
|
<div class="stat-card primary fade-in animate-delay-1"> |
|
<div class="stat-icon primary"> |
|
<i class="bi bi-robot"></i> |
|
</div> |
|
<div class="stat-content"> |
|
<p class="stat-label">AI 助手</p> |
|
<h3 class="stat-value" id="agent-count">0</h3> |
|
</div> |
|
</div> |
|
|
|
<div class="stat-card success fade-in animate-delay-2"> |
|
<div class="stat-icon success"> |
|
<i class="bi bi-database"></i> |
|
</div> |
|
<div class="stat-content"> |
|
<p class="stat-label">知识库</p> |
|
<h3 class="stat-value" id="knowledge-count">0</h3> |
|
</div> |
|
</div> |
|
|
|
<div class="stat-card warning fade-in animate-delay-3"> |
|
<div class="stat-icon warning"> |
|
<i class="bi bi-share"></i> |
|
</div> |
|
<div class="stat-content"> |
|
<p class="stat-label">分发链接</p> |
|
<h3 class="stat-value" id="distribution-count">0</h3> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="row"> |
|
<div class="col-lg-7 fade-in animate-delay-4"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h3><i class="bi bi-robot"></i> 最近创建的 Agent</h3> |
|
<a href="#" class="text-decoration-none" style="color: var(--secondary-color);" onclick="switchSection('agent-list')">查看全部</a> |
|
</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-lg-5 fade-in animate-delay-5"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h3><i class="bi bi-lightning-charge"></i> 快速入门</h3> |
|
</div> |
|
<div class="card-body"> |
|
<div class="quick-start-item"> |
|
<div class="quick-start-num">1</div> |
|
<div class="quick-start-content"> |
|
<h5>创建知识库</h5> |
|
<p>上传教材、讲义等文档,构建 AI 助手的知识基础</p> |
|
<button class="btn btn-sm btn-outline-primary" onclick="switchSection('knowledge-base')"> |
|
<i class="bi bi-plus me-1"></i> 开始创建 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="quick-start-item"> |
|
<div class="quick-start-num">2</div> |
|
<div class="quick-start-content"> |
|
<h5>创建 Agent</h5> |
|
<p>设计您的 AI 助手,选择插件和现有知识库</p> |
|
<button class="btn btn-sm btn-outline-primary" onclick="switchSection('create-agent')"> |
|
<i class="bi bi-plus me-1"></i> 开始创建 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="quick-start-item"> |
|
<div class="quick-start-num">3</div> |
|
<div class="quick-start-content"> |
|
<h5>分发给学生</h5> |
|
<p>生成访问链接,分享给您的学生使用</p> |
|
<button class="btn btn-sm btn-outline-primary" onclick="switchSection('agent-list')"> |
|
<i class="bi bi-share me-1"></i> 查看 Agent |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="create-agent" class="content-section"> |
|
<div class="page-header"> |
|
<div class="content-header"> |
|
<h1>创建 Agent</h1> |
|
<p>设计您的教育AI助手,定制其功能和知识源</p> |
|
</div> |
|
</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="row"> |
|
<div class="col-md-8"> |
|
<div class="form-group mb-4"> |
|
<label for="agent-name" class="form-label">Agent 名称</label> |
|
<input type="text" class="form-control" id="agent-name" placeholder="例如:计算机组成原理助教"> |
|
</div> |
|
<div class="form-group mb-4"> |
|
<label for="agent-description" class="form-label">描述</label> |
|
<textarea class="form-control" id="agent-description" rows="3" placeholder="描述这个Agent的功能和用途..."></textarea> |
|
</div> |
|
<div class="row"> |
|
<div class="col-md-6"> |
|
<div class="form-group mb-4"> |
|
<label for="agent-subject" class="form-label">学科/课程名称</label> |
|
<input type="text" class="form-control" id="agent-subject" placeholder="例如:计算机组成原理"> |
|
</div> |
|
</div> |
|
<div class="col-md-6"> |
|
<div class="form-group mb-4"> |
|
<label for="agent-instructor" class="form-label">指导教师</label> |
|
<input type="text" class="form-control" id="agent-instructor" placeholder="例如:李志刚教授"> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="form-group mb-4"> |
|
<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> |
|
</div> |
|
</div> |
|
|
|
<div class="d-flex justify-content-end mt-4"> |
|
<button class="btn btn-primary" onclick="nextTab('plugins-tab')"> |
|
下一步 <i class="bi bi-arrow-right ms-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="tab-pane fade" id="plugins" role="tabpanel" aria-labelledby="plugins-tab"> |
|
<div class="mt-4"> |
|
<div class="mb-4"> |
|
<h5>选择Agent所需的插件</h5> |
|
<p class="text-muted">插件可以扩展Agent的能力,使其更加强大</p> |
|
</div> |
|
|
|
<div class="plugin-option" data-plugin="code" onclick="togglePluginSelection(this)"> |
|
<div class="plugin-icon"> |
|
<i class="bi bi-code-square"></i> |
|
</div> |
|
<div class="plugin-content"> |
|
<div class="plugin-title">代码执行</div> |
|
<div class="plugin-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="plugin-icon"> |
|
<i class="bi bi-bar-chart"></i> |
|
</div> |
|
<div class="plugin-content"> |
|
<div class="plugin-title">3D可视化</div> |
|
<div class="plugin-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="plugin-icon"> |
|
<i class="bi bi-diagram-3"></i> |
|
</div> |
|
<div class="plugin-content"> |
|
<div class="plugin-title">思维导图</div> |
|
<div class="plugin-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-light" onclick="nextTab('basic-tab')"> |
|
<i class="bi bi-arrow-left me-1"></i> 上一步 |
|
</button> |
|
<button class="btn btn-primary" onclick="nextTab('knowledge-tab')"> |
|
下一步 <i class="bi bi-arrow-right ms-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="tab-pane fade" id="knowledge" role="tabpanel" aria-labelledby="knowledge-tab"> |
|
<div class="mt-4"> |
|
<div class="mb-4"> |
|
<h5>选择Agent使用的知识库</h5> |
|
<p class="text-muted">知识库提供Agent回答问题的专业知识</p> |
|
</div> |
|
|
|
<div class="mb-4"> |
|
<div class="input-group"> |
|
<span class="input-group-text"><i class="bi bi-search"></i></span> |
|
<input type="text" class="form-control" placeholder="搜索知识库..." id="knowledge-search"> |
|
</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-light" onclick="nextTab('plugins-tab')"> |
|
<i class="bi bi-arrow-left me-1"></i> 上一步 |
|
</button> |
|
<button class="btn btn-primary" onclick="nextTab('workflow-tab')"> |
|
下一步 <i class="bi bi-arrow-right ms-1"></i> |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
|
|
<div class="tab-pane fade" id="workflow" role="tabpanel" aria-labelledby="workflow-tab"> |
|
<div class="mt-4"> |
|
<div class="mb-4"> |
|
<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-light" onclick="nextTab('knowledge-tab')"> |
|
<i class="bi bi-arrow-left me-1"></i> 上一步 |
|
</button> |
|
<button class="btn btn-success" id="create-agent-btn"> |
|
<i class="bi bi-check-lg me-1"></i> 创建 Agent |
|
</button> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
</section> |
|
|
|
|
|
<section id="knowledge-base" class="content-section"> |
|
<div class="page-header"> |
|
<div class="content-header"> |
|
<h1>知识库管理</h1> |
|
<p>上传和管理Agent的知识源</p> |
|
</div> |
|
</div> |
|
|
|
<div class="row"> |
|
<div class="col-md-4"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h3><i class="bi bi-plus-circle"></i> 创建知识库</h3> |
|
</div> |
|
<div class="card-body"> |
|
<form id="knowledge-form"> |
|
<div class="form-group mb-3"> |
|
<label for="knowledge-name" class="form-label">知识库名称</label> |
|
<input type="text" class="form-control" id="knowledge-name" placeholder="例如:计算机组成原理" required> |
|
</div> |
|
|
|
<div class="form-group mb-3"> |
|
<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"> |
|
<i class="bi bi-upload me-1"></i> 创建知识库 |
|
</button> |
|
</form> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="col-md-8"> |
|
<div class="card"> |
|
<div class="card-header"> |
|
<h3><i class="bi bi-collection"></i> 现有知识库</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="page-header"> |
|
<div class="content-header"> |
|
<h1>Agent 列表</h1> |
|
<p>管理和分发您创建的AI助手</p> |
|
</div> |
|
<div> |
|
<button class="btn btn-primary" onclick="switchSection('create-agent')"> |
|
<i class="bi bi-plus-lg me-1"></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> |
|
|
|
|
|
<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-light" 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"> |
|
<i class="bi bi-check-circle me-2"></i> 链接创建成功!请复制以下链接分享给学生 |
|
</div> |
|
<div class="input-group mb-3"> |
|
<input type="text" class="form-control" id="distribution-link" readonly> |
|
<button class="btn btn-outline-primary" 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 a').forEach(item => { |
|
item.addEventListener('click', function(e) { |
|
e.preventDefault(); |
|
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(); |
|
|
|
document.getElementById('logout-btn').addEventListener('click', logout); |
|
}); |
|
|
|
async function logout() { |
|
try { |
|
const response = await fetch('/api/auth/logout', { |
|
method: 'POST' |
|
}); |
|
|
|
const data = await response.json(); |
|
|
|
if (data.success) { |
|
window.location.href = '/login.html'; |
|
} else { |
|
alert('登出失败: ' + data.message); |
|
} |
|
} catch (error) { |
|
console.error('登出出错:', error); |
|
alert('登出失败,请重试'); |
|
} |
|
} |
|
|
|
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.querySelector('a').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="plugin-icon"> |
|
<i class="bi bi-journal-text"></i> |
|
</div> |
|
<div class="plugin-content"> |
|
<div class="plugin-title">${knowledge.name}</div> |
|
<div class="plugin-description"> |
|
<small>${knowledge.fileCount || 0}个文件</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="knowledge-files mt-3">'; |
|
knowledge.files.forEach(file => { |
|
|
|
const isImage = /\.(jpg|jpeg|png|gif|bmp)$/i.test(file); |
|
const isPdf = /\.pdf$/i.test(file); |
|
const isDoc = /\.(doc|docx)$/i.test(file); |
|
const isTxt = /\.txt$/i.test(file); |
|
const isMd = /\.md$/i.test(file); |
|
|
|
let iconClass = 'bi-file-text'; |
|
if (isImage) iconClass = 'bi-file-image'; |
|
if (isPdf) iconClass = 'bi-file-pdf'; |
|
if (isDoc) iconClass = 'bi-file-earmark-word'; |
|
if (isTxt) iconClass = 'bi-file-text'; |
|
if (isMd) iconClass = 'bi-file-text'; |
|
|
|
fileListHtml += ` |
|
<div class="d-flex align-items-center p-2 border-bottom"> |
|
<i class="bi ${iconClass} me-2" style="font-size: 1.2rem; color: var(--secondary-color);"></i> |
|
<div class="flex-grow-1 text-truncate" title="${file}"> |
|
${file} |
|
</div> |
|
<button class="btn btn-sm btn-icon btn-light" onclick="deleteFile('${knowledge.id}', '${encodeURIComponent(file)}')"> |
|
<i class="bi bi-trash"></i> |
|
</button> |
|
</div> |
|
`; |
|
}); |
|
fileListHtml += '</div>'; |
|
} |
|
|
|
item.innerHTML = ` |
|
<div class="knowledge-header"> |
|
<h5 class="knowledge-title"> |
|
<i class="bi bi-journal-text"></i> ${knowledge.name} |
|
</h5> |
|
<div> |
|
<button class="btn btn-sm btn-light me-1" onclick="editKnowledgeBase('${knowledge.id}')"> |
|
<i class="bi bi-pencil"></i> |
|
</button> |
|
<button class="btn btn-sm btn-light text-danger" onclick="deleteKnowledgeBase('${knowledge.id}')"> |
|
<i class="bi bi-trash"></i> |
|
</button> |
|
</div> |
|
</div> |
|
${fileListHtml} |
|
<div class="knowledge-meta"> |
|
<div class="knowledge-meta-item"> |
|
<i class="bi bi-file-text"></i> |
|
<span>${knowledge.fileCount || 0}个文件</span> |
|
</div> |
|
<div class="knowledge-meta-item"> |
|
<i class="bi bi-calendar3"></i> |
|
<span>创建于 ${new Date(knowledge.created_at * 1000).toLocaleDateString()}</span> |
|
</div> |
|
</div> |
|
<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'); |
|
const submitBtn = event.submitter || document.querySelector('#knowledge-form button[type="submit"]'); |
|
|
|
if (!nameInput.value || !fileInput.files[0]) { |
|
alert('请填写知识库名称并选择文件'); |
|
return; |
|
} |
|
|
|
|
|
const fileName = fileInput.files[0].name; |
|
const fileExt = fileName.substring(fileName.lastIndexOf('.')).toLowerCase(); |
|
|
|
|
|
const supportedExts = ['.txt', '.pdf', '.md', '.jpg', '.jpeg', '.png', '.gif', '.bmp']; |
|
if (!supportedExts.includes(fileExt)) { |
|
alert(`不支持的文件类型: ${fileExt}。\n支持的文件类型: ${supportedExts.join(', ')}`); |
|
return; |
|
} |
|
|
|
|
|
const fileSize = fileInput.files[0].size; |
|
const fileSizeMB = fileSize / (1024 * 1024); |
|
const maxFileSizeMB = 16; |
|
|
|
if (fileSizeMB > maxFileSizeMB) { |
|
alert(`文件大小(${fileSizeMB.toFixed(2)}MB)超过最大限制(${maxFileSizeMB}MB)`); |
|
return; |
|
} |
|
|
|
|
|
progressBar.style.display = 'block'; |
|
progressBar.querySelector('.progress-bar').style.width = '0%'; |
|
submitBtn.disabled = true; |
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 上传中...'; |
|
|
|
const formData = new FormData(); |
|
formData.append('name', nameInput.value); |
|
formData.append('file', fileInput.files[0]); |
|
|
|
try { |
|
|
|
const controller = new AbortController(); |
|
const timeoutId = setTimeout(() => controller.abort(), 60000); |
|
|
|
const response = await fetch('/api/knowledge', { |
|
method: 'POST', |
|
body: formData, |
|
signal: controller.signal |
|
}); |
|
|
|
clearTimeout(timeoutId); |
|
|
|
const result = await response.json(); |
|
|
|
if (result.success) { |
|
|
|
pollProgress(result.task_id, progressBar, submitBtn); |
|
} else { |
|
alert('创建知识库失败: ' + result.message); |
|
progressBar.style.display = 'none'; |
|
submitBtn.disabled = false; |
|
submitBtn.textContent = '创建知识库'; |
|
} |
|
} catch (error) { |
|
console.error('创建知识库出错:', error); |
|
|
|
|
|
let errorMessage = '创建知识库出错,请查看控制台日志'; |
|
if (error.name === 'AbortError') { |
|
errorMessage = '上传请求超时,请检查网络连接或尝试上传较小的文件'; |
|
} |
|
|
|
alert(errorMessage); |
|
progressBar.style.display = 'none'; |
|
submitBtn.disabled = false; |
|
submitBtn.textContent = '创建知识库'; |
|
} |
|
} |
|
|
|
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; |
|
} |
|
|
|
|
|
const fileName = fileInput.files[0].name; |
|
const fileExt = fileName.substring(fileName.lastIndexOf('.')).toLowerCase(); |
|
|
|
|
|
const supportedExts = ['.txt', '.pdf', '.md', '.jpg', '.jpeg', '.png', '.gif', '.bmp']; |
|
if (!supportedExts.includes(fileExt)) { |
|
alert(`不支持的文件类型: ${fileExt}。\n支持的文件类型: ${supportedExts.join(', ')}`); |
|
return; |
|
} |
|
|
|
|
|
const fileSize = fileInput.files[0].size; |
|
const fileSizeMB = fileSize / (1024 * 1024); |
|
const maxFileSizeMB = 16; |
|
|
|
if (fileSizeMB > maxFileSizeMB) { |
|
alert(`文件大小(${fileSizeMB.toFixed(2)}MB)超过最大限制(${maxFileSizeMB}MB)`); |
|
return; |
|
} |
|
|
|
|
|
submitBtn.disabled = true; |
|
submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 上传中...'; |
|
|
|
const formData = new FormData(); |
|
formData.append('file', fileInput.files[0]); |
|
|
|
try { |
|
|
|
const controller = new AbortController(); |
|
const timeoutId = setTimeout(() => controller.abort(), 60000); |
|
|
|
const response = await fetch(`/api/knowledge/${knowledgeId}/documents`, { |
|
method: 'POST', |
|
body: formData, |
|
signal: controller.signal |
|
}); |
|
|
|
clearTimeout(timeoutId); |
|
|
|
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); |
|
|
|
|
|
let errorMessage = '添加文件出错,请查看控制台日志'; |
|
if (error.name === 'AbortError') { |
|
errorMessage = '上传请求超时,请检查网络连接或尝试上传较小的文件'; |
|
} |
|
|
|
alert(errorMessage); |
|
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('删除知识库出错,请查看控制台日志'); |
|
} |
|
} |
|
|
|
async function deleteFile(indexId, encodedFileName) { |
|
try { |
|
|
|
const fileName = decodeURIComponent(encodedFileName); |
|
|
|
if (!confirm(`确定要删除文件 "${fileName}" 吗?此操作不可恢复。`)) { |
|
return; |
|
} |
|
|
|
const response = await fetch(`/api/knowledge/${indexId}/documents/${encodedFileName}`, { |
|
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, submitBtn) { |
|
const progressBar = progressElement.querySelector('.progress-bar'); |
|
let retryCount = 0; |
|
const maxRetries = 3; |
|
|
|
const interval = setInterval(async () => { |
|
try { |
|
const response = await fetch(`/api/progress/${taskId}`); |
|
|
|
if (!response.ok) { |
|
throw new Error(`HTTP错误: ${response.status}`); |
|
} |
|
|
|
const data = await response.json(); |
|
|
|
if (data.success) { |
|
const progressData = data.data; |
|
|
|
|
|
progressBar.style.width = progressData.progress + '%'; |
|
progressBar.setAttribute('aria-valuenow', progressData.progress); |
|
|
|
|
|
if (submitBtn.tagName === 'BUTTON') { |
|
submitBtn.innerHTML = `<i class="bi bi-arrow-repeat spin me-1"></i> ${progressData.status || '处理中...'}`; |
|
} |
|
|
|
|
|
if (progressData.progress >= 100 || progressData.error) { |
|
clearInterval(interval); |
|
|
|
if (progressData.error) { |
|
alert('处理失败: ' + progressData.status); |
|
} else { |
|
alert(`知识库处理成功! 已处理${progressData.docCount}个文档片段`); |
|
} |
|
|
|
|
|
progressElement.style.display = 'none'; |
|
|
|
|
|
submitBtn.disabled = false; |
|
submitBtn.innerHTML = '创建知识库'; |
|
|
|
|
|
document.getElementById('knowledge-name').value = ''; |
|
document.getElementById('knowledge-file').value = ''; |
|
|
|
|
|
loadKnowledgeBases(); |
|
} |
|
} else { |
|
throw new Error('轮询返回错误状态'); |
|
} |
|
|
|
|
|
retryCount = 0; |
|
|
|
} catch (error) { |
|
console.error('获取进度信息出错:', error); |
|
retryCount++; |
|
|
|
if (retryCount >= maxRetries) { |
|
clearInterval(interval); |
|
alert('获取进度信息失败,请检查控制台日志'); |
|
|
|
|
|
submitBtn.disabled = false; |
|
submitBtn.innerHTML = '创建知识库'; |
|
progressElement.style.display = 'none'; |
|
} |
|
} |
|
}, 1000); |
|
} |
|
|
|
|
|
function pollProgressSimple(taskId, buttonElement) { |
|
const originalText = buttonElement.textContent; |
|
let retryCount = 0; |
|
const maxRetries = 3; |
|
|
|
buttonElement.innerHTML = '<i class="bi bi-arrow-repeat spin me-1"></i> 处理中...'; |
|
|
|
const interval = setInterval(async () => { |
|
try { |
|
const response = await fetch(`/api/progress/${taskId}`); |
|
|
|
if (!response.ok) { |
|
throw new Error(`HTTP错误: ${response.status}`); |
|
} |
|
|
|
const data = await response.json(); |
|
|
|
if (data.success) { |
|
const progressData = data.data; |
|
|
|
|
|
buttonElement.innerHTML = `<i class="bi bi-arrow-repeat spin me-1"></i> ${progressData.status || '处理中...'} (${progressData.progress}%)`; |
|
|
|
|
|
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(); |
|
} |
|
} else { |
|
throw new Error('轮询返回错误状态'); |
|
} |
|
|
|
|
|
retryCount = 0; |
|
|
|
} catch (error) { |
|
console.error('获取进度信息出错:', error); |
|
retryCount++; |
|
|
|
if (retryCount >= maxRetries) { |
|
clearInterval(interval); |
|
alert('获取进度信息失败,请检查控制台日志'); |
|
|
|
|
|
buttonElement.disabled = false; |
|
buttonElement.textContent = originalText; |
|
} |
|
} |
|
}, 1000); |
|
} |
|
|
|
function getFileIconClass(fileName) { |
|
if (!fileName) return 'bi-file'; |
|
|
|
const ext = fileName.split('.').pop().toLowerCase(); |
|
|
|
switch (ext) { |
|
case 'pdf': return 'bi-file-pdf'; |
|
case 'doc': |
|
case 'docx': return 'bi-file-earmark-word'; |
|
case 'xls': |
|
case 'xlsx': return 'bi-file-earmark-excel'; |
|
case 'ppt': |
|
case 'pptx': return 'bi-file-earmark-ppt'; |
|
case 'jpg': |
|
case 'jpeg': |
|
case 'png': |
|
case 'gif': |
|
case 'bmp': return 'bi-file-image'; |
|
case 'md': return 'bi-markdown'; |
|
case 'txt': return 'bi-file-text'; |
|
default: return 'bi-file'; |
|
} |
|
} |
|
|
|
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 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 typeText = getAgentTypeText(agent.type); |
|
|
|
|
|
let agentTagsHtml = ''; |
|
if (agent.subject || agent.instructor) { |
|
agentTagsHtml = '<div class="agent-tags">'; |
|
if (agent.subject) { |
|
agentTagsHtml += `<span class="agent-tag"><i class="bi bi-book"></i> ${agent.subject}</span>`; |
|
} |
|
if (agent.instructor) { |
|
agentTagsHtml += `<span class="agent-tag"><i class="bi bi-person"></i> ${agent.instructor}</span>`; |
|
} |
|
agentTagsHtml += '</div>'; |
|
} |
|
|
|
item.innerHTML = ` |
|
<div class="agent-content"> |
|
<div class="header"> |
|
<h5 class="title"><i class="bi bi-robot"></i> ${agent.name}</h5> |
|
<span class="badge badge-primary">${typeText}</span> |
|
</div> |
|
<div class="description">${agent.description || '暂无描述'}</div> |
|
${agentTagsHtml} |
|
<div class="agent-stats"> |
|
<div class="agent-stat"> |
|
<span class="agent-stat-value">${formattedDate}</span> |
|
<span class="agent-stat-label">创建时间</span> |
|
</div> |
|
<div class="agent-stat"> |
|
<span class="agent-stat-value">${agent.distribution_count || 0}</span> |
|
<span class="agent-stat-label">分发链接</span> |
|
</div> |
|
<div class="agent-stat"> |
|
<span class="agent-stat-value">${agent.usage_count || 0}</span> |
|
<span class="agent-stat-label">使用次数</span> |
|
</div> |
|
</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(); |
|
|
|
const typeText = getAgentTypeText(agent.type); |
|
|
|
|
|
let tagsHtml = '<div class="agent-tags">'; |
|
|
|
if (agent.subject) { |
|
tagsHtml += `<span class="agent-tag"><i class="bi bi-book"></i> ${agent.subject}</span>`; |
|
} |
|
|
|
if (agent.instructor) { |
|
tagsHtml += `<span class="agent-tag"><i class="bi bi-person"></i> ${agent.instructor}</span>`; |
|
} |
|
|
|
if (agent.plugins && agent.plugins.length > 0) { |
|
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'; |
|
} |
|
|
|
tagsHtml += ` |
|
<span class="agent-tag"> |
|
<i class="bi bi-${pluginIcon}"></i> ${pluginName} |
|
</span> |
|
`; |
|
});} |
|
|
|
tagsHtml += '</div>'; |
|
|
|
item.innerHTML = ` |
|
<div class="agent-content"> |
|
<div class="header"> |
|
<h5 class="title"><i class="bi bi-robot"></i> ${agent.name}</h5> |
|
<span class="badge badge-primary">${typeText}</span> |
|
</div> |
|
<div class="description">${agent.description || '暂无描述'}</div> |
|
${tagsHtml} |
|
<div class="agent-stats"> |
|
<div class="agent-stat"> |
|
<span class="agent-stat-value">${formattedDate}</span> |
|
<span class="agent-stat-label">创建时间</span> |
|
</div> |
|
<div class="agent-stat"> |
|
<span class="agent-stat-value">${agent.distribution_count || 0}</span> |
|
<span class="agent-stat-label">分发链接</span> |
|
</div> |
|
<div class="agent-stat"> |
|
<span class="agent-stat-value">${agent.usage_count || 0}</span> |
|
<span class="agent-stat-label">使用次数</span> |
|
</div> |
|
</div> |
|
<div class="agent-actions"> |
|
<button class="btn btn-sm btn-primary" onclick="showCreateDistributionModal('${agent.id}')"> |
|
<i class="bi bi-share me-1"></i> 分发 |
|
</button> |
|
<button class="btn btn-sm btn-light" onclick="showAgentDetail('${agent.id}')"> |
|
<i class="bi bi-info-circle me-1"></i> 详情 |
|
</button> |
|
<button class="btn btn-sm btn-light" onclick="editAgent('${agent.id}')"> |
|
<i class="bi bi-pencil me-1"></i> 编辑 |
|
</button> |
|
<button class="btn btn-sm btn-light text-danger" onclick="deleteAgent('${agent.id}')"> |
|
<i class="bi bi-trash me-1"></i> 删除 |
|
</button> |
|
</div> |
|
</div> |
|
`; |
|
|
|
agentListContainer.appendChild(item); |
|
}); |
|
} |
|
} |
|
} |
|
|
|
|
|
function getAgentTypeText(type) { |
|
switch (type) { |
|
case 'educational': return '教育辅导'; |
|
case 'programming': return '编程辅助'; |
|
case 'math': return '数学辅导'; |
|
case 'general': return '通用助手'; |
|
default: return type || '教育辅导'; |
|
} |
|
} |
|
|
|
|
|
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(); |
|
const typeText = getAgentTypeText(agent.type); |
|
|
|
|
|
let totalUsageCount = 0; |
|
if (agent.distributions && agent.distributions.length > 0) { |
|
agent.distributions.forEach(dist => { |
|
totalUsageCount += (dist.usage_count || 0); |
|
}); |
|
} |
|
|
|
|
|
modalBody.innerHTML = ` |
|
<div class="card mb-4"> |
|
<div class="card-header bg-light"> |
|
<h6 class="mb-0"><i class="bi bi-info-circle me-2"></i>基本信息</h6> |
|
</div> |
|
<div class="card-body"> |
|
<div class="mb-3"> |
|
<h6 class="text-muted mb-2">描述</h6> |
|
<p class="mb-0">${agent.description || '暂无描述'}</p> |
|
</div> |
|
|
|
<div class="row mb-3"> |
|
<div class="col-md-6"> |
|
<h6 class="text-muted mb-2">学科/课程</h6> |
|
<p class="mb-0">${agent.subject || '-'}</p> |
|
</div> |
|
<div class="col-md-6"> |
|
<h6 class="text-muted mb-2">指导教师</h6> |
|
<p class="mb-0">${agent.instructor || '-'}</p> |
|
</div> |
|
</div> |
|
|
|
<div class="row mb-0"> |
|
<div class="col-md-4"> |
|
<h6 class="text-muted mb-2">类型</h6> |
|
<p class="mb-0">${typeText}</p> |
|
</div> |
|
<div class="col-md-4"> |
|
<h6 class="text-muted mb-2">创建时间</h6> |
|
<p class="mb-0">${formattedDate}</p> |
|
</div> |
|
<div class="col-md-4"> |
|
<h6 class="text-muted mb-2">总使用次数</h6> |
|
<p class="mb-0"><span class="badge bg-primary">${totalUsageCount}</span></p> |
|
</div> |
|
</div> |
|
</div> |
|
</div> |
|
|
|
<div class="card mb-4"> |
|
<div class="card-header bg-light"> |
|
<h6 class="mb-0"><i class="bi bi-puzzle me-2"></i>插件</h6> |
|
</div> |
|
<div class="card-body"> |
|
${getPluginsHtml(agent.plugins)} |
|
</div> |
|
</div> |
|
|
|
<div class="card mb-4"> |
|
<div class="card-header bg-light"> |
|
<h6 class="mb-0"><i class="bi bi-journal-text me-2"></i>知识库</h6> |
|
</div> |
|
<div class="card-body"> |
|
${getKnowledgeBasesHtml(agent.knowledge_bases)} |
|
</div> |
|
</div> |
|
|
|
<div class="card mb-4"> |
|
<div class="card-header bg-light"> |
|
<h6 class="mb-0"><i class="bi bi-share me-2"></i>分发链接</h6> |
|
</div> |
|
<div class="card-body"> |
|
${getDistributionsHtml(agent.distributions, agent.id)} |
|
</div> |
|
</div> |
|
|
|
<div class="card mb-4"> |
|
<div class="card-header bg-light"> |
|
<h6 class="mb-0"><i class="bi bi-diagram-3 me-2"></i>工作流配置</h6> |
|
</div> |
|
<div class="card-body"> |
|
<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> |
|
</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() { |
|
|
|
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 getDistributionsHtml(distributions, agentId) { |
|
if (!distributions || distributions.length === 0) { |
|
return ` |
|
<div class="text-center py-3"> |
|
<p class="text-muted">暂无分发链接</p> |
|
<button class="btn btn-sm btn-primary" onclick="showCreateDistributionModal('${agentId}')"> |
|
<i class="bi bi-plus-lg me-1"></i> 创建分发链接 |
|
</button> |
|
</div> |
|
`; |
|
} |
|
|
|
let html = '<div class="mb-3">'; |
|
distributions.forEach(dist => { |
|
const expiryText = dist.expires_at > 0 |
|
? `过期时间: ${new Date(dist.expires_at * 1000).toLocaleString()}` |
|
: '永不过期'; |
|
|
|
|
|
const accessToken = `${agentId}?token=${dist.token}`; |
|
const fullUrl = `${window.location.origin}/student/${accessToken}`; |
|
|
|
html += ` |
|
<div class="p-3 mb-3 border rounded bg-light"> |
|
<div class="mb-3"> |
|
<h6 class="text-muted mb-2">完整链接</h6> |
|
<div class="input-group"> |
|
<input type="text" class="form-control" value="${fullUrl}" readonly> |
|
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('${fullUrl}')"> |
|
<i class="bi bi-clipboard"></i> 复制 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-3"> |
|
<h6 class="text-muted mb-2">学生免登录访问令牌</h6> |
|
<div class="input-group"> |
|
<input type="text" class="form-control" value="${accessToken}" readonly> |
|
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('${accessToken}')"> |
|
<i class="bi bi-clipboard"></i> 复制 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="d-flex justify-content-between"> |
|
<div class="small text-muted"> |
|
<span class="me-3">${expiryText}</span> |
|
</div> |
|
<div class="small text-muted"> |
|
<span>链接使用次数: ${dist.usage_count || 0}</span> |
|
</div> |
|
</div> |
|
</div> |
|
`; |
|
}); |
|
|
|
html += ` |
|
<div class="text-center mt-3"> |
|
<button class="btn btn-sm btn-primary" onclick="showCreateDistributionModal('${agentId}')"> |
|
<i class="bi bi-plus-lg me-1"></i> 创建新链接 |
|
</button> |
|
</div> |
|
</div>`; |
|
|
|
return html; |
|
} |
|
|
|
|
|
|
|
function getPluginsHtml(plugins) { |
|
if (!plugins || plugins.length === 0) { |
|
return '<p>无</p>'; |
|
} |
|
|
|
let html = '<div class="agent-tags">'; |
|
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'; |
|
} |
|
|
|
html += `<span class="agent-tag"><i class="bi bi-${pluginIcon}"></i> ${pluginName}</span>`; |
|
}); |
|
html += '</div>'; |
|
|
|
return html; |
|
} |
|
|
|
|
|
function getKnowledgeBasesHtml(knowledgeBases) { |
|
if (!knowledgeBases || knowledgeBases.length === 0) { |
|
return '<p>无</p>'; |
|
} |
|
|
|
let html = '<div class="agent-tags">'; |
|
knowledgeBases.forEach(kb => { |
|
|
|
const knowledge = allKnowledgeBases.find(k => k.id === kb); |
|
const knowledgeName = knowledge ? knowledge.name : kb; |
|
|
|
html += `<span class="agent-tag"><i class="bi bi-journal-text"></i> ${knowledgeName}</span>`; |
|
}); |
|
html += '</div>'; |
|
|
|
return html; |
|
} |
|
|
|
|
|
|
|
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'); |
|
const checkbox = item.querySelector('input[type="checkbox"]'); |
|
if (checkbox) 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出错,请查看控制台日志'); |
|
} |
|
} |
|
|
|
|
|
function editAgent(agentId) { |
|
const agent = allAgents.find(a => a.id === agentId); |
|
if (!agent) { |
|
alert('找不到要编辑的Agent'); |
|
return; |
|
} |
|
|
|
|
|
document.getElementById('agent-name').value = agent.name || ''; |
|
document.getElementById('agent-description').value = agent.description || ''; |
|
document.getElementById('agent-subject').value = agent.subject || ''; |
|
document.getElementById('agent-instructor').value = agent.instructor || ''; |
|
document.getElementById('agent-type').value = agent.type || 'educational'; |
|
|
|
|
|
selectedPlugins = agent.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; |
|
}); |
|
|
|
|
|
selectedKnowledgeBases = agent.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; |
|
}); |
|
|
|
|
|
currentWorkflow = agent.workflow || { nodes: [], edges: [] }; |
|
updateWorkflowCodeView(currentWorkflow); |
|
|
|
|
|
switchSection('create-agent'); |
|
nextTab('basic-tab'); |
|
|
|
|
|
} |
|
|
|
|
|
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 accessToken = `${agentId}?token=${result.distribution.token}`; |
|
const fullUrl = `${window.location.origin}/student/${accessToken}`; |
|
|
|
|
|
document.getElementById('showDistributionModal').querySelector('.modal-body').innerHTML = ` |
|
<div class="alert alert-success"> |
|
<i class="bi bi-check-circle me-2"></i> 链接创建成功!请复制以下链接分享给学生 |
|
</div> |
|
|
|
<div class="mb-3"> |
|
<h6 class="text-muted mb-2">完整链接</h6> |
|
<div class="input-group"> |
|
<input type="text" class="form-control" id="full-distribution-link" value="${fullUrl}" readonly> |
|
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('full-distribution-link')"> |
|
<i class="bi bi-clipboard"></i> 复制 |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<div class="mb-3"> |
|
<h6 class="text-muted mb-2">学生免登录访问令牌</h6> |
|
<div class="input-group"> |
|
<input type="text" class="form-control" id="access-token" value="${accessToken}" readonly> |
|
<button class="btn btn-outline-primary" type="button" onclick="copyToClipboard('access-token')"> |
|
<i class="bi bi-clipboard"></i> 复制 |
|
</button> |
|
</div> |
|
</div> |
|
`; |
|
|
|
|
|
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"> |
|
<i class="bi bi-diagram-3 fs-2 mb-3"></i> |
|
<p>暂无工作流数据</p> |
|
</div> |
|
`; |
|
return; |
|
} |
|
|
|
|
|
if (jsPlumbWorkflowInstance) { |
|
jsPlumbWorkflowInstance.reset(); |
|
} |
|
|
|
jsPlumbWorkflowInstance = jsPlumb.getInstance({ |
|
Endpoint: ["Dot", { radius: 4 }], |
|
Connector: ["Bezier", { curviness: 50 }], |
|
PaintStyle: { stroke: "#4a6cfd", strokeWidth: 2 }, |
|
HoverPaintStyle: { stroke: "#7b91ff", 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-header"> |
|
<div class="node-icon">${getNodeIcon(node.type)}</div> |
|
<div class="node-title">${node.data.name}</div> |
|
</div> |
|
<div class="node-content">${node.data.description || ''}</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: "#4a6cfd", 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> |