dashboard.html•23.1 kB
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ワークフロー管理ダッシュボード</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
text-align: center;
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-value {
font-size: 2rem;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
color: #666;
font-size: 0.9rem;
}
.main-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
.chart-container {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.chart-title {
font-size: 1.2rem;
font-weight: bold;
margin-bottom: 15px;
color: #333;
}
.workflow-section {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.workflow-header {
display: flex;
justify-content: between;
align-items: center;
margin-bottom: 15px;
}
.workflow-title {
font-size: 1.3rem;
font-weight: bold;
}
.workflow-status {
padding: 5px 15px;
border-radius: 20px;
font-size: 0.8rem;
font-weight: bold;
text-transform: uppercase;
}
.status-executing { background: #e3f2fd; color: #1976d2; }
.status-completed { background: #e8f5e8; color: #2e7d32; }
.status-planning { background: #fff3e0; color: #f57c00; }
.status-failed { background: #ffebee; color: #d32f2f; }
.progress-bar {
width: 100%;
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
margin: 10px 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #8bc34a);
transition: width 0.3s ease;
}
.task-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
}
.task-card {
padding: 15px;
border-radius: 8px;
border-left: 4px solid;
font-size: 0.9rem;
}
.task-pending {
background: #f5f5f5;
border-left-color: #9e9e9e;
}
.task-in_progress {
background: #e3f2fd;
border-left-color: #2196f3;
}
.task-completed {
background: #e8f5e8;
border-left-color: #4caf50;
}
.task-failed {
background: #ffebee;
border-left-color: #f44336;
}
.task-blocked {
background: #fff3e0;
border-left-color: #ff9800;
}
.task-title {
font-weight: bold;
margin-bottom: 5px;
}
.task-meta {
color: #666;
font-size: 0.8rem;
}
.agent-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.agent-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
.agent-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.agent-name {
font-size: 1.1rem;
font-weight: bold;
}
.agent-type {
padding: 3px 8px;
background: #f0f0f0;
border-radius: 15px;
font-size: 0.7rem;
text-transform: uppercase;
}
.load-indicator {
width: 100%;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
overflow: hidden;
margin: 10px 0;
}
.load-fill {
height: 100%;
transition: width 0.3s ease;
}
.load-low { background: #4caf50; }
.load-medium { background: #ff9800; }
.load-high { background: #f44336; }
.control-panel {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
margin-bottom: 20px;
}
.input-group {
margin-bottom: 15px;
}
.input-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.input-group input, .input-group textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 1rem;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 25px;
cursor: pointer;
font-size: 1rem;
font-weight: bold;
transition: transform 0.2s ease;
}
.btn:hover {
transform: translateY(-2px);
}
.btn:active {
transform: translateY(0);
}
#networkViz {
width: 100%;
height: 400px;
border: 1px solid #ddd;
border-radius: 8px;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.hidden {
display: none;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🤖 ワークフロー管理ダッシュボード</h1>
<p>AIによる自動ワークフロー生成・タスク分解・エージェントアサイン</p>
</div>
<!-- 統計情報 -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value" id="totalWorkflows">0</div>
<div class="stat-label">アクティブワークフロー</div>
</div>
<div class="stat-card">
<div class="stat-value" id="totalTasks">0</div>
<div class="stat-label">総タスク数</div>
</div>
<div class="stat-card">
<div class="stat-value" id="completedTasks">0</div>
<div class="stat-label">完了タスク</div>
</div>
<div class="stat-card">
<div class="stat-value" id="activeAgents">0</div>
<div class="stat-label">アクティブエージェント</div>
</div>
</div>
<!-- 制御パネル -->
<div class="control-panel">
<h3>新しいワークフロー作成</h3>
<div class="input-group">
<label for="workflowInput">要求内容:</label>
<textarea id="workflowInput" rows="3" placeholder="例: ECサイトの商品管理システムを開発してください"></textarea>
</div>
<button class="btn" onclick="generateWorkflow()">
<span id="generateBtn">ワークフロー生成</span>
<span id="generateLoading" class="loading hidden"></span>
</button>
</div>
<!-- メインチャート -->
<div class="main-grid">
<div class="chart-container">
<div class="chart-title">タスク状態分布</div>
<canvas id="taskStatusChart"></canvas>
</div>
<div class="chart-container">
<div class="chart-title">エージェント負荷</div>
<canvas id="agentLoadChart"></canvas>
</div>
</div>
<!-- ネットワーク可視化 -->
<div class="chart-container">
<div class="chart-title">タスク依存関係ネットワーク</div>
<div id="networkViz"></div>
</div>
<!-- ワークフロー一覧 -->
<div id="workflowList"></div>
<!-- エージェント一覧 -->
<div class="workflow-section">
<h3>エージェント状態</h3>
<div id="agentList" class="agent-section"></div>
</div>
</div>
<script>
// グローバル変数
let workflows = [];
let agents = [];
let taskStatusChart, agentLoadChart;
// 初期化
document.addEventListener('DOMContentLoaded', function() {
initCharts();
loadSampleData();
setInterval(updateDashboard, 5000); // 5秒ごとに更新
});
// チャート初期化
function initCharts() {
// タスク状態チャート
const taskCtx = document.getElementById('taskStatusChart').getContext('2d');
taskStatusChart = new Chart(taskCtx, {
type: 'doughnut',
data: {
labels: ['待機中', '実行中', '完了', '失敗', 'ブロック'],
datasets: [{
data: [0, 0, 0, 0, 0],
backgroundColor: ['#9e9e9e', '#2196f3', '#4caf50', '#f44336', '#ff9800']
}]
},
options: {
responsive: true,
plugins: {
legend: {
position: 'bottom'
}
}
}
});
// エージェント負荷チャート
const agentCtx = document.getElementById('agentLoadChart').getContext('2d');
agentLoadChart = new Chart(agentCtx, {
type: 'bar',
data: {
labels: [],
datasets: [{
label: '負荷率 (%)',
data: [],
backgroundColor: 'rgba(54, 162, 235, 0.6)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
// サンプルデータ読み込み
function loadSampleData() {
// サンプルワークフロー
workflows = [
{
id: '1',
title: 'ECサイト開発プロジェクト',
status: 'executing',
progress: 65,
tasks: [
{ id: '1', title: '要件定義', status: 'completed', agent: '分析エージェント' },
{ id: '2', title: 'UI設計', status: 'in_progress', agent: '開発エージェント' },
{ id: '3', title: 'バックエンド開発', status: 'pending', agent: null },
{ id: '4', title: 'テスト', status: 'blocked', agent: null }
]
}
];
// サンプルエージェント
agents = [
{ id: '1', name: '分析エージェント', type: 'analyst', load: 33, maxTasks: 3, currentTasks: 1 },
{ id: '2', name: '開発エージェント', type: 'developer', load: 67, maxTasks: 3, currentTasks: 2 },
{ id: '3', name: '研究エージェント', type: 'researcher', load: 0, maxTasks: 2, currentTasks: 0 }
];
updateDashboard();
}
// ダッシュボード更新
function updateDashboard() {
updateStatistics();
updateCharts();
updateWorkflowList();
updateAgentList();
updateNetworkVisualization();
}
// 統計情報更新
function updateStatistics() {
document.getElementById('totalWorkflows').textContent = workflows.length;
const totalTasks = workflows.reduce((sum, wf) => sum + wf.tasks.length, 0);
document.getElementById('totalTasks').textContent = totalTasks;
const completedTasks = workflows.reduce((sum, wf) =>
sum + wf.tasks.filter(t => t.status === 'completed').length, 0);
document.getElementById('completedTasks').textContent = completedTasks;
const activeAgents = agents.filter(a => a.currentTasks > 0).length;
document.getElementById('activeAgents').textContent = activeAgents;
}
// チャート更新
function updateCharts() {
// タスク状態分布
const allTasks = workflows.flatMap(wf => wf.tasks);
const statusCounts = {
pending: allTasks.filter(t => t.status === 'pending').length,
in_progress: allTasks.filter(t => t.status === 'in_progress').length,
completed: allTasks.filter(t => t.status === 'completed').length,
failed: allTasks.filter(t => t.status === 'failed').length,
blocked: allTasks.filter(t => t.status === 'blocked').length
};
taskStatusChart.data.datasets[0].data = [
statusCounts.pending,
statusCounts.in_progress,
statusCounts.completed,
statusCounts.failed,
statusCounts.blocked
];
taskStatusChart.update();
// エージェント負荷
agentLoadChart.data.labels = agents.map(a => a.name);
agentLoadChart.data.datasets[0].data = agents.map(a => a.load);
agentLoadChart.update();
}
// ワークフロー一覧更新
function updateWorkflowList() {
const container = document.getElementById('workflowList');
container.innerHTML = '';
workflows.forEach(workflow => {
const workflowDiv = document.createElement('div');
workflowDiv.className = 'workflow-section';
workflowDiv.innerHTML = `
<div class="workflow-header">
<div class="workflow-title">${workflow.title}</div>
<div class="workflow-status status-${workflow.status}">${workflow.status}</div>
</div>
<div class="progress-bar">
<div class="progress-fill" style="width: ${workflow.progress}%"></div>
</div>
<div class="task-grid">
${workflow.tasks.map(task => `
<div class="task-card task-${task.status}">
<div class="task-title">${task.title}</div>
<div class="task-meta">
状態: ${task.status}<br>
${task.agent ? `担当: ${task.agent}` : '未アサイン'}
</div>
</div>
`).join('')}
</div>
`;
container.appendChild(workflowDiv);
});
}
// エージェント一覧更新
function updateAgentList() {
const container = document.getElementById('agentList');
container.innerHTML = '';
agents.forEach(agent => {
const agentDiv = document.createElement('div');
agentDiv.className = 'agent-card';
let loadClass = 'load-low';
if (agent.load > 70) loadClass = 'load-high';
else if (agent.load > 40) loadClass = 'load-medium';
agentDiv.innerHTML = `
<div class="agent-header">
<div class="agent-name">${agent.name}</div>
<div class="agent-type">${agent.type}</div>
</div>
<div class="load-indicator">
<div class="load-fill ${loadClass}" style="width: ${agent.load}%"></div>
</div>
<div class="task-meta">
負荷: ${agent.load}% (${agent.currentTasks}/${agent.maxTasks})
</div>
`;
container.appendChild(agentDiv);
});
}
// ネットワーク可視化更新
function updateNetworkVisualization() {
const container = d3.select('#networkViz');
container.selectAll('*').remove();
if (workflows.length === 0) return;
const workflow = workflows[0]; // 最初のワークフローを可視化
const tasks = workflow.tasks;
const width = 800;
const height = 400;
const svg = container.append('svg')
.attr('width', width)
.attr('height', height);
// ノードとリンクのデータ作成
const nodes = tasks.map(task => ({
id: task.id,
title: task.title,
status: task.status,
x: Math.random() * width,
y: Math.random() * height
}));
// シンプルな配置(実際の依存関係に基づく)
const simulation = d3.forceSimulation(nodes)
.force('charge', d3.forceManyBody().strength(-300))
.force('center', d3.forceCenter(width / 2, height / 2))
.force('collision', d3.forceCollide().radius(30));
// ノード描画
const node = svg.selectAll('.node')
.data(nodes)
.enter().append('g')
.attr('class', 'node');
node.append('circle')
.attr('r', 20)
.attr('fill', d => {
const colors = {
pending: '#9e9e9e',
in_progress: '#2196f3',
completed: '#4caf50',
failed: '#f44336',
blocked: '#ff9800'
};
return colors[d.status] || '#9e9e9e';
});
node.append('text')
.attr('text-anchor', 'middle')
.attr('dy', '.35em')
.attr('font-size', '10px')
.attr('fill', 'white')
.text(d => d.id);
// シミュレーション更新
simulation.on('tick', () => {
node.attr('transform', d => `translate(${d.x},${d.y})`);
});
}
// ワークフロー生成
async function generateWorkflow() {
const input = document.getElementById('workflowInput');
const btnText = document.getElementById('generateBtn');
const btnLoading = document.getElementById('generateLoading');
if (!input.value.trim()) {
alert('要求内容を入力してください');
return;
}
// ローディング表示
btnText.classList.add('hidden');
btnLoading.classList.remove('hidden');
try {
// サンプル応答(実際はAPIを呼び出し)
await new Promise(resolve => setTimeout(resolve, 2000));
const newWorkflow = {
id: Date.now().toString(),
title: `生成されたワークフロー: ${input.value.substring(0, 20)}...`,
status: 'planning',
progress: 0,
tasks: [
{ id: `${Date.now()}-1`, title: '要件分析', status: 'pending', agent: null },
{ id: `${Date.now()}-2`, title: '設計', status: 'pending', agent: null },
{ id: `${Date.now()}-3`, title: '実装', status: 'pending', agent: null },
{ id: `${Date.now()}-4`, title: 'テスト', status: 'pending', agent: null }
]
};
workflows.push(newWorkflow);
input.value = '';
updateDashboard();
alert('ワークフローが生成されました!');
} catch (error) {
alert('ワークフロー生成に失敗しました: ' + error.message);
} finally {
// ローディング非表示
btnText.classList.remove('hidden');
btnLoading.classList.add('hidden');
}
}
</script>
</body>
</html>