summaryrefslogblamecommitdiff
path: root/frontend/src/components/document/DocumentLayout.tsx
blob: a555ad00626dd88db83a1c5bafd33ae3ef06f060 (plain) (tree)



























































































































































































































































































































                                                                                            
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>
  )
}