Compare commits

...

1 Commits

Author SHA1 Message Date
018f5e22a2 refactor: modularize lua editor layout 2025-12-27 17:38:22 +00:00
13 changed files with 993 additions and 645 deletions

View File

@@ -0,0 +1,133 @@
import { Badge } from '@/components/ui'
import { Button } from '@/components/ui'
import { Input } from '@/components/ui'
import { Label } from '@/components/ui'
import { Trash } from '@phosphor-icons/react'
import type { LuaExecutionResult } from '@/lib/lua-engine'
import type { LuaScript } from '@/lib/level-types'
import { LuaExecutionResultCard } from './LuaExecutionResultCard'
import { LuaContextInfo } from './LuaContextInfo'
interface LuaBlocksBridgeProps {
currentScript: LuaScript
testInputs: Record<string, any>
testOutput: LuaExecutionResult | null
onAddParameter: () => void
onDeleteParameter: (index: number) => void
onUpdateParameter: (index: number, updates: { name?: string; type?: string }) => void
onUpdateScript: (updates: Partial<LuaScript>) => void
onUpdateTestInput: (name: string, value: any) => void
}
export function LuaBlocksBridge({
currentScript,
testInputs,
testOutput,
onAddParameter,
onDeleteParameter,
onUpdateParameter,
onUpdateScript,
onUpdateTestInput,
}: LuaBlocksBridgeProps) {
return (
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Script Name</Label>
<Input
value={currentScript.name}
onChange={event => onUpdateScript({ name: event.target.value })}
placeholder="validate_user"
className="font-mono"
/>
</div>
<div className="space-y-2">
<Label>Return Type</Label>
<Input
value={currentScript.returnType || ''}
onChange={event => onUpdateScript({ returnType: event.target.value })}
placeholder="table, boolean, string..."
/>
</div>
</div>
<div className="space-y-2">
<Label>Description</Label>
<Input
value={currentScript.description || ''}
onChange={event => onUpdateScript({ description: event.target.value })}
placeholder="What this script does..."
/>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>Parameters</Label>
<Button size="sm" variant="outline" onClick={onAddParameter}>
Add Parameter
</Button>
</div>
<div className="space-y-2">
{currentScript.parameters.length === 0 ? (
<p className="text-xs text-muted-foreground text-center py-3 border border-dashed rounded-lg">No parameters defined</p>
) : (
currentScript.parameters.map((param, index) => (
<div key={param.name} className="flex gap-2 items-center">
<Input
value={param.name}
onChange={event => onUpdateParameter(index, { name: event.target.value })}
placeholder="paramName"
className="flex-1 font-mono text-sm"
/>
<Input
value={param.type}
onChange={event => onUpdateParameter(index, { type: event.target.value })}
placeholder="string"
className="w-32 text-sm"
/>
<Button variant="ghost" size="sm" onClick={() => onDeleteParameter(index)}>
<Trash size={14} />
</Button>
</div>
))
)}
</div>
</div>
{currentScript.parameters.length > 0 && (
<div>
<Label className="mb-2 block">Test Input Values</Label>
<div className="space-y-2">
{currentScript.parameters.map(param => (
<div key={param.name} className="flex gap-2 items-center">
<Label className="w-32 text-sm font-mono">{param.name}</Label>
<Input
value={testInputs[param.name] ?? ''}
onChange={event => {
const value =
param.type === 'number'
? parseFloat(event.target.value) || 0
: param.type === 'boolean'
? event.target.value === 'true'
: event.target.value
onUpdateTestInput(param.name, value)
}}
placeholder={`Enter ${param.type} value`}
className="flex-1 text-sm"
type={param.type === 'number' ? 'number' : 'text'}
/>
<Badge variant="outline" className="text-xs">
{param.type}
</Badge>
</div>
))}
</div>
</div>
)}
{testOutput && <LuaExecutionResultCard result={testOutput} />}
<LuaContextInfo />
</div>
)
}

View File

@@ -0,0 +1,125 @@
import Editor from '@monaco-editor/react'
import { ArrowsOut, BookOpen, FileCode } from '@phosphor-icons/react'
import { Button } from '@/components/ui'
import { Label } from '@/components/ui'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui'
import { LuaSnippetLibrary } from '@/components/editors/lua/LuaSnippetLibrary'
import { getLuaExampleCode, getLuaExamplesList } from '@/lib/lua-examples'
import type { LuaScript } from '@/lib/level-types'
import { toast } from 'sonner'
interface LuaCodeEditorViewProps {
currentScript: LuaScript
isFullscreen: boolean
showSnippetLibrary: boolean
onSnippetLibraryChange: (value: boolean) => void
onInsertSnippet: (code: string) => void
onToggleFullscreen: () => void
onUpdateCode: (code: string) => void
editorRef: { current: any }
}
export function LuaCodeEditorView({
currentScript,
isFullscreen,
showSnippetLibrary,
onSnippetLibraryChange,
onInsertSnippet,
onToggleFullscreen,
onUpdateCode,
editorRef,
}: LuaCodeEditorViewProps) {
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Lua Code</Label>
<div className="flex gap-2">
<Sheet open={showSnippetLibrary} onOpenChange={onSnippetLibraryChange}>
<SheetTrigger asChild>
<Button variant="outline" size="sm">
<BookOpen size={16} className="mr-2" />
Snippet Library
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-full sm:max-w-4xl overflow-y-auto">
<SheetHeader>
<SheetTitle>Lua Snippet Library</SheetTitle>
<SheetDescription>Browse and insert pre-built code templates</SheetDescription>
</SheetHeader>
<div className="mt-6">
<LuaSnippetLibrary onInsertSnippet={onInsertSnippet} />
</div>
</SheetContent>
</Sheet>
<Select
onValueChange={value => {
const exampleCode = getLuaExampleCode(value as any)
onUpdateCode(exampleCode)
toast.success('Example loaded')
}}
>
<SelectTrigger className="w-[180px]">
<FileCode size={16} className="mr-2" />
<SelectValue placeholder="Examples" />
</SelectTrigger>
<SelectContent>
{getLuaExamplesList().map(example => (
<SelectItem key={example.key} value={example.key}>
<div>
<div className="font-medium">{example.name}</div>
<div className="text-xs text-muted-foreground">{example.description}</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" size="sm" onClick={onToggleFullscreen}>
<ArrowsOut size={16} />
</Button>
</div>
</div>
<div className={`border rounded-lg overflow-hidden ${isFullscreen ? 'fixed inset-4 z-50 bg-background' : ''}`}>
<Editor
height={isFullscreen ? 'calc(100vh - 8rem)' : '400px'}
language="lua"
value={currentScript.code}
onChange={value => onUpdateCode(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,
}}
/>
</div>
<p className="text-xs text-muted-foreground">
Write Lua code. Access parameters via <code className="font-mono">context.data</code>. Use
<code className="font-mono"> log()</code> or <code className="font-mono">print()</code> for output. Press
<code className="font-mono"> Ctrl+Space</code> for autocomplete.
</p>
</div>
)
}

View File

@@ -0,0 +1,23 @@
export function LuaContextInfo() {
return (
<div className="bg-muted/50 rounded-lg p-4 border border-dashed">
<div className="space-y-2 text-xs text-muted-foreground">
<p className="font-semibold text-foreground">Available in context:</p>
<ul className="space-y-1 list-disc list-inside">
<li>
<code className="font-mono">context.data</code> - Input data
</li>
<li>
<code className="font-mono">context.user</code> - Current user info
</li>
<li>
<code className="font-mono">context.kv</code> - Key-value storage
</li>
<li>
<code className="font-mono">context.log(msg)</code> - Logging function
</li>
</ul>
</div>
</div>
)
}

View File

@@ -1,28 +1,12 @@
import { useState, useEffect, useRef } from 'react'
import { Button } from '@/components/ui'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
import { Input } from '@/components/ui'
import { Label } from '@/components/ui'
import { Badge } from '@/components/ui'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui'
import { Plus, Trash, Play, CheckCircle, XCircle, FileCode, ArrowsOut, BookOpen, ShieldCheck } from '@phosphor-icons/react'
import { toast } from 'sonner'
import { executeLuaScriptWithProfile } from '@/lib/lua/execute-lua-script-with-profile'
import 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/editors/lua/LuaSnippetLibrary'
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui'
import { securityScanner, type SecurityScanResult } from '@/lib/security-scanner'
import { Card, CardContent } from '@/components/ui'
import { SecurityWarningDialog } from '@/components/organisms/security/SecurityWarningDialog'
import { LuaEditorToolbar } from './LuaEditorToolbar'
import { LuaCodeEditorView } from './LuaCodeEditorView'
import { LuaBlocksBridge } from './LuaBlocksBridge'
import { LuaScriptsSidebar } from './LuaScriptsSidebar'
import { useLuaEditorState } from './state/useLuaEditorState'
import { useLuaEditorPersistence } from './persistence/useLuaEditorPersistence'
import type { LuaScript } from '@/lib/level-types'
interface LuaEditorProps {
scripts: LuaScript[]
@@ -30,365 +14,26 @@ interface LuaEditorProps {
}
export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) {
const [selectedScript, setSelectedScript] = useState<string | null>(
scripts.length > 0 ? scripts[0].id : null
)
const [testOutput, setTestOutput] = useState<LuaExecutionResult | null>(null)
const [testInputs, setTestInputs] = useState<Record<string, any>>({})
const [isExecuting, setIsExecuting] = useState(false)
const [isFullscreen, setIsFullscreen] = useState(false)
const [showSnippetLibrary, setShowSnippetLibrary] = useState(false)
const [securityScanResult, setSecurityScanResult] = useState<SecurityScanResult | null>(null)
const [showSecurityDialog, setShowSecurityDialog] = useState(false)
const editorRef = useRef<any>(null)
const monaco = useMonaco()
const state = useLuaEditorState({ scripts, onScriptsChange })
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<string, any> = {}
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<LuaScript>) => {
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 contextData: any = {}
currentScript.parameters.forEach((param) => {
contextData[param.name] = testInputs[param.name]
})
const result = await executeLuaScriptWithProfile(currentScript.code, {
data: contextData,
user: { username: 'test_user', role: 'god' },
log: (...args: any[]) => console.log('[Lua]', ...args)
}, currentScript)
setTestOutput(result)
if (result.success) {
toast.success('Script executed successfully')
} else {
toast.error('Script execution failed')
}
} 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 contextData: any = {}
currentScript.parameters.forEach((param) => {
contextData[param.name] = testInputs[param.name]
})
const result = await executeLuaScriptWithProfile(currentScript.code, {
data: contextData,
user: { username: 'test_user', role: 'god' },
log: (...args: any[]) => console.log('[Lua]', ...args)
}, currentScript)
setTestOutput(result)
if (result.success) {
toast.success('Script executed successfully')
} else {
toast.error('Script execution failed')
}
} 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)
}
useLuaEditorPersistence({
monaco: state.monaco,
currentScript: state.currentScript,
setTestInputs: state.setTestInputs,
})
return (
<div className="grid md:grid-cols-3 gap-6 h-full">
<Card className="md:col-span-1">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">Lua Scripts</CardTitle>
<Button size="sm" onClick={handleAddScript}>
<Plus size={16} />
</Button>
</div>
<CardDescription>Custom logic scripts</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{scripts.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">
No scripts yet. Create one to start.
</p>
) : (
scripts.map((script) => (
<div
key={script.id}
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors ${
selectedScript === script.id
? 'bg-accent border-accent-foreground'
: 'hover:bg-muted border-border'
}`}
onClick={() => setSelectedScript(script.id)}
>
<div>
<div className="font-medium text-sm font-mono">{script.name}</div>
<div className="text-xs text-muted-foreground">
{script.parameters.length} params
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDeleteScript(script.id)
}}
>
<Trash size={14} />
</Button>
</div>
))
)}
</div>
</CardContent>
</Card>
<LuaScriptsSidebar
scripts={scripts}
selectedScript={state.selectedScript}
onSelect={state.setSelectedScript}
onAdd={state.handleAddScript}
onDelete={state.handleDeleteScript}
/>
<Card className="md:col-span-2">
{!currentScript ? (
{!state.currentScript ? (
<CardContent className="flex items-center justify-center h-full min-h-[400px]">
<div className="text-center text-muted-foreground">
<p>Select or create a script to edit</p>
@@ -396,282 +41,46 @@ export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) {
</CardContent>
) : (
<>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Edit Script: {currentScript.name}</CardTitle>
<CardDescription>Write custom Lua logic</CardDescription>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={handleScanCode}>
<ShieldCheck className="mr-2" size={16} />
Security Scan
</Button>
<Button onClick={handleTestScript} disabled={isExecuting}>
<Play className="mr-2" size={16} />
{isExecuting ? 'Executing...' : 'Test Script'}
</Button>
</div>
</div>
</CardHeader>
<LuaEditorToolbar
scriptName={state.currentScript.name}
onScan={state.handleScanCode}
onTest={state.handleTestScript}
isExecuting={state.isExecuting}
/>
<CardContent className="space-y-6">
<div className="grid gap-4 md:grid-cols-2">
<div className="space-y-2">
<Label>Script Name</Label>
<Input
value={currentScript.name}
onChange={(e) => handleUpdateScript({ name: e.target.value })}
placeholder="validate_user"
className="font-mono"
/>
</div>
<div className="space-y-2">
<Label>Return Type</Label>
<Input
value={currentScript.returnType || ''}
onChange={(e) => handleUpdateScript({ returnType: e.target.value })}
placeholder="table, boolean, string..."
/>
</div>
</div>
<LuaBlocksBridge
currentScript={state.currentScript}
testInputs={state.testInputs}
testOutput={state.testOutput}
onAddParameter={state.handleAddParameter}
onDeleteParameter={state.handleDeleteParameter}
onUpdateParameter={state.handleUpdateParameter}
onUpdateScript={state.handleUpdateScript}
onUpdateTestInput={state.handleUpdateTestInput}
/>
<div className="space-y-2">
<Label>Description</Label>
<Input
value={currentScript.description || ''}
onChange={(e) => handleUpdateScript({ description: e.target.value })}
placeholder="What this script does..."
/>
</div>
<div>
<div className="flex items-center justify-between mb-2">
<Label>Parameters</Label>
<Button size="sm" variant="outline" onClick={handleAddParameter}>
<Plus className="mr-2" size={14} />
Add Parameter
</Button>
</div>
<div className="space-y-2">
{currentScript.parameters.length === 0 ? (
<p className="text-xs text-muted-foreground text-center py-3 border border-dashed rounded-lg">
No parameters defined
</p>
) : (
currentScript.parameters.map((param, index) => (
<div key={index} className="flex gap-2 items-center">
<Input
value={param.name}
onChange={(e) => handleUpdateParameter(index, { name: e.target.value })}
placeholder="paramName"
className="flex-1 font-mono text-sm"
/>
<Input
value={param.type}
onChange={(e) => handleUpdateParameter(index, { type: e.target.value })}
placeholder="string"
className="w-32 text-sm"
/>
<Button
variant="ghost"
size="sm"
onClick={() => handleDeleteParameter(index)}
>
<Trash size={14} />
</Button>
</div>
))
)}
</div>
</div>
{currentScript.parameters.length > 0 && (
<div>
<Label className="mb-2 block">Test Input Values</Label>
<div className="space-y-2">
{currentScript.parameters.map((param) => (
<div key={param.name} className="flex gap-2 items-center">
<Label className="w-32 text-sm font-mono">{param.name}</Label>
<Input
value={testInputs[param.name] ?? ''}
onChange={(e) => {
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'}
/>
<Badge variant="outline" className="text-xs">
{param.type}
</Badge>
</div>
))}
</div>
</div>
)}
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label>Lua Code</Label>
<div className="flex gap-2">
<Sheet open={showSnippetLibrary} onOpenChange={setShowSnippetLibrary}>
<SheetTrigger asChild>
<Button variant="outline" size="sm">
<BookOpen size={16} className="mr-2" />
Snippet Library
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-full sm:max-w-4xl overflow-y-auto">
<SheetHeader>
<SheetTitle>Lua Snippet Library</SheetTitle>
<SheetDescription>
Browse and insert pre-built code templates
</SheetDescription>
</SheetHeader>
<div className="mt-6">
<LuaSnippetLibrary onInsertSnippet={handleInsertSnippet} />
</div>
</SheetContent>
</Sheet>
<Select
onValueChange={(value) => {
const exampleCode = getLuaExampleCode(value as any)
handleUpdateScript({ code: exampleCode })
toast.success('Example loaded')
}}
>
<SelectTrigger className="w-[180px]">
<FileCode size={16} className="mr-2" />
<SelectValue placeholder="Examples" />
</SelectTrigger>
<SelectContent>
{getLuaExamplesList().map((example) => (
<SelectItem key={example.key} value={example.key}>
<div>
<div className="font-medium">{example.name}</div>
<div className="text-xs text-muted-foreground">{example.description}</div>
</div>
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() => setIsFullscreen(!isFullscreen)}
>
<ArrowsOut size={16} />
</Button>
</div>
</div>
<div className={`border rounded-lg overflow-hidden ${isFullscreen ? 'fixed inset-4 z-50 bg-background' : ''}`}>
<Editor
height={isFullscreen ? 'calc(100vh - 8rem)' : '400px'}
language="lua"
value={currentScript.code}
onChange={(value) => 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,
}}
/>
</div>
<p className="text-xs text-muted-foreground">
Write Lua code. Access parameters via <code className="font-mono">context.data</code>. Use <code className="font-mono">log()</code> or <code className="font-mono">print()</code> for output. Press <code className="font-mono">Ctrl+Space</code> for autocomplete.
</p>
</div>
{testOutput && (
<Card className={testOutput.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}>
<CardHeader>
<div className="flex items-center gap-2">
{testOutput.success ? (
<CheckCircle size={20} className="text-green-600" />
) : (
<XCircle size={20} className="text-red-600" />
)}
<CardTitle className="text-sm">
{testOutput.success ? 'Execution Successful' : 'Execution Failed'}
</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-3">
{testOutput.error && (
<div>
<Label className="text-xs text-red-600 mb-1">Error</Label>
<pre className="text-xs font-mono whitespace-pre-wrap text-red-700 bg-red-100 p-2 rounded">
{testOutput.error}
</pre>
</div>
)}
{testOutput.logs.length > 0 && (
<div>
<Label className="text-xs mb-1">Logs</Label>
<pre className="text-xs font-mono whitespace-pre-wrap bg-muted p-2 rounded">
{testOutput.logs.join('\n')}
</pre>
</div>
)}
{testOutput.result !== null && testOutput.result !== undefined && (
<div>
<Label className="text-xs mb-1">Return Value</Label>
<pre className="text-xs font-mono whitespace-pre-wrap bg-muted p-2 rounded">
{JSON.stringify(testOutput.result, null, 2)}
</pre>
</div>
)}
</CardContent>
</Card>
)}
<div className="bg-muted/50 rounded-lg p-4 border border-dashed">
<div className="space-y-2 text-xs text-muted-foreground">
<p className="font-semibold text-foreground">Available in context:</p>
<ul className="space-y-1 list-disc list-inside">
<li><code className="font-mono">context.data</code> - Input data</li>
<li><code className="font-mono">context.user</code> - Current user info</li>
<li><code className="font-mono">context.kv</code> - Key-value storage</li>
<li><code className="font-mono">context.log(msg)</code> - Logging function</li>
</ul>
</div>
</div>
<LuaCodeEditorView
currentScript={state.currentScript}
isFullscreen={state.isFullscreen}
showSnippetLibrary={state.showSnippetLibrary}
onSnippetLibraryChange={state.setShowSnippetLibrary}
onInsertSnippet={state.handleInsertSnippet}
onToggleFullscreen={state.handleToggleFullscreen}
onUpdateCode={code => state.handleUpdateScript({ code })}
editorRef={state.editorRef}
/>
</CardContent>
</>
)}
</Card>
{securityScanResult && (
{state.securityScanResult && (
<SecurityWarningDialog
open={showSecurityDialog}
onOpenChange={setShowSecurityDialog}
scanResult={securityScanResult}
onProceed={handleProceedWithExecution}
onCancel={() => setShowSecurityDialog(false)}
open={state.showSecurityDialog}
onOpenChange={state.setShowSecurityDialog}
scanResult={state.securityScanResult}
onProceed={state.handleProceedWithExecution}
onCancel={() => state.setShowSecurityDialog(false)}
codeType="Lua script"
showProceedButton={true}
/>

View File

@@ -0,0 +1,33 @@
import { Button } from '@/components/ui'
import { CardDescription, CardHeader, CardTitle } from '@/components/ui'
import { Play, ShieldCheck } from '@phosphor-icons/react'
interface LuaEditorToolbarProps {
scriptName: string
onScan: () => void
onTest: () => void
isExecuting: boolean
}
export function LuaEditorToolbar({ scriptName, onScan, onTest, isExecuting }: LuaEditorToolbarProps) {
return (
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Edit Script: {scriptName}</CardTitle>
<CardDescription>Write custom Lua logic</CardDescription>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={onScan}>
<ShieldCheck className="mr-2" size={16} />
Security Scan
</Button>
<Button onClick={onTest} disabled={isExecuting}>
<Play className="mr-2" size={16} />
{isExecuting ? 'Executing...' : 'Test Script'}
</Button>
</div>
</div>
</CardHeader>
)
}

View File

@@ -0,0 +1,51 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui'
import { Label } from '@/components/ui'
import { CheckCircle, XCircle } from '@phosphor-icons/react'
import type { LuaExecutionResult } from '@/lib/lua-engine'
interface LuaExecutionResultCardProps {
result: LuaExecutionResult
}
export function LuaExecutionResultCard({ result }: LuaExecutionResultCardProps) {
return (
<Card className={result.success ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}>
<CardHeader>
<div className="flex items-center gap-2">
{result.success ? (
<CheckCircle size={20} className="text-green-600" />
) : (
<XCircle size={20} className="text-red-600" />
)}
<CardTitle className="text-sm">
{result.success ? 'Execution Successful' : 'Execution Failed'}
</CardTitle>
</div>
</CardHeader>
<CardContent className="space-y-3">
{result.error && (
<div>
<Label className="text-xs text-red-600 mb-1">Error</Label>
<pre className="text-xs font-mono whitespace-pre-wrap text-red-700 bg-red-100 p-2 rounded">{result.error}</pre>
</div>
)}
{result.logs.length > 0 && (
<div>
<Label className="text-xs mb-1">Logs</Label>
<pre className="text-xs font-mono whitespace-pre-wrap bg-muted p-2 rounded">{result.logs.join('\n')}</pre>
</div>
)}
{result.result !== null && result.result !== undefined && (
<div>
<Label className="text-xs mb-1">Return Value</Label>
<pre className="text-xs font-mono whitespace-pre-wrap bg-muted p-2 rounded">
{JSON.stringify(result.result, null, 2)}
</pre>
</div>
)}
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,60 @@
import { Button } from '@/components/ui'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
import { Plus, Trash } from '@phosphor-icons/react'
import type { LuaScript } from '@/lib/level-types'
interface LuaScriptsSidebarProps {
scripts: LuaScript[]
selectedScript: string | null
onSelect: (scriptId: string) => void
onAdd: () => void
onDelete: (scriptId: string) => void
}
export function LuaScriptsSidebar({ scripts, selectedScript, onSelect, onAdd, onDelete }: LuaScriptsSidebarProps) {
return (
<Card className="md:col-span-1">
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-lg">Lua Scripts</CardTitle>
<Button size="sm" onClick={onAdd}>
<Plus size={16} />
</Button>
</div>
<CardDescription>Custom logic scripts</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{scripts.length === 0 ? (
<p className="text-sm text-muted-foreground text-center py-4">No scripts yet. Create one to start.</p>
) : (
scripts.map(script => (
<div
key={script.id}
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors ${
selectedScript === script.id ? 'bg-accent border-accent-foreground' : 'hover:bg-muted border-border'
}`}
onClick={() => onSelect(script.id)}
>
<div>
<div className="font-medium text-sm font-mono">{script.name}</div>
<div className="text-xs text-muted-foreground">{script.parameters.length} params</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={event => {
event.stopPropagation()
onDelete(script.id)
}}
>
<Trash size={14} />
</Button>
</div>
))
)}
</div>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,120 @@
import { toast } from 'sonner'
import { executeLuaScriptWithProfile } from '@/lib/lua/execute-lua-script-with-profile'
import type { LuaScript } from '@/lib/level-types'
import type { LuaExecutionResult } from '@/lib/lua-engine'
import { securityScanner } from '@/lib/security-scanner'
interface ScriptGetter {
getCurrentScript: () => LuaScript | null
getTestInputs: () => Record<string, any>
}
interface ExecutionState {
setIsExecuting: (value: boolean) => void
setTestOutput: (value: LuaExecutionResult | null) => void
setSecurityScanResult: (result: any) => void
setShowSecurityDialog: (value: boolean) => void
}
export const createTestScript = ({
getCurrentScript,
getTestInputs,
setIsExecuting,
setTestOutput,
setSecurityScanResult,
setShowSecurityDialog,
}: ScriptGetter & ExecutionState) => async () => {
const currentScript = getCurrentScript()
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`)
}
await executeScript({ currentScript, getTestInputs, setIsExecuting, setTestOutput })
}
export const createScanCode = ({
getCurrentScript,
setSecurityScanResult,
setShowSecurityDialog,
}: Omit<ExecutionState, 'setIsExecuting' | 'setTestOutput'> & ScriptGetter) => () => {
const currentScript = getCurrentScript()
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`)
}
}
export const createProceedExecution = ({
getCurrentScript,
getTestInputs,
setIsExecuting,
setTestOutput,
setShowSecurityDialog,
}: ScriptGetter & Omit<ExecutionState, 'setSecurityScanResult'>) => () => {
setShowSecurityDialog(false)
const currentScript = getCurrentScript()
if (!currentScript) return
setTimeout(() => executeScript({ currentScript, getTestInputs, setIsExecuting, setTestOutput }), 100)
}
const executeScript = async ({
currentScript,
getTestInputs,
setIsExecuting,
setTestOutput,
}: {
currentScript: LuaScript
getTestInputs: () => Record<string, any>
setIsExecuting: (value: boolean) => void
setTestOutput: (value: LuaExecutionResult | null) => void
}) => {
setIsExecuting(true)
setTestOutput(null)
try {
const contextData: Record<string, any> = {}
currentScript.parameters.forEach(param => {
contextData[param.name] = getTestInputs()[param.name]
})
const result = await executeLuaScriptWithProfile(
currentScript.code,
{
data: contextData,
user: { username: 'test_user', role: 'god' },
log: (...args: any[]) => console.log('[Lua]', ...args),
},
currentScript
)
setTestOutput(result)
toast[result.success ? 'success' : 'error'](
result.success ? 'Script executed successfully' : 'Script execution failed'
)
} catch (error) {
const message = error instanceof Error ? error.message : String(error)
toast.error(`Execution error: ${message}`)
setTestOutput({ success: false, error: message, logs: [] })
} finally {
setIsExecuting(false)
}
}

View File

@@ -0,0 +1,52 @@
import type { LuaScript } from '@/lib/level-types'
interface ParameterHandlerProps {
currentScript: LuaScript | null
handleUpdateScript: (updates: Partial<LuaScript>) => void
}
interface TestInputHandlerProps {
getTestInputs: () => Record<string, any>
setTestInputs: (value: Record<string, any>) => void
}
export const createAddParameter = ({ currentScript, handleUpdateScript }: ParameterHandlerProps) => () => {
if (!currentScript) return
const newParam = {
name: `param${currentScript.parameters.length + 1}`,
type: 'string',
}
handleUpdateScript({ parameters: [...currentScript.parameters, newParam] })
}
export const createDeleteParameter = ({ currentScript, handleUpdateScript }: ParameterHandlerProps) => (
index: number
) => {
if (!currentScript) return
handleUpdateScript({
parameters: currentScript.parameters.filter((_, i) => i !== index),
})
}
export const createUpdateParameter = ({ currentScript, handleUpdateScript }: ParameterHandlerProps) => (
index: number,
updates: { name?: string; type?: string }
) => {
if (!currentScript) return
handleUpdateScript({
parameters: currentScript.parameters.map((param, i) =>
i === index ? { ...param, ...updates } : param
),
})
}
export const createUpdateTestInput = ({ getTestInputs, setTestInputs }: TestInputHandlerProps) => (
name: string,
value: any
) => {
setTestInputs({ ...getTestInputs(), [name]: value })
}

View File

@@ -0,0 +1,64 @@
import { toast } from 'sonner'
import type { Dispatch, SetStateAction } from 'react'
import type { LuaScript } from '@/lib/level-types'
const defaultCode = `-- Lua script example
-- Access input parameters via context.data
-- Use log() or print() to output messages
log("Script started")
if context.data then
log("Received data:", context.data)
end
local result = {
success = true,
message = "Script executed successfully"
}
return result`
interface UpdateProps {
scripts: LuaScript[]
onScriptsChange: (scripts: LuaScript[]) => void
selectedScript: string | null
}
interface ScriptCrudProps extends UpdateProps {
setSelectedScript: Dispatch<SetStateAction<string | null>>
}
export const createAddScript = ({ scripts, onScriptsChange, setSelectedScript }: ScriptCrudProps) => () => {
const newScript: LuaScript = {
id: `lua_${Date.now()}`,
name: 'New Script',
code: defaultCode,
parameters: [],
}
onScriptsChange([...scripts, newScript])
setSelectedScript(newScript.id)
toast.success('Script created')
}
export const createDeleteScript = ({
scripts,
onScriptsChange,
selectedScript,
setSelectedScript,
}: ScriptCrudProps) => (scriptId: string) => {
onScriptsChange(scripts.filter(script => script.id !== scriptId))
if (selectedScript === scriptId) {
setSelectedScript(scripts.length > 1 ? scripts[0]?.id ?? null : null)
}
toast.success('Script deleted')
}
export const createUpdateScript = ({ scripts, onScriptsChange, selectedScript }: UpdateProps) => (
updates: Partial<LuaScript>
) => {
if (!selectedScript) return
onScriptsChange(
scripts.map(script => (script.id === selectedScript ? { ...script, ...updates } : script))
)
}

View File

@@ -0,0 +1,49 @@
import type { Dispatch, MutableRefObject, SetStateAction } from 'react'
import type { LuaScript } from '@/lib/level-types'
interface SnippetProps {
currentScript: LuaScript | null
handleUpdateScript: (updates: Partial<LuaScript>) => void
editorRef: MutableRefObject<any>
setShowSnippetLibrary: Dispatch<SetStateAction<boolean>>
}
interface FullscreenProps {
isFullscreen: boolean
setIsFullscreen: Dispatch<SetStateAction<boolean>>
}
export const createInsertSnippet = ({
currentScript,
handleUpdateScript,
editorRef,
setShowSnippetLibrary,
}: SnippetProps) => (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 newCode = currentScript.code ? `${currentScript.code}\n\n${code}` : code
handleUpdateScript({ code: newCode })
}
} else {
const newCode = currentScript.code ? `${currentScript.code}\n\n${code}` : code
handleUpdateScript({ code: newCode })
}
setShowSnippetLibrary(false)
}
export const createToggleFullscreen = ({ isFullscreen, setIsFullscreen }: FullscreenProps) => () => {
setIsFullscreen(!isFullscreen)
}

View File

@@ -0,0 +1,119 @@
import { useEffect } from 'react'
import type { languages } from 'monaco-editor'
import type { LuaScript } from '@/lib/level-types'
interface UsePersistenceProps {
monaco: typeof import('monaco-editor') | null
currentScript: LuaScript | null
setTestInputs: (value: Record<string, any>) => void
}
export function useLuaEditorPersistence({
monaco,
currentScript,
setTestInputs,
}: UsePersistenceProps) {
useEffect(() => {
if (!monaco) return
monaco.languages.registerCompletionItemProvider('lua', {
provideCompletionItems: (model, position) => {
const word = model.getWordUntilPosition(position)
const range: languages.CompletionItem['range'] = {
startLineNumber: position.lineNumber,
endLineNumber: position.lineNumber,
startColumn: word.startColumn,
endColumn: word.endColumn,
}
const suggestions: languages.CompletionItem[] = [
{
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) return
const inputs: Record<string, any> = {}
currentScript.parameters.forEach(param => {
inputs[param.name] = param.type === 'number' ? 0 : param.type === 'boolean' ? false : ''
})
setTestInputs(inputs)
}, [currentScript?.id, currentScript?.parameters.length, setTestInputs])
}

View File

@@ -0,0 +1,110 @@
import { useMemo, useRef, useState } from 'react'
import { useMonaco } from '@monaco-editor/react'
import type { LuaScript } from '@/lib/level-types'
import type { LuaExecutionResult } from '@/lib/lua-engine'
import type { SecurityScanResult } from '@/lib/security-scanner'
import {
createAddScript,
createDeleteScript,
createUpdateScript,
} from '../handlers/scriptHandlers'
import {
createAddParameter,
createDeleteParameter,
createUpdateParameter,
createUpdateTestInput,
} from '../handlers/parameterHandlers'
import {
createInsertSnippet,
createToggleFullscreen,
} from '../handlers/snippetHandlers'
import {
createProceedExecution,
createScanCode,
createTestScript,
} from '../handlers/executionHandlers'
interface UseLuaEditorStateProps {
scripts: LuaScript[]
onScriptsChange: (scripts: LuaScript[]) => void
}
export function useLuaEditorState({ scripts, onScriptsChange }: UseLuaEditorStateProps) {
const [selectedScript, setSelectedScript] = useState<string | null>(
scripts.length > 0 ? scripts[0].id : null
)
const [testOutput, setTestOutput] = useState<LuaExecutionResult | null>(null)
const [testInputs, setTestInputs] = useState<Record<string, any>>({})
const [isExecuting, setIsExecuting] = useState(false)
const [isFullscreen, setIsFullscreen] = useState(false)
const [showSnippetLibrary, setShowSnippetLibrary] = useState(false)
const [securityScanResult, setSecurityScanResult] = useState<SecurityScanResult | null>(null)
const [showSecurityDialog, setShowSecurityDialog] = useState(false)
const editorRef = useRef<any>(null)
const monaco = useMonaco()
const currentScript = useMemo(
() => scripts.find(script => script.id === selectedScript) || null,
[scripts, selectedScript]
)
const handleUpdateScript = createUpdateScript({
scripts,
onScriptsChange,
selectedScript,
})
return {
monaco,
editorRef,
currentScript,
selectedScript,
setSelectedScript,
testOutput,
setTestOutput,
testInputs,
setTestInputs,
isExecuting,
setIsExecuting,
isFullscreen,
setIsFullscreen,
showSnippetLibrary,
setShowSnippetLibrary,
securityScanResult,
setSecurityScanResult,
showSecurityDialog,
setShowSecurityDialog,
handleAddScript: createAddScript({ scripts, onScriptsChange, setSelectedScript }),
handleDeleteScript: createDeleteScript({ scripts, onScriptsChange, selectedScript, setSelectedScript }),
handleUpdateScript,
handleAddParameter: createAddParameter({ currentScript, handleUpdateScript }),
handleDeleteParameter: createDeleteParameter({ currentScript, handleUpdateScript }),
handleUpdateParameter: createUpdateParameter({ currentScript, handleUpdateScript }),
handleUpdateTestInput: createUpdateTestInput({
getTestInputs: () => testInputs,
setTestInputs,
}),
handleInsertSnippet: createInsertSnippet({ currentScript, handleUpdateScript, editorRef, setShowSnippetLibrary }),
handleToggleFullscreen: createToggleFullscreen({ isFullscreen, setIsFullscreen }),
handleTestScript: createTestScript({
getCurrentScript: () => currentScript,
getTestInputs: () => testInputs,
setIsExecuting,
setTestOutput,
setSecurityScanResult,
setShowSecurityDialog,
}),
handleScanCode: createScanCode({
getCurrentScript: () => currentScript,
setSecurityScanResult,
setShowSecurityDialog,
}),
handleProceedWithExecution: createProceedExecution({
getCurrentScript: () => currentScript,
getTestInputs: () => testInputs,
setIsExecuting,
setTestOutput,
setShowSecurityDialog,
}),
}
}