Files

14 lines
44 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"name": "ai-code",
"description": "AI-powered code generation and analysis",
"dependencies": [
"@hanzo/ui"
],
"files": [
{
"name": "ai-code.tsx",
"content": "\"use client\"\n\nimport * as React from \"react\"\nimport { Grid } from \"@hanzo/ui/grid\"\nimport Editor, { OnChange, OnMount } from \"@monaco-editor/react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\nimport {\n AlertTriangle,\n Bot,\n Bug,\n Check,\n ChevronDown,\n ChevronRight,\n Code,\n Copy,\n Download,\n Eye,\n EyeOff,\n FileCode,\n FileText,\n GitCompare,\n Loader2,\n MessageSquare,\n Play,\n RotateCcw,\n Send,\n Settings,\n Shield,\n Split,\n TestTube,\n Upload,\n User,\n Wand2,\n Zap,\n} from \"lucide-react\"\nimport { useTheme } from \"next-themes\"\nimport { toast } from \"sonner\"\n\nimport { cn } from \"@/lib/utils\"\nimport {\n Avatar,\n AvatarFallback,\n AvatarImage,\n} from \"@/registry/default/ui/avatar\"\nimport { Badge } from \"@/registry/default/ui/badge\"\nimport { Button } from \"@/registry/default/ui/button\"\nimport {\n Card,\n CardContent,\n CardHeader,\n CardTitle,\n} from \"@/registry/default/ui/card\"\nimport {\n DropdownMenu,\n DropdownMenuCheckboxItem,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n} from \"@/registry/default/ui/dropdown-menu\"\nimport { Progress } from \"@/registry/default/ui/progress\"\nimport {\n ResizableHandle,\n ResizablePanel,\n ResizablePanelGroup,\n} from \"@/registry/default/ui/resizable\"\nimport { ScrollArea, ScrollBar } from \"@/registry/default/ui/scroll-area\"\nimport { Separator } from \"@/registry/default/ui/separator\"\nimport {\n Tabs,\n TabsContent,\n TabsList,\n TabsTrigger,\n} from \"@/registry/default/ui/tabs\"\nimport { Textarea } from \"@/registry/default/ui/textarea\"\nimport {\n Tooltip,\n TooltipContent,\n TooltipProvider,\n TooltipTrigger,\n} from \"@/registry/default/ui/tooltip\"\n\n// Types\nexport type CodeFeature =\n \"generate\" | \"explain\" | \"refactor\" | \"debug\" | \"test\" | \"document\" | \"review\"\n\nexport interface Explanation {\n id: string\n line: number\n content: string\n type: \"explanation\" | \"suggestion\" | \"warning\" | \"error\"\n}\n\nexport interface CodeSuggestion {\n id: string\n type: CodeFeature\n title: string\n description: string\n code?: string\n startLine?: number\n endLine?: number\n severity?: \"info\" | \"warning\" | \"error\"\n}\n\nexport interface SecurityIssue {\n id: string\n line: number\n severity: \"low\" | \"medium\" | \"high\" | \"critical\"\n message: string\n fix?: string\n}\n\nexport interface AICodeProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n \"onChange\"\n> {\n value?: string\n defaultValue?: string\n language?: string\n theme?: \"dark\" | \"light\" | \"auto\"\n height?: string | number\n onGenerate?: (prompt: string) => Promise<string>\n onExplain?: (code: string, line?: number) => Promise<Explanation[]>\n onRefactor?: (code: string) => Promise<string>\n onDebug?: (code: string) => Promise<CodeSuggestion[]>\n onTest?: (code: string) => Promise<string>\n onDocument?: (code: string) => Promise<string>\n onReview?: (code: string) => Promise<CodeSuggestion[]>\n onSecurityScan?: (code: string) => Promise<SecurityIssue[]>\n onChange?: (value: string) => void\n features?: CodeFeature[]\n showMinimap?: boolean\n showLineNumbers?: boolean\n isGenerating?: boolean\n multiFileSupport?: boolean\n files?: { name: string; content: string; language?: string }[]\n activeFile?: string\n onFileChange?: (fileName: string) => void\n onFileCreate?: (fileName: string, content: string) => void\n placeholder?: string\n}\n\n// Component variants\nconst suggestionVariants = cva(\n \"relative rounded-lg border p-3 transition-colors\",\n {\n variants: {\n severity: {\n info: \"border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950\",\n warning:\n \"border-yellow-200 bg-yellow-50 dark:border-yellow-800 dark:bg-yellow-950\",\n error: \"border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950\",\n },\n },\n defaultVariants: {\n severity: \"info\",\n },\n }\n)\n\n// AI Chat Message for prompts\ninterface AIMessage {\n id: string\n content: string\n role: \"user\" | \"assistant\"\n timestamp: Date\n type?: \"prompt\" | \"code\" | \"explanation\"\n}\n\n// Feature icons mapping\nconst featureIcons = {\n generate: Wand2,\n explain: Eye,\n refactor: Code,\n debug: Bug,\n test: TestTube,\n document: FileText,\n review: GitCompare,\n}\n\n// Language detection patterns\nconst languagePatterns = {\n typescript: /\\.(tsx?|ts)$/,\n javascript: /\\.(jsx?|js|mjs)$/,\n python: /\\.py$/,\n java: /\\.java$/,\n csharp: /\\.cs$/,\n cpp: /\\.(cpp|cc|cxx)$/,\n c: /\\.c$/,\n go: /\\.go$/,\n rust: /\\.rs$/,\n php: /\\.php$/,\n ruby: /\\.rb$/,\n swift: /\\.swift$/,\n}\n\n// Security scanner component\nconst SecurityPanel = React.forwardRef<\n HTMLDivElement,\n {\n issues: SecurityIssue[]\n onFixIssue?: (issue: SecurityIssue) => void\n } & React.HTMLAttributes<HTMLDivElement>\n>(({ issues, onFixIssue, className, ...props }, ref) => (\n <Grid ref={ref} columns={1} gap={8} className={className} {...props}>\n <div className=\"flex items-center gap-2\">\n <Shield className=\"h-4 w-4\" />\n <span className=\"font-medium\">Security Analysis</span>\n {issues.length > 0 && (\n <Badge\n variant={\n issues.some((i) => i.severity === \"critical\")\n ? \"destructive\"\n : \"secondary\"\n }\n >\n {issues.length} issue{issues.length !== 1 ? \"s\" : \"\"}\n </Badge>\n )}\n </div>\n {issues.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">\n No security issues detected\n </p>\n ) : (\n <Grid columns={1} gap={8}>\n {issues.map((issue) => (\n <div\n key={issue.id}\n className={cn(\n \"rounded-lg border p-3 text-sm\",\n issue.severity === \"critical\" &&\n \"border-red-500 bg-red-50 dark:bg-red-950\",\n issue.severity === \"high\" &&\n \"border-orange-500 bg-orange-50 dark:bg-orange-950\",\n issue.severity === \"medium\" &&\n \"border-yellow-500 bg-yellow-50 dark:bg-yellow-950\",\n issue.severity === \"low\" &&\n \"border-blue-500 bg-blue-50 dark:bg-blue-950\"\n )}\n >\n <Grid\n columns={onFixIssue && issue.fix ? \"minmax(0, 1fr) auto\" : 1}\n gap={8}\n style={{ alignItems: \"start\" }}\n >\n <Grid columns={1} gap={4}>\n <div className=\"flex items-center gap-2\">\n <Badge\n variant={\n issue.severity === \"critical\" || issue.severity === \"high\"\n ? \"destructive\"\n : \"secondary\"\n }\n className=\"text-xs\"\n >\n {issue.severity.toUpperCase()}\n </Badge>\n <span className=\"text-xs text-muted-foreground\">\n Line {issue.line}\n </span>\n </div>\n <p>{issue.message}</p>\n {issue.fix && (\n <p className=\"text-xs text-muted-foreground\">\n Fix: {issue.fix}\n </p>\n )}\n </Grid>\n {onFixIssue && issue.fix && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onFixIssue(issue)}\n >\n Fix\n </Button>\n )}\n </Grid>\n </div>\n ))}\n </Grid>\n )}\n </Grid>\n))\nSecurityPanel.displayName = \"SecurityPanel\"\n\n// Suggestions panel component\nconst SuggestionsPanel = React.forwardRef<\n HTMLDivElement,\n {\n suggestions: CodeSuggestion[]\n onApplySuggestion?: (suggestion: CodeSuggestion) => void\n } & React.HTMLAttributes<HTMLDivElement>\n>(({ suggestions, onApplySuggestion, className, ...props }, ref) => (\n <Grid ref={ref} columns={1} gap={8} className={className} {...props}>\n <div className=\"flex items-center gap-2\">\n <Zap className=\"h-4 w-4\" />\n <span className=\"font-medium\">AI Suggestions</span>\n </div>\n {suggestions.length === 0 ? (\n <p className=\"text-sm text-muted-foreground\">No suggestions available</p>\n ) : (\n <Grid columns={1} gap={8}>\n {suggestions.map((suggestion) => {\n const Icon = featureIcons[suggestion.type] || Code\n return (\n <Grid\n key={suggestion.id}\n columns={1}\n gap={8}\n className={suggestionVariants({\n severity: suggestion.severity || \"info\",\n })}\n >\n <Grid\n columns={onApplySuggestion ? \"minmax(0, 1fr) auto\" : 1}\n gap={8}\n style={{ alignItems: \"start\" }}\n >\n <Grid columns={1} gap={4}>\n <div className=\"flex items-center gap-2\">\n <Icon className=\"h-4 w-4\" />\n <span className=\"font-medium text-sm\">\n {suggestion.title}\n </span>\n </div>\n <p className=\"text-sm text-muted-foreground\">\n {suggestion.description}\n </p>\n {suggestion.startLine && suggestion.endLine && (\n <span className=\"text-xs text-muted-foreground\">\n Lines {suggestion.startLine}-{suggestion.endLine}\n </span>\n )}\n </Grid>\n {onApplySuggestion && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onApplySuggestion(suggestion)}\n >\n Apply\n </Button>\n )}\n </Grid>\n {suggestion.code && (\n <div className=\"rounded bg-background/50 p-2\">\n <pre className=\"text-xs overflow-x-auto\">\n <code>{suggestion.code}</code>\n </pre>\n </div>\n )}\n </Grid>\n )\n })}\n </Grid>\n )}\n </Grid>\n))\nSuggestionsPanel.displayName = \"SuggestionsPanel\"\n\n// Chat panel for AI interaction\nconst ChatPanel = React.forwardRef<\n HTMLDivElement,\n {\n messages: AIMessage[]\n onSendMessage: (message: string) => void\n isGenerating?: boolean\n placeholder?: string\n } & React.HTMLAttributes<HTMLDivElement>\n>(\n (\n {\n messages,\n onSendMessage,\n isGenerating,\n placeholder = \"Ask AI about your code...\",\n className,\n ...props\n },\n ref\n ) => {\n const [input, setInput] = React.useState(\"\")\n const scrollRef = React.useRef<HTMLDivElement>(null)\n\n React.useEffect(() => {\n if (scrollRef.current) {\n scrollRef.current.scrollTop = scrollRef.current.scrollHeight\n }\n }, [messages])\n\n const handleSend = () => {\n if (!input.trim() || isGenerating) return\n onSendMessage(input)\n setInput(\"\")\n }\n\n const handleKeyPress = (e: React.KeyboardEvent) => {\n if (e.key === \"Enter\" && !e.shiftKey) {\n e.preventDefault()\n handleSend()\n }\n }\n\n return (\n <Grid\n ref={ref}\n columns={1}\n rows=\"auto minmax(0, 1fr) auto\"\n gap={0}\n className={cn(\"h-full\", className)}\n {...props}\n >\n <div className=\"flex items-center gap-2 p-3 border-b\">\n <MessageSquare className=\"h-4 w-4\" />\n <span className=\"font-medium\">AI Assistant</span>\n </div>\n\n <ScrollArea ref={scrollRef} className=\"p-3\">\n <Grid columns={1} gap={12}>\n {messages.map((message) => {\n const mine = message.role === \"user\"\n return (\n <Grid\n key={message.id}\n columns={mine ? \"minmax(0, 1fr) auto\" : \"auto minmax(0, 1fr)\"}\n gap={8}\n style={{ alignItems: \"start\" }}\n >\n {!mine && (\n <Avatar className=\"h-6 w-6\">\n <AvatarFallback>\n <Bot className=\"h-3 w-3\" />\n </AvatarFallback>\n </Avatar>\n )}\n <div\n className={cn(\n \"w-fit max-w-[80%] rounded-lg px-3 py-2 text-sm\",\n mine\n ? \"bg-primary text-primary-foreground ml-auto\"\n : \"bg-muted\"\n )}\n >\n {message.type === \"code\" ? (\n <pre className=\"overflow-x-auto\">\n <code>{message.content}</code>\n </pre>\n ) : (\n <p>{message.content}</p>\n )}\n </div>\n {mine && (\n <Avatar className=\"h-6 w-6\">\n <AvatarFallback>\n <User className=\"h-3 w-3\" />\n </AvatarFallback>\n </Avatar>\n )}\n </Grid>\n )\n })}\n {isGenerating && (\n <Grid\n columns=\"auto minmax(0, 1fr)\"\n gap={8}\n style={{ alignItems: \"start\" }}\n >\n <Avatar className=\"h-6 w-6\">\n <AvatarFallback>\n <Bot className=\"h-3 w-3\" />\n </AvatarFallback>\n </Avatar>\n <div className=\"bg-muted w-fit rounded-lg px-3 py-2 text-sm\">\n <div className=\"flex items-center gap-2\">\n <Loader2 className=\"h-3 w-3 animate-spin\" />\n <span>AI is thinking...</span>\n </div>\n </div>\n </Grid>\n )}\n </Grid>\n </ScrollArea>\n\n <div className=\"border-t p-3\">\n <Grid columns=\"minmax(0, 1fr) auto\" gap={8}>\n <Textarea\n value={input}\n onChange={(e) => setInput(e.target.value)}\n onKeyPress={handleKeyPress}\n placeholder={placeholder}\n disabled={isGenerating}\n className=\"min-h-[60px] resize-none\"\n rows={2}\n />\n <Button\n size=\"icon\"\n onClick={handleSend}\n disabled={!input.trim() || isGenerating}\n >\n <Send className=\"h-4 w-4\" />\n </Button>\n </Grid>\n </div>\n </Grid>\n )\n }\n)\nChatPanel.displayName = \"ChatPanel\"\n\n// File tabs component for multi-file support\nconst FileTabs = React.forwardRef<\n HTMLDivElement,\n {\n files: { name: string; content: string; language?: string }[]\n activeFile?: string\n onFileChange?: (fileName: string) => void\n onFileCreate?: (fileName: string) => void\n onFileDelete?: (fileName: string) => void\n } & React.HTMLAttributes<HTMLDivElement>\n>(\n (\n {\n files,\n activeFile,\n onFileChange,\n onFileCreate,\n onFileDelete,\n className,\n ...props\n },\n ref\n ) => {\n const [newFileName, setNewFileName] = React.useState(\"\")\n const [isCreating, setIsCreating] = React.useState(false)\n\n const handleCreateFile = () => {\n if (newFileName.trim() && onFileCreate) {\n onFileCreate(newFileName.trim())\n setNewFileName(\"\")\n setIsCreating(false)\n }\n }\n\n const getLanguageFromFile = (fileName: string) => {\n for (const [lang, pattern] of Object.entries(languagePatterns)) {\n if (pattern.test(fileName)) {\n return lang\n }\n }\n return \"plaintext\"\n }\n\n return (\n <Grid\n ref={ref}\n columns={onFileCreate ? \"minmax(0, 1fr) auto\" : 1}\n gap={4}\n className={cn(\"border-b bg-muted/30 px-2\", className)}\n style={{ alignItems: \"center\" }}\n {...props}\n >\n <ScrollArea>\n <div className=\"flex items-center gap-1 py-1\">\n {files.map((file) => (\n <Button\n key={file.name}\n variant={activeFile === file.name ? \"secondary\" : \"ghost\"}\n size=\"sm\"\n className=\"h-8 gap-2 rounded-t-md rounded-b-none\"\n onClick={() => onFileChange?.(file.name)}\n >\n <FileCode className=\"h-3 w-3\" />\n <span className=\"text-xs\">{file.name}</span>\n {onFileDelete && files.length > 1 && (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-4 w-4 hover:bg-destructive hover:text-destructive-foreground\"\n onClick={(e) => {\n e.stopPropagation()\n onFileDelete(file.name)\n }}\n >\n ×\n </Button>\n )}\n </Button>\n ))}\n </div>\n <ScrollBar orientation=\"horizontal\" />\n </ScrollArea>\n\n {onFileCreate && (\n <div className=\"flex items-center gap-1\">\n {isCreating ? (\n <div className=\"flex items-center gap-1\">\n <input\n type=\"text\"\n value={newFileName}\n onChange={(e) => setNewFileName(e.target.value)}\n placeholder=\"filename.ext\"\n className=\"h-6 w-24 rounded border px-2 text-xs\"\n onKeyPress={(e) => {\n if (e.key === \"Enter\") handleCreateFile()\n if (e.key === \"Escape\") setIsCreating(false)\n }}\n autoFocus\n />\n <Button\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={handleCreateFile}\n >\n <Check className=\"h-3 w-3\" />\n </Button>\n </div>\n ) : (\n <Button\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-6 w-6\"\n onClick={() => setIsCreating(true)}\n >\n +\n </Button>\n )}\n </div>\n )}\n </Grid>\n )\n }\n)\nFileTabs.displayName = \"FileTabs\"\n\n// Main AI Code component\nconst AICode = React.forwardRef<HTMLDivElement, AICodeProps>(\n (\n {\n value,\n defaultValue = \"\",\n language = \"typescript\",\n theme: themeProp = \"auto\",\n height = \"500px\",\n onGenerate,\n onExplain,\n onRefactor,\n onDebug,\n onTest,\n onDocument,\n onReview,\n onSecurityScan,\n onChange,\n features = [\n \"generate\",\n \"explain\",\n \"refactor\",\n \"debug\",\n \"test\",\n \"document\",\n \"review\",\n ],\n showMinimap = false,\n showLineNumbers = true,\n isGenerating = false,\n multiFileSupport = false,\n files = [{ name: \"main.ts\", content: defaultValue, language }],\n activeFile,\n onFileChange,\n onFileCreate,\n placeholder = \"Ask AI to generate code or explain existing code...\",\n className,\n ...props\n },\n ref\n ) => {\n const { theme: systemTheme } = useTheme()\n const [editorValue, setEditorValue] = React.useState(value || defaultValue)\n const [currentLanguage, setCurrentLanguage] = React.useState(language)\n const [explanations, setExplanations] = React.useState<Explanation[]>([])\n const [suggestions, setSuggestions] = React.useState<CodeSuggestion[]>([])\n const [securityIssues, setSecurityIssues] = React.useState<SecurityIssue[]>(\n []\n )\n const [messages, setMessages] = React.useState<AIMessage[]>([])\n const [currentFiles, setCurrentFiles] = React.useState(files)\n const [currentActiveFile, setCurrentActiveFile] = React.useState(\n activeFile || files[0]?.name\n )\n const [showSidePanel, setShowSidePanel] = React.useState(true)\n const [sidePanelTab, setSidePanelTab] = React.useState<\n \"chat\" | \"suggestions\" | \"security\"\n >(\"chat\")\n const [isProcessing, setIsProcessing] = React.useState(false)\n const [progress, setProgress] = React.useState(0)\n\n const editorRef = React.useRef<any>(null)\n\n // Get current file content\n const currentFile = React.useMemo(() => {\n return (\n currentFiles.find((f) => f.name === currentActiveFile) ||\n currentFiles[0]\n )\n }, [currentFiles, currentActiveFile])\n\n // Update editor value when file changes\n React.useEffect(() => {\n if (currentFile) {\n setEditorValue(currentFile.content)\n setCurrentLanguage(\n currentFile.language || getLanguageFromFile(currentFile.name)\n )\n }\n }, [currentFile])\n\n // External value changes\n React.useEffect(() => {\n if (value !== undefined) {\n setEditorValue(value)\n }\n }, [value])\n\n const resolveTheme = React.useMemo(() => {\n if (themeProp === \"auto\") {\n return systemTheme === \"dark\" ? \"vs-dark\" : \"light\"\n }\n return themeProp === \"dark\" ? \"vs-dark\" : \"light\"\n }, [themeProp, systemTheme])\n\n const getLanguageFromFile = React.useCallback((fileName: string) => {\n for (const [lang, pattern] of Object.entries(languagePatterns)) {\n if (pattern.test(fileName)) {\n return lang\n }\n }\n return \"plaintext\"\n }, [])\n\n // Handle editor changes\n const handleEditorChange: OnChange = React.useCallback(\n (newValue) => {\n const content = newValue || \"\"\n setEditorValue(content)\n onChange?.(content)\n\n // Update current file content\n if (multiFileSupport && currentActiveFile) {\n setCurrentFiles((prev) =>\n prev.map((f) =>\n f.name === currentActiveFile ? { ...f, content } : f\n )\n )\n }\n },\n [onChange, multiFileSupport, currentActiveFile]\n )\n\n // Handle editor mount\n const handleEditorMount: OnMount = React.useCallback(\n (editor, monaco) => {\n editorRef.current = editor\n\n // Setup hover providers for explanations\n monaco.languages.registerHoverProvider(currentLanguage, {\n provideHover: (model: any, position: any) => {\n const line = position.lineNumber\n const explanation = explanations.find((e) => e.line === line)\n\n if (explanation) {\n return {\n range: new monaco.Range(line, 1, line, 1),\n contents: [\n { value: `**${explanation.type.toUpperCase()}**` },\n { value: explanation.content },\n ],\n }\n }\n return null\n },\n })\n\n // Setup code actions for suggestions\n monaco.languages.registerCodeActionProvider(currentLanguage, {\n provideCodeActions: (\n model: any,\n range: any,\n context: any,\n token: any\n ) => {\n const actions = suggestions\n .filter(\n (s) =>\n s.startLine &&\n s.endLine &&\n range.startLineNumber >= s.startLine &&\n range.endLineNumber <= s.endLine\n )\n .map((suggestion) => ({\n title: suggestion.title,\n kind: \"quickfix\",\n edit: suggestion.code\n ? {\n edits: [\n {\n resource: model.uri,\n edit: {\n range: new monaco.Range(\n suggestion.startLine!,\n 1,\n suggestion.endLine!,\n model.getLineMaxColumn(suggestion.endLine!)\n ),\n text: suggestion.code,\n },\n },\n ],\n }\n : undefined,\n }))\n\n return { actions, dispose: () => {} } as any\n },\n })\n },\n [currentLanguage, explanations, suggestions]\n )\n\n // AI feature handlers\n const handleGenerate = React.useCallback(\n async (prompt: string) => {\n if (!onGenerate) return\n\n setIsProcessing(true)\n setProgress(0)\n\n try {\n // Simulate progress\n const progressInterval = setInterval(() => {\n setProgress((prev) => Math.min(prev + 10, 90))\n }, 200)\n\n const newMessage: AIMessage = {\n id: Date.now().toString(),\n content: prompt,\n role: \"user\",\n timestamp: new Date(),\n type: \"prompt\",\n }\n setMessages((prev) => [...prev, newMessage])\n\n const generatedCode = await onGenerate(prompt)\n\n clearInterval(progressInterval)\n setProgress(100)\n\n setEditorValue(generatedCode)\n onChange?.(generatedCode)\n\n const responseMessage: AIMessage = {\n id: (Date.now() + 1).toString(),\n content: generatedCode,\n role: \"assistant\",\n timestamp: new Date(),\n type: \"code\",\n }\n setMessages((prev) => [...prev, responseMessage])\n\n // Update current file\n if (multiFileSupport && currentActiveFile) {\n setCurrentFiles((prev) =>\n prev.map((f) =>\n f.name === currentActiveFile\n ? { ...f, content: generatedCode }\n : f\n )\n )\n }\n } catch (error) {\n console.error(\"Failed to generate code:\", error)\n toast.error(\"Failed to generate code\", {\n description: \"Please try again\",\n action: {\n label: \"Retry\",\n onClick: () => handleGenerate(prompt),\n },\n })\n } finally {\n setIsProcessing(false)\n setProgress(0)\n }\n },\n [onGenerate, onChange, multiFileSupport, currentActiveFile]\n )\n\n const handleExplain = React.useCallback(\n async (selectedLine?: number) => {\n if (!onExplain) return\n\n setIsProcessing(true)\n try {\n const explanationsResult = await onExplain(editorValue, selectedLine)\n setExplanations(explanationsResult)\n setSidePanelTab(\"suggestions\")\n } catch (error) {\n console.error(\"Failed to explain code:\", error)\n toast.error(\"Failed to explain code\", {\n description: \"Please try again\",\n })\n } finally {\n setIsProcessing(false)\n }\n },\n [onExplain, editorValue]\n )\n\n const handleFeatureAction = React.useCallback(\n async (feature: CodeFeature) => {\n if (isProcessing) return\n\n setIsProcessing(true)\n try {\n switch (feature) {\n case \"refactor\":\n if (onRefactor) {\n const refactoredCode = await onRefactor(editorValue)\n setEditorValue(refactoredCode)\n onChange?.(refactoredCode)\n }\n break\n\n case \"debug\":\n if (onDebug) {\n const debugSuggestions = await onDebug(editorValue)\n setSuggestions(debugSuggestions)\n setSidePanelTab(\"suggestions\")\n }\n break\n\n case \"test\":\n if (onTest) {\n const testCode = await onTest(editorValue)\n // Handle test generation result\n const testMessage: AIMessage = {\n id: Date.now().toString(),\n content: testCode,\n role: \"assistant\",\n timestamp: new Date(),\n type: \"code\",\n }\n setMessages((prev) => [...prev, testMessage])\n setSidePanelTab(\"chat\")\n }\n break\n\n case \"document\":\n if (onDocument) {\n const documentation = await onDocument(editorValue)\n const docMessage: AIMessage = {\n id: Date.now().toString(),\n content: documentation,\n role: \"assistant\",\n timestamp: new Date(),\n type: \"explanation\",\n }\n setMessages((prev) => [...prev, docMessage])\n setSidePanelTab(\"chat\")\n }\n break\n\n case \"review\":\n if (onReview) {\n const reviewSuggestions = await onReview(editorValue)\n setSuggestions(reviewSuggestions)\n setSidePanelTab(\"suggestions\")\n }\n break\n }\n } catch (error) {\n console.error(`Failed to ${feature}:`, error)\n } finally {\n setIsProcessing(false)\n }\n },\n [\n isProcessing,\n editorValue,\n onRefactor,\n onDebug,\n onTest,\n onDocument,\n onReview,\n onChange,\n ]\n )\n\n const handleSecurityScan = React.useCallback(async () => {\n if (!onSecurityScan) return\n\n setIsProcessing(true)\n try {\n const issues = await onSecurityScan(editorValue)\n setSecurityIssues(issues)\n setSidePanelTab(\"security\")\n } catch (error) {\n console.error(\"Failed to scan for security issues:\", error)\n toast.error(\"Security scan failed\", {\n description: \"Please try again\",\n })\n } finally {\n setIsProcessing(false)\n }\n }, [onSecurityScan, editorValue])\n\n // File management\n const handleFileChange = React.useCallback(\n (fileName: string) => {\n setCurrentActiveFile(fileName)\n onFileChange?.(fileName)\n },\n [onFileChange]\n )\n\n const handleFileCreate = React.useCallback(\n (fileName: string) => {\n const newFile = {\n name: fileName,\n content: \"\",\n language: getLanguageFromFile(fileName),\n }\n setCurrentFiles((prev) => [...prev, newFile])\n setCurrentActiveFile(fileName)\n onFileCreate?.(fileName, \"\")\n },\n [getLanguageFromFile, onFileCreate]\n )\n\n const handleFileDelete = React.useCallback(\n (fileName: string) => {\n if (currentFiles.length <= 1) return\n\n setCurrentFiles((prev) => prev.filter((f) => f.name !== fileName))\n\n if (currentActiveFile === fileName) {\n const remainingFiles = currentFiles.filter((f) => f.name !== fileName)\n setCurrentActiveFile(remainingFiles[0]?.name)\n }\n },\n [currentFiles, currentActiveFile]\n )\n\n return (\n <TooltipProvider>\n <Grid\n ref={ref}\n columns={1}\n rows={\n multiFileSupport\n ? \"auto auto minmax(0, 1fr)\"\n : \"auto minmax(0, 1fr)\"\n }\n gap={0}\n className={cn(\n \"h-full w-full overflow-hidden rounded-lg border bg-background\",\n className\n )}\n {...props}\n >\n {/* Header with feature buttons */}\n <div className=\"flex items-center justify-between border-b bg-muted/50 p-2\">\n <div className=\"flex items-center gap-1\">\n {features.map((feature) => {\n const Icon = featureIcons[feature]\n const isActive =\n feature === \"explain\" && explanations.length > 0\n\n return (\n <Tooltip key={feature}>\n <TooltipTrigger asChild>\n <Button\n variant={isActive ? \"secondary\" : \"ghost\"}\n size=\"sm\"\n className=\"h-8 gap-1.5 px-2\"\n onClick={() => {\n if (feature === \"explain\") {\n handleExplain()\n } else {\n handleFeatureAction(feature)\n }\n }}\n disabled={isProcessing}\n >\n <Icon className=\"h-3 w-3\" />\n <span className=\"text-xs capitalize\">{feature}</span>\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n <p>AI {feature} assistance</p>\n </TooltipContent>\n </Tooltip>\n )\n })}\n\n <Separator orientation=\"vertical\" className=\"h-4\" />\n\n <Tooltip>\n <TooltipTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8 gap-1.5 px-2\"\n onClick={handleSecurityScan}\n disabled={isProcessing}\n >\n <Shield className=\"h-3 w-3\" />\n <span className=\"text-xs\">Security</span>\n </Button>\n </TooltipTrigger>\n <TooltipContent>\n <p>Scan for security vulnerabilities</p>\n </TooltipContent>\n </Tooltip>\n </div>\n\n <div className=\"flex items-center gap-2\">\n {isProcessing && (\n <div className=\"flex items-center gap-2\">\n <Loader2 className=\"h-3 w-3 animate-spin\" />\n <span className=\"text-xs text-muted-foreground\">\n Processing...\n </span>\n {progress > 0 && (\n <Progress value={progress} className=\"w-16 h-1\" />\n )}\n </div>\n )}\n\n <Button\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-8\"\n onClick={() => setShowSidePanel(!showSidePanel)}\n >\n {showSidePanel ? (\n <EyeOff className=\"h-3 w-3\" />\n ) : (\n <Eye className=\"h-3 w-3\" />\n )}\n </Button>\n\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"sm\" className=\"h-8 gap-1\">\n <Settings className=\"h-3 w-3\" />\n <ChevronDown className=\"h-3 w-3\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuCheckboxItem\n checked={showMinimap}\n onCheckedChange={(checked) => {\n // Handle minimap toggle - would need to be passed as prop\n }}\n >\n Show Minimap\n </DropdownMenuCheckboxItem>\n <DropdownMenuCheckboxItem\n checked={showLineNumbers}\n onCheckedChange={(checked) => {\n // Handle line numbers toggle - would need to be passed as prop\n }}\n >\n Show Line Numbers\n </DropdownMenuCheckboxItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem>\n <Download className=\"mr-2 h-3 w-3\" />\n Export Code\n </DropdownMenuItem>\n <DropdownMenuItem>\n <Upload className=\"mr-2 h-3 w-3\" />\n Import Code\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n </div>\n </div>\n\n {/* File tabs for multi-file support */}\n {multiFileSupport && (\n <FileTabs\n files={currentFiles}\n activeFile={currentActiveFile}\n onFileChange={handleFileChange}\n onFileCreate={onFileCreate ? handleFileCreate : undefined}\n onFileDelete={\n currentFiles.length > 1 ? handleFileDelete : undefined\n }\n />\n )}\n\n {/* Main content area */}\n <ResizablePanelGroup direction=\"horizontal\">\n {/* Code editor panel */}\n <ResizablePanel defaultSize={showSidePanel ? 70 : 100} minSize={30}>\n <div className=\"h-full\">\n <Editor\n height={height}\n language={currentLanguage}\n value={editorValue}\n theme={resolveTheme}\n onChange={handleEditorChange}\n onMount={handleEditorMount}\n options={{\n readOnly: isGenerating || isProcessing,\n lineNumbers: showLineNumbers ? \"on\" : \"off\",\n minimap: { enabled: showMinimap },\n wordWrap: \"on\",\n fontSize: 14,\n scrollBeyondLastLine: false,\n automaticLayout: true,\n tabSize: 2,\n insertSpaces: true,\n formatOnPaste: true,\n formatOnType: true,\n quickSuggestions: true,\n parameterHints: { enabled: true },\n suggestOnTriggerCharacters: true,\n acceptSuggestionOnEnter: \"on\",\n accessibilitySupport: \"auto\",\n scrollbar: {\n vertical: \"auto\",\n horizontal: \"auto\",\n verticalScrollbarSize: 10,\n horizontalScrollbarSize: 10,\n },\n padding: {\n top: 16,\n bottom: 16,\n },\n }}\n />\n </div>\n </ResizablePanel>\n\n {/* Side panel */}\n {showSidePanel && (\n <>\n <ResizableHandle />\n <ResizablePanel defaultSize={30} minSize={20} maxSize={50}>\n <Tabs\n value={sidePanelTab}\n onValueChange={(value) => setSidePanelTab(value as any)}\n >\n <TabsList className=\"grid w-full grid-cols-3\">\n <TabsTrigger value=\"chat\" className=\"text-xs\">\n <MessageSquare className=\"h-3 w-3 mr-1\" />\n Chat\n </TabsTrigger>\n <TabsTrigger value=\"suggestions\" className=\"text-xs\">\n <Zap className=\"h-3 w-3 mr-1\" />\n AI\n {suggestions.length > 0 && (\n <Badge\n variant=\"secondary\"\n className=\"ml-1 h-4 text-xs\"\n >\n {suggestions.length}\n </Badge>\n )}\n </TabsTrigger>\n <TabsTrigger value=\"security\" className=\"text-xs\">\n <Shield className=\"h-3 w-3 mr-1\" />\n Security\n {securityIssues.length > 0 && (\n <Badge\n variant={\n securityIssues.some(\n (i) => i.severity === \"critical\"\n )\n ? \"destructive\"\n : \"secondary\"\n }\n className=\"ml-1 h-4 text-xs\"\n >\n {securityIssues.length}\n </Badge>\n )}\n </TabsTrigger>\n </TabsList>\n\n <TabsContent value=\"chat\" className=\"h-[calc(100%-40px)]\">\n <ChatPanel\n messages={messages}\n onSendMessage={handleGenerate}\n isGenerating={isGenerating || isProcessing}\n placeholder={placeholder}\n />\n </TabsContent>\n\n <TabsContent\n value=\"suggestions\"\n className=\"h-[calc(100%-40px)] overflow-hidden\"\n >\n <ScrollArea className=\"h-full p-3\">\n <SuggestionsPanel\n suggestions={suggestions}\n onApplySuggestion={(suggestion) => {\n if (suggestion.code) {\n setEditorValue(suggestion.code)\n onChange?.(suggestion.code)\n }\n }}\n />\n </ScrollArea>\n </TabsContent>\n\n <TabsContent\n value=\"security\"\n className=\"h-[calc(100%-40px)] overflow-hidden\"\n >\n <ScrollArea className=\"h-full p-3\">\n <SecurityPanel\n issues={securityIssues}\n onFixIssue={(issue) => {\n if (issue.fix && editorRef.current) {\n // Apply security fix to editor\n const model = editorRef.current.getModel()\n if (model) {\n const line = model.getLineContent(issue.line)\n const newLine = issue.fix\n editorRef.current.executeEdits(\"security-fix\", [\n {\n range: {\n startLineNumber: issue.line,\n startColumn: 1,\n endLineNumber: issue.line,\n endColumn: line.length + 1,\n },\n text: newLine,\n },\n ])\n }\n }\n }}\n />\n </ScrollArea>\n </TabsContent>\n </Tabs>\n </ResizablePanel>\n </>\n )}\n </ResizablePanelGroup>\n </Grid>\n </TooltipProvider>\n )\n }\n)\n\nAICode.displayName = \"AICode\"\n\nexport { AICode }\n"
}
],
"type": "components:ai"
}