mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-23 18:29:18 -05:00
Updates to get Docker working and adding Claude OAUTH token variable, and finish of the style guide mockup.
This commit is contained in:
@@ -31,6 +31,9 @@ LOG_LEVEL=INFO
|
||||
# Get your API key from: https://console.anthropic.com/
|
||||
# Required for the agent work orders service to execute Claude CLI commands
|
||||
ANTHROPIC_API_KEY=
|
||||
# Generate an OAUTH token in terminal and it will use your Claude OAUTH token from your subscription.
|
||||
CLAUDE_CODE_OAUTH_TOKEN=
|
||||
|
||||
|
||||
# GitHub Personal Access Token (Required for Agent Work Orders PR creation)
|
||||
# Get your token from: https://github.com/settings/tokens
|
||||
@@ -55,7 +58,7 @@ ARCHON_DOCS_PORT=3838
|
||||
# Default: false (feature disabled)
|
||||
# Set to "true" to enable: ENABLE_AGENT_WORK_ORDERS=true
|
||||
# When enabled, requires Claude API key and GitHub PAT (see above)
|
||||
ENABLE_AGENT_WORK_ORDERS=false
|
||||
ENABLE_AGENT_WORK_ORDERS=true
|
||||
|
||||
# Agent Work Orders Service Configuration (Optional)
|
||||
# Only needed if ENABLE_AGENT_WORK_ORDERS=true
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Button } from "@/features/ui/primitives/button";
|
||||
import { Card } from "@/features/ui/primitives/card";
|
||||
import { cn } from "@/features/ui/primitives/styles";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/features/ui/primitives/tooltip";
|
||||
import { RealTimeStatsExample } from "./components/RealTimeStatsExample";
|
||||
import { StepHistoryCard } from "./components/StepHistoryCard";
|
||||
import { WorkflowStepButton } from "./components/WorkflowStepButton";
|
||||
|
||||
@@ -116,6 +117,9 @@ export const AgentWorkOrderExample = () => {
|
||||
collapsible history, and integrated document editing for human-in-the-loop approval.
|
||||
</p>
|
||||
|
||||
{/* Real-Time Execution Stats */}
|
||||
<RealTimeStatsExample status="plan" stepNumber={2} />
|
||||
|
||||
{/* Workflow Progress Bar */}
|
||||
<Card blur="md" transparency="light" edgePosition="top" edgeColor="cyan" size="lg" className="overflow-visible">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import {
|
||||
Activity,
|
||||
CheckCircle2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
Copy,
|
||||
Eye,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
Pin,
|
||||
Play,
|
||||
Plus,
|
||||
Search,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
@@ -24,6 +27,7 @@ import { SelectableCard } from "@/features/ui/primitives/selectable-card";
|
||||
import { cn } from "@/features/ui/primitives/styles";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/features/ui/primitives/tooltip";
|
||||
import { AgentWorkOrderExample } from "./AgentWorkOrderExample";
|
||||
import { RealTimeStatsExample } from "./components/RealTimeStatsExample";
|
||||
|
||||
const MOCK_REPOSITORIES = [
|
||||
{
|
||||
@@ -69,7 +73,7 @@ interface WorkOrder {
|
||||
|
||||
const MOCK_WORK_ORDERS: WorkOrder[] = [
|
||||
{
|
||||
id: "wo-1",
|
||||
id: "wo-1dc27d9e",
|
||||
repositoryId: "1",
|
||||
repositoryName: "archon-frontend",
|
||||
request: "Add dark mode toggle to settings page",
|
||||
@@ -78,7 +82,7 @@ const MOCK_WORK_ORDERS: WorkOrder[] = [
|
||||
createdAt: "2024-01-15T10:30:00Z",
|
||||
},
|
||||
{
|
||||
id: "wo-2",
|
||||
id: "wo-2af8b3c1",
|
||||
repositoryId: "1",
|
||||
repositoryName: "archon-frontend",
|
||||
request: "Refactor navigation component to use new design system",
|
||||
@@ -87,7 +91,7 @@ const MOCK_WORK_ORDERS: WorkOrder[] = [
|
||||
createdAt: "2024-01-15T09:15:00Z",
|
||||
},
|
||||
{
|
||||
id: "wo-3",
|
||||
id: "wo-4e372af3",
|
||||
repositoryId: "2",
|
||||
repositoryName: "archon-backend",
|
||||
request: "Implement caching layer for API responses",
|
||||
@@ -96,7 +100,7 @@ const MOCK_WORK_ORDERS: WorkOrder[] = [
|
||||
createdAt: "2024-01-14T16:45:00Z",
|
||||
},
|
||||
{
|
||||
id: "wo-4",
|
||||
id: "wo-8b91f2d6",
|
||||
repositoryId: "2",
|
||||
repositoryName: "archon-backend",
|
||||
request: "Add rate limiting to authentication endpoints",
|
||||
@@ -105,7 +109,7 @@ const MOCK_WORK_ORDERS: WorkOrder[] = [
|
||||
createdAt: "2024-01-14T14:20:00Z",
|
||||
},
|
||||
{
|
||||
id: "wo-5",
|
||||
id: "wo-5c7d4a89",
|
||||
repositoryId: "1",
|
||||
repositoryName: "archon-frontend",
|
||||
request: "Fix responsive layout issues on mobile devices",
|
||||
@@ -114,7 +118,7 @@ const MOCK_WORK_ORDERS: WorkOrder[] = [
|
||||
createdAt: "2024-01-13T11:00:00Z",
|
||||
},
|
||||
{
|
||||
id: "wo-6",
|
||||
id: "wo-9f3e1b5a",
|
||||
repositoryId: "3",
|
||||
repositoryName: "archon-docs",
|
||||
request: "Update API documentation with new endpoints",
|
||||
@@ -126,7 +130,7 @@ const MOCK_WORK_ORDERS: WorkOrder[] = [
|
||||
|
||||
export const AgentWorkOrderLayoutExample = () => {
|
||||
const [selectedRepositoryId, setSelectedRepositoryId] = useState("1");
|
||||
const [layoutMode, setLayoutMode] = useState<"horizontal" | "sidebar">("horizontal");
|
||||
const [layoutMode, setLayoutMode] = useState<"horizontal" | "sidebar">("sidebar");
|
||||
const [sidebarExpanded, setSidebarExpanded] = useState(true);
|
||||
const [showAddRepoModal, setShowAddRepoModal] = useState(false);
|
||||
const [showNewWorkOrderModal, setShowNewWorkOrderModal] = useState(false);
|
||||
@@ -134,6 +138,7 @@ export const AgentWorkOrderLayoutExample = () => {
|
||||
const [activeTab, setActiveTab] = useState<string>("all");
|
||||
const [showDetailView, setShowDetailView] = useState(false);
|
||||
const [selectedWorkOrderId, setSelectedWorkOrderId] = useState<string | null>(null);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const selectedRepository = MOCK_REPOSITORIES.find((r) => r.id === selectedRepositoryId);
|
||||
const selectedWorkOrder = workOrders.find((wo) => wo.id === selectedWorkOrderId);
|
||||
@@ -183,9 +188,42 @@ export const AgentWorkOrderLayoutExample = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Layout Mode Toggle */}
|
||||
<div className="flex justify-end">
|
||||
<div className="flex gap-1 p-1 bg-black/30 rounded-lg border border-white/10">
|
||||
{/* Header Section */}
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* Title */}
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Agent Work Orders</h1>
|
||||
|
||||
{/* Search Bar */}
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400 dark:text-gray-500"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search repositories..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
aria-label="Search repositories"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* View Toggle - Sidebar is default/primary */}
|
||||
<div className="flex gap-1 p-1 bg-black/30 dark:bg-white/10 rounded-lg border border-white/10 dark:border-gray-700">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setLayoutMode("sidebar")}
|
||||
className={cn(
|
||||
"px-3",
|
||||
layoutMode === "sidebar" && "bg-purple-500/20 dark:bg-purple-500/30 text-purple-400 dark:text-purple-300",
|
||||
)}
|
||||
aria-label="Switch to sidebar layout"
|
||||
aria-pressed={layoutMode === "sidebar"}
|
||||
>
|
||||
<List className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@@ -200,22 +238,18 @@ export const AgentWorkOrderLayoutExample = () => {
|
||||
>
|
||||
<LayoutGrid className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setLayoutMode("sidebar")}
|
||||
className={cn(
|
||||
"px-3",
|
||||
layoutMode === "sidebar" && "bg-purple-500/20 dark:bg-purple-500/30 text-purple-400 dark:text-purple-300",
|
||||
)}
|
||||
aria-label="Switch to sidebar layout"
|
||||
aria-pressed={layoutMode === "sidebar"}
|
||||
>
|
||||
<List className="w-4 h-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* New Repo Button */}
|
||||
<Button variant="cyan" onClick={() => setShowAddRepoModal(true)} aria-label="Add new repository">
|
||||
<Plus className="w-4 h-4 mr-2" aria-hidden="true" />
|
||||
New Repo
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Add Repository Modal */}
|
||||
<AddRepositoryModal open={showAddRepoModal} onOpenChange={setShowAddRepoModal} />
|
||||
|
||||
{layoutMode === "horizontal" ? (
|
||||
<>
|
||||
{/* Horizontal Repository Cards - ONLY cards scroll, not whole page */}
|
||||
@@ -233,8 +267,6 @@ export const AgentWorkOrderLayoutExample = () => {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{/* Add Repository Button */}
|
||||
<AddRepositoryModal open={showAddRepoModal} onOpenChange={setShowAddRepoModal} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -756,8 +788,9 @@ const WorkOrdersTableView = ({
|
||||
<thead>
|
||||
<tr className="bg-gradient-to-r from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 border-b-2 border-gray-200 dark:border-gray-700">
|
||||
<th className="w-12" aria-label="Status indicator" />
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Work Order ID
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">WO ID</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300 w-40">
|
||||
Repository
|
||||
</th>
|
||||
<th className="px-4 py-3 text-left text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Request Summary
|
||||
@@ -783,7 +816,7 @@ const WorkOrdersTableView = ({
|
||||
);
|
||||
};
|
||||
|
||||
// Work Order Row with status-based styling
|
||||
// Work Order Row with status-based styling and expandable real-time stats
|
||||
const WorkOrderRow = ({
|
||||
workOrder,
|
||||
index,
|
||||
@@ -795,103 +828,165 @@ const WorkOrderRow = ({
|
||||
onStart: () => void;
|
||||
onViewDetails: () => void;
|
||||
}) => {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// Status colors - STATIC lookup with all properties
|
||||
const statusColors: Record<
|
||||
WorkOrderStatus,
|
||||
{ color: "pink" | "cyan" | "blue" | "orange" | "purple" | "green"; edge: string; glow: string; label: string }
|
||||
{
|
||||
color: "pink" | "cyan" | "blue" | "orange" | "purple" | "green";
|
||||
edge: string;
|
||||
glow: string;
|
||||
label: string;
|
||||
stepNumber: number;
|
||||
}
|
||||
> = {
|
||||
pending: {
|
||||
color: "pink",
|
||||
edge: "bg-pink-500",
|
||||
glow: "rgba(236,72,153,0.5)",
|
||||
label: "Pending",
|
||||
stepNumber: 0,
|
||||
},
|
||||
create_branch: {
|
||||
color: "cyan",
|
||||
edge: "bg-cyan-500",
|
||||
glow: "rgba(34,211,238,0.5)",
|
||||
label: "+ Branch",
|
||||
stepNumber: 1,
|
||||
},
|
||||
plan: {
|
||||
color: "blue",
|
||||
edge: "bg-blue-500",
|
||||
glow: "rgba(59,130,246,0.5)",
|
||||
label: "Planning",
|
||||
stepNumber: 2,
|
||||
},
|
||||
execute: {
|
||||
color: "orange",
|
||||
edge: "bg-orange-500",
|
||||
glow: "rgba(249,115,22,0.5)",
|
||||
label: "Executing",
|
||||
stepNumber: 3,
|
||||
},
|
||||
commit: {
|
||||
color: "purple",
|
||||
edge: "bg-purple-500",
|
||||
glow: "rgba(168,85,247,0.5)",
|
||||
label: "Commit",
|
||||
stepNumber: 4,
|
||||
},
|
||||
create_pr: {
|
||||
color: "green",
|
||||
edge: "bg-green-500",
|
||||
glow: "rgba(34,197,94,0.5)",
|
||||
label: "Create PR",
|
||||
stepNumber: 5,
|
||||
},
|
||||
};
|
||||
|
||||
const colors = statusColors[workOrder.status];
|
||||
const canExpand = workOrder.status !== "pending";
|
||||
|
||||
const handleStart = () => {
|
||||
setIsExpanded(true); // Auto-expand when started
|
||||
onStart();
|
||||
};
|
||||
|
||||
return (
|
||||
<tr
|
||||
className={cn(
|
||||
"group transition-all duration-200",
|
||||
index % 2 === 0 ? "bg-white/50 dark:bg-black/50" : "bg-gray-50/80 dark:bg-gray-900/30",
|
||||
"hover:bg-gradient-to-r hover:from-cyan-50/70 hover:to-purple-50/70 dark:hover:from-cyan-900/20 dark:hover:to-purple-900/20",
|
||||
"border-b border-gray-200 dark:border-gray-800",
|
||||
)}
|
||||
>
|
||||
{/* Status indicator - glowing circle */}
|
||||
<td className="px-3 py-2 w-12">
|
||||
<div className="flex items-center justify-center">
|
||||
<div className={cn("w-3 h-3 rounded-full", colors.edge)} style={{ boxShadow: `0 0 8px ${colors.glow}` }} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Work Order ID */}
|
||||
<td className="px-4 py-2">
|
||||
<span className="font-mono text-sm text-gray-700 dark:text-gray-300">{workOrder.id}</span>
|
||||
</td>
|
||||
|
||||
{/* Request Summary */}
|
||||
<td className="px-4 py-2">
|
||||
<p className="text-sm text-gray-900 dark:text-white line-clamp-2">{workOrder.request}</p>
|
||||
</td>
|
||||
|
||||
{/* Status Badge - using StatPill */}
|
||||
<td className="px-4 py-2 w-32">
|
||||
<StatPill color={colors.color} value={colors.label} size="sm" />
|
||||
</td>
|
||||
|
||||
{/* Actions */}
|
||||
<td className="px-4 py-2 w-32">
|
||||
{workOrder.status === "pending" ? (
|
||||
<Button onClick={onStart} size="xs" variant="green" className="w-full text-xs" aria-label="Start work order">
|
||||
<Play className="w-3 h-3 mr-1" aria-hidden="true" />
|
||||
Start
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onViewDetails}
|
||||
size="xs"
|
||||
variant="blue"
|
||||
className="w-full text-xs"
|
||||
aria-label="Observe work order details"
|
||||
>
|
||||
<Eye className="w-3 h-3 mr-1" aria-hidden="true" />
|
||||
Observe
|
||||
</Button>
|
||||
<>
|
||||
<tr
|
||||
className={cn(
|
||||
"group transition-all duration-200",
|
||||
index % 2 === 0 ? "bg-white/50 dark:bg-black/50" : "bg-gray-50/80 dark:bg-gray-900/30",
|
||||
"hover:bg-gradient-to-r hover:from-cyan-50/70 hover:to-purple-50/70 dark:hover:from-cyan-900/20 dark:hover:to-purple-900/20",
|
||||
"border-b border-gray-200 dark:border-gray-800",
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
>
|
||||
{/* Status indicator - glowing circle with optional collapse button */}
|
||||
<td className="px-3 py-2 w-12">
|
||||
<div className="flex items-center justify-center gap-1">
|
||||
{canExpand && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className="p-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 rounded transition-colors"
|
||||
aria-label={isExpanded ? "Collapse details" : "Expand details"}
|
||||
aria-expanded={isExpanded}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="w-3 h-3 text-gray-600 dark:text-gray-400" aria-hidden="true" />
|
||||
) : (
|
||||
<ChevronDown className="w-3 h-3 text-gray-600 dark:text-gray-400" aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
<div className={cn("w-3 h-3 rounded-full", colors.edge)} style={{ boxShadow: `0 0 8px ${colors.glow}` }} />
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Work Order ID */}
|
||||
<td className="px-4 py-2">
|
||||
<span className="font-mono text-sm text-gray-700 dark:text-gray-300">{workOrder.id}</span>
|
||||
</td>
|
||||
|
||||
{/* Repository */}
|
||||
<td className="px-4 py-2 w-40">
|
||||
<span className="text-sm text-gray-900 dark:text-white">{workOrder.repositoryName}</span>
|
||||
</td>
|
||||
|
||||
{/* Request Summary */}
|
||||
<td className="px-4 py-2">
|
||||
<p className="text-sm text-gray-900 dark:text-white line-clamp-2">{workOrder.request}</p>
|
||||
</td>
|
||||
|
||||
{/* Status Badge - using StatPill */}
|
||||
<td className="px-4 py-2 w-32">
|
||||
<StatPill color={colors.color} value={colors.label} size="sm" />
|
||||
</td>
|
||||
|
||||
{/* Actions */}
|
||||
<td className="px-4 py-2 w-32">
|
||||
{workOrder.status === "pending" ? (
|
||||
<Button
|
||||
onClick={handleStart}
|
||||
size="xs"
|
||||
variant="green"
|
||||
className="w-full text-xs"
|
||||
aria-label="Start work order"
|
||||
>
|
||||
<Play className="w-3 h-3 mr-1" aria-hidden="true" />
|
||||
Start
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onClick={onViewDetails}
|
||||
size="xs"
|
||||
variant="blue"
|
||||
className="w-full text-xs"
|
||||
aria-label="View work order details"
|
||||
>
|
||||
<Eye className="w-3 h-3 mr-1" aria-hidden="true" />
|
||||
Details
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{/* Expanded row with real-time stats */}
|
||||
{isExpanded && canExpand && (
|
||||
<tr
|
||||
className={cn(
|
||||
index % 2 === 0 ? "bg-white/50 dark:bg-black/50" : "bg-gray-50/80 dark:bg-gray-900/30",
|
||||
"border-b border-gray-200 dark:border-gray-800",
|
||||
)}
|
||||
>
|
||||
<td colSpan={6} className="px-4 py-4">
|
||||
<RealTimeStatsExample status={workOrder.status} stepNumber={colors.stepNumber} />
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -926,23 +1021,6 @@ const AddRepositoryModal = ({ open, onOpenChange }: { open: boolean; onOpenChang
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"w-72 min-h-[180px] flex flex-col items-center justify-center shrink-0",
|
||||
"rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-700",
|
||||
"hover:border-cyan-400 dark:hover:border-cyan-500",
|
||||
"transition-colors duration-200",
|
||||
"bg-white/30 dark:bg-black/20",
|
||||
"backdrop-blur-sm",
|
||||
)}
|
||||
aria-label="Add repository"
|
||||
>
|
||||
<Plus className="w-8 h-8 text-gray-400 dark:text-gray-500 mb-2" aria-hidden="true" />
|
||||
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">Add Repository</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Repository</DialogTitle>
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import { Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/features/ui/primitives/button";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/features/ui/primitives/select";
|
||||
import { cn } from "@/features/ui/primitives/styles";
|
||||
import { Switch } from "@/features/ui/primitives/switch";
|
||||
|
||||
interface ExecutionLogsExampleProps {
|
||||
/** Work order status to generate appropriate mock logs */
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface MockLog {
|
||||
timestamp: string;
|
||||
level: "info" | "warning" | "error" | "debug";
|
||||
event: string;
|
||||
step?: string;
|
||||
progress?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get color class for log level badge - STATIC lookup
|
||||
*/
|
||||
const logLevelColors: Record<string, string> = {
|
||||
info: "bg-blue-500/20 text-blue-600 dark:text-blue-400 border-blue-400/30",
|
||||
warning: "bg-yellow-500/20 text-yellow-600 dark:text-yellow-400 border-yellow-400/30",
|
||||
error: "bg-red-500/20 text-red-600 dark:text-red-400 border-red-400/30",
|
||||
debug: "bg-gray-500/20 text-gray-600 dark:text-gray-400 border-gray-400/30",
|
||||
};
|
||||
|
||||
/**
|
||||
* Format timestamp to relative time
|
||||
*/
|
||||
function formatRelativeTime(timestamp: string): string {
|
||||
const now = Date.now();
|
||||
const logTime = new Date(timestamp).getTime();
|
||||
const diffSeconds = Math.floor((now - logTime) / 1000);
|
||||
|
||||
if (diffSeconds < 60) return `${diffSeconds}s ago`;
|
||||
if (diffSeconds < 3600) return `${Math.floor(diffSeconds / 60)}m ago`;
|
||||
return `${Math.floor(diffSeconds / 3600)}h ago`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Individual log entry component
|
||||
*/
|
||||
function LogEntryRow({ log }: { log: MockLog }) {
|
||||
const colorClass = logLevelColors[log.level] || logLevelColors.debug;
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-2 py-1 px-2 hover:bg-white/5 dark:hover:bg-black/20 rounded font-mono text-sm">
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs whitespace-nowrap">
|
||||
{formatRelativeTime(log.timestamp)}
|
||||
</span>
|
||||
<span className={cn("px-1.5 py-0.5 rounded text-xs border uppercase whitespace-nowrap", colorClass)}>
|
||||
{log.level}
|
||||
</span>
|
||||
{log.step && <span className="text-cyan-600 dark:text-cyan-400 text-xs whitespace-nowrap">[{log.step}]</span>}
|
||||
<span className="text-gray-900 dark:text-gray-300 flex-1">{log.event}</span>
|
||||
{log.progress && (
|
||||
<span className="text-gray-500 dark:text-gray-400 text-xs whitespace-nowrap">{log.progress}</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExecutionLogsExample({ status }: ExecutionLogsExampleProps) {
|
||||
const [autoScroll, setAutoScroll] = useState(true);
|
||||
const [levelFilter, setLevelFilter] = useState<string>("all");
|
||||
|
||||
// Generate mock logs based on status
|
||||
const generateMockLogs = (): MockLog[] => {
|
||||
const now = Date.now();
|
||||
const baseTime = now - 300000; // 5 minutes ago
|
||||
|
||||
const logs: MockLog[] = [
|
||||
{ timestamp: new Date(baseTime).toISOString(), level: "info", event: "workflow_started" },
|
||||
{ timestamp: new Date(baseTime + 1000).toISOString(), level: "info", event: "sandbox_setup_started" },
|
||||
{
|
||||
timestamp: new Date(baseTime + 3000).toISOString(),
|
||||
level: "info",
|
||||
event: "repository_cloned",
|
||||
step: "setup",
|
||||
},
|
||||
{ timestamp: new Date(baseTime + 5000).toISOString(), level: "info", event: "sandbox_setup_completed" },
|
||||
];
|
||||
|
||||
if (status !== "pending") {
|
||||
logs.push(
|
||||
{
|
||||
timestamp: new Date(baseTime + 10000).toISOString(),
|
||||
level: "info",
|
||||
event: "step_started",
|
||||
step: "create-branch",
|
||||
progress: "1/5",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(baseTime + 12000).toISOString(),
|
||||
level: "info",
|
||||
event: "agent_command_started",
|
||||
step: "create-branch",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(baseTime + 45000).toISOString(),
|
||||
level: "info",
|
||||
event: "branch_created",
|
||||
step: "create-branch",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (status === "plan" || status === "execute" || status === "commit" || status === "create_pr") {
|
||||
logs.push(
|
||||
{
|
||||
timestamp: new Date(baseTime + 60000).toISOString(),
|
||||
level: "info",
|
||||
event: "step_started",
|
||||
step: "planning",
|
||||
progress: "2/5",
|
||||
},
|
||||
{
|
||||
timestamp: new Date(baseTime + 120000).toISOString(),
|
||||
level: "debug",
|
||||
event: "analyzing_codebase",
|
||||
step: "planning",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return logs;
|
||||
};
|
||||
|
||||
const mockLogs = generateMockLogs();
|
||||
const filteredLogs = levelFilter === "all" ? mockLogs : mockLogs.filter((log) => log.level === levelFilter);
|
||||
|
||||
return (
|
||||
<div className="border border-white/10 dark:border-gray-700/30 rounded-lg overflow-hidden bg-black/20 dark:bg-white/5 backdrop-blur">
|
||||
{/* Header with controls */}
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10 dark:border-gray-700/30 bg-gray-900/50 dark:bg-gray-800/30">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="font-semibold text-gray-900 dark:text-gray-300">Execution Logs</span>
|
||||
|
||||
{/* Live indicator */}
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
|
||||
<span className="text-xs text-green-600 dark:text-green-400">Live</span>
|
||||
</div>
|
||||
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400">({filteredLogs.length} entries)</span>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Level filter using proper Select primitive */}
|
||||
<Select value={levelFilter} onValueChange={setLevelFilter}>
|
||||
<SelectTrigger className="w-32 h-8 text-xs" aria-label="Filter log level">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Levels</SelectItem>
|
||||
<SelectItem value="info">Info</SelectItem>
|
||||
<SelectItem value="warning">Warning</SelectItem>
|
||||
<SelectItem value="error">Error</SelectItem>
|
||||
<SelectItem value="debug">Debug</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Auto-scroll toggle using Switch primitive */}
|
||||
<div className="flex items-center gap-2">
|
||||
<label htmlFor="auto-scroll-toggle" className="text-xs text-gray-700 dark:text-gray-300">
|
||||
Auto-scroll:
|
||||
</label>
|
||||
<Switch
|
||||
id="auto-scroll-toggle"
|
||||
checked={autoScroll}
|
||||
onCheckedChange={setAutoScroll}
|
||||
aria-label="Toggle auto-scroll"
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"text-xs font-medium",
|
||||
autoScroll ? "text-cyan-600 dark:text-cyan-400" : "text-gray-500 dark:text-gray-400",
|
||||
)}
|
||||
>
|
||||
{autoScroll ? "ON" : "OFF"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Clear logs button */}
|
||||
<Button variant="ghost" size="xs" aria-label="Clear logs">
|
||||
<Trash2 className="w-3 h-3" aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log content - scrollable area */}
|
||||
<div className="max-h-96 overflow-y-auto bg-black/40 dark:bg-black/20">
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-gray-500 dark:text-gray-400">
|
||||
<p>No logs match the current filter</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-2">
|
||||
{filteredLogs.map((log, index) => (
|
||||
<LogEntryRow key={`${log.timestamp}-${index}`} log={log} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import { Activity, ChevronDown, ChevronUp, Clock, TrendingUp } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/features/ui/primitives/button";
|
||||
import { ExecutionLogsExample } from "./ExecutionLogsExample";
|
||||
|
||||
interface RealTimeStatsExampleProps {
|
||||
/** Work order status for determining progress */
|
||||
status: string;
|
||||
/** Step number (1-5) */
|
||||
stepNumber: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format elapsed seconds to human-readable duration
|
||||
*/
|
||||
function formatDuration(seconds: number): string {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${secs}s`;
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${secs}s`;
|
||||
}
|
||||
return `${secs}s`;
|
||||
}
|
||||
|
||||
export function RealTimeStatsExample({ status, stepNumber }: RealTimeStatsExampleProps) {
|
||||
const [showLogs, setShowLogs] = useState(false);
|
||||
|
||||
// Mock data based on status
|
||||
const stepNames: Record<string, string> = {
|
||||
create_branch: "create-branch",
|
||||
plan: "planning",
|
||||
execute: "execute",
|
||||
commit: "commit",
|
||||
create_pr: "create-pr",
|
||||
};
|
||||
|
||||
const currentStep = stepNames[status] || "initializing";
|
||||
const progressPct = (stepNumber / 5) * 100;
|
||||
const mockElapsedSeconds = stepNumber * 120; // 2 minutes per step
|
||||
|
||||
const activities: Record<string, string> = {
|
||||
create_branch: "Creating new branch for work order...",
|
||||
plan: "Analyzing codebase and generating implementation plan...",
|
||||
execute: "Writing code and applying changes...",
|
||||
commit: "Committing changes to branch...",
|
||||
create_pr: "Creating pull request on GitHub...",
|
||||
};
|
||||
|
||||
const currentActivity = activities[status] || "Initializing workflow...";
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="border border-white/10 dark:border-gray-700/30 rounded-lg p-4 bg-black/20 dark:bg-white/5 backdrop-blur">
|
||||
<h3 className="text-sm font-semibold text-gray-900 dark:text-gray-300 mb-3 flex items-center gap-2">
|
||||
<Activity className="w-4 h-4" aria-hidden="true" />
|
||||
Real-Time Execution
|
||||
</h3>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{/* Current Step */}
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Current Step</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||
{currentStep}
|
||||
<span className="text-gray-500 dark:text-gray-400 ml-2">({stepNumber}/5)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress */}
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide flex items-center gap-1">
|
||||
<TrendingUp className="w-3 h-3" aria-hidden="true" />
|
||||
Progress
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 h-2 bg-gray-700 dark:bg-gray-200/20 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-cyan-500 to-blue-500 transition-all duration-500 ease-out"
|
||||
style={{ width: `${progressPct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm font-medium text-cyan-600 dark:text-cyan-400">{progressPct}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Elapsed Time */}
|
||||
<div className="space-y-1">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" aria-hidden="true" />
|
||||
Elapsed Time
|
||||
</div>
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-200">
|
||||
{formatDuration(mockElapsedSeconds)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Latest Activity with Status Indicator - at top */}
|
||||
<div className="mt-4 pt-3 border-t border-white/10 dark:border-gray-700/30">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-start gap-2 flex-1 min-w-0">
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide whitespace-nowrap">
|
||||
Latest Activity:
|
||||
</div>
|
||||
<div className="text-sm text-gray-900 dark:text-gray-300 flex-1 truncate">{currentActivity}</div>
|
||||
</div>
|
||||
{/* Status Indicator - right side of Latest Activity */}
|
||||
<div className="flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400 flex-shrink-0">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" />
|
||||
<span>Running</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Show Execution Logs button - at bottom */}
|
||||
<div className="mt-3 pt-3 border-t border-white/10 dark:border-gray-700/30">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowLogs(!showLogs)}
|
||||
className="w-full justify-center text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500/10"
|
||||
aria-label={showLogs ? "Hide execution logs" : "Show execution logs"}
|
||||
aria-expanded={showLogs}
|
||||
>
|
||||
{showLogs ? (
|
||||
<>
|
||||
<ChevronUp className="w-4 h-4 mr-1" aria-hidden="true" />
|
||||
Hide Execution Logs
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="w-4 h-4 mr-1" aria-hidden="true" />
|
||||
Show Execution Logs
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Collapsible Execution Logs */}
|
||||
{showLogs && <ExecutionLogsExample status={status} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Briefcase, Database, FileText, FolderKanban, Navigation, Settings } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { AgentWorkOrderExample } from "../layouts/AgentWorkOrderExample";
|
||||
import { AgentWorkOrderLayoutExample } from "../layouts/AgentWorkOrderLayoutExample";
|
||||
import { DocumentBrowserExample } from "../layouts/DocumentBrowserExample";
|
||||
import { KnowledgeLayoutExample } from "../layouts/KnowledgeLayoutExample";
|
||||
|
||||
@@ -172,6 +172,7 @@ services:
|
||||
- SUPABASE_SERVICE_KEY=${SUPABASE_SERVICE_KEY}
|
||||
- OPENAI_API_KEY=${OPENAI_API_KEY:-}
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY:-}
|
||||
- CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN:-}
|
||||
- LOGFIRE_TOKEN=${LOGFIRE_TOKEN:-}
|
||||
- LOG_LEVEL=${LOG_LEVEL:-INFO}
|
||||
- AGENT_WORK_ORDERS_PORT=${AGENT_WORK_ORDERS_PORT:-8053}
|
||||
|
||||
@@ -59,6 +59,10 @@ RUN mkdir -p /repos /tmp/agent-work-orders && \
|
||||
USER agentuser
|
||||
RUN curl -fsSL https://claude.ai/install.sh | bash
|
||||
|
||||
# Configure git to use gh CLI for GitHub authentication
|
||||
# This allows git clone to authenticate using GH_TOKEN environment variable
|
||||
RUN git config --global credential.helper '!gh auth git-credential'
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONPATH="/app:$PYTHONPATH"
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
|
||||
Reference in New Issue
Block a user