import { useEffect, useState, useCallback, useRef } from 'react'
import { useParams, useNavigate, Link } from 'react-router-dom'
import { DirectiveFileTree } from './DirectiveFileTree'
import DocumentEditor from './DocumentEditor'
import { ToastProvider, useToast } from './Toast'
import {
type DirectiveWithSteps,
type DirectiveStep,
getDirective,
getDirectiveSteps,
updateGoal,
updateDirective,
cleanupDirective,
createPr,
pickUpOrders,
pauseDirective,
startDirective,
} from '../../services/directiveApi'
import './DocumentLayout.css'
function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
active: '#4caf50',
running: '#4caf50',
idle: '#ffc107',
paused: '#ffc107',
draft: '#9e9e9e',
pending: '#9e9e9e',
archived: '#f44336',
failed: '#f44336',
}
const color = colors[status.toLowerCase()] || '#9e9e9e'
return (
<span className="doc-status-badge" style={{ backgroundColor: color }}>
{status}
</span>
)
}
function DocumentLayoutInner() {
const { id: urlDirectiveId } = useParams<{ id: string }>()
const navigate = useNavigate()
const { addToast } = useToast()
const [selectedId, setSelectedId] = useState<string | null>(urlDirectiveId || null)
const [directive, setDirective] = useState<DirectiveWithSteps | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [sidebarWidth, setSidebarWidth] = useState(250)
const resizingRef = useRef(false)
const startXRef = useRef(0)
const startWidthRef = useRef(250)
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null)
// Sync URL param on mount
useEffect(() => {
if (urlDirectiveId && urlDirectiveId !== selectedId) {
setSelectedId(urlDirectiveId)
}
}, [urlDirectiveId])
// Handle directive selection - update URL
const handleSelectDirective = useCallback((id: string) => {
setSelectedId(id)
navigate(`/directives/${id}`, { replace: true })
}, [navigate])
// Load directive when selected
useEffect(() => {
if (!selectedId) {
setDirective(null)
return
}
let cancelled = false
async function load() {
try {
setLoading(true)
setError(null)
const data = await getDirective(selectedId!)
if (!cancelled) setDirective(data)
} catch (err) {
if (!cancelled) {
const msg = err instanceof Error ? err.message : 'Failed to load directive'
setError(msg)
addToast(msg, 'error')
}
} finally {
if (!cancelled) setLoading(false)
}
}
load()
return () => { cancelled = true }
}, [selectedId, addToast])
// Step polling (after goal update triggers supervisor)
const startStepPolling = useCallback(() => {
if (pollRef.current) clearInterval(pollRef.current)
pollRef.current = setInterval(async () => {
if (!selectedId) return
try {
const data = await getDirective(selectedId)
setDirective(data)
} catch {
// Silently fail for polling
}
}, 3000)
// Stop after 60 seconds
setTimeout(() => {
if (pollRef.current) {
clearInterval(pollRef.current)
pollRef.current = null
}
}, 60000)
}, [selectedId])
useEffect(() => {
return () => {
if (pollRef.current) clearInterval(pollRef.current)
}
}, [])
// Auto-save goal changes
const handleGoalChange = useCallback(async (newGoal: string) => {
if (!selectedId) return
try {
const updated = await updateGoal(selectedId, newGoal)
setDirective(updated)
addToast('Goal saved', 'success')
startStepPolling()
} catch (err) {
addToast(`Failed to save goal: ${(err as Error).message}`, 'error')
}
}, [selectedId, addToast, startStepPolling])
const handleTitleChange = useCallback(async (newTitle: string) => {
if (!selectedId || !directive) return
try {
const updated = await updateDirective(selectedId, {
title: newTitle,
version: directive.version,
})
setDirective(updated)
} catch (err) {
addToast(`Failed to update title: ${(err as Error).message}`, 'error')
}
}, [selectedId, directive, addToast])
const handleCleanup = useCallback(async () => {
if (!selectedId) return
try {
await cleanupDirective(selectedId)
addToast('Cleanup task spawned', 'success')
startStepPolling()
} catch (err) {
addToast(`Cleanup failed: ${(err as Error).message}`, 'error')
}
}, [selectedId, addToast, startStepPolling])
const handleCreatePr = useCallback(async () => {
if (!selectedId) return
try {
await createPr(selectedId)
addToast('PR update triggered', 'success')
} catch (err) {
addToast(`PR update failed: ${(err as Error).message}`, 'error')
}
}, [selectedId, addToast])
const handlePlanOrders = useCallback(async () => {
if (!selectedId) return
try {
await pickUpOrders(selectedId)
addToast('Planning orders...', 'info')
startStepPolling()
} catch (err) {
addToast(`Plan orders failed: ${(err as Error).message}`, 'error')
}
}, [selectedId, addToast, startStepPolling])
const handleTogglePause = useCallback(async () => {
if (!selectedId || !directive) return
try {
if (directive.status === 'paused') {
const result = await startDirective(selectedId)
setDirective(result)
addToast('Directive resumed', 'success')
} else {
const updated = await pauseDirective(selectedId)
setDirective(updated)
addToast('Directive paused', 'info')
}
} catch (err) {
addToast(`Failed to toggle pause: ${(err as Error).message}`, 'error')
}
}, [selectedId, directive, addToast])
// Sidebar resize handlers
const handleMouseDown = useCallback((e: React.MouseEvent) => {
resizingRef.current = true
startXRef.current = e.clientX
startWidthRef.current = sidebarWidth
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
const handleMouseMove = (e: MouseEvent) => {
if (!resizingRef.current) return
const diff = e.clientX - startXRef.current
const newWidth = Math.max(180, Math.min(500, startWidthRef.current + diff))
setSidebarWidth(newWidth)
}
const handleMouseUp = () => {
resizingRef.current = false
document.body.style.cursor = ''
document.body.style.userSelect = ''
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}, [sidebarWidth])
const handleNewDirective = useCallback(() => {
// Placeholder - will be implemented with full directive creation flow
console.log('New directive requested')
}, [])
return (
<div className="document-layout">
{/* Sidebar */}
<div className="document-sidebar" style={{ width: sidebarWidth }}>
<div className="document-sidebar-back">
<Link to="/" className="document-back-link">
{'\u2190'} Back to Main
</Link>
</div>
<DirectiveFileTree
selectedDirectiveId={selectedId}
onSelectDirective={handleSelectDirective}
onNewDirective={handleNewDirective}
/>
</div>
{/* Resize handle */}
<div className="document-resize-handle" onMouseDown={handleMouseDown} />
{/* Main content */}
<div className="document-main">
{directive && (
<div className="document-topbar">
<div className="document-topbar-left">
<h1 className="document-topbar-title">{directive.title || 'Untitled'}</h1>
<StatusBadge status={directive.status} />
</div>
<div className="document-topbar-right">
<button className="document-topbar-gear" title="Settings">
{'\u2699'}
</button>
</div>
</div>
)}
<div className="document-content">
{loading && (
<div className="document-placeholder">
<p>Loading directive...</p>
</div>
)}
{error && (
<div className="document-placeholder">
<p className="document-error">Error: {error}</p>
</div>
)}
{!loading && !error && !directive && (
<div className="document-placeholder">
<div className="document-placeholder-icon">{'\u{1F4DD}'}</div>
<h2>No directive selected</h2>
<p>Select a directive from the sidebar or create a new one to get started.</p>
</div>
)}
{!loading && !error && directive && (
<DocumentEditor
directiveId={directive.id}
title={directive.title || 'Untitled'}
goal={directive.goal || ''}
status={directive.status}
prBranch={directive.prBranch || directive.pr_branch}
onGoalChange={handleGoalChange}
onTitleChange={handleTitleChange}
onCleanup={handleCleanup}
onCreatePr={handleCreatePr}
onPlanOrders={handlePlanOrders}
onTogglePause={handleTogglePause}
/>
)}
</div>
</div>
</div>
)
}
// Wrapper that provides toast context
export default function DocumentLayout() {
return (
<ToastProvider>
<DocumentLayoutInner />
</ToastProvider>
)
}