import { useState, useEffect, useRef } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' import { Badge } from '@/components/ui/badge' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select' import { Plus, Trash, Play, CheckCircle, XCircle, FileCode, ArrowsOut, BookOpen, ShieldCheck } from '@phosphor-icons/react' import { toast } from 'sonner' import { createLuaEngine, type LuaExecutionResult } from '@/lib/lua-engine' import { getLuaExampleCode, getLuaExamplesList } from '@/lib/lua-examples' import type { LuaScript } from '@/lib/level-types' import Editor from '@monaco-editor/react' import { useMonaco } from '@monaco-editor/react' import { LuaSnippetLibrary } from '@/components/LuaSnippetLibrary' import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet' import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner' import { SecurityWarningDialog } from '@/components/SecurityWarningDialog' interface LuaEditorProps { scripts: LuaScript[] onScriptsChange: (scripts: LuaScript[]) => void } export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) { const [selectedScript, setSelectedScript] = useState( scripts.length > 0 ? scripts[0].id : null ) const [testOutput, setTestOutput] = useState(null) const [testInputs, setTestInputs] = useState>({}) const [isExecuting, setIsExecuting] = useState(false) const [isFullscreen, setIsFullscreen] = useState(false) const [showSnippetLibrary, setShowSnippetLibrary] = useState(false) const [securityScanResult, setSecurityScanResult] = useState(null) const [showSecurityDialog, setShowSecurityDialog] = useState(false) const editorRef = useRef(null) const monaco = useMonaco() const currentScript = scripts.find(s => s.id === selectedScript) useEffect(() => { if (monaco) { monaco.languages.registerCompletionItemProvider('lua', { provideCompletionItems: (model, position) => { const word = model.getWordUntilPosition(position) const range = { startLineNumber: position.lineNumber, endLineNumber: position.lineNumber, startColumn: word.startColumn, endColumn: word.endColumn } const suggestions: any[] = [ { label: 'context.data', kind: monaco.languages.CompletionItemKind.Property, insertText: 'context.data', documentation: 'Access input parameters passed to the script', range }, { label: 'context.user', kind: monaco.languages.CompletionItemKind.Property, insertText: 'context.user', documentation: 'Current user information (username, role, etc.)', range }, { label: 'context.kv', kind: monaco.languages.CompletionItemKind.Property, insertText: 'context.kv', documentation: 'Key-value storage interface', range }, { label: 'context.log', kind: monaco.languages.CompletionItemKind.Function, insertText: 'context.log(${1:message})', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Log a message to the output console', range }, { label: 'log', kind: monaco.languages.CompletionItemKind.Function, insertText: 'log(${1:message})', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Log a message (shortcut for context.log)', range }, { label: 'print', kind: monaco.languages.CompletionItemKind.Function, insertText: 'print(${1:message})', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Print a message to output', range }, { label: 'return', kind: monaco.languages.CompletionItemKind.Keyword, insertText: 'return ${1:result}', insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, documentation: 'Return a value from the script', range }, ] return { suggestions } } }) monaco.languages.setLanguageConfiguration('lua', { comments: { lineComment: '--', blockComment: ['--[[', ']]'] }, brackets: [ ['{', '}'], ['[', ']'], ['(', ')'] ], autoClosingPairs: [ { open: '{', close: '}' }, { open: '[', close: ']' }, { open: '(', close: ')' }, { open: '"', close: '"' }, { open: "'", close: "'" } ] }) } }, [monaco]) useEffect(() => { if (currentScript) { const inputs: Record = {} currentScript.parameters.forEach((param) => { inputs[param.name] = param.type === 'number' ? 0 : param.type === 'boolean' ? false : '' }) setTestInputs(inputs) } }, [selectedScript, currentScript?.parameters.length]) const handleAddScript = () => { const newScript: LuaScript = { id: `lua_${Date.now()}`, name: 'New Script', code: '-- Lua script example\n-- Access input parameters via context.data\n-- Use log() or print() to output messages\n\nlog("Script started")\n\nif context.data then\n log("Received data:", context.data)\nend\n\nlocal result = {\n success = true,\n message = "Script executed successfully"\n}\n\nreturn result', parameters: [], } onScriptsChange([...scripts, newScript]) setSelectedScript(newScript.id) toast.success('Script created') } const handleDeleteScript = (scriptId: string) => { onScriptsChange(scripts.filter(s => s.id !== scriptId)) if (selectedScript === scriptId) { setSelectedScript(scripts.length > 1 ? scripts[0].id : null) } toast.success('Script deleted') } const handleUpdateScript = (updates: Partial) => { if (!currentScript) return onScriptsChange( scripts.map(s => s.id === selectedScript ? { ...s, ...updates } : s) ) } const handleTestScript = async () => { if (!currentScript) return const scanResult = securityScanner.scanLua(currentScript.code) setSecurityScanResult(scanResult) if (scanResult.severity === 'critical' || scanResult.severity === 'high') { setShowSecurityDialog(true) toast.warning('Security issues detected in script') return } if (scanResult.severity === 'medium' && scanResult.issues.length > 0) { toast.warning(`${scanResult.issues.length} security warning(s) detected`) } setIsExecuting(true) setTestOutput(null) try { const engine = createLuaEngine() const contextData: any = {} currentScript.parameters.forEach((param) => { contextData[param.name] = testInputs[param.name] }) const result = await engine.execute(currentScript.code, { data: contextData, user: { username: 'test_user', role: 'god' }, log: (...args: any[]) => console.log('[Lua]', ...args) }) setTestOutput(result) if (result.success) { toast.success('Script executed successfully') } else { toast.error('Script execution failed') } engine.destroy() } catch (error) { toast.error('Execution error: ' + (error instanceof Error ? error.message : String(error))) setTestOutput({ success: false, error: error instanceof Error ? error.message : String(error), logs: [] }) } finally { setIsExecuting(false) } } const handleScanCode = () => { if (!currentScript) return const scanResult = securityScanner.scanLua(currentScript.code) setSecurityScanResult(scanResult) setShowSecurityDialog(true) if (scanResult.safe) { toast.success('No security issues detected') } else { toast.warning(`${scanResult.issues.length} security issue(s) detected`) } } const handleProceedWithExecution = () => { setShowSecurityDialog(false) if (!currentScript) return setIsExecuting(true) setTestOutput(null) setTimeout(async () => { try { const engine = createLuaEngine() const contextData: any = {} currentScript.parameters.forEach((param) => { contextData[param.name] = testInputs[param.name] }) const result = await engine.execute(currentScript.code, { data: contextData, user: { username: 'test_user', role: 'god' }, log: (...args: any[]) => console.log('[Lua]', ...args) }) setTestOutput(result) if (result.success) { toast.success('Script executed successfully') } else { toast.error('Script execution failed') } engine.destroy() } catch (error) { toast.error('Execution error: ' + (error instanceof Error ? error.message : String(error))) setTestOutput({ success: false, error: error instanceof Error ? error.message : String(error), logs: [] }) } finally { setIsExecuting(false) } }, 100) } const handleAddParameter = () => { if (!currentScript) return const newParam = { name: `param${currentScript.parameters.length + 1}`, type: 'string' } handleUpdateScript({ parameters: [...currentScript.parameters, newParam], }) } const handleDeleteParameter = (index: number) => { if (!currentScript) return handleUpdateScript({ parameters: currentScript.parameters.filter((_, i) => i !== index), }) } const handleUpdateParameter = (index: number, updates: { name?: string; type?: string }) => { if (!currentScript) return handleUpdateScript({ parameters: currentScript.parameters.map((p, i) => i === index ? { ...p, ...updates } : p ), }) } const handleInsertSnippet = (code: string) => { if (!currentScript) return if (editorRef.current) { const selection = editorRef.current.getSelection() if (selection) { editorRef.current.executeEdits('', [{ range: selection, text: code, forceMoveMarkers: true }]) editorRef.current.focus() } else { const currentCode = currentScript.code const newCode = currentCode ? currentCode + '\n\n' + code : code handleUpdateScript({ code: newCode }) } } else { const currentCode = currentScript.code const newCode = currentCode ? currentCode + '\n\n' + code : code handleUpdateScript({ code: newCode }) } setShowSnippetLibrary(false) } return (
Lua Scripts
Custom logic scripts
{scripts.length === 0 ? (

No scripts yet. Create one to start.

) : ( scripts.map((script) => (
setSelectedScript(script.id)} >
{script.name}
{script.parameters.length} params
)) )}
{!currentScript ? (

Select or create a script to edit

) : ( <>
Edit Script: {currentScript.name} Write custom Lua logic
handleUpdateScript({ name: e.target.value })} placeholder="validate_user" className="font-mono" />
handleUpdateScript({ returnType: e.target.value })} placeholder="table, boolean, string..." />
handleUpdateScript({ description: e.target.value })} placeholder="What this script does..." />
{currentScript.parameters.length === 0 ? (

No parameters defined

) : ( currentScript.parameters.map((param, index) => (
handleUpdateParameter(index, { name: e.target.value })} placeholder="paramName" className="flex-1 font-mono text-sm" /> handleUpdateParameter(index, { type: e.target.value })} placeholder="string" className="w-32 text-sm" />
)) )}
{currentScript.parameters.length > 0 && (
{currentScript.parameters.map((param) => (
{ const value = param.type === 'number' ? parseFloat(e.target.value) || 0 : param.type === 'boolean' ? e.target.value === 'true' : e.target.value setTestInputs({ ...testInputs, [param.name]: value }) }} placeholder={`Enter ${param.type} value`} className="flex-1 text-sm" type={param.type === 'number' ? 'number' : 'text'} /> {param.type}
))}
)}
Lua Snippet Library Browse and insert pre-built code templates
handleUpdateScript({ code: value || '' })} onMount={(editor) => { editorRef.current = editor }} theme="vs-dark" options={{ minimap: { enabled: isFullscreen }, fontSize: 14, fontFamily: 'JetBrains Mono, monospace', lineNumbers: 'on', roundedSelection: true, scrollBeyondLastLine: false, automaticLayout: true, tabSize: 2, wordWrap: 'on', quickSuggestions: true, suggestOnTriggerCharacters: true, acceptSuggestionOnEnter: 'on', snippetSuggestions: 'inline', parameterHints: { enabled: true }, formatOnPaste: true, formatOnType: true, }} />

Write Lua code. Access parameters via context.data. Use log() or print() for output. Press Ctrl+Space for autocomplete.

{testOutput && (
{testOutput.success ? ( ) : ( )} {testOutput.success ? 'Execution Successful' : 'Execution Failed'}
{testOutput.error && (
                          {testOutput.error}
                        
)} {testOutput.logs.length > 0 && (
                          {testOutput.logs.join('\n')}
                        
)} {testOutput.result !== null && testOutput.result !== undefined && (
                          {JSON.stringify(testOutput.result, null, 2)}
                        
)}
)}

Available in context:

  • context.data - Input data
  • context.user - Current user info
  • context.kv - Key-value storage
  • context.log(msg) - Logging function
)}
{securityScanResult && ( setShowSecurityDialog(false)} codeType="Lua script" showProceedButton={true} /> )}
) }