436 lines
14 KiB
React
436 lines
14 KiB
React
|
|
import React, { useCallback, useEffect, useState } from 'react';
|
|||
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|||
|
|
import { Badge } from '@/components/ui/badge';
|
|||
|
|
import { Button } from '@/components/ui/button';
|
|||
|
|
import { ChevronDown } from 'lucide-react';
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Simple Collapsible Component (no external dependencies)
|
|||
|
|
*/
|
|||
|
|
function SimpleCollapsible({ defaultOpen = false, children, title }) {
|
|||
|
|
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="mb-3 group">
|
|||
|
|
<div
|
|||
|
|
className="flex items-center justify-between p-3 cursor-pointer hover:bg-muted/30 transition-colors rounded-t-xl"
|
|||
|
|
onClick={() => setIsOpen(!isOpen)}
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
{title}
|
|||
|
|
</div>
|
|||
|
|
<ChevronDown
|
|||
|
|
className={`h-4 w-4 text-muted-foreground transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
{isOpen && (
|
|||
|
|
<div className="border-x border-b border-border/70 rounded-b-xl bg-background/65 p-3">
|
|||
|
|
{children}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Priority mapping for color coding
|
|||
|
|
const PRIORITY_COLORS = {
|
|||
|
|
'🔴': { bg: 'bg-red-500/10', border: 'border-l-4 border-red-500', text: 'text-red-600', label: 'CRITICAL' },
|
|||
|
|
'🟠': { bg: 'bg-orange-500/10', border: 'border-l-4 border-orange-500', text: 'text-orange-600', label: 'HIGH' },
|
|||
|
|
'🟡': { bg: 'bg-yellow-500/10', border: 'border-l-4 border-yellow-500', text: 'text-yellow-600', label: 'MEDIUM' },
|
|||
|
|
'🔵': { bg: 'bg-blue-500/10', border: 'border-l-4 border-blue-500', text: 'text-blue-600', label: 'LOW' },
|
|||
|
|
'💭': { bg: 'bg-gray-500/10', border: 'border-l-4 border-gray-500', text: 'text-gray-600', label: 'NICE TO HAVE' },
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Parse FUTURE.md content into structured roadmap items
|
|||
|
|
*/
|
|||
|
|
function parseFutureMarkdown(markdown) {
|
|||
|
|
const items = [];
|
|||
|
|
const lines = markdown.split('\n');
|
|||
|
|
|
|||
|
|
let currentPriority = null;
|
|||
|
|
let currentItem = null;
|
|||
|
|
|
|||
|
|
for (let i = 0; i < lines.length; i++) {
|
|||
|
|
const line = lines[i].trim();
|
|||
|
|
|
|||
|
|
// Priority section header: ## 🔴 CRITICAL
|
|||
|
|
if (line.startsWith('## 🔴') || line.startsWith('## 🟠') ||
|
|||
|
|
line.startsWith('## 🟡') || line.startsWith('## 🔵') ||
|
|||
|
|
line.startsWith('## 💭')) {
|
|||
|
|
const match = line.match(/##\s+(🔴|🟠|🟡|🔵|💭)\s+(CRITICAL|HIGH|MEDIUM|LOW|NICE TO HAVE)/);
|
|||
|
|
if (match) {
|
|||
|
|
currentPriority = match[1];
|
|||
|
|
}
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Item header: ### 🔴 Title — CRITICAL
|
|||
|
|
if (line.startsWith('### 🔴') || line.startsWith('### 🟠') ||
|
|||
|
|
line.startsWith('### 🟡') || line.startsWith('### 🔵') ||
|
|||
|
|
line.startsWith('### 💭')) {
|
|||
|
|
if (currentItem) {
|
|||
|
|
items.push(currentItem);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const match = line.match(/###\s+(🔴|🟠|🟡|🔵|💭)\s+(.+?)\s*(—\s*(CRITICAL|HIGH|MEDIUM|LOW|NICE TO HAVE))?/);
|
|||
|
|
if (match) {
|
|||
|
|
currentItem = {
|
|||
|
|
priority: match[1],
|
|||
|
|
title: match[2].trim(),
|
|||
|
|
description: '',
|
|||
|
|
status: 'PENDING',
|
|||
|
|
added: '',
|
|||
|
|
addedBy: '',
|
|||
|
|
priorityLabel: match[4] || matchPriorityToLabel(match[1])
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
continue;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Parse item content
|
|||
|
|
if (currentItem && line) {
|
|||
|
|
if (line.startsWith('**Status:**')) {
|
|||
|
|
currentItem.status = line.replace('**Status:**', '').trim();
|
|||
|
|
}
|
|||
|
|
else if (line.startsWith('**Added:**')) {
|
|||
|
|
const dateMatch = line.match(/(\d{4}-\d{2}-\d{2})/);
|
|||
|
|
if (dateMatch) {
|
|||
|
|
currentItem.added = dateMatch[1];
|
|||
|
|
}
|
|||
|
|
const byMatch = line.match(/by\s+(.+)/);
|
|||
|
|
if (byMatch) {
|
|||
|
|
currentItem.addedBy = byMatch[1];
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
else if (!line.startsWith('**') || line.startsWith('**Description:**') || line.startsWith('**Rationale:**') || line.startsWith('**Implementation Notes:**')) {
|
|||
|
|
currentItem.description += line + '\n';
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (currentItem) {
|
|||
|
|
items.push(currentItem);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return items;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Map priority emoji to label
|
|||
|
|
*/
|
|||
|
|
function matchPriorityToLabel(emoji) {
|
|||
|
|
const mapping = {
|
|||
|
|
'🔴': 'CRITICAL',
|
|||
|
|
'🟠': 'HIGH',
|
|||
|
|
'🟡': 'MEDIUM',
|
|||
|
|
'🔵': 'LOW',
|
|||
|
|
'💭': 'NICE TO HAVE'
|
|||
|
|
};
|
|||
|
|
return mapping[emoji] || 'UNKNOWN';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Priority Badge Component
|
|||
|
|
*/
|
|||
|
|
function PriorityBadge({ emoji, label }) {
|
|||
|
|
const colors = PRIORITY_COLORS[emoji] || PRIORITY_COLORS['💭'];
|
|||
|
|
return (
|
|||
|
|
<Badge variant="outline" className={`${colors.bg} ${colors.text} border-0 font-semibold px-2`}>
|
|||
|
|
{emoji} {label}
|
|||
|
|
</Badge>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Roadmap Card Component
|
|||
|
|
*/
|
|||
|
|
function RoadmapCard({ item }) {
|
|||
|
|
const colors = PRIORITY_COLORS[item.priority] || PRIORITY_COLORS['💭'];
|
|||
|
|
const isHighPriority = item.priority === '🔴' || item.priority === '🟠';
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<SimpleCollapsible defaultOpen={isHighPriority} title={
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<PriorityBadge emoji={item.priority} label={item.priorityLabel} />
|
|||
|
|
<span className="font-medium text-sm">{item.title}</span>
|
|||
|
|
</div>
|
|||
|
|
}>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
<div className="flex flex-wrap gap-2 text-xs">
|
|||
|
|
{item.status && (
|
|||
|
|
<Badge variant="secondary" className="bg-muted/50">
|
|||
|
|
Status: {item.status}
|
|||
|
|
</Badge>
|
|||
|
|
)}
|
|||
|
|
{item.added && (
|
|||
|
|
<span className="text-muted-foreground flex items-center gap-1">
|
|||
|
|
Added: {item.added}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
{item.addedBy && (
|
|||
|
|
<span className="text-muted-foreground flex items-center gap-1">
|
|||
|
|
by {item.addedBy}
|
|||
|
|
</span>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="prose prose-sm dark:prose-invert max-w-none text-sm">
|
|||
|
|
<div className="whitespace-pre-wrap text-muted-foreground">
|
|||
|
|
{item.description}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</SimpleCollapsible>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Development Log Entry Component
|
|||
|
|
*/
|
|||
|
|
function DevLogEntry({ entry }) {
|
|||
|
|
const [isOpen, setIsOpen] = useState(false);
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="mb-4 rounded-xl border border-border/70 bg-background/65 shadow-sm overflow-hidden">
|
|||
|
|
<div
|
|||
|
|
className="flex items-center justify-between px-4 py-3 cursor-pointer hover:bg-muted/30 transition-colors"
|
|||
|
|
onClick={() => setIsOpen(!isOpen)}
|
|||
|
|
>
|
|||
|
|
<div className="flex items-center gap-2">
|
|||
|
|
<span className="font-mono font-semibold text-sm">{entry.version}</span>
|
|||
|
|
<span className="text-xs text-muted-foreground">{entry.date}</span>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="flex items-center gap-3">
|
|||
|
|
{entry.status && (
|
|||
|
|
<Badge
|
|||
|
|
variant="outline"
|
|||
|
|
className={entry.status.includes('COMPLETED')
|
|||
|
|
? 'bg-green-500/10 text-green-600 border-green-500/20'
|
|||
|
|
: 'bg-muted/50 text-muted-foreground'}
|
|||
|
|
>
|
|||
|
|
{entry.status}
|
|||
|
|
</Badge>
|
|||
|
|
)}
|
|||
|
|
<ChevronDown
|
|||
|
|
className={`h-4 w-4 text-muted-foreground transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{isOpen && (
|
|||
|
|
<div className="px-4 pb-3 pt-1 border-t border-border/70 space-y-2">
|
|||
|
|
{entry.agents && entry.agents.length > 0 && (
|
|||
|
|
<div className="flex flex-wrap gap-2 text-xs">
|
|||
|
|
{entry.agents.map((agent, idx) => (
|
|||
|
|
<span key={idx} className="text-muted-foreground">
|
|||
|
|
{agent.status === 'COMPLETED' && '✅ '}
|
|||
|
|
{agent.name}: {agent.notes}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{entry.filesModified && entry.filesModified.length > 0 && (
|
|||
|
|
<div>
|
|||
|
|
<p className="text-xs font-semibold text-muted-foreground mb-1">Files Modified:</p>
|
|||
|
|
<div className="flex flex-wrap gap-1">
|
|||
|
|
{entry.filesModified.map((file, idx) => (
|
|||
|
|
<code key={idx} className="text-xs bg-muted/50 px-1.5 py-0.5 rounded text-muted-foreground">
|
|||
|
|
{file}
|
|||
|
|
</code>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{entry.details && (
|
|||
|
|
<div className="prose prose-sm dark:prose-invert max-w-none mt-2">
|
|||
|
|
<div className="whitespace-pre-wrap text-sm text-muted-foreground">
|
|||
|
|
{entry.details}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Parse DEVELOPMENT_LOG.md content
|
|||
|
|
*/
|
|||
|
|
function parseDevLogMarkdown(markdown) {
|
|||
|
|
const entries = [];
|
|||
|
|
const sections = markdown.split('---');
|
|||
|
|
|
|||
|
|
for (const section of sections) {
|
|||
|
|
if (!section.trim()) continue;
|
|||
|
|
if (section.includes('Current Work') && !section.includes('Status:')) continue;
|
|||
|
|
if (section.includes('Completed Work') && !section.includes('Date:')) continue;
|
|||
|
|
|
|||
|
|
const versionMatch = section.match(/v(\d+\.\d+\.\d+)/);
|
|||
|
|
const dateMatch = section.match(/(\d{4}-\d{2}-\d{2})/);
|
|||
|
|
|
|||
|
|
if (versionMatch || dateMatch) {
|
|||
|
|
const entry = {
|
|||
|
|
version: versionMatch ? `v${versionMatch[1]}` : 'Unknown',
|
|||
|
|
date: dateMatch ? dateMatch[0] : 'Unknown',
|
|||
|
|
agents: [],
|
|||
|
|
filesModified: [],
|
|||
|
|
status: 'UNKNOWN',
|
|||
|
|
details: section.trim(),
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// Try to extract agent info from table-like format
|
|||
|
|
// Example: "Neo | ✅ COMPLETED | 1m 38s | Added `run()` functions..."
|
|||
|
|
const agentLines = section.split('\n').filter(line =>
|
|||
|
|
line.includes('|') && (line.includes('✅') || line.includes('❌') || line.includes('⏳') || line.includes('⚠️'))
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
for (const agentLine of agentLines) {
|
|||
|
|
const parts = agentLine.split('|').map(p => p.trim());
|
|||
|
|
if (parts.length >= 4) {
|
|||
|
|
entry.agents.push({
|
|||
|
|
name: parts[0],
|
|||
|
|
status: parts[1],
|
|||
|
|
time: parts[2],
|
|||
|
|
notes: parts.slice(3).join('|'),
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Extract files modified
|
|||
|
|
const filesMatch = section.match(/Files Modified:\s*(.*)/);
|
|||
|
|
if (filesMatch) {
|
|||
|
|
entry.filesModified = filesMatch[1].split(',').map(f => f.trim());
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Extract status from headers
|
|||
|
|
if (section.includes('COMPLETED')) {
|
|||
|
|
entry.status = 'COMPLETED';
|
|||
|
|
} else if (section.includes('In Progress') || section.includes('IN PROGRESS')) {
|
|||
|
|
entry.status = 'IN PROGRESS';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
entries.push(entry);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Sort by date descending (most recent first)
|
|||
|
|
entries.sort((a, b) => {
|
|||
|
|
const dateA = new Date(a.date);
|
|||
|
|
const dateB = new Date(b.date);
|
|||
|
|
return dateB - dateA;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
return entries;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* Admin Dashboard Component
|
|||
|
|
*/
|
|||
|
|
export default function AdminDashboard({ about }) {
|
|||
|
|
const [roadmapItems, setRoadmapItems] = useState([]);
|
|||
|
|
const [devLogEntries, setDevLogEntries] = useState([]);
|
|||
|
|
const [loading, setLoading] = useState(true);
|
|||
|
|
|
|||
|
|
const parseData = useCallback(() => {
|
|||
|
|
setLoading(true);
|
|||
|
|
try {
|
|||
|
|
if (about?.future) {
|
|||
|
|
const roadmap = parseFutureMarkdown(about.future);
|
|||
|
|
setRoadmapItems(roadmap);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (about?.developmentLog) {
|
|||
|
|
const logs = parseDevLogMarkdown(about.developmentLog);
|
|||
|
|
setDevLogEntries(logs);
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
setLoading(false);
|
|||
|
|
}
|
|||
|
|
}, [about]);
|
|||
|
|
|
|||
|
|
useEffect(() => { parseData(); }, [parseData]);
|
|||
|
|
|
|||
|
|
if (loading) {
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-4">
|
|||
|
|
<div className="h-8 w-48 bg-muted rounded animate-pulse" />
|
|||
|
|
<div className="h-4 bg-muted rounded animate-pulse" />
|
|||
|
|
<div className="h-4 bg-muted rounded animate-pulse" />
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="space-y-6">
|
|||
|
|
{/* Roadmap Section */}
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="flex items-center gap-2">
|
|||
|
|
<span className="h-6 w-6 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
|||
|
|
🗺️
|
|||
|
|
</span>
|
|||
|
|
Roadmap
|
|||
|
|
</CardTitle>
|
|||
|
|
<CardDescription>
|
|||
|
|
Current and upcoming features organized by priority
|
|||
|
|
</CardDescription>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{roadmapItems.length === 0 ? (
|
|||
|
|
<div className="text-center py-8 text-muted-foreground">
|
|||
|
|
No roadmap items found
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="max-h-[500px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{roadmapItems.map((item, idx) => (
|
|||
|
|
<RoadmapCard key={idx} item={item} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
|
|||
|
|
{/* Activity Log Section */}
|
|||
|
|
<Card>
|
|||
|
|
<CardHeader>
|
|||
|
|
<CardTitle className="flex items-center gap-2">
|
|||
|
|
<span className="h-6 w-6 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
|||
|
|
📝
|
|||
|
|
</span>
|
|||
|
|
Development Activity Log
|
|||
|
|
</CardTitle>
|
|||
|
|
<CardDescription>
|
|||
|
|
Recent development work and completed tasks
|
|||
|
|
</CardDescription>
|
|||
|
|
</CardHeader>
|
|||
|
|
<CardContent>
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{devLogEntries.length === 0 ? (
|
|||
|
|
<div className="text-center py-8 text-muted-foreground">
|
|||
|
|
No activity log entries found
|
|||
|
|
</div>
|
|||
|
|
) : (
|
|||
|
|
<div className="max-h-[500px] overflow-y-auto pr-2 scrollbar-thin scrollbar-thumb-muted scrollbar-track-transparent">
|
|||
|
|
<div className="space-y-2">
|
|||
|
|
{devLogEntries.map((entry, idx) => (
|
|||
|
|
<DevLogEntry key={idx} entry={entry} />
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</CardContent>
|
|||
|
|
</Card>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|