mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Compare commits
1 Commits
a973b3cf8f
...
codex/refa
| Author | SHA1 | Date | |
|---|---|---|---|
| 018f5e22a2 |
133
frontends/nextjs/src/components/editors/lua/LuaBlocksBridge.tsx
Normal file
133
frontends/nextjs/src/components/editors/lua/LuaBlocksBridge.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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])
|
||||
|
||||
}
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user