| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Workflows — NSFWStudio</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/lucide@latest"></script> |
| <script src="https://unpkg.com/react@18/umd/react.development.js"></script> |
| <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> |
| <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> |
| <link rel="preconnect" href="https://fonts.googleapis.com"> |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,300..800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet"> |
| <script> |
| tailwind.config = { theme: { extend: { fontFamily: { sans: ['Inter','system-ui','sans-serif'], mono: ['JetBrains Mono','monospace'] }, colors: { bg: 'oklch(0.07 0.006 285)', surface: 'oklch(0.12 0.02 285)', 'surface-2': 'oklch(0.16 0.025 285)', 'surface-3': 'oklch(0.20 0.028 285)', fg: 'oklch(0.97 0.005 285)', muted: 'oklch(0.55 0.02 285)', primary: 'oklch(0.70 0.28 350)', 'primary-foreground': 'oklch(0.98 0.005 285)', secondary: 'oklch(0.75 0.15 215)', accent: 'oklch(0.80 0.18 85)', border: 'oklch(0.22 0.02 285)', ring: 'oklch(0.70 0.28 350)', destructive: 'oklch(0.55 0.22 25)', success: 'oklch(0.65 0.2 145)' } } } } |
| </script> |
| <style> |
| body { background-color: oklch(0.07 0.006 285); color: oklch(0.97 0.005 285); font-family: 'Inter', system-ui, sans-serif; -webkit-font-smoothing: antialiased; } |
| .glass { background: oklch(0.14 0.015 285 / 0.6); backdrop-filter: blur(24px) saturate(1.2); -webkit-backdrop-filter: blur(24px) saturate(1.2); border: 1px solid oklch(0.25 0.02 285 / 0.5); } |
| ::selection { background: oklch(0.70 0.28 350 / 0.3); } |
| body::after { content:''; position:fixed; inset:0; pointer-events:none; z-index:9999; opacity:0.025; background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); background-repeat:repeat; background-size:256px 256px; } |
| .no-scrollbar::-webkit-scrollbar { display: none; } .no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; } |
| </style> |
| </head> |
| <body class="antialiased"> |
| <div id="root"></div> |
| <script type="text/babel"> |
| const { useState } = React; |
| const Icon = ({ name, size = 20, className = "", strokeWidth = 2 }) => { |
| const ref = React.useRef(null); |
| React.useEffect(() => { if (ref.current) { const existing = ref.current.querySelector('svg'); if (existing) existing.remove(); const svg = lucide.createElement(lucide.icons[name]); if (svg) { svg.setAttribute('width',size); svg.setAttribute('height',size); svg.setAttribute('stroke-width',strokeWidth); ref.current.appendChild(svg); } } }, [name, size, className, strokeWidth]); |
| return <span ref={ref} className={`inline-flex items-center justify-center ${className}`} style={{ width: size, height: size }} />; |
| }; |
| |
| const navItems = [ |
| { name: 'Dashboard', icon: 'layout-dashboard', href: 'dashboard.html' }, |
| { name: 'Generator', icon: 'sparkles', href: 'generator.html' }, |
| { name: 'Playground', icon: 'code', href: 'playground.html' }, |
| { name: 'Models', icon: 'box', href: 'models.html' }, |
| { name: 'Workflows', icon: 'workflow', href: 'workflows.html', active: true }, |
| { name: 'Marketplace', icon: 'shopping-bag', href: 'marketplace.html' }, |
| { name: 'API Keys', icon: 'key', href: 'api-keys.html' }, |
| { divider: true }, |
| { name: 'Profile', icon: 'user', href: 'profile.html' }, |
| { name: 'Settings', icon: 'settings', href: 'settings.html' }, |
| ]; |
| |
| const Sidebar = ({ isOpen, setIsOpen }) => ( |
| <> |
| <div className={`fixed inset-0 z-40 lg:hidden transition-all ${isOpen ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}> |
| <div className="absolute inset-0 bg-black/60 backdrop-blur-sm" onClick={() => setIsOpen(false)} /> |
| </div> |
| <aside className={`fixed top-0 left-0 bottom-0 z-50 w-64 bg-surface border-r border-border flex flex-col transition-transform duration-300 lg:translate-x-0 ${isOpen ? 'translate-x-0' : '-translate-x-full'}`}> |
| <div className="p-4 flex items-center gap-2.5 border-b border-border"><div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center"><Icon name="sparkles" size={16} className="text-primary-foreground" /></div><span className="text-lg font-bold tracking-tight">NSFW<span className="text-muted">Studio</span></span></div> |
| <nav className="flex-1 p-3 space-y-1 overflow-y-auto no-scrollbar">{navItems.map((item, i) => item.divider ? <div key={i} className="my-3 border-t border-border" /> : <a key={i} href={item.href} className={`flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm font-medium transition-colors ${item.active ? 'bg-primary/10 text-primary border border-primary/20' : 'text-muted hover:text-fg hover:bg-surface-2 border border-transparent'}`}><Icon name={item.icon} size={18} />{item.name}</a>)}</nav> |
| <div className="p-4 border-t border-border"><a href="documentation.html" className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-muted hover:text-fg hover:bg-surface-2 transition-colors"><Icon name="book-open" size={18} />Documentation</a><a href="api-reference.html" className="flex items-center gap-3 px-3 py-2.5 rounded-xl text-sm text-muted hover:text-fg hover:bg-surface-2 transition-colors"><Icon name="file-code" size={18} />API Reference</a></div> |
| </aside> |
| </> |
| ); |
| |
| const App = () => { |
| const [sidebarOpen, setSidebarOpen] = useState(false); |
| const [filter, setFilter] = useState('all'); |
| |
| const workflows = [ |
| { name: 'Auto Tag & Categorize', desc: 'Automatically tag and categorize generated content using AI', steps: 4, runs: '12.4K', lastRun: '2 min ago', status: 'active', trigger: 'On Generation', icon: 'tag' }, |
| { name: 'Batch Upscale Pipeline', desc: 'Upscale images in batch with quality enhancement', steps: 3, runs: '8.2K', lastRun: '1 hour ago', status: 'active', trigger: 'Manual', icon: 'maximize' }, |
| { name: 'Content Moderation', desc: 'Auto-moderate content before publishing', steps: 5, runs: '45.1K', lastRun: '5 min ago', status: 'active', trigger: 'On Upload', icon: 'shield' }, |
| { name: 'Social Media Scheduler', desc: 'Generate and schedule social media content', steps: 6, runs: '3.7K', lastRun: '3 days ago', status: 'paused', trigger: 'Scheduled', icon: 'calendar' }, |
| { name: 'Model Fine-Tuner', desc: 'Automated fine-tuning workflow with dataset preparation', steps: 7, runs: '1.2K', lastRun: '1 week ago', status: 'draft', trigger: 'Manual', icon: 'cpu' }, |
| { name: 'Video Frame Extractor', desc: 'Extract and enhance key frames from video generations', steps: 3, runs: '6.8K', lastRun: '30 min ago', status: 'active', trigger: 'On Video Gen', icon: 'film' }, |
| ]; |
| |
| const filtered = filter === 'all' ? workflows : workflows.filter(w => w.status === filter); |
| const statusColors = { active: 'bg-success/10 text-success border-success/20', paused: 'bg-accent/10 text-accent border-accent/20', draft: 'bg-muted/10 text-muted border-border' }; |
| |
| return ( |
| <div className="min-h-screen bg-bg"> |
| <Sidebar isOpen={sidebarOpen} setIsOpen={setSidebarOpen} /> |
| <div className="lg:ml-64"> |
| <header className="sticky top-0 z-30 glass border-b border-border"> |
| <div className="flex items-center justify-between px-4 sm:px-6 h-16"> |
| <div className="flex items-center gap-4"> |
| <button onClick={() => setSidebarOpen(true)} className="lg:hidden p-2 rounded-lg hover:bg-surface-2 text-muted"><Icon name="menu" size={20} /></button> |
| <h1 className="text-xl font-bold">Workflows</h1> |
| </div> |
| <div className="flex items-center gap-3"> |
| <a href="#" className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-semibold hover:brightness-110 transition-all flex items-center gap-2"><Icon name="plus" size={16} />Create Workflow</a> |
| <a href="profile.html" className="w-9 h-9 rounded-lg overflow-hidden border border-border"><img src="http://static.photos/people/100x100/10" alt="" className="w-full h-full object-cover" /></a> |
| </div> |
| </div> |
| </header> |
| |
| <main className="p-4 sm:p-6 lg:p-8"> |
| {/* Stats */} |
| <div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-8"> |
| {[ |
| { label: 'Total Workflows', value: '6', icon: 'workflow', color: 'text-primary', bg: 'bg-primary/10' }, |
| { label: 'Active', value: '4', icon: 'play-circle', color: 'text-success', bg: 'bg-success/10' }, |
| { label: 'Total Runs', value: '77.4K', icon: 'activity', color: 'text-secondary', bg: 'bg-secondary/10' }, |
| { label: 'Avg Duration', value: '2.4s', icon: 'clock', color: 'text-accent', bg: 'bg-accent/10' }, |
| ].map((s, i) => ( |
| <div key={i} className="rounded-2xl p-5 bg-surface border border-border"> |
| <div className="flex items-center gap-3 mb-3"> |
| <div className={`w-9 h-9 rounded-lg ${s.bg} flex items-center justify-center`}><Icon name={s.icon} size={16} className={s.color} /></div> |
| <span className="text-xs text-muted">{s.label}</span> |
| </div> |
| <div className="text-2xl font-bold text-fg">{s.value}</div> |
| </div> |
| ))} |
| </div> |
| |
| {/* Filters */} |
| <div className="flex items-center gap-2 mb-6"> |
| {['all', 'active', 'paused', 'draft'].map(f => ( |
| <button key={f} onClick={() => setFilter(f)} className={`px-4 py-2 rounded-lg text-sm font-medium capitalize transition-colors ${filter === f ? 'bg-primary text-primary-foreground' : 'text-muted hover:text-fg hover:bg-surface-2'}`}>{f}</button> |
| ))} |
| </div> |
| |
| {/* Workflow list */} |
| <div className="space-y-4"> |
| {filtered.map((w, i) => ( |
| <div key={i} className="group rounded-2xl p-5 bg-surface border border-border hover:border-surface-3 transition-all"> |
| <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4"> |
| <div className="flex items-start gap-4"> |
| <div className="w-12 h-12 rounded-xl bg-primary/10 flex items-center justify-center shrink-0"><Icon name={w.icon} size={22} className="text-primary" /></div> |
| <div> |
| <div className="flex items-center gap-2 mb-1"> |
| <h3 className="font-semibold text-fg">{w.name}</h3> |
| <span className={`px-2.5 py-0.5 rounded-full text-xs font-semibold border ${statusColors[w.status]}`}>{w.status}</span> |
| </div> |
| <p className="text-sm text-muted mb-2">{w.desc}</p> |
| <div className="flex items-center gap-4 text-xs text-muted"> |
| <span className="flex items-center gap-1"><Icon name="git-branch" size={12} />{w.steps} steps</span> |
| <span className="flex items-center gap-1"><Icon name="play" size={12} />{w.runs} runs</span> |
| <span className="flex items-center gap-1"><Icon name="zap" size={12} />{w.trigger}</span> |
| </div> |
| </div> |
| </div> |
| <div className="flex items-center gap-2"> |
| <button className="px-3 py-2 rounded-lg border border-border text-sm text-muted hover:text-fg hover:bg-surface-2 transition-colors flex items-center gap-1.5"><Icon name="play" size={14} />Run</button> |
| <button className="px-3 py-2 rounded-lg border border-border text-sm text-muted hover:text-fg hover:bg-surface-2 transition-colors flex items-center gap-1.5"><Icon name="pencil" size={14} />Edit</button> |
| <button className="p-2 rounded-lg border border-border text-muted hover:text-fg hover:bg-surface-2 transition-colors"><Icon name="more-vertical" size={16} /></button> |
| </div> |
| </div> |
| |
| {/* Mini flow visualization */} |
| <div className="mt-4 flex items-center gap-2 overflow-x-auto no-scrollbar pb-1"> |
| {Array.from({length: w.steps}).map((_, j) => ( |
| <React.Fragment key={j}> |
| <div className="flex items-center gap-2 px-3 py-2 rounded-lg bg-surface-2 border border-border shrink-0"> |
| <div className={`w-2 h-2 rounded-full ${j < 3 ? 'bg-success' : j === 3 ? 'bg-primary' : 'bg-muted'}`} /> |
| <span className="text-xs text-muted">Step {j + 1}</span> |
| </div> |
| {j < w.steps - 1 && <Icon name="chevron-right" size={14} className="text-muted shrink-0" />} |
| </React.Fragment> |
| ))} |
| </div> |
| </div> |
| ))} |
| </div> |
| </main> |
| </div> |
| </div> |
| ); |
| }; |
| ReactDOM.createRoot(document.getElementById('root')).render(<App />); |
| </script> |
| </body> |
| </html> |