436 lines
14 KiB
JavaScript
436 lines
14 KiB
JavaScript
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>
|
||
);
|
||
}
|