feat: complete Phase 1 MVP - add agent panel, email notifications, and PDF invoicing
Agent Panel UI: - Visual dashboard showing all agents with status (idle/working) - Agent cards with stats: total tasks, completed tasks - Capabilities display for each agent - Execution log showing recent agent activity - Real-time updates (refreshes every 10 seconds) - Click agent to filter execution log - Status indicators: pending, running, completed, failed, pending_approval Email Notifications: - SMTP email service with HTML templates - Task assignment notifications - Agent approval request emails with action details - Agent completion notifications - Configurable via environment variables (SMTP_HOST, SMTP_USER, SMTP_PASSWORD, SMTP_TO) - Integrated with agent executor service PDF Invoice Generation: - Professional PDF invoices using ReportLab - Generate from time entries by project - Client information and company branding - Itemized time entries with hours, rates, amounts - Automatic totals calculation - Download as PDF attachment - API endpoints: /api/opc/invoices/generate - Projects and clients management endpoints Additional Features: - Projects CRUD API - Clients CRUD API - Project time tracking summary - Billable vs non-billable hours tracking Phase 1 MVP 100% Complete: ✅ PostgreSQL database with full schema ✅ Task CRUD API with automatic time tracking ✅ Kanban board with drag-and-drop ✅ Agent executor service with LLM integration ✅ WebSocket real-time updates ✅ 3 core agents (PM, CTO, COO) ✅ Agent panel UI with execution logs ✅ Email notifications (Telegram + Email) ✅ PDF invoice generation All 12 tasks completed!
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { getAgents, getExecutions } from "../../lib/opc-api.js";
|
||||
|
||||
let agents = $state([]);
|
||||
let executions = $state([]);
|
||||
let loading = $state(true);
|
||||
let selectedAgent = $state(null);
|
||||
|
||||
async function loadData() {
|
||||
loading = true;
|
||||
try {
|
||||
const [agentsRes, executionsRes] = await Promise.all([
|
||||
getAgents(),
|
||||
getExecutions({ limit: 20 })
|
||||
]);
|
||||
agents = agentsRes.items || [];
|
||||
executions = executionsRes.items || [];
|
||||
} catch (e) {
|
||||
console.error("Failed to load agent data:", e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getAgentIcon(agentId) {
|
||||
const icons = {
|
||||
pm: "📋",
|
||||
cto: "💻",
|
||||
coo: "⚙️",
|
||||
ceo: "🎯",
|
||||
marketing: "📢",
|
||||
social_media: "📱"
|
||||
};
|
||||
return icons[agentId] || "🤖";
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
pending: "bg-slate-100 text-slate-700",
|
||||
running: "bg-blue-100 text-blue-700",
|
||||
completed: "bg-emerald-100 text-emerald-700",
|
||||
failed: "bg-rose-100 text-rose-700",
|
||||
pending_approval: "bg-amber-100 text-amber-700"
|
||||
};
|
||||
return colors[status] || colors.pending;
|
||||
}
|
||||
|
||||
function getAgentExecutions(agentId) {
|
||||
return executions.filter(e => e.agent_id === agentId);
|
||||
}
|
||||
|
||||
function formatTimestamp(timestamp) {
|
||||
if (!timestamp) return "N/A";
|
||||
return new Date(timestamp).toLocaleString();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
loadData();
|
||||
// Refresh every 10 seconds
|
||||
const interval = setInterval(loadData, 10000);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-xl font-semibold text-surface-900 dark:text-surface-100">
|
||||
AI Agents
|
||||
</h2>
|
||||
|
||||
{#if loading}
|
||||
<div class="text-surface-600 dark:text-surface-400">Loading agents...</div>
|
||||
{:else}
|
||||
<!-- Agent Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{#each agents as agent}
|
||||
{@const agentExecs = getAgentExecutions(agent.id)}
|
||||
{@const runningExecs = agentExecs.filter(e => e.status === "running")}
|
||||
{@const completedExecs = agentExecs.filter(e => e.status === "completed")}
|
||||
|
||||
<button
|
||||
onclick={() => selectedAgent = selectedAgent?.id === agent.id ? null : agent}
|
||||
class="bg-white dark:bg-surface-800 rounded-lg p-4 border-2 transition-all text-left hover:shadow-lg {selectedAgent?.id === agent.id ? 'border-primary-500' : 'border-surface-200 dark:border-surface-700'}"
|
||||
>
|
||||
<!-- Agent Header -->
|
||||
<div class="flex items-start justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-3xl">{getAgentIcon(agent.id)}</span>
|
||||
<div>
|
||||
<h3 class="font-semibold text-surface-900 dark:text-surface-100">
|
||||
{agent.name}
|
||||
</h3>
|
||||
<p class="text-xs text-surface-600 dark:text-surface-400">
|
||||
{agent.role}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{#if runningExecs.length > 0}
|
||||
<span class="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
|
||||
<span class="animate-pulse">●</span>
|
||||
Working
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-xs text-surface-500">Idle</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Agent Stats -->
|
||||
<div class="grid grid-cols-2 gap-2 text-sm">
|
||||
<div class="bg-surface-50 dark:bg-surface-900 rounded p-2">
|
||||
<div class="text-xs text-surface-600 dark:text-surface-400">Total Tasks</div>
|
||||
<div class="text-lg font-semibold text-surface-900 dark:text-surface-100">
|
||||
{agentExecs.length}
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-surface-50 dark:bg-surface-900 rounded p-2">
|
||||
<div class="text-xs text-surface-600 dark:text-surface-400">Completed</div>
|
||||
<div class="text-lg font-semibold text-emerald-600 dark:text-emerald-400">
|
||||
{completedExecs.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Capabilities -->
|
||||
<div class="mt-3">
|
||||
<div class="text-xs text-surface-600 dark:text-surface-400 mb-1">Capabilities:</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
{#each agent.capabilities.slice(0, 3) as capability}
|
||||
<span class="text-xs px-2 py-0.5 rounded bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300">
|
||||
{capability.replace(/_/g, ' ')}
|
||||
</span>
|
||||
{/each}
|
||||
{#if agent.capabilities.length > 3}
|
||||
<span class="text-xs text-surface-500">+{agent.capabilities.length - 3}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Execution Log -->
|
||||
<div class="bg-white dark:bg-surface-800 rounded-lg border border-surface-200 dark:border-surface-700">
|
||||
<div class="p-4 border-b border-surface-200 dark:border-surface-700">
|
||||
<h3 class="font-semibold text-surface-900 dark:text-surface-100">
|
||||
{selectedAgent ? `${selectedAgent.name} Execution Log` : "Recent Executions"}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="divide-y divide-surface-200 dark:divide-surface-700 max-h-96 overflow-y-auto">
|
||||
{#each (selectedAgent ? getAgentExecutions(selectedAgent.id) : executions) as execution}
|
||||
<div class="p-4 hover:bg-surface-50 dark:hover:bg-surface-900/50">
|
||||
<div class="flex items-start justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xl">{getAgentIcon(execution.agent_id)}</span>
|
||||
<div>
|
||||
<div class="font-medium text-surface-900 dark:text-surface-100">
|
||||
Task #{execution.task_id}
|
||||
</div>
|
||||
<div class="text-xs text-surface-600 dark:text-surface-400">
|
||||
{formatTimestamp(execution.started_at)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-xs px-2 py-1 rounded {getStatusColor(execution.status)}">
|
||||
{execution.status.replace(/_/g, ' ')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{#if execution.output_result?.reasoning}
|
||||
<p class="text-sm text-surface-700 dark:text-surface-300 mb-2">
|
||||
{execution.output_result.reasoning}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
{#if execution.actions_proposed && execution.actions_proposed.length > 0}
|
||||
<div class="text-xs text-surface-600 dark:text-surface-400">
|
||||
Actions: {execution.actions_proposed.length}
|
||||
{#each execution.actions_proposed.slice(0, 2) as action}
|
||||
<span class="ml-2 px-1.5 py-0.5 rounded bg-surface-100 dark:bg-surface-700">
|
||||
{action.type}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if execution.error_message}
|
||||
<div class="mt-2 text-xs text-rose-600 dark:text-rose-400">
|
||||
Error: {execution.error_message}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-8 text-center text-surface-500">
|
||||
No executions yet
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user