diff --git a/DECLARATIVE_COMPONENTS.md b/DECLARATIVE_COMPONENTS.md new file mode 100644 index 000000000..4388e4777 --- /dev/null +++ b/DECLARATIVE_COMPONENTS.md @@ -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 +} +``` + +## 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 ... +} +``` + +## 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 ( + +

Count: {count}

+ +
+ ) +} +``` + +### 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 ... +} +``` + +--- + +For questions or contributions, see the main project documentation. diff --git a/PRD.md b/PRD.md index 5c25eb37d..884e69036 100644 --- a/PRD.md +++ b/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 diff --git a/src/App.tsx b/src/App.tsx index 6d5f44950..45d426641 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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() diff --git a/src/components/IRCWebchatDeclarative.tsx b/src/components/IRCWebchatDeclarative.tsx new file mode 100644 index 000000000..387c797b5 --- /dev/null +++ b/src/components/IRCWebchatDeclarative.tsx @@ -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(`chat_${channelName}`, []) + const [onlineUsers, setOnlineUsers] = useKV(`chat_${channelName}_users`, []) + const [inputMessage, setInputMessage] = useState('') + const [showSettings, setShowSettings] = useState(false) + const scrollRef = useRef(null) + const messagesEndRef = useRef(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 => { + 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>({}) + + useEffect(() => { + const updateTimes = async () => { + const times: Record = {} + 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 ( + + +
+ + # + {channelName} + +
+ + + {onlineUsers?.length || 0} + + + {onClose && ( + + )} +
+
+
+ +
+ +
+ {(messages || []).map((msg) => ( +
+ {msg.type === 'message' && ( +
+ {formattedTimes[msg.id] || ''} + <{msg.username}> + {msg.message} +
+ )} + {msg.type === 'system' && msg.username === 'System' && ( +
+ {formattedTimes[msg.id] || ''} + *** {msg.message} +
+ )} + {msg.type === 'system' && msg.username !== 'System' && ( +
+ {formattedTimes[msg.id] || ''} + * {msg.username} {msg.message} +
+ )} + {(msg.type === 'join' || msg.type === 'leave') && ( +
+ {formattedTimes[msg.id] || ''} + + --> {msg.message} + +
+ )} +
+ ))} +
+
+ + + {showSettings && ( +
+

Online Users

+
+ {(onlineUsers || []).map((username) => ( +
+
+ {username} +
+ ))} +
+
+ )} +
+ +
+
+ setInputMessage(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Type a message... (/help for commands)" + className="flex-1 font-mono" + /> + +
+

+ Press Enter to send. Type /help for commands. +

+
+ + + ) +} diff --git a/src/components/Level2.tsx b/src/components/Level2.tsx index 6a106ae69..d2292e0b8 100644 --- a/src/components/Level2.tsx +++ b/src/components/Level2.tsx @@ -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) { - +
diff --git a/src/components/RenderComponent.tsx b/src/components/RenderComponent.tsx index ea437b9fd..0d7d84c28 100644 --- a/src/components/RenderComponent.tsx +++ b/src/components/RenderComponent.tsx @@ -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 } -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 ( + + ) + } + + return ( +
+ Declarative Component: {type} +
+ This is a package-defined component +
+
+ ) + } switch (type) { case 'Container': diff --git a/src/lib/builder-types.ts b/src/lib/builder-types.ts index f416f3f32..8ddc0bc1b 100644 --- a/src/lib/builder-types.ts +++ b/src/lib/builder-types.ts @@ -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 diff --git a/src/lib/component-catalog.ts b/src/lib/component-catalog.ts index 57aff48b9..c565c9893 100644 --- a/src/lib/component-catalog.ts +++ b/src/lib/component-catalog.ts @@ -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' }, + ], + }, ] diff --git a/src/lib/declarative-component-renderer.ts b/src/lib/declarative-component-renderer.ts new file mode 100644 index 000000000..d2e03eb3c --- /dev/null +++ b/src/lib/declarative-component-renderer.ts @@ -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 = {} + private luaScripts: Record = {} + + 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 { + const script = this.luaScripts[scriptId] + if (!script) { + throw new Error(`Lua script not found: ${scriptId}`) + } + + const paramContext: Record = {} + 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 { + 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): boolean { + if (typeof condition === 'boolean') return condition + if (!condition) return true + + const value = context[condition] + return Boolean(value) + } + + resolveDataSource(dataSource: string, context: Record): 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', + }) + }) + } +} diff --git a/src/lib/package-catalog.ts b/src/lib/package-catalog.ts index 5b2eddb65..517e99f5d 100644 --- a/src/lib/package-catalog.ts +++ b/src/lib/package-catalog.ts @@ -661,4 +661,509 @@ export const PACKAGE_CATALOG: Record 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 " + 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(), + }, + ], + }, + }, + }, } diff --git a/src/lib/package-loader.ts b/src/lib/package-loader.ts new file mode 100644 index 000000000..af662c7d7 --- /dev/null +++ b/src/lib/package-loader.ts @@ -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 +}