mirror of
https://github.com/hanzoai/shadcn.git
synced 2026-11-03 03:18:52 +00:00
14 lines
44 KiB
JSON
14 lines
44 KiB
JSON
{
|
||
"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"
|
||
} |