Spaces:
Build error
Build error
| import { NextRequest, NextResponse } from 'next/server'; | |
| import type { SandboxState } from '@/types/sandbox'; | |
| import type { ConversationState } from '@/types/conversation'; | |
| declare global { | |
| var conversationState: ConversationState | null; | |
| } | |
| interface ParsedResponse { | |
| explanation: string; | |
| template: string; | |
| files: Array<{ path: string; content: string }>; | |
| packages: string[]; | |
| commands: string[]; | |
| structure: string | null; | |
| } | |
| function parseAIResponse(response: string): ParsedResponse { | |
| const sections = { | |
| files: [] as Array<{ path: string; content: string }>, | |
| commands: [] as string[], | |
| packages: [] as string[], | |
| structure: null as string | null, | |
| explanation: '', | |
| template: '' | |
| }; | |
| // Parse file sections - handle duplicates and prefer complete versions | |
| const fileMap = new Map<string, { content: string; isComplete: boolean }>(); | |
| const fileRegex = /<file path="([^"]+)">([\s\S]*?)(?:<\/file>|$)/g; | |
| let match; | |
| while ((match = fileRegex.exec(response)) !== null) { | |
| const filePath = match[1]; | |
| const content = match[2].trim(); | |
| const hasClosingTag = response.substring(match.index, match.index + match[0].length).includes('</file>'); | |
| // Check if this file already exists in our map | |
| const existing = fileMap.get(filePath); | |
| // Decide whether to keep this version | |
| let shouldReplace = false; | |
| if (!existing) { | |
| shouldReplace = true; // First occurrence | |
| } else if (!existing.isComplete && hasClosingTag) { | |
| shouldReplace = true; // Replace incomplete with complete | |
| console.log(`[parseAIResponse] Replacing incomplete ${filePath} with complete version`); | |
| } else if (existing.isComplete && hasClosingTag && content.length > existing.content.length) { | |
| shouldReplace = true; // Replace with longer complete version | |
| console.log(`[parseAIResponse] Replacing ${filePath} with longer complete version`); | |
| } else if (!existing.isComplete && !hasClosingTag && content.length > existing.content.length) { | |
| shouldReplace = true; // Both incomplete, keep longer one | |
| } | |
| if (shouldReplace) { | |
| // Additional validation: reject obviously broken content | |
| if (content.includes('...') && !content.includes('...props') && !content.includes('...rest')) { | |
| console.warn(`[parseAIResponse] Warning: ${filePath} contains ellipsis, may be truncated`); | |
| // Still use it if it's the only version we have | |
| if (!existing) { | |
| fileMap.set(filePath, { content, isComplete: hasClosingTag }); | |
| } | |
| } else { | |
| fileMap.set(filePath, { content, isComplete: hasClosingTag }); | |
| } | |
| } | |
| } | |
| // Convert map to array for sections.files | |
| for (const [path, { content, isComplete }] of fileMap.entries()) { | |
| if (!isComplete) { | |
| console.log(`[parseAIResponse] Warning: File ${path} appears to be truncated (no closing tag)`); | |
| } | |
| sections.files.push({ | |
| path, | |
| content | |
| }); | |
| } | |
| // Parse commands | |
| const cmdRegex = /<command>(.*?)<\/command>/g; | |
| while ((match = cmdRegex.exec(response)) !== null) { | |
| sections.commands.push(match[1].trim()); | |
| } | |
| // Parse packages - support both <package> and <packages> tags | |
| const pkgRegex = /<package>(.*?)<\/package>/g; | |
| while ((match = pkgRegex.exec(response)) !== null) { | |
| sections.packages.push(match[1].trim()); | |
| } | |
| // Also parse <packages> tag with multiple packages | |
| const packagesRegex = /<packages>([\s\S]*?)<\/packages>/; | |
| const packagesMatch = response.match(packagesRegex); | |
| if (packagesMatch) { | |
| const packagesContent = packagesMatch[1].trim(); | |
| // Split by newlines or commas | |
| const packagesList = packagesContent.split(/[\n,]+/) | |
| .map(pkg => pkg.trim()) | |
| .filter(pkg => pkg.length > 0); | |
| sections.packages.push(...packagesList); | |
| } | |
| // Parse structure | |
| const structureMatch = /<structure>([\s\S]*?)<\/structure>/; | |
| const structResult = response.match(structureMatch); | |
| if (structResult) { | |
| sections.structure = structResult[1].trim(); | |
| } | |
| // Parse explanation | |
| const explanationMatch = /<explanation>([\s\S]*?)<\/explanation>/; | |
| const explResult = response.match(explanationMatch); | |
| if (explResult) { | |
| sections.explanation = explResult[1].trim(); | |
| } | |
| // Parse template | |
| const templateMatch = /<template>(.*?)<\/template>/; | |
| const templResult = response.match(templateMatch); | |
| if (templResult) { | |
| sections.template = templResult[1].trim(); | |
| } | |
| return sections; | |
| } | |
| declare global { | |
| var activeSandbox: any; | |
| var existingFiles: Set<string>; | |
| var sandboxState: SandboxState; | |
| } | |
| export async function POST(request: NextRequest) { | |
| try { | |
| const { response, isEdit = false, packages = [] } = await request.json(); | |
| if (!response) { | |
| return NextResponse.json({ | |
| error: 'response is required' | |
| }, { status: 400 }); | |
| } | |
| // Parse the AI response | |
| const parsed = parseAIResponse(response); | |
| // Initialize existingFiles if not already | |
| if (!global.existingFiles) { | |
| global.existingFiles = new Set<string>(); | |
| } | |
| // If no active sandbox, just return parsed results | |
| if (!global.activeSandbox) { | |
| return NextResponse.json({ | |
| success: true, | |
| results: { | |
| filesCreated: parsed.files.map(f => f.path), | |
| packagesInstalled: parsed.packages, | |
| commandsExecuted: parsed.commands, | |
| errors: [] | |
| }, | |
| explanation: parsed.explanation, | |
| structure: parsed.structure, | |
| parsedFiles: parsed.files, | |
| message: `Parsed ${parsed.files.length} files successfully. Create a sandbox to apply them.` | |
| }); | |
| } | |
| // Apply to active sandbox | |
| console.log('[apply-ai-code] Applying code to sandbox...'); | |
| console.log('[apply-ai-code] Is edit mode:', isEdit); | |
| console.log('[apply-ai-code] Files to write:', parsed.files.map(f => f.path)); | |
| console.log('[apply-ai-code] Existing files:', Array.from(global.existingFiles)); | |
| const results = { | |
| filesCreated: [] as string[], | |
| filesUpdated: [] as string[], | |
| packagesInstalled: [] as string[], | |
| packagesAlreadyInstalled: [] as string[], | |
| packagesFailed: [] as string[], | |
| commandsExecuted: [] as string[], | |
| errors: [] as string[] | |
| }; | |
| // Combine packages from tool calls and parsed XML tags | |
| const allPackages = [...packages.filter((pkg: any) => pkg && typeof pkg === 'string'), ...parsed.packages]; | |
| const uniquePackages = [...new Set(allPackages)]; // Remove duplicates | |
| if (uniquePackages.length > 0) { | |
| console.log('[apply-ai-code] Installing packages from XML tags and tool calls:', uniquePackages); | |
| try { | |
| const installResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/install-packages`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ packages: uniquePackages }) | |
| }); | |
| if (installResponse.ok) { | |
| const installResult = await installResponse.json(); | |
| console.log('[apply-ai-code] Package installation result:', installResult); | |
| if (installResult.installed && installResult.installed.length > 0) { | |
| results.packagesInstalled = installResult.installed; | |
| } | |
| if (installResult.failed && installResult.failed.length > 0) { | |
| results.packagesFailed = installResult.failed; | |
| } | |
| } | |
| } catch (error) { | |
| console.error('[apply-ai-code] Error installing packages:', error); | |
| } | |
| } else { | |
| // Fallback to detecting packages from code | |
| console.log('[apply-ai-code] No packages provided, detecting from generated code...'); | |
| console.log('[apply-ai-code] Number of files to scan:', parsed.files.length); | |
| // Filter out config files first | |
| const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js']; | |
| const filteredFilesForDetection = parsed.files.filter(file => { | |
| const fileName = file.path.split('/').pop() || ''; | |
| return !configFiles.includes(fileName); | |
| }); | |
| // Build files object for package detection | |
| const filesForPackageDetection: Record<string, string> = {}; | |
| for (const file of filteredFilesForDetection) { | |
| filesForPackageDetection[file.path] = file.content; | |
| // Log if heroicons is found | |
| if (file.content.includes('heroicons')) { | |
| console.log(`[apply-ai-code] Found heroicons import in ${file.path}`); | |
| } | |
| } | |
| try { | |
| console.log('[apply-ai-code] Calling detect-and-install-packages...'); | |
| const packageResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/detect-and-install-packages`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ files: filesForPackageDetection }) | |
| }); | |
| console.log('[apply-ai-code] Package detection response status:', packageResponse.status); | |
| if (packageResponse.ok) { | |
| const packageResult = await packageResponse.json(); | |
| console.log('[apply-ai-code] Package installation result:', JSON.stringify(packageResult, null, 2)); | |
| if (packageResult.packagesInstalled && packageResult.packagesInstalled.length > 0) { | |
| results.packagesInstalled = packageResult.packagesInstalled; | |
| console.log(`[apply-ai-code] Installed packages: ${packageResult.packagesInstalled.join(', ')}`); | |
| } | |
| if (packageResult.packagesAlreadyInstalled && packageResult.packagesAlreadyInstalled.length > 0) { | |
| results.packagesAlreadyInstalled = packageResult.packagesAlreadyInstalled; | |
| console.log(`[apply-ai-code] Already installed: ${packageResult.packagesAlreadyInstalled.join(', ')}`); | |
| } | |
| if (packageResult.packagesFailed && packageResult.packagesFailed.length > 0) { | |
| results.packagesFailed = packageResult.packagesFailed; | |
| console.error(`[apply-ai-code] Failed to install packages: ${packageResult.packagesFailed.join(', ')}`); | |
| results.errors.push(`Failed to install packages: ${packageResult.packagesFailed.join(', ')}`); | |
| } | |
| // Force Vite restart after package installation | |
| if (results.packagesInstalled.length > 0) { | |
| console.log('[apply-ai-code] Packages were installed, forcing Vite restart...'); | |
| try { | |
| // Call the restart-vite endpoint | |
| const restartResponse = await fetch(`${process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'}/api/restart-vite`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| if (restartResponse.ok) { | |
| const restartResult = await restartResponse.json(); | |
| console.log('[apply-ai-code] Vite restart result:', restartResult.message); | |
| } else { | |
| console.error('[apply-ai-code] Failed to restart Vite:', await restartResponse.text()); | |
| } | |
| } catch (e) { | |
| console.error('[apply-ai-code] Error calling restart-vite:', e); | |
| } | |
| // Additional delay to ensure files can be written after restart | |
| await new Promise(resolve => setTimeout(resolve, 1000)); | |
| } | |
| } else { | |
| console.error('[apply-ai-code] Package detection/installation failed:', await packageResponse.text()); | |
| } | |
| } catch (error) { | |
| console.error('[apply-ai-code] Error detecting/installing packages:', error); | |
| // Continue with file writing even if package installation fails | |
| } | |
| } | |
| // Filter out config files that shouldn't be created | |
| const configFiles = ['tailwind.config.js', 'vite.config.js', 'package.json', 'package-lock.json', 'tsconfig.json', 'postcss.config.js']; | |
| const filteredFiles = parsed.files.filter(file => { | |
| const fileName = file.path.split('/').pop() || ''; | |
| if (configFiles.includes(fileName)) { | |
| console.warn(`[apply-ai-code] Skipping config file: ${file.path} - already exists in template`); | |
| return false; | |
| } | |
| return true; | |
| }); | |
| // Create or update files AFTER package installation | |
| for (const file of filteredFiles) { | |
| try { | |
| // Normalize the file path | |
| let normalizedPath = file.path; | |
| // Remove leading slash if present | |
| if (normalizedPath.startsWith('/')) { | |
| normalizedPath = normalizedPath.substring(1); | |
| } | |
| // Ensure src/ prefix for component files | |
| if (!normalizedPath.startsWith('src/') && | |
| !normalizedPath.startsWith('public/') && | |
| normalizedPath !== 'index.html' && | |
| normalizedPath !== 'package.json' && | |
| normalizedPath !== 'vite.config.js' && | |
| normalizedPath !== 'tailwind.config.js' && | |
| normalizedPath !== 'postcss.config.js') { | |
| normalizedPath = 'src/' + normalizedPath; | |
| } | |
| const fullPath = `/home/user/app/${normalizedPath}`; | |
| const isUpdate = global.existingFiles.has(normalizedPath); | |
| // Remove any CSS imports from JSX/JS files (we're using Tailwind) | |
| let fileContent = file.content; | |
| if (file.path.endsWith('.jsx') || file.path.endsWith('.js') || file.path.endsWith('.tsx') || file.path.endsWith('.ts')) { | |
| fileContent = fileContent.replace(/import\s+['"]\.\/[^'"]+\.css['"];?\s*\n?/g, ''); | |
| } | |
| console.log(`[apply-ai-code] Writing file using E2B files API: ${fullPath}`); | |
| try { | |
| // Use the correct E2B API - sandbox.files.write() | |
| await global.activeSandbox.files.write(fullPath, fileContent); | |
| console.log(`[apply-ai-code] Successfully wrote file: ${fullPath}`); | |
| // Update file cache | |
| if (global.sandboxState?.fileCache) { | |
| global.sandboxState.fileCache.files[normalizedPath] = { | |
| content: fileContent, | |
| lastModified: Date.now() | |
| }; | |
| console.log(`[apply-ai-code] Updated file cache for: ${normalizedPath}`); | |
| } | |
| } catch (writeError) { | |
| console.error(`[apply-ai-code] E2B file write error:`, writeError); | |
| throw writeError; | |
| } | |
| if (isUpdate) { | |
| results.filesUpdated.push(normalizedPath); | |
| } else { | |
| results.filesCreated.push(normalizedPath); | |
| global.existingFiles.add(normalizedPath); | |
| } | |
| } catch (error) { | |
| results.errors.push(`Failed to create ${file.path}: ${(error as Error).message}`); | |
| } | |
| } | |
| // Only create App.jsx if it's not an edit and doesn't exist | |
| const appFileInParsed = parsed.files.some(f => { | |
| const normalized = f.path.replace(/^\//, '').replace(/^src\//, ''); | |
| return normalized === 'App.jsx' || normalized === 'App.tsx'; | |
| }); | |
| const appFileExists = global.existingFiles.has('src/App.jsx') || | |
| global.existingFiles.has('src/App.tsx') || | |
| global.existingFiles.has('App.jsx') || | |
| global.existingFiles.has('App.tsx'); | |
| if (!isEdit && !appFileInParsed && !appFileExists && parsed.files.length > 0) { | |
| // Find all component files | |
| const componentFiles = parsed.files.filter(f => | |
| (f.path.endsWith('.jsx') || f.path.endsWith('.tsx')) && | |
| f.path.includes('component') | |
| ); | |
| // Generate imports for components | |
| const imports = componentFiles | |
| .filter(f => !f.path.includes('App.') && !f.path.includes('main.') && !f.path.includes('index.')) | |
| .map(f => { | |
| const pathParts = f.path.split('/'); | |
| const fileName = pathParts[pathParts.length - 1]; | |
| const componentName = fileName.replace(/\.(jsx|tsx)$/, ''); | |
| // Fix import path - components are in src/components/ | |
| const importPath = f.path.startsWith('src/') | |
| ? f.path.replace('src/', './').replace(/\.(jsx|tsx)$/, '') | |
| : './' + f.path.replace(/\.(jsx|tsx)$/, ''); | |
| return `import ${componentName} from '${importPath}';`; | |
| }) | |
| .join('\n'); | |
| // Find the main component | |
| const mainComponent = componentFiles.find(f => { | |
| const name = f.path.toLowerCase(); | |
| return name.includes('header') || | |
| name.includes('hero') || | |
| name.includes('layout') || | |
| name.includes('main') || | |
| name.includes('home'); | |
| }) || componentFiles[0]; | |
| const mainComponentName = mainComponent | |
| ? mainComponent.path.split('/').pop()?.replace(/\.(jsx|tsx)$/, '') | |
| : null; | |
| // Create App.jsx with better structure | |
| const appContent = `import React from 'react'; | |
| ${imports} | |
| function App() { | |
| return ( | |
| <div className="min-h-screen bg-gray-900 text-white p-8"> | |
| ${mainComponentName ? `<${mainComponentName} />` : '<div className="text-center">\n <h1 className="text-4xl font-bold mb-4">Welcome to your React App</h1>\n <p className="text-gray-400">Your components have been created but need to be added here.</p>\n </div>'} | |
| {/* Generated components: ${componentFiles.map(f => f.path).join(', ')} */} | |
| </div> | |
| ); | |
| } | |
| export default App;`; | |
| try { | |
| await global.activeSandbox.runCode(` | |
| file_path = "/home/user/app/src/App.jsx" | |
| file_content = """${appContent.replace(/"/g, '\\"').replace(/\n/g, '\\n')}""" | |
| with open(file_path, 'w') as f: | |
| f.write(file_content) | |
| print(f"Auto-generated: {file_path}") | |
| `); | |
| results.filesCreated.push('src/App.jsx (auto-generated)'); | |
| } catch (error) { | |
| results.errors.push(`Failed to create App.jsx: ${(error as Error).message}`); | |
| } | |
| // Don't auto-generate App.css - we're using Tailwind CSS | |
| // Only create index.css if it doesn't exist | |
| const indexCssInParsed = parsed.files.some(f => { | |
| const normalized = f.path.replace(/^\//, '').replace(/^src\//, ''); | |
| return normalized === 'index.css' || f.path === 'src/index.css'; | |
| }); | |
| const indexCssExists = global.existingFiles.has('src/index.css') || | |
| global.existingFiles.has('index.css'); | |
| if (!isEdit && !indexCssInParsed && !indexCssExists) { | |
| try { | |
| await global.activeSandbox.runCode(` | |
| file_path = "/home/user/app/src/index.css" | |
| file_content = """@tailwind base; | |
| @tailwind components; | |
| @tailwind utilities; | |
| :root { | |
| font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; | |
| line-height: 1.5; | |
| font-weight: 400; | |
| color-scheme: dark; | |
| color: rgba(255, 255, 255, 0.87); | |
| background-color: #0a0a0a; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| } | |
| body { | |
| margin: 0; | |
| min-width: 320px; | |
| min-height: 100vh; | |
| }""" | |
| with open(file_path, 'w') as f: | |
| f.write(file_content) | |
| print(f"Auto-generated: {file_path}") | |
| `); | |
| results.filesCreated.push('src/index.css (with Tailwind)'); | |
| } catch (error) { | |
| results.errors.push('Failed to create index.css with Tailwind'); | |
| } | |
| } | |
| } | |
| // Execute commands | |
| for (const cmd of parsed.commands) { | |
| try { | |
| await global.activeSandbox.runCode(` | |
| import subprocess | |
| os.chdir('/home/user/app') | |
| result = subprocess.run(${JSON.stringify(cmd.split(' '))}, capture_output=True, text=True) | |
| print(f"Executed: ${cmd}") | |
| print(result.stdout) | |
| if result.stderr: | |
| print(f"Errors: {result.stderr}") | |
| `); | |
| results.commandsExecuted.push(cmd); | |
| } catch (error) { | |
| results.errors.push(`Failed to execute ${cmd}: ${(error as Error).message}`); | |
| } | |
| } | |
| // Check for missing imports in App.jsx | |
| const missingImports: string[] = []; | |
| const appFile = parsed.files.find(f => | |
| f.path === 'src/App.jsx' || f.path === 'App.jsx' | |
| ); | |
| if (appFile) { | |
| // Extract imports from App.jsx | |
| const importRegex = /import\s+(?:\w+|\{[^}]+\})\s+from\s+['"]([^'"]+)['"]/g; | |
| let match; | |
| const imports: string[] = []; | |
| while ((match = importRegex.exec(appFile.content)) !== null) { | |
| const importPath = match[1]; | |
| if (importPath.startsWith('./') || importPath.startsWith('../')) { | |
| imports.push(importPath); | |
| } | |
| } | |
| // Check if all imported files exist | |
| for (const imp of imports) { | |
| // Skip CSS imports for this check | |
| if (imp.endsWith('.css')) continue; | |
| // Convert import path to expected file paths | |
| const basePath = imp.replace('./', 'src/'); | |
| const possiblePaths = [ | |
| basePath + '.jsx', | |
| basePath + '.js', | |
| basePath + '/index.jsx', | |
| basePath + '/index.js' | |
| ]; | |
| const fileExists = parsed.files.some(f => | |
| possiblePaths.some(path => f.path === path) | |
| ); | |
| if (!fileExists) { | |
| missingImports.push(imp); | |
| } | |
| } | |
| } | |
| // Prepare response | |
| const responseData: any = { | |
| success: true, | |
| results, | |
| explanation: parsed.explanation, | |
| structure: parsed.structure, | |
| message: `Applied ${results.filesCreated.length} files successfully` | |
| }; | |
| // Handle missing imports automatically | |
| if (missingImports.length > 0) { | |
| console.warn('[apply-ai-code] Missing imports detected:', missingImports); | |
| // Automatically generate missing components | |
| try { | |
| console.log('[apply-ai-code] Auto-generating missing components...'); | |
| const autoCompleteResponse = await fetch( | |
| `${request.nextUrl.origin}/api/auto-complete-components`, | |
| { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| missingImports, | |
| model: 'claude-sonnet-4-20250514' | |
| }) | |
| } | |
| ); | |
| const autoCompleteData = await autoCompleteResponse.json(); | |
| if (autoCompleteData.success) { | |
| responseData.autoCompleted = true; | |
| responseData.autoCompletedComponents = autoCompleteData.components; | |
| responseData.message = `Applied ${results.filesCreated.length} files + auto-generated ${autoCompleteData.files} missing components`; | |
| // Add auto-completed files to results | |
| results.filesCreated.push(...autoCompleteData.components); | |
| } else { | |
| // If auto-complete fails, still warn the user | |
| responseData.warning = `Missing ${missingImports.length} imported components: ${missingImports.join(', ')}`; | |
| responseData.missingImports = missingImports; | |
| } | |
| } catch (error) { | |
| console.error('[apply-ai-code] Auto-complete failed:', error); | |
| responseData.warning = `Missing ${missingImports.length} imported components: ${missingImports.join(', ')}`; | |
| responseData.missingImports = missingImports; | |
| } | |
| } | |
| // Track applied files in conversation state | |
| if (global.conversationState && results.filesCreated.length > 0) { | |
| // Update the last message metadata with edited files | |
| const messages = global.conversationState.context.messages; | |
| if (messages.length > 0) { | |
| const lastMessage = messages[messages.length - 1]; | |
| if (lastMessage.role === 'user') { | |
| lastMessage.metadata = { | |
| ...lastMessage.metadata, | |
| editedFiles: results.filesCreated | |
| }; | |
| } | |
| } | |
| // Track applied code in project evolution | |
| if (global.conversationState.context.projectEvolution) { | |
| global.conversationState.context.projectEvolution.majorChanges.push({ | |
| timestamp: Date.now(), | |
| description: parsed.explanation || 'Code applied', | |
| filesAffected: results.filesCreated | |
| }); | |
| } | |
| // Update last updated timestamp | |
| global.conversationState.lastUpdated = Date.now(); | |
| console.log('[apply-ai-code] Updated conversation state with applied files:', results.filesCreated); | |
| } | |
| return NextResponse.json(responseData); | |
| } catch (error) { | |
| console.error('Apply AI code error:', error); | |
| return NextResponse.json( | |
| { error: error instanceof Error ? error.message : 'Failed to parse AI code' }, | |
| { status: 500 } | |
| ); | |
| } | |
| } |