mirror of
https://github.com/johndoe6345789/low-code-react-app-b.git
synced 2026-04-26 14:44:55 +00:00
Generated by Spark: Too risky making changes without refactoring now. Create hook library, All components <150LOC.
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
50
src/components/molecules/CodeExplanationDialog.tsx
Normal file
50
src/components/molecules/CodeExplanationDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
32
src/components/molecules/EditorActions.tsx
Normal file
32
src/components/molecules/EditorActions.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
40
src/components/molecules/EditorToolbar.tsx
Normal file
40
src/components/molecules/EditorToolbar.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
12
src/components/molecules/EmptyEditorState.tsx
Normal file
12
src/components/molecules/EmptyEditorState.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
39
src/components/molecules/FileTabs.tsx
Normal file
39
src/components/molecules/FileTabs.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
28
src/components/molecules/MonacoEditorPanel.tsx
Normal file
28
src/components/molecules/MonacoEditorPanel.tsx
Normal 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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user