Generated by Spark: Too risky making changes without refactoring now. Create hook library, All components <150LOC.

This commit is contained in:
2026-01-16 18:11:41 +00:00
committed by GitHub
parent c81277fadd
commit ba3dcf538a
21 changed files with 1152 additions and 155 deletions

View File

@@ -1,21 +1,12 @@
import { useState } from 'react'
import Editor from '@monaco-editor/react'
import { Card } from '@/components/ui/card'
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
import { Button } from '@/components/ui/button'
import { ProjectFile } from '@/types/project'
import { FileCode, X, Sparkle, Info } from '@phosphor-icons/react'
import { AIService } from '@/lib/ai-service'
import { toast } from 'sonner'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { Textarea } from '@/components/ui/textarea'
import { ScrollArea } from '@/components/ui/scroll-area'
import { useDialogState } from '@/hooks/use-dialog-state'
import { useFileFilters } from '@/hooks/use-file-filters'
import { useCodeExplanation } from '@/hooks/use-code-explanation'
import { useAIOperations } from '@/hooks/use-ai-operations'
import { EditorToolbar } from '@/components/molecules/EditorToolbar'
import { MonacoEditorPanel } from '@/components/molecules/MonacoEditorPanel'
import { EmptyEditorState } from '@/components/molecules/EmptyEditorState'
import { CodeExplanationDialog } from '@/components/molecules/CodeExplanationDialog'
interface CodeEditorProps {
files: ProjectFile[]
@@ -32,163 +23,65 @@ export function CodeEditor({
onFileSelect,
onFileClose,
}: CodeEditorProps) {
const [showExplainDialog, setShowExplainDialog] = useState(false)
const [explanation, setExplanation] = useState('')
const [isExplaining, setIsExplaining] = useState(false)
const { isOpen: showExplainDialog, setIsOpen: setShowExplainDialog } = useDialogState()
const { explanation, isExplaining, explain } = useCodeExplanation()
const { improveCode } = useAIOperations()
const { getOpenFiles, findFileById } = useFileFilters(files)
const activeFile = files.find((f) => f.id === activeFileId)
const openFiles = files.filter((f) => f.id === activeFileId || files.length < 5)
const activeFile = findFileById(activeFileId) || undefined
const openFiles = getOpenFiles(activeFileId)
const improveCodeWithAI = async () => {
const handleImproveCode = async () => {
if (!activeFile) return
const instruction = prompt('How would you like to improve this code?')
if (!instruction) return
try {
toast.info('Improving code with AI...')
const improvedCode = await AIService.improveCode(activeFile.content, instruction)
if (improvedCode) {
onFileChange(activeFile.id, improvedCode)
toast.success('Code improved successfully!')
} else {
toast.error('AI improvement failed. Please try again.')
}
} catch (error) {
toast.error('Failed to improve code')
console.error(error)
const improvedCode = await improveCode(activeFile.content, instruction)
if (improvedCode) {
onFileChange(activeFile.id, improvedCode)
}
}
const explainCode = async () => {
const handleExplainCode = async () => {
if (!activeFile) return
try {
setIsExplaining(true)
setShowExplainDialog(true)
setExplanation('Analyzing code...')
const codeExplanation = await AIService.explainCode(activeFile.content)
if (codeExplanation) {
setExplanation(codeExplanation)
} else {
setExplanation('Failed to generate explanation. Please try again.')
}
} catch (error) {
setExplanation('Error generating explanation.')
console.error(error)
} finally {
setIsExplaining(false)
}
setShowExplainDialog(true)
await explain(activeFile.content)
}
return (
<div className="h-full flex flex-col">
{openFiles.length > 0 ? (
<>
<div className="flex items-center gap-1 bg-secondary/50 border-b border-border px-2 py-1 justify-between">
<div className="flex items-center gap-1">
{openFiles.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
file.id === activeFileId
? 'bg-card text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
>
<FileCode size={16} />
<span>{file.name}</span>
<button
onClick={(e) => {
e.stopPropagation()
onFileClose(file.id)
}}
className="hover:text-destructive"
>
<X size={14} />
</button>
</button>
))}
</div>
{activeFile && (
<div className="flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={explainCode}
className="h-7 text-xs"
>
<Info size={14} className="mr-1" />
Explain
</Button>
<Button
size="sm"
variant="ghost"
onClick={improveCodeWithAI}
className="h-7 text-xs"
>
<Sparkle size={14} className="mr-1" weight="duotone" />
Improve
</Button>
</div>
)}
</div>
<EditorToolbar
openFiles={openFiles}
activeFileId={activeFileId}
activeFile={activeFile}
onFileSelect={onFileSelect}
onFileClose={onFileClose}
onExplain={handleExplainCode}
onImprove={handleImproveCode}
/>
<div className="flex-1">
{activeFile && (
<Editor
height="100%"
language={activeFile.language}
value={activeFile.content}
onChange={(value) => onFileChange(activeFile.id, value || '')}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: 'JetBrains Mono, monospace',
fontLigatures: true,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
<MonacoEditorPanel
file={activeFile}
onChange={(content) => onFileChange(activeFile.id, content)}
/>
)}
</div>
</>
) : (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<FileCode size={48} className="mx-auto mb-4 opacity-50" />
<p>Select a file to edit</p>
</div>
</div>
<EmptyEditorState />
)}
<Dialog open={showExplainDialog} onOpenChange={setShowExplainDialog}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Code Explanation</DialogTitle>
<DialogDescription>
AI-generated explanation of {activeFile?.name}
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
<div className="p-4 bg-muted rounded-lg">
{isExplaining ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Sparkle size={16} weight="duotone" className="animate-pulse" />
Analyzing code...
</div>
) : (
<p className="whitespace-pre-wrap text-sm">{explanation}</p>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
<CodeExplanationDialog
open={showExplainDialog}
onOpenChange={setShowExplainDialog}
fileName={activeFile?.name}
explanation={explanation}
isLoading={isExplaining}
/>
</div>
)
}

View File

@@ -0,0 +1,50 @@
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Sparkle } from '@phosphor-icons/react'
interface CodeExplanationDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
fileName: string | undefined
explanation: string
isLoading: boolean
}
export function CodeExplanationDialog({
open,
onOpenChange,
fileName,
explanation,
isLoading,
}: CodeExplanationDialogProps) {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Code Explanation</DialogTitle>
<DialogDescription>
AI-generated explanation of {fileName}
</DialogDescription>
</DialogHeader>
<ScrollArea className="max-h-96">
<div className="p-4 bg-muted rounded-lg">
{isLoading ? (
<div className="flex items-center gap-2 text-muted-foreground">
<Sparkle size={16} weight="duotone" className="animate-pulse" />
Analyzing code...
</div>
) : (
<p className="whitespace-pre-wrap text-sm">{explanation}</p>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
)
}

View File

@@ -0,0 +1,32 @@
import { Button } from '@/components/ui/button'
import { Info, Sparkle } from '@phosphor-icons/react'
interface EditorActionsProps {
onExplain: () => void
onImprove: () => void
}
export function EditorActions({ onExplain, onImprove }: EditorActionsProps) {
return (
<div className="flex gap-2">
<Button
size="sm"
variant="ghost"
onClick={onExplain}
className="h-7 text-xs"
>
<Info size={14} className="mr-1" />
Explain
</Button>
<Button
size="sm"
variant="ghost"
onClick={onImprove}
className="h-7 text-xs"
>
<Sparkle size={14} className="mr-1" weight="duotone" />
Improve
</Button>
</div>
)
}

View File

@@ -0,0 +1,40 @@
import { ProjectFile } from '@/types/project'
import { FileTabs } from './FileTabs'
import { EditorActions } from './EditorActions'
interface EditorToolbarProps {
openFiles: ProjectFile[]
activeFileId: string | null
activeFile: ProjectFile | undefined
onFileSelect: (fileId: string) => void
onFileClose: (fileId: string) => void
onExplain: () => void
onImprove: () => void
}
export function EditorToolbar({
openFiles,
activeFileId,
activeFile,
onFileSelect,
onFileClose,
onExplain,
onImprove,
}: EditorToolbarProps) {
return (
<div className="flex items-center gap-1 bg-secondary/50 border-b border-border px-2 py-1 justify-between">
<FileTabs
files={openFiles}
activeFileId={activeFileId}
onFileSelect={onFileSelect}
onFileClose={onFileClose}
/>
{activeFile && (
<EditorActions
onExplain={onExplain}
onImprove={onImprove}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,12 @@
import { FileCode } from '@phosphor-icons/react'
export function EmptyEditorState() {
return (
<div className="flex-1 flex items-center justify-center text-muted-foreground">
<div className="text-center">
<FileCode size={48} className="mx-auto mb-4 opacity-50" />
<p>Select a file to edit</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,39 @@
import { ProjectFile } from '@/types/project'
import { FileCode, X } from '@phosphor-icons/react'
interface FileTabsProps {
files: ProjectFile[]
activeFileId: string | null
onFileSelect: (fileId: string) => void
onFileClose: (fileId: string) => void
}
export function FileTabs({ files, activeFileId, onFileSelect, onFileClose }: FileTabsProps) {
return (
<div className="flex items-center gap-1">
{files.map((file) => (
<button
key={file.id}
onClick={() => onFileSelect(file.id)}
className={`flex items-center gap-2 px-3 py-1.5 rounded text-sm transition-colors ${
file.id === activeFileId
? 'bg-card text-foreground'
: 'text-muted-foreground hover:text-foreground hover:bg-card/50'
}`}
>
<FileCode size={16} />
<span>{file.name}</span>
<button
onClick={(e) => {
e.stopPropagation()
onFileClose(file.id)
}}
className="hover:text-destructive"
>
<X size={14} />
</button>
</button>
))}
</div>
)
}

View File

@@ -0,0 +1,28 @@
import Editor from '@monaco-editor/react'
import { ProjectFile } from '@/types/project'
interface MonacoEditorPanelProps {
file: ProjectFile
onChange: (content: string) => void
}
export function MonacoEditorPanel({ file, onChange }: MonacoEditorPanelProps) {
return (
<Editor
height="100%"
language={file.language}
value={file.content}
onChange={(value) => onChange(value || '')}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 14,
fontFamily: 'JetBrains Mono, monospace',
fontLigatures: true,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
)
}

View File

@@ -1,13 +1,19 @@
export { SaveIndicator } from './SaveIndicator'
export { AppBranding } from './AppBranding'
export { PageHeaderContent } from './PageHeaderContent'
export { ToolbarButton } from './ToolbarButton'
export { NavigationItem } from './NavigationItem'
export { NavigationGroupHeader } from './NavigationGroupHeader'
export { CodeExplanationDialog } from './CodeExplanationDialog'
export { EditorActions } from './EditorActions'
export { EditorToolbar } from './EditorToolbar'
export { EmptyEditorState } from './EmptyEditorState'
export { EmptyState } from './EmptyState'
export { LoadingState } from './LoadingState'
export { StatCard } from './StatCard'
export { FileTabs } from './FileTabs'
export { LabelWithBadge } from './LabelWithBadge'
export { LoadingState } from './LoadingState'
export { MonacoEditorPanel } from './MonacoEditorPanel'
export { NavigationGroupHeader } from './NavigationGroupHeader'
export { NavigationItem } from './NavigationItem'
export { PageHeaderContent } from './PageHeaderContent'
export { SaveIndicator } from './SaveIndicator'
export { StatCard } from './StatCard'
export { ToolbarButton } from './ToolbarButton'
export { TreeCard } from './TreeCard'
export { TreeFormDialog } from './TreeFormDialog'
export { TreeListHeader } from './TreeListHeader'