mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-24 13:44:54 +00:00
Refactor docker build debugger UI
This commit is contained in:
@@ -1,405 +1,85 @@
|
||||
import { useState } from 'react'
|
||||
import { useKV } from '@/hooks/use-kv'
|
||||
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 { Terminal, MagnifyingGlass } from '@phosphor-icons/react'
|
||||
import { parseDockerLog } from '@/lib/docker-parser'
|
||||
import { DockerError } from '@/types/docker'
|
||||
import { ErrorList } from '@/components/docker-build-debugger/ErrorList'
|
||||
import { LogAnalyzer } from '@/components/docker-build-debugger/LogAnalyzer'
|
||||
import { KnowledgeBaseView } from '@/components/docker-build-debugger/KnowledgeBaseView'
|
||||
import { toast } from 'sonner'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import dockerBuildDebuggerText from '@/data/docker-build-debugger.json'
|
||||
import { useState } from 'react'
|
||||
|
||||
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')
|
||||
toast.error(dockerBuildDebuggerText.analyzer.emptyLogError)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const errors = parseDockerLog(logInput)
|
||||
|
||||
|
||||
if (errors.length === 0) {
|
||||
toast.info('No errors detected in the log')
|
||||
toast.info(dockerBuildDebuggerText.analyzer.noErrorsToast)
|
||||
} else {
|
||||
setParsedErrors(errors)
|
||||
toast.success(`Found ${errors.length} error${errors.length > 1 ? 's' : ''}`)
|
||||
toast.success(
|
||||
dockerBuildDebuggerText.analyzer.errorsFoundToast
|
||||
.replace('{{count}}', String(errors.length))
|
||||
.replace('{{plural}}', errors.length > 1 ? 's' : '')
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopy = (text: string, label: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
toast.success(`${label} copied to clipboard`)
|
||||
toast.success(dockerBuildDebuggerText.errors.copiedToast.replace('{{label}}', label))
|
||||
}
|
||||
|
||||
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>
|
||||
<span className="hidden sm:inline">{dockerBuildDebuggerText.tabs.analyzer.label}</span>
|
||||
<span className="sm:hidden">{dockerBuildDebuggerText.tabs.analyzer.shortLabel}</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>
|
||||
<span className="hidden sm:inline">{dockerBuildDebuggerText.tabs.knowledge.label}</span>
|
||||
<span className="sm:hidden">{dockerBuildDebuggerText.tabs.knowledge.shortLabel}</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>
|
||||
<LogAnalyzer
|
||||
logInput={logInput}
|
||||
onLogChange={setLogInput}
|
||||
onAnalyze={handleParse}
|
||||
onClear={() => {
|
||||
setLogInput('')
|
||||
setParsedErrors([])
|
||||
}}
|
||||
text={dockerBuildDebuggerText.analyzer}
|
||||
/>
|
||||
<ErrorList
|
||||
errors={parsedErrors}
|
||||
onCopy={handleCopy}
|
||||
text={dockerBuildDebuggerText.errors}
|
||||
commonText={dockerBuildDebuggerText.common}
|
||||
/>
|
||||
</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>
|
||||
<KnowledgeBaseView
|
||||
onCopy={handleCopy}
|
||||
text={dockerBuildDebuggerText.knowledge}
|
||||
commonText={dockerBuildDebuggerText.common}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
149
src/components/docker-build-debugger/ErrorList.tsx
Normal file
149
src/components/docker-build-debugger/ErrorList.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { getSolutionsForError } from '@/lib/docker-parser'
|
||||
import { DockerError } from '@/types/docker'
|
||||
import { CheckCircle, Code, Copy, Warning } from '@phosphor-icons/react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
type ErrorListProps = {
|
||||
errors: DockerError[]
|
||||
onCopy: (text: string, label: string) => void
|
||||
text: {
|
||||
title: string
|
||||
exitCodeLabel: string
|
||||
contextTitle: string
|
||||
solutionsTitle: string
|
||||
}
|
||||
commonText: {
|
||||
stepsLabel: string
|
||||
codeLabel: string
|
||||
copyButton: string
|
||||
languageLabel: string
|
||||
codeCopyLabel: string
|
||||
}
|
||||
}
|
||||
export function ErrorList({ errors, onCopy, text, commonText }: ErrorListProps) {
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{errors.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"
|
||||
>
|
||||
{errors.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">
|
||||
{text.title.replace('{{index}}', String(index + 1))}
|
||||
</CardTitle>
|
||||
<Badge variant="destructive" className="font-mono">
|
||||
{error.type}
|
||||
</Badge>
|
||||
{error.exitCode && (
|
||||
<Badge variant="outline" className="font-mono">
|
||||
{text.exitCodeLabel} {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" />
|
||||
{text.contextTitle}
|
||||
</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" />
|
||||
{text.solutionsTitle}
|
||||
</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">{commonText.stepsLabel}</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">{commonText.codeLabel}</h5>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onCopy(solution.code!, commonText.codeCopyLabel)}
|
||||
className="gap-2 h-7"
|
||||
>
|
||||
<Copy size={14} />
|
||||
{commonText.copyButton}
|
||||
</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">
|
||||
{commonText.languageLabel} {solution.codeLanguage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
)
|
||||
}
|
||||
133
src/components/docker-build-debugger/KnowledgeBaseModal.tsx
Normal file
133
src/components/docker-build-debugger/KnowledgeBaseModal.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { KnowledgeBaseItem } from '@/types/docker'
|
||||
import { CheckCircle, Copy } from '@phosphor-icons/react'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
|
||||
type KnowledgeBaseModalProps = {
|
||||
item: KnowledgeBaseItem | null
|
||||
onClose: () => void
|
||||
onCopy: (text: string, label: string) => void
|
||||
text: {
|
||||
closeButton: string
|
||||
patternLabel: string
|
||||
explanationTitle: string
|
||||
solutionsTitle: string
|
||||
}
|
||||
commonText: {
|
||||
stepsLabel: string
|
||||
codeLabel: string
|
||||
copyButton: string
|
||||
codeCopyLabel: string
|
||||
}
|
||||
}
|
||||
|
||||
export function KnowledgeBaseModal({
|
||||
item,
|
||||
onClose,
|
||||
onCopy,
|
||||
text,
|
||||
commonText,
|
||||
}: KnowledgeBaseModalProps) {
|
||||
return (
|
||||
<AnimatePresence mode="wait">
|
||||
{item && (
|
||||
<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={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ scale: 0.9 }}
|
||||
animate={{ scale: 1 }}
|
||||
exit={{ scale: 0.9 }}
|
||||
onClick={(event) => event.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">{item.category}</Badge>
|
||||
<CardTitle>{item.title}</CardTitle>
|
||||
</div>
|
||||
<p className="text-sm font-mono text-muted-foreground">
|
||||
{text.patternLabel} {item.pattern}
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={onClose}>
|
||||
{text.closeButton}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-2">{text.explanationTitle}</h4>
|
||||
<p className="text-sm text-muted-foreground">{item.explanation}</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4 flex items-center gap-2">
|
||||
<CheckCircle size={18} weight="bold" className="text-accent" />
|
||||
{text.solutionsTitle}
|
||||
</h4>
|
||||
<div className="space-y-4">
|
||||
{item.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">{commonText.stepsLabel}</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">{commonText.codeLabel}</h5>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onCopy(solution.code!, commonText.codeCopyLabel)}
|
||||
className="gap-2 h-7"
|
||||
>
|
||||
<Copy size={14} />
|
||||
{commonText.copyButton}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { KnowledgeBaseItem } from '@/types/docker'
|
||||
import { motion } from 'framer-motion'
|
||||
|
||||
type KnowledgeBaseResultsProps = {
|
||||
items: KnowledgeBaseItem[]
|
||||
onSelect: (item: KnowledgeBaseItem) => void
|
||||
searchQuery: string
|
||||
text: {
|
||||
noResults: string
|
||||
}
|
||||
}
|
||||
|
||||
export function KnowledgeBaseResults({
|
||||
items,
|
||||
onSelect,
|
||||
searchQuery,
|
||||
text,
|
||||
}: KnowledgeBaseResultsProps) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{items.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={() => onSelect(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>
|
||||
|
||||
{items.length === 0 && (
|
||||
<Alert>
|
||||
<AlertDescription>{text.noResults.replace('{{query}}', searchQuery)}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { MagnifyingGlass } from '@phosphor-icons/react'
|
||||
|
||||
type KnowledgeBaseSearchPanelProps = {
|
||||
searchQuery: string
|
||||
onSearchChange: (value: string) => void
|
||||
text: {
|
||||
title: string
|
||||
description: string
|
||||
searchPlaceholder: string
|
||||
}
|
||||
}
|
||||
|
||||
export function KnowledgeBaseSearchPanel({
|
||||
searchQuery,
|
||||
onSearchChange,
|
||||
text,
|
||||
}: KnowledgeBaseSearchPanelProps) {
|
||||
return (
|
||||
<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" />
|
||||
{text.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{text.description}</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) => onSearchChange(e.target.value)}
|
||||
placeholder={text.searchPlaceholder}
|
||||
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>
|
||||
)
|
||||
}
|
||||
65
src/components/docker-build-debugger/KnowledgeBaseView.tsx
Normal file
65
src/components/docker-build-debugger/KnowledgeBaseView.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { knowledgeBase } from '@/lib/docker-parser'
|
||||
import { KnowledgeBaseItem } from '@/types/docker'
|
||||
import { KnowledgeBaseModal } from './KnowledgeBaseModal'
|
||||
import { KnowledgeBaseResults } from './KnowledgeBaseResults'
|
||||
import { KnowledgeBaseSearchPanel } from './KnowledgeBaseSearchPanel'
|
||||
|
||||
type KnowledgeBaseViewProps = {
|
||||
onCopy: (text: string, label: string) => void
|
||||
text: {
|
||||
title: string
|
||||
description: string
|
||||
searchPlaceholder: string
|
||||
noResults: string
|
||||
closeButton: string
|
||||
patternLabel: string
|
||||
explanationTitle: string
|
||||
solutionsTitle: string
|
||||
}
|
||||
commonText: {
|
||||
stepsLabel: string
|
||||
codeLabel: string
|
||||
copyButton: string
|
||||
codeCopyLabel: string
|
||||
}
|
||||
}
|
||||
|
||||
export function KnowledgeBaseView({ onCopy, text, commonText }: KnowledgeBaseViewProps) {
|
||||
const [selectedKbItem, setSelectedKbItem] = useState<KnowledgeBaseItem | null>(null)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
const filteredKnowledgeBase = useMemo(
|
||||
() =>
|
||||
knowledgeBase.filter(
|
||||
(item) =>
|
||||
item.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.category.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
item.explanation.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
),
|
||||
[searchQuery]
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<KnowledgeBaseSearchPanel
|
||||
searchQuery={searchQuery}
|
||||
onSearchChange={setSearchQuery}
|
||||
text={text}
|
||||
/>
|
||||
<KnowledgeBaseResults
|
||||
items={filteredKnowledgeBase}
|
||||
onSelect={setSelectedKbItem}
|
||||
searchQuery={searchQuery}
|
||||
text={text}
|
||||
/>
|
||||
<KnowledgeBaseModal
|
||||
item={selectedKbItem}
|
||||
onClose={() => setSelectedKbItem(null)}
|
||||
onCopy={onCopy}
|
||||
text={text}
|
||||
commonText={commonText}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
src/components/docker-build-debugger/LogAnalyzer.tsx
Normal file
55
src/components/docker-build-debugger/LogAnalyzer.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Sparkle, Terminal } from '@phosphor-icons/react'
|
||||
|
||||
type LogAnalyzerText = {
|
||||
title: string
|
||||
description: string
|
||||
placeholder: string
|
||||
analyzeButton: string
|
||||
clearButton: string
|
||||
}
|
||||
|
||||
type LogAnalyzerProps = {
|
||||
logInput: string
|
||||
onLogChange: (value: string) => void
|
||||
onAnalyze: () => void
|
||||
onClear: () => void
|
||||
text: LogAnalyzerText
|
||||
}
|
||||
|
||||
export function LogAnalyzer({ logInput, onLogChange, onAnalyze, onClear, text }: LogAnalyzerProps) {
|
||||
return (
|
||||
<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" />
|
||||
{text.title}
|
||||
</CardTitle>
|
||||
<CardDescription>{text.description}</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
value={logInput}
|
||||
onChange={(e) => onLogChange(e.target.value)}
|
||||
placeholder={text.placeholder}
|
||||
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={onAnalyze} className="gap-2" size="lg">
|
||||
<Sparkle size={18} weight="fill" />
|
||||
{text.analyzeButton}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={onClear} size="lg">
|
||||
{text.clearButton}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
46
src/data/docker-build-debugger.json
Normal file
46
src/data/docker-build-debugger.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"tabs": {
|
||||
"analyzer": {
|
||||
"label": "Log Analyzer",
|
||||
"shortLabel": "Analyze"
|
||||
},
|
||||
"knowledge": {
|
||||
"label": "Knowledge Base",
|
||||
"shortLabel": "Knowledge"
|
||||
}
|
||||
},
|
||||
"analyzer": {
|
||||
"title": "Paste Build Log",
|
||||
"description": "Copy your Docker build output and paste it below for analysis",
|
||||
"placeholder": "Paste your Docker build log here...\n\nExample:\n#30 50.69 Error: Cannot find module @rollup/rollup-linux-arm64-musl\n#30 ERROR: process '/bin/sh -c npm run build' did not complete successfully: exit code: 1",
|
||||
"analyzeButton": "Analyze Log",
|
||||
"clearButton": "Clear",
|
||||
"emptyLogError": "Please paste a Docker build log first",
|
||||
"noErrorsToast": "No errors detected in the log",
|
||||
"errorsFoundToast": "Found {{count}} error{{plural}}"
|
||||
},
|
||||
"errors": {
|
||||
"title": "Error #{{index}}",
|
||||
"exitCodeLabel": "Exit Code:",
|
||||
"contextTitle": "Error Context",
|
||||
"solutionsTitle": "Recommended Solutions",
|
||||
"copiedToast": "{{label}} copied to clipboard"
|
||||
},
|
||||
"knowledge": {
|
||||
"title": "Search Knowledge Base",
|
||||
"description": "Browse common Docker build errors and their solutions",
|
||||
"searchPlaceholder": "Search errors, categories, or keywords...",
|
||||
"noResults": "No results found for \"{{query}}\". Try different keywords.",
|
||||
"closeButton": "Close",
|
||||
"patternLabel": "Pattern:",
|
||||
"explanationTitle": "Explanation",
|
||||
"solutionsTitle": "Solutions"
|
||||
},
|
||||
"common": {
|
||||
"stepsLabel": "Steps:",
|
||||
"codeLabel": "Code:",
|
||||
"codeCopyLabel": "Code",
|
||||
"copyButton": "Copy",
|
||||
"languageLabel": "Language:"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user