From 67d2e063e021ab6ace61a14f683f510050b549e4 Mon Sep 17 00:00:00 2001 From: Amin Khorramii Date: Mon, 4 Aug 2025 12:19:50 +0200 Subject: [PATCH] python prompt notebook in progress --- .../components/tabs/notebooks/CellDivider.tsx | 14 +- .../tabs/notebooks/NotebooksWorkspace.tsx | 35 +- .../components/tabs/notebooks/PromptCell.tsx | 813 ++++++++++++++++++ frontend/src/hooks/notebooks/useNotebookAI.ts | 274 ++++++ .../src/hooks/notebooks/useNotebookContext.ts | 346 ++++++++ frontend/src/lib/python/types.ts | 8 +- 6 files changed, 1479 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/tabs/notebooks/PromptCell.tsx create mode 100644 frontend/src/hooks/notebooks/useNotebookAI.ts create mode 100644 frontend/src/hooks/notebooks/useNotebookContext.ts diff --git a/frontend/src/components/tabs/notebooks/CellDivider.tsx b/frontend/src/components/tabs/notebooks/CellDivider.tsx index 581474d..3bb876b 100644 --- a/frontend/src/components/tabs/notebooks/CellDivider.tsx +++ b/frontend/src/components/tabs/notebooks/CellDivider.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { Code2, Type } from "lucide-react"; +import { Code2, Type, Sparkles } from "lucide-react"; import { usePythonStore } from "@/store/pythonStore"; interface CellDividerProps { @@ -14,7 +14,7 @@ const CellDivider: React.FC = ({ insertIndex, isLastCell = fal const { createCell } = usePythonStore(); const [isHovered, setIsHovered] = useState(false); - const handleCreateCell = (type: 'code' | 'markdown') => { + const handleCreateCell = (type: 'code' | 'markdown' | 'prompt') => { createCell(type, "", insertIndex); }; @@ -45,12 +45,20 @@ const CellDivider: React.FC = ({ insertIndex, isLastCell = fal + ); diff --git a/frontend/src/components/tabs/notebooks/NotebooksWorkspace.tsx b/frontend/src/components/tabs/notebooks/NotebooksWorkspace.tsx index 5054999..13ff40d 100644 --- a/frontend/src/components/tabs/notebooks/NotebooksWorkspace.tsx +++ b/frontend/src/components/tabs/notebooks/NotebooksWorkspace.tsx @@ -16,6 +16,7 @@ import { Clock, AlertCircle, RefreshCw, + Sparkles, } from 'lucide-react'; import { usePythonStore } from '@/store/pythonStore'; @@ -35,6 +36,7 @@ import { } from '@/utils/notebookExport'; import PythonCell from './PythonCell'; +import PromptCell from './PromptCell'; import CellDivider from './CellDivider'; import ScriptHistory from './ScriptHistory'; import PackageManager from './PackageManager'; @@ -617,6 +619,18 @@ const NotebooksWorkspace: React.FC = () => { + + + + {
{cells.map((cell, index) => ( - setActiveCellId(cell.id)} - cellNumber={index + 1} - /> + {cell.type === 'prompt' ? ( + setActiveCellId(cell.id)} + cellNumber={index + 1} + /> + ) : ( + setActiveCellId(cell.id)} + cellNumber={index + 1} + /> + )} {/* Cell Divider - always show after each cell */} void; + cellNumber: number; +} + +const PromptCell: React.FC = ({ + cell, + isActive, + onActivate, + cellNumber, +}) => { + const { + updateCell, + deleteCell, + moveCell, + toggleCellInputCollapse, + createCell, + executeCell, + cells, + } = usePythonStore(); + + // Helper function to update prompt cell properties + const updatePromptCell = (cellId: string, code: string, additionalProps?: Partial) => { + const state = usePythonStore.getState(); + const updatedCells = state.cells.map((c) => + c.id === cellId + ? { ...c, code, updatedAt: new Date(), ...additionalProps } + : c + ); + usePythonStore.setState({ cells: updatedCells }); + // Mark as unsaved + state.markAsUnsaved(); + }; + + // Helper function to split generated code into executable blocks + const splitCodeIntoBlocks = (code: string): string[] => { + if (!code) return []; + + // Split by comment separator or natural code blocks + const blocks = code.split(/\n\n# --- Next Code Block ---\n\n/); + + // Further split by logical breaks (imports, function definitions, etc.) + const splitBlocks: string[] = []; + + blocks.forEach(block => { + const lines = block.split('\n'); + let currentBlock: string[] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + const trimmedLine = line.trim(); + + // Check if this line should start a new block + const isNewBlockTrigger = + (trimmedLine.startsWith('import ') || trimmedLine.startsWith('from ')) && currentBlock.length > 0 || + (trimmedLine.startsWith('def ') || trimmedLine.startsWith('class ')) && currentBlock.length > 0 || + (trimmedLine.startsWith('# ') && trimmedLine.length > 10) && currentBlock.length > 0; + + if (isNewBlockTrigger) { + // Save current block and start new one + if (currentBlock.length > 0) { + splitBlocks.push(currentBlock.join('\n').trim()); + currentBlock = []; + } + } + + currentBlock.push(line); + } + + // Add remaining block + if (currentBlock.length > 0) { + splitBlocks.push(currentBlock.join('\n').trim()); + } + }); + + return splitBlocks.filter(block => block.trim().length > 0); + }; + + // Create executable code cells from generated code + const createCodeCells = () => { + if (!cell.generatedCode) return; + + const codeBlocks = splitCodeIntoBlocks(cell.generatedCode); + if (codeBlocks.length === 0) return; + + // Find the current cell index + const currentIndex = cells.findIndex(c => c.id === cell.id); + if (currentIndex === -1) return; + + // Create cells after the current prompt cell + codeBlocks.forEach((code, index) => { + const insertIndex = currentIndex + 1 + index; + createCell('code', code, insertIndex); + }); + }; + + // Execute all generated code cells + const executeGeneratedCode = async () => { + if (!cell.generatedCode) return; + + // First create the code cells + createCodeCells(); + + // Wait a bit for cells to be created, then execute them + setTimeout(async () => { + const currentIndex = cells.findIndex(c => c.id === cell.id); + if (currentIndex === -1) return; + + const codeBlocks = splitCodeIntoBlocks(cell.generatedCode || ''); + + // Execute each created cell sequentially + for (let i = 0; i < codeBlocks.length; i++) { + const cellIndex = currentIndex + 1 + i; + const updatedCells = usePythonStore.getState().cells; + + if (cellIndex < updatedCells.length) { + const cellToExecute = updatedCells[cellIndex]; + try { + await executeCell(cellToExecute.id); + // Small delay between executions + await new Promise(resolve => setTimeout(resolve, 500)); + } catch (error) { + console.error(`Error executing cell ${i + 1}:`, error); + // Continue with next cell even if one fails + } + } + } + }, 100); + }; + + const { isAuthenticated } = useAuth(); + const { + context, + getAIContext, + refreshContext, + toggleTableSelection, + toggleVariableSelection, + toggleAllTables, + toggleAllVariables + } = useNotebookContext(); + + const { status: aiStatus, processAIRequest, resetStatus } = useNotebookAI(); + const [showMenu, setShowMenu] = useState(false); + const [showAuthModal, setShowAuthModal] = useState(false); + const [showContext, setShowContext] = useState(false); + const menuRef = useRef(null); + const cellRef = useRef(null); + const textareaRef = useRef(null); + + // Auto-resize textarea + const adjustTextareaHeight = useCallback(() => { + const textarea = textareaRef.current; + if (textarea) { + textarea.style.height = 'auto'; + textarea.style.height = `${Math.max(120, textarea.scrollHeight)}px`; + } + }, []); + + const handleInputChange = (e: React.ChangeEvent) => { + updateCell(cell.id, e.target.value); + adjustTextareaHeight(); + }; + + const handleCodeChange = (value: string | undefined) => { + updateCell(cell.id, value || ''); + }; + + // Parse AI response to show code blocks and explanations separately + const parseAIOutput = (response: string) => { + if (!response) return []; + + const parts = []; + const lines = response.split('\n'); + let currentPart = { type: 'text', content: '' }; + let inCodeBlock = false; + + for (const line of lines) { + if (line.trim().startsWith('```python')) { + // Starting a code block + if (currentPart.content.trim()) { + parts.push(currentPart); + } + currentPart = { type: 'code', content: '' }; + inCodeBlock = true; + } else if (line.trim() === '```' && inCodeBlock) { + // Ending a code block + parts.push(currentPart); + currentPart = { type: 'text', content: '' }; + inCodeBlock = false; + } else { + // Regular line + if (currentPart.content) { + currentPart.content += '\n'; + } + currentPart.content += line; + } + } + + if (currentPart.content.trim()) { + parts.push(currentPart); + } + + return parts.filter(part => part.content.trim()); + }; + + const handleExecutePrompt = async () => { + if (!cell.code.trim()) return; + + // Phase 2: Authentication check + if (!isAuthenticated) { + setShowAuthModal(true); + return; + } + + // Phase 3: Get current context + const aiContext = getAIContext(); + + // Mark cell as processing + updatePromptCell(cell.id, cell.code, { + isProcessing: true, + generatedCode: undefined, + aiResponse: undefined, + }); + + try { + // Phase 4: Real AI processing with DataKit + console.log('Processing AI request with context:', aiContext); + + const aiResponse = await processAIRequest( + { + prompt: cell.code, + context: aiContext, + }, + // Streaming callback for real-time updates + (chunk) => { + updatePromptCell(cell.id, cell.code, { + isProcessing: true, + aiResponse: aiStatus.currentResponse, + }); + } + ); + + if (aiResponse.error) { + // Handle AI errors + updatePromptCell(cell.id, cell.code, { + isProcessing: false, + aiResponse: `āŒ AI Error: ${aiResponse.error}\n\nPlease try again or check your connection.`, + }); + return; + } + + // Update cell with successful AI response - put code directly in the cell + updatePromptCell(cell.id, aiResponse.generatedCode || cell.code, { + isProcessing: false, + generatedCode: aiResponse.generatedCode, + aiResponse: aiResponse.explanation || 'AI processing completed.', + }); + + // Log usage information + if (aiResponse.tokensUsed || aiResponse.creditsUsed) { + console.log('AI Usage:', { + tokens: aiResponse.tokensUsed, + credits: aiResponse.creditsUsed, + }); + } + + } catch (error) { + console.error('Error executing AI prompt:', error); + updatePromptCell(cell.id, cell.code, { + isProcessing: false, + aiResponse: `āŒ Error: ${error instanceof Error ? error.message : 'Unknown error'}\n\nPlease try again.`, + }); + } + }; + + const handleAuthSuccess = () => { + setShowAuthModal(false); + // Reset AI status after auth + resetStatus(); + // After successful auth, automatically execute the prompt + setTimeout(() => { + handleExecutePrompt(); + }, 100); + }; + + const handleCopyPrompt = () => { + navigator.clipboard.writeText(cell.code); + setShowMenu(false); + }; + + // Auto-adjust height on mount and content change + React.useEffect(() => { + adjustTextareaHeight(); + }, [cell.code, adjustTextareaHeight]); + + // Close menu when clicking outside + React.useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (menuRef.current && !menuRef.current.contains(event.target as Node)) { + setShowMenu(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + return ( +
+ {/* Cell Header */} +
+
+ {/* Collapse/Expand Input Button */} + + + {/* Cell Number with AI Icon */} +
+ + Prompt + + {cellNumber} + +
+ + {/* Processing Status */} + {cell.isProcessing && ( +
+
+ {aiStatus.currentResponse ? 'AI responding...' : 'AI thinking...'} +
+ )} + + {/* Context Status */} + {isAuthenticated && !cell.isProcessing && ( +
+ + + {/* Code Ready Indicator */} + {cell.generatedCode && ( +
+ + Code generated +
+ )} +
+ )} + + {/* Authentication Status */} + {!isAuthenticated && ( + + )} +
+ +
+ {/* Execute Button */} + + + + + {/* Delete Button */} + + + {/* Menu Button */} +
+ + + {/* Dropdown Menu */} + {showMenu && ( +
+ + +
+ + +
+ )} +
+
+
+ + {/* Cell Input Area */} + {!cell.isInputCollapsed && ( +
+ {/* Show original prompt as textarea when no code is generated, otherwise show Monaco editor */} + {!cell.generatedCode ? ( +
+