remove: Delete entire PRP directory (4,611 lines)

- Remove PRPViewer component and all related files
- Delete 29 PRP-related files including sections, renderers, utilities
- Clean up unused complex document rendering system
- Simplifies codebase by removing over-engineered flip card viewer

Files removed:
- PRPViewer.tsx/css - Main component
- sections/ - 13 specialized section components
- components/ - 5 rendering components
- utils/ - 6 utility files
- renderers/ - Section rendering logic
- types/ - PRP type definitions

Part of frontend vertical slice refactoring effort.
This commit is contained in:
Rasmus Widing
2025-09-03 10:39:48 +03:00
parent 665098ed17
commit ff8db38bcc
28 changed files with 0 additions and 4942 deletions

View File

@@ -1,304 +0,0 @@
/* PRP Viewer Styles - Beautiful Archon Theme */
.prp-viewer {
animation: fadeIn 0.5s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Smooth collapse animations */
.prp-viewer .collapsible-content {
transition: max-height 0.3s cubic-bezier(0.4, 0, 0.2, 1),
opacity 0.3s ease-out;
}
/* Hover effects for cards */
.prp-viewer .persona-card,
.prp-viewer .metric-item,
.prp-viewer .flow-diagram {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Glow effects for icons */
.prp-viewer .icon-glow {
filter: drop-shadow(0 0 8px currentColor);
}
/* Gradient text animations */
@keyframes gradientShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.prp-viewer .gradient-text {
background-size: 200% 200%;
animation: gradientShift 3s ease infinite;
}
/* Section reveal animations */
.prp-viewer .section-content {
animation: slideIn 0.4s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Pulse animation for important metrics */
@keyframes metricPulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
}
.prp-viewer .metric-highlight {
animation: metricPulse 2s ease-in-out infinite;
}
/* Flow diagram connections */
.prp-viewer .flow-connection {
position: relative;
}
.prp-viewer .flow-connection::before {
content: '';
position: absolute;
left: -12px;
top: 50%;
width: 8px;
height: 8px;
background: linear-gradient(135deg, #3b82f6, #a855f7);
border-radius: 50%;
transform: translateY(-50%);
box-shadow: 0 0 12px rgba(59, 130, 246, 0.6);
}
/* Interactive hover states */
.prp-viewer .interactive-section:hover {
transform: translateY(-2px);
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
/* Dark mode enhancements */
.dark .prp-viewer .interactive-section:hover {
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5),
0 10px 10px -5px rgba(0, 0, 0, 0.2),
0 0 20px rgba(59, 130, 246, 0.3);
}
/* Loading skeleton animation */
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
.prp-viewer .skeleton {
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0) 100%
);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
/* Collapsible chevron animation */
.prp-viewer .chevron-animate {
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Card entrance animations */
.prp-viewer .card-entrance {
animation: cardSlideUp 0.5s ease-out;
animation-fill-mode: both;
}
@keyframes cardSlideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Stagger animation for lists */
.prp-viewer .stagger-item {
animation: fadeInUp 0.4s ease-out;
animation-fill-mode: both;
}
.prp-viewer .stagger-item:nth-child(1) { animation-delay: 0.1s; }
.prp-viewer .stagger-item:nth-child(2) { animation-delay: 0.2s; }
.prp-viewer .stagger-item:nth-child(3) { animation-delay: 0.3s; }
.prp-viewer .stagger-item:nth-child(4) { animation-delay: 0.4s; }
.prp-viewer .stagger-item:nth-child(5) { animation-delay: 0.5s; }
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Floating animation for icons */
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-5px);
}
}
.prp-viewer .float-icon {
animation: float 3s ease-in-out infinite;
}
/* Glow border effect */
.prp-viewer .glow-border {
position: relative;
overflow: hidden;
}
.prp-viewer .glow-border::before {
content: '';
position: absolute;
top: -2px;
left: -2px;
right: -2px;
bottom: -2px;
background: linear-gradient(45deg, #3b82f6, #a855f7, #ec4899, #3b82f6);
border-radius: inherit;
opacity: 0;
transition: opacity 0.3s ease;
z-index: -1;
background-size: 400% 400%;
animation: gradientRotate 3s ease infinite;
}
.prp-viewer .glow-border:hover::before {
opacity: 1;
}
@keyframes gradientRotate {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
/* Success metric animations */
.prp-viewer .metric-success {
position: relative;
}
.prp-viewer .metric-success::after {
content: '✓';
position: absolute;
right: -20px;
top: 50%;
transform: translateY(-50%);
color: #10b981;
font-weight: bold;
opacity: 0;
transition: all 0.3s ease;
}
.prp-viewer .metric-success:hover::after {
opacity: 1;
right: 10px;
}
/* Smooth scrolling for sections */
.prp-viewer {
scroll-behavior: smooth;
}
/* Progress indicator for implementation phases */
.prp-viewer .phase-progress {
position: relative;
padding-left: 30px;
}
.prp-viewer .phase-progress::before {
content: '';
position: absolute;
left: 10px;
top: 0;
bottom: 0;
width: 2px;
background: linear-gradient(to bottom, #3b82f6, #a855f7);
}
.prp-viewer .phase-progress .phase-dot {
position: absolute;
left: 6px;
top: 20px;
width: 10px;
height: 10px;
background: white;
border: 2px solid #3b82f6;
border-radius: 50%;
z-index: 1;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.prp-viewer .grid {
grid-template-columns: 1fr;
}
.prp-viewer .text-3xl {
font-size: 1.5rem;
}
.prp-viewer .p-6 {
padding: 1rem;
}
}

View File

@@ -1,279 +0,0 @@
import React from 'react';
import { PRPContent } from './types/prp.types';
import { MetadataSection } from './sections/MetadataSection';
import { SectionRenderer } from './renderers/SectionRenderer';
import { normalizePRPDocument } from './utils/normalizer';
import { processContentForPRP, isMarkdownContent, isDocumentWithMetadata } from './utils/markdownParser';
import { MarkdownDocumentRenderer } from './components/MarkdownDocumentRenderer';
import './PRPViewer.css';
interface PRPViewerProps {
content: PRPContent;
isDarkMode?: boolean;
sectionOverrides?: Record<string, React.ComponentType<any>>;
}
/**
* Process content to handle [Image #N] placeholders
*/
const processContent = (content: any): any => {
if (typeof content === 'string') {
// Replace [Image #N] with proper markdown image syntax
return content.replace(/\[Image #(\d+)\]/g, (match, num) => {
return `![Image ${num}](placeholder-image-${num})`;
});
}
if (Array.isArray(content)) {
return content.map(item => processContent(item));
}
if (typeof content === 'object' && content !== null) {
const processed: any = {};
for (const [key, value] of Object.entries(content)) {
processed[key] = processContent(value);
}
return processed;
}
return content;
};
/**
* Flexible PRP Viewer that dynamically renders sections based on content structure
*/
export const PRPViewer: React.FC<PRPViewerProps> = ({
content,
isDarkMode = false,
sectionOverrides = {}
}) => {
try {
if (!content) {
return <div className="text-gray-500">No PRP content available</div>;
}
console.log('PRPViewer: Received content:', {
type: typeof content,
isString: typeof content === 'string',
isObject: typeof content === 'object',
hasMetadata: typeof content === 'object' && content !== null ? isDocumentWithMetadata(content) : false,
isMarkdown: typeof content === 'string' ? isMarkdownContent(content) : false,
keys: typeof content === 'object' && content !== null ? Object.keys(content) : [],
contentPreview: typeof content === 'string' ? content.substring(0, 200) + '...' : 'Not a string'
});
// Route to appropriate renderer based on content type
// 1. Check if it's a document with metadata + markdown content
if (isDocumentWithMetadata(content)) {
console.log('PRPViewer: Detected document with metadata, using MarkdownDocumentRenderer');
return (
<MarkdownDocumentRenderer
content={content}
isDarkMode={isDarkMode}
sectionOverrides={sectionOverrides}
/>
);
}
// 2. Check if it's a pure markdown string
if (typeof content === 'string' && isMarkdownContent(content)) {
console.log('PRPViewer: Detected pure markdown content, using MarkdownDocumentRenderer');
return (
<MarkdownDocumentRenderer
content={content}
isDarkMode={isDarkMode}
sectionOverrides={sectionOverrides}
/>
);
}
// 3. Check if it's an object that might contain markdown content in any field
if (typeof content === 'object' && content !== null) {
// Check for markdown field first (common in PRP documents)
if (typeof content.markdown === 'string') {
console.log('PRPViewer: Found markdown field, using MarkdownDocumentRenderer');
return (
<MarkdownDocumentRenderer
content={content}
isDarkMode={isDarkMode}
sectionOverrides={sectionOverrides}
/>
);
}
// Look for markdown content in any field
for (const [key, value] of Object.entries(content)) {
if (typeof value === 'string' && isMarkdownContent(value)) {
console.log(`PRPViewer: Found markdown content in field '${key}', using MarkdownDocumentRenderer`);
// Create a proper document structure
const documentContent = {
title: content.title || 'Document',
content: value,
...content // Include all other fields as metadata
};
return (
<MarkdownDocumentRenderer
content={documentContent}
isDarkMode={isDarkMode}
sectionOverrides={sectionOverrides}
/>
);
}
}
}
// 4. For any other content that might contain documents, try MarkdownDocumentRenderer first
console.log('PRPViewer: Checking if content should use MarkdownDocumentRenderer anyway');
// If it's an object with any text content, try MarkdownDocumentRenderer
if (typeof content === 'object' && content !== null) {
const hasAnyTextContent = Object.values(content).some(value =>
typeof value === 'string' && value.length > 50
);
if (hasAnyTextContent) {
console.log('PRPViewer: Object has substantial text content, trying MarkdownDocumentRenderer');
return (
<MarkdownDocumentRenderer
content={content}
isDarkMode={isDarkMode}
sectionOverrides={sectionOverrides}
/>
);
}
}
// 5. Final fallback to original PRPViewer logic for purely structured JSON content
console.log('PRPViewer: Using standard JSON structure renderer as final fallback');
// First, check if content is raw markdown and process it
let processedForPRP = content;
// Handle the case where content is a raw markdown string (non-markdown strings)
if (typeof content === 'string') {
// For non-markdown strings, wrap in a simple structure
processedForPRP = {
title: 'Document Content',
content: content,
document_type: 'text'
};
} else if (typeof content === 'object' && content !== null) {
// For objects, process normally
processedForPRP = processContentForPRP(content);
}
// Ensure we have an object to work with
if (!processedForPRP || typeof processedForPRP !== 'object') {
return <div className="text-gray-500">Unable to process PRP content</div>;
}
// Normalize the content
const normalizedContent = normalizePRPDocument(processedForPRP);
// Process content to handle [Image #N] placeholders
const processedContent = processContent(normalizedContent);
// Extract sections (skip metadata fields)
const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type', 'id', '_id', 'project_id', 'created_at', 'updated_at'];
const sections = Object.entries(processedContent).filter(([key]) => !metadataFields.includes(key));
// Debug: Log sections being rendered
console.log('PRP Sections found:', sections.map(([key]) => key));
// Priority-based sorting for common PRP sections
const getSectionPriority = (key: string): number => {
const normalizedKey = key.toLowerCase();
// Define priority order (lower number = higher priority)
if (normalizedKey.includes('goal') || normalizedKey.includes('objective')) return 1;
if (normalizedKey.includes('why') || normalizedKey.includes('rationale')) return 2;
if (normalizedKey.includes('what') || normalizedKey === 'description') return 3;
if (normalizedKey.includes('context') || normalizedKey.includes('background')) return 4;
if (normalizedKey.includes('persona') || normalizedKey.includes('user') || normalizedKey.includes('stakeholder')) return 5;
if (normalizedKey.includes('flow') || normalizedKey.includes('journey') || normalizedKey.includes('workflow')) return 6;
if (normalizedKey.includes('requirement') && !normalizedKey.includes('technical')) return 7;
if (normalizedKey.includes('metric') || normalizedKey.includes('success') || normalizedKey.includes('kpi')) return 8;
if (normalizedKey.includes('timeline') || normalizedKey.includes('roadmap') || normalizedKey.includes('milestone')) return 9;
if (normalizedKey.includes('plan') || normalizedKey.includes('implementation')) return 10;
if (normalizedKey.includes('technical') || normalizedKey.includes('architecture') || normalizedKey.includes('tech')) return 11;
if (normalizedKey.includes('validation') || normalizedKey.includes('testing') || normalizedKey.includes('quality')) return 12;
if (normalizedKey.includes('risk') || normalizedKey.includes('mitigation')) return 13;
// Default priority for unknown sections
return 50;
};
// Sort sections by priority
const sortedSections = sections.sort(([a], [b]) => {
return getSectionPriority(a) - getSectionPriority(b);
});
return (
<div className={`prp-viewer ${isDarkMode ? 'dark' : ''}`}>
{/* Metadata Header */}
<MetadataSection content={processedContent} isDarkMode={isDarkMode} />
{/* Dynamic Sections */}
{sortedSections.map(([sectionKey, sectionData], index) => (
<div key={sectionKey} className="mb-6">
<SectionRenderer
sectionKey={sectionKey}
data={sectionData}
index={index}
isDarkMode={isDarkMode}
sectionOverrides={sectionOverrides}
/>
</div>
))}
{sections.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p>No additional sections found in this PRP document.</p>
</div>
)}
</div>
);
} catch (error) {
console.error('PRPViewer: Error rendering content:', error);
// Provide a meaningful error display instead of black screen
return (
<div className="p-6 bg-red-50 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-lg">
<h3 className="text-red-800 dark:text-red-200 font-semibold mb-2">Error Rendering PRP</h3>
<p className="text-red-600 dark:text-red-300 text-sm mb-4">
There was an error rendering this PRP document. The content may be in an unexpected format.
</p>
{/* Show error details for debugging */}
<details className="mt-4">
<summary className="cursor-pointer text-sm text-red-600 dark:text-red-400 hover:underline">
Show error details
</summary>
<div className="mt-2 space-y-2">
<pre className="p-4 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto">
{error instanceof Error ? error.message : String(error)}
</pre>
{error instanceof Error && error.stack && (
<pre className="p-4 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto max-h-48">
{error.stack}
</pre>
)}
</div>
</details>
{/* Show raw content for debugging */}
<details className="mt-2">
<summary className="cursor-pointer text-sm text-red-600 dark:text-red-400 hover:underline">
Show raw content
</summary>
<pre className="mt-2 p-4 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto max-h-96">
{typeof content === 'string'
? content
: JSON.stringify(content, null, 2)}
</pre>
</details>
</div>
);
}
};

View File

@@ -1,411 +0,0 @@
import React, { useState, useEffect, ReactNode } from 'react';
import {
ChevronDown,
Brain, Users, Workflow, BarChart3, Clock, Shield,
Code, Layers, FileText, List, Hash, Box, Type, ToggleLeft,
CheckCircle, AlertCircle, Info, Lightbulb
} from 'lucide-react';
import { SectionProps } from '../types/prp.types';
import { SimpleMarkdown } from './SimpleMarkdown';
import { formatValue } from '../utils/formatters';
interface CollapsibleSectionRendererProps extends SectionProps {
children?: ReactNode;
headerContent?: ReactNode;
sectionKey?: string;
contentType?: 'markdown' | 'code' | 'json' | 'list' | 'object' | 'auto';
animationDuration?: number;
showPreview?: boolean;
previewLines?: number;
}
/**
* Enhanced CollapsibleSectionRenderer with beautiful animations and content-aware styling
* Features:
* - Section-specific icons and colors
* - Smooth expand/collapse animations with dynamic height
* - Content type detection and appropriate formatting
* - Code block syntax highlighting support
* - Nested structure handling
* - Preview mode for collapsed content
*/
export const CollapsibleSectionRenderer: React.FC<CollapsibleSectionRendererProps> = ({
title,
data,
icon,
accentColor = 'gray',
defaultOpen = true,
isDarkMode = false,
isCollapsible = true,
isOpen: controlledIsOpen,
onToggle,
children,
headerContent,
sectionKey = '',
contentType = 'auto',
animationDuration = 300,
showPreview = true,
previewLines = 2
}) => {
// State management for collapsible behavior
const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen);
const [contentHeight, setContentHeight] = useState<number | 'auto'>('auto');
const [isAnimating, setIsAnimating] = useState(false);
const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
// Content ref for measuring height
const contentRef = React.useRef<HTMLDivElement>(null);
useEffect(() => {
if (controlledIsOpen === undefined) {
setInternalIsOpen(defaultOpen);
}
}, [defaultOpen, controlledIsOpen]);
// Measure content height for smooth animations
useEffect(() => {
if (contentRef.current && isCollapsible) {
const height = contentRef.current.scrollHeight;
setContentHeight(isOpen ? height : 0);
}
}, [isOpen, data, children]);
const handleToggle = () => {
if (!isCollapsible) return;
setIsAnimating(true);
if (controlledIsOpen === undefined) {
setInternalIsOpen(!internalIsOpen);
}
onToggle?.();
// Reset animation state after duration
setTimeout(() => setIsAnimating(false), animationDuration);
};
// Auto-detect section type and get appropriate icon
const getSectionIcon = (): ReactNode => {
if (icon) return icon;
const normalizedKey = sectionKey.toLowerCase();
const normalizedTitle = title.toLowerCase();
// Check both section key and title for better detection
const checkKeywords = (keywords: string[]) =>
keywords.some(keyword =>
normalizedKey.includes(keyword) || normalizedTitle.includes(keyword)
);
if (checkKeywords(['context', 'overview', 'background']))
return <Brain className="w-5 h-5" />;
if (checkKeywords(['persona', 'user', 'actor', 'stakeholder']))
return <Users className="w-5 h-5" />;
if (checkKeywords(['flow', 'journey', 'workflow', 'process']))
return <Workflow className="w-5 h-5" />;
if (checkKeywords(['metric', 'success', 'kpi', 'measurement']))
return <BarChart3 className="w-5 h-5" />;
if (checkKeywords(['plan', 'implementation', 'roadmap', 'timeline']))
return <Clock className="w-5 h-5" />;
if (checkKeywords(['validation', 'gate', 'criteria', 'acceptance']))
return <Shield className="w-5 h-5" />;
if (checkKeywords(['technical', 'tech', 'architecture', 'system']))
return <Code className="w-5 h-5" />;
if (checkKeywords(['architecture', 'structure', 'design']))
return <Layers className="w-5 h-5" />;
if (checkKeywords(['feature', 'functionality', 'capability']))
return <Lightbulb className="w-5 h-5" />;
if (checkKeywords(['requirement', 'spec', 'specification']))
return <CheckCircle className="w-5 h-5" />;
if (checkKeywords(['risk', 'issue', 'concern', 'challenge']))
return <AlertCircle className="w-5 h-5" />;
if (checkKeywords(['info', 'note', 'detail']))
return <Info className="w-5 h-5" />;
// Fallback based on data type
if (typeof data === 'string') return <Type className="w-5 h-5" />;
if (typeof data === 'number') return <Hash className="w-5 h-5" />;
if (typeof data === 'boolean') return <ToggleLeft className="w-5 h-5" />;
if (Array.isArray(data)) return <List className="w-5 h-5" />;
if (typeof data === 'object' && data !== null) return <Box className="w-5 h-5" />;
return <FileText className="w-5 h-5" />;
};
// Get section-specific color scheme
const getColorScheme = () => {
const normalizedKey = sectionKey.toLowerCase();
const normalizedTitle = title.toLowerCase();
const checkKeywords = (keywords: string[]) =>
keywords.some(keyword =>
normalizedKey.includes(keyword) || normalizedTitle.includes(keyword)
);
if (checkKeywords(['context', 'overview'])) return 'blue';
if (checkKeywords(['persona', 'user'])) return 'purple';
if (checkKeywords(['flow', 'journey'])) return 'orange';
if (checkKeywords(['metric', 'success'])) return 'green';
if (checkKeywords(['plan', 'implementation'])) return 'cyan';
if (checkKeywords(['validation', 'gate'])) return 'emerald';
if (checkKeywords(['technical', 'architecture'])) return 'indigo';
if (checkKeywords(['feature'])) return 'yellow';
if (checkKeywords(['risk', 'issue'])) return 'red';
return accentColor;
};
// Auto-detect content type if not specified
const getContentType = () => {
if (contentType !== 'auto') return contentType;
if (typeof data === 'string') {
// Check for code patterns
if (/^```[\s\S]*```$/m.test(data) ||
/^\s*(function|class|const|let|var|import|export)\s/m.test(data) ||
/^\s*[{[][\s\S]*[}\]]$/m.test(data)) {
return 'code';
}
// Check for markdown patterns
if (/^#{1,6}\s+.+$|^[-*+]\s+.+$|^\d+\.\s+.+$|```|^\>.+$|\*\*.+\*\*|\*.+\*|`[^`]+`/m.test(data)) {
return 'markdown';
}
}
if (Array.isArray(data)) return 'list';
if (typeof data === 'object' && data !== null) {
try {
JSON.stringify(data);
return 'json';
} catch {
return 'object';
}
}
return 'auto';
};
// Render content based on type
const renderContent = (): ReactNode => {
if (children) return children;
const detectedType = getContentType();
switch (detectedType) {
case 'markdown':
return <SimpleMarkdown content={data} className="text-gray-700 dark:text-gray-300" />;
case 'code':
return (
<div className="bg-gray-900 dark:bg-gray-950 rounded-lg p-4 overflow-x-auto">
<pre className="text-sm text-gray-100">
<code>{data}</code>
</pre>
</div>
);
case 'json':
return (
<div className="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 overflow-x-auto">
<pre className="text-sm text-gray-700 dark:text-gray-300">
{JSON.stringify(data, null, 2)}
</pre>
</div>
);
case 'list':
if (!Array.isArray(data)) return <span className="text-gray-500 italic">Invalid list data</span>;
return (
<ul className="space-y-2">
{data.map((item, index) => (
<li key={index} className="flex items-start gap-2">
<span className="text-gray-400 mt-0.5 flex-shrink-0"></span>
<span className="text-gray-700 dark:text-gray-300">{formatValue(item)}</span>
</li>
))}
</ul>
);
default:
return <span className="text-gray-700 dark:text-gray-300">{formatValue(data)}</span>;
}
};
// Generate preview content when collapsed
const renderPreview = (): ReactNode => {
if (!showPreview || isOpen || !data) return null;
const dataStr = typeof data === 'string' ? data : JSON.stringify(data);
const lines = dataStr.split('\n').slice(0, previewLines);
const preview = lines.join('\n');
const hasMore = dataStr.split('\n').length > previewLines;
return (
<div className="text-sm text-gray-500 dark:text-gray-400 mt-2 px-4 pb-2">
<div className="truncate">
{preview}
{hasMore && <span className="ml-1">...</span>}
</div>
</div>
);
};
const colorScheme = getColorScheme();
const sectionIcon = getSectionIcon();
// Color mapping for backgrounds and borders
const getColorClasses = () => {
const colorMap = {
blue: {
bg: 'bg-blue-50/50 dark:bg-blue-950/20',
border: 'border-blue-200 dark:border-blue-800',
iconBg: 'bg-blue-100 dark:bg-blue-900',
iconText: 'text-blue-600 dark:text-blue-400',
accent: 'border-l-blue-500'
},
purple: {
bg: 'bg-purple-50/50 dark:bg-purple-950/20',
border: 'border-purple-200 dark:border-purple-800',
iconBg: 'bg-purple-100 dark:bg-purple-900',
iconText: 'text-purple-600 dark:text-purple-400',
accent: 'border-l-purple-500'
},
green: {
bg: 'bg-green-50/50 dark:bg-green-950/20',
border: 'border-green-200 dark:border-green-800',
iconBg: 'bg-green-100 dark:bg-green-900',
iconText: 'text-green-600 dark:text-green-400',
accent: 'border-l-green-500'
},
orange: {
bg: 'bg-orange-50/50 dark:bg-orange-950/20',
border: 'border-orange-200 dark:border-orange-800',
iconBg: 'bg-orange-100 dark:bg-orange-900',
iconText: 'text-orange-600 dark:text-orange-400',
accent: 'border-l-orange-500'
},
cyan: {
bg: 'bg-cyan-50/50 dark:bg-cyan-950/20',
border: 'border-cyan-200 dark:border-cyan-800',
iconBg: 'bg-cyan-100 dark:bg-cyan-900',
iconText: 'text-cyan-600 dark:text-cyan-400',
accent: 'border-l-cyan-500'
},
indigo: {
bg: 'bg-indigo-50/50 dark:bg-indigo-950/20',
border: 'border-indigo-200 dark:border-indigo-800',
iconBg: 'bg-indigo-100 dark:bg-indigo-900',
iconText: 'text-indigo-600 dark:text-indigo-400',
accent: 'border-l-indigo-500'
},
emerald: {
bg: 'bg-emerald-50/50 dark:bg-emerald-950/20',
border: 'border-emerald-200 dark:border-emerald-800',
iconBg: 'bg-emerald-100 dark:bg-emerald-900',
iconText: 'text-emerald-600 dark:text-emerald-400',
accent: 'border-l-emerald-500'
},
yellow: {
bg: 'bg-yellow-50/50 dark:bg-yellow-950/20',
border: 'border-yellow-200 dark:border-yellow-800',
iconBg: 'bg-yellow-100 dark:bg-yellow-900',
iconText: 'text-yellow-600 dark:text-yellow-400',
accent: 'border-l-yellow-500'
},
red: {
bg: 'bg-red-50/50 dark:bg-red-950/20',
border: 'border-red-200 dark:border-red-800',
iconBg: 'bg-red-100 dark:bg-red-900',
iconText: 'text-red-600 dark:text-red-400',
accent: 'border-l-red-500'
},
gray: {
bg: 'bg-gray-50/50 dark:bg-gray-950/20',
border: 'border-gray-200 dark:border-gray-800',
iconBg: 'bg-gray-100 dark:bg-gray-900',
iconText: 'text-gray-600 dark:text-gray-400',
accent: 'border-l-gray-500'
}
};
return colorMap[colorScheme as keyof typeof colorMap] || colorMap.gray;
};
const colors = getColorClasses();
if (!isCollapsible) {
return (
<div className={`rounded-lg border-l-4 ${colors.accent} ${colors.bg} ${colors.border} shadow-sm`}>
<div className="p-6">
<div className="flex items-center gap-3 mb-4">
<div className={`p-2 rounded-lg ${colors.iconBg} ${colors.iconText}`}>
{sectionIcon}
</div>
<h3 className="font-semibold text-gray-800 dark:text-white flex-1">
{title}
</h3>
{headerContent}
</div>
<div className="space-y-4">
{renderContent()}
</div>
</div>
</div>
);
}
return (
<div className={`rounded-lg border-l-4 ${colors.accent} ${colors.bg} ${colors.border} shadow-sm overflow-hidden`}>
{/* Header */}
<div
className={`
cursor-pointer select-none p-6
hover:bg-opacity-75 transition-colors duration-200
${isAnimating ? 'pointer-events-none' : ''}
`}
onClick={handleToggle}
>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${colors.iconBg} ${colors.iconText}`}>
{sectionIcon}
</div>
<h3 className="font-semibold text-gray-800 dark:text-white flex-1">
{title}
</h3>
{headerContent}
<div className={`
transform transition-transform duration-200
${isOpen ? 'rotate-180' : 'rotate-0'}
text-gray-500 dark:text-gray-400
hover:text-gray-700 dark:hover:text-gray-200
`}>
<ChevronDown className="w-5 h-5" />
</div>
</div>
{renderPreview()}
</div>
{/* Content with smooth height animation */}
<div
className="overflow-hidden transition-all ease-in-out"
style={{
maxHeight: isOpen ? contentHeight : 0,
transitionDuration: `${animationDuration}ms`
}}
>
<div
ref={contentRef}
className={`px-6 pb-6 space-y-4 ${
isOpen ? 'opacity-100' : 'opacity-0'
} transition-opacity duration-200`}
style={{
transitionDelay: isOpen ? '100ms' : '0ms'
}}
>
{renderContent()}
</div>
</div>
</div>
);
};

View File

@@ -1,80 +0,0 @@
import React, { useState, useEffect, ReactNode } from 'react';
import { ChevronDown } from 'lucide-react';
interface CollapsibleSectionWrapperProps {
children: ReactNode;
header: ReactNode;
isCollapsible?: boolean;
defaultOpen?: boolean;
isOpen?: boolean;
onToggle?: () => void;
}
/**
* A wrapper component that makes any section collapsible by clicking on its header
*/
export const CollapsibleSectionWrapper: React.FC<CollapsibleSectionWrapperProps> = ({
children,
header,
isCollapsible = true,
defaultOpen = true,
isOpen: controlledIsOpen,
onToggle
}) => {
// Use controlled state if provided, otherwise manage internally
const [internalIsOpen, setInternalIsOpen] = useState(defaultOpen);
const isOpen = controlledIsOpen !== undefined ? controlledIsOpen : internalIsOpen;
useEffect(() => {
if (controlledIsOpen === undefined) {
setInternalIsOpen(defaultOpen);
}
}, [defaultOpen, controlledIsOpen]);
const handleToggle = () => {
if (controlledIsOpen === undefined) {
setInternalIsOpen(!internalIsOpen);
}
onToggle?.();
};
if (!isCollapsible) {
return (
<>
{header}
{children}
</>
);
}
return (
<>
<div
className="cursor-pointer select-none group"
onClick={handleToggle}
>
<div className="relative">
{header}
<div className={`
absolute right-4 top-1/2 -translate-y-1/2
transform transition-transform duration-200
${isOpen ? 'rotate-180' : ''}
text-gray-500 dark:text-gray-400
group-hover:text-gray-700 dark:group-hover:text-gray-200
`}>
<ChevronDown className="w-5 h-5" />
</div>
</div>
</div>
<div className={`
transition-all duration-300
${isOpen ? 'max-h-none opacity-100' : 'max-h-0 opacity-0 overflow-hidden'}
`}>
<div className={isOpen ? 'pb-4' : ''}>
{children}
</div>
</div>
</>
);
};

View File

@@ -1,323 +0,0 @@
import React from 'react';
import { ParsedMarkdownDocument, parseMarkdownToDocument, isDocumentWithMetadata, isMarkdownContent } from '../utils/markdownParser';
import { MetadataSection } from '../sections/MetadataSection';
import { MarkdownSectionRenderer } from './MarkdownSectionRenderer';
interface MarkdownDocumentRendererProps {
content: any;
isDarkMode?: boolean;
sectionOverrides?: Record<string, React.ComponentType<any>>;
}
/**
* Renders markdown documents with metadata header and flowing content sections
* Handles both pure markdown strings and documents with metadata + content structure
*/
/**
* Processes JSON content and converts it to markdown format
* Handles nested objects, arrays, and various data types
*/
function processContentToMarkdown(content: any): string {
if (typeof content === 'string') {
return content;
}
if (typeof content !== 'object' || content === null) {
return String(content);
}
const markdownSections: string[] = [];
// Extract metadata fields first (don't include in content conversion)
const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type', 'created_at', 'updated_at'];
for (const [key, value] of Object.entries(content)) {
// Skip metadata fields as they're handled separately
if (metadataFields.includes(key)) {
continue;
}
// Skip null or undefined values
if (value === null || value === undefined) {
continue;
}
const sectionTitle = formatSectionTitle(key);
const sectionContent = formatSectionContent(value);
if (sectionContent.trim()) {
markdownSections.push(`## ${sectionTitle}\n\n${sectionContent}`);
}
}
return markdownSections.join('\n\n');
}
/**
* Formats a section title from a JSON key
*/
function formatSectionTitle(key: string): string {
return key
.replace(/([A-Z])/g, ' $1') // Add space before capital letters
.replace(/[_-]/g, ' ') // Replace underscores and hyphens with spaces
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ')
.trim();
}
/**
* Formats section content based on its type
*/
function formatSectionContent(value: any): string {
if (typeof value === 'string') {
return value;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}
if (Array.isArray(value)) {
return formatArrayContent(value);
}
if (typeof value === 'object' && value !== null) {
return formatObjectContent(value);
}
return String(value);
}
/**
* Formats array content as markdown list or nested structure
*/
function formatArrayContent(array: any[]): string {
if (array.length === 0) {
return '_No items_';
}
// Check if all items are simple values (strings, numbers, booleans)
const allSimple = array.every(item =>
typeof item === 'string' ||
typeof item === 'number' ||
typeof item === 'boolean'
);
if (allSimple) {
return array.map(item => `- ${String(item)}`).join('\n');
}
// Handle complex objects in array
return array.map((item, index) => {
if (typeof item === 'object' && item !== null) {
const title = item.title || item.name || `Item ${index + 1}`;
const content = formatObjectContent(item, true);
return `### ${title}\n\n${content}`;
}
return `- ${String(item)}`;
}).join('\n\n');
}
/**
* Formats object content as key-value pairs or nested structure
*/
function formatObjectContent(obj: Record<string, any>, isNested: boolean = false): string {
const entries = Object.entries(obj);
if (entries.length === 0) {
return '_Empty_';
}
const formatted = entries.map(([key, value]) => {
if (value === null || value === undefined) {
return null;
}
const label = formatSectionTitle(key);
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return `**${label}:** ${String(value)}`;
}
if (Array.isArray(value)) {
const arrayContent = formatArrayContent(value);
return `**${label}:**\n${arrayContent}`;
}
if (typeof value === 'object') {
const nestedContent = formatObjectContent(value, true);
return `**${label}:**\n${nestedContent}`;
}
return `**${label}:** ${String(value)}`;
}).filter(Boolean);
return formatted.join('\n\n');
}
export const MarkdownDocumentRenderer: React.FC<MarkdownDocumentRendererProps> = ({
content,
isDarkMode = false,
sectionOverrides = {}
}) => {
try {
let parsedDocument: ParsedMarkdownDocument;
let documentMetadata: any = {};
console.log('MarkdownDocumentRenderer: Processing content:', {
type: typeof content,
keys: typeof content === 'object' && content !== null ? Object.keys(content) : [],
isDocWithMetadata: typeof content === 'object' && content !== null ? isDocumentWithMetadata(content) : false
});
// Handle different content structures
if (typeof content === 'string') {
console.log('MarkdownDocumentRenderer: Processing pure markdown string');
// Pure markdown string
parsedDocument = parseMarkdownToDocument(content);
// Create synthetic metadata for display
documentMetadata = {
title: parsedDocument.title || 'Document',
document_type: 'markdown'
};
} else if (typeof content === 'object' && content !== null) {
console.log('MarkdownDocumentRenderer: Processing object content');
// Extract all potential metadata fields first
const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type', 'created_at', 'updated_at'];
metadataFields.forEach(field => {
if (content[field]) {
documentMetadata[field] = content[field];
}
});
// Find the markdown content in any field
let markdownContent = '';
// First check common markdown field names
if (typeof content.markdown === 'string') {
markdownContent = content.markdown;
console.log('MarkdownDocumentRenderer: Found markdown in "markdown" field');
} else if (typeof content.content === 'string' && isMarkdownContent(content.content)) {
markdownContent = content.content;
console.log('MarkdownDocumentRenderer: Found markdown in "content" field');
} else {
// Look for markdown content in any field
for (const [key, value] of Object.entries(content)) {
if (typeof value === 'string' && isMarkdownContent(value)) {
markdownContent = value;
console.log(`MarkdownDocumentRenderer: Found markdown in field '${key}'`);
break;
}
}
}
// If no existing markdown found, try to convert JSON structure to markdown
if (!markdownContent) {
console.log('MarkdownDocumentRenderer: No markdown found, converting JSON to markdown');
markdownContent = processContentToMarkdown(content);
}
if (markdownContent) {
console.log('MarkdownDocumentRenderer: Parsing markdown content:', {
contentLength: markdownContent.length,
contentPreview: markdownContent.substring(0, 100) + '...'
});
parsedDocument = parseMarkdownToDocument(markdownContent);
console.log('MarkdownDocumentRenderer: Parsed document:', {
sectionsCount: parsedDocument.sections.length,
sections: parsedDocument.sections.map(s => ({ title: s.title, type: s.type }))
});
} else {
// No markdown content found, create empty document
console.log('MarkdownDocumentRenderer: No markdown content found in document');
parsedDocument = { sections: [], metadata: {}, hasMetadata: false };
}
// Use document title from metadata if available
if (content.title && !parsedDocument.title) {
parsedDocument.title = content.title;
}
} else {
console.log('MarkdownDocumentRenderer: Unexpected content structure');
// Fallback for unexpected content structure
return (
<div className="text-center py-12 text-gray-500">
<p>Unable to parse document content</p>
</div>
);
}
// ALWAYS show metadata - force hasMetadata to true
parsedDocument.hasMetadata = true;
// Combine parsed metadata with document metadata and add defaults
const finalMetadata = {
// Default values for better display
document_type: 'prp',
version: '1.0',
status: 'draft',
...parsedDocument.metadata,
...documentMetadata,
title: parsedDocument.title || documentMetadata.title || 'Untitled Document'
};
console.log('MarkdownDocumentRenderer: Final render data:', {
hasMetadata: parsedDocument.hasMetadata,
finalMetadata,
sectionsCount: parsedDocument.sections.length,
sections: parsedDocument.sections.map(s => ({ title: s.title, type: s.type, templateType: s.templateType }))
});
return (
<div className="markdown-document-renderer">
{/* ALWAYS show metadata header */}
<MetadataSection content={finalMetadata} isDarkMode={isDarkMode} />
{/* Document Sections */}
<div className="space-y-2">
{parsedDocument.sections.map((section, index) => (
<MarkdownSectionRenderer
key={`${section.sectionKey}-${index}`}
section={section}
index={index}
isDarkMode={isDarkMode}
sectionOverrides={sectionOverrides}
/>
))}
</div>
{/* Empty state */}
{parsedDocument.sections.length === 0 && (
<div className="text-center py-12 text-gray-500">
<p>No content sections found in this document.</p>
</div>
)}
</div>
);
} catch (error) {
console.error('MarkdownDocumentRenderer: Error rendering content:', error);
// Provide a meaningful error display instead of black screen
return (
<div className="p-6 bg-red-50 dark:bg-red-900/20 border border-red-300 dark:border-red-800 rounded-lg">
<h3 className="text-red-800 dark:text-red-200 font-semibold mb-2">Error Rendering Document</h3>
<p className="text-red-600 dark:text-red-300 text-sm mb-4">
There was an error rendering this document. The content may be in an unexpected format.
</p>
{/* Show raw content for debugging */}
<details className="mt-4">
<summary className="cursor-pointer text-sm text-red-600 dark:text-red-400 hover:underline">
Show raw content
</summary>
<pre className="mt-2 p-4 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-auto max-h-96">
{typeof content === 'string'
? content
: JSON.stringify(content, null, 2)}
</pre>
</details>
</div>
);
}
};

View File

@@ -1,71 +0,0 @@
import React from 'react';
import { ParsedSection } from '../utils/markdownParser';
import { SectionRenderer } from '../renderers/SectionRenderer';
import { SimpleMarkdown } from './SimpleMarkdown';
import { detectSectionType } from '../utils/sectionDetector';
interface MarkdownSectionRendererProps {
section: ParsedSection;
index: number;
isDarkMode?: boolean;
sectionOverrides?: Record<string, React.ComponentType<any>>;
}
/**
* Renders individual markdown sections with smart template detection
* Uses specialized components for known PRP templates, beautiful styling for generic sections
*/
export const MarkdownSectionRenderer: React.FC<MarkdownSectionRendererProps> = ({
section,
index,
isDarkMode = false,
sectionOverrides = {}
}) => {
// If section matches a known PRP template, use the specialized component
if (section.templateType) {
const { type } = detectSectionType(section.sectionKey, section.rawContent);
// Use the existing SectionRenderer with the detected type
return (
<div className="mb-6">
<SectionRenderer
sectionKey={section.sectionKey}
data={section.rawContent}
index={index}
isDarkMode={isDarkMode}
sectionOverrides={sectionOverrides}
/>
</div>
);
}
// For generic sections, render with beautiful floating styling
return (
<section className="mb-8">
<div className="relative">
{/* Section Header */}
<div className="mb-4">
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
{section.title}
</h2>
<div className="mt-1 h-0.5 w-16 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
</div>
{/* Section Content */}
<div className="relative">
{/* Subtle background for sections with complex content */}
{(section.type === 'code' || section.type === 'mixed') && (
<div className="absolute inset-0 bg-gray-50/30 dark:bg-gray-900/20 rounded-xl -m-4 backdrop-blur-sm border border-gray-200/30 dark:border-gray-700/30"></div>
)}
<div className="relative z-10">
<SimpleMarkdown
content={section.content}
className="prose prose-gray dark:prose-invert max-w-none prose-headings:text-gray-900 dark:prose-headings:text-white prose-p:text-gray-700 dark:prose-p:text-gray-300 prose-li:text-gray-700 dark:prose-li:text-gray-300"
/>
</div>
</div>
</div>
</section>
);
};

View File

@@ -1,340 +0,0 @@
import React from 'react';
import { formatValue } from '../utils/formatters';
interface SimpleMarkdownProps {
content: string;
className?: string;
}
/**
* Simple markdown renderer that handles basic formatting without external dependencies
*/
export const SimpleMarkdown: React.FC<SimpleMarkdownProps> = ({ content, className = '' }) => {
try {
// Process image placeholders first
const processedContent = formatValue(content);
// Split content into lines for processing
const lines = processedContent.split('\n');
const elements: React.ReactNode[] = [];
let currentList: string[] = [];
let listType: 'ul' | 'ol' | null = null;
const flushList = () => {
if (currentList.length > 0 && listType) {
const ListComponent = listType === 'ul' ? 'ul' : 'ol';
elements.push(
<div key={elements.length} className="my-3">
<ListComponent className={`space-y-2 ${listType === 'ul' ? 'list-disc' : 'list-decimal'} pl-6 text-gray-700 dark:text-gray-300`}>
{currentList.map((item, idx) => (
<li key={idx} className="leading-relaxed">{processInlineMarkdown(item)}</li>
))}
</ListComponent>
</div>
);
currentList = [];
listType = null;
}
};
const processInlineMarkdown = (text: string): React.ReactNode => {
const processed = text;
const elements: React.ReactNode[] = [];
let lastIndex = 0;
// Process **bold** text
const boldRegex = /\*\*(.*?)\*\*/g;
let match;
while ((match = boldRegex.exec(processed)) !== null) {
if (match.index > lastIndex) {
elements.push(processed.slice(lastIndex, match.index));
}
elements.push(<strong key={match.index} className="font-semibold">{match[1]}</strong>);
lastIndex = match.index + match[0].length;
}
// Process *italic* text
const italicRegex = /\*(.*?)\*/g;
const remainingText = processed.slice(lastIndex);
lastIndex = 0;
const italicElements: React.ReactNode[] = [];
while ((match = italicRegex.exec(remainingText)) !== null) {
if (match.index > lastIndex) {
italicElements.push(remainingText.slice(lastIndex, match.index));
}
italicElements.push(<em key={match.index} className="italic">{match[1]}</em>);
lastIndex = match.index + match[0].length;
}
if (lastIndex < remainingText.length) {
italicElements.push(remainingText.slice(lastIndex));
}
if (elements.length > 0) {
elements.push(...italicElements);
return <>{elements}</>;
}
if (italicElements.length > 0) {
return <>{italicElements}</>;
}
// Process `inline code`
const codeRegex = /`([^`]+)`/g;
const parts = text.split(codeRegex);
if (parts.length > 1) {
return (
<>
{parts.map((part, index) =>
index % 2 === 0 ? (
part
) : (
<code key={index} className="bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded text-sm font-mono text-gray-800 dark:text-gray-200">
{part}
</code>
)
)}
</>
);
}
return <span>{text}</span>;
};
let inCodeBlock = false;
let codeBlockContent: string[] = [];
let codeBlockLanguage = '';
let inTable = false;
let tableRows: string[][] = [];
let tableHeaders: string[] = [];
const flushTable = () => {
if (tableRows.length > 0) {
elements.push(
<div key={elements.length} className="my-6 overflow-x-auto">
<div className="inline-block min-w-full overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 shadow-sm">
<table className="min-w-full">
{tableHeaders.length > 0 && (
<thead className="bg-gray-50 dark:bg-gray-900/50">
<tr>
{tableHeaders.map((header, idx) => (
<th key={idx} className="px-4 py-3 text-left text-sm font-semibold text-gray-900 dark:text-white border-b border-gray-200 dark:border-gray-700">
{processInlineMarkdown(header.trim())}
</th>
))}
</tr>
</thead>
)}
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
{tableRows.map((row, rowIdx) => (
<tr key={rowIdx} className="hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors">
{row.map((cell, cellIdx) => (
<td key={cellIdx} className="px-4 py-3 text-sm text-gray-700 dark:text-gray-300">
{processInlineMarkdown(cell.trim())}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>
);
tableRows = [];
tableHeaders = [];
inTable = false;
}
};
lines.forEach((line, index) => {
// Handle code block start/end
if (line.startsWith('```')) {
if (!inCodeBlock) {
// Starting code block
flushList();
inCodeBlock = true;
codeBlockLanguage = line.substring(3).trim();
codeBlockContent = [];
} else {
// Ending code block
inCodeBlock = false;
elements.push(
<div key={index} className="my-4 rounded-lg overflow-hidden bg-gradient-to-br from-gray-900 via-gray-800 to-gray-900 border border-gray-700 shadow-lg">
{codeBlockLanguage && (
<div className="px-4 py-2 bg-gray-800/50 border-b border-gray-700 text-sm text-gray-300 font-mono">
{codeBlockLanguage}
</div>
)}
<pre className="p-4 overflow-x-auto">
<code className="text-gray-100 font-mono text-sm leading-relaxed">
{codeBlockContent.join('\n')}
</code>
</pre>
</div>
);
codeBlockContent = [];
codeBlockLanguage = '';
}
return;
}
// If inside code block, collect content
if (inCodeBlock) {
codeBlockContent.push(line);
return;
}
// Handle table rows
if (line.includes('|') && line.trim() !== '') {
const cells = line.split('|').map(cell => cell.trim()).filter(cell => cell !== '');
if (cells.length > 0) {
if (!inTable) {
// Starting a new table
flushList();
inTable = true;
tableHeaders = cells;
} else if (cells.every(cell => cell.match(/^:?-+:?$/))) {
// This is a header separator line (|---|---|), skip it
return;
} else {
// This is a regular table row
tableRows.push(cells);
}
return;
}
} else if (inTable) {
// End of table (empty line or non-table content)
flushTable();
}
// Handle headings
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
flushList();
const level = headingMatch[1].length;
const text = headingMatch[2];
const HeadingTag = `h${level}` as keyof JSX.IntrinsicElements;
const sizeClasses = ['text-2xl', 'text-xl', 'text-lg', 'text-base', 'text-sm', 'text-xs'];
const colorClasses = ['text-gray-900 dark:text-white', 'text-gray-800 dark:text-gray-100', 'text-gray-700 dark:text-gray-200', 'text-gray-700 dark:text-gray-200', 'text-gray-600 dark:text-gray-300', 'text-gray-600 dark:text-gray-300'];
elements.push(
<HeadingTag key={index} className={`font-bold mb-3 mt-6 ${sizeClasses[level - 1] || 'text-base'} ${colorClasses[level - 1] || 'text-gray-700 dark:text-gray-200'} border-b border-gray-200 dark:border-gray-700 pb-1`}>
{processInlineMarkdown(text)}
</HeadingTag>
);
return;
}
// Handle checkboxes (task lists)
const checkboxMatch = line.match(/^[-*+]\s+\[([ x])\]\s+(.+)$/);
if (checkboxMatch) {
flushList();
const isChecked = checkboxMatch[1] === 'x';
const content = checkboxMatch[2];
elements.push(
<div key={index} className="flex items-start gap-3 my-2">
<div className={`flex-shrink-0 w-5 h-5 rounded-md border-2 flex items-center justify-center mt-0.5 transition-colors ${
isChecked
? 'bg-green-500 border-green-500 text-white'
: 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800'
}`}>
{isChecked && (
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
</div>
<div className={`flex-1 leading-relaxed ${isChecked ? 'text-gray-500 dark:text-gray-400 line-through' : 'text-gray-700 dark:text-gray-300'}`}>
{processInlineMarkdown(content)}
</div>
</div>
);
return;
}
// Handle bullet lists
const bulletMatch = line.match(/^[-*+]\s+(.+)$/);
if (bulletMatch) {
if (listType !== 'ul') {
flushList();
listType = 'ul';
}
currentList.push(bulletMatch[1]);
return;
}
// Handle numbered lists
const numberMatch = line.match(/^\d+\.\s+(.+)$/);
if (numberMatch) {
if (listType !== 'ol') {
flushList();
listType = 'ol';
}
currentList.push(numberMatch[1]);
return;
}
// Handle code blocks
if (line.startsWith('```')) {
flushList();
// Simple code block handling - just skip the backticks
return;
}
// Handle blockquotes
if (line.startsWith('>')) {
flushList();
const content = line.substring(1).trim();
elements.push(
<blockquote key={index} className="border-l-4 border-blue-400 dark:border-blue-500 bg-blue-50/50 dark:bg-blue-900/20 pl-4 pr-4 py-3 italic my-4 rounded-r-lg backdrop-blur-sm">
<div className="text-gray-700 dark:text-gray-300">
{processInlineMarkdown(content)}
</div>
</blockquote>
);
return;
}
// Handle horizontal rules
if (line.match(/^(-{3,}|_{3,}|\*{3,})$/)) {
flushList();
elements.push(<hr key={index} className="my-4 border-gray-300 dark:border-gray-700" />);
return;
}
// Regular paragraph
if (line.trim()) {
flushList();
elements.push(
<p key={index} className="mb-3 leading-relaxed text-gray-700 dark:text-gray-300">
{processInlineMarkdown(line)}
</p>
);
}
});
// Flush any remaining list or table
flushList();
flushTable();
return (
<div className={`max-w-none ${className}`}>
<div className="space-y-1">
{elements}
</div>
</div>
);
} catch (error) {
console.error('Error rendering markdown:', error, content);
return (
<div className={`text-gray-700 dark:text-gray-300 ${className}`}>
<p>Error rendering markdown content</p>
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded mt-2 whitespace-pre-wrap">
{content}
</pre>
</div>
);
}
};

View File

@@ -1,25 +0,0 @@
// Main component exports
export { PRPViewer } from './PRPViewer';
// Section component exports
export { MetadataSection } from './sections/MetadataSection';
export { ContextSection } from './sections/ContextSection';
export { PersonaSection } from './sections/PersonaSection';
export { FlowSection } from './sections/FlowSection';
export { MetricsSection } from './sections/MetricsSection';
export { PlanSection } from './sections/PlanSection';
export { ListSection } from './sections/ListSection';
export { ObjectSection } from './sections/ObjectSection';
export { KeyValueSection } from './sections/KeyValueSection';
export { FeatureSection } from './sections/FeatureSection';
export { GenericSection } from './sections/GenericSection';
// Renderer exports
export { SectionRenderer } from './renderers/SectionRenderer';
// Type exports
export * from './types/prp.types';
// Utility exports
export { detectSectionType, formatSectionTitle, getSectionIcon } from './utils/sectionDetector';
export { formatKey, formatValue, truncateText, getAccentColor } from './utils/formatters';

View File

@@ -1,141 +0,0 @@
import React from 'react';
import {
Brain, Users, Workflow, BarChart3, Clock, Shield,
Code, Layers, FileText, List, Hash, Box
} from 'lucide-react';
import { detectSectionType, formatSectionTitle } from '../utils/sectionDetector';
import { getAccentColor } from '../utils/formatters';
// Import all section components
import { ContextSection } from '../sections/ContextSection';
import { PersonaSection } from '../sections/PersonaSection';
import { FlowSection } from '../sections/FlowSection';
import { MetricsSection } from '../sections/MetricsSection';
import { PlanSection } from '../sections/PlanSection';
import { ListSection } from '../sections/ListSection';
import { ObjectSection } from '../sections/ObjectSection';
import { KeyValueSection } from '../sections/KeyValueSection';
import { FeatureSection } from '../sections/FeatureSection';
import { GenericSection } from '../sections/GenericSection';
import { RolloutPlanSection } from '../sections/RolloutPlanSection';
import { TokenSystemSection } from '../sections/TokenSystemSection';
interface SectionRendererProps {
sectionKey: string;
data: any;
index: number;
isDarkMode?: boolean;
sectionOverrides?: Record<string, React.ComponentType<any>>;
}
/**
* Dynamically renders sections based on their type
*/
export const SectionRenderer: React.FC<SectionRendererProps> = ({
sectionKey,
data,
index,
isDarkMode = false,
sectionOverrides = {},
}) => {
// Skip metadata fields (handled by MetadataSection)
const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type'];
if (metadataFields.includes(sectionKey)) {
return null;
}
// Check for custom override first
if (sectionOverrides[sectionKey]) {
const CustomComponent = sectionOverrides[sectionKey];
return <CustomComponent data={data} title={formatSectionTitle(sectionKey)} />;
}
// Detect section type
const { type } = detectSectionType(sectionKey, data);
// Get appropriate icon based on section key
const getIcon = () => {
const normalizedKey = sectionKey.toLowerCase();
if (normalizedKey.includes('context') || normalizedKey.includes('overview')) return <Brain className="w-5 h-5" />;
if (normalizedKey.includes('persona') || normalizedKey.includes('user')) return <Users className="w-5 h-5" />;
if (normalizedKey.includes('flow') || normalizedKey.includes('journey')) return <Workflow className="w-5 h-5" />;
if (normalizedKey.includes('metric') || normalizedKey.includes('success')) return <BarChart3 className="w-5 h-5" />;
if (normalizedKey.includes('plan') || normalizedKey.includes('implementation')) return <Clock className="w-5 h-5" />;
if (normalizedKey.includes('validation') || normalizedKey.includes('gate')) return <Shield className="w-5 h-5" />;
if (normalizedKey.includes('technical') || normalizedKey.includes('tech')) return <Code className="w-5 h-5" />;
if (normalizedKey.includes('architecture')) return <Layers className="w-5 h-5" />;
if (Array.isArray(data)) return <List className="w-5 h-5" />;
if (typeof data === 'object') return <Box className="w-5 h-5" />;
return <FileText className="w-5 h-5" />;
};
// Get accent color based on section or index
const getColor = () => {
const normalizedKey = sectionKey.toLowerCase();
if (normalizedKey.includes('context')) return 'blue';
if (normalizedKey.includes('persona')) return 'purple';
if (normalizedKey.includes('flow') || normalizedKey.includes('journey')) return 'orange';
if (normalizedKey.includes('metric')) return 'green';
if (normalizedKey.includes('plan')) return 'cyan';
if (normalizedKey.includes('validation')) return 'emerald';
return getAccentColor(index);
};
const commonProps = {
title: formatSectionTitle(sectionKey),
data,
icon: getIcon(),
accentColor: getColor(),
isDarkMode,
defaultOpen: index < 5, // Open first 5 sections by default
isCollapsible: true, // Make all sections collapsible by default
};
// Check for specific section types by key name first
const normalizedKey = sectionKey.toLowerCase();
// Special handling for rollout plans
if (normalizedKey.includes('rollout') || normalizedKey === 'rollout_plan') {
return <RolloutPlanSection {...commonProps} />;
}
// Special handling for token systems
if (normalizedKey.includes('token') || normalizedKey === 'token_system' ||
normalizedKey === 'design_tokens' || normalizedKey === 'design_system') {
return <TokenSystemSection {...commonProps} />;
}
// Render based on detected type
switch (type) {
case 'context':
return <ContextSection {...commonProps} />;
case 'personas':
return <PersonaSection {...commonProps} />;
case 'flows':
return <FlowSection {...commonProps} />;
case 'metrics':
return <MetricsSection {...commonProps} />;
case 'plan':
return <PlanSection {...commonProps} />;
case 'list':
return <ListSection {...commonProps} />;
case 'keyvalue':
return <KeyValueSection {...commonProps} />;
case 'object':
return <ObjectSection {...commonProps} />;
case 'features':
return <FeatureSection {...commonProps} />;
case 'generic':
default:
return <GenericSection {...commonProps} />;
}
};

View File

@@ -1,82 +0,0 @@
import React from 'react';
import { Target, BookOpen, Sparkles, CheckCircle2 } from 'lucide-react';
import { SectionProps } from '../types/prp.types';
// Temporarily disabled to debug black screen issue
// import { renderValue, renderValueInline } from '../utils/objectRenderer';
/**
* Renders context sections like scope, background, objectives
*/
export const ContextSection: React.FC<SectionProps> = ({
title,
data,
icon,
accentColor = 'blue',
defaultOpen = true,
isDarkMode = false,
}) => {
if (!data || typeof data !== 'object') return null;
const renderContextItem = (key: string, value: any) => {
const getItemIcon = (itemKey: string) => {
const normalizedKey = itemKey.toLowerCase();
if (normalizedKey.includes('scope')) return <Target className="w-4 h-4 text-blue-500" />;
if (normalizedKey.includes('background')) return <BookOpen className="w-4 h-4 text-purple-500" />;
if (normalizedKey.includes('objective')) return <Sparkles className="w-4 h-4 text-green-500" />;
if (normalizedKey.includes('requirement')) return <CheckCircle2 className="w-4 h-4 text-orange-500" />;
return <CheckCircle2 className="w-4 h-4 text-gray-500" />;
};
const getItemColor = (itemKey: string) => {
const normalizedKey = itemKey.toLowerCase();
if (normalizedKey.includes('scope')) return 'blue';
if (normalizedKey.includes('background')) return 'purple';
if (normalizedKey.includes('objective')) return 'green';
if (normalizedKey.includes('requirement')) return 'orange';
return 'gray';
};
const color = getItemColor(key);
const colorMap = {
blue: 'bg-blue-50/50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
purple: 'bg-purple-50/50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800',
green: 'bg-green-50/50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
orange: 'bg-orange-50/50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800',
gray: 'bg-gray-50/50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800',
};
const itemTitle = key.replace(/_/g, ' ').charAt(0).toUpperCase() + key.replace(/_/g, ' ').slice(1);
return (
<div key={key} className={`p-4 rounded-lg border ${colorMap[color as keyof typeof colorMap]}`}>
<h4 className="font-semibold text-gray-800 dark:text-white mb-2 flex items-center gap-2">
{getItemIcon(key)}
{itemTitle}
</h4>
{Array.isArray(value) ? (
<ul className="space-y-2">
{value.map((item: any, idx: number) => (
<li key={idx} className="flex items-start gap-2 text-gray-700 dark:text-gray-300">
<CheckCircle2 className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
{typeof item === 'string' ? item : JSON.stringify(item)}
</li>
))}
</ul>
) : typeof value === 'string' ? (
<p className="text-gray-700 dark:text-gray-300">{value}</p>
) : (
<div className="text-gray-700 dark:text-gray-300">
{JSON.stringify(value, null, 2)}
</div>
)}
</div>
);
};
return (
<div className="space-y-4">
{Object.entries(data).map(([key, value]) => renderContextItem(key, value))}
</div>
);
};

View File

@@ -1,155 +0,0 @@
import React from 'react';
import { Package, Star, FileText } from 'lucide-react';
import { PRPSectionProps } from '../types/prp.types';
import { formatKey, formatValue } from '../utils/formatters';
/**
* Specialized component for feature requirements and capabilities
* Renders features in organized categories with proper hierarchy
*/
export const FeatureSection: React.FC<PRPSectionProps> = ({
title,
data,
icon = <Package className="w-5 h-5" />,
accentColor = 'blue',
isDarkMode = false,
defaultOpen = true
}) => {
if (!data || typeof data !== 'object') return null;
const colorMap = {
blue: 'from-blue-400 to-blue-600',
purple: 'from-purple-400 to-purple-600',
green: 'from-green-400 to-green-600',
orange: 'from-orange-400 to-orange-600',
pink: 'from-pink-400 to-pink-600',
cyan: 'from-cyan-400 to-cyan-600',
indigo: 'from-indigo-400 to-indigo-600',
emerald: 'from-emerald-400 to-emerald-600',
};
const bgColorMap = {
blue: 'bg-blue-50 dark:bg-blue-950',
purple: 'bg-purple-50 dark:bg-purple-950',
green: 'bg-green-50 dark:bg-green-950',
orange: 'bg-orange-50 dark:bg-orange-950',
pink: 'bg-pink-50 dark:bg-pink-950',
cyan: 'bg-cyan-50 dark:bg-cyan-950',
indigo: 'bg-indigo-50 dark:bg-indigo-950',
emerald: 'bg-emerald-50 dark:bg-emerald-950',
};
const renderFeatureGroup = (groupName: string, features: any, isPremium: boolean = false) => {
if (!features || typeof features !== 'object') return null;
const IconComponent = isPremium ? Star : FileText;
const iconColor = isPremium ? 'text-yellow-500' : 'text-blue-500';
return (
<div key={groupName} className="mb-6">
<div className="flex items-center gap-3 mb-4">
<IconComponent className={`w-5 h-5 ${iconColor}`} />
<h4 className="font-semibold text-gray-800 dark:text-white text-lg">
{formatKey(groupName)}
</h4>
{isPremium && (
<span className="px-2 py-1 bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 text-xs rounded-full font-medium">
Premium
</span>
)}
</div>
<div className="space-y-4 ml-8">
{Object.entries(features).map(([featureName, featureData]) => (
<div key={featureName} className="border-l-2 border-gray-200 dark:border-gray-700 pl-4">
<h5 className="font-medium text-gray-700 dark:text-gray-300 mb-2">
{formatKey(featureName)}
</h5>
{Array.isArray(featureData) ? (
<ul className="space-y-1">
{featureData.map((item, index) => (
<li key={index} className="flex items-start gap-2 text-gray-600 dark:text-gray-400">
<span className="text-gray-400 mt-1"></span>
<span>{formatValue(item)}</span>
</li>
))}
</ul>
) : typeof featureData === 'string' ? (
<p className="text-gray-600 dark:text-gray-400">{featureData}</p>
) : (
<div className="text-gray-600 dark:text-gray-400">
{formatValue(featureData)}
</div>
)}
</div>
))}
</div>
</div>
);
};
const renderFeatureList = (features: any) => {
if (Array.isArray(features)) {
return (
<ul className="space-y-2">
{features.map((feature, index) => (
<li key={index} className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<Package className="w-4 h-4 text-blue-500 mt-1 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">{formatValue(feature)}</span>
</li>
))}
</ul>
);
}
if (typeof features === 'object' && features !== null) {
return (
<div className="space-y-6">
{Object.entries(features).map(([key, value]) => {
const isPremium = key.toLowerCase().includes('premium') ||
key.toLowerCase().includes('advanced') ||
key.toLowerCase().includes('pro');
if (typeof value === 'object' && value !== null) {
return renderFeatureGroup(key, value, isPremium);
}
return (
<div key={key} className="flex items-start gap-3 p-4 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
<Package className="w-5 h-5 text-blue-500 mt-1 flex-shrink-0" />
<div className="flex-1">
<h4 className="font-medium text-gray-800 dark:text-white mb-1">
{formatKey(key)}
</h4>
<div className="text-gray-600 dark:text-gray-400">
{formatValue(value)}
</div>
</div>
</div>
);
})}
</div>
);
}
return <div className="text-gray-600 dark:text-gray-400">{formatValue(features)}</div>;
};
return (
<div className="space-y-4">
<div className={`rounded-lg p-6 ${bgColorMap[accentColor as keyof typeof bgColorMap] || bgColorMap.blue} border-l-4 border-blue-500`}>
<div className="flex items-center gap-3 mb-6">
<div className={`p-2 rounded-lg bg-gradient-to-br ${colorMap[accentColor as keyof typeof colorMap] || colorMap.blue} text-white shadow-lg`}>
{icon}
</div>
<h3 className="text-xl font-bold text-gray-800 dark:text-white">
{title}
</h3>
</div>
{renderFeatureList(data)}
</div>
</div>
);
};

View File

@@ -1,72 +0,0 @@
import React from 'react';
import { Workflow, Navigation } from 'lucide-react';
import { SectionProps } from '../types/prp.types';
import { formatKey } from '../utils/formatters';
/**
* Renders user flows and journey diagrams
*/
export const FlowSection: React.FC<SectionProps> = ({
title,
data,
icon,
accentColor = 'orange',
defaultOpen = true,
isDarkMode = false,
}) => {
if (!data || typeof data !== 'object') return null;
const renderFlowNode = (obj: any, depth: number = 0): React.ReactNode => {
if (!obj || typeof obj !== 'object') {
return <span className="text-gray-600 dark:text-gray-400">{String(obj)}</span>;
}
return Object.entries(obj).map(([key, value]) => {
const nodeKey = `${key}-${depth}-${Math.random()}`;
if (typeof value === 'string') {
return (
<div key={nodeKey} className="flex items-center gap-2 p-2" style={{ marginLeft: depth * 24 }}>
<div className="w-2 h-2 rounded-full bg-blue-500"></div>
<span className="text-sm font-medium text-gray-700 dark:text-gray-300">
{formatKey(key)}:
</span>
<span className="text-sm text-gray-600 dark:text-gray-400">{value}</span>
</div>
);
} else if (typeof value === 'object' && value !== null) {
return (
<div key={nodeKey} className="mb-3">
<div className="flex items-center gap-2 p-2 font-medium text-gray-800 dark:text-white" style={{ marginLeft: depth * 24 }}>
<Navigation className="w-4 h-4 text-purple-500" />
{formatKey(key)}
</div>
<div className="border-l-2 border-purple-200 dark:border-purple-800 ml-6">
{renderFlowNode(value, depth + 1)}
</div>
</div>
);
}
return null;
});
};
return (
<div className="grid gap-4">
{Object.entries(data).map(([flowName, flow]) => (
<div
key={flowName}
className="p-4 rounded-lg bg-gradient-to-br from-purple-50/50 to-pink-50/50 dark:from-purple-900/20 dark:to-pink-900/20 border border-purple-200 dark:border-purple-800"
>
<h4 className="font-semibold text-gray-800 dark:text-white mb-3 flex items-center gap-2">
<Workflow className="w-5 h-5 text-purple-500" />
{formatKey(flowName)}
</h4>
<div className="overflow-x-auto">
{renderFlowNode(flow)}
</div>
</div>
))}
</div>
);
};

View File

@@ -1,233 +0,0 @@
import React from 'react';
import { FileText, Hash, List, Box, Type, ToggleLeft } from 'lucide-react';
import { SectionProps } from '../types/prp.types';
import { formatKey, formatValue } from '../utils/formatters';
import { hasComplexNesting } from '../utils/normalizer';
import { CollapsibleSectionWrapper } from '../components/CollapsibleSectionWrapper';
import { SimpleMarkdown } from '../components/SimpleMarkdown';
/**
* Generic fallback section component that intelligently renders any data structure
* This component provides comprehensive rendering for any data type with proper formatting
*/
export const GenericSection: React.FC<SectionProps> = ({
title,
data,
icon = <FileText className="w-5 h-5" />,
accentColor = 'gray',
defaultOpen = true,
isDarkMode = false,
isCollapsible = true,
isOpen,
onToggle
}) => {
// Auto-detect appropriate icon based on data type
const getAutoIcon = () => {
if (typeof data === 'string') return <Type className="w-5 h-5" />;
if (typeof data === 'number') return <Hash className="w-5 h-5" />;
if (typeof data === 'boolean') return <ToggleLeft className="w-5 h-5" />;
if (Array.isArray(data)) return <List className="w-5 h-5" />;
if (typeof data === 'object' && data !== null) return <Box className="w-5 h-5" />;
return icon;
};
const renderValue = (value: any, depth: number = 0): React.ReactNode => {
const indent = depth * 16;
const maxDepth = 5; // Prevent infinite recursion
// Handle null/undefined
if (value === null || value === undefined) {
return <span className="text-gray-400 italic">Empty</span>;
}
// Handle primitives
if (typeof value === 'string') {
// Check if the string looks like markdown content
const hasMarkdownIndicators = /^#{1,6}\s+.+$|^[-*+]\s+.+$|^\d+\.\s+.+$|```|^\>.+$|\*\*.+\*\*|\*.+\*|`[^`]+`/m.test(value);
if (hasMarkdownIndicators && value.length > 20) {
// Render as markdown for content with markdown syntax
// Remove any leading headers since the section already has a title
const contentWithoutLeadingHeaders = value.replace(/^#{1,6}\s+.+$/m, '').trim();
const finalContent = contentWithoutLeadingHeaders || value;
return <SimpleMarkdown content={finalContent} className="text-gray-700 dark:text-gray-300" />;
}
// For shorter strings or non-markdown, use simple formatting
return <span className="text-gray-700 dark:text-gray-300">{formatValue(value)}</span>;
}
if (typeof value === 'number' || typeof value === 'boolean') {
return <span className="text-gray-700 dark:text-gray-300 font-mono">{formatValue(value)}</span>;
}
// Prevent deep recursion
if (depth >= maxDepth) {
return (
<span className="text-gray-500 italic text-sm">
[Complex nested structure - too deep to display]
</span>
);
}
// Handle arrays
if (Array.isArray(value)) {
if (value.length === 0) {
return <span className="text-gray-400 italic">No items</span>;
}
// Check if it's an array of primitives
const isSimpleArray = value.every(item =>
typeof item === 'string' ||
typeof item === 'number' ||
typeof item === 'boolean' ||
item === null ||
item === undefined
);
if (isSimpleArray) {
// For very long arrays, show first 10 and count
const displayItems = value.length > 10 ? value.slice(0, 10) : value;
const hasMore = value.length > 10;
return (
<div>
<ul className="space-y-1 mt-2">
{displayItems.map((item, index) => (
<li key={index} className="flex items-start gap-2" style={{ marginLeft: indent }}>
<span className="text-gray-400 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">{formatValue(item)}</span>
</li>
))}
</ul>
{hasMore && (
<p className="text-sm text-gray-500 italic mt-2" style={{ marginLeft: indent + 16 }}>
... and {value.length - 10} more items
</p>
)}
</div>
);
}
// Array of objects
const displayItems = value.length > 5 ? value.slice(0, 5) : value;
const hasMore = value.length > 5;
return (
<div className="space-y-3 mt-2">
{displayItems.map((item, index) => (
<div key={index} className="relative" style={{ marginLeft: indent }}>
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-gradient-to-b from-gray-300 to-transparent dark:from-gray-600"></div>
<div className="pl-4">
<div className="text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">
[{index}]
</div>
{renderValue(item, depth + 1)}
</div>
</div>
))}
{hasMore && (
<p className="text-sm text-gray-500 italic" style={{ marginLeft: indent + 16 }}>
... and {value.length - 5} more items
</p>
)}
</div>
);
}
// Handle objects
if (typeof value === 'object' && value !== null) {
// Simplified object rendering to debug black screen
return (
<div className="mt-2 text-gray-700 dark:text-gray-300">
<pre className="text-xs bg-gray-100 dark:bg-gray-800 p-2 rounded whitespace-pre-wrap">
{JSON.stringify(value, null, 2)}
</pre>
</div>
);
}
// Fallback for any other type (functions, symbols, etc.)
return (
<span className="text-gray-500 italic text-sm">
[{typeof value}]
</span>
);
};
const getBackgroundColor = () => {
const colorMap = {
blue: 'bg-blue-50/50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800',
purple: 'bg-purple-50/50 dark:bg-purple-900/20 border-purple-200 dark:border-purple-800',
green: 'bg-green-50/50 dark:bg-green-900/20 border-green-200 dark:border-green-800',
orange: 'bg-orange-50/50 dark:bg-orange-900/20 border-orange-200 dark:border-orange-800',
pink: 'bg-pink-50/50 dark:bg-pink-900/20 border-pink-200 dark:border-pink-800',
cyan: 'bg-cyan-50/50 dark:bg-cyan-900/20 border-cyan-200 dark:border-cyan-800',
gray: 'bg-gray-50/50 dark:bg-gray-900/20 border-gray-200 dark:border-gray-800',
};
return colorMap[accentColor as keyof typeof colorMap] || colorMap.gray;
};
const finalIcon = icon === <FileText className="w-5 h-5" /> ? getAutoIcon() : icon;
// Enhanced styling based on data complexity
const isComplexData = hasComplexNesting(data);
const headerClass = isComplexData
? `p-6 rounded-lg border-2 shadow-sm ${getBackgroundColor()}`
: `p-4 rounded-lg border ${getBackgroundColor()}`;
const header = (
<div className={headerClass}>
<h3 className="font-semibold text-gray-800 dark:text-white flex items-center gap-2">
<div className="p-1.5 rounded bg-gray-100 dark:bg-gray-800 text-gray-600 dark:text-gray-400">
{finalIcon}
</div>
<span className="flex-1">{title}</span>
</h3>
</div>
);
const contentClass = isComplexData
? `px-6 pb-6 -mt-1 rounded-b-lg border-2 border-t-0 shadow-sm ${getBackgroundColor()}`
: `px-4 pb-4 -mt-1 rounded-b-lg border border-t-0 ${getBackgroundColor()}`;
const content = (
<div className={contentClass}>
<div className="overflow-x-auto">
{/* Add a subtle background for complex data */}
{isComplexData ? (
<div className="bg-gray-50 dark:bg-gray-900/50 rounded p-3 -mx-2">
{renderValue(data)}
</div>
) : (
renderValue(data)
)}
</div>
</div>
);
try {
return (
<CollapsibleSectionWrapper
header={header}
isCollapsible={isCollapsible}
defaultOpen={defaultOpen}
isOpen={isOpen}
onToggle={onToggle}
>
{content}
</CollapsibleSectionWrapper>
);
} catch (error) {
console.error('Error rendering GenericSection:', error, { title, data });
return (
<div className="p-4 border border-red-300 rounded bg-red-50 dark:bg-red-900">
<h3 className="text-red-800 dark:text-red-200 font-semibold">{title}</h3>
<p className="text-red-600 dark:text-red-300 text-sm mt-2">Error rendering section content</p>
<pre className="text-xs mt-2 bg-gray-100 dark:bg-gray-800 p-2 rounded overflow-auto">
{JSON.stringify(data, null, 2)}
</pre>
</div>
);
}
};

View File

@@ -1,111 +0,0 @@
import React from 'react';
import { Hash } from 'lucide-react';
import { PRPSectionProps } from '../types/prp.types';
import { formatKey, formatValue } from '../utils/formatters';
/**
* Component for rendering simple key-value pairs
* Used for sections like budget, resources, team, etc.
*/
export const KeyValueSection: React.FC<PRPSectionProps> = ({
title,
data,
icon = <Hash className="w-5 h-5" />,
accentColor = 'green',
isDarkMode = false,
defaultOpen = true
}) => {
if (!data || typeof data !== 'object') return null;
const colorMap = {
blue: 'from-blue-400 to-blue-600',
purple: 'from-purple-400 to-purple-600',
green: 'from-green-400 to-green-600',
orange: 'from-orange-400 to-orange-600',
pink: 'from-pink-400 to-pink-600',
cyan: 'from-cyan-400 to-cyan-600',
indigo: 'from-indigo-400 to-indigo-600',
emerald: 'from-emerald-400 to-emerald-600',
};
const borderColorMap = {
blue: 'border-blue-200 dark:border-blue-800',
purple: 'border-purple-200 dark:border-purple-800',
green: 'border-green-200 dark:border-green-800',
orange: 'border-orange-200 dark:border-orange-800',
pink: 'border-pink-200 dark:border-pink-800',
cyan: 'border-cyan-200 dark:border-cyan-800',
indigo: 'border-indigo-200 dark:border-indigo-800',
emerald: 'border-emerald-200 dark:border-emerald-800',
};
const renderValue = (value: any): React.ReactNode => {
if (Array.isArray(value)) {
return (
<ul className="list-disc list-inside space-y-1 mt-1">
{value.map((item, index) => (
<li key={index} className="text-gray-600 dark:text-gray-400">
{formatValue(item)}
</li>
))}
</ul>
);
}
if (typeof value === 'object' && value !== null) {
return (
<div className="mt-2 space-y-2 bg-gray-50 dark:bg-gray-700 p-3 rounded">
{Object.entries(value).map(([k, v]) => (
<div key={k} className="flex items-center justify-between">
<span className="font-medium text-gray-600 dark:text-gray-400">
{formatKey(k)}
</span>
<span className="text-gray-700 dark:text-gray-300 font-semibold">
{formatValue(v)}
</span>
</div>
))}
</div>
);
}
return (
<span className="text-gray-700 dark:text-gray-300 font-semibold">
{formatValue(value)}
</span>
);
};
return (
<div className="space-y-4">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-6 border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3 mb-6">
<div className={`p-2 rounded-lg bg-gradient-to-br ${colorMap[accentColor as keyof typeof colorMap] || colorMap.green} text-white shadow-lg`}>
{icon}
</div>
<h3 className="text-lg font-bold text-gray-800 dark:text-white">
{title}
</h3>
</div>
<div className="space-y-4">
{Object.entries(data).map(([key, value]) => (
<div
key={key}
className={`pb-4 border-b ${borderColorMap[accentColor as keyof typeof borderColorMap] || borderColorMap.green} last:border-0 last:pb-0`}
>
<div className="flex items-start justify-between gap-4">
<h4 className="font-semibold text-gray-700 dark:text-gray-300 min-w-[120px]">
{formatKey(key)}
</h4>
<div className="flex-1 text-right">
{renderValue(value)}
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};

View File

@@ -1,79 +0,0 @@
import React from 'react';
import { CheckCircle2, Circle } from 'lucide-react';
import { SectionProps } from '../types/prp.types';
/**
* Renders simple list/array data
*/
export const ListSection: React.FC<SectionProps> = ({
title,
data,
icon,
accentColor = 'green',
defaultOpen = true,
isDarkMode = false,
}) => {
if (!Array.isArray(data)) return null;
const getItemIcon = (item: any, index: number) => {
// Use checkmarks for validation/success items
if (title.toLowerCase().includes('validation') ||
title.toLowerCase().includes('success') ||
title.toLowerCase().includes('complete')) {
return <CheckCircle2 className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />;
}
// Use circles for general items
return <Circle className="w-3 h-3 text-gray-400 mt-1 flex-shrink-0" />;
};
const getBackgroundColor = () => {
const colorMap = {
green: 'bg-gradient-to-br from-green-50/50 to-emerald-50/50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800',
blue: 'bg-gradient-to-br from-blue-50/50 to-cyan-50/50 dark:from-blue-900/20 dark:to-cyan-900/20 border-blue-200 dark:border-blue-800',
purple: 'bg-gradient-to-br from-purple-50/50 to-pink-50/50 dark:from-purple-900/20 dark:to-pink-900/20 border-purple-200 dark:border-purple-800',
orange: 'bg-gradient-to-br from-orange-50/50 to-yellow-50/50 dark:from-orange-900/20 dark:to-yellow-900/20 border-orange-200 dark:border-orange-800',
gray: 'bg-gradient-to-br from-gray-50/50 to-slate-50/50 dark:from-gray-900/20 dark:to-slate-900/20 border-gray-200 dark:border-gray-800',
};
return colorMap[accentColor as keyof typeof colorMap] || colorMap.gray;
};
if (data.length === 0) {
return (
<div className={`p-4 rounded-lg border ${getBackgroundColor()}`}>
<p className="text-gray-500 dark:text-gray-500 italic">No items</p>
</div>
);
}
return (
<div className={`p-4 rounded-lg border ${getBackgroundColor()}`}>
<ul className="space-y-2">
{data.map((item: any, idx: number) => (
<li key={idx} className="flex items-start gap-3 p-3 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
{getItemIcon(item, idx)}
<div className="flex-1">
{typeof item === 'string' ? (
<span className="text-gray-700 dark:text-gray-300">{item}</span>
) : typeof item === 'object' && item !== null ? (
<div className="space-y-2">
{Object.entries(item).map(([key, value]) => (
<div key={key} className="flex items-start gap-2">
<span className="font-medium text-gray-600 dark:text-gray-400 min-w-[80px] capitalize">
{key.replace(/_/g, ' ')}:
</span>
<span className="text-gray-700 dark:text-gray-300 flex-1">
{typeof value === 'string' ? value : JSON.stringify(value)}
</span>
</div>
))}
</div>
) : (
<span className="text-gray-700 dark:text-gray-300">{String(item)}</span>
)}
</div>
</li>
))}
</ul>
</div>
);
};

View File

@@ -1,85 +0,0 @@
import React from 'react';
import { Award, Users, Clock, Tag, FileText } from 'lucide-react';
import { PRPContent } from '../types/prp.types';
interface MetadataSectionProps {
content: PRPContent;
isDarkMode?: boolean;
}
/**
* Renders the metadata header section of a PRP document
*/
export const MetadataSection: React.FC<MetadataSectionProps> = ({ content, isDarkMode = false }) => {
const getIcon = (field: string) => {
switch (field) {
case 'version': return <Award className="w-4 h-4 text-blue-500" />;
case 'author': return <Users className="w-4 h-4 text-purple-500" />;
case 'date': return <Clock className="w-4 h-4 text-green-500" />;
case 'status': return <Tag className="w-4 h-4 text-orange-500" />;
default: return <FileText className="w-4 h-4 text-gray-500" />;
}
};
const formatStatus = (status: string) => {
const statusColors = {
draft: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
review: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
approved: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
published: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400',
};
const colorClass = statusColors[status.toLowerCase() as keyof typeof statusColors] ||
'bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400';
return (
<span className={`px-2 py-1 rounded-full text-xs font-medium ${colorClass}`}>
{status.charAt(0).toUpperCase() + status.slice(1)}
</span>
);
};
const metadataFields = ['version', 'author', 'date', 'status'];
const hasMetadata = metadataFields.some(field => content[field]);
if (!hasMetadata && !content.title) {
return null;
}
return (
<div className="mb-8 p-6 rounded-xl bg-gradient-to-r from-blue-500/10 to-purple-500/10 border border-blue-200 dark:border-blue-800">
<h1 className="text-3xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent mb-4">
{content.title || 'Product Requirements Prompt'}
</h1>
<div className="flex flex-wrap gap-4 text-sm">
{metadataFields.map(field => {
const value = content[field];
if (!value) return null;
return (
<div key={field} className="flex items-center gap-2">
{getIcon(field)}
{field === 'status' ? (
formatStatus(value)
) : (
<span className="text-gray-600 dark:text-gray-400">
{field === 'version' && 'Version'} {value}
</span>
)}
</div>
);
})}
{content.document_type && (
<div className="flex items-center gap-2">
<FileText className="w-4 h-4 text-indigo-500" />
<span className="text-gray-600 dark:text-gray-400 capitalize">
{content.document_type}
</span>
</div>
)}
</div>
</div>
);
};

View File

@@ -1,74 +0,0 @@
import React from 'react';
import { BarChart3, Settings, Users, Gauge } from 'lucide-react';
import { SectionProps } from '../types/prp.types';
import { formatKey } from '../utils/formatters';
/**
* Renders success metrics and KPIs
*/
export const MetricsSection: React.FC<SectionProps> = ({
title,
data,
icon,
accentColor = 'green',
defaultOpen = true,
isDarkMode = false,
}) => {
if (!data || typeof data !== 'object') return null;
const getCategoryColor = (category: string): string => {
const normalizedCategory = category.toLowerCase();
if (normalizedCategory.includes('admin')) return 'from-blue-400 to-blue-600';
if (normalizedCategory.includes('business')) return 'from-purple-400 to-purple-600';
if (normalizedCategory.includes('customer')) return 'from-green-400 to-green-600';
if (normalizedCategory.includes('technical')) return 'from-orange-400 to-orange-600';
if (normalizedCategory.includes('performance')) return 'from-red-400 to-red-600';
return 'from-gray-400 to-gray-600';
};
const getCategoryIcon = (category: string): React.ReactNode => {
const normalizedCategory = category.toLowerCase();
if (normalizedCategory.includes('admin')) return <Settings className="w-4 h-4" />;
if (normalizedCategory.includes('business')) return <BarChart3 className="w-4 h-4" />;
if (normalizedCategory.includes('customer')) return <Users className="w-4 h-4" />;
return <Gauge className="w-4 h-4" />;
};
const renderMetric = (metric: string, category: string, index: number) => {
return (
<div
key={`${category}-${index}`}
className="flex items-center gap-3 p-3 rounded-lg bg-white/50 dark:bg-black/30 border border-gray-200 dark:border-gray-700 hover:border-blue-400 dark:hover:border-blue-500 transition-all duration-200 group"
>
<div className={`p-2 rounded-lg bg-gradient-to-br ${getCategoryColor(category)} text-white shadow-md group-hover:scale-110 transition-transform duration-200`}>
{getCategoryIcon(category)}
</div>
<p className="text-sm text-gray-700 dark:text-gray-300 flex-1">{metric}</p>
</div>
);
};
return (
<div className="grid gap-4">
{Object.entries(data).map(([category, metrics]: [string, any]) => (
<div key={category}>
<h4 className="font-semibold text-gray-800 dark:text-white mb-3 capitalize">
{formatKey(category)}
</h4>
<div className="grid gap-2">
{Array.isArray(metrics) ?
metrics.map((metric: string, idx: number) =>
renderMetric(metric, category, idx)
) :
typeof metrics === 'object' && metrics !== null ?
Object.entries(metrics).map(([key, value], idx) =>
renderMetric(`${formatKey(key)}: ${value}`, category, idx)
) :
renderMetric(String(metrics), category, 0)
}
</div>
</div>
))}
</div>
);
};

View File

@@ -1,193 +0,0 @@
import React from 'react';
import { Box, FileText } from 'lucide-react';
import { PRPSectionProps } from '../types/prp.types';
import { formatKey, formatValue } from '../utils/formatters';
import { CollapsibleSectionWrapper } from '../components/CollapsibleSectionWrapper';
/**
* Component for rendering complex object structures with nested data
* Used for sections like design systems, architecture, etc.
*/
export const ObjectSection: React.FC<PRPSectionProps> = ({
title,
data,
icon = <Box className="w-5 h-5" />,
accentColor = 'indigo',
isDarkMode = false,
defaultOpen = true,
isCollapsible = true,
isOpen,
onToggle
}) => {
if (!data || typeof data !== 'object') return null;
const colorMap = {
blue: 'from-blue-400 to-blue-600 border-blue-500',
purple: 'from-purple-400 to-purple-600 border-purple-500',
green: 'from-green-400 to-green-600 border-green-500',
orange: 'from-orange-400 to-orange-600 border-orange-500',
pink: 'from-pink-400 to-pink-600 border-pink-500',
cyan: 'from-cyan-400 to-cyan-600 border-cyan-500',
indigo: 'from-indigo-400 to-indigo-600 border-indigo-500',
emerald: 'from-emerald-400 to-emerald-600 border-emerald-500',
};
const bgColorMap = {
blue: 'bg-blue-50 dark:bg-blue-950',
purple: 'bg-purple-50 dark:bg-purple-950',
green: 'bg-green-50 dark:bg-green-950',
orange: 'bg-orange-50 dark:bg-orange-950',
pink: 'bg-pink-50 dark:bg-pink-950',
cyan: 'bg-cyan-50 dark:bg-cyan-950',
indigo: 'bg-indigo-50 dark:bg-indigo-950',
emerald: 'bg-emerald-50 dark:bg-emerald-950',
};
const renderNestedObject = (obj: any, depth: number = 0): React.ReactNode => {
if (!obj || typeof obj !== 'object') {
return <span className="text-gray-700 dark:text-gray-300">{formatValue(obj)}</span>;
}
if (Array.isArray(obj)) {
// Handle empty arrays
if (obj.length === 0) {
return <span className="text-gray-500 italic">No items</span>;
}
// Check if it's a simple array (strings/numbers/booleans)
const isSimpleArray = obj.every(item =>
typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'
);
if (isSimpleArray) {
return (
<ul className="space-y-1 mt-2">
{obj.map((item, index) => (
<li key={index} className="flex items-start gap-2">
<span className="text-gray-400 mt-0.5"></span>
<span className="text-gray-700 dark:text-gray-300">{String(item)}</span>
</li>
))}
</ul>
);
}
// Complex array with objects
return (
<div className="space-y-3 mt-2">
{obj.map((item, index) => (
<div key={index} className={`${depth > 0 ? 'border-l-2 border-gray-200 dark:border-gray-700 pl-4' : ''}`}>
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Item {index + 1}</div>
{renderNestedObject(item, depth + 1)}
</div>
))}
</div>
);
}
// Handle objects
const entries = Object.entries(obj);
// Group entries by type for better organization
const stringEntries = entries.filter(([_, v]) => typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean');
const arrayEntries = entries.filter(([_, v]) => Array.isArray(v));
const objectEntries = entries.filter(([_, v]) => typeof v === 'object' && v !== null && !Array.isArray(v));
return (
<div className={`space-y-3 ${depth > 0 ? 'mt-2' : ''}`}>
{/* Render simple key-value pairs first */}
{stringEntries.length > 0 && (
<div className={`${depth > 0 ? 'ml-4' : ''} space-y-2`}>
{stringEntries.map(([key, value]) => (
<div key={key} className="flex items-start gap-2">
<span className="text-gray-600 dark:text-gray-400 min-w-[100px] text-sm">
{formatKey(key)}:
</span>
<span className="text-gray-700 dark:text-gray-300 text-sm">
{String(value)}
</span>
</div>
))}
</div>
)}
{/* Render arrays */}
{arrayEntries.map(([key, value]) => (
<div key={key} className={`${depth > 0 ? 'ml-4' : ''}`}>
<div className="flex items-start gap-2 mb-2">
<FileText className="w-4 h-4 text-gray-400 mt-1 flex-shrink-0" />
<div className="flex-1">
<h5 className={`font-semibold text-gray-700 dark:text-gray-300 ${depth > 2 ? 'text-sm' : ''}`}>
{formatKey(key)}
</h5>
<div className="text-sm">
{renderNestedObject(value, depth + 1)}
</div>
</div>
</div>
</div>
))}
{/* Render nested objects */}
{objectEntries.map(([key, value]) => {
// Determine if this is a complex nested structure
const isComplex = Object.values(value as object).some(v =>
typeof v === 'object' && v !== null
);
return (
<div key={key} className={`${depth > 0 ? 'ml-4' : ''}`}>
<div className={`
${isComplex ? 'border-l-4 border-gray-300 dark:border-gray-600 pl-4' : ''}
${depth > 1 ? 'mt-4' : ''}
`}>
<h5 className={`
font-semibold text-gray-700 dark:text-gray-300 mb-2
${depth === 0 ? 'text-base' : depth === 1 ? 'text-sm' : 'text-xs'}
`}>
{formatKey(key)}
</h5>
<div className={depth > 2 ? 'text-xs' : 'text-sm'}>
{renderNestedObject(value, depth + 1)}
</div>
</div>
</div>
);
})}
</div>
);
};
const header = (
<div className={`rounded-lg p-6 ${bgColorMap[accentColor as keyof typeof bgColorMap] || bgColorMap.indigo} border-l-4 ${colorMap[accentColor as keyof typeof colorMap].split(' ')[2]}`}>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg bg-gradient-to-br ${colorMap[accentColor as keyof typeof colorMap].split(' ').slice(0, 2).join(' ')} text-white shadow-lg`}>
{icon}
</div>
<h3 className="text-lg font-bold text-gray-800 dark:text-white flex-1">
{title}
</h3>
</div>
</div>
);
const content = (
<div className={`rounded-b-lg px-6 pb-6 -mt-1 ${bgColorMap[accentColor as keyof typeof bgColorMap] || bgColorMap.indigo} border-l-4 ${colorMap[accentColor as keyof typeof colorMap].split(' ')[2]}`}>
{renderNestedObject(data)}
</div>
);
return (
<div className="space-y-0">
<CollapsibleSectionWrapper
header={header}
isCollapsible={isCollapsible}
defaultOpen={defaultOpen}
isOpen={isOpen}
onToggle={onToggle}
>
{content}
</CollapsibleSectionWrapper>
</div>
);
};

View File

@@ -1,184 +0,0 @@
import React, { useState } from 'react';
import { Target, Zap } from 'lucide-react';
import { SectionProps, PRPPersona } from '../types/prp.types';
/**
* Renders user personas with expandable cards
*/
export const PersonaSection: React.FC<SectionProps> = ({
title,
data,
icon,
accentColor = 'purple',
defaultOpen = true,
isDarkMode = false,
}) => {
if (!data || typeof data !== 'object') return null;
return (
<div className="grid gap-4">
{Object.entries(data).map(([key, persona]) => (
<PersonaCard key={key} persona={persona as PRPPersona} personaKey={key} />
))}
</div>
);
};
interface PersonaCardProps {
persona: PRPPersona;
personaKey: string;
}
const PersonaCard: React.FC<PersonaCardProps> = ({ persona, personaKey }) => {
const [isExpanded, setIsExpanded] = useState(false);
const getPersonaIcon = (key: string) => {
if (key.includes('admin')) return '👨‍💼';
if (key.includes('formulator')) return '🧪';
if (key.includes('purchasing')) return '💰';
if (key.includes('developer')) return '👨‍💻';
if (key.includes('designer')) return '🎨';
if (key.includes('manager')) return '👔';
if (key.includes('customer')) return '🛍️';
return '👤';
};
const renderJourney = (journey: Record<string, any>) => {
return (
<div className="space-y-1">
{Object.entries(journey).map(([stage, description]) => (
<div key={stage} className="flex items-start gap-2 text-sm">
<span className="font-medium text-gray-700 dark:text-gray-300 capitalize min-w-[100px]">
{stage}:
</span>
<span className="text-gray-600 dark:text-gray-400">
{typeof description === 'string' ? description : JSON.stringify(description)}
</span>
</div>
))}
</div>
);
};
const renderWorkflow = (workflow: Record<string, any>) => {
return (
<div className="space-y-1">
{Object.entries(workflow).map(([time, task]) => (
<div key={time} className="flex items-start gap-2 text-sm">
<span className="font-medium text-gray-700 dark:text-gray-300 capitalize min-w-[100px]">
{time}:
</span>
<span className="text-gray-600 dark:text-gray-400">
{typeof task === 'string' ? task : JSON.stringify(task)}
</span>
</div>
))}
</div>
);
};
return (
<div className="group">
<div
className="p-6 rounded-xl bg-gradient-to-br from-white/80 to-white/60 dark:from-gray-800/50 dark:to-gray-900/50 border border-gray-200 dark:border-gray-700 hover:border-purple-400 dark:hover:border-purple-500 transition-all duration-300 shadow-lg hover:shadow-xl hover:scale-[1.02] cursor-pointer"
onClick={() => setIsExpanded(!isExpanded)}
>
<div className="flex items-start gap-4">
<div className="text-4xl">{getPersonaIcon(personaKey)}</div>
<div className="flex-1">
<h3 className="text-lg font-bold text-gray-800 dark:text-white mb-1">
{persona.name || personaKey}
</h3>
{persona.role && (
<p className="text-sm text-gray-600 dark:text-gray-400 mb-3">{persona.role}</p>
)}
{/* Always visible goals */}
{persona.goals && Array.isArray(persona.goals) && persona.goals.length > 0 && (
<div className="mb-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
<Target className="w-4 h-4 text-green-500" />
Goals
</h4>
<ul className="space-y-1">
{persona.goals.slice(0, isExpanded ? undefined : 2).map((goal: string, idx: number) => (
<li key={idx} className="text-sm text-gray-600 dark:text-gray-400 flex items-start gap-2">
<span className="text-green-500 mt-0.5"></span>
{goal}
</li>
))}
{!isExpanded && persona.goals.length > 2 && (
<li className="text-sm text-gray-500 dark:text-gray-500 italic">
+{persona.goals.length - 2} more...
</li>
)}
</ul>
</div>
)}
{/* Expandable content */}
{isExpanded && (
<>
{persona.pain_points && Array.isArray(persona.pain_points) && persona.pain_points.length > 0 && (
<div className="mb-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2 flex items-center gap-2">
<Zap className="w-4 h-4 text-orange-500" />
Pain Points
</h4>
<ul className="space-y-1">
{persona.pain_points.map((point: string, idx: number) => (
<li key={idx} className="text-sm text-gray-600 dark:text-gray-400 flex items-start gap-2">
<span className="text-orange-500 mt-0.5"></span>
{point}
</li>
))}
</ul>
</div>
)}
{persona.journey && Object.keys(persona.journey).length > 0 && (
<div className="mb-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
User Journey
</h4>
{renderJourney(persona.journey)}
</div>
)}
{persona.workflow && Object.keys(persona.workflow).length > 0 && (
<div className="mb-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Daily Workflow
</h4>
{renderWorkflow(persona.workflow)}
</div>
)}
{/* Render any other fields */}
{Object.entries(persona).map(([key, value]) => {
if (['name', 'role', 'goals', 'pain_points', 'journey', 'workflow'].includes(key)) {
return null;
}
return (
<div key={key} className="mb-3">
<h4 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2 capitalize">
{key.replace(/_/g, ' ')}
</h4>
<div className="text-sm text-gray-600 dark:text-gray-400">
{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
</div>
</div>
);
})}
</>
)}
</div>
</div>
<div className="mt-3 text-xs text-gray-500 dark:text-gray-500 text-right">
Click to {isExpanded ? 'collapse' : 'expand'} details
</div>
</div>
</div>
);
};

View File

@@ -1,136 +0,0 @@
import React from 'react';
import { Clock, Zap, CheckCircle2 } from 'lucide-react';
import { SectionProps, PRPPhase } from '../types/prp.types';
/**
* Renders implementation plans and phases
*/
export const PlanSection: React.FC<SectionProps> = ({
title,
data,
icon,
accentColor = 'orange',
defaultOpen = true,
isDarkMode = false,
}) => {
if (!data || typeof data !== 'object') return null;
const getPhaseColor = (index: number): string => {
const colors = ['orange', 'yellow', 'green', 'blue', 'purple'];
return colors[index % colors.length];
};
const renderPhase = (phaseKey: string, phase: PRPPhase, index: number) => {
const color = getPhaseColor(index);
const colorMap = {
orange: 'from-orange-50/50 to-yellow-50/50 dark:from-orange-900/20 dark:to-yellow-900/20 border-orange-200 dark:border-orange-800',
yellow: 'from-yellow-50/50 to-amber-50/50 dark:from-yellow-900/20 dark:to-amber-900/20 border-yellow-200 dark:border-yellow-800',
green: 'from-green-50/50 to-emerald-50/50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800',
blue: 'from-blue-50/50 to-cyan-50/50 dark:from-blue-900/20 dark:to-cyan-900/20 border-blue-200 dark:border-blue-800',
purple: 'from-purple-50/50 to-pink-50/50 dark:from-purple-900/20 dark:to-pink-900/20 border-purple-200 dark:border-purple-800',
};
return (
<div
key={phaseKey}
className={`p-4 rounded-lg bg-gradient-to-r ${colorMap[color as keyof typeof colorMap]} border`}
>
<h4 className="font-bold text-gray-800 dark:text-white mb-2 flex items-center gap-2">
<Zap className="w-5 h-5 text-orange-500" />
{phaseKey.toUpperCase()}
{phase.duration && (
<span className="text-sm font-normal text-gray-600 dark:text-gray-400 ml-2">
({phase.duration})
</span>
)}
</h4>
{phase.deliverables && Array.isArray(phase.deliverables) && (
<div className="mb-3">
<h5 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Deliverables
</h5>
<ul className="space-y-1">
{phase.deliverables.map((item: string, idx: number) => (
<li key={idx} className="text-sm text-gray-700 dark:text-gray-300 flex items-start gap-2">
<CheckCircle2 className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
{item}
</li>
))}
</ul>
</div>
)}
{phase.tasks && Array.isArray(phase.tasks) && (
<div>
<h5 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Tasks
</h5>
<ul className="space-y-1">
{phase.tasks.map((task: any, idx: number) => (
<li key={idx} className="text-sm text-gray-700 dark:text-gray-300 flex items-start gap-2">
<div className="w-4 h-4 rounded-full bg-gray-300 dark:bg-gray-600 mt-0.5 flex-shrink-0" />
{typeof task === 'string' ? task : task.description || JSON.stringify(task)}
</li>
))}
</ul>
</div>
)}
{/* Render any other phase properties */}
{Object.entries(phase).map(([key, value]) => {
if (['duration', 'deliverables', 'tasks'].includes(key)) return null;
return (
<div key={key} className="mt-3">
<h5 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1 capitalize">
{key.replace(/_/g, ' ')}
</h5>
<div className="text-sm text-gray-600 dark:text-gray-400">
{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
</div>
</div>
);
})}
</div>
);
};
// Check if this is a phased plan or a general plan structure
const isPhased = Object.values(data).some(value =>
typeof value === 'object' &&
value !== null &&
(value.duration || value.deliverables || value.tasks)
);
if (isPhased) {
return (
<div className="space-y-4">
{Object.entries(data).map(([phaseKey, phase], index) =>
renderPhase(phaseKey, phase as PRPPhase, index)
)}
</div>
);
}
// Fallback to generic rendering for non-phased plans
return (
<div className="p-4 rounded-lg bg-gradient-to-r from-orange-50/50 to-yellow-50/50 dark:from-orange-900/20 dark:to-yellow-900/20 border border-orange-200 dark:border-orange-800">
<h4 className="font-semibold text-gray-800 dark:text-white mb-3 flex items-center gap-2">
<Clock className="w-5 h-5 text-orange-500" />
{title}
</h4>
<div className="space-y-2">
{Object.entries(data).map(([key, value]) => (
<div key={key} className="text-sm">
<span className="font-medium text-gray-700 dark:text-gray-300">
{key.replace(/_/g, ' ').charAt(0).toUpperCase() + key.replace(/_/g, ' ').slice(1)}:
</span>{' '}
<span className="text-gray-600 dark:text-gray-400">
{typeof value === 'string' ? value : JSON.stringify(value, null, 2)}
</span>
</div>
))}
</div>
</div>
);
};

View File

@@ -1,236 +0,0 @@
import React from 'react';
import { Calendar, CheckCircle, AlertCircle } from 'lucide-react';
import { PRPSectionProps } from '../types/prp.types';
import { formatKey, formatValue } from '../utils/formatters';
import { CollapsibleSectionWrapper } from '../components/CollapsibleSectionWrapper';
/**
* Component for rendering rollout plans and deployment strategies
*/
export const RolloutPlanSection: React.FC<PRPSectionProps> = ({
title,
data,
icon = <Calendar className="w-5 h-5" />,
accentColor = 'orange',
isDarkMode = false,
defaultOpen = true,
isCollapsible = true,
isOpen,
onToggle
}) => {
if (!data) return null;
const colorMap = {
blue: 'from-blue-400 to-blue-600 border-blue-500',
purple: 'from-purple-400 to-purple-600 border-purple-500',
green: 'from-green-400 to-green-600 border-green-500',
orange: 'from-orange-400 to-orange-600 border-orange-500',
pink: 'from-pink-400 to-pink-600 border-pink-500',
cyan: 'from-cyan-400 to-cyan-600 border-cyan-500',
indigo: 'from-indigo-400 to-indigo-600 border-indigo-500',
emerald: 'from-emerald-400 to-emerald-600 border-emerald-500',
};
const bgColorMap = {
blue: 'bg-blue-50 dark:bg-blue-950',
purple: 'bg-purple-50 dark:bg-purple-950',
green: 'bg-green-50 dark:bg-green-950',
orange: 'bg-orange-50 dark:bg-orange-950',
pink: 'bg-pink-50 dark:bg-pink-950',
cyan: 'bg-cyan-50 dark:bg-cyan-950',
indigo: 'bg-indigo-50 dark:bg-indigo-950',
emerald: 'bg-emerald-50 dark:bg-emerald-950',
};
const renderPhase = (phase: any, index: number) => {
if (typeof phase === 'string') {
return (
<div key={index} className="flex items-start gap-3">
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-sm font-bold">
{index + 1}
</div>
<div className="flex-1">
<p className="text-gray-700 dark:text-gray-300">{phase}</p>
</div>
</div>
);
}
if (typeof phase === 'object' && phase !== null) {
const phaseName = phase.name || phase.title || phase.phase || `Phase ${index + 1}`;
const duration = phase.duration || phase.timeline || phase.timeframe;
const description = phase.description || phase.details || phase.summary;
const tasks = phase.tasks || phase.activities || phase.items;
const risks = phase.risks || phase.considerations;
return (
<div key={index} className="border-l-4 border-gray-300 dark:border-gray-600 pl-4 ml-4">
<div className="flex items-start gap-3 mb-3">
<div className="flex-shrink-0 w-10 h-10 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 text-white flex items-center justify-center font-bold shadow-md">
{index + 1}
</div>
<div className="flex-1">
<h4 className="font-bold text-gray-800 dark:text-white text-lg">{phaseName}</h4>
{duration && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-1">{duration}</p>
)}
</div>
</div>
{description && (
<p className="text-gray-700 dark:text-gray-300 mb-3 ml-13">{description}</p>
)}
{tasks && Array.isArray(tasks) && tasks.length > 0 && (
<div className="ml-13 mb-3">
<p className="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">Tasks:</p>
<ul className="space-y-1">
{tasks.map((task, taskIndex) => (
<li key={taskIndex} className="flex items-start gap-2 text-sm">
<CheckCircle className="w-4 h-4 text-green-500 mt-0.5 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">{formatValue(task)}</span>
</li>
))}
</ul>
</div>
)}
{risks && Array.isArray(risks) && risks.length > 0 && (
<div className="ml-13">
<p className="text-sm font-semibold text-gray-600 dark:text-gray-400 mb-2">Risks & Considerations:</p>
<ul className="space-y-1">
{risks.map((risk, riskIndex) => (
<li key={riskIndex} className="flex items-start gap-2 text-sm">
<AlertCircle className="w-4 h-4 text-yellow-500 mt-0.5 flex-shrink-0" />
<span className="text-gray-700 dark:text-gray-300">{formatValue(risk)}</span>
</li>
))}
</ul>
</div>
)}
{/* Render any other properties */}
{Object.entries(phase).map(([key, value]) => {
if (['name', 'title', 'phase', 'duration', 'timeline', 'timeframe', 'description', 'details', 'summary', 'tasks', 'activities', 'items', 'risks', 'considerations'].includes(key)) {
return null;
}
return (
<div key={key} className="ml-13 mt-3">
<p className="text-sm font-semibold text-gray-600 dark:text-gray-400">{formatKey(key)}:</p>
<div className="mt-1 text-sm text-gray-700 dark:text-gray-300">
{typeof value === 'string' || typeof value === 'number' ? (
<span>{value}</span>
) : Array.isArray(value) ? (
<ul className="space-y-1 mt-1">
{value.map((item, i) => (
<li key={i} className="flex items-start gap-2">
<span className="text-gray-400"></span>
<span>{formatValue(item)}</span>
</li>
))}
</ul>
) : (
<pre className="mt-1 p-2 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-x-auto">
{JSON.stringify(value, null, 2)}
</pre>
)}
</div>
</div>
);
})}
</div>
);
}
return null;
};
const renderRolloutPlan = () => {
// Handle array of phases
if (Array.isArray(data)) {
return (
<div className="space-y-6">
{data.map((phase, index) => renderPhase(phase, index))}
</div>
);
}
// Handle object with phases
if (typeof data === 'object' && data !== null) {
const phases = data.phases || data.plan || data.steps || data.stages;
if (phases && Array.isArray(phases)) {
return (
<div className="space-y-6">
{phases.map((phase, index) => renderPhase(phase, index))}
</div>
);
}
// Handle object with other properties
return (
<div className="space-y-4">
{Object.entries(data).map(([key, value]) => (
<div key={key}>
<h4 className="font-semibold text-gray-700 dark:text-gray-300 mb-2">
{formatKey(key)}
</h4>
{Array.isArray(value) ? (
<div className="space-y-4">
{value.map((item, index) => renderPhase(item, index))}
</div>
) : typeof value === 'object' && value !== null ? (
<div className="pl-4 border-l-2 border-gray-200 dark:border-gray-700">
{renderPhase(value, 0)}
</div>
) : (
<p className="text-gray-700 dark:text-gray-300">{formatValue(value)}</p>
)}
</div>
))}
</div>
);
}
// Handle string
if (typeof data === 'string') {
return <p className="text-gray-700 dark:text-gray-300">{data}</p>;
}
return null;
};
const header = (
<div className={`rounded-lg p-6 ${bgColorMap[accentColor as keyof typeof bgColorMap] || bgColorMap.orange} border-l-4 ${colorMap[accentColor as keyof typeof colorMap].split(' ')[2]}`}>
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg bg-gradient-to-br ${colorMap[accentColor as keyof typeof colorMap].split(' ').slice(0, 2).join(' ')} text-white shadow-lg`}>
{icon}
</div>
<h3 className="text-lg font-bold text-gray-800 dark:text-white flex-1">
{title}
</h3>
</div>
</div>
);
const content = (
<div className={`rounded-b-lg px-6 pb-6 -mt-1 ${bgColorMap[accentColor as keyof typeof bgColorMap] || bgColorMap.orange} border-l-4 ${colorMap[accentColor as keyof typeof colorMap].split(' ')[2]}`}>
{renderRolloutPlan()}
</div>
);
return (
<div className="space-y-0">
<CollapsibleSectionWrapper
header={header}
isCollapsible={isCollapsible}
defaultOpen={defaultOpen}
isOpen={isOpen}
onToggle={onToggle}
>
{content}
</CollapsibleSectionWrapper>
</div>
);
};

View File

@@ -1,235 +0,0 @@
import React from 'react';
import { Palette, Layers } from 'lucide-react';
import { PRPSectionProps } from '../types/prp.types';
import { formatKey, formatValue } from '../utils/formatters';
/**
* Component for rendering design token systems and style guides
*/
export const TokenSystemSection: React.FC<PRPSectionProps> = ({
title,
data,
icon = <Palette className="w-5 h-5" />,
accentColor = 'indigo',
isDarkMode = false,
defaultOpen = true
}) => {
if (!data) return null;
const colorMap = {
blue: 'from-blue-400 to-blue-600 border-blue-500',
purple: 'from-purple-400 to-purple-600 border-purple-500',
green: 'from-green-400 to-green-600 border-green-500',
orange: 'from-orange-400 to-orange-600 border-orange-500',
pink: 'from-pink-400 to-pink-600 border-pink-500',
cyan: 'from-cyan-400 to-cyan-600 border-cyan-500',
indigo: 'from-indigo-400 to-indigo-600 border-indigo-500',
emerald: 'from-emerald-400 to-emerald-600 border-emerald-500',
};
const bgColorMap = {
blue: 'bg-blue-50 dark:bg-blue-950',
purple: 'bg-purple-50 dark:bg-purple-950',
green: 'bg-green-50 dark:bg-green-950',
orange: 'bg-orange-50 dark:bg-orange-950',
pink: 'bg-pink-50 dark:bg-pink-950',
cyan: 'bg-cyan-50 dark:bg-cyan-950',
indigo: 'bg-indigo-50 dark:bg-indigo-950',
emerald: 'bg-emerald-50 dark:bg-emerald-950',
};
const renderColorSwatch = (color: string, name: string) => {
// Check if it's a valid color value
const isHex = /^#[0-9A-F]{6}$/i.test(color);
const isRgb = /^rgb/.test(color);
const isHsl = /^hsl/.test(color);
const isNamedColor = /^[a-z]+$/i.test(color);
if (isHex || isRgb || isHsl || isNamedColor) {
return (
<div className="flex items-center gap-3">
<div
className="w-12 h-12 rounded-lg border-2 border-gray-300 dark:border-gray-600 shadow-sm"
style={{ backgroundColor: color }}
/>
<div>
<p className="font-medium text-gray-700 dark:text-gray-300">{name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{color}</p>
</div>
</div>
);
}
return null;
};
const renderSpacingValue = (value: string | number, name: string) => {
const numValue = typeof value === 'string' ? parseFloat(value) : value;
const unit = typeof value === 'string' ? value.replace(/[0-9.-]/g, '') : 'px';
return (
<div className="flex items-center gap-3">
<div
className="bg-indigo-500 rounded"
style={{
width: `${Math.min(numValue * 2, 100)}px`,
height: '24px'
}}
/>
<div>
<p className="font-medium text-gray-700 dark:text-gray-300">{name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{value}{unit}</p>
</div>
</div>
);
};
const renderTokenGroup = (tokens: any, groupName: string) => {
if (!tokens || typeof tokens !== 'object') return null;
const entries = Object.entries(tokens);
const isColorGroup = groupName.toLowerCase().includes('color') ||
entries.some(([_, v]) => typeof v === 'string' && (v.startsWith('#') || v.startsWith('rgb')));
const isSpacingGroup = groupName.toLowerCase().includes('spacing') ||
groupName.toLowerCase().includes('size') ||
groupName.toLowerCase().includes('radius');
return (
<div className="space-y-3">
<h4 className="font-semibold text-gray-700 dark:text-gray-300 flex items-center gap-2">
<Layers className="w-4 h-4" />
{formatKey(groupName)}
</h4>
<div className={`grid gap-4 ${isColorGroup ? 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3' : 'grid-cols-1'}`}>
{entries.map(([key, value]) => {
if (isColorGroup && typeof value === 'string') {
const swatch = renderColorSwatch(value, formatKey(key));
if (swatch) return <div key={key}>{swatch}</div>;
}
if (isSpacingGroup && (typeof value === 'string' || typeof value === 'number')) {
return <div key={key}>{renderSpacingValue(value, formatKey(key))}</div>;
}
// Handle nested token groups
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
return (
<div key={key} className="col-span-full">
{renderTokenGroup(value, key)}
</div>
);
}
// Default rendering
return (
<div key={key} className="flex items-start gap-2">
<span className="font-medium text-gray-600 dark:text-gray-400">{formatKey(key)}:</span>
<span className="text-gray-700 dark:text-gray-300">{formatValue(value)}</span>
</div>
);
})}
</div>
</div>
);
};
const renderTokenSystem = () => {
// Handle string description
if (typeof data === 'string') {
return <p className="text-gray-700 dark:text-gray-300">{data}</p>;
}
// Handle array of token groups
if (Array.isArray(data)) {
return (
<div className="space-y-6">
{data.map((group, index) => (
<div key={index}>
{typeof group === 'object' && group !== null ? (
renderTokenGroup(group, `Group ${index + 1}`)
) : (
<p className="text-gray-700 dark:text-gray-300">{formatValue(group)}</p>
)}
</div>
))}
</div>
);
}
// Handle object with token categories
if (typeof data === 'object' && data !== null) {
const categories = Object.entries(data);
// Special handling for common token categories
const colorTokens = categories.filter(([k]) => k.toLowerCase().includes('color'));
const typographyTokens = categories.filter(([k]) => k.toLowerCase().includes('typography') || k.toLowerCase().includes('font'));
const spacingTokens = categories.filter(([k]) => k.toLowerCase().includes('spacing') || k.toLowerCase().includes('size'));
const otherTokens = categories.filter(([k]) =>
!k.toLowerCase().includes('color') &&
!k.toLowerCase().includes('typography') &&
!k.toLowerCase().includes('font') &&
!k.toLowerCase().includes('spacing') &&
!k.toLowerCase().includes('size')
);
return (
<div className="space-y-8">
{/* Colors */}
{colorTokens.length > 0 && (
<div className="space-y-6">
{colorTokens.map(([key, value]) => (
<div key={key}>{renderTokenGroup(value, key)}</div>
))}
</div>
)}
{/* Typography */}
{typographyTokens.length > 0 && (
<div className="space-y-6">
{typographyTokens.map(([key, value]) => (
<div key={key}>{renderTokenGroup(value, key)}</div>
))}
</div>
)}
{/* Spacing */}
{spacingTokens.length > 0 && (
<div className="space-y-6">
{spacingTokens.map(([key, value]) => (
<div key={key}>{renderTokenGroup(value, key)}</div>
))}
</div>
)}
{/* Others */}
{otherTokens.length > 0 && (
<div className="space-y-6">
{otherTokens.map(([key, value]) => (
<div key={key}>{renderTokenGroup(value, key)}</div>
))}
</div>
)}
</div>
);
}
return null;
};
return (
<div className="space-y-4">
<div className={`rounded-lg p-6 ${bgColorMap[accentColor as keyof typeof bgColorMap] || bgColorMap.indigo} border-l-4 ${colorMap[accentColor as keyof typeof colorMap].split(' ')[2]}`}>
<div className="flex items-center gap-3 mb-4">
<div className={`p-2 rounded-lg bg-gradient-to-br ${colorMap[accentColor as keyof typeof colorMap].split(' ').slice(0, 2).join(' ')} text-white shadow-lg`}>
{icon}
</div>
<h3 className="text-lg font-bold text-gray-800 dark:text-white">
{title}
</h3>
</div>
{renderTokenSystem()}
</div>
</div>
);
};

View File

@@ -1,121 +0,0 @@
import { ReactNode } from 'react';
// Base section types
export type SectionType =
| 'metadata'
| 'context'
| 'personas'
| 'flows'
| 'metrics'
| 'plan'
| 'list'
| 'object'
| 'keyvalue'
| 'features'
| 'generic';
export interface SectionProps {
title: string;
data: any;
icon?: ReactNode;
accentColor?: string;
defaultOpen?: boolean;
isDarkMode?: boolean;
isCollapsible?: boolean;
onToggle?: () => void;
isOpen?: boolean;
}
// Alias for component compatibility
export type PRPSectionProps = SectionProps;
export interface PRPMetadata {
title?: string;
version?: string;
author?: string;
date?: string;
status?: string;
document_type?: string;
[key: string]: any;
}
export interface PRPContext {
scope?: string;
background?: string;
objectives?: string[];
requirements?: any;
[key: string]: any;
}
export interface PRPPersona {
name?: string;
role?: string;
goals?: string[];
pain_points?: string[];
journey?: Record<string, any>;
workflow?: Record<string, any>;
[key: string]: any;
}
export interface PRPPhase {
duration?: string;
deliverables?: string[];
tasks?: any[];
[key: string]: any;
}
export interface PRPContent {
// Common fields
title?: string;
version?: string;
author?: string;
date?: string;
status?: string;
document_type?: string;
// Section fields
context?: PRPContext;
user_personas?: Record<string, PRPPersona>;
user_flows?: Record<string, any>;
success_metrics?: Record<string, string[] | Record<string, any>>;
implementation_plan?: Record<string, PRPPhase>;
validation_gates?: Record<string, string[]>;
technical_implementation?: Record<string, any>;
ui_improvements?: Record<string, any>;
information_architecture?: Record<string, any>;
current_state_analysis?: Record<string, any>;
component_architecture?: Record<string, any>;
// Allow any other fields
[key: string]: any;
}
export interface SectionDetectorResult {
type: SectionType;
confidence: number;
}
export interface SectionComponentProps extends SectionProps {
content: PRPContent;
sectionKey: string;
}
// Color maps for consistent theming
export const sectionColorMap: Record<string, string> = {
metadata: 'blue',
context: 'purple',
personas: 'pink',
flows: 'orange',
metrics: 'green',
plan: 'cyan',
technical: 'indigo',
validation: 'emerald',
generic: 'gray'
};
// Icon size constants
export const ICON_SIZES = {
section: 'w-5 h-5',
subsection: 'w-4 h-4',
item: 'w-3 h-3'
} as const;

View File

@@ -1,53 +0,0 @@
import { normalizeImagePlaceholders } from './normalizer';
/**
* Formats a key into a human-readable label
*/
export function formatKey(key: string): string {
return key
.replace(/_/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Truncates text with ellipsis
*/
export function truncateText(text: string, maxLength: number = 100): string {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength - 3) + '...';
}
/**
* Formats a value for display
*/
export function formatValue(value: any): string {
if (value === null || value === undefined) return '';
if (typeof value === 'boolean') return value ? 'Yes' : 'No';
if (typeof value === 'number') return value.toLocaleString();
if (typeof value === 'string') {
// Temporarily disabled to debug black screen issue
// return normalizeImagePlaceholders(value);
return value;
}
if (Array.isArray(value)) return `${value.length} items`;
if (typeof value === 'object') return `${Object.keys(value).length} properties`;
return String(value);
}
/**
* Gets accent color based on index for variety
*/
export function getAccentColor(index: number): string {
const colors = ['blue', 'purple', 'green', 'orange', 'pink', 'cyan', 'indigo', 'emerald'];
return colors[index % colors.length];
}
/**
* Generates a unique key for React components
*/
export function generateKey(prefix: string, ...parts: (string | number)[]): string {
return [prefix, ...parts].filter(Boolean).join('-');
}

View File

@@ -1,397 +0,0 @@
/**
* Markdown Parser for PRP Documents
*
* Parses raw markdown content into structured sections that can be rendered
* by the PRPViewer component with collapsible sections and beautiful formatting.
*/
export interface ParsedSection {
title: string;
content: string;
level: number;
type: 'text' | 'list' | 'code' | 'mixed';
rawContent: string;
sectionKey: string;
templateType?: string; // For matching to PRP templates
}
export interface ParsedMarkdownDocument {
title?: string;
sections: ParsedSection[];
metadata: Record<string, any>;
hasMetadata: boolean;
}
export interface ParsedMarkdown {
title?: string;
sections: Record<string, ParsedSection>;
metadata: Record<string, any>;
}
/**
* Parses markdown content into structured sections based on headers
*/
export function parseMarkdownToPRP(content: string): ParsedMarkdown {
if (!content || typeof content !== 'string') {
return { sections: {}, metadata: {} };
}
const lines = content.split('\n');
const sections: Record<string, ParsedSection> = {};
let currentSection: ParsedSection | null = null;
let documentTitle: string | undefined;
let sectionCounter = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for headers (## Section Name or # Document Title)
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headerMatch) {
const level = headerMatch[1].length;
const title = headerMatch[2].trim();
// Save previous section if exists
if (currentSection) {
const sectionKey = generateSectionKey(currentSection.title, sectionCounter);
sections[sectionKey] = {
...currentSection,
content: currentSection.content.trim(),
rawContent: currentSection.rawContent.trim(),
type: detectContentType(currentSection.content)
};
sectionCounter++;
}
// Handle document title (# level headers)
if (level === 1 && !documentTitle) {
documentTitle = title;
currentSection = null;
continue;
}
// Start new section
currentSection = {
title,
content: '',
level,
type: 'text',
rawContent: ''
};
} else if (currentSection) {
// Add content to current section
currentSection.content += line + '\n';
currentSection.rawContent += line + '\n';
} else if (!documentTitle && line.trim()) {
// If we haven't found a title yet and encounter content, treat first non-empty line as title
documentTitle = line.trim();
}
}
// Save final section
if (currentSection) {
const sectionKey = generateSectionKey(currentSection.title, sectionCounter);
sections[sectionKey] = {
...currentSection,
content: currentSection.content.trim(),
rawContent: currentSection.rawContent.trim(),
type: detectContentType(currentSection.content)
};
}
return {
title: documentTitle,
sections,
metadata: {
document_type: 'prp',
parsed_from_markdown: true,
section_count: Object.keys(sections).length
}
};
}
/**
* Generates a consistent section key for use in the sections object
*/
function generateSectionKey(title: string, counter: number): string {
// Convert title to a key format
const baseKey = title
.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '_')
.substring(0, 30); // Limit length
return baseKey || `section_${counter}`;
}
/**
* Detects the type of content in a section
*/
function detectContentType(content: string): 'text' | 'list' | 'code' | 'mixed' {
if (!content.trim()) return 'text';
const lines = content.split('\n').filter(line => line.trim());
let hasText = false;
let hasList = false;
let hasCode = false;
for (const line of lines) {
if (line.startsWith('```')) {
hasCode = true;
} else if (line.match(/^[-*+]\s/) || line.match(/^\d+\.\s/)) {
hasList = true;
} else if (line.trim()) {
hasText = true;
}
}
if (hasCode) return 'code';
if (hasList && hasText) return 'mixed';
if (hasList) return 'list';
return 'text';
}
/**
* Converts parsed markdown back to a structure compatible with PRPViewer
* Each section becomes a separate collapsible section in the viewer
*/
export function convertParsedMarkdownToPRPStructure(parsed: ParsedMarkdown): any {
const result: any = {
title: parsed.title || 'Untitled Document',
...parsed.metadata
};
// Add each section as a top-level property
// The content will be the raw markdown for that section only
for (const [key, section] of Object.entries(parsed.sections)) {
result[key] = section.rawContent;
}
return result;
}
/**
* Checks if content appears to be raw markdown
*/
export function isMarkdownContent(content: any): boolean {
if (typeof content !== 'string') return false;
// Look for markdown indicators
const markdownIndicators = [
/^#{1,6}\s+.+$/m, // Headers
/^[-*+]\s+.+$/m, // Bullet lists
/^\d+\.\s+.+$/m, // Numbered lists
/```/, // Code blocks
/^\>.+$/m, // Blockquotes
/\*\*.+\*\*/, // Bold text
/\*.+\*/, // Italic text
];
return markdownIndicators.some(pattern => pattern.test(content));
}
/**
* Parses markdown content into a flowing document structure
*/
export function parseMarkdownToDocument(content: string): ParsedMarkdownDocument {
if (!content || typeof content !== 'string') {
return { sections: [], metadata: {}, hasMetadata: false };
}
const lines = content.split('\n');
const sections: ParsedSection[] = [];
let currentSection: Partial<ParsedSection> | null = null;
let documentTitle: string | undefined;
let sectionCounter = 0;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for headers (## Section Name or # Document Title)
const headerMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headerMatch) {
const level = headerMatch[1].length;
const title = headerMatch[2].trim();
// Save previous section if exists
if (currentSection && currentSection.title) {
sections.push({
title: currentSection.title,
content: (currentSection.content || '').trim(),
level: currentSection.level || 2,
type: detectContentType(currentSection.content || ''),
rawContent: (currentSection.rawContent || '').trim(),
sectionKey: generateSectionKey(currentSection.title, sectionCounter),
templateType: detectTemplateType(currentSection.title)
});
sectionCounter++;
}
// Handle document title (# level headers)
if (level === 1 && !documentTitle) {
documentTitle = title;
currentSection = null;
continue;
}
// Start new section
currentSection = {
title,
content: '',
level,
rawContent: ''
};
} else if (currentSection) {
// Add content to current section
currentSection.content = (currentSection.content || '') + line + '\n';
currentSection.rawContent = (currentSection.rawContent || '') + line + '\n';
} else if (!documentTitle && line.trim()) {
// If we haven't found a title yet and encounter content, treat first non-empty line as title
documentTitle = line.trim();
}
}
// Save final section
if (currentSection && currentSection.title) {
sections.push({
title: currentSection.title,
content: (currentSection.content || '').trim(),
level: currentSection.level || 2,
type: detectContentType(currentSection.content || ''),
rawContent: (currentSection.rawContent || '').trim(),
sectionKey: generateSectionKey(currentSection.title, sectionCounter),
templateType: detectTemplateType(currentSection.title)
});
}
return {
title: documentTitle,
sections,
metadata: {
document_type: 'prp', // Set as PRP to get the right styling
section_count: sections.length,
parsed_from_markdown: true
},
hasMetadata: false
};
}
/**
* Detects if a section title matches a known PRP template type
*/
function detectTemplateType(title: string): string | undefined {
const normalizedTitle = title.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
// Map common PRP section names to template types
const templateMap: Record<string, string> = {
'goal': 'context',
'objective': 'context',
'purpose': 'context',
'why': 'context',
'rationale': 'context',
'what': 'context',
'description': 'context',
'overview': 'context',
'context': 'context',
'background': 'context',
'problem statement': 'context',
'success metrics': 'metrics',
'metrics': 'metrics',
'kpis': 'metrics',
'success criteria': 'metrics',
'estimated impact': 'metrics',
'implementation plan': 'plan',
'plan': 'plan',
'roadmap': 'plan',
'timeline': 'plan',
'phases': 'plan',
'rollout plan': 'plan',
'migration strategy': 'plan',
'personas': 'personas',
'users': 'personas',
'stakeholders': 'personas',
'target audience': 'personas',
'user flow': 'flows',
'user journey': 'flows',
'workflow': 'flows',
'user experience': 'flows',
'validation': 'list',
'testing': 'list',
'quality gates': 'list',
'acceptance criteria': 'list',
'features': 'features',
'feature requirements': 'features',
'capabilities': 'features',
'technical requirements': 'object',
'architecture': 'object',
'design': 'object',
'components': 'object',
'budget': 'keyvalue',
'resources': 'keyvalue',
'team': 'keyvalue',
'cost': 'keyvalue'
};
return templateMap[normalizedTitle];
}
/**
* Checks if content is a document with metadata structure
*/
export function isDocumentWithMetadata(content: any): boolean {
if (typeof content !== 'object' || content === null) return false;
// Check if it has typical document metadata fields
const metadataFields = ['title', 'version', 'author', 'date', 'status', 'document_type', 'created_at', 'updated_at'];
const hasMetadata = metadataFields.some(field => field in content);
// Check if it has a content field that looks like markdown
const hasMarkdownContent = typeof content.content === 'string' &&
isMarkdownContent(content.content);
// Also check if any field contains markdown content (broader detection)
const hasAnyMarkdownField = Object.values(content).some(value =>
typeof value === 'string' && isMarkdownContent(value)
);
// Return true if it has metadata AND markdown content, OR if it has obvious document structure
return (hasMetadata && (hasMarkdownContent || hasAnyMarkdownField)) ||
(hasMetadata && Object.keys(content).length <= 10); // Simple document structure
}
/**
* Main function to process content for PRPViewer
*/
export function processContentForPRP(content: any): any {
// If it's already an object, return as-is
if (typeof content === 'object' && content !== null) {
return content;
}
// If it's a string that looks like markdown, parse it
if (typeof content === 'string' && isMarkdownContent(content)) {
const parsed = parseMarkdownToPRP(content);
return convertParsedMarkdownToPRPStructure(parsed);
}
// For any other string content, wrap it in a generic structure
if (typeof content === 'string') {
return {
title: 'Document Content',
content: content,
document_type: 'text'
};
}
return content;
}

View File

@@ -1,211 +0,0 @@
/**
* Normalizes PRP document data to ensure consistent rendering
*/
/**
* Normalizes image placeholders to proper markdown format
*/
export function normalizeImagePlaceholders(content: string): string {
return content.replace(/\[Image #(\d+)\]/g, (match, num) => {
return `![Image ${num}](placeholder-image-${num})`;
});
}
/**
* Attempts to parse JSON strings into objects
*/
export function parseJsonStrings(value: any): any {
if (typeof value === 'string') {
const trimmed = value.trim();
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
return JSON.parse(trimmed);
} catch (e) {
// Return original string if parsing fails
return value;
}
}
// Normalize image placeholders in strings
return normalizeImagePlaceholders(value);
}
if (Array.isArray(value)) {
return value.map(item => parseJsonStrings(item));
}
if (value && typeof value === 'object') {
const normalized: any = {};
for (const [key, val] of Object.entries(value)) {
normalized[key] = parseJsonStrings(val);
}
return normalized;
}
return value;
}
/**
* Flattens nested content fields
*/
export function flattenNestedContent(data: any): any {
// Handle nested content field
if (data && typeof data === 'object' && 'content' in data) {
const { content, ...rest } = data;
// If content is an object, merge it with the rest
if (content && typeof content === 'object' && !Array.isArray(content)) {
return flattenNestedContent({ ...rest, ...content });
}
// If content is a string or array, keep it as a field
return { ...rest, content };
}
return data;
}
/**
* Normalizes section names to be more readable
*/
export function normalizeSectionName(name: string): string {
// Common abbreviations and their expansions
const expansions: Record<string, string> = {
'ui': 'User Interface',
'ux': 'User Experience',
'api': 'API',
'kpi': 'KPI',
'prp': 'PRP',
'prd': 'PRD',
'mvp': 'MVP',
'poc': 'Proof of Concept',
};
// Split by underscore or camelCase
const words = name
.replace(/_/g, ' ')
.replace(/([a-z])([A-Z])/g, '$1 $2')
.split(' ')
.filter(word => word.length > 0);
// Process each word
const processed = words.map(word => {
const lower = word.toLowerCase();
// Check if it's a known abbreviation
if (expansions[lower]) {
return expansions[lower];
}
// Otherwise, capitalize first letter
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase();
});
return processed.join(' ');
}
/**
* Normalizes the entire PRP document structure
*/
export function normalizePRPDocument(content: any): any {
if (!content) return content;
// First, flatten any nested content fields
let normalized = flattenNestedContent(content);
// Then parse any JSON strings
normalized = parseJsonStrings(normalized);
// Handle raw markdown content
if (typeof normalized === 'string') {
// For strings, just normalize image placeholders and return as-is
// The PRPViewer will handle the markdown parsing
return normalizeImagePlaceholders(normalized);
}
// For objects, process each field recursively
if (normalized && typeof normalized === 'object' && !Array.isArray(normalized)) {
const result: any = {};
for (const [key, value] of Object.entries(normalized)) {
// Skip empty values
if (value === null || value === undefined ||
(typeof value === 'string' && value.trim() === '') ||
(Array.isArray(value) && value.length === 0) ||
(typeof value === 'object' && Object.keys(value).length === 0)) {
continue;
}
// Recursively process nested values
if (typeof value === 'string') {
result[key] = normalizeImagePlaceholders(value);
} else if (Array.isArray(value)) {
result[key] = value.map(item =>
typeof item === 'string' ? normalizeImagePlaceholders(item) : normalizePRPDocument(item)
);
} else if (typeof value === 'object') {
result[key] = normalizePRPDocument(value);
} else {
result[key] = value;
}
}
return result;
}
// For arrays, process each item
if (Array.isArray(normalized)) {
return normalized.map(item =>
typeof item === 'string' ? normalizeImagePlaceholders(item) : normalizePRPDocument(item)
);
}
return normalized;
}
/**
* Checks if a value contains complex nested structures
*/
export function hasComplexNesting(value: any): boolean {
if (!value || typeof value !== 'object') return false;
if (Array.isArray(value)) {
return value.some(item =>
typeof item === 'object' && item !== null
);
}
return Object.values(value).some(val =>
(typeof val === 'object' && val !== null) ||
(Array.isArray(val) && val.some(item => typeof item === 'object'))
);
}
/**
* Extracts metadata fields from content
*/
export function extractMetadata(content: any): { metadata: any; sections: any } {
if (!content || typeof content !== 'object') {
return { metadata: {}, sections: content };
}
const metadataFields = [
'title', 'version', 'author', 'date', 'status',
'document_type', 'created_at', 'updated_at',
'id', '_id', 'project_id'
];
const metadata: any = {};
const sections: any = {};
for (const [key, value] of Object.entries(content)) {
if (metadataFields.includes(key)) {
metadata[key] = value;
} else {
sections[key] = value;
}
}
return { metadata, sections };
}

View File

@@ -1,107 +0,0 @@
import React from 'react';
import { formatKey, formatValue } from './formatters';
/**
* Renders any value in a formatted way without using JSON.stringify
*/
export function renderValue(value: any, depth: number = 0): React.ReactNode {
try {
// Prevent infinite recursion
if (depth > 10) {
return <span className="text-gray-500 italic">Too deeply nested</span>;
}
// Handle null/undefined
if (value === null || value === undefined) {
return <span className="text-gray-400 italic">Empty</span>;
}
// Handle primitives
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return <span className="text-gray-700 dark:text-gray-300">{formatValue(value)}</span>;
}
// Handle arrays
if (Array.isArray(value)) {
if (value.length === 0) {
return <span className="text-gray-400 italic">No items</span>;
}
// Check if it's a simple array
const isSimple = value.every(item =>
typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'
);
if (isSimple) {
return (
<ul className="list-disc list-inside space-y-1">
{value.map((item, index) => (
<li key={index} className="text-gray-700 dark:text-gray-300">
{formatValue(item)}
</li>
))}
</ul>
);
}
// Complex array
return (
<div className="space-y-2">
{value.map((item, index) => (
<div key={index} className="pl-4 border-l-2 border-gray-200 dark:border-gray-700">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">Item {index + 1}</div>
{renderValue(item, depth + 1)}
</div>
))}
</div>
);
}
// Handle objects
if (typeof value === 'object' && value !== null) {
const entries = Object.entries(value);
if (entries.length === 0) {
return <span className="text-gray-400 italic">No properties</span>;
}
return (
<div className="space-y-2">
{entries.map(([key, val]) => (
<div key={key} className="flex flex-col gap-1">
<span className="font-medium text-gray-600 dark:text-gray-400">
{formatKey(key)}:
</span>
<div className="pl-4">
{renderValue(val, depth + 1)}
</div>
</div>
))}
</div>
);
}
// Fallback
return <span className="text-gray-700 dark:text-gray-300">{String(value)}</span>;
} catch (error) {
console.error('Error rendering value:', error, value);
return <span className="text-red-500 italic">Error rendering content</span>;
}
}
/**
* Renders a value inline for simple display
*/
export function renderValueInline(value: any): string {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return formatValue(value);
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
if (Array.isArray(value)) return value.map(v => renderValueInline(v)).join(', ');
if (typeof value === 'object') {
// For objects, just show a summary
const keys = Object.keys(value);
if (keys.length === 0) return 'Empty object';
if (keys.length <= 3) return keys.map(k => `${k}: ${renderValueInline(value[k])}`).join(', ');
return `${keys.length} properties`;
}
return String(value);
}

View File

@@ -1,204 +0,0 @@
import { SectionType, SectionDetectorResult } from '../types/prp.types';
/**
* Detects the type of a section based on its key and content structure
*/
export function detectSectionType(key: string, value: any): SectionDetectorResult {
const normalizedKey = key.toLowerCase().replace(/_/g, '').replace(/\s+/g, '');
// Check metadata fields
if (['title', 'version', 'author', 'date', 'status', 'documenttype'].includes(normalizedKey)) {
return { type: 'metadata', confidence: 1.0 };
}
// Check context sections (including common markdown headers)
if (normalizedKey === 'context' || normalizedKey === 'overview' ||
normalizedKey === 'executivesummary' || normalizedKey === 'problemstatement' ||
normalizedKey === 'visionstatement' || normalizedKey === 'proposedsolution' ||
normalizedKey === 'goal' || normalizedKey === 'objective' || normalizedKey === 'purpose' ||
normalizedKey === 'why' || normalizedKey === 'rationale' || normalizedKey === 'what' ||
normalizedKey === 'description' || normalizedKey === 'background') {
return { type: 'context', confidence: 1.0 };
}
// Check personas
if (normalizedKey.includes('persona') || normalizedKey.includes('user') ||
normalizedKey === 'stakeholders' || normalizedKey === 'targetaudience') {
// Always treat these as personas, even if structure doesn't match perfectly
return { type: 'personas', confidence: 0.9 };
}
// Check flows/journeys
if (normalizedKey.includes('flow') || normalizedKey.includes('journey') ||
normalizedKey.includes('workflow') || normalizedKey === 'userexperience') {
return { type: 'flows', confidence: 0.9 };
}
// Check metrics (including common markdown headers)
if (normalizedKey.includes('metric') || normalizedKey.includes('success') ||
normalizedKey.includes('kpi') || normalizedKey === 'estimatedimpact' ||
normalizedKey === 'successmetrics' || normalizedKey === 'successcriteria') {
return { type: 'metrics', confidence: 0.9 };
}
// Check implementation plans (including common markdown headers)
if (normalizedKey.includes('plan') || normalizedKey.includes('phase') ||
normalizedKey.includes('implementation') || normalizedKey.includes('roadmap') ||
normalizedKey === 'timeline' || normalizedKey === 'rolloutplan' ||
normalizedKey === 'migrationstrategy' || normalizedKey === 'implementationplan') {
return { type: 'plan', confidence: 0.9 };
}
// Check validation/testing (including common markdown headers)
if (normalizedKey.includes('validation') || normalizedKey.includes('test') ||
normalizedKey.includes('gate') || normalizedKey === 'compliance' ||
normalizedKey.includes('quality') || normalizedKey === 'accessibilitystandards' ||
normalizedKey === 'acceptancecriteria' || normalizedKey === 'qualitygates') {
return { type: 'list', confidence: 0.8 };
}
// Check risk assessment
if (normalizedKey.includes('risk') || normalizedKey === 'riskassessment') {
return { type: 'list', confidence: 0.9 };
}
// Check design/architecture sections
if (normalizedKey.includes('design') || normalizedKey.includes('architecture') ||
normalizedKey.includes('component') || normalizedKey === 'tokensystem' ||
normalizedKey === 'designprinciples' || normalizedKey === 'designguidelines') {
return { type: 'object', confidence: 0.8 };
}
// Check budget/resources
if (normalizedKey.includes('budget') || normalizedKey.includes('resource') ||
normalizedKey.includes('cost') || normalizedKey === 'team' ||
normalizedKey === 'budgetestimate' || normalizedKey === 'budgetandresources') {
return { type: 'keyvalue', confidence: 0.9 };
}
// Check feature requirements specifically
if (normalizedKey === 'featurerequirements' || normalizedKey === 'features' ||
normalizedKey === 'capabilities') {
return { type: 'features', confidence: 0.9 };
}
// Check requirements
if (normalizedKey.includes('requirement') ||
normalizedKey === 'technicalrequirements') {
return { type: 'object', confidence: 0.8 };
}
// Check data/information sections
if (normalizedKey.includes('data') || normalizedKey.includes('information') ||
normalizedKey === 'currentstateanalysis' || normalizedKey === 'informationarchitecture') {
return { type: 'object', confidence: 0.8 };
}
// Check governance/process sections
if (normalizedKey.includes('governance') || normalizedKey.includes('process') ||
normalizedKey === 'governancemodel' || normalizedKey === 'testingstrategy') {
return { type: 'object', confidence: 0.8 };
}
// Check technical sections
if (normalizedKey.includes('technical') || normalizedKey.includes('tech') ||
normalizedKey === 'aimodelspecifications' || normalizedKey === 'performancerequirements' ||
normalizedKey === 'toolingandinfrastructure' || normalizedKey === 'monitoringandanalytics') {
return { type: 'object', confidence: 0.8 };
}
// Analyze value structure
if (Array.isArray(value)) {
return { type: 'list', confidence: 0.7 };
}
if (typeof value === 'object' && value !== null) {
// Check if it's a simple key-value object
if (isSimpleKeyValue(value)) {
return { type: 'keyvalue', confidence: 0.7 };
}
// Check if it's a complex nested object
if (hasNestedObjects(value)) {
return { type: 'object', confidence: 0.7 };
}
}
// Default fallback
return { type: 'generic', confidence: 0.5 };
}
/**
* Checks if the value structure matches a persona pattern
*/
function isPersonaStructure(value: any): boolean {
if (typeof value !== 'object' || value === null) return false;
// Check if it's a collection of personas
const values = Object.values(value);
if (values.length === 0) return false;
// Check if first value has persona-like properties
const firstValue = values[0];
if (typeof firstValue !== 'object') return false;
const personaKeys = ['name', 'role', 'goals', 'pain_points', 'journey', 'workflow'];
return personaKeys.some(key => key in firstValue);
}
/**
* Checks if an object is a simple key-value structure
*/
function isSimpleKeyValue(obj: any): boolean {
if (typeof obj !== 'object' || obj === null) return false;
const values = Object.values(obj);
return values.every(val =>
typeof val === 'string' ||
typeof val === 'number' ||
typeof val === 'boolean'
);
}
/**
* Checks if an object has nested objects
*/
function hasNestedObjects(obj: any): boolean {
if (typeof obj !== 'object' || obj === null) return false;
const values = Object.values(obj);
return values.some(val =>
typeof val === 'object' &&
val !== null &&
!Array.isArray(val)
);
}
/**
* Gets a suggested icon based on section key
*/
export function getSectionIcon(key: string): string {
const normalizedKey = key.toLowerCase();
if (normalizedKey.includes('persona') || normalizedKey.includes('user')) return 'Users';
if (normalizedKey.includes('flow') || normalizedKey.includes('journey')) return 'Workflow';
if (normalizedKey.includes('metric') || normalizedKey.includes('success')) return 'BarChart3';
if (normalizedKey.includes('plan') || normalizedKey.includes('implementation')) return 'Clock';
if (normalizedKey.includes('context') || normalizedKey.includes('overview')) return 'Brain';
if (normalizedKey.includes('technical') || normalizedKey.includes('tech')) return 'Code';
if (normalizedKey.includes('validation') || normalizedKey.includes('test')) return 'Shield';
if (normalizedKey.includes('component') || normalizedKey.includes('architecture')) return 'Layers';
return 'FileText';
}
/**
* Formats a section key into a human-readable title
*/
export function formatSectionTitle(key: string): string {
return key
.replace(/_/g, ' ')
.split(' ')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}