Generated by Spark: Docker Build Debugger should just be a option on burger menu

This commit is contained in:
2026-01-17 15:04:03 +00:00
committed by GitHub
parent 82fdc6a727
commit 7c65bfd2bf
3 changed files with 514 additions and 401 deletions

View File

@@ -4,9 +4,9 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Docker Build Debugger</title>
<title>DevTools Hub</title>
<meta name="description" content="Analyze Docker build errors and get instant solutions">
<meta name="description" content="Your developer toolkit - Docker debugger and more">
<meta name="theme-color" content="#8b5cf6">
<link rel="preconnect" href="https://fonts.googleapis.com">

View File

@@ -1,52 +1,23 @@
import { useState } from 'react'
import { useKV } from '@github/spark/hooks'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Terminal, Warning, CheckCircle, Copy, Code, Stack, MagnifyingGlass, Sparkle } from '@phosphor-icons/react'
import { parseDockerLog, getSolutionsForError, knowledgeBase } from '@/lib/docker-parser'
import { DockerError, KnowledgeBaseItem } from '@/types/docker'
import { toast } from 'sonner'
import { motion, AnimatePresence } from 'framer-motion'
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
import { List, Stack, House, Wrench } from '@phosphor-icons/react'
import { DockerBuildDebugger } from '@/components/DockerBuildDebugger'
import { motion } from 'framer-motion'
type View = 'home' | 'docker-debugger'
function App() {
const [logInput, setLogInput] = useKV<string>('docker-log-input', '')
const [parsedErrors, setParsedErrors] = useState<DockerError[]>([])
const [selectedKbItem, setSelectedKbItem] = useState<KnowledgeBaseItem | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [currentView, setCurrentView] = useKV<View>('current-view', 'home')
const [isMenuOpen, setIsMenuOpen] = useState(false)
const handleParse = () => {
if (!logInput.trim()) {
toast.error('Please paste a Docker build log first')
return
}
const errors = parseDockerLog(logInput)
if (errors.length === 0) {
toast.info('No errors detected in the log')
} else {
setParsedErrors(errors)
toast.success(`Found ${errors.length} error${errors.length > 1 ? 's' : ''}`)
}
const handleNavigation = (view: View) => {
setCurrentView(view)
setIsMenuOpen(false)
}
const handleCopy = (text: string, label: string) => {
navigator.clipboard.writeText(text)
toast.success(`${label} copied to clipboard`)
}
const filteredKnowledgeBase = knowledgeBase.filter(item =>
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.explanation.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<div className="min-h-screen bg-background">
<div className="relative overflow-hidden">
@@ -54,374 +25,109 @@ function App() {
<div className="absolute inset-0 bg-[linear-gradient(to_right,#4f4f4f12_1px,transparent_1px),linear-gradient(to_bottom,#4f4f4f12_1px,transparent_1px)] bg-[size:4rem_4rem]" />
<div className="relative">
<header className="border-b border-border/40 backdrop-blur-sm">
<div className="container mx-auto px-4 sm:px-6 py-6">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
<Stack size={32} weight="bold" className="text-primary" />
</div>
<div>
<h1 className="text-2xl sm:text-3xl font-bold tracking-tight">Docker Build Debugger</h1>
<p className="text-sm text-muted-foreground">Analyze errors and get instant solutions</p>
<header className="border-b border-border/40 backdrop-blur-sm sticky top-0 z-40">
<div className="container mx-auto px-4 sm:px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
<Stack size={28} weight="bold" className="text-primary" />
</div>
<div>
<h1 className="text-xl sm:text-2xl font-bold tracking-tight">DevTools Hub</h1>
<p className="text-xs sm:text-sm text-muted-foreground">Your developer toolkit</p>
</div>
</div>
<Sheet open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<SheetTrigger asChild>
<Button variant="outline" size="icon" className="gap-2">
<List size={20} weight="bold" />
</Button>
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>Navigation</SheetTitle>
</SheetHeader>
<nav className="mt-6 space-y-2">
<Button
variant={currentView === 'home' ? 'secondary' : 'ghost'}
className="w-full justify-start gap-3"
onClick={() => handleNavigation('home')}
>
<House size={20} weight="bold" />
Home
</Button>
<Button
variant={currentView === 'docker-debugger' ? 'secondary' : 'ghost'}
className="w-full justify-start gap-3"
onClick={() => handleNavigation('docker-debugger')}
>
<Wrench size={20} weight="bold" />
Docker Build Debugger
</Button>
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</header>
<main className="container mx-auto px-4 sm:px-6 py-8">
<Tabs defaultValue="analyzer" className="space-y-6">
<TabsList className="grid w-full grid-cols-2 lg:w-auto lg:inline-grid bg-card/50 backdrop-blur-sm">
<TabsTrigger value="analyzer" className="gap-2">
<Terminal size={16} weight="bold" />
<span className="hidden sm:inline">Log Analyzer</span>
<span className="sm:hidden">Analyze</span>
</TabsTrigger>
<TabsTrigger value="knowledge" className="gap-2">
<MagnifyingGlass size={16} weight="bold" />
<span className="hidden sm:inline">Knowledge Base</span>
<span className="sm:hidden">Knowledge</span>
</TabsTrigger>
</TabsList>
<TabsContent value="analyzer" className="space-y-6">
<Card className="border-border/50 bg-card/50 backdrop-blur-sm">
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Terminal size={20} weight="bold" className="text-primary" />
Paste Build Log
</CardTitle>
<CardDescription>
Copy your Docker build output and paste it below for analysis
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<Textarea
value={logInput}
onChange={(e) => setLogInput(e.target.value)}
placeholder="Paste your Docker build log here...
Example:
#30 50.69 Error: Cannot find module @rollup/rollup-linux-arm64-musl
#30 ERROR: process '/bin/sh -c npm run build' did not complete successfully: exit code: 1"
className="min-h-[300px] font-mono text-sm bg-secondary/50 border-border/50 focus:border-accent/50 focus:ring-accent/20"
/>
<div className="flex gap-3">
<Button onClick={handleParse} className="gap-2" size="lg">
<Sparkle size={18} weight="fill" />
Analyze Log
</Button>
<Button
variant="outline"
onClick={() => {
setLogInput('')
setParsedErrors([])
}}
size="lg"
>
Clear
</Button>
</div>
</CardContent>
</Card>
<AnimatePresence mode="wait">
{parsedErrors.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="space-y-6"
>
{parsedErrors.map((error, index) => (
<Card key={error.id} className="border-destructive/30 bg-card/50 backdrop-blur-sm">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center gap-2 flex-wrap">
<Warning size={24} weight="fill" className="text-destructive animate-pulse" />
<CardTitle className="text-destructive">Error #{index + 1}</CardTitle>
<Badge variant="destructive" className="font-mono">
{error.type}
</Badge>
{error.exitCode && (
<Badge variant="outline" className="font-mono">
Exit Code: {error.exitCode}
</Badge>
)}
{error.stage && (
<Badge variant="secondary" className="font-mono">
{error.stage}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground font-mono">{error.message}</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{error.context.length > 0 && (
<div>
<h4 className="text-sm font-semibold mb-2 flex items-center gap-2">
<Code size={16} weight="bold" />
Error Context
</h4>
<ScrollArea className="h-32 rounded-md border border-border/50 bg-secondary/50 p-3">
<pre className="text-xs font-mono text-muted-foreground whitespace-pre-wrap">
{error.context.join('\n')}
</pre>
</ScrollArea>
</div>
)}
<Separator />
<div>
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
<CheckCircle size={20} weight="bold" className="text-accent" />
Recommended Solutions
</h4>
<div className="space-y-4">
{getSolutionsForError(error).map((solution, sIndex) => (
<motion.div
key={sIndex}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: sIndex * 0.1 }}
>
<Card className="bg-secondary/30 border-accent/20">
<CardHeader>
<CardTitle className="text-base text-accent">
{solution.title}
</CardTitle>
<CardDescription>{solution.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div>
<h5 className="text-sm font-semibold mb-2">Steps:</h5>
<ol className="list-decimal list-inside space-y-1 text-sm text-muted-foreground">
{solution.steps.map((step, stepIndex) => (
<li key={stepIndex}>{step}</li>
))}
</ol>
</div>
{solution.code && (
<div>
<div className="flex items-center justify-between mb-2">
<h5 className="text-sm font-semibold">Code:</h5>
<Button
variant="outline"
size="sm"
onClick={() => handleCopy(solution.code!, 'Code')}
className="gap-2 h-7"
>
<Copy size={14} />
Copy
</Button>
</div>
<ScrollArea className="max-h-48 rounded-md border border-border/50 bg-secondary/50 p-3">
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap">
{solution.code}
</pre>
</ScrollArea>
{solution.codeLanguage && (
<p className="text-xs text-muted-foreground mt-1">
Language: {solution.codeLanguage}
</p>
)}
</div>
)}
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</CardContent>
</Card>
))}
</motion.div>
)}
</AnimatePresence>
</TabsContent>
<TabsContent value="knowledge" className="space-y-6">
<Card className="border-border/50 bg-card/50 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MagnifyingGlass size={20} weight="bold" className="text-primary" />
Search Knowledge Base
</CardTitle>
<CardDescription>
Browse common Docker build errors and their solutions
</CardDescription>
</CardHeader>
<CardContent>
<div className="relative">
<MagnifyingGlass
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search errors, categories, or keywords..."
className="w-full pl-10 pr-4 py-3 rounded-lg border border-border/50 bg-secondary/50 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent/50"
/>
</div>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-2">
{filteredKnowledgeBase.map((item) => (
<motion.div
key={item.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
>
<Card
className="border-border/50 bg-card/50 backdrop-blur-sm cursor-pointer hover:border-accent/50 hover:shadow-lg hover:shadow-accent/5 transition-all"
onClick={() => setSelectedKbItem(item)}
>
<CardHeader>
<div className="flex items-start justify-between gap-2">
<CardTitle className="text-base">{item.title}</CardTitle>
<Badge variant="secondary" className="text-xs shrink-0">
{item.category}
</Badge>
</div>
<CardDescription className="text-xs font-mono text-muted-foreground">
{item.pattern}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground line-clamp-2">
{item.explanation}
</p>
</CardContent>
</Card>
</motion.div>
))}
{currentView === 'home' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
className="space-y-6"
>
<div>
<h2 className="text-2xl font-bold mb-2">Welcome to DevTools Hub</h2>
<p className="text-muted-foreground">
Select a tool from the menu to get started
</p>
</div>
{filteredKnowledgeBase.length === 0 && (
<Alert>
<AlertDescription>
No results found for "{searchQuery}". Try different keywords.
</AlertDescription>
</Alert>
)}
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<Card
className="border-border/50 bg-card/50 backdrop-blur-sm cursor-pointer hover:border-accent/50 hover:shadow-lg hover:shadow-accent/5 transition-all"
onClick={() => handleNavigation('docker-debugger')}
>
<CardHeader>
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-accent/10 border border-accent/20">
<Wrench size={24} weight="bold" className="text-accent" />
</div>
<CardTitle>Docker Build Debugger</CardTitle>
</div>
<CardDescription>
Analyze Docker build errors and get instant solutions with an intelligent knowledge base
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Paste your build logs and get detailed error analysis with recommended fixes
</p>
</CardContent>
</Card>
</div>
</motion.div>
)}
<AnimatePresence mode="wait">
{selectedKbItem && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm"
onClick={() => setSelectedKbItem(null)}
>
<motion.div
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
exit={{ scale: 0.9 }}
onClick={(e) => e.stopPropagation()}
className="w-full max-w-3xl max-h-[90vh] overflow-auto"
>
<Card className="border-border bg-card">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Badge variant="secondary">{selectedKbItem.category}</Badge>
<CardTitle>{selectedKbItem.title}</CardTitle>
</div>
<p className="text-sm font-mono text-muted-foreground">
Pattern: {selectedKbItem.pattern}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedKbItem(null)}
>
Close
</Button>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h4 className="font-semibold mb-2">Explanation</h4>
<p className="text-sm text-muted-foreground">
{selectedKbItem.explanation}
</p>
</div>
<Separator />
<div>
<h4 className="font-semibold mb-4 flex items-center gap-2">
<CheckCircle size={18} weight="bold" className="text-accent" />
Solutions
</h4>
<div className="space-y-4">
{selectedKbItem.solutions.map((solution, index) => (
<Card key={index} className="bg-secondary/30 border-accent/20">
<CardHeader>
<CardTitle className="text-base text-accent">
{solution.title}
</CardTitle>
<CardDescription>{solution.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div>
<h5 className="text-sm font-semibold mb-2">Steps:</h5>
<ol className="list-decimal list-inside space-y-1 text-sm text-muted-foreground">
{solution.steps.map((step, stepIndex) => (
<li key={stepIndex}>{step}</li>
))}
</ol>
</div>
{solution.code && (
<div>
<div className="flex items-center justify-between mb-2">
<h5 className="text-sm font-semibold">Code:</h5>
<Button
variant="outline"
size="sm"
onClick={() => handleCopy(solution.code!, 'Code')}
className="gap-2 h-7"
>
<Copy size={14} />
Copy
</Button>
</div>
<ScrollArea className="max-h-48 rounded-md border border-border/50 bg-secondary/50 p-3">
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap">
{solution.code}
</pre>
</ScrollArea>
</div>
)}
</CardContent>
</Card>
))}
</div>
</div>
</CardContent>
</Card>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</TabsContent>
</Tabs>
{currentView === 'docker-debugger' && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.3 }}
>
<div className="mb-6">
<h2 className="text-2xl font-bold mb-2">Docker Build Debugger</h2>
<p className="text-muted-foreground">
Analyze errors and get instant solutions
</p>
</div>
<DockerBuildDebugger />
</motion.div>
)}
</main>
</div>
</div>

View File

@@ -0,0 +1,407 @@
import { useState } from 'react'
import { useKV } from '@github/spark/hooks'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Textarea } from '@/components/ui/textarea'
import { Badge } from '@/components/ui/badge'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator'
import { Alert, AlertDescription } from '@/components/ui/alert'
import { Terminal, Warning, CheckCircle, Copy, Code, MagnifyingGlass, Sparkle } from '@phosphor-icons/react'
import { parseDockerLog, getSolutionsForError, knowledgeBase } from '@/lib/docker-parser'
import { DockerError, KnowledgeBaseItem } from '@/types/docker'
import { toast } from 'sonner'
import { motion, AnimatePresence } from 'framer-motion'
export function DockerBuildDebugger() {
const [logInput, setLogInput] = useKV<string>('docker-log-input', '')
const [parsedErrors, setParsedErrors] = useState<DockerError[]>([])
const [selectedKbItem, setSelectedKbItem] = useState<KnowledgeBaseItem | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const handleParse = () => {
if (!logInput.trim()) {
toast.error('Please paste a Docker build log first')
return
}
const errors = parseDockerLog(logInput)
if (errors.length === 0) {
toast.info('No errors detected in the log')
} else {
setParsedErrors(errors)
toast.success(`Found ${errors.length} error${errors.length > 1 ? 's' : ''}`)
}
}
const handleCopy = (text: string, label: string) => {
navigator.clipboard.writeText(text)
toast.success(`${label} copied to clipboard`)
}
const filteredKnowledgeBase = knowledgeBase.filter(item =>
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
item.explanation.toLowerCase().includes(searchQuery.toLowerCase())
)
return (
<div className="space-y-6">
<Tabs defaultValue="analyzer" className="space-y-6">
<TabsList className="grid w-full grid-cols-2 lg:w-auto lg:inline-grid bg-card/50 backdrop-blur-sm">
<TabsTrigger value="analyzer" className="gap-2">
<Terminal size={16} weight="bold" />
<span className="hidden sm:inline">Log Analyzer</span>
<span className="sm:hidden">Analyze</span>
</TabsTrigger>
<TabsTrigger value="knowledge" className="gap-2">
<MagnifyingGlass size={16} weight="bold" />
<span className="hidden sm:inline">Knowledge Base</span>
<span className="sm:hidden">Knowledge</span>
</TabsTrigger>
</TabsList>
<TabsContent value="analyzer" className="space-y-6">
<Card className="border-border/50 bg-card/50 backdrop-blur-sm">
<CardHeader>
<div className="flex items-start justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Terminal size={20} weight="bold" className="text-primary" />
Paste Build Log
</CardTitle>
<CardDescription>
Copy your Docker build output and paste it below for analysis
</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-4">
<Textarea
value={logInput}
onChange={(e) => setLogInput(e.target.value)}
placeholder="Paste your Docker build log here...
Example:
#30 50.69 Error: Cannot find module @rollup/rollup-linux-arm64-musl
#30 ERROR: process '/bin/sh -c npm run build' did not complete successfully: exit code: 1"
className="min-h-[300px] font-mono text-sm bg-secondary/50 border-border/50 focus:border-accent/50 focus:ring-accent/20"
/>
<div className="flex gap-3">
<Button onClick={handleParse} className="gap-2" size="lg">
<Sparkle size={18} weight="fill" />
Analyze Log
</Button>
<Button
variant="outline"
onClick={() => {
setLogInput('')
setParsedErrors([])
}}
size="lg"
>
Clear
</Button>
</div>
</CardContent>
</Card>
<AnimatePresence mode="wait">
{parsedErrors.length > 0 && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
className="space-y-6"
>
{parsedErrors.map((error, index) => (
<Card key={error.id} className="border-destructive/30 bg-card/50 backdrop-blur-sm">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center gap-2 flex-wrap">
<Warning size={24} weight="fill" className="text-destructive animate-pulse" />
<CardTitle className="text-destructive">Error #{index + 1}</CardTitle>
<Badge variant="destructive" className="font-mono">
{error.type}
</Badge>
{error.exitCode && (
<Badge variant="outline" className="font-mono">
Exit Code: {error.exitCode}
</Badge>
)}
{error.stage && (
<Badge variant="secondary" className="font-mono">
{error.stage}
</Badge>
)}
</div>
<p className="text-sm text-muted-foreground font-mono">{error.message}</p>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
{error.context.length > 0 && (
<div>
<h4 className="text-sm font-semibold mb-2 flex items-center gap-2">
<Code size={16} weight="bold" />
Error Context
</h4>
<ScrollArea className="h-32 rounded-md border border-border/50 bg-secondary/50 p-3">
<pre className="text-xs font-mono text-muted-foreground whitespace-pre-wrap">
{error.context.join('\n')}
</pre>
</ScrollArea>
</div>
)}
<Separator />
<div>
<h4 className="text-lg font-semibold mb-4 flex items-center gap-2">
<CheckCircle size={20} weight="bold" className="text-accent" />
Recommended Solutions
</h4>
<div className="space-y-4">
{getSolutionsForError(error).map((solution, sIndex) => (
<motion.div
key={sIndex}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: sIndex * 0.1 }}
>
<Card className="bg-secondary/30 border-accent/20">
<CardHeader>
<CardTitle className="text-base text-accent">
{solution.title}
</CardTitle>
<CardDescription>{solution.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div>
<h5 className="text-sm font-semibold mb-2">Steps:</h5>
<ol className="list-decimal list-inside space-y-1 text-sm text-muted-foreground">
{solution.steps.map((step, stepIndex) => (
<li key={stepIndex}>{step}</li>
))}
</ol>
</div>
{solution.code && (
<div>
<div className="flex items-center justify-between mb-2">
<h5 className="text-sm font-semibold">Code:</h5>
<Button
variant="outline"
size="sm"
onClick={() => handleCopy(solution.code!, 'Code')}
className="gap-2 h-7"
>
<Copy size={14} />
Copy
</Button>
</div>
<ScrollArea className="max-h-48 rounded-md border border-border/50 bg-secondary/50 p-3">
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap">
{solution.code}
</pre>
</ScrollArea>
{solution.codeLanguage && (
<p className="text-xs text-muted-foreground mt-1">
Language: {solution.codeLanguage}
</p>
)}
</div>
)}
</CardContent>
</Card>
</motion.div>
))}
</div>
</div>
</CardContent>
</Card>
))}
</motion.div>
)}
</AnimatePresence>
</TabsContent>
<TabsContent value="knowledge" className="space-y-6">
<Card className="border-border/50 bg-card/50 backdrop-blur-sm">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MagnifyingGlass size={20} weight="bold" className="text-primary" />
Search Knowledge Base
</CardTitle>
<CardDescription>
Browse common Docker build errors and their solutions
</CardDescription>
</CardHeader>
<CardContent>
<div className="relative">
<MagnifyingGlass
size={20}
className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search errors, categories, or keywords..."
className="w-full pl-10 pr-4 py-3 rounded-lg border border-border/50 bg-secondary/50 text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-accent/20 focus:border-accent/50"
/>
</div>
</CardContent>
</Card>
<div className="grid gap-4 md:grid-cols-2">
{filteredKnowledgeBase.map((item) => (
<motion.div
key={item.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
>
<Card
className="border-border/50 bg-card/50 backdrop-blur-sm cursor-pointer hover:border-accent/50 hover:shadow-lg hover:shadow-accent/5 transition-all"
onClick={() => setSelectedKbItem(item)}
>
<CardHeader>
<div className="flex items-start justify-between gap-2">
<CardTitle className="text-base">{item.title}</CardTitle>
<Badge variant="secondary" className="text-xs shrink-0">
{item.category}
</Badge>
</div>
<CardDescription className="text-xs font-mono text-muted-foreground">
{item.pattern}
</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground line-clamp-2">
{item.explanation}
</p>
</CardContent>
</Card>
</motion.div>
))}
</div>
{filteredKnowledgeBase.length === 0 && (
<Alert>
<AlertDescription>
No results found for "{searchQuery}". Try different keywords.
</AlertDescription>
</Alert>
)}
<AnimatePresence mode="wait">
{selectedKbItem && (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-background/80 backdrop-blur-sm"
onClick={() => setSelectedKbItem(null)}
>
<motion.div
initial={{ scale: 0.9 }}
animate={{ scale: 1 }}
exit={{ scale: 0.9 }}
onClick={(e) => e.stopPropagation()}
className="w-full max-w-3xl max-h-[90vh] overflow-auto"
>
<Card className="border-border bg-card">
<CardHeader>
<div className="flex items-start justify-between">
<div className="space-y-2">
<div className="flex items-center gap-2">
<Badge variant="secondary">{selectedKbItem.category}</Badge>
<CardTitle>{selectedKbItem.title}</CardTitle>
</div>
<p className="text-sm font-mono text-muted-foreground">
Pattern: {selectedKbItem.pattern}
</p>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedKbItem(null)}
>
Close
</Button>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div>
<h4 className="font-semibold mb-2">Explanation</h4>
<p className="text-sm text-muted-foreground">
{selectedKbItem.explanation}
</p>
</div>
<Separator />
<div>
<h4 className="font-semibold mb-4 flex items-center gap-2">
<CheckCircle size={18} weight="bold" className="text-accent" />
Solutions
</h4>
<div className="space-y-4">
{selectedKbItem.solutions.map((solution, index) => (
<Card key={index} className="bg-secondary/30 border-accent/20">
<CardHeader>
<CardTitle className="text-base text-accent">
{solution.title}
</CardTitle>
<CardDescription>{solution.description}</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div>
<h5 className="text-sm font-semibold mb-2">Steps:</h5>
<ol className="list-decimal list-inside space-y-1 text-sm text-muted-foreground">
{solution.steps.map((step, stepIndex) => (
<li key={stepIndex}>{step}</li>
))}
</ol>
</div>
{solution.code && (
<div>
<div className="flex items-center justify-between mb-2">
<h5 className="text-sm font-semibold">Code:</h5>
<Button
variant="outline"
size="sm"
onClick={() => handleCopy(solution.code!, 'Code')}
className="gap-2 h-7"
>
<Copy size={14} />
Copy
</Button>
</div>
<ScrollArea className="max-h-48 rounded-md border border-border/50 bg-secondary/50 p-3">
<pre className="text-xs font-mono text-foreground whitespace-pre-wrap">
{solution.code}
</pre>
</ScrollArea>
</div>
)}
</CardContent>
</Card>
))}
</div>
</div>
</CardContent>
</Card>
</motion.div>
</motion.div>
)}
</AnimatePresence>
</TabsContent>
</Tabs>
</div>
)
}