mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-04-03 11:13:08 +00:00
refactor(ui): migrate to shadcn/ui components and fix scroll issues
Migrate UI component library from custom implementations to shadcn/ui: - Add shadcn/ui primitives (Button, Card, Dialog, Input, etc.) - Replace custom styles with Tailwind CSS v4 theme configuration - Remove custom-theme.css in favor of globals.css with @theme directive Fix scroll overflow issues in multiple components: - ProjectSelector: "New Project" button no longer overlays project list - FolderBrowser: folder list now scrolls properly within modal - AgentCard: log modal content stays within bounds - ConversationHistory: conversation list scrolls correctly - KanbanColumn: feature cards scroll within fixed height - ScheduleModal: schedule form content scrolls properly Key technical changes: - Replace ScrollArea component with native overflow-y-auto divs - Add min-h-0 to flex containers to allow proper shrinking - Restructure dropdown layouts with flex-col for fixed footers New files: - ui/components.json (shadcn/ui configuration) - ui/src/components/ui/* (20 UI primitive components) - ui/src/lib/utils.ts (cn utility for class merging) - ui/tsconfig.app.json (app-specific TypeScript config) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,6 +3,9 @@ import { useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { AgentAvatar } from './AgentAvatar'
|
||||
import type { ActiveAgent, AgentLogEntry, AgentType } from '../lib/types'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
|
||||
interface AgentCardProps {
|
||||
agent: ActiveAgent
|
||||
@@ -31,22 +34,22 @@ function getStateText(state: ActiveAgent['state']): string {
|
||||
}
|
||||
}
|
||||
|
||||
// Get state color
|
||||
// Get state color class
|
||||
function getStateColor(state: ActiveAgent['state']): string {
|
||||
switch (state) {
|
||||
case 'success':
|
||||
return 'text-neo-done'
|
||||
return 'text-primary'
|
||||
case 'error':
|
||||
return 'text-neo-pending' // Yellow - just pivoting, not a real error
|
||||
return 'text-yellow-600'
|
||||
case 'struggling':
|
||||
return 'text-orange-500' // Orange - working hard, being persistent
|
||||
return 'text-orange-500'
|
||||
case 'working':
|
||||
case 'testing':
|
||||
return 'text-neo-progress'
|
||||
return 'text-primary'
|
||||
case 'thinking':
|
||||
return 'text-neo-pending'
|
||||
return 'text-yellow-600'
|
||||
default:
|
||||
return 'text-neo-text-secondary'
|
||||
return 'text-muted-foreground'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,14 +58,13 @@ function getAgentTypeBadge(agentType: AgentType): { label: string; className: st
|
||||
if (agentType === 'testing') {
|
||||
return {
|
||||
label: 'TEST',
|
||||
className: 'bg-purple-100 text-purple-700 border-purple-300',
|
||||
className: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300',
|
||||
icon: FlaskConical,
|
||||
}
|
||||
}
|
||||
// Default to coding
|
||||
return {
|
||||
label: 'CODE',
|
||||
className: 'bg-blue-100 text-blue-700 border-blue-300',
|
||||
className: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
icon: Code,
|
||||
}
|
||||
}
|
||||
@@ -74,75 +76,66 @@ export function AgentCard({ agent, onShowLogs }: AgentCardProps) {
|
||||
const TypeIcon = typeBadge.icon
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
neo-card p-3 min-w-[180px] max-w-[220px]
|
||||
${isActive ? 'animate-pulse-neo' : ''}
|
||||
transition-all duration-300
|
||||
`}
|
||||
>
|
||||
{/* Agent type badge */}
|
||||
<div className="flex justify-end mb-1">
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold
|
||||
uppercase tracking-wide rounded border
|
||||
${typeBadge.className}
|
||||
`}
|
||||
>
|
||||
<TypeIcon size={10} />
|
||||
{typeBadge.label}
|
||||
</span>
|
||||
</div>
|
||||
<Card className={`min-w-[180px] max-w-[220px] py-3 ${isActive ? 'animate-pulse' : ''}`}>
|
||||
<CardContent className="p-3 space-y-2">
|
||||
{/* Agent type badge */}
|
||||
<div className="flex justify-end">
|
||||
<Badge variant="outline" className={`text-[10px] ${typeBadge.className}`}>
|
||||
<TypeIcon size={10} />
|
||||
{typeBadge.label}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{/* Header with avatar and name */}
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AgentAvatar name={agent.agentName} state={agent.state} size="sm" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-display font-bold text-sm truncate">
|
||||
{agent.agentName}
|
||||
{/* Header with avatar and name */}
|
||||
<div className="flex items-center gap-2">
|
||||
<AgentAvatar name={agent.agentName} state={agent.state} size="sm" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-semibold text-sm truncate">
|
||||
{agent.agentName}
|
||||
</div>
|
||||
<div className={`text-xs ${getStateColor(agent.state)}`}>
|
||||
{getStateText(agent.state)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-xs ${getStateColor(agent.state)}`}>
|
||||
{getStateText(agent.state)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Log button */}
|
||||
{hasLogs && onShowLogs && (
|
||||
<button
|
||||
onClick={() => onShowLogs(agent.agentIndex)}
|
||||
className="p-1 hover:bg-neo-bg-secondary rounded transition-colors"
|
||||
title={`View logs (${agent.logs?.length || 0} entries)`}
|
||||
>
|
||||
<ScrollText size={14} className="text-neo-text-secondary" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Feature info */}
|
||||
<div className="mb-2">
|
||||
<div className="text-xs text-neo-text-secondary mb-0.5">
|
||||
Feature #{agent.featureId}
|
||||
</div>
|
||||
<div className="text-sm font-medium truncate" title={agent.featureName}>
|
||||
{agent.featureName}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Thought bubble */}
|
||||
{agent.thought && (
|
||||
<div className="relative mt-2 pt-2 border-t-2 border-neo-border/30">
|
||||
<div className="flex items-start gap-1.5">
|
||||
<MessageCircle size={14} className="text-neo-progress shrink-0 mt-0.5" />
|
||||
<p
|
||||
className="text-xs text-neo-text-secondary line-clamp-2 italic"
|
||||
title={agent.thought}
|
||||
{/* Log button */}
|
||||
{hasLogs && onShowLogs && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
onClick={() => onShowLogs(agent.agentIndex)}
|
||||
title={`View logs (${agent.logs?.length || 0} entries)`}
|
||||
>
|
||||
{agent.thought}
|
||||
</p>
|
||||
<ScrollText size={14} className="text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Feature info */}
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground mb-0.5">
|
||||
Feature #{agent.featureId}
|
||||
</div>
|
||||
<div className="text-sm font-medium truncate" title={agent.featureName}>
|
||||
{agent.featureName}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Thought bubble */}
|
||||
{agent.thought && (
|
||||
<div className="pt-2 border-t border-border/50">
|
||||
<div className="flex items-start gap-1.5">
|
||||
<MessageCircle size={14} className="text-primary shrink-0 mt-0.5" />
|
||||
<p
|
||||
className="text-xs text-muted-foreground line-clamp-2 italic"
|
||||
title={agent.thought}
|
||||
>
|
||||
{agent.thought}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -170,91 +163,76 @@ export function AgentLogModal({ agent, logs, onClose }: AgentLogModalProps) {
|
||||
const getLogColor = (type: AgentLogEntry['type']) => {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return 'text-neo-danger'
|
||||
return 'text-destructive'
|
||||
case 'state_change':
|
||||
return 'text-neo-progress'
|
||||
return 'text-primary'
|
||||
default:
|
||||
return 'text-neo-text'
|
||||
return 'text-foreground'
|
||||
}
|
||||
}
|
||||
|
||||
// Use portal to render modal at document body level (avoids overflow:hidden issues)
|
||||
return createPortal(
|
||||
<div
|
||||
className="fixed inset-0 flex items-center justify-center p-4 bg-black/50"
|
||||
style={{ zIndex: 9999 }}
|
||||
onClick={(e) => {
|
||||
// Close when clicking backdrop
|
||||
if (e.target === e.currentTarget) onClose()
|
||||
}}
|
||||
>
|
||||
<div className="neo-card w-full max-w-4xl max-h-[80vh] flex flex-col bg-neo-bg">
|
||||
<Card className="w-full max-w-4xl max-h-[80vh] flex flex-col py-0">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-4 border-b-3 border-neo-border">
|
||||
<div className="flex items-center justify-between p-4 border-b">
|
||||
<div className="flex items-center gap-3">
|
||||
<AgentAvatar name={agent.agentName} state={agent.state} size="sm" />
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="font-display font-bold text-lg">
|
||||
<h2 className="font-semibold text-lg">
|
||||
{agent.agentName} Logs
|
||||
</h2>
|
||||
<span
|
||||
className={`
|
||||
inline-flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-bold
|
||||
uppercase tracking-wide rounded border
|
||||
${typeBadge.className}
|
||||
`}
|
||||
>
|
||||
<Badge variant="outline" className={`text-[10px] ${typeBadge.className}`}>
|
||||
<TypeIcon size={10} />
|
||||
{typeBadge.label}
|
||||
</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-neo-text-secondary">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Feature #{agent.featureId}: {agent.featureName}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="neo-button neo-button-sm flex items-center gap-1"
|
||||
title="Copy all logs"
|
||||
>
|
||||
<Button variant="outline" size="sm" onClick={handleCopy}>
|
||||
{copied ? <Check size={14} /> : <Copy size={14} />}
|
||||
{copied ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-neo-bg-secondary rounded transition-colors"
|
||||
>
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon-sm" onClick={onClose}>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Log content */}
|
||||
<div className="flex-1 overflow-auto p-4 bg-neo-bg-secondary font-mono text-xs">
|
||||
{logs.length === 0 ? (
|
||||
<p className="text-neo-text-secondary italic">No logs available</p>
|
||||
) : (
|
||||
<div className="space-y-1">
|
||||
{logs.map((log, idx) => (
|
||||
<div className="flex-1 min-h-0 overflow-y-auto p-4 bg-muted/50">
|
||||
<div className="font-mono text-xs space-y-1">
|
||||
{logs.length === 0 ? (
|
||||
<p className="text-muted-foreground italic">No logs available</p>
|
||||
) : (
|
||||
logs.map((log, idx) => (
|
||||
<div key={idx} className={`${getLogColor(log.type)} whitespace-pre-wrap break-all`}>
|
||||
<span className="text-neo-muted">
|
||||
<span className="text-muted-foreground">
|
||||
[{new Date(log.timestamp).toLocaleTimeString()}]
|
||||
</span>{' '}
|
||||
{log.line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-3 border-t-2 border-neo-border/30 text-xs text-neo-text-secondary">
|
||||
<div className="p-3 border-t text-xs text-muted-foreground">
|
||||
{logs.length} log entries
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>,
|
||||
document.body
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user