← back to Letsbegin

components/RalphConverterStep.tsx

473 lines

'use client'

import { useState } from 'react'

interface UserStory {
  id: string
  title: string
  description: string
  acceptanceCriteria: string[]
  priority: number
  passes: boolean
  notes: string
}

interface RalphData {
  project: string
  branchName: string
  description: string
  userStories: UserStory[]
}

interface Props {
  prdMarkdown: string
  projectName: string
  onConvert: (data: RalphData) => void
  ralphData: RalphData | null
  validationResults: Record<string, boolean>
  setValidationResults: (results: Record<string, boolean>) => void
  onBack: () => void
}

const validationChecks = [
  { id: 'storySize', label: 'Each story is small enough (2-3 sentence description)' },
  { id: 'ordering', label: 'Stories ordered by dependency (schema → backend → UI)' },
  { id: 'typecheck', label: 'Every story has "Typecheck passes" criterion' },
  { id: 'browser', label: 'UI stories have "Verify in browser" criterion' },
  { id: 'noDeps', label: 'No story depends on a later story' },
  { id: 'verifiable', label: 'Acceptance criteria are verifiable (not vague)' },
]

export default function RalphConverterStep({
  prdMarkdown,
  projectName,
  onConvert,
  ralphData,
  validationResults,
  setValidationResults,
  onBack
}: Props) {
  const [isConverting, setIsConverting] = useState(false)
  const [editingStory, setEditingStory] = useState<string | null>(null)
  const [localRalphData, setLocalRalphData] = useState<RalphData | null>(ralphData)
  const [validationIssues, setValidationIssues] = useState<string[]>([])
  const [archivedTo, setArchivedTo] = useState<string | null>(null)
  const [isFixing, setIsFixing] = useState(false)
  const [fixedMessage, setFixedMessage] = useState<string | null>(null)

  // Determine story type based on title/description
  function getStoryType(story: UserStory): 'schema' | 'backend' | 'ui' | 'other' {
    const text = `${story.title} ${story.description}`.toLowerCase()
    if (text.includes('schema') || text.includes('database') || text.includes('model') || text.includes('migration')) {
      return 'schema'
    }
    if (text.includes('api') || text.includes('endpoint') || text.includes('backend') || text.includes('service') || text.includes('server')) {
      return 'backend'
    }
    if (text.includes('ui') || text.includes('component') || text.includes('page') || text.includes('form') || text.includes('button') || text.includes('display') || text.includes('view') || text.includes('dashboard') || text.includes('interface')) {
      return 'ui'
    }
    return 'other'
  }

  // Auto-fix all validation issues
  function handleFixIssues() {
    if (!localRalphData) return
    setIsFixing(true)
    setFixedMessage(null)

    try {
      let fixedStories = localRalphData.userStories.map(story => ({ ...story, acceptanceCriteria: [...story.acceptanceCriteria] }))
      const fixedIssues: string[] = []

      // Fix 1: Add "Verify in browser" to UI stories that don't have it
      fixedStories = fixedStories.map(story => {
        const storyType = getStoryType(story)
        if (storyType === 'ui') {
          const hasBrowserCriterion = story.acceptanceCriteria.some(c =>
            c.toLowerCase().includes('verify in browser') ||
            c.toLowerCase().includes('browser verification') ||
            c.toLowerCase().includes('visually verify')
          )
          if (!hasBrowserCriterion) {
            fixedIssues.push(`Added "Verify in browser" to ${story.id}`)
            return {
              ...story,
              acceptanceCriteria: [...story.acceptanceCriteria, 'Verify in browser that the UI displays correctly']
            }
          }
        }
        return story
      })

      // Fix 2: Add "Typecheck passes" to stories that don't have it
      fixedStories = fixedStories.map(story => {
        const hasTypecheck = story.acceptanceCriteria.some(c =>
          c.toLowerCase().includes('typecheck') ||
          c.toLowerCase().includes('type check') ||
          c.toLowerCase().includes('typescript')
        )
        if (!hasTypecheck) {
          fixedIssues.push(`Added "Typecheck passes" to ${story.id}`)
          return {
            ...story,
            acceptanceCriteria: [...story.acceptanceCriteria, 'Typecheck passes']
          }
        }
        return story
      })

      // Fix 3: Reorder stories by type (schema → backend → other → ui)
      const typeOrder = { schema: 0, backend: 1, other: 2, ui: 3 }
      const originalOrder = fixedStories.map(s => s.id).join(',')

      fixedStories.sort((a, b) => {
        const typeA = getStoryType(a)
        const typeB = getStoryType(b)
        return typeOrder[typeA] - typeOrder[typeB]
      })

      const newOrder = fixedStories.map(s => s.id).join(',')
      if (originalOrder !== newOrder) {
        fixedIssues.push('Reordered stories: schema → backend → other → UI')
      }

      // Update priorities after reordering
      fixedStories = fixedStories.map((story, idx) => ({
        ...story,
        priority: idx + 1
      }))

      // Create the new ralph data
      const newRalphData = { ...localRalphData, userStories: fixedStories }

      // Update state
      setLocalRalphData(newRalphData)

      // Update validation results - mark all fixable ones as true
      const newValidationResults = {
        ...validationResults,
        browser: true,
        typecheck: true,
        ordering: true,
        noDeps: true
      }
      setValidationResults(newValidationResults)

      // Clear issues that were fixed - use more inclusive pattern matching
      const remainingIssues = validationIssues.filter(issue => {
        const lowerIssue = issue.toLowerCase()
        // Remove issues we just fixed
        if (lowerIssue.includes('verify in browser') || lowerIssue.includes('browser')) return false
        if (lowerIssue.includes('story ordering') || lowerIssue.includes('should come before')) return false
        if (lowerIssue.includes('typecheck')) return false
        return true
      })
      setValidationIssues(remainingIssues)

      // Show what was fixed
      if (fixedIssues.length > 0) {
        setFixedMessage(`Fixed ${fixedIssues.length} issue(s): ${fixedIssues.join(', ')}`)
        console.log('Fixed issues:', fixedIssues)
      } else {
        setFixedMessage('No auto-fixable issues found')
      }

    } catch (err) {
      console.error('Error fixing issues:', err)
      setFixedMessage('Error fixing issues: ' + (err as Error).message)
    } finally {
      setIsFixing(false)
    }
  }

  async function handleConvert() {
    setIsConverting(true)
    setValidationIssues([])
    try {
      const res = await fetch('/api/ralph/convert', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ prdMarkdown, projectName })
      })
      const data = await res.json()
      if (data.ralphData) {
        setLocalRalphData(data.ralphData)

        // Use validation results from API
        if (data.validation) {
          setValidationIssues(data.validation.issues || [])

          // Map validation issues to checkbox states
          const results: Record<string, boolean> = {}
          const issues = data.validation.issues || []

          // Check each validation type based on issue patterns
          results['storySize'] = !issues.some((i: string) => i.includes('Description too long'))
          results['typecheck'] = !issues.some((i: string) => i.includes('Typecheck passes'))
          results['browser'] = !issues.some((i: string) => i.includes('Verify in browser'))
          results['verifiable'] = !issues.some((i: string) => i.includes('Vague criterion'))
          results['ordering'] = !issues.some((i: string) => i.includes('Story ordering'))
          results['noDeps'] = !issues.some((i: string) => i.includes('depends on a later'))

          setValidationResults(results)
        }

        if (data.archivedTo) {
          setArchivedTo(data.archivedTo)
        }
      }
    } catch (err) {
      console.error('Failed to convert:', err)
    } finally {
      setIsConverting(false)
    }
  }

  function handleSaveAndNext() {
    if (localRalphData) {
      onConvert(localRalphData)
    }
  }

  function moveStory(index: number, direction: 'up' | 'down') {
    if (!localRalphData) return
    const newStories = [...localRalphData.userStories]
    const targetIndex = direction === 'up' ? index - 1 : index + 1
    if (targetIndex < 0 || targetIndex >= newStories.length) return

    [newStories[index], newStories[targetIndex]] = [newStories[targetIndex], newStories[index]]
    // Update priorities
    newStories.forEach((story, i) => { story.priority = i + 1 })
    setLocalRalphData({ ...localRalphData, userStories: newStories })
  }

  function deleteStory(id: string) {
    if (!localRalphData) return
    const newStories = localRalphData.userStories.filter(s => s.id !== id)
    newStories.forEach((story, i) => { story.priority = i + 1 })
    setLocalRalphData({ ...localRalphData, userStories: newStories })
  }

  const allValid = Object.values(validationResults).every(v => v)

  return (
    <div className="space-y-6">
      {/* Side by Side View */}
      <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
        {/* PRD Input */}
        <div className="card">
          <div className="p-4 border-b border-[var(--border)] flex items-center gap-2">
            <span>📄</span>
            <h3 className="font-medium">PRD (Input)</h3>
          </div>
          <div className="p-4 max-h-80 overflow-y-auto">
            <pre className="text-sm whitespace-pre-wrap">{prdMarkdown || 'No PRD generated yet'}</pre>
          </div>
        </div>

        {/* prd.json Output */}
        <div className="card">
          <div className="p-4 border-b border-[var(--border)] flex items-center justify-between">
            <div className="flex items-center gap-2">
              <span>📦</span>
              <h3 className="font-medium">prd.json (Output)</h3>
            </div>
            {!localRalphData && (
              <button
                onClick={handleConvert}
                className="btn-primary text-sm"
                disabled={isConverting || !prdMarkdown}
              >
                {isConverting ? 'Converting...' : 'Convert →'}
              </button>
            )}
          </div>
          <div className="p-4 max-h-80 overflow-y-auto">
            {localRalphData ? (
              <pre className="code-block text-xs">
                {JSON.stringify(localRalphData, null, 2)}
              </pre>
            ) : (
              <p className="text-[var(--foreground-tertiary)] text-sm">
                Click "Convert" to generate prd.json
              </p>
            )}
          </div>
        </div>
      </div>

      {/* Archive Notice */}
      {archivedTo && (
        <div className="p-3 bg-blue-500/10 border border-blue-500/30 rounded-lg text-blue-400 text-sm">
          Previous prd.json archived to: {archivedTo}
        </div>
      )}

      {/* Validation Issues */}
      {validationIssues.length > 0 && (
        <div className="card p-4 border-[var(--warning)]">
          <h3 className="font-medium mb-3 flex items-center gap-2 text-[var(--warning)]">
            <span>⚠️</span>
            Validation Issues ({validationIssues.length})
          </h3>
          <ul className="space-y-2 text-sm">
            {validationIssues.map((issue, idx) => (
              <li key={idx} className="flex items-start gap-2 text-[var(--foreground-secondary)]">
                <span className="text-[var(--warning)]">•</span>
                {issue}
              </li>
            ))}
          </ul>
          <div className="mt-4 flex items-center gap-3">
            <button
              onClick={handleFixIssues}
              className="btn-primary text-sm"
              disabled={isFixing}
            >
              {isFixing ? 'Fixing...' : '🔧 Fix Validation Issues'}
            </button>
            <span className="text-xs text-[var(--foreground-tertiary)]">
              or manually check the boxes below to proceed
            </span>
          </div>
          {fixedMessage && (
            <div className="mt-3 p-2 bg-green-500/20 border border-green-500/30 rounded text-green-400 text-sm">
              ✅ {fixedMessage}
            </div>
          )}
        </div>
      )}

      {/* Validation Checklist */}
      {localRalphData && (
        <div className="card p-4">
          <h3 className="font-medium mb-4 flex items-center gap-2">
            <span>✓</span>
            Validation Checklist
          </h3>
          <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
            {validationChecks.map((check) => (
              <div
                key={check.id}
                className={`checkbox-option ${validationResults[check.id] ? 'checked' : ''}`}
                onClick={() => setValidationResults({
                  ...validationResults,
                  [check.id]: !validationResults[check.id]
                })}
              >
                <div className={`checkbox-box ${validationResults[check.id] ? 'checked' : ''}`}>
                  {validationResults[check.id] && (
                    <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                      <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
                    </svg>
                  )}
                </div>
                <span className="text-sm">{check.label}</span>
              </div>
            ))}
          </div>
        </div>
      )}

      {/* Story Editor */}
      {localRalphData && (
        <div className="card p-4">
          <h3 className="font-medium mb-4 flex items-center gap-2">
            <span>📝</span>
            Edit Stories (Drag to Reorder)
          </h3>
          <div className="space-y-2">
            {localRalphData.userStories.map((story, index) => (
              <div
                key={story.id}
                className="flex items-center gap-3 p-3 bg-[var(--surface-alt)] rounded-lg border border-[var(--border)]"
              >
                {/* Drag Handle */}
                <span className="text-[var(--foreground-tertiary)] cursor-move">≡</span>

                {/* Story Info */}
                <div className="flex-1">
                  <div className="font-medium text-sm">{story.id}: {story.title}</div>
                  <div className="text-xs text-[var(--foreground-tertiary)]">
                    Priority: {story.priority} | Criteria: {story.acceptanceCriteria.length}
                  </div>
                </div>

                {/* Actions */}
                <div className="flex items-center gap-2">
                  <button
                    onClick={() => moveStory(index, 'up')}
                    disabled={index === 0}
                    className="p-1 hover:bg-[var(--surface-elevated)] rounded disabled:opacity-30"
                  >
                    ↑
                  </button>
                  <button
                    onClick={() => moveStory(index, 'down')}
                    disabled={index === localRalphData.userStories.length - 1}
                    className="p-1 hover:bg-[var(--surface-elevated)] rounded disabled:opacity-30"
                  >
                    ↓
                  </button>
                  <button
                    onClick={() => setEditingStory(story.id)}
                    className="p-1 hover:bg-[var(--surface-elevated)] rounded text-[var(--primary)]"
                  >
                    ✏️
                  </button>
                  <button
                    onClick={() => deleteStory(story.id)}
                    className="p-1 hover:bg-[var(--surface-elevated)] rounded text-[var(--error)]"
                  >
                    🗑️
                  </button>
                </div>
              </div>
            ))}
          </div>
          <div className="mt-4 flex items-center gap-3">
            <button className="btn-secondary text-sm">+ Add Story</button>
            <button className="btn-secondary text-sm">🔀 Split Selected</button>
          </div>
        </div>
      )}

      {/* Actions */}
      <div className="flex items-center justify-between p-4 card">
        <button onClick={onBack} className="btn-secondary">
          ← Back to PRD
        </button>
        <div className="flex items-center gap-3">
          <button
            onClick={() => {
              if (localRalphData) {
                navigator.clipboard.writeText(JSON.stringify(localRalphData, null, 2))
                alert('prd.json copied to clipboard!')
              }
            }}
            className="btn-secondary"
            disabled={!localRalphData}
          >
            Copy prd.json
          </button>
          <button
            onClick={() => {
              if (!localRalphData) return
              if (!allValid) {
                const proceed = confirm('Some validation checks are not passing. Proceed anyway?')
                if (!proceed) return
              }
              handleSaveAndNext()
            }}
            className="btn-primary"
            disabled={!localRalphData}
          >
            Next: Ralphy Boy →
          </button>
        </div>
      </div>
    </div>
  )
}