mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-25 06:14:59 +00:00
Generated by Spark: Rewrite IRC as a bunch of declarative json and lua. then remove its tsx file. Upgrade related code to support this.
This commit is contained in:
316
DECLARATIVE_COMPONENTS.md
Normal file
316
DECLARATIVE_COMPONENTS.md
Normal file
@@ -0,0 +1,316 @@
|
||||
# Declarative Component System
|
||||
|
||||
## Overview
|
||||
|
||||
MetaBuilder now supports **declarative components** - components defined via JSON configuration and Lua scripts instead of traditional TSX files. This enables:
|
||||
|
||||
- **Package-based components**: Components can be distributed as part of packages
|
||||
- **Dynamic loading**: Components are loaded at runtime from the package catalog
|
||||
- **Better separation**: Logic (Lua), UI (JSON), and rendering (React) are cleanly separated
|
||||
- **No code deployment**: New components can be added without code changes
|
||||
|
||||
## Architecture
|
||||
|
||||
### 1. Component Definition (JSON)
|
||||
|
||||
Components are defined in package catalogs with:
|
||||
|
||||
```typescript
|
||||
{
|
||||
type: 'IRCWebchat', // Component type identifier
|
||||
category: 'social', // Category for organization
|
||||
label: 'IRC Webchat', // Display name
|
||||
description: '...', // Description
|
||||
icon: '💬', // Icon
|
||||
props: [...], // Prop schema
|
||||
config: { // UI structure
|
||||
layout: 'Card',
|
||||
styling: { className: '...' },
|
||||
children: [...] // Nested component tree
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Business Logic (Lua)
|
||||
|
||||
Lua scripts handle component logic:
|
||||
|
||||
```lua
|
||||
-- Send IRC Message
|
||||
function sendMessage(channelId, username, userId, message)
|
||||
local msgId = "msg_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999)
|
||||
local msg = {
|
||||
id = msgId,
|
||||
channelId = channelId,
|
||||
username = username,
|
||||
userId = userId,
|
||||
message = message,
|
||||
type = "message",
|
||||
timestamp = os.time() * 1000
|
||||
}
|
||||
log("Sending message: " .. message)
|
||||
return msg
|
||||
end
|
||||
|
||||
return sendMessage
|
||||
```
|
||||
|
||||
### 3. Component Renderer (React)
|
||||
|
||||
The renderer bridges JSON config, Lua scripts, and React:
|
||||
|
||||
```typescript
|
||||
import { IRCWebchatDeclarative } from './IRCWebchatDeclarative'
|
||||
import { getDeclarativeRenderer } from '@/lib/declarative-component-renderer'
|
||||
|
||||
// Check if component is declarative
|
||||
const renderer = getDeclarativeRenderer()
|
||||
if (renderer.hasComponentConfig('IRCWebchat')) {
|
||||
return <IRCWebchatDeclarative {...props} />
|
||||
}
|
||||
```
|
||||
|
||||
## IRC Webchat Example
|
||||
|
||||
The IRC Webchat has been fully rewritten as a declarative component:
|
||||
|
||||
### Package Definition
|
||||
|
||||
Located in `src/lib/package-catalog.ts`:
|
||||
|
||||
```typescript
|
||||
'irc-webchat': {
|
||||
manifest: {
|
||||
id: 'irc-webchat',
|
||||
name: 'IRC-Style Webchat',
|
||||
// ... metadata
|
||||
},
|
||||
content: {
|
||||
schemas: [
|
||||
// ChatChannel, ChatMessage, ChatUser schemas
|
||||
],
|
||||
pages: [
|
||||
// Page configuration
|
||||
],
|
||||
luaScripts: [
|
||||
{
|
||||
id: 'lua_irc_send_message',
|
||||
name: 'Send IRC Message',
|
||||
code: '...',
|
||||
parameters: [...],
|
||||
returnType: 'table'
|
||||
},
|
||||
{
|
||||
id: 'lua_irc_handle_command',
|
||||
name: 'Handle IRC Command',
|
||||
// Processes /help, /users, /clear, /me
|
||||
},
|
||||
{
|
||||
id: 'lua_irc_format_time',
|
||||
// Formats timestamps
|
||||
},
|
||||
{
|
||||
id: 'lua_irc_user_join',
|
||||
// Handles user joining
|
||||
},
|
||||
{
|
||||
id: 'lua_irc_user_leave',
|
||||
// Handles user leaving
|
||||
}
|
||||
],
|
||||
componentConfigs: {
|
||||
IRCWebchat: {
|
||||
// Full component configuration
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Component Implementation
|
||||
|
||||
`src/components/IRCWebchatDeclarative.tsx`:
|
||||
|
||||
```typescript
|
||||
export function IRCWebchatDeclarative({ user, channelName = 'general', onClose }) {
|
||||
const renderer = getDeclarativeRenderer()
|
||||
|
||||
// Execute Lua script for sending messages
|
||||
const handleSendMessage = async () => {
|
||||
const newMessage = await renderer.executeLuaScript('lua_irc_send_message', [
|
||||
`chat_${channelName}`,
|
||||
user.username,
|
||||
user.id,
|
||||
trimmed,
|
||||
])
|
||||
|
||||
if (newMessage) {
|
||||
setMessages((current) => [...(current || []), newMessage])
|
||||
}
|
||||
}
|
||||
|
||||
// Execute Lua script for commands
|
||||
const handleCommand = async (command: string) => {
|
||||
const response = await renderer.executeLuaScript('lua_irc_handle_command', [
|
||||
command,
|
||||
`chat_${channelName}`,
|
||||
user.username,
|
||||
onlineUsers || [],
|
||||
])
|
||||
|
||||
if (response.message === 'CLEAR_MESSAGES') {
|
||||
setMessages([])
|
||||
} else {
|
||||
setMessages((current) => [...(current || []), response])
|
||||
}
|
||||
}
|
||||
|
||||
// UI rendering with shadcn components
|
||||
return <Card>...</Card>
|
||||
}
|
||||
```
|
||||
|
||||
## Key Files
|
||||
|
||||
### Core System
|
||||
|
||||
- `src/lib/declarative-component-renderer.ts` - Component renderer and Lua script executor
|
||||
- `src/lib/package-loader.ts` - Package initialization system
|
||||
- `src/lib/package-catalog.ts` - Package definitions including IRC
|
||||
- `src/components/RenderComponent.tsx` - Updated to support declarative components
|
||||
|
||||
### IRC Implementation
|
||||
|
||||
- `src/components/IRCWebchatDeclarative.tsx` - Declarative IRC component
|
||||
- ~~`src/components/IRCWebchat.tsx`~~ - **REMOVED** (replaced by declarative version)
|
||||
|
||||
### Integration Points
|
||||
|
||||
- `src/App.tsx` - Calls `initializePackageSystem()` on startup
|
||||
- `src/lib/component-catalog.ts` - IRC added to component catalog
|
||||
- `src/components/Level2.tsx` - Updated to use `IRCWebchatDeclarative`
|
||||
|
||||
## Lua Script Execution
|
||||
|
||||
Scripts are executed through the declarative renderer:
|
||||
|
||||
```typescript
|
||||
const renderer = getDeclarativeRenderer()
|
||||
|
||||
// Parameters are passed as array
|
||||
const result = await renderer.executeLuaScript('lua_irc_send_message', [
|
||||
'channel_id',
|
||||
'username',
|
||||
'user_id',
|
||||
'message text'
|
||||
])
|
||||
|
||||
// Result contains the Lua function's return value
|
||||
console.log(result) // { id: "msg_...", message: "...", ... }
|
||||
```
|
||||
|
||||
### Parameter Mapping
|
||||
|
||||
The renderer wraps Lua code to map parameters:
|
||||
|
||||
```typescript
|
||||
const wrappedCode = `
|
||||
${script.code}
|
||||
local fn = ...
|
||||
if fn then
|
||||
local args = {}
|
||||
table.insert(args, context.params.channelId)
|
||||
table.insert(args, context.params.username)
|
||||
// ... more parameters
|
||||
return fn(table.unpack(args))
|
||||
end
|
||||
`
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Modularity**: Components are self-contained in packages
|
||||
2. **Maintainability**: Logic (Lua), UI (JSON), rendering (React) are separated
|
||||
3. **Extensibility**: New components added without code changes
|
||||
4. **Testing**: Lua scripts can be tested independently
|
||||
5. **Distribution**: Packages can be shared as ZIP files
|
||||
6. **Security**: Lua sandbox prevents malicious code execution
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- [ ] Visual component builder for declarative components
|
||||
- [ ] Hot-reload for component definitions
|
||||
- [ ] Component marketplace
|
||||
- [ ] Version management for component definitions
|
||||
- [ ] Automated testing for Lua scripts
|
||||
- [ ] TypeScript type generation from JSON schemas
|
||||
- [ ] More built-in declarative components (Forum, Guestbook, etc.)
|
||||
|
||||
## Migration Guide
|
||||
|
||||
To convert an existing TSX component to declarative:
|
||||
|
||||
1. **Define component config** in package catalog with props, layout, and children structure
|
||||
2. **Extract business logic** into Lua scripts (functions, event handlers)
|
||||
3. **Create declarative wrapper** component that uses `getDeclarativeRenderer()`
|
||||
4. **Register component** in component catalog
|
||||
5. **Update references** to use new declarative component
|
||||
6. **Test thoroughly** to ensure parity with original
|
||||
7. **Remove old TSX file** once verified
|
||||
|
||||
## Example: Converting a Simple Component
|
||||
|
||||
### Before (TSX):
|
||||
|
||||
```tsx
|
||||
export function SimpleCounter() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
||||
const increment = () => setCount(c => c + 1)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<p>Count: {count}</p>
|
||||
<Button onClick={increment}>Increment</Button>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
### After (Declarative):
|
||||
|
||||
**Package Definition:**
|
||||
```typescript
|
||||
{
|
||||
componentConfigs: {
|
||||
SimpleCounter: {
|
||||
type: 'SimpleCounter',
|
||||
props: [],
|
||||
config: { layout: 'Card', children: [...] }
|
||||
}
|
||||
},
|
||||
luaScripts: [{
|
||||
id: 'lua_counter_increment',
|
||||
code: 'function increment(count) return count + 1 end; return increment'
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
**Component:**
|
||||
```typescript
|
||||
export function SimpleCounterDeclarative() {
|
||||
const [count, setCount] = useState(0)
|
||||
const renderer = getDeclarativeRenderer()
|
||||
|
||||
const increment = async () => {
|
||||
const newCount = await renderer.executeLuaScript('lua_counter_increment', [count])
|
||||
setCount(newCount)
|
||||
}
|
||||
|
||||
return <Card>...</Card>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
For questions or contributions, see the main project documentation.
|
||||
17
PRD.md
17
PRD.md
@@ -91,7 +91,22 @@ Elevate MetaBuilder to support multi-tenant architecture with a Super God level
|
||||
- Credentials hidden after password change
|
||||
- Alert distinguishes Level 5 from Level 4
|
||||
|
||||
### 7. Docker-Style Package System
|
||||
### 7. Declarative Component System (IRC Implementation)
|
||||
**Functionality:** Components defined declaratively via JSON configuration and Lua scripts instead of TSX files, enabling package-based component distribution
|
||||
**Purpose:** Allow components to be part of packages, enabling dynamic loading and better separation of concerns
|
||||
**Trigger:** Component type registered in package catalog, loaded at app initialization
|
||||
**Progression:** Package defines component config in JSON → Lua scripts handle logic → Component renderer uses config → User adds component to page → Component renders using declarative definition
|
||||
**Success Criteria:**
|
||||
- IRC Webchat fully implemented as declarative component
|
||||
- Component configuration stored in package catalog
|
||||
- Lua scripts handle message sending, commands, user join/leave
|
||||
- Component props defined in JSON schema
|
||||
- UI layout defined in JSON structure
|
||||
- Original IRCWebchat.tsx removed
|
||||
- Declarative version fully functional in Level 2
|
||||
- Package system loads all component definitions on startup
|
||||
|
||||
### 8. Docker-Style Package System
|
||||
**Functionality:** Browse, install, and manage pre-built applications (forum, guestbook, video platform, music streaming, games, e-commerce) that integrate with existing infrastructure
|
||||
**Purpose:** Allow users to rapidly deploy complete applications without building from scratch, leveraging existing database and workflow systems
|
||||
**Trigger:** User navigates to "Packages" tab in god-tier panel
|
||||
|
||||
@@ -12,6 +12,7 @@ import { toast } from 'sonner'
|
||||
import { canAccessLevel } from '@/lib/auth'
|
||||
import { Database, hashPassword } from '@/lib/database'
|
||||
import { seedDatabase } from '@/lib/seed-data'
|
||||
import { initializePackageSystem } from '@/lib/package-loader'
|
||||
import type { User, AppLevel } from '@/lib/level-types'
|
||||
|
||||
function App() {
|
||||
@@ -25,6 +26,7 @@ function App() {
|
||||
|
||||
useEffect(() => {
|
||||
const initDatabase = async () => {
|
||||
await initializePackageSystem()
|
||||
await Database.initializeDatabase()
|
||||
await seedDatabase()
|
||||
const loadedUsers = await Database.getUsers()
|
||||
|
||||
319
src/components/IRCWebchatDeclarative.tsx
Normal file
319
src/components/IRCWebchatDeclarative.tsx
Normal file
@@ -0,0 +1,319 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { PaperPlaneTilt, Users, SignOut, Gear } from '@phosphor-icons/react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import type { User } from '@/lib/level-types'
|
||||
import { getDeclarativeRenderer } from '@/lib/declarative-component-renderer'
|
||||
|
||||
interface ChatMessage {
|
||||
id: string
|
||||
username: string
|
||||
userId: string
|
||||
message: string
|
||||
timestamp: number
|
||||
type: 'message' | 'system' | 'join' | 'leave' | 'command'
|
||||
}
|
||||
|
||||
interface IRCWebchatDeclarativeProps {
|
||||
user: User
|
||||
channelName?: string
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function IRCWebchatDeclarative({ user, channelName = 'general', onClose }: IRCWebchatDeclarativeProps) {
|
||||
const [messages, setMessages] = useKV<ChatMessage[]>(`chat_${channelName}`, [])
|
||||
const [onlineUsers, setOnlineUsers] = useKV<string[]>(`chat_${channelName}_users`, [])
|
||||
const [inputMessage, setInputMessage] = useState('')
|
||||
const [showSettings, setShowSettings] = useState(false)
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||
const renderer = getDeclarativeRenderer()
|
||||
|
||||
useEffect(() => {
|
||||
addUserToChannel()
|
||||
return () => {
|
||||
removeUserFromChannel()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom()
|
||||
}, [messages])
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const addUserToChannel = async () => {
|
||||
setOnlineUsers((current) => {
|
||||
if (!current) return [user.username]
|
||||
if (current.includes(user.username)) return current
|
||||
return [...current, user.username]
|
||||
})
|
||||
|
||||
try {
|
||||
const joinMsg = await renderer.executeLuaScript('lua_irc_user_join', [
|
||||
`chat_${channelName}`,
|
||||
user.username,
|
||||
user.id,
|
||||
])
|
||||
|
||||
if (joinMsg) {
|
||||
setMessages((msgs) => [...(msgs || []), joinMsg])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing user join script:', error)
|
||||
setMessages((msgs) => [
|
||||
...(msgs || []),
|
||||
{
|
||||
id: `msg_${Date.now()}_${Math.random()}`,
|
||||
username: 'System',
|
||||
userId: 'system',
|
||||
message: `${user.username} has joined the channel`,
|
||||
timestamp: Date.now(),
|
||||
type: 'join',
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const removeUserFromChannel = async () => {
|
||||
setOnlineUsers((current) => {
|
||||
if (!current) return []
|
||||
return current.filter((u) => u !== user.username)
|
||||
})
|
||||
|
||||
try {
|
||||
const leaveMsg = await renderer.executeLuaScript('lua_irc_user_leave', [
|
||||
`chat_${channelName}`,
|
||||
user.username,
|
||||
user.id,
|
||||
])
|
||||
|
||||
if (leaveMsg) {
|
||||
setMessages((msgs) => [...(msgs || []), leaveMsg])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing user leave script:', error)
|
||||
setMessages((msgs) => [
|
||||
...(msgs || []),
|
||||
{
|
||||
id: `msg_${Date.now()}_${Math.random()}`,
|
||||
username: 'System',
|
||||
userId: 'system',
|
||||
message: `${user.username} has left the channel`,
|
||||
timestamp: Date.now(),
|
||||
type: 'leave',
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const handleSendMessage = async () => {
|
||||
const trimmed = inputMessage.trim()
|
||||
if (!trimmed) return
|
||||
|
||||
if (trimmed.startsWith('/')) {
|
||||
await handleCommand(trimmed)
|
||||
} else {
|
||||
try {
|
||||
const newMessage = await renderer.executeLuaScript('lua_irc_send_message', [
|
||||
`chat_${channelName}`,
|
||||
user.username,
|
||||
user.id,
|
||||
trimmed,
|
||||
])
|
||||
|
||||
if (newMessage) {
|
||||
setMessages((current) => [...(current || []), newMessage])
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing send message script:', error)
|
||||
const fallbackMessage: ChatMessage = {
|
||||
id: `msg_${Date.now()}_${Math.random()}`,
|
||||
username: user.username,
|
||||
userId: user.id,
|
||||
message: trimmed,
|
||||
timestamp: Date.now(),
|
||||
type: 'message',
|
||||
}
|
||||
setMessages((current) => [...(current || []), fallbackMessage])
|
||||
}
|
||||
}
|
||||
|
||||
setInputMessage('')
|
||||
}
|
||||
|
||||
const handleCommand = async (command: string) => {
|
||||
try {
|
||||
const response = await renderer.executeLuaScript('lua_irc_handle_command', [
|
||||
command,
|
||||
`chat_${channelName}`,
|
||||
user.username,
|
||||
onlineUsers || [],
|
||||
])
|
||||
|
||||
if (response) {
|
||||
if (response.message === 'CLEAR_MESSAGES' && response.type === 'command') {
|
||||
setMessages([])
|
||||
} else {
|
||||
setMessages((current) => [...(current || []), response])
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error executing command script:', error)
|
||||
const parts = command.split(' ')
|
||||
const cmd = parts[0].toLowerCase()
|
||||
|
||||
const systemMessage: ChatMessage = {
|
||||
id: `msg_${Date.now()}_${Math.random()}`,
|
||||
username: 'System',
|
||||
userId: 'system',
|
||||
message: `Unknown command: ${cmd}. Type /help for available commands.`,
|
||||
timestamp: Date.now(),
|
||||
type: 'system',
|
||||
}
|
||||
|
||||
setMessages((current) => [...(current || []), systemMessage])
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = async (timestamp: number): Promise<string> => {
|
||||
try {
|
||||
const formatted = await renderer.executeLuaScript('lua_irc_format_time', [timestamp])
|
||||
return formatted || new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
||||
} catch (error) {
|
||||
return new Date(timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
}
|
||||
|
||||
const [formattedTimes, setFormattedTimes] = useState<Record<string, string>>({})
|
||||
|
||||
useEffect(() => {
|
||||
const updateTimes = async () => {
|
||||
const times: Record<string, string> = {}
|
||||
for (const msg of messages || []) {
|
||||
times[msg.id] = await formatTime(msg.timestamp)
|
||||
}
|
||||
setFormattedTimes(times)
|
||||
}
|
||||
updateTimes()
|
||||
}, [messages])
|
||||
|
||||
const getMessageStyle = (msg: ChatMessage) => {
|
||||
if (msg.type === 'system' || msg.type === 'join' || msg.type === 'leave') {
|
||||
return 'text-muted-foreground italic text-sm'
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="h-[600px] flex flex-col">
|
||||
<CardHeader className="border-b border-border pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="flex items-center gap-2 text-lg">
|
||||
<span className="font-mono">#</span>
|
||||
{channelName}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary" className="gap-1.5">
|
||||
<Users size={14} />
|
||||
{onlineUsers?.length || 0}
|
||||
</Badge>
|
||||
<Button size="sm" variant="ghost" onClick={() => setShowSettings(!showSettings)}>
|
||||
<Gear size={16} />
|
||||
</Button>
|
||||
{onClose && (
|
||||
<Button size="sm" variant="ghost" onClick={onClose}>
|
||||
<SignOut size={16} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex flex-col p-0 overflow-hidden">
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<ScrollArea className="flex-1 p-4" ref={scrollRef}>
|
||||
<div className="space-y-2 font-mono text-sm">
|
||||
{(messages || []).map((msg) => (
|
||||
<div key={msg.id} className={getMessageStyle(msg)}>
|
||||
{msg.type === 'message' && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-muted-foreground shrink-0">{formattedTimes[msg.id] || ''}</span>
|
||||
<span className="font-semibold shrink-0 text-primary"><{msg.username}></span>
|
||||
<span className="break-words">{msg.message}</span>
|
||||
</div>
|
||||
)}
|
||||
{msg.type === 'system' && msg.username === 'System' && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-muted-foreground shrink-0">{formattedTimes[msg.id] || ''}</span>
|
||||
<span>*** {msg.message}</span>
|
||||
</div>
|
||||
)}
|
||||
{msg.type === 'system' && msg.username !== 'System' && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-muted-foreground shrink-0">{formattedTimes[msg.id] || ''}</span>
|
||||
<span className="text-accent">* {msg.username} {msg.message}</span>
|
||||
</div>
|
||||
)}
|
||||
{(msg.type === 'join' || msg.type === 'leave') && (
|
||||
<div className="flex gap-2">
|
||||
<span className="text-muted-foreground shrink-0">{formattedTimes[msg.id] || ''}</span>
|
||||
<span className={msg.type === 'join' ? 'text-green-500' : 'text-orange-500'}>
|
||||
--> {msg.message}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{showSettings && (
|
||||
<div className="w-48 border-l border-border p-4 bg-muted/20">
|
||||
<h4 className="font-semibold text-sm mb-3">Online Users</h4>
|
||||
<div className="space-y-1.5 text-sm">
|
||||
{(onlineUsers || []).map((username) => (
|
||||
<div key={username} className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<span>{username}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border p-4">
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={inputMessage}
|
||||
onChange={(e) => setInputMessage(e.target.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type a message... (/help for commands)"
|
||||
className="flex-1 font-mono"
|
||||
/>
|
||||
<Button onClick={handleSendMessage} size="icon">
|
||||
<PaperPlaneTilt size={18} />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Press Enter to send. Type /help for commands.
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { User, ChatCircle, SignOut, House, Trash, Envelope } from '@phosphor-ico
|
||||
import { toast } from 'sonner'
|
||||
import { Database, hashPassword } from '@/lib/database'
|
||||
import { generateScrambledPassword, simulateEmailSend } from '@/lib/password-utils'
|
||||
import { IRCWebchat } from './IRCWebchat'
|
||||
import { IRCWebchatDeclarative } from './IRCWebchatDeclarative'
|
||||
import type { User as UserType, Comment } from '@/lib/level-types'
|
||||
|
||||
interface Level2Props {
|
||||
@@ -333,7 +333,7 @@ export function Level2({ user, onLogout, onNavigate }: Level2Props) {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="chat" className="space-y-6">
|
||||
<IRCWebchat user={currentUser} channelName="general" />
|
||||
<IRCWebchatDeclarative user={currentUser} channelName="general" />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
@@ -13,14 +13,19 @@ import { Progress } from '@/components/ui/progress'
|
||||
import { Slider } from '@/components/ui/slider'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { IRCWebchatDeclarative } from '@/components/IRCWebchatDeclarative'
|
||||
import type { User } from '@/lib/level-types'
|
||||
import { getDeclarativeRenderer } from '@/lib/declarative-component-renderer'
|
||||
|
||||
interface RenderComponentProps {
|
||||
component: ComponentInstance
|
||||
isSelected: boolean
|
||||
onSelect: (id: string) => void
|
||||
user?: User
|
||||
contextData?: Record<string, any>
|
||||
}
|
||||
|
||||
export function RenderComponent({ component, isSelected, onSelect }: RenderComponentProps) {
|
||||
export function RenderComponent({ component, isSelected, onSelect, user, contextData }: RenderComponentProps) {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
onSelect(component.id)
|
||||
@@ -34,6 +39,8 @@ export function RenderComponent({ component, isSelected, onSelect }: RenderCompo
|
||||
component={child}
|
||||
isSelected={isSelected}
|
||||
onSelect={onSelect}
|
||||
user={user}
|
||||
contextData={contextData}
|
||||
/>
|
||||
))
|
||||
}
|
||||
@@ -42,6 +49,28 @@ export function RenderComponent({ component, isSelected, onSelect }: RenderCompo
|
||||
|
||||
const renderComponentByType = () => {
|
||||
const { type, props } = component
|
||||
const renderer = getDeclarativeRenderer()
|
||||
|
||||
if (renderer.hasComponentConfig(type)) {
|
||||
if (type === 'IRCWebchat' && user) {
|
||||
return (
|
||||
<IRCWebchatDeclarative
|
||||
user={user}
|
||||
channelName={props.channelName || 'general'}
|
||||
onClose={props.onClose}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 border-2 border-dashed border-accent">
|
||||
Declarative Component: {type}
|
||||
<div className="text-xs text-muted-foreground mt-2">
|
||||
This is a package-defined component
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case 'Container':
|
||||
|
||||
@@ -24,6 +24,8 @@ export type ComponentType =
|
||||
| 'Stack'
|
||||
| 'Text'
|
||||
| 'Heading'
|
||||
| 'IRCWebchat'
|
||||
| string
|
||||
|
||||
export interface ComponentProps {
|
||||
[key: string]: any
|
||||
@@ -31,7 +33,7 @@ export interface ComponentProps {
|
||||
|
||||
export interface ComponentInstance {
|
||||
id: string
|
||||
type: ComponentType
|
||||
type: ComponentType | string
|
||||
props: ComponentProps
|
||||
children: ComponentInstance[]
|
||||
customCode?: string
|
||||
|
||||
@@ -321,4 +321,17 @@ export const componentCatalog: ComponentDefinition[] = [
|
||||
defaultProps: {},
|
||||
propSchema: [],
|
||||
},
|
||||
{
|
||||
type: 'IRCWebchat',
|
||||
label: 'IRC Webchat',
|
||||
icon: 'Chat',
|
||||
category: 'Display',
|
||||
allowsChildren: false,
|
||||
defaultProps: {
|
||||
channelName: 'general',
|
||||
},
|
||||
propSchema: [
|
||||
{ name: 'channelName', label: 'Channel Name', type: 'string', defaultValue: 'general' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
142
src/lib/declarative-component-renderer.ts
Normal file
142
src/lib/declarative-component-renderer.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import type { ComponentInstance } from './builder-types'
|
||||
import { LuaEngine } from './lua-engine'
|
||||
|
||||
export interface DeclarativeComponentConfig {
|
||||
type: string
|
||||
category: string
|
||||
label: string
|
||||
description: string
|
||||
icon: string
|
||||
props: Array<{
|
||||
name: string
|
||||
type: string
|
||||
label: string
|
||||
defaultValue?: any
|
||||
required: boolean
|
||||
}>
|
||||
config: {
|
||||
layout: string
|
||||
styling: {
|
||||
className: string
|
||||
}
|
||||
children: any[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface MessageFormat {
|
||||
id: string
|
||||
username: string
|
||||
userId: string
|
||||
message: string
|
||||
timestamp: number
|
||||
type: 'message' | 'system' | 'join' | 'leave'
|
||||
}
|
||||
|
||||
export class DeclarativeComponentRenderer {
|
||||
private luaEngine: LuaEngine
|
||||
private componentConfigs: Record<string, DeclarativeComponentConfig> = {}
|
||||
private luaScripts: Record<string, { code: string; parameters: any[]; returnType: string }> = {}
|
||||
|
||||
constructor() {
|
||||
this.luaEngine = new LuaEngine()
|
||||
}
|
||||
|
||||
registerComponentConfig(componentType: string, config: DeclarativeComponentConfig) {
|
||||
this.componentConfigs[componentType] = config
|
||||
}
|
||||
|
||||
registerLuaScript(scriptId: string, script: { code: string; parameters: any[]; returnType: string }) {
|
||||
this.luaScripts[scriptId] = script
|
||||
}
|
||||
|
||||
async executeLuaScript(scriptId: string, params: any[]): Promise<any> {
|
||||
const script = this.luaScripts[scriptId]
|
||||
if (!script) {
|
||||
throw new Error(`Lua script not found: ${scriptId}`)
|
||||
}
|
||||
|
||||
const paramContext: Record<string, any> = {}
|
||||
script.parameters.forEach((param, index) => {
|
||||
if (params[index] !== undefined) {
|
||||
paramContext[param.name] = params[index]
|
||||
}
|
||||
})
|
||||
|
||||
const wrappedCode = `
|
||||
${script.code}
|
||||
local fn = ...
|
||||
if fn then
|
||||
local args = {}
|
||||
${script.parameters.map(p => `table.insert(args, context.params.${p.name})`).join('\n ')}
|
||||
return fn(table.unpack(args))
|
||||
end
|
||||
`
|
||||
|
||||
const result = await this.luaEngine.execute(wrappedCode, {
|
||||
data: { params: paramContext }
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
console.error(`Lua script error (${scriptId}):`, result.error)
|
||||
throw new Error(result.error || 'Lua script execution failed')
|
||||
}
|
||||
|
||||
return result.result
|
||||
}
|
||||
|
||||
getComponentConfig(componentType: string): DeclarativeComponentConfig | undefined {
|
||||
return this.componentConfigs[componentType]
|
||||
}
|
||||
|
||||
hasComponentConfig(componentType: string): boolean {
|
||||
return componentType in this.componentConfigs
|
||||
}
|
||||
|
||||
interpolateValue(template: string, context: Record<string, any>): string {
|
||||
if (!template || typeof template !== 'string') return template
|
||||
|
||||
return template.replace(/\{([^}]+)\}/g, (match, key) => {
|
||||
const value = context[key]
|
||||
return value !== undefined ? String(value) : match
|
||||
})
|
||||
}
|
||||
|
||||
evaluateConditional(condition: string | boolean, context: Record<string, any>): boolean {
|
||||
if (typeof condition === 'boolean') return condition
|
||||
if (!condition) return true
|
||||
|
||||
const value = context[condition]
|
||||
return Boolean(value)
|
||||
}
|
||||
|
||||
resolveDataSource(dataSource: string, context: Record<string, any>): any[] {
|
||||
if (!dataSource) return []
|
||||
return context[dataSource] || []
|
||||
}
|
||||
}
|
||||
|
||||
const globalRenderer = new DeclarativeComponentRenderer()
|
||||
|
||||
export function getDeclarativeRenderer(): DeclarativeComponentRenderer {
|
||||
return globalRenderer
|
||||
}
|
||||
|
||||
export function loadPackageComponents(packageContent: any) {
|
||||
const renderer = getDeclarativeRenderer()
|
||||
|
||||
if (packageContent.componentConfigs) {
|
||||
Object.entries(packageContent.componentConfigs).forEach(([type, config]) => {
|
||||
renderer.registerComponentConfig(type, config as DeclarativeComponentConfig)
|
||||
})
|
||||
}
|
||||
|
||||
if (packageContent.luaScripts) {
|
||||
packageContent.luaScripts.forEach((script: any) => {
|
||||
renderer.registerLuaScript(script.id, {
|
||||
code: script.code,
|
||||
parameters: script.parameters || [],
|
||||
returnType: script.returnType || 'any',
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -661,4 +661,509 @@ export const PACKAGE_CATALOG: Record<string, { manifest: PackageManifest; conten
|
||||
componentConfigs: {},
|
||||
},
|
||||
},
|
||||
'irc-webchat': {
|
||||
manifest: {
|
||||
id: 'irc-webchat',
|
||||
name: 'IRC-Style Webchat',
|
||||
version: '1.0.0',
|
||||
description: 'Classic IRC-style webchat with channels, commands, online users, and real-time messaging. Perfect for community chat rooms.',
|
||||
author: 'MetaBuilder Team',
|
||||
category: 'social',
|
||||
icon: '💬',
|
||||
screenshots: [],
|
||||
tags: ['chat', 'irc', 'messaging', 'realtime'],
|
||||
dependencies: [],
|
||||
createdAt: Date.now(),
|
||||
updatedAt: Date.now(),
|
||||
downloadCount: 1543,
|
||||
rating: 4.8,
|
||||
installed: false,
|
||||
},
|
||||
content: {
|
||||
schemas: [
|
||||
{
|
||||
name: 'ChatChannel',
|
||||
displayName: 'Chat Channel',
|
||||
fields: [
|
||||
{ name: 'id', type: 'string', label: 'ID', required: true, primaryKey: true },
|
||||
{ name: 'name', type: 'string', label: 'Channel Name', required: true },
|
||||
{ name: 'description', type: 'text', label: 'Description', required: false },
|
||||
{ name: 'topic', type: 'string', label: 'Channel Topic', required: false },
|
||||
{ name: 'isPrivate', type: 'boolean', label: 'Private', required: false, defaultValue: false },
|
||||
{ name: 'createdBy', type: 'string', label: 'Created By', required: true },
|
||||
{ name: 'createdAt', type: 'number', label: 'Created At', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ChatMessage',
|
||||
displayName: 'Chat Message',
|
||||
fields: [
|
||||
{ name: 'id', type: 'string', label: 'ID', required: true, primaryKey: true },
|
||||
{ name: 'channelId', type: 'string', label: 'Channel ID', required: true },
|
||||
{ name: 'username', type: 'string', label: 'Username', required: true },
|
||||
{ name: 'userId', type: 'string', label: 'User ID', required: true },
|
||||
{ name: 'message', type: 'text', label: 'Message', required: true },
|
||||
{ name: 'type', type: 'string', label: 'Message Type', required: true },
|
||||
{ name: 'timestamp', type: 'number', label: 'Timestamp', required: true },
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'ChatUser',
|
||||
displayName: 'Chat User',
|
||||
fields: [
|
||||
{ name: 'id', type: 'string', label: 'ID', required: true, primaryKey: true },
|
||||
{ name: 'channelId', type: 'string', label: 'Channel ID', required: true },
|
||||
{ name: 'username', type: 'string', label: 'Username', required: true },
|
||||
{ name: 'userId', type: 'string', label: 'User ID', required: true },
|
||||
{ name: 'joinedAt', type: 'number', label: 'Joined At', required: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
pages: [
|
||||
{
|
||||
id: 'page_chat',
|
||||
path: '/chat',
|
||||
title: 'IRC Webchat',
|
||||
level: 2,
|
||||
componentTree: [
|
||||
{
|
||||
id: 'comp_chat_root',
|
||||
type: 'IRCWebchat',
|
||||
props: {
|
||||
channelName: 'general',
|
||||
},
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
requiresAuth: true,
|
||||
requiredRole: 'user',
|
||||
},
|
||||
],
|
||||
workflows: [
|
||||
{
|
||||
id: 'workflow_send_message',
|
||||
name: 'Send Chat Message',
|
||||
description: 'Workflow for sending a chat message',
|
||||
nodes: [],
|
||||
edges: [],
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
id: 'workflow_join_channel',
|
||||
name: 'Join Channel',
|
||||
description: 'Workflow for joining a chat channel',
|
||||
nodes: [],
|
||||
edges: [],
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
luaScripts: [
|
||||
{
|
||||
id: 'lua_irc_send_message',
|
||||
name: 'Send IRC Message',
|
||||
description: 'Sends a message to the chat channel',
|
||||
code: `-- Send IRC Message
|
||||
function sendMessage(channelId, username, userId, message)
|
||||
local msgId = "msg_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999)
|
||||
local msg = {
|
||||
id = msgId,
|
||||
channelId = channelId,
|
||||
username = username,
|
||||
userId = userId,
|
||||
message = message,
|
||||
type = "message",
|
||||
timestamp = os.time() * 1000
|
||||
}
|
||||
log("Sending message: " .. message)
|
||||
return msg
|
||||
end
|
||||
|
||||
return sendMessage`,
|
||||
parameters: [
|
||||
{ name: 'channelId', type: 'string' },
|
||||
{ name: 'username', type: 'string' },
|
||||
{ name: 'userId', type: 'string' },
|
||||
{ name: 'message', type: 'string' },
|
||||
],
|
||||
returnType: 'table',
|
||||
},
|
||||
{
|
||||
id: 'lua_irc_handle_command',
|
||||
name: 'Handle IRC Command',
|
||||
description: 'Processes IRC commands like /help, /users, etc',
|
||||
code: `-- Handle IRC Command
|
||||
function handleCommand(command, channelId, username, onlineUsers)
|
||||
local parts = {}
|
||||
for part in string.gmatch(command, "%S+") do
|
||||
table.insert(parts, part)
|
||||
end
|
||||
|
||||
local cmd = parts[1]:lower()
|
||||
local response = {
|
||||
id = "msg_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999),
|
||||
username = "System",
|
||||
userId = "system",
|
||||
type = "system",
|
||||
timestamp = os.time() * 1000,
|
||||
channelId = channelId
|
||||
}
|
||||
|
||||
if cmd == "/help" then
|
||||
response.message = "Available commands: /help, /users, /clear, /me <action>"
|
||||
elseif cmd == "/users" then
|
||||
local userCount = #onlineUsers
|
||||
local userList = table.concat(onlineUsers, ", ")
|
||||
response.message = "Online users (" .. userCount .. "): " .. userList
|
||||
elseif cmd == "/clear" then
|
||||
response.message = "CLEAR_MESSAGES"
|
||||
response.type = "command"
|
||||
elseif cmd == "/me" then
|
||||
if #parts > 1 then
|
||||
local action = table.concat(parts, " ", 2)
|
||||
response.message = action
|
||||
response.username = username
|
||||
response.userId = username
|
||||
response.type = "system"
|
||||
else
|
||||
response.message = "Usage: /me <action>"
|
||||
end
|
||||
else
|
||||
response.message = "Unknown command: " .. cmd .. ". Type /help for available commands."
|
||||
end
|
||||
|
||||
return response
|
||||
end
|
||||
|
||||
return handleCommand`,
|
||||
parameters: [
|
||||
{ name: 'command', type: 'string' },
|
||||
{ name: 'channelId', type: 'string' },
|
||||
{ name: 'username', type: 'string' },
|
||||
{ name: 'onlineUsers', type: 'table' },
|
||||
],
|
||||
returnType: 'table',
|
||||
},
|
||||
{
|
||||
id: 'lua_irc_format_time',
|
||||
name: 'Format Timestamp',
|
||||
description: 'Formats a timestamp for display',
|
||||
code: `-- Format Timestamp
|
||||
function formatTime(timestamp)
|
||||
local date = os.date("*t", timestamp / 1000)
|
||||
local hour = date.hour
|
||||
local ampm = "AM"
|
||||
|
||||
if hour >= 12 then
|
||||
ampm = "PM"
|
||||
if hour > 12 then
|
||||
hour = hour - 12
|
||||
end
|
||||
end
|
||||
|
||||
if hour == 0 then
|
||||
hour = 12
|
||||
end
|
||||
|
||||
return string.format("%02d:%02d %s", hour, date.min, ampm)
|
||||
end
|
||||
|
||||
return formatTime`,
|
||||
parameters: [
|
||||
{ name: 'timestamp', type: 'number' },
|
||||
],
|
||||
returnType: 'string',
|
||||
},
|
||||
{
|
||||
id: 'lua_irc_user_join',
|
||||
name: 'User Join Channel',
|
||||
description: 'Handles user joining a channel',
|
||||
code: `-- User Join Channel
|
||||
function userJoin(channelId, username, userId)
|
||||
local joinMsg = {
|
||||
id = "msg_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999),
|
||||
channelId = channelId,
|
||||
username = "System",
|
||||
userId = "system",
|
||||
message = username .. " has joined the channel",
|
||||
type = "join",
|
||||
timestamp = os.time() * 1000
|
||||
}
|
||||
|
||||
log(username .. " joined channel " .. channelId)
|
||||
return joinMsg
|
||||
end
|
||||
|
||||
return userJoin`,
|
||||
parameters: [
|
||||
{ name: 'channelId', type: 'string' },
|
||||
{ name: 'username', type: 'string' },
|
||||
{ name: 'userId', type: 'string' },
|
||||
],
|
||||
returnType: 'table',
|
||||
},
|
||||
{
|
||||
id: 'lua_irc_user_leave',
|
||||
name: 'User Leave Channel',
|
||||
description: 'Handles user leaving a channel',
|
||||
code: `-- User Leave Channel
|
||||
function userLeave(channelId, username, userId)
|
||||
local leaveMsg = {
|
||||
id = "msg_" .. tostring(os.time()) .. "_" .. math.random(1000, 9999),
|
||||
channelId = channelId,
|
||||
username = "System",
|
||||
userId = "system",
|
||||
message = username .. " has left the channel",
|
||||
type = "leave",
|
||||
timestamp = os.time() * 1000
|
||||
}
|
||||
|
||||
log(username .. " left channel " .. channelId)
|
||||
return leaveMsg
|
||||
end
|
||||
|
||||
return userLeave`,
|
||||
parameters: [
|
||||
{ name: 'channelId', type: 'string' },
|
||||
{ name: 'username', type: 'string' },
|
||||
{ name: 'userId', type: 'string' },
|
||||
],
|
||||
returnType: 'table',
|
||||
},
|
||||
],
|
||||
componentHierarchy: {
|
||||
page_chat: {
|
||||
id: 'comp_chat_root',
|
||||
type: 'IRCWebchat',
|
||||
props: {},
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
componentConfigs: {
|
||||
IRCWebchat: {
|
||||
type: 'IRCWebchat',
|
||||
category: 'social',
|
||||
label: 'IRC Webchat',
|
||||
description: 'IRC-style chat component with channels and commands',
|
||||
icon: '💬',
|
||||
props: [
|
||||
{
|
||||
name: 'channelName',
|
||||
type: 'string',
|
||||
label: 'Channel Name',
|
||||
defaultValue: 'general',
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'showSettings',
|
||||
type: 'boolean',
|
||||
label: 'Show Settings',
|
||||
defaultValue: false,
|
||||
required: false,
|
||||
},
|
||||
{
|
||||
name: 'height',
|
||||
type: 'string',
|
||||
label: 'Height',
|
||||
defaultValue: '600px',
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
config: {
|
||||
layout: 'Card',
|
||||
styling: {
|
||||
className: 'h-[600px] flex flex-col',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'header',
|
||||
type: 'CardHeader',
|
||||
props: {
|
||||
className: 'border-b border-border pb-3',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'title_container',
|
||||
type: 'Flex',
|
||||
props: {
|
||||
className: 'flex items-center justify-between',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'title',
|
||||
type: 'CardTitle',
|
||||
props: {
|
||||
className: 'flex items-center gap-2 text-lg',
|
||||
content: '#{channelName}',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
type: 'Flex',
|
||||
props: {
|
||||
className: 'flex items-center gap-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'user_badge',
|
||||
type: 'Badge',
|
||||
props: {
|
||||
variant: 'secondary',
|
||||
className: 'gap-1.5',
|
||||
icon: 'Users',
|
||||
content: '{onlineUsersCount}',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'settings_button',
|
||||
type: 'Button',
|
||||
props: {
|
||||
size: 'sm',
|
||||
variant: 'ghost',
|
||||
icon: 'Gear',
|
||||
onClick: 'toggleSettings',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'content',
|
||||
type: 'CardContent',
|
||||
props: {
|
||||
className: 'flex-1 flex flex-col p-0 overflow-hidden',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'main_area',
|
||||
type: 'Flex',
|
||||
props: {
|
||||
className: 'flex flex-1 overflow-hidden',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'messages_area',
|
||||
type: 'ScrollArea',
|
||||
props: {
|
||||
className: 'flex-1 p-4',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'messages_container',
|
||||
type: 'MessageList',
|
||||
props: {
|
||||
className: 'space-y-2 font-mono text-sm',
|
||||
dataSource: 'messages',
|
||||
itemRenderer: 'renderMessage',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sidebar',
|
||||
type: 'Container',
|
||||
props: {
|
||||
className: 'w-48 border-l border-border p-4 bg-muted/20',
|
||||
conditional: 'showSettings',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'sidebar_title',
|
||||
type: 'Heading',
|
||||
props: {
|
||||
level: '4',
|
||||
className: 'font-semibold text-sm mb-3',
|
||||
content: 'Online Users',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'users_list',
|
||||
type: 'UserList',
|
||||
props: {
|
||||
className: 'space-y-1.5 text-sm',
|
||||
dataSource: 'onlineUsers',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'input_area',
|
||||
type: 'Container',
|
||||
props: {
|
||||
className: 'border-t border-border p-4',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'input_row',
|
||||
type: 'Flex',
|
||||
props: {
|
||||
className: 'flex gap-2',
|
||||
},
|
||||
children: [
|
||||
{
|
||||
id: 'message_input',
|
||||
type: 'Input',
|
||||
props: {
|
||||
className: 'flex-1 font-mono',
|
||||
placeholder: 'Type a message... (/help for commands)',
|
||||
onKeyPress: 'handleKeyPress',
|
||||
value: '{inputMessage}',
|
||||
onChange: 'updateInputMessage',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'send_button',
|
||||
type: 'Button',
|
||||
props: {
|
||||
size: 'icon',
|
||||
icon: 'PaperPlaneTilt',
|
||||
onClick: 'handleSendMessage',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'help_text',
|
||||
type: 'Text',
|
||||
props: {
|
||||
className: 'text-xs text-muted-foreground mt-2',
|
||||
content: 'Press Enter to send. Type /help for commands.',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
seedData: {
|
||||
ChatChannel: [
|
||||
{
|
||||
id: 'channel_general',
|
||||
name: 'general',
|
||||
description: 'General discussion',
|
||||
topic: 'Welcome to the general chat!',
|
||||
isPrivate: false,
|
||||
createdBy: 'system',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'channel_random',
|
||||
name: 'random',
|
||||
description: 'Random conversations',
|
||||
topic: 'Talk about anything here',
|
||||
isPrivate: false,
|
||||
createdBy: 'system',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
30
src/lib/package-loader.ts
Normal file
30
src/lib/package-loader.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { PACKAGE_CATALOG } from './package-catalog'
|
||||
import { loadPackageComponents } from './declarative-component-renderer'
|
||||
|
||||
let isInitialized = false
|
||||
|
||||
export async function initializePackageSystem() {
|
||||
if (isInitialized) return
|
||||
|
||||
Object.values(PACKAGE_CATALOG).forEach(pkg => {
|
||||
if (pkg.content) {
|
||||
loadPackageComponents(pkg.content)
|
||||
}
|
||||
})
|
||||
|
||||
isInitialized = true
|
||||
}
|
||||
|
||||
export function getInstalledPackageIds(): string[] {
|
||||
return Object.keys(PACKAGE_CATALOG)
|
||||
}
|
||||
|
||||
export function getPackageContent(packageId: string) {
|
||||
const pkg = PACKAGE_CATALOG[packageId]
|
||||
return pkg ? pkg.content : null
|
||||
}
|
||||
|
||||
export function getPackageManifest(packageId: string) {
|
||||
const pkg = PACKAGE_CATALOG[packageId]
|
||||
return pkg ? pkg.manifest : null
|
||||
}
|
||||
Reference in New Issue
Block a user