mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 22:34:56 +00:00
594 lines
23 KiB
TypeScript
594 lines
23 KiB
TypeScript
import { useState, useEffect, useRef } from 'react'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
import { Plus, Trash, Play, CheckCircle, XCircle, FileCode, ArrowsOut, BookOpen } from '@phosphor-icons/react'
|
|
import { toast } from 'sonner'
|
|
import { createLuaEngine, type LuaExecutionResult } from '@/lib/lua-engine'
|
|
import { getLuaExampleCode, getLuaExamplesList } from '@/lib/lua-examples'
|
|
import type { LuaScript } from '@/lib/level-types'
|
|
import Editor, { useMonaco } from '@monaco-editor/react'
|
|
import type { editor } from 'monaco-editor'
|
|
import { LuaSnippetLibrary } from '@/components/LuaSnippetLibrary'
|
|
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '@/components/ui/sheet'
|
|
|
|
interface LuaEditorProps {
|
|
scripts: LuaScript[]
|
|
onScriptsChange: (scripts: LuaScript[]) => void
|
|
}
|
|
|
|
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 editorRef = useRef<editor.IStandaloneCodeEditor | null>(null)
|
|
const monaco = useMonaco()
|
|
|
|
const currentScript = scripts.find(s => s.id === selectedScript)
|
|
|
|
useEffect(() => {
|
|
if (monaco) {
|
|
monaco.languages.registerCompletionItemProvider('lua', {
|
|
provideCompletionItems: (model, position) => {
|
|
const word = model.getWordUntilPosition(position)
|
|
const range = {
|
|
startLineNumber: position.lineNumber,
|
|
endLineNumber: position.lineNumber,
|
|
startColumn: word.startColumn,
|
|
endColumn: word.endColumn
|
|
}
|
|
|
|
const suggestions: any[] = [
|
|
{
|
|
label: 'context.data',
|
|
kind: monaco.languages.CompletionItemKind.Property,
|
|
insertText: 'context.data',
|
|
documentation: 'Access input parameters passed to the script',
|
|
range
|
|
},
|
|
{
|
|
label: 'context.user',
|
|
kind: monaco.languages.CompletionItemKind.Property,
|
|
insertText: 'context.user',
|
|
documentation: 'Current user information (username, role, etc.)',
|
|
range
|
|
},
|
|
{
|
|
label: 'context.kv',
|
|
kind: monaco.languages.CompletionItemKind.Property,
|
|
insertText: 'context.kv',
|
|
documentation: 'Key-value storage interface',
|
|
range
|
|
},
|
|
{
|
|
label: 'context.log',
|
|
kind: monaco.languages.CompletionItemKind.Function,
|
|
insertText: 'context.log(${1:message})',
|
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
documentation: 'Log a message to the output console',
|
|
range
|
|
},
|
|
{
|
|
label: 'log',
|
|
kind: monaco.languages.CompletionItemKind.Function,
|
|
insertText: 'log(${1:message})',
|
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
documentation: 'Log a message (shortcut for context.log)',
|
|
range
|
|
},
|
|
{
|
|
label: 'print',
|
|
kind: monaco.languages.CompletionItemKind.Function,
|
|
insertText: 'print(${1:message})',
|
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
documentation: 'Print a message to output',
|
|
range
|
|
},
|
|
{
|
|
label: 'return',
|
|
kind: monaco.languages.CompletionItemKind.Keyword,
|
|
insertText: 'return ${1:result}',
|
|
insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
|
|
documentation: 'Return a value from the script',
|
|
range
|
|
},
|
|
]
|
|
|
|
return { suggestions }
|
|
}
|
|
})
|
|
|
|
monaco.languages.setLanguageConfiguration('lua', {
|
|
comments: {
|
|
lineComment: '--',
|
|
blockComment: ['--[[', ']]']
|
|
},
|
|
brackets: [
|
|
['{', '}'],
|
|
['[', ']'],
|
|
['(', ')']
|
|
],
|
|
autoClosingPairs: [
|
|
{ open: '{', close: '}' },
|
|
{ open: '[', close: ']' },
|
|
{ open: '(', close: ')' },
|
|
{ open: '"', close: '"' },
|
|
{ open: "'", close: "'" }
|
|
]
|
|
})
|
|
}
|
|
}, [monaco])
|
|
|
|
useEffect(() => {
|
|
if (currentScript) {
|
|
const inputs: Record<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
|
|
|
|
setIsExecuting(true)
|
|
setTestOutput(null)
|
|
|
|
try {
|
|
const engine = createLuaEngine()
|
|
|
|
const contextData: any = {}
|
|
currentScript.parameters.forEach((param) => {
|
|
contextData[param.name] = testInputs[param.name]
|
|
})
|
|
|
|
const result = await engine.execute(currentScript.code, {
|
|
data: contextData,
|
|
user: { username: 'test_user', role: 'god' },
|
|
log: (...args: any[]) => console.log('[Lua]', ...args)
|
|
})
|
|
|
|
setTestOutput(result)
|
|
|
|
if (result.success) {
|
|
toast.success('Script executed successfully')
|
|
} else {
|
|
toast.error('Script execution failed')
|
|
}
|
|
|
|
engine.destroy()
|
|
} catch (error) {
|
|
toast.error('Execution error: ' + (error instanceof Error ? error.message : String(error)))
|
|
setTestOutput({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
logs: []
|
|
})
|
|
} finally {
|
|
setIsExecuting(false)
|
|
}
|
|
}
|
|
|
|
const handleAddParameter = () => {
|
|
if (!currentScript) return
|
|
|
|
const newParam = { name: `param${currentScript.parameters.length + 1}`, type: 'string' }
|
|
handleUpdateScript({
|
|
parameters: [...currentScript.parameters, newParam],
|
|
})
|
|
}
|
|
|
|
const handleDeleteParameter = (index: number) => {
|
|
if (!currentScript) return
|
|
|
|
handleUpdateScript({
|
|
parameters: currentScript.parameters.filter((_, i) => i !== index),
|
|
})
|
|
}
|
|
|
|
const handleUpdateParameter = (index: number, updates: { name?: string; type?: string }) => {
|
|
if (!currentScript) return
|
|
|
|
handleUpdateScript({
|
|
parameters: currentScript.parameters.map((p, i) =>
|
|
i === index ? { ...p, ...updates } : p
|
|
),
|
|
})
|
|
}
|
|
|
|
const handleInsertSnippet = (code: string) => {
|
|
if (!currentScript) return
|
|
|
|
if (editorRef.current) {
|
|
const selection = editorRef.current.getSelection()
|
|
if (selection) {
|
|
editorRef.current.executeEdits('', [{
|
|
range: selection,
|
|
text: code,
|
|
forceMoveMarkers: true
|
|
}])
|
|
editorRef.current.focus()
|
|
} else {
|
|
const currentCode = currentScript.code
|
|
const newCode = currentCode ? currentCode + '\n\n' + code : code
|
|
handleUpdateScript({ code: newCode })
|
|
}
|
|
} else {
|
|
const currentCode = currentScript.code
|
|
const newCode = currentCode ? currentCode + '\n\n' + code : code
|
|
handleUpdateScript({ code: newCode })
|
|
}
|
|
|
|
setShowSnippetLibrary(false)
|
|
}
|
|
|
|
return (
|
|
<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>
|
|
|
|
<Card className="md:col-span-2">
|
|
{!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>
|
|
</div>
|
|
</CardContent>
|
|
) : (
|
|
<>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<CardTitle>Edit Script: {currentScript.name}</CardTitle>
|
|
<CardDescription>Write custom Lua logic</CardDescription>
|
|
</div>
|
|
<Button onClick={handleTestScript} disabled={isExecuting}>
|
|
<Play className="mr-2" size={16} />
|
|
{isExecuting ? 'Executing...' : 'Test Script'}
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<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>
|
|
|
|
<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>
|
|
</CardContent>
|
|
</>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|