Complete prompt builder for Lemonade AI with advanced features
// ==UserScript==
// @name Lemonade Prompt Builder
// @namespace http://tampermonkey.net/
// @version 8.8.3.1
// @description Complete prompt builder for Lemonade AI with advanced features
// @author Silverfox0338
// @match https://lemonade.gg/code/*
// @match https://*.lemonade.gg/code/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @license CC-BY-NC-ND-4.0
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
const CATEGORIES = window.LPB_CATEGORIES = {
'large-systems': {
name: 'Large Systems',
description: 'Complete game systems from scratch',
templates: {
'inventory': {
name: 'Inventory System',
fields: [
{ id: 'slots', label: 'Max Inventory Slots', type: 'number', default: '20', required: true },
{ id: 'stackable', label: 'Stackable Items', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'maxStack', label: 'Max Stack Size', type: 'number', default: '64', show_if: { field: 'stackable', value: 'Yes' } },
{ id: 'dragDrop', label: 'Drag and Drop', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'persistence', label: 'Save to DataStore', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'features', label: 'Additional Features', type: 'textarea', placeholder: 'Search, filters, sorting, tooltips...' }
],
generate: (d) => `Create an Inventory System with ${d.slots} slots.
STRUCTURE:
- LocalScript in StarterPlayerScripts (creates GUI, handles input)
- ModuleScript in ReplicatedStorage (shared inventory data structure)
- Script in ServerScriptService (validates changes, manages data)
- RemoteEvent in ReplicatedStorage for client-server communication
CLIENT (LocalScript):
1. Create ScreenGui with ${d.slots}-slot grid using Instance.new()
2. Each slot: ImageButton with ImageLabel (icon) and TextLabel (quantity)
${d.dragDrop === 'Yes' ? '3. Implement drag-drop: track dragging state, update positions with UserInputService' : ''}
4. Show tooltips on hover using MouseEnter/MouseLeave
5. Fire RemoteEvent for any inventory changes
SERVER (Script):
1. Store player inventories in a table
2. On RemoteEvent: validate the action, update server data, return result
3. Functions: AddItem(player, itemId, qty), RemoveItem(player, itemId, qty), MoveItem(player, from, to)
${d.stackable === 'Yes' ? `4. Stack items up to ${d.maxStack} per slot` : '4. One item per slot only'}
${d.persistence === 'Yes' ? `5. Save to DataStore on PlayerRemoving, load on PlayerAdded` : ''}
${d.features ? `\nExtra: ${d.features}` : ''}`
},
'shop': {
name: 'Shop/Store System',
fields: [
{ id: 'currency', label: 'Currency Name', type: 'text', default: 'Coins', required: true },
{ id: 'shopType', label: 'Shop Interface', type: 'select', options: ['GUI Menu', 'NPC Vendor', 'Both'], default: 'GUI Menu' },
{ id: 'categories', label: 'Shop Categories', type: 'list', placeholder: 'Weapons, Tools, Cosmetics...', required: true },
{ id: 'confirmPurchase', label: 'Purchase Confirmation', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'persistence', label: 'Save Purchases', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create a Shop System using ${d.currency} as currency.
STRUCTURE:
- LocalScript in StarterPlayerScripts (GUI creation and interaction)
- ModuleScript in ReplicatedStorage (item definitions with prices)
- Script in ServerScriptService (purchase validation)
- RemoteFunction in ReplicatedStorage for purchases
CLIENT (LocalScript):
1. Create ScreenGui with Instance.new(), parent to PlayerGui
2. Build shop frame with category tabs: ${d.categories ? d.categories.join(', ') : 'General'}
3. Display items as buttons showing: icon, name, price in ${d.currency}
${d.confirmPurchase === 'Yes' ? '4. Show confirmation popup before purchase' : '4. Purchase on single click'}
5. Call RemoteFunction to request purchase
6. Update GUI based on server response
${d.shopType.includes('NPC') ? '7. Add ProximityPrompt detection to open shop near NPCs' : ''}
SERVER (Script):
1. Handle RemoteFunction requests
2. Check player has enough ${d.currency}
3. Deduct ${d.currency} and grant item
4. Return success/failure to client
${d.persistence === 'Yes' ? '5. Save purchases and currency to DataStore' : ''}`
},
'combat': {
name: 'Combat System',
fields: [
{ id: 'combatType', label: 'Combat Type', type: 'select', options: ['Melee', 'Ranged', 'Magic', 'Hybrid'], default: 'Melee' },
{ id: 'hitDetection', label: 'Hit Detection', type: 'select', options: ['Raycast', 'Region3', 'Touched Event'], default: 'Raycast' },
{ id: 'cooldown', label: 'Attack Cooldown (seconds)', type: 'number', default: '1' },
{ id: 'animations', label: 'Attack Animations', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'vfx', label: 'Visual Effects', type: 'radio', options: ['Yes (particles, sounds)', 'No'], default: 'Yes (particles, sounds)' }
],
generate: (d) => `Create a ${d.combatType} Combat System.
STRUCTURE:
- LocalScript in StarterPlayerScripts (input, animations, effects)
- Script in ServerScriptService (hit validation, damage)
- RemoteEvent in ReplicatedStorage
CLIENT (LocalScript):
1. Detect attack input (mouse click or key)
2. Check local cooldown (${d.cooldown}s) before allowing attack
${d.animations === 'Yes' ? '3. Play attack animation using Animator:LoadAnimation()' : ''}
${d.vfx.includes('Yes') ? '4. Play sound and particle effects locally' : ''}
5. Fire RemoteEvent with attack data (position, direction)
SERVER (Script):
1. Track cooldowns per player in a table
2. Validate cooldown hasn't been bypassed
3. Perform ${d.hitDetection} from player's position
4. If hit enemy Humanoid: apply damage
5. Replicate effects to other clients if needed
${d.combatType === 'Melee' ? 'For melee: short raycast or small Region3 in front of player' : ''}
${d.combatType === 'Ranged' ? 'For ranged: raycast from camera through mouse position' : ''}
${d.combatType === 'Magic' ? 'For magic: spawn projectile part, move with RunService, check collisions' : ''}`
},
'datastore': {
name: 'DataStore Manager',
fields: [
{ id: 'dataTypes', label: 'Data to Save', type: 'list', placeholder: 'Coins, Level, Inventory...', required: true },
{ id: 'autoSave', label: 'Auto-Save Frequency', type: 'select', options: ['Every 1 minute', 'Every 5 minutes', 'Only on leave'], default: 'Every 5 minutes' },
{ id: 'defaultData', label: 'Default Data Template', type: 'textarea', placeholder: '{ coins = 0, level = 1 }', required: true }
],
generate: (d) => `Create a DataStore system.
LOCATION: Script in ServerScriptService
DATA TO SAVE:
${d.dataTypes ? d.dataTypes.map(t => `- ${t}`).join('\n') : '- PlayerData'}
DEFAULT TEMPLATE:
${d.defaultData}
IMPLEMENTATION:
1. Use DataStoreService:GetDataStore("PlayerData")
2. Create sessionData table to cache player data
LoadData(player):
- pcall GetAsync with player.UserId as key
- If no data exists, use default template
- Store in sessionData[player]
SaveData(player):
- pcall SetAsync with player.UserId and sessionData[player]
- Log any errors
EVENTS:
- PlayerAdded: call LoadData
- PlayerRemoving: call SaveData
- game:BindToClose: loop through all players and save
${d.autoSave !== 'Only on leave' ? `AUTO-SAVE:\n- Use while loop with ${d.autoSave === 'Every 1 minute' ? '60' : '300'} second wait\n- Save all players in sessionData` : ''}`
},
'leaderstats': {
name: 'Leaderstats System',
fields: [
{ id: 'stats', label: 'Stats to Display', type: 'list', placeholder: 'Kills, Points, Level...', required: true },
{ id: 'persistence', label: 'Save Stats', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create a Leaderstats system.
LOCATION: Script in ServerScriptService
STATS: ${d.stats ? d.stats.join(', ') : 'Points'}
IMPLEMENTATION:
game.Players.PlayerAdded:Connect(function(player)
-- Create leaderstats folder
local leaderstats = Instance.new("Folder")
leaderstats.Name = "leaderstats"
leaderstats.Parent = player
${d.stats ? d.stats.map(s => ` -- Create ${s}\n local ${s.toLowerCase().replace(/\s+/g, '')} = Instance.new("IntValue")\n ${s.toLowerCase().replace(/\s+/g, '')}.Name = "${s}"\n ${s.toLowerCase().replace(/\s+/g, '')}.Parent = leaderstats`).join('\n\n') : ''}
${d.persistence === 'Yes' ? '\n -- Load saved values from DataStore here' : '\n -- Initialize all to 0'}
end)
${d.persistence === 'Yes' ? '\nAdd PlayerRemoving to save values to DataStore.' : ''}`
},
'custom': {
name: 'Custom Large System',
fields: [
{ id: 'systemName', label: 'System Name', type: 'text', required: true, placeholder: 'Quest System, Crafting, etc.' },
{ id: 'purpose', label: 'System Purpose', type: 'textarea', required: true, placeholder: 'What should this system do?' },
{ id: 'features', label: 'Features', type: 'list', placeholder: 'Feature 1, Feature 2...', required: true },
{ id: 'gui', label: 'GUI Requirements', type: 'textarea', placeholder: 'Describe any UI needed' },
{ id: 'dataStore', label: 'Data Persistence', type: 'radio', options: ['Yes', 'No'], default: 'No' }
],
generate: (d) => `Create a ${d.systemName}.
PURPOSE: ${d.purpose}
FEATURES:
${d.features ? d.features.map((f, i) => `${i + 1}. ${f}`).join('\n') : ''}
STRUCTURE:
- LocalScript in StarterPlayerScripts (client logic${d.gui ? ', GUI' : ''})
- Script in ServerScriptService (server logic, validation)
- RemoteEvent/RemoteFunction in ReplicatedStorage
${d.gui ? `\nGUI (in LocalScript):\n${d.gui}\nCreate all UI elements using Instance.new()` : ''}
${d.dataStore === 'Yes' ? '\nDATA: Save relevant data to DataStore on PlayerRemoving, load on PlayerAdded' : ''}`
}
}
},
'bug-fixes': {
name: 'Bug Fixes & Changes',
description: 'Fix issues or modify existing code',
templates: {
'fix-bug': {
name: 'Fix Specific Bug',
fields: [
{ id: 'system', label: 'Affected System', type: 'text', required: true, placeholder: 'Inventory System, Shop GUI...' },
{ id: 'bug', label: 'Bug Description', type: 'textarea', required: true, placeholder: 'What is broken?' },
{ id: 'expected', label: 'Expected Behavior', type: 'textarea', required: true, placeholder: 'What should happen?' },
{ id: 'errors', label: 'Error Messages', type: 'textarea', placeholder: 'Paste any errors from Output' }
],
generate: (d) => `Fix bug in ${d.system}.
PROBLEM: ${d.bug}
EXPECTED: ${d.expected}
${d.errors ? `\nERROR OUTPUT:\n${d.errors}` : ''}
Please identify the cause and provide the corrected code.`
},
'optimize': {
name: 'Performance Optimization',
fields: [
{ id: 'system', label: 'System to Optimize', type: 'text', required: true },
{ id: 'issue', label: 'Performance Issue', type: 'select', options: ['Lag/Low FPS', 'Memory Leak', 'Slow Script'], default: 'Lag/Low FPS' },
{ id: 'description', label: 'Issue Description', type: 'textarea', required: true, placeholder: 'When does the lag occur?' }
],
generate: (d) => `Optimize ${d.system} for ${d.issue.toLowerCase()}.
ISSUE: ${d.description}
Check for:
- Unnecessary loops or frequent GetChildren/FindFirstChild calls
- Missing connection disconnects (memory leaks)
- Heavy operations in RenderStepped (move to Heartbeat if possible)
- Objects not being Destroyed when removed
Provide optimized code with comments explaining changes.`
},
'add-feature': {
name: 'Add Feature to Existing Code',
fields: [
{ id: 'system', label: 'System to Modify', type: 'text', required: true, placeholder: 'Shop System, Combat...' },
{ id: 'feature', label: 'Feature to Add', type: 'textarea', required: true, placeholder: 'What should be added?' },
{ id: 'preserve', label: 'Must Preserve', type: 'textarea', placeholder: 'What functionality must stay the same?' }
],
generate: (d) => `Add feature to ${d.system}.
NEW FEATURE: ${d.feature}
${d.preserve ? `\nPRESERVE: ${d.preserve}` : ''}
Add this feature while keeping existing functionality intact. Show only the modified/new code sections.`
},
'custom': {
name: 'Custom Bug Fix/Change',
fields: [
{ id: 'description', label: 'Description', type: 'textarea', required: true, placeholder: 'Describe what needs to change' }
],
generate: (d) => d.description
}
}
},
'ui-systems': {
name: 'UI & Interface',
description: 'GUIs, menus, and visual interfaces',
templates: {
'main-menu': {
name: 'Main Menu',
fields: [
{ id: 'buttons', label: 'Menu Buttons', type: 'list', placeholder: 'Play, Settings, Shop...', required: true },
{ id: 'style', label: 'Menu Style', type: 'select', options: ['Modern', 'Minimal', 'Classic'], default: 'Modern' },
{ id: 'animations', label: 'Button Animations', type: 'radio', options: ['Smooth tweens', 'Simple', 'None'], default: 'Smooth tweens' }
],
generate: (d) => `Create a ${d.style} Main Menu.
LOCATION: LocalScript in StarterPlayerScripts
BUTTONS: ${d.buttons ? d.buttons.join(', ') : 'Play, Settings'}
IMPLEMENTATION:
1. Create ScreenGui with Instance.new(), parent to PlayerGui
2. Create main Frame centered on screen (use AnchorPoint 0.5, 0.5)
3. Add title TextLabel at top
4. Create buttons using Instance.new("TextButton") for each:
${d.buttons ? d.buttons.map(b => ` - ${b}`).join('\n') : ' - Play\n - Settings'}
5. Use UIListLayout for vertical button arrangement
${d.animations === 'Smooth tweens' ? '6. Add hover effects: TweenService to scale buttons to 1.05 on MouseEnter, back to 1 on MouseLeave' : ''}
${d.animations === 'Simple' ? '6. Change BackgroundColor3 on hover' : ''}
7. Connect button.Activated to respective functions
8. Disable PlayerControls while menu is open (optional)`
},
'hud': {
name: 'HUD/Overlay',
fields: [
{ id: 'elements', label: 'HUD Elements', type: 'list', placeholder: 'Health bar, coin counter...', required: true },
{ id: 'position', label: 'HUD Position', type: 'select', options: ['Top', 'Bottom', 'Corners'], default: 'Top' }
],
generate: (d) => `Create a HUD overlay.
LOCATION: LocalScript in StarterPlayerScripts
ELEMENTS: ${d.elements ? d.elements.join(', ') : 'Health, Coins'}
IMPLEMENTATION:
1. Create ScreenGui with Instance.new()
2. Create container Frame at ${d.position.toLowerCase()} of screen
3. For each element:
${d.elements ? d.elements.map(e => ` - ${e}: Frame with icon ImageLabel and value TextLabel`).join('\n') : ''}
4. Use UIListLayout for horizontal arrangement
5. Create update functions that change TextLabel.Text when values change
6. Connect to value changes:
- Health: Humanoid.HealthChanged
- Currency: leaderstats value.Changed
- Other: appropriate events`
},
'notification': {
name: 'Notification System',
fields: [
{ id: 'types', label: 'Notification Types', type: 'list', placeholder: 'Success, Error, Warning...', required: true },
{ id: 'duration', label: 'Display Duration (seconds)', type: 'number', default: '3' }
],
generate: (d) => `Create a Notification System.
LOCATION: LocalScript in StarterPlayerScripts
TYPES: ${d.types ? d.types.join(', ') : 'Info, Success, Error'}
IMPLEMENTATION:
1. Create ScreenGui with container Frame at top-right
2. Define colors for each type:
${d.types ? d.types.map(t => ` - ${t}: appropriate color`).join('\n') : ' - Success: green\n - Error: red'}
ShowNotification(message, type) function:
1. Create notification Frame with Instance.new()
2. Add TextLabel with message
3. Set BackgroundColor3 based on type
4. Tween position from off-screen to visible
5. Wait ${d.duration} seconds
6. Tween out and Destroy
Use UIListLayout so multiple notifications stack properly.`
},
'inventory-gui': {
name: 'Inventory GUI',
fields: [
{ id: 'layout', label: 'Layout', type: 'select', options: ['Grid', 'List', 'Hotbar'], default: 'Grid' },
{ id: 'slots', label: 'Visible Slots', type: 'number', default: '20' },
{ id: 'dragDrop', label: 'Drag and Drop', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create an Inventory GUI with ${d.layout.toLowerCase()} layout.
LOCATION: LocalScript in StarterPlayerScripts
SLOTS: ${d.slots}
IMPLEMENTATION:
1. Create ScreenGui and main Frame
2. Create ${d.slots} slot frames using a loop
3. ${d.layout === 'Grid' ? 'Use UIGridLayout for grid arrangement' : d.layout === 'List' ? 'Use UIListLayout for vertical list' : 'Use UIListLayout horizontal for hotbar at bottom'}
4. Each slot contains:
- ImageLabel for item icon
- TextLabel for quantity (bottom-right corner)
- Background that changes when selected/hovered
${d.dragDrop === 'Yes' ? `
DRAG AND DROP:
1. On MouseButton1Down: start drag, create clone following mouse
2. Track with UserInputService.InputChanged
3. On MouseButton1Up: check if over valid slot, swap items
4. Fire RemoteEvent to server to validate move` : ''}
5. Add tooltip Frame that shows item details on hover`
},
'settings-menu': {
name: 'Settings Menu',
fields: [
{ id: 'settings', label: 'Settings', type: 'list', placeholder: 'Graphics, volume...', required: true },
{ id: 'save', label: 'Save Settings', type: 'radio', options: ['Yes (DataStore)', 'Session only'], default: 'Yes (DataStore)' }
],
generate: (d) => `Create a Settings Menu.
LOCATION: LocalScript in StarterPlayerScripts
SETTINGS: ${d.settings ? d.settings.join(', ') : 'Volume, Graphics'}
IMPLEMENTATION:
1. Create ScreenGui with centered Frame
2. Add title "Settings" at top
3. For each setting, create a row with:
- Label (TextLabel)
- Control (Slider for volume, Dropdown for graphics, Toggle for on/off)
CONTROLS:
- Slider: Frame with draggable inner Frame, calculate value from position
- Toggle: TextButton that switches between states
- Dropdown: TextButton that shows/hides list of options
4. Add "Save" and "Close" buttons at bottom
5. Apply settings when changed (adjust volume, graphics quality, etc.)
${d.save === 'Yes (DataStore)' ? '\n6. Fire RemoteEvent to save settings to DataStore\n7. Load saved settings on join via RemoteFunction' : ''}`
},
'custom': {
name: 'Custom UI',
fields: [
{ id: 'guiName', label: 'GUI Name', type: 'text', required: true },
{ id: 'purpose', label: 'GUI Purpose', type: 'textarea', required: true, placeholder: 'What should this GUI do?' },
{ id: 'elements', label: 'UI Elements', type: 'list', placeholder: 'Buttons, frames, text...', required: true }
],
generate: (d) => `Create ${d.guiName} GUI.
LOCATION: LocalScript in StarterPlayerScripts
PURPOSE: ${d.purpose}
ELEMENTS:
${d.elements ? d.elements.map(e => `- ${e}`).join('\n') : ''}
Create all elements using Instance.new(). Parent ScreenGui to PlayerGui.
Use TweenService for any animations. Connect button events with .Activated.`
}
}
},
'gameplay-features': {
name: 'Gameplay Features',
description: 'Common game mechanics and player systems',
templates: {
'teams': {
name: 'Team System',
fields: [
{ id: 'teams', label: 'Team Names', type: 'list', placeholder: 'Red Team, Blue Team...', required: true },
{ id: 'autoAssign', label: 'Auto-Assign Teams', type: 'radio', options: ['Yes - Random', 'Yes - Balanced', 'No - Manual'], default: 'Yes - Balanced' },
{ id: 'teamColors', label: 'Custom Team Colors', type: 'radio', options: ['Yes', 'No'], default: 'No' },
{ id: 'friendlyFire', label: 'Friendly Fire', type: 'radio', options: ['Enabled', 'Disabled'], default: 'Disabled' },
{ id: 'spawnLocations', label: 'Team Spawn Points', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create a Team System with: ${d.teams ? d.teams.join(', ') : 'Teams'}.
LOCATION: Script in ServerScriptService
SETUP:
1. Create Team instances in game.Teams for each: ${d.teams ? d.teams.join(', ') : 'team'}
${d.teamColors === 'Yes' ? '2. Set unique TeamColor for each team' : '2. Use default team colors'}
${d.spawnLocations === 'Yes' ? '3. Create SpawnLocation parts with matching TeamColor properties' : ''}
ASSIGNMENT:
${d.autoAssign === 'Yes - Random' ? 'On PlayerAdded: assign player to random team using math.random()' : ''}
${d.autoAssign === 'Yes - Balanced' ? 'On PlayerAdded: count players on each team, assign to team with fewest members' : ''}
${d.autoAssign === 'No - Manual' ? 'Create team selection GUI with buttons. On button click, fire RemoteEvent to set player.Team' : ''}
${d.friendlyFire === 'Disabled' ? 'FRIENDLY FIRE PREVENTION:\nIn damage script: check if attacker.Team == victim.Team, if yes cancel damage' : ''}
Players automatically spawn at their team's SpawnLocation and get team-colored name.`
},
'teleport': {
name: 'Teleportation System',
fields: [
{ id: 'teleportType', label: 'Teleport Type', type: 'select', options: ['Part Proximity', 'GUI Button', 'Command', 'All'], default: 'Part Proximity' },
{ id: 'destinations', label: 'Destination Names', type: 'list', placeholder: 'Lobby, Arena, Shop...', required: true },
{ id: 'cooldown', label: 'Cooldown (seconds)', type: 'number', default: '5' },
{ id: 'effect', label: 'Teleport Effect', type: 'radio', options: ['Particles + Sound', 'Simple fade', 'None'], default: 'Particles + Sound' }
],
generate: (d) => `Create Teleportation System for: ${d.destinations ? d.destinations.join(', ') : 'locations'}.
STRUCTURE:
- Script in ServerScriptService
- Parts in Workspace marking each destination location
${d.teleportType.includes('GUI') || d.teleportType === 'All' ? '- LocalScript for teleport menu GUI' : ''}
- RemoteEvent for teleport requests
DESTINATIONS:
Place Parts at each location: ${d.destinations ? d.destinations.join(', ') : 'destinations'}
Name them clearly and save their CFrame positions
${d.teleportType.includes('Part') || d.teleportType === 'All' ? `PROXIMITY PADS:
Create teleport pads with ProximityPrompt
On Triggered: check ${d.cooldown}s cooldown, then teleport player to destination CFrame` : ''}
${d.teleportType.includes('GUI') || d.teleportType === 'All' ? `GUI MENU:
Create buttons for each destination
On click: fire RemoteEvent with destination name` : ''}
${d.teleportType.includes('Command') || d.teleportType === 'All' ? `CHAT COMMAND:
Listen for player.Chatted
Parse "/tp [destination]" commands
Validate and teleport` : ''}
COOLDOWN: Track last teleport time per player in table, require ${d.cooldown}s between uses
TELEPORT: Set player.Character.HumanoidRootPart.CFrame to destination + Vector3.new(0,3,0)
${d.effect === 'Particles + Sound' ? 'Add ParticleEmitter burst and sound at departure/arrival positions' : ''}
${d.effect === 'Simple fade' ? 'Fire RemoteEvent to client to show white fade in/out during teleport' : ''}`
},
'admin': {
name: 'Admin Commands System',
fields: [
{ id: 'adminList', label: 'Admin User IDs', type: 'list', placeholder: '123456789, 987654321...', required: true },
{ id: 'commands', label: 'Commands to Include', type: 'checkboxes',
options: ['Kick', 'Teleport', 'Give Item', 'Speed', 'Kill', 'Heal', 'Announce'],
default: [] },
{ id: 'prefix', label: 'Command Prefix', type: 'text', default: ':', required: true },
{ id: 'logging', label: 'Log Admin Actions', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Admin Commands System.
LOCATION: Script in ServerScriptService
PREFIX: ${d.prefix}
ADMINS: ${d.adminList ? d.adminList.join(', ') : 'Admin UserIds'}
SETUP:
1. Store admin UserIds in table
2. Create IsAdmin(player) function to check if player.UserId is in table
3. Connect to player.Chatted for each player
4. Parse messages starting with "${d.prefix}"
COMMANDS:
${d.commands.includes('Kick') ? `${d.prefix}kick [player] - Find player by name, call player:Kick()` : ''}
${d.commands.includes('Teleport') ? `${d.prefix}tp [player1] [player2] - Teleport player1 to player2's position` : ''}
${d.commands.includes('Give Item') ? `${d.prefix}give [player] [item] - Clone item from Storage to player's Backpack` : ''}
${d.commands.includes('Speed') ? `${d.prefix}speed [player] [value] - Set player.Character.Humanoid.WalkSpeed` : ''}
${d.commands.includes('Kill') ? `${d.prefix}kill [player] - Set player.Character.Humanoid.Health = 0` : ''}
${d.commands.includes('Heal') ? `${d.prefix}heal [player] - Set Health to MaxHealth` : ''}
${d.commands.includes('Announce') ? `${d.prefix}announce [message] - Fire RemoteEvent to all clients to show message` : ''}
Create FindPlayer(name) function to match partial player names
${d.logging === 'Yes' ? 'Print all command usage to console with admin name and target' : ''}`
},
'animations': {
name: 'Animation Controller',
fields: [
{ id: 'animType', label: 'Animation Type', type: 'select',
options: ['Tool/Weapon Animations', 'Emotes/Gestures', 'Custom Movement'], default: 'Tool/Weapon Animations' },
{ id: 'animations', label: 'Animation Names', type: 'list', placeholder: 'Swing, Slash, Block...', required: true },
{ id: 'priority', label: 'Animation Priority', type: 'select',
options: ['Action', 'Movement', 'Core'], default: 'Action' },
{ id: 'keybinds', label: 'Keyboard Controls', type: 'radio', options: ['Yes', 'No'], default: 'No' }
],
generate: (d) => `Create Animation Controller for: ${d.animations ? d.animations.join(', ') : 'animations'}.
LOCATION: LocalScript in StarterPlayerScripts
SETUP:
1. Create Animation objects in ReplicatedStorage with AnimationId properties
2. Wait for character, get Humanoid's Animator
3. Use Animator:LoadAnimation() for each animation
4. Store loaded tracks in table with names: ${d.animations ? d.animations.join(', ') : 'animations'}
5. Set Priority to ${d.priority}
${d.animType === 'Tool/Weapon Animations' ? `TOOL TRIGGERS:
Detect when tool equipped (character.ChildAdded)
On tool.Activated: play attack animation
Cycle through multiple animations if needed` : ''}
${d.animType === 'Emotes/Gestures' ? `EMOTE BUTTONS:
Create GUI with button for each emote
On click: stop current animation, play selected emote` : ''}
${d.animType === 'Custom Movement' ? `MOVEMENT STATES:
Connect to Humanoid.Running and Humanoid.StateChanged
Play appropriate animations based on movement state` : ''}
${d.keybinds === 'Yes' ? `KEYBOARD CONTROLS:
Use UserInputService.InputBegan
Map keys to animations, add cooldown to prevent spam` : ''}
Control animations with track:Play() and track:Stop()`
},
'achievements': {
name: 'Achievement System',
fields: [
{ id: 'achievements', label: 'Achievement Names', type: 'list', placeholder: 'First Win, 100 Coins, Level 10...', required: true },
{ id: 'notifications', label: 'Achievement Notifications', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'rewards', label: 'Give Rewards', type: 'radio', options: ['Yes (coins/items)', 'No'], default: 'No' },
{ id: 'saving', label: 'Save Achievements', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Achievement System for: ${d.achievements ? d.achievements.join(', ') : 'achievements'}.
STRUCTURE:
- Script in ServerScriptService
- ModuleScript defining all achievements with descriptions${d.rewards === 'Yes (coins/items)' ? ' and rewards' : ''}
${d.notifications === 'Yes' ? '- LocalScript for notification popup\n- RemoteEvent to trigger popups' : ''}
${d.saving === 'Yes' ? '- DataStore to save earned achievements' : ''}
TRACKING:
${d.saving === 'Yes' ? 'Load/save earned achievements per player using DataStore' : 'Store earned achievements in session table (resets on leave)'}
GRANTING:
Create GrantAchievement(player, achievementName) function:
1. Check if already earned (prevent duplicates)
2. Add to player's achievements list
${d.rewards === 'Yes (coins/items)' ? '3. Award coins or items defined in achievement data' : ''}
${d.notifications === 'Yes' ? '4. Fire RemoteEvent to show popup on client' : ''}
${d.saving === 'Yes' ? '5. Save to DataStore' : ''}
TRIGGERS:
Call GrantAchievement() when player reaches milestones (wins game, collects 100 coins, reaches level, etc.)
${d.notifications === 'Yes' ? 'POPUP: Create GUI that tweens in from off-screen, shows achievement name/icon, displays 3-5 seconds, tweens out' : ''}`
},
'proximity': {
name: 'Proximity Interaction',
fields: [
{ id: 'interactType', label: 'Interaction Type', type: 'select',
options: ['Collect Items', 'Open Doors', 'Talk to NPCs', 'Activate Objects'], default: 'Collect Items' },
{ id: 'promptText', label: 'Prompt Text', type: 'text', default: 'Press E to interact', required: true },
{ id: 'maxDistance', label: 'Interaction Range (studs)', type: 'number', default: '10' },
{ id: 'holdDuration', label: 'Hold Duration (0 for instant)', type: 'number', default: '0' },
{ id: 'objectTag', label: 'Tag for Interactive Objects', type: 'text', default: 'Interactable', required: true }
],
generate: (d) => `Create Proximity Interaction for ${d.interactType}.
LOCATION: Script in ServerScriptService
SETUP:
1. Use CollectionService to find all objects tagged "${d.objectTag}"
2. For each object, create ProximityPrompt with:
- ActionText = "${d.promptText}"
- MaxActivationDistance = ${d.maxDistance}
- HoldDuration = ${d.holdDuration}
- KeyboardKeyCode = E
INTERACTION:
Connect to ProximityPrompt.Triggered for each object
${d.interactType === 'Collect Items' ? 'When triggered: add item to player inventory/stats, destroy object, play sound' : ''}
${d.interactType === 'Open Doors' ? 'When triggered: check if locked, if not tween door open, wait, tween closed' : ''}
${d.interactType === 'Talk to NPCs' ? 'When triggered: fire RemoteEvent with NPC dialogue, show on client GUI' : ''}
${d.interactType === 'Activate Objects' ? 'When triggered: toggle object state, trigger connected systems (buttons, levers, chests)' : ''}
Tag objects in Studio using CollectionService or Tag Editor plugin with "${d.objectTag}"`
}
}
},
'utilities': {
name: 'Utilities & Helpers',
description: 'Helper systems and convenience tools',
templates: {
'chatcommands': {
name: 'Player Chat Commands',
fields: [
{ id: 'commands', label: 'Command Names', type: 'list', placeholder: 'help, stats, reset...', required: true },
{ id: 'prefix', label: 'Command Prefix', type: 'text', default: '/', required: true },
{ id: 'feedback', label: 'Command Feedback', type: 'radio', options: ['Chat messages', 'GUI notifications', 'Both'], default: 'Chat messages' }
],
generate: (d) => `Create Chat Commands System.
PREFIX: ${d.prefix}
COMMANDS: ${d.commands ? d.commands.join(', ') : 'help, stats'}
LOCATION: Script in ServerScriptService
SETUP:
1. Connect to player.Chatted for each player
2. Check if message starts with "${d.prefix}"
3. Parse command name and arguments (split by spaces)
COMMANDS:
${d.commands ? d.commands.map(cmd => `${d.prefix}${cmd} - Define behavior for this command`).join('\n') : 'Define command functions'}
FEEDBACK:
${d.feedback === 'Chat messages' || d.feedback === 'Both' ? 'Send responses using game.StarterGui:SetCore("ChatMakeSystemMessage")' : ''}
${d.feedback === 'GUI notifications' || d.feedback === 'Both' ? 'Fire RemoteEvent to show GUI popup on client' : ''}
Add cooldown (1-2 seconds) to prevent spam
Show error for unknown commands or missing arguments`
},
'soundmanager': {
name: 'Sound Manager',
fields: [
{ id: 'soundTypes', label: 'Sound Categories', type: 'list', placeholder: 'Music, SFX, Ambience...', required: true },
{ id: 'volumeControl', label: 'Player Volume Control', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'persistence', label: 'Save Volume Preferences', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Sound Manager System.
CATEGORIES: ${d.soundTypes ? d.soundTypes.join(', ') : 'Sound types'}
LOCATION: LocalScript in StarterPlayerScripts
STRUCTURE:
Organize sounds in ReplicatedStorage folders by category: ${d.soundTypes ? d.soundTypes.join(', ') : 'categories'}
PLAYING SOUNDS:
Create PlaySound(soundName, category) function:
1. Clone sound from storage
2. Set Volume based on category setting
3. Parent to SoundService or Workspace
4. Play and auto-destroy when ended
${d.volumeControl === 'Yes' ? `VOLUME CONTROLS:
Create settings GUI with slider for each category
When adjusted: update all playing sounds in that category
${d.persistence === 'Yes' ? 'Save settings to DataStore, load on join' : 'Settings reset on leave'}` : 'Use default volumes'}
Categories can be controlled independently (mute music but keep SFX)`
},
'servermessages': {
name: 'Server Message System',
fields: [
{ id: 'messageTypes', label: 'Message Types', type: 'checkboxes',
options: ['Announcements', 'Warnings', 'Tips', 'Events'], default: ['Announcements'] },
{ id: 'displayMethod', label: 'Display Method', type: 'select',
options: ['Top banner', 'Center screen', 'Chat messages'], default: 'Top banner' },
{ id: 'duration', label: 'Display Duration (seconds)', type: 'number', default: '5' },
{ id: 'colors', label: 'Color-Coded Types', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Server Message System.
TYPES: ${d.messageTypes ? d.messageTypes.join(', ') : 'Messages'}
DISPLAY: ${d.displayMethod}
STRUCTURE:
- Script in ServerScriptService (sends messages)
- LocalScript in StarterPlayerScripts (displays messages)
- RemoteEvent "ServerMessage"
SENDING (Server):
Create BroadcastMessage(text, type) function
Fire RemoteEvent:FireAllClients(text, type)
DISPLAYING (Client):
${d.displayMethod === 'Top banner' ? `Create banner Frame at top of screen
${d.colors === 'Yes' ? 'Color-code by message type' : ''}
Tween in, display ${d.duration}s, tween out` : ''}
${d.displayMethod === 'Center screen' ? `Create centered Frame with semi-transparent background
${d.colors === 'Yes' ? 'Border color by type' : ''}
Fade in, display ${d.duration}s, fade out` : ''}
${d.displayMethod === 'Chat messages' ? `Use StarterGui:SetCore("ChatMakeSystemMessage")
${d.colors === 'Yes' ? 'Set Color property by type' : ''}
Appears in chat window` : ''}
Queue multiple messages to prevent overlap
Allow admins to send custom messages with command`
}
}
},
'multiplayer-systems': {
name: 'Multiplayer & Social',
description: 'Player interaction and social features',
templates: {
'party': {
name: 'Party/Group System',
fields: [
{ id: 'maxPartySize', label: 'Max Party Size', type: 'number', default: '4', required: true },
{ id: 'partyLeader', label: 'Party Leader Privileges', type: 'checkboxes',
options: ['Kick members', 'Invite players', 'Start games'], default: [] },
{ id: 'partyChat', label: 'Party-Only Chat', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'partyGui', label: 'Party GUI', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Party System (max ${d.maxPartySize} members).
STRUCTURE:
- Script in ServerScriptService
- ModuleScript to store party data (parties table with leader and members)
- RemoteEvents for party actions
FUNCTIONS:
- CreateParty(player) - Make new party with player as leader
- InviteToParty(leader, targetName) - Send 30-second invite
- JoinParty(player, partyId) - Add player if under ${d.maxPartySize}
- LeaveParty(player) - Remove player from current party
${d.partyLeader.includes('Kick members') ? '- KickFromParty(leader, target) - Leader can remove members' : ''}
${d.partyChat === 'Yes' ? 'PARTY CHAT:\nListen for "/p [message]" in Chatted event, send only to party members' : ''}
${d.partyGui === 'Yes' ? `GUI:\nShow party member list, leader has crown icon, invite/leave buttons
${d.partyLeader.includes('Kick members') ? 'Leader sees kick buttons next to members' : ''}` : ''}
On PlayerRemoving: remove from party, promote new leader if needed, delete empty parties`
},
'trading': {
name: 'Player Trading System',
fields: [
{ id: 'tradeableItems', label: 'What Can Be Traded', type: 'checkboxes',
options: ['Inventory items', 'Currency', 'Pets', 'Weapons'], default: ['Inventory items'] },
{ id: 'tradeDistance', label: 'Max Trade Distance', type: 'number', default: '20' },
{ id: 'confirmation', label: 'Confirmation Type', type: 'select',
options: ['Both players confirm', '5 second countdown', 'Instant'], default: 'Both players confirm' }
],
generate: (d) => `Create Player Trading System.
TRADEABLE: ${d.tradeableItems ? d.tradeableItems.join(', ') : 'Items'}
STRUCTURE:
- Script in ServerScriptService
- LocalScript for trade window GUI
- RemoteEvents: RequestTrade, OfferItem, ConfirmTrade, CancelTrade
STARTING TRADE:
Player uses "/trade [name]" or proximity button
Check both players within ${d.tradeDistance} studs
Create trade session storing both players' offerings
TRADE WINDOW:
Split screen: your items (left) | their items (right)
Drag items to offer, fire OfferItem RemoteEvent
Server validates ownership and updates both windows
${d.confirmation === 'Both players confirm' ? 'CONFIRM: Both click Confirm to lock offerings, then both Accept to execute' : ''}
${d.confirmation === '5 second countdown' ? 'CONFIRM: 5 second countdown starts when both have items, auto-executes at 0' : ''}
${d.confirmation === 'Instant' ? 'CONFIRM: Instant trade when Trade button clicked' : ''}
EXECUTE:
Validate items still exist and players still in range
Swap items between inventories
Clear trade session and close windows`
},
'friends': {
name: 'In-Game Friends System',
fields: [
{ id: 'features', label: 'Friend Features', type: 'checkboxes',
options: ['See online friends', 'Friend requests', 'Join friend server', 'Gift items'],
default: ['See online friends', 'Friend requests'] },
{ id: 'maxFriends', label: 'Max Friends Per Player', type: 'number', default: '50' },
{ id: 'persistence', label: 'Save Friend Lists', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Friends System (max ${d.maxFriends} friends).
STRUCTURE:
- Script in ServerScriptService
- LocalScript for friends GUI
${d.persistence === 'Yes' ? '- DataStore for friend lists' : ''}
- RemoteEvents for friend actions
${d.persistence === 'Yes' ? 'DATA: Save Friends table, PendingRequests, SentRequests per player' : 'SESSION DATA: Friends stored in table, resets on leave'}
${d.features.includes('Friend requests') ? `REQUESTS:
SendRequest(from, toName) - Add to receiver's PendingRequests
AcceptRequest(player, fromId) - Add both to each other's Friends lists
DeclineRequest(player, fromId) - Remove from pending` : ''}
${d.features.includes('See online friends') ? 'ONLINE STATUS: Check if friend UserIds are in game.Players, show with green dot' : ''}
${d.features.includes('Join friend server') ? 'JOIN FRIEND: Use TeleportService with MessagingService to get friend JobId' : ''}
${d.features.includes('Gift items') ? 'GIFTING: Select friend and item, send to their pending gifts inbox' : ''}
GUI: Tabs for friends list and requests, buttons to add/remove/join friends
Enforce ${d.maxFriends} friend limit`
},
'leaderboard': {
name: 'Global Leaderboard',
fields: [
{ id: 'statType', label: 'Stat to Track', type: 'text', required: true, placeholder: 'Wins, Points, Time, Level...' },
{ id: 'topCount', label: 'Show Top Players', type: 'number', default: '10' },
{ id: 'updateFrequency', label: 'Update Frequency', type: 'select',
options: ['Real-time', 'Every 30 seconds', 'Every 5 minutes'], default: 'Every 30 seconds' },
{ id: 'scope', label: 'Leaderboard Type', type: 'radio', options: ['Server-only', 'Global (all servers)'], default: 'Global (all servers)' }
],
generate: (d) => `Create Leaderboard tracking ${d.statType} (top ${d.topCount}).
STRUCTURE:
- Script in ServerScriptService
${d.scope === 'Global (all servers)' ? '- OrderedDataStore for global rankings' : ''}
- LocalScript for leaderboard GUI
- RemoteFunction to get rankings
${d.scope === 'Global (all servers)' ? `GLOBAL:
Use OrderedDataStore:GetSortedAsync() to get top ${d.topCount}
Update OrderedDataStore when player's ${d.statType} changes
Use Players:GetNameFromUserIdAsync() for usernames` : `SERVER-ONLY:
Loop through game.Players, get ${d.statType} from leaderstats
Sort by value, return top ${d.topCount}`}
GUI:
Create ScrollingFrame with entries for each rank
Show: rank number, username, ${d.statType} value
Highlight current player's entry
${d.updateFrequency === 'Real-time' ? 'Update when any player\'s stat changes' : `Update every ${d.updateFrequency === 'Every 30 seconds' ? '30' : '300'} seconds`}`
}
}
},
'game-modes': {
name: 'Game Modes',
description: 'Complete game mode systems',
templates: {
'round': {
name: 'Round-Based Game',
fields: [
{ id: 'roundDuration', label: 'Round Duration (seconds)', type: 'number', default: '180', required: true },
{ id: 'intermission', label: 'Intermission Time (seconds)', type: 'number', default: '15' },
{ id: 'minPlayers', label: 'Min Players to Start', type: 'number', default: '2' },
{ id: 'winCondition', label: 'Win Condition', type: 'select',
options: ['Last player alive', 'Most points', 'Time runs out'], default: 'Last player alive' },
{ id: 'rewards', label: 'Winner Rewards', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Round-Based Game (${d.roundDuration}s rounds, ${d.intermission}s intermission).
LOCATION: Script in ServerScriptService
ROUND LOOP:
1. WAITING: repeat until Players >= ${d.minPlayers}
2. INTERMISSION: countdown ${d.intermission} seconds
3. START: teleport players to arena, reset characters
4. ACTIVE: run for ${d.roundDuration}s or until win condition met
5. END: declare winner, award rewards, cleanup
WIN CONDITION:
${d.winCondition === 'Last player alive' ? 'Check alive players each loop, winner when only 1 remains' : ''}
${d.winCondition === 'Most points' ? 'At round end, find player with highest Points in leaderstats' : ''}
${d.winCondition === 'Time runs out' ? 'When timer reaches 0, surviving players win' : ''}
${d.rewards === 'Yes' ? 'REWARDS: Add to winner\'s leaderstats.Wins and Coins' : ''}
Use StringValue "Status" in ReplicatedStorage to show current phase
Loop continuously, resetting after each round`
},
'battle-royale': {
name: 'Battle Royale Mode',
fields: [
{ id: 'shrinkingZone', label: 'Shrinking Safe Zone', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'shrinkInterval', label: 'Zone Shrinks Every (seconds)', type: 'number', default: '60',
show_if: { field: 'shrinkingZone', value: 'Yes' } },
{ id: 'lootSpawns', label: 'Random Loot', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'startingPlayers', label: 'Min Players to Start', type: 'number', default: '10' },
{ id: 'spectate', label: 'Spectate After Death', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Battle Royale Mode (min ${d.startingPlayers} players).
STRUCTURE:
- Script in ServerScriptService
${d.shrinkingZone === 'Yes' ? '- Part "SafeZone" in Workspace (transparent sphere)' : ''}
- Folder "SpawnPoints" with many spawn parts
${d.lootSpawns === 'Yes' ? '- Folder "LootItems" in ServerStorage' : ''}
START:
Wait for ${d.startingPlayers}+ players, teleport to random spawn points
${d.shrinkingZone === 'Yes' ? `SAFE ZONE:
Every ${d.shrinkInterval}s: tween SafeZone smaller and to new position
Players outside zone take 5 damage per second
Use RunService to check player distance from zone center` : ''}
${d.lootSpawns === 'Yes' ? 'LOOT: Spawn random items at loot points every 30s during match' : ''}
WIN: When only 1 player alive, award victory and coins
${d.spectate === 'Yes' ? 'SPECTATE: Dead players camera follows alive players' : 'Dead players wait in lobby'}
After winner declared, wait 10s and restart`
},
'tycoon': {
name: 'Tycoon Game',
fields: [
{ id: 'claimMethod', label: 'How to Claim Tycoon', type: 'radio', options: ['Touch part', 'GUI button', 'Automatic'], default: 'Touch part' },
{ id: 'moneyGen', label: 'Money Generation', type: 'select',
options: ['Droppers (physical coins)', 'Passive income', 'Both'], default: 'Droppers (physical coins)' },
{ id: 'maxTycoons', label: 'Max Tycoons', type: 'number', default: '8' },
{ id: 'saving', label: 'Save Progress', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Tycoon Game (${d.maxTycoons} tycoons).
SETUP:
Create ${d.maxTycoons} identical tycoon folders in Workspace
Each contains: Base, ClaimPart, Purchases folder, IntValue "Owner" (0=unclaimed), IntValue "Cash"
CLAIMING:
${d.claimMethod === 'Touch part' ? 'ClaimPart.Touched: if Owner==0, set Owner to player.UserId' : ''}
${d.claimMethod === 'GUI button' ? 'ProximityPrompt on ClaimPart to claim' : ''}
${d.claimMethod === 'Automatic' ? 'Auto-assign first unclaimed tycoon on PlayerAdded' : ''}
${d.moneyGen.includes('Droppers') ? `DROPPERS:
Loop spawning coin parts, on touch by owner: add to Cash, destroy coin` : ''}
${d.moneyGen.includes('Passive') ? `PASSIVE:
Loop adding 50 to Cash every 5 seconds` : ''}
PURCHASES:
Items in Purchases folder start invisible/non-solid
Create ProximityPrompt buttons with cost
On triggered: check owner, check Cash >= cost, deduct and unlock item
${d.saving === 'Yes' ? 'SAVE: DataStore tracks owned tycoon, Cash value, and purchased items' : 'RESET: On leave, reset Owner to 0'}`
},
'obby': {
name: 'Obby/Parkour Course',
fields: [
{ id: 'stages', label: 'Number of Stages', type: 'number', default: '50', required: true },
{ id: 'checkpoints', label: 'Checkpoint System', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'timer', label: 'Show Completion Timer', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'killBricks', label: 'Instant Kill Parts', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'rewards', label: 'Stage Completion Rewards', type: 'radio', options: ['Yes (coins per stage)', 'No'], default: 'Yes (coins per stage)' }
],
generate: (d) => `Create Obby with ${d.stages} stages.
STRUCTURE:
- Script in ServerScriptService
- ${d.stages} checkpoint parts in Workspace.Stages folder (named "1", "2", etc.)
${d.killBricks === 'Yes' ? '- Kill parts in KillBricks folder' : ''}
SETUP:
Create leaderstats.Stage (IntValue) starting at 1 for each player
${d.checkpoints === 'Yes' ? `CHECKPOINTS:
Each checkpoint: Part.Touched checks if player touched stage = their current stage + 1
If yes: update Stage value, set RespawnLocation to checkpoint
${d.rewards === 'Yes (coins per stage)' ? 'Award coins = stage * 10' : ''}
Stage ${d.stages} = completion, award bonus` : ''}
${d.killBricks === 'Yes' ? 'KILL BRICKS:\nPart.Touched: set Humanoid.Health = 0, player respawns at last checkpoint' : ''}
${d.timer === 'Yes' ? 'TIMER:\nLocalScript tracks time since spawn, displays in GUI, saves best time to OrderedDataStore' : ''}
Players respawn at their highest checkpoint reached`
},
'tag': {
name: 'Tag/Chase Game',
fields: [
{ id: 'variant', label: 'Tag Type', type: 'select',
options: ['Classic Tag', 'Freeze Tag', 'Infection'], default: 'Classic Tag' },
{ id: 'roundTime', label: 'Round Duration (seconds)', type: 'number', default: '120' },
{ id: 'taggerBoost', label: 'Tagger Speed Boost', type: 'radio', options: ['Yes', 'No'], default: 'No' },
{ id: 'safeZones', label: 'Temporary Safe Zones', type: 'radio', options: ['Yes', 'No'], default: 'No' }
],
generate: (d) => `Create ${d.variant} game (${d.roundTime}s rounds).
STRUCTURE:
- Script in ServerScriptService
- RemoteEvents for tag updates
- LocalScript for visual effects
START:
Select random player as initial tagger
${d.variant === 'Infection' ? 'Create Survivors and Infected teams' : ''}
Mark tagger with red color${d.taggerBoost === 'Yes' ? ' and WalkSpeed 20' : ''}
${d.variant === 'Classic Tag' ? `TAGGING:
Use Touched event, when tagger touches player: transfer tagger status to new player` : ''}
${d.variant === 'Freeze Tag' ? `FREEZING:
Tagger touch freezes player (WalkSpeed=0, blue color)
Non-frozen players unfreeze by touching frozen players
Win: Tagger freezes all, or survivors last until timer ends` : ''}
${d.variant === 'Infection' ? `INFECTION:
Infected touch survivors to convert them to infected team
Win: All infected, or survivors last until timer` : ''}
${d.safeZones === 'Yes' ? 'SAFE ZONES: Parts where players can\'t be tagged, max 5s stay with 10s cooldown' : ''}
ROUND END:
Award coins based on performance, reset players, start new round`
}
}
},
'world-features': {
name: 'World & Environment',
description: 'Day/night cycles, weather, and world systems',
templates: {
'day-night': {
name: 'Day/Night Cycle',
fields: [
{ id: 'cycleLength', label: 'Full Cycle Duration (minutes)', type: 'number', default: '20', required: true },
{ id: 'startTime', label: 'Starting Time', type: 'select',
options: ['Dawn (6:00)', 'Noon (12:00)', 'Dusk (18:00)', 'Midnight (0:00)'], default: 'Noon (12:00)' },
{ id: 'lighting', label: 'Lighting Changes', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'effects', label: 'Time-Based Effects', type: 'checkboxes',
options: ['Street lights', 'Sky color changes', 'Spawn different NPCs'], default: [] }
],
generate: (d) => `Create Day/Night Cycle (${d.cycleLength} minute cycle).
LOCATION: Script in ServerScriptService
SETUP:
Set Lighting.ClockTime to ${d.startTime === 'Dawn (6:00)' ? '6' : d.startTime === 'Noon (12:00)' ? '12' : d.startTime === 'Dusk (18:00)' ? '18' : '0'}
CYCLE:
Calculate: (24 hours * 60 minutes) / (${d.cycleLength} real minutes * 60 seconds) = minutes to add per second
Loop: add to ClockTime, wait 1 second, repeat
${d.lighting === 'Yes' ? 'LIGHTING:\nDay (6-18): Brightness 2, bright ambient\nNight (18-6): Brightness 0.5, dark blue ambient\nTween transitions smoothly' : ''}
${d.effects.includes('Street lights') ? 'STREET LIGHTS:\nTag lights with CollectionService, enable PointLight when ClockTime >= 18 or < 6' : ''}
${d.effects.includes('Sky color changes') ? 'SKY COLORS:\nTween Sky properties to match time (orange dawn, blue noon, red dusk, dark night)' : ''}
${d.effects.includes('Spawn different NPCs') ? 'NPCS:\nSpawn daytime NPCs (6-18), despawn and spawn nighttime NPCs (18-6)' : ''}`
},
'weather': {
name: 'Dynamic Weather System',
fields: [
{ id: 'weatherTypes', label: 'Weather Types', type: 'checkboxes',
options: ['Rain', 'Snow', 'Fog', 'Thunderstorm', 'Sunny'], default: ['Rain', 'Sunny'], required: true },
{ id: 'randomChanges', label: 'Random Weather Changes', type: 'radio', options: ['Yes', 'No'], default: 'Yes' },
{ id: 'changeInterval', label: 'Weather Changes Every (minutes)', type: 'number', default: '5',
show_if: { field: 'randomChanges', value: 'Yes' } },
{ id: 'effects', label: 'Weather Effects', type: 'radio', options: ['Visual only', 'Affects gameplay'], default: 'Visual only' }
],
generate: (d) => `Create Weather System: ${d.weatherTypes ? d.weatherTypes.join(', ') : 'weather types'}.
LOCATION: Script in ServerScriptService
${d.weatherTypes.includes('Rain') ? 'RAIN: ParticleEmitter falling droplets, fog, gray lighting' + (d.effects === 'Affects gameplay' ? ', slippery ground' : '') : ''}
${d.weatherTypes.includes('Snow') ? 'SNOW: Snowflake particles (slower fall), white ambient' + (d.effects === 'Affects gameplay' ? ', reduced friction' : '') : ''}
${d.weatherTypes.includes('Fog') ? 'FOG: Lighting.FogEnd = 100' + (d.effects === 'Affects gameplay' ? ', reduced vision range' : '') : ''}
${d.weatherTypes.includes('Thunderstorm') ? 'STORM: Rain + random lightning flashes (Brightness spike) + thunder sounds' + (d.effects === 'Affects gameplay' ? ', lightning damage' : '') : ''}
${d.weatherTypes.includes('Sunny') ? 'SUNNY: Clear fog, high brightness, warm lighting, sun rays' + (d.effects === 'Affects gameplay' ? ', speed boost' : '') : ''}
${d.randomChanges === 'Yes' ? `RANDOM CHANGES:
Loop: wait ${d.changeInterval} minutes, pick random weather from list, activate it
Prevent same weather twice in a row` : 'MANUAL: Create SetWeather(weatherName) function for on-demand changes'}
Create activate/deactivate functions for each weather type`
},
'zones': {
name: 'Map Zones/Regions',
fields: [
{ id: 'zones', label: 'Zone Names', type: 'list', placeholder: 'Safe Zone, PvP Arena, Shop District...', required: true },
{ id: 'effects', label: 'Zone Effects', type: 'select',
options: ['Just show name', 'Enable/disable features', 'Apply buffs/debuffs'], default: 'Enable/disable features' },
{ id: 'notifications', label: 'Entry/Exit Notifications', type: 'radio', options: ['Yes', 'No'], default: 'Yes' }
],
generate: (d) => `Create Map Zones: ${d.zones ? d.zones.join(', ') : 'zones'}.
SETUP:
Create invisible Parts for each zone boundary
Name parts: ${d.zones ? d.zones.join(', ') : 'zone names'}
Add StringValue "ZoneType" with zone name
Parent to Workspace.Zones
DETECTION:
Use Part.Touched or Region3 to detect when players enter/exit zones
Track current zone per player
${d.effects === 'Just show name' ? 'DISPLAY: Update GUI showing current zone name' : ''}
${d.effects === 'Enable/disable features' ? 'FEATURES:\nSafe Zone: disable PvP, enable regen\nPvP Arena: enable combat, show kill feed\nShop: open shop GUI, reduce speed' : ''}
${d.effects === 'Apply buffs/debuffs' ? 'BUFFS: Define effects per zone (speed multiplier, damage multiplier, health regen, etc.)\nApply on enter, remove on exit' : ''}
${d.notifications === 'Yes' ? 'NOTIFICATIONS: Fire RemoteEvent on zone change, show "Entering [Zone]" popup' : ''}
Update GUI to show current zone, color-coded by danger level`
},
'teleporters': {
name: 'Teleporter Network',
fields: [
{ id: 'teleporters', label: 'Teleporter Locations', type: 'list', placeholder: 'Spawn to Shop, Arena to Lobby...', required: true },
{ id: 'teleportMethod', label: 'Activation Method', type: 'select',
options: ['Touch part', 'ProximityPrompt', 'GUI menu'], default: 'ProximityPrompt' },
{ id: 'cooldown', label: 'Teleport Cooldown (seconds)', type: 'number', default: '3' },
{ id: 'effect', label: 'Teleport Effect', type: 'radio', options: ['Particles and sound', 'Simple fade', 'Instant'], default: 'Particles and sound' }
],
generate: (d) => `Create Teleporter Network: ${d.teleporters ? d.teleporters.join(' | ') : 'locations'}.
SETUP:
Create teleporter pads at each location
Add ObjectValue "Destination" pointing to target teleporter
${d.teleportMethod === 'ProximityPrompt' ? 'Add ProximityPrompt with "Teleport to [Name]"' : ''}
COOLDOWN:
Track last teleport time per player, require ${d.cooldown}s between uses
${d.teleportMethod === 'Touch part' ? 'TOUCH: Part.Touched checks cooldown then teleports' : ''}
${d.teleportMethod === 'ProximityPrompt' ? 'PROMPT: ProximityPrompt.Triggered checks cooldown then teleports' : ''}
${d.teleportMethod === 'GUI menu' ? 'GUI: Menu with destination buttons, fire RemoteEvent to teleport' : ''}
TELEPORT:
Set HumanoidRootPart.CFrame = destination.CFrame + Vector3.new(0,3,0)
${d.effect === 'Particles and sound' ? 'EFFECTS: ParticleEmitter burst + sound at departure and arrival' : ''}
${d.effect === 'Simple fade' ? 'EFFECTS: White screen fade in/out on client' : ''}
${d.effect === 'Instant' ? 'No effects, instant teleport' : ''}
Set up two-way or network (hub-and-spoke) teleporter layout`
}
}
}
};
const CONFIG = {
MAX_HISTORY_ITEMS: 50,
AUTO_SUBMIT_DEFAULT_DELAY: 500,
TOAST_DURATION: 3000,
PREVIEW_DEBOUNCE: 150,
STORAGE_KEYS: {
HISTORY: 'lemonade_history',
FAVORITES: 'lemonade_favorites',
TEMPLATE_STATS: 'lemonade_template_stats',
AUTO_SUBMIT: 'autoSubmit',
AUTO_SUBMIT_DELAY: 'autoSubmitDelay',
THEME: 'theme',
LAST_VIEW: 'lemonade_last_view'
}
};
GM_addStyle(`
.lpb-trigger {
position: fixed;
bottom: 20px;
left: 20px;
background: linear-gradient(135deg, #2196F3, #1976D2);
color: white;
border: none;
padding: 12px 20px;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 999998;
transition: all 0.3s ease;
font-family: -apple-system, system-ui, sans-serif;
}
.lpb-trigger:hover {
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(33,150,243,0.4);
}
.lpb-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.85);
z-index: 999999;
align-items: center;
justify-content: center;
}
.lpb-modal.active {
display: flex;
}
.lpb-modal-content {
background: #1a1a1a;
border: 2px solid #2196F3;
border-radius: 12px;
width: 90%;
max-width: 900px;
max-height: 85vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
[data-lpb-theme="light"] .lpb-modal-content {
background: #ffffff;
border-color: #2196F3;
}
[data-lpb-theme="light"] .lpb-header {
background: #f5f5f5 !important;
border-bottom-color: #e0e0e0 !important;
}
[data-lpb-theme="light"] .lpb-body {
color: #333 !important;
}
[data-lpb-theme="light"] .lpb-category-card,
[data-lpb-theme="light"] .lpb-template-card,
[data-lpb-theme="light"] .lpb-history-item,
[data-lpb-theme="light"] .lpb-settings-section {
background: #f9f9f9 !important;
border-color: #e0e0e0 !important;
}
[data-lpb-theme="light"] .lpb-input,
[data-lpb-theme="light"] .lpb-select,
[data-lpb-theme="light"] .lpb-textarea {
background: #fff !important;
border-color: #ccc !important;
color: #333 !important;
}
[data-lpb-theme="light"] .lpb-preview-content {
background: #f5f5f5 !important;
color: #333 !important;
}
.lpb-header {
padding: 20px 24px;
border-bottom: 2px solid #2a2a2a;
display: flex;
justify-content: space-between;
align-items: center;
background: #222;
}
.lpb-title {
font-size: 20px;
font-weight: 600;
color: #2196F3;
margin: 0;
font-family: -apple-system, system-ui, sans-serif;
}
.lpb-close-btn {
background: #333;
border: none;
color: #aaa;
font-size: 24px;
cursor: pointer;
width: 36px;
height: 36px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
line-height: 1;
}
.lpb-close-btn:hover {
background: #2196F3;
color: white;
}
.lpb-body {
padding: 24px;
overflow-y: auto;
flex: 1;
color: #ddd;
font-family: -apple-system, system-ui, sans-serif;
}
.lpb-body::-webkit-scrollbar {
width: 10px;
}
.lpb-body::-webkit-scrollbar-track {
background: #222;
}
.lpb-body::-webkit-scrollbar-thumb {
background: #2196F3;
border-radius: 5px;
}
.lpb-tabs {
display: flex;
gap: 4px;
margin-bottom: 24px;
border-bottom: 2px solid #2a2a2a;
}
.lpb-tab {
padding: 12px 24px;
background: none;
border: none;
color: #777;
cursor: pointer;
font-weight: 600;
font-size: 14px;
border-bottom: 3px solid transparent;
transition: all 0.2s;
margin-bottom: -2px;
font-family: inherit;
}
.lpb-tab:hover {
color: #2196F3;
}
.lpb-tab.active {
color: #2196F3;
border-bottom-color: #2196F3;
}
.lpb-category-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.lpb-category-card {
background: #242424;
border: 2px solid #333;
border-radius: 10px;
padding: 24px;
cursor: pointer;
transition: all 0.3s ease;
}
.lpb-category-card:hover {
border-color: #2196F3;
transform: translateY(-4px);
box-shadow: 0 6px 16px rgba(33,150,243,0.2);
}
.lpb-category-name {
font-size: 18px;
font-weight: 600;
color: #fff;
margin-bottom: 8px;
}
.lpb-category-desc {
font-size: 13px;
color: #888;
line-height: 1.5;
}
.lpb-template-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 14px;
}
.lpb-template-card {
background: #242424;
border: 2px solid #333;
border-radius: 8px;
padding: 18px;
cursor: pointer;
transition: all 0.2s;
position: relative;
}
.lpb-template-card:hover {
border-color: #2196F3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(33,150,243,0.15);
}
.lpb-template-name {
font-size: 15px;
font-weight: 600;
color: #fff;
}
.lpb-usage-badge {
position: absolute;
top: 8px;
right: 8px;
background: rgba(33,150,243,0.2);
color: #2196F3;
padding: 2px 8px;
border-radius: 10px;
font-size: 10px;
font-weight: 600;
}
.lpb-back-btn {
display: inline-flex;
align-items: center;
gap: 8px;
background: #333;
color: #ddd;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
margin-bottom: 20px;
transition: all 0.2s;
font-family: inherit;
}
.lpb-back-btn:hover {
background: #444;
}
.lpb-form-field {
margin-bottom: 22px;
}
.lpb-label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #2196F3;
font-size: 14px;
}
.lpb-required {
color: #f44336;
margin-left: 4px;
}
.lpb-help-text {
font-size: 12px;
color: #777;
margin-top: 6px;
font-style: italic;
}
.lpb-input, .lpb-select, .lpb-textarea {
width: 100%;
background: #242424;
border: 2px solid #333;
border-radius: 6px;
padding: 10px 14px;
color: #ddd;
font-size: 14px;
font-family: inherit;
transition: border-color 0.2s;
}
.lpb-input.lpb-error, .lpb-select.lpb-error, .lpb-textarea.lpb-error {
border-color: #f44336;
}
.lpb-input:focus, .lpb-select:focus, .lpb-textarea:focus {
outline: none;
border-color: #2196F3;
}
.lpb-textarea {
min-height: 90px;
resize: vertical;
font-family: 'Consolas', 'Monaco', monospace;
line-height: 1.6;
}
.lpb-radio-group, .lpb-checkbox-group {
display: flex;
flex-direction: column;
gap: 10px;
}
.lpb-radio-item, .lpb-checkbox-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: #242424;
border: 2px solid #333;
border-radius: 6px;
cursor: pointer;
transition: border-color 0.2s;
}
.lpb-radio-item:hover, .lpb-checkbox-item:hover {
border-color: #2196F3;
}
.lpb-radio-item input, .lpb-checkbox-item input {
cursor: pointer;
width: 18px;
height: 18px;
margin: 0;
}
.lpb-radio-item label, .lpb-checkbox-item label {
cursor: pointer;
flex: 1;
color: #ddd;
margin: 0;
}
.lpb-list-wrapper {
background: #242424;
border: 2px solid #333;
border-radius: 6px;
padding: 14px;
}
.lpb-list-row {
display: flex;
gap: 8px;
margin-bottom: 8px;
}
.lpb-list-input {
flex: 1;
background: #1a1a1a;
border: 1px solid #444;
border-radius: 4px;
padding: 8px 10px;
color: #ddd;
font-size: 13px;
font-family: inherit;
}
.lpb-list-input:focus {
outline: none;
border-color: #2196F3;
}
.lpb-list-remove {
background: #333;
border: 1px solid #555;
color: #ddd;
padding: 8px 14px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
font-family: inherit;
}
.lpb-list-remove:hover {
background: #f44336;
border-color: #f44336;
}
.lpb-list-add {
background: #2196F3;
border: none;
color: white;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 13px;
transition: all 0.2s;
font-family: inherit;
}
.lpb-list-add:hover {
background: #1976D2;
}
.lpb-preview {
background: #0d0d0d;
border: 2px solid #2196F3;
border-radius: 8px;
padding: 18px;
margin-top: 24px;
}
.lpb-preview-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
flex-wrap: wrap;
gap: 10px;
}
.lpb-preview-title {
font-weight: 600;
color: #2196F3;
font-size: 15px;
}
.lpb-preview-stats {
display: flex;
gap: 12px;
align-items: center;
}
.lpb-char-count {
font-size: 12px;
color: #888;
}
.lpb-preview-content {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
line-height: 1.8;
white-space: pre-wrap;
color: #ccc;
max-height: 400px;
overflow-y: auto;
background: #000;
padding: 14px;
border-radius: 6px;
}
.lpb-preview-content::-webkit-scrollbar {
width: 8px;
}
.lpb-preview-content::-webkit-scrollbar-track {
background: #111;
}
.lpb-preview-content::-webkit-scrollbar-thumb {
background: #2196F3;
border-radius: 4px;
}
.lpb-actions {
display: flex;
gap: 12px;
margin-top: 24px;
padding-top: 24px;
border-top: 2px solid #2a2a2a;
}
.lpb-btn {
flex: 1;
background: linear-gradient(135deg, #2196F3, #1976D2);
color: white;
border: none;
padding: 12px 24px;
border-radius: 6px;
font-weight: 600;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
font-family: inherit;
}
.lpb-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(33,150,243,0.3);
}
.lpb-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.lpb-btn-secondary {
background: #333;
color: #ddd;
}
.lpb-btn-secondary:hover {
background: #444;
box-shadow: none;
}
.lpb-btn-small {
padding: 8px 16px;
font-size: 13px;
flex: none;
}
.lpb-search-wrapper {
margin-bottom: 20px;
}
.lpb-search-input {
max-width: 400px;
}
.lpb-history-item {
background: #242424;
border: 2px solid #333;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
transition: all 0.2s;
}
.lpb-history-item:hover {
border-color: #2196F3;
}
.lpb-history-top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.lpb-history-info {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap;
}
.lpb-badge {
background: rgba(33,150,243,0.2);
color: #2196F3;
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
}
.lpb-custom-name {
background: rgba(76,175,80,0.2);
color: #4CAF50;
padding: 4px 10px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.lpb-timestamp {
font-size: 12px;
color: #666;
}
.lpb-fav-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
transition: transform 0.2s;
line-height: 1;
}
.lpb-fav-btn:hover {
transform: scale(1.2);
}
.lpb-history-preview {
font-size: 13px;
color: #999;
font-family: 'Consolas', monospace;
cursor: pointer;
user-select: none;
display: flex;
align-items: flex-start;
gap: 8px;
padding: 8px;
border-radius: 4px;
transition: background 0.2s;
}
.lpb-history-preview:hover {
background: rgba(33,150,243,0.1);
}
.lpb-history-preview-text {
flex: 1;
overflow: hidden;
}
.lpb-history-preview-text.truncated {
text-overflow: ellipsis;
white-space: nowrap;
}
.lpb-history-preview-text.expanded {
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
background: #0d0d0d;
padding: 12px;
border-radius: 4px;
border: 1px solid #333;
}
.lpb-history-preview-text.expanded::-webkit-scrollbar {
width: 8px;
}
.lpb-history-preview-text.expanded::-webkit-scrollbar-track {
background: #111;
}
.lpb-history-preview-text.expanded::-webkit-scrollbar-thumb {
background: #2196F3;
border-radius: 4px;
}
.lpb-expand-icon {
color: #2196F3;
font-size: 12px;
margin-top: 2px;
flex-shrink: 0;
}
.lpb-edit-modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.9);
z-index: 10000000;
align-items: center;
justify-content: center;
}
.lpb-edit-modal.active {
display: flex;
}
.lpb-edit-modal-content {
background: #1a1a1a;
border: 2px solid #2196F3;
border-radius: 12px;
padding: 24px;
width: 90%;
max-width: 500px;
}
.lpb-edit-modal h3 {
margin: 0 0 20px 0;
color: #2196F3;
font-size: 18px;
}
.lpb-edit-actions {
display: flex;
gap: 12px;
margin-top: 20px;
}
.lpb-file-autocomplete {
position: absolute;
background: #1a1a1a;
border: 2px solid #2196F3;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
max-height: 300px;
overflow-y: auto;
z-index: 10000001;
min-width: 250px;
display: none;
}
.lpb-file-autocomplete.active {
display: block;
}
.lpb-file-autocomplete::-webkit-scrollbar {
width: 8px;
}
.lpb-file-autocomplete::-webkit-scrollbar-track {
background: #222;
}
.lpb-file-autocomplete::-webkit-scrollbar-thumb {
background: #2196F3;
border-radius: 4px;
}
.lpb-file-item {
padding: 10px 14px;
cursor: pointer;
transition: background 0.2s;
color: #ddd;
font-size: 13px;
font-family: 'Consolas', monospace;
display: flex;
align-items: center;
gap: 8px;
}
.lpb-file-item:hover,
.lpb-file-item.selected {
background: #2196F3;
color: white;
}
.lpb-file-icon {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
}
.lpb-file-name {
flex: 1;
}
.lpb-file-path {
font-size: 11px;
opacity: 0.7;
}
.lpb-toast {
position: fixed;
bottom: 80px;
left: 20px;
background: linear-gradient(135deg, #2196F3, #1976D2);
color: white;
padding: 14px 24px;
border-radius: 8px;
font-weight: 600;
box-shadow: 0 4px 16px rgba(0,0,0,0.3);
z-index: 9999999;
animation: lpb-slide-in 0.3s ease;
font-family: -apple-system, system-ui, sans-serif;
}
.lpb-toast-error {
background: linear-gradient(135deg, #f44336, #d32f2f) !important;
}
.lpb-toast-info {
background: linear-gradient(135deg, #FF9800, #F57C00) !important;
}
.lpb-toast-success {
background: linear-gradient(135deg, #4CAF50, #388E3C) !important;
}
@keyframes lpb-slide-in {
from { transform: translateX(-300px); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
@keyframes lpb-slide-out {
from { transform: translateX(0); opacity: 1; }
to { transform: translateX(-300px); opacity: 0; }
}
.lpb-empty {
text-align: center;
padding: 60px 20px;
color: #666;
}
.lpb-field-hidden {
display: none;
}
.lpb-settings-section {
background: #242424;
border: 2px solid #333;
border-radius: 8px;
padding: 20px;
margin-bottom: 16px;
}
.lpb-settings-section h3 {
margin: 0 0 16px 0;
color: #2196F3;
font-size: 16px;
}
.lpb-setting-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #333;
gap: 16px;
}
.lpb-setting-item:last-child {
border-bottom: none;
}
.lpb-setting-label {
flex: 1;
}
.lpb-setting-label h4 {
margin: 0 0 4px 0;
color: #fff;
font-size: 14px;
}
.lpb-setting-label p {
margin: 0;
color: #888;
font-size: 12px;
}
.lpb-toggle-switch {
position: relative;
width: 50px;
height: 26px;
background: #333;
border-radius: 13px;
cursor: pointer;
transition: background 0.3s;
flex-shrink: 0;
}
.lpb-toggle-switch.active {
background: #2196F3;
}
.lpb-toggle-switch::after {
content: '';
position: absolute;
width: 22px;
height: 22px;
border-radius: 50%;
background: white;
top: 2px;
left: 2px;
transition: transform 0.3s;
}
.lpb-toggle-switch.active::after {
transform: translateX(24px);
}
.lpb-slider {
width: 100px;
}
.lpb-error-message {
background: rgba(244, 67, 54, 0.1);
border: 1px solid #f44336;
color: #f44336;
padding: 12px;
border-radius: 6px;
font-size: 13px;
margin-top: 12px;
}
.lpb-file-icon {
width: 16px;
height: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
flex-shrink: 0;
}
.lpb-file-icon img {
width: 16px;
height: 16px;
object-fit: contain;
}
.lpb-file-name {
flex: 1;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.lpb-file-path {
font-size: 11px;
opacity: 0.7;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@media (max-width: 768px) {
.lpb-modal-content {
width: 95%;
max-height: 90vh;
}
.lpb-category-grid, .lpb-template-grid {
grid-template-columns: 1fr;
}
.lpb-actions {
flex-direction: column;
}
.lpb-btn {
width: 100%;
}
.lpb-setting-item {
flex-direction: column;
align-items: flex-start;
}
}
`);
class Settings {
constructor() {
this.settings = {
autoSubmit: GM_getValue(CONFIG.STORAGE_KEYS.AUTO_SUBMIT, false),
autoSubmitDelay: GM_getValue(CONFIG.STORAGE_KEYS.AUTO_SUBMIT_DELAY, CONFIG.AUTO_SUBMIT_DEFAULT_DELAY),
theme: GM_getValue(CONFIG.STORAGE_KEYS.THEME, 'dark')
};
this.applyTheme();
}
get(key) {
return this.settings[key];
}
set(key, value) {
this.settings[key] = value;
GM_setValue(key, value);
}
toggleAutoSubmit() {
this.set(CONFIG.STORAGE_KEYS.AUTO_SUBMIT, !this.settings.autoSubmit);
this.settings.autoSubmit = !this.settings.autoSubmit;
return this.settings.autoSubmit;
}
setAutoSubmitDelay(delay) {
this.set(CONFIG.STORAGE_KEYS.AUTO_SUBMIT_DELAY, delay);
this.settings.autoSubmitDelay = delay;
}
toggleTheme() {
const newTheme = this.settings.theme === 'dark' ? 'light' : 'dark';
this.set(CONFIG.STORAGE_KEYS.THEME, newTheme);
this.settings.theme = newTheme;
this.applyTheme();
return newTheme;
}
applyTheme() {
document.body.setAttribute('data-lpb-theme', this.settings.theme);
}
}
class Storage {
save(key, data) {
GM_setValue(key, JSON.stringify(data));
}
load(key, defaultValue = null) {
const data = GM_getValue(key);
return data ? JSON.parse(data) : defaultValue;
}
}
class PromptHistory {
constructor() {
this.storage = new Storage();
this.items = this.storage.load(CONFIG.STORAGE_KEYS.HISTORY, []);
this.favorites = this.storage.load(CONFIG.STORAGE_KEYS.FAVORITES, []);
this.templateStats = this.storage.load(CONFIG.STORAGE_KEYS.TEMPLATE_STATS, {});
}
add(prompt, category, template, data) {
const item = {
id: Date.now() + '_' + Math.random().toString(36).substr(2, 9),
prompt,
category,
template,
data,
timestamp: Date.now(),
customName: null
};
this.items.unshift(item);
if (this.items.length > CONFIG.MAX_HISTORY_ITEMS) this.items.pop();
const key = `${category}:${template}`;
this.templateStats[key] = (this.templateStats[key] || 0) + 1;
this.storage.save(CONFIG.STORAGE_KEYS.HISTORY, this.items);
this.storage.save(CONFIG.STORAGE_KEYS.TEMPLATE_STATS, this.templateStats);
return item;
}
toggleFavorite(id) {
const index = this.favorites.indexOf(id);
if (index > -1) {
this.favorites.splice(index, 1);
} else {
this.favorites.push(id);
}
this.storage.save(CONFIG.STORAGE_KEYS.FAVORITES, this.favorites);
}
isFavorite(id) {
return this.favorites.includes(id);
}
rename(id, newName) {
const item = this.items.find(i => i.id === id);
if (item) {
item.customName = newName.trim() || null;
this.storage.save(CONFIG.STORAGE_KEYS.HISTORY, this.items);
}
}
remove(id) {
this.items = this.items.filter(i => i.id !== id);
this.favorites = this.favorites.filter(f => f !== id);
this.storage.save(CONFIG.STORAGE_KEYS.HISTORY, this.items);
this.storage.save(CONFIG.STORAGE_KEYS.FAVORITES, this.favorites);
}
getAll() {
return this.items;
}
getFavorites() {
return this.items.filter(i => this.favorites.includes(i.id));
}
getMostUsed(limit = 5) {
return Object.entries(this.templateStats)
.sort((a, b) => b[1] - a[1])
.slice(0, limit);
}
getUsageCount(categoryKey, templateKey) {
const key = `${categoryKey}:${templateKey}`;
return this.templateStats[key] || 0;
}
}
class FileAutocomplete {
constructor() {
this.activeInput = null;
this.dropdown = null;
this.selectedIndex = 0;
this.currentFiles = [];
this.atPosition = -1;
this.isMirroring = false;
this.lemonadeTextarea = null;
this.originalLemonadeValue = '';
this.extractInterval = null;
this.mirrorAttempts = 0;
this.maxMirrorAttempts = 3;
this.createDropdown();
this.findLemonadeTextarea();
}
findLemonadeTextarea() {
this.lemonadeTextarea = document.querySelector('textarea[name="chat-input"]');
if (!this.lemonadeTextarea) {
console.warn('Lemonade textarea not found');
}
}
createDropdown() {
this.dropdown = document.createElement('div');
this.dropdown.className = 'lpb-file-autocomplete';
this.dropdown.style.cssText = `
position: fixed;
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
max-height: 320px;
overflow-y: auto;
z-index: 99999999;
display: none;
min-width: 300px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
document.body.appendChild(this.dropdown);
}
attach(input) {
if (input.dataset.fileAutocompleteAttached) return;
input.dataset.fileAutocompleteAttached = 'true';
input.addEventListener('input', (e) => this.handleInput(e));
input.addEventListener('keydown', (e) => this.handleKeydown(e));
input.addEventListener('blur', (e) => {
if (this.isMirroring) {
return;
}
setTimeout(() => {
if (this.dropdown.matches(':hover')) return;
if (document.activeElement === input) return;
this.hide();
}, 200);
});
}
async handleInput(e) {
const input = e.target;
const value = input.value;
const cursorPos = input.selectionStart;
// Find the last @ before cursor
const atIndex = value.lastIndexOf('@', cursorPos - 1);
// Check if @ exists and is at start or after a space
if (atIndex !== -1 && (atIndex === 0 || value[atIndex - 1] === ' ')) {
const afterAt = value.substring(atIndex + 1, cursorPos);
// FIX: Stop if there's a space in the search term
if (afterAt.includes(' ')) {
this.stopMirroring();
this.hide();
return;
}
this.atPosition = atIndex;
this.activeInput = input;
if (!this.dropdown.classList.contains('active')) {
this.showLoading(input);
await this.mirrorToLemonade('@' + afterAt);
this.startExtractingDropdown(input, afterAt);
} else {
await this.mirrorToLemonade('@' + afterAt);
this.extractAndShowDropdown(input, afterAt);
}
return;
}
this.stopMirroring();
this.hide();
}
async mirrorToLemonade(text) {
if (!this.lemonadeTextarea) {
this.findLemonadeTextarea();
if (!this.lemonadeTextarea) {
console.warn('Cannot mirror: Lemonade textarea not found');
return;
}
}
if (!this.isMirroring) {
this.originalLemonadeValue = this.lemonadeTextarea.value;
this.isMirroring = true;
this.mirrorAttempts = 0;
}
const formInput = this.activeInput;
try {
this.lemonadeTextarea.focus();
this.lemonadeTextarea.click();
await new Promise(resolve => setTimeout(resolve, 50));
const nativeTextareaSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
).set;
nativeTextareaSetter.call(this.lemonadeTextarea, text);
this.lemonadeTextarea.setSelectionRange(text.length, text.length);
const eventTypes = [
new Event('input', { bubbles: true, cancelable: true }),
new Event('change', { bubbles: true, cancelable: true }),
new InputEvent('input', {
data: text[text.length - 1] || '@',
inputType: 'insertText',
bubbles: true,
cancelable: true,
composed: true
}),
new KeyboardEvent('keydown', {
key: text[text.length - 1] || '@',
bubbles: true,
cancelable: true
}),
new KeyboardEvent('keyup', {
key: text[text.length - 1] || '@',
bubbles: true,
cancelable: true
})
];
eventTypes.forEach(event => {
this.lemonadeTextarea.dispatchEvent(event);
});
setTimeout(() => {
if (formInput) {
formInput.focus();
if (formInput.setSelectionRange) {
const cursorPos = formInput.selectionStart;
formInput.setSelectionRange(cursorPos, cursorPos);
}
}
}, 10);
this.mirrorAttempts++;
} catch (error) {
console.error('Mirror error:', error);
if (formInput) {
formInput.focus();
}
}
}
startExtractingDropdown(input, query) {
if (this.extractInterval) {
clearInterval(this.extractInterval);
}
setTimeout(() => {
this.extractAndShowDropdown(input, query);
}, 300);
this.extractInterval = setInterval(() => {
this.extractAndShowDropdown(input, query);
if (this.mirrorAttempts >= this.maxMirrorAttempts && this.currentFiles.length === 0) {
clearInterval(this.extractInterval);
this.extractInterval = null;
}
}, 300);
}
extractAndShowDropdown(input, query) {
const lemonadeDropdown = document.querySelector('div.absolute.z-50') ||
document.querySelector('[role="listbox"]') ||
document.querySelector('div.shadow-lg');
if (!lemonadeDropdown) {
return;
}
const files = [];
const fileItems = lemonadeDropdown.querySelectorAll('div.flex.items-center.gap-2.px-3.py-2.cursor-pointer') ||
lemonadeDropdown.querySelectorAll('div.cursor-pointer.transition-colors') ||
lemonadeDropdown.querySelectorAll('div[class*="cursor-pointer"]');
fileItems.forEach((item) => {
try {
const textElements = item.querySelectorAll('div, span');
let fileName = null;
let filePath = null;
for (let el of textElements) {
const text = el.textContent.trim();
if (text && text.length > 0 && text.length < 100) {
if (!fileName && !text.includes('/')) {
fileName = text;
} else if (text.includes('/')) {
filePath = text;
}
}
}
if (fileName) {
const img = item.querySelector('img');
const fileType = img ? img.getAttribute('alt') || 'File' : 'File';
files.push({
name: fileName,
path: filePath || '',
fullPath: filePath || fileName,
type: fileType
});
}
} catch (e) {}
});
if (files.length > 0) {
this.currentFiles = files;
this.show(input, query);
}
}
showLoading(input) {
this.activeInput = input;
this.dropdown.innerHTML = `
<div class="lpb-file-item" style="justify-content: center; opacity: 0.7; padding: 20px;">
<div>
<div style="margin-bottom: 8px;">⏳ Loading files from Lemonade...</div>
<div style="font-size: 11px; opacity: 0.6;">This may take a few seconds</div>
</div>
</div>
`;
const rect = input.getBoundingClientRect();
this.dropdown.style.left = `${rect.left}px`;
this.dropdown.style.top = `${rect.bottom + 5}px`;
this.dropdown.classList.add('active');
this.dropdown.style.display = 'block';
}
show(input, query) {
this.activeInput = input;
this.selectedIndex = 0;
const filteredFiles = query ? this.currentFiles.filter(f =>
f.name.toLowerCase().includes(query.toLowerCase()) ||
f.path.toLowerCase().includes(query.toLowerCase())
) : this.currentFiles;
this.currentFiles = filteredFiles;
if (this.currentFiles.length === 0) {
this.dropdown.innerHTML = `
<div class="lpb-file-item" style="justify-content: center; opacity: 0.7; padding: 16px;">
<div>
<div>No files found</div>
<div style="font-size: 11px; margin-top: 4px;">Try a different search term</div>
</div>
</div>
`;
const rect = input.getBoundingClientRect();
this.dropdown.style.left = `${rect.left}px`;
this.dropdown.style.top = `${rect.bottom + 5}px`;
this.dropdown.classList.add('active');
this.dropdown.style.display = 'block';
return;
}
const filesHtml = this.currentFiles.map((file, index) => `
<div class="lpb-file-item ${index === 0 ? 'selected' : ''}" data-index="${index}" style="
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
background: ${index === 0 ? '#2196F3' : 'transparent'};
color: ${index === 0 ? '#fff' : '#ddd'};
font-family: -apple-system, system-ui, sans-serif;
border-radius: 4px;
transition: background 0.15s;
">
<div class="lpb-file-icon" style="font-size: 16px;">${file.type === 'Script' ? '📜' : '📦'}</div>
<div style="flex: 1; min-width: 0;">
<div class="lpb-file-name" style="font-weight: 500; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${this.escapeHtml(file.name)}</div>
${file.path ? `<div class="lpb-file-path" style="font-size: 11px; opacity: 0.6; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${this.escapeHtml(file.path)}</div>` : ''}
</div>
</div>
`).join('');
const footerHtml = `
<div style="padding: 8px 12px; border-top: 1px solid #333; background: rgba(0,0,0,0.2); font-size: 11px; color: #888;">
${this.currentFiles.length} file${this.currentFiles.length !== 1 ? 's' : ''} • Use ↑↓ to navigate • Enter to select • Space to close
</div>
`;
this.dropdown.innerHTML = filesHtml + footerHtml;
this.dropdown.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
});
this.dropdown.querySelectorAll('.lpb-file-item[data-index]').forEach(item => {
item.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const index = parseInt(item.dataset.index);
this.selectFile(this.currentFiles[index]);
});
item.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
});
item.addEventListener('mouseup', (e) => {
e.preventDefault();
e.stopPropagation();
});
});
const rect = input.getBoundingClientRect();
this.dropdown.style.left = `${rect.left}px`;
this.dropdown.style.top = `${rect.bottom + 5}px`;
this.dropdown.style.maxHeight = '320px';
this.dropdown.classList.add('active');
this.dropdown.style.display = 'block';
}
handleKeydown(e) {
if (!this.dropdown.classList.contains('active')) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
this.selectedIndex = Math.min(this.selectedIndex + 1, this.currentFiles.length - 1);
this.updateSelection();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
this.selectedIndex = Math.max(this.selectedIndex - 1, 0);
this.updateSelection();
} else if (e.key === 'Enter' || e.key === 'Tab') {
if (this.currentFiles.length > 0) {
e.preventDefault();
this.selectFile(this.currentFiles[this.selectedIndex]);
}
} else if (e.key === 'Escape' || e.key === ' ') {
e.preventDefault();
this.stopMirroring();
this.hide();
}
}
updateSelection() {
this.dropdown.querySelectorAll('.lpb-file-item[data-index]').forEach((item, index) => {
const isSelected = index === this.selectedIndex;
item.classList.toggle('selected', isSelected);
item.style.background = isSelected ? '#2196F3' : 'transparent';
item.style.color = isSelected ? '#fff' : '#ddd';
});
const selected = this.dropdown.querySelector('.lpb-file-item.selected');
if (selected) {
selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
}
}
selectFile(file) {
if (!this.activeInput || this.atPosition === -1) return;
const input = this.activeInput;
const value = input.value;
const cursorPos = input.selectionStart;
// FIX: Only replace from @ to cursor, preserving everything else
const beforeAt = value.substring(0, this.atPosition);
const afterCursor = value.substring(cursorPos);
const fileRef = `@${file.name}`;
const newValue = beforeAt + fileRef + ' ' + afterCursor;
const newCursorPos = beforeAt.length + fileRef.length + 1;
const nativeSetter = Object.getOwnPropertyDescriptor(
input.constructor.prototype,
'value'
).set;
nativeSetter.call(input, newValue);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
this.clearLemonadeChat();
this.hide();
this.stopMirroring();
input.focus();
input.setSelectionRange(newCursorPos, newCursorPos);
const toast = document.createElement('div');
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 12px 20px;
border-radius: 8px;
z-index: 999999999;
font-family: -apple-system, system-ui, sans-serif;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
`;
toast.textContent = `✓ Added @${file.name}`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 2000);
}
clearLemonadeChat() {
if (!this.lemonadeTextarea) return;
setTimeout(() => {
try {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
).set;
nativeSetter.call(this.lemonadeTextarea, '');
this.lemonadeTextarea.dispatchEvent(new Event('input', { bubbles: true }));
this.lemonadeTextarea.dispatchEvent(new Event('change', { bubbles: true }));
this.lemonadeTextarea.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
code: 'Escape',
keyCode: 27,
bubbles: false,
cancelable: true
}));
this.lemonadeTextarea.blur();
this.isMirroring = false;
this.mirrorAttempts = 0;
this.originalLemonadeValue = '';
console.log('[FileAutocomplete] Cleared Lemonade chat after delay');
} catch (error) {
console.error('Error clearing Lemonade chat:', error);
}
}, 500);
}
hide() {
this.dropdown.classList.remove('active');
this.dropdown.style.display = 'none';
this.atPosition = -1;
if (this.extractInterval) {
clearInterval(this.extractInterval);
this.extractInterval = null;
}
}
stopMirroring() {
if (this.extractInterval) {
clearInterval(this.extractInterval);
this.extractInterval = null;
}
if (this.isMirroring && this.lemonadeTextarea) {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLTextAreaElement.prototype,
'value'
).set;
nativeSetter.call(this.lemonadeTextarea, '');
this.lemonadeTextarea.dispatchEvent(new Event('input', { bubbles: true }));
this.lemonadeTextarea.dispatchEvent(new KeyboardEvent('keydown', {
key: 'Escape',
code: 'Escape',
keyCode: 27,
bubbles: false,
cancelable: true
}));
this.isMirroring = false;
this.mirrorAttempts = 0;
}
}
cleanup() {
this.stopMirroring();
this.hide();
}
escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
destroy() {
this.stopMirroring();
if (this.dropdown) {
this.dropdown.remove();
}
}
}
const fileAutocomplete = new FileAutocomplete();
// Find and attach to forum input when page loads
document.addEventListener('DOMContentLoaded', () => {
const forumInput = document.querySelector('textarea[name="message"]'); // or whatever your forum input selector is
if (forumInput) {
fileAutocomplete.attach(forumInput);
}
});
// Also watch for dynamically loaded inputs
const observer = new MutationObserver(() => {
const forumInput = document.querySelector('textarea[name="message"]'); // same selector
if (forumInput && !forumInput.dataset.autocompleteAttached) {
forumInput.dataset.autocompleteAttached = 'true';
fileAutocomplete.attach(forumInput);
}
});
observer.observe(document.body, { childList: true, subtree: true });
class Utils {
static log(message, data = null) {
console.log(`[Lemonade Builder] ${message}`, data || '');
}
static showToast(message, type = 'success', duration = CONFIG.TOAST_DURATION) {
const toast = document.createElement('div');
toast.className = `lpb-toast lpb-toast-${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.animation = 'lpb-slide-out 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, duration);
}
static downloadFile(content, filename, type = 'text/plain') {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
static escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
}
class ViewState {
constructor() {
this.storage = new Storage();
}
save(state) {
this.storage.save(CONFIG.STORAGE_KEYS.LAST_VIEW, state);
}
load() {
return this.storage.load(CONFIG.STORAGE_KEYS.LAST_VIEW, { tab: 'categories', category: null });
}
}
class UI {
constructor() {
this.history = new PromptHistory();
this.settings = new Settings();
this.viewState = new ViewState();
this.fileAutocomplete = new FileAutocomplete();
this.currentCategory = null;
this.currentTemplate = null;
this.currentData = {};
this.previewDebounceTimer = null;
this.validationErrors = [];
this.init();
}
init() {
this.createTrigger();
this.createMainModal();
this.createFormModal();
this.createEditModal();
this.setupEvents();
this.setupKeyboardShortcuts();
// Don't auto-scan on start, wait for first @ or manual refresh
Utils.showToast('Type @ in text fields to load and reference files', 'info', 4000);
}
createTrigger() {
const btn = document.createElement('button');
btn.className = 'lpb-trigger';
btn.textContent = 'Prompt Builder';
btn.id = 'lpb-trigger';
document.body.appendChild(btn);
}
createMainModal() {
const modal = document.createElement('div');
modal.id = 'lpb-main-modal';
modal.className = 'lpb-modal';
modal.innerHTML = `
<div class="lpb-modal-content">
<div class="lpb-header">
<h2 class="lpb-title">Prompt Builder</h2>
<button class="lpb-close-btn" data-modal="main">×</button>
</div>
<div class="lpb-body">
<div class="lpb-tabs">
<button class="lpb-tab active" data-tab="categories">Templates</button>
<button class="lpb-tab" data-tab="history">History</button>
<button class="lpb-tab" data-tab="favorites">Favorites</button>
<button class="lpb-tab" data-tab="settings">Settings</button>
</div>
<div id="lpb-main-content"></div>
</div>
</div>
`;
document.body.appendChild(modal);
}
createFormModal() {
const modal = document.createElement('div');
modal.id = 'lpb-form-modal';
modal.className = 'lpb-modal';
modal.innerHTML = `
<div class="lpb-modal-content">
<div class="lpb-header">
<h2 class="lpb-title" id="lpb-form-title">Template</h2>
<button class="lpb-close-btn" data-modal="form">×</button>
</div>
<div class="lpb-body">
<div id="lpb-form-content"></div>
</div>
</div>
`;
document.body.appendChild(modal);
}
createEditModal() {
const modal = document.createElement('div');
modal.id = 'lpb-edit-modal';
modal.className = 'lpb-edit-modal';
modal.innerHTML = `
<div class="lpb-edit-modal-content">
<h3>Rename Prompt</h3>
<input type="text" class="lpb-input" id="lpb-edit-name-input" placeholder="Enter custom name...">
<div class="lpb-edit-actions">
<button class="lpb-btn lpb-btn-secondary" id="lpb-edit-cancel">Cancel</button>
<button class="lpb-btn" id="lpb-edit-save">Save</button>
</div>
</div>
`;
document.body.appendChild(modal);
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
}
});
document.getElementById('lpb-edit-cancel').addEventListener('click', () => {
modal.classList.remove('active');
});
}
setupEvents() {
document.getElementById('lpb-trigger').addEventListener('click', () => {
this.showModal('main');
this.restoreLastView();
});
document.querySelectorAll('.lpb-close-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const modal = e.target.dataset.modal;
// Close file autocomplete dropdown
if (this.fileAutocomplete) {
this.fileAutocomplete.cleanup();
}
this.hideModal(modal);
if (modal === 'main') {
this.saveCurrentView();
}
});
});
document.querySelectorAll('.lpb-modal').forEach(modal => {
modal.addEventListener('click', (e) => {
// Don't close if clicking on the file autocomplete dropdown
if (e.target.closest('.lpb-file-autocomplete')) {
return;
}
if (e.target === modal) {
modal.classList.remove('active');
// Close file autocomplete dropdown
if (this.fileAutocomplete) {
this.fileAutocomplete.cleanup();
}
if (modal.id === 'lpb-main-modal') {
this.saveCurrentView();
}
}
});
});
document.querySelectorAll('.lpb-tab').forEach(tab => {
tab.addEventListener('click', (e) => {
document.querySelectorAll('.lpb-tab').forEach(t => t.classList.remove('active'));
e.target.classList.add('active');
const tabName = e.target.dataset.tab;
if (tabName === 'categories') this.showCategories();
else if (tabName === 'history') this.showHistory();
else if (tabName === 'favorites') this.showFavorites();
else if (tabName === 'settings') this.showSettings();
});
});
}
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
e.preventDefault();
this.showModal('main');
this.restoreLastView();
}
if (e.key === 'Escape') {
// Close file autocomplete first
if (this.fileAutocomplete) {
this.fileAutocomplete.cleanup();
}
if (document.getElementById('lpb-edit-modal').classList.contains('active')) {
document.getElementById('lpb-edit-modal').classList.remove('active');
} else if (document.getElementById('lpb-form-modal').classList.contains('active')) {
this.hideModal('form');
} else if (document.getElementById('lpb-main-modal').classList.contains('active')) {
this.hideModal('main');
this.saveCurrentView();
}
}
});
}
saveCurrentView() {
const activeTab = document.querySelector('.lpb-tab.active');
const state = {
tab: activeTab ? activeTab.dataset.tab : 'categories',
category: this.currentCategory
};
this.viewState.save(state);
}
restoreLastView() {
const state = this.viewState.load();
document.querySelectorAll('.lpb-tab').forEach(tab => {
tab.classList.toggle('active', tab.dataset.tab === state.tab);
});
if (state.tab === 'categories') {
if (state.category) {
this.showTemplates(state.category);
} else {
this.showCategories();
}
} else if (state.tab === 'history') {
this.showHistory();
} else if (state.tab === 'favorites') {
this.showFavorites();
} else if (state.tab === 'settings') {
this.showSettings();
}
}
showModal(type) {
const id = type === 'main' ? 'lpb-main-modal' : 'lpb-form-modal';
document.getElementById(id).classList.add('active');
}
hideModal(type) {
const id = type === 'main' ? 'lpb-main-modal' : 'lpb-form-modal';
document.getElementById(id).classList.remove('active');
if (this.fileAutocomplete) {
this.fileAutocomplete.cleanup();
}
}
showSettings() {
const content = document.getElementById('lpb-main-content');
content.innerHTML = `
<div class="lpb-settings-section">
<h3>Appearance</h3>
<div class="lpb-setting-item">
<div class="lpb-setting-label">
<h4>Theme</h4>
<p>Switch between dark and light mode</p>
</div>
<div class="lpb-toggle-switch ${this.settings.get('theme') === 'dark' ? 'active' : ''}" id="lpb-toggle-theme"></div>
</div>
</div>
<div class="lpb-settings-section">
<h3>Automation</h3>
<div class="lpb-setting-item">
<div class="lpb-setting-label">
<h4>Auto-Submit Prompts</h4>
<p>Automatically submit the prompt after inserting from history</p>
</div>
<div class="lpb-toggle-switch ${this.settings.get('autoSubmit') ? 'active' : ''}" id="lpb-toggle-autosubmit"></div>
</div>
<div class="lpb-setting-item">
<div class="lpb-setting-label">
<h4>Auto-Submit Delay</h4>
<p>Wait time before submitting (milliseconds)</p>
</div>
<input type="number" class="lpb-input lpb-slider" id="lpb-autosubmit-delay" value="${this.settings.get('autoSubmitDelay')}" min="100" max="3000" step="100" style="width: 100px;">
</div>
</div>
<div class="lpb-settings-section">
<h3>File References</h3>
<div class="lpb-setting-item">
<div class="lpb-setting-label">
<h4>@ File Mentions</h4>
<p>Type @ in any text field to reference scripts/files from Lemonade's file system</p>
</div>
</div>
<div class="lpb-setting-item">
<div class="lpb-setting-label" style="font-size: 12px; color: #888;">
<strong>How it works:</strong> When you type @ in a text field, it mirrors your input to Lemonade's chat to trigger their file autocomplete, then extracts and shows you the files in a custom dropdown.
</div>
</div>
</div>
<div class="lpb-settings-section">
<h3>Data Management</h3>
<div class="lpb-setting-item">
<div class="lpb-setting-label">
<h4>Export All Data</h4>
<p>Download all your prompts and settings</p>
</div>
<button class="lpb-btn lpb-btn-small" id="lpb-export-data">Export</button>
</div>
<div class="lpb-setting-item">
<div class="lpb-setting-label">
<h4>Import Data</h4>
<p>Restore from a backup file</p>
</div>
<button class="lpb-btn lpb-btn-small" id="lpb-import-data">Import</button>
</div>
<div class="lpb-setting-item">
<div class="lpb-setting-label">
<h4>Clear All History</h4>
<p>Delete all saved prompts and history</p>
</div>
<button class="lpb-btn lpb-btn-small lpb-btn-secondary" id="lpb-clear-all">Clear</button>
</div>
</div>
<div class="lpb-settings-section">
<h3>Statistics</h3>
<div style="color: #888; font-size: 13px; line-height: 1.8;">
<p style="margin: 8px 0;"> Total prompts created: <strong style="color: #2196F3;">${this.history.items.length}</strong></p>
<p style="margin: 8px 0;"> Favorited prompts: <strong style="color: #2196F3;">${this.history.favorites.length}</strong></p>
<p style="margin: 8px 0;"> Most used template: <strong style="color: #2196F3;">${this.getMostUsedTemplateName()}</strong></p>
</div>
</div>
<div class="lpb-settings-section">
<h3>About</h3>
<p style="color: #888; font-size: 13px; line-height: 1.6;">
<strong style="color: #2196F3;">Lemonade Prompt Builder v8.8.3.1</strong><br>
Enhanced workflow for Roblox script generation<br>
Features: Templates, validation, auto-submit, history, favorites, search, themes, rename, file mentions<br><br>
<strong>Keyboard Shortcuts:</strong><br>
• Ctrl+Shift+P - Open builder<br>
• Escape - Close modals<br>
• @ - Mention files/scripts in text fields (extracts from Lemonade's system)
</p>
</div>
`;
document.getElementById('lpb-toggle-theme')?.addEventListener('click', (e) => {
const newTheme = this.settings.toggleTheme();
e.target.classList.toggle('active', newTheme === 'dark');
Utils.showToast(`Theme changed to ${newTheme} mode`, 'info');
});
document.getElementById('lpb-toggle-autosubmit')?.addEventListener('click', (e) => {
const enabled = this.settings.toggleAutoSubmit();
e.target.classList.toggle('active', enabled);
Utils.showToast(enabled ? 'Auto-submit enabled' : 'Auto-submit disabled', 'info');
});
document.getElementById('lpb-autosubmit-delay')?.addEventListener('change', (e) => {
this.settings.setAutoSubmitDelay(parseInt(e.target.value));
Utils.showToast('Delay updated', 'success');
});
document.getElementById('lpb-export-data')?.addEventListener('click', () => {
const data = {
settings: this.settings.settings,
history: this.history.items,
favorites: this.history.favorites,
templateStats: this.history.templateStats,
exportDate: new Date().toISOString(),
version: '8.8.3.1'
};
Utils.downloadFile(
JSON.stringify(data, null, 2),
`lemonade-builder-backup-${Date.now()}.json`,
'application/json'
);
Utils.showToast('Data exported successfully', 'success');
});
document.getElementById('lpb-import-data')?.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const data = JSON.parse(event.target.result);
if (confirm('Import will overwrite current data. Continue?')) {
this.history.items = data.history || [];
this.history.favorites = data.favorites || [];
this.history.templateStats = data.templateStats || {};
this.history.storage.save(CONFIG.STORAGE_KEYS.HISTORY, this.history.items);
this.history.storage.save(CONFIG.STORAGE_KEYS.FAVORITES, this.history.favorites);
this.history.storage.save(CONFIG.STORAGE_KEYS.TEMPLATE_STATS, this.history.templateStats);
Object.keys(data.settings || {}).forEach(key => {
this.settings.set(key, data.settings[key]);
});
Utils.showToast('Data imported successfully!', 'success');
this.showSettings();
}
} catch (err) {
Utils.showToast('Invalid backup file', 'error');
console.error('Import error:', err);
}
};
reader.readAsText(file);
};
input.click();
});
document.getElementById('lpb-clear-all')?.addEventListener('click', () => {
if (confirm('Are you sure you want to clear ALL history and data? This cannot be undone!')) {
this.history.items = [];
this.history.favorites = [];
this.history.templateStats = {};
this.history.storage.save(CONFIG.STORAGE_KEYS.HISTORY, []);
this.history.storage.save(CONFIG.STORAGE_KEYS.FAVORITES, []);
this.history.storage.save(CONFIG.STORAGE_KEYS.TEMPLATE_STATS, {});
Utils.showToast('All data cleared', 'success');
this.showSettings();
}
});
document.getElementById('lpb-import-data')?.addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
try {
const data = JSON.parse(event.target.result);
if (confirm('Import will overwrite current data. Continue?')) {
this.history.items = data.history || [];
this.history.favorites = data.favorites || [];
this.history.templateStats = data.templateStats || {};
this.history.storage.save(CONFIG.STORAGE_KEYS.HISTORY, this.history.items);
this.history.storage.save(CONFIG.STORAGE_KEYS.FAVORITES, this.history.favorites);
this.history.storage.save(CONFIG.STORAGE_KEYS.TEMPLATE_STATS, this.history.templateStats);
Object.keys(data.settings || {}).forEach(key => {
this.settings.set(key, data.settings[key]);
});
Utils.showToast('Data imported successfully!', 'success');
this.showSettings();
}
} catch (err) {
Utils.showToast('Invalid backup file', 'error');
console.error('Import error:', err);
}
};
reader.readAsText(file);
};
input.click();
});
document.getElementById('lpb-clear-all')?.addEventListener('click', () => {
if (confirm('Are you sure you want to clear ALL history and data? This cannot be undone!')) {
this.history.items = [];
this.history.favorites = [];
this.history.templateStats = {};
this.history.storage.save(CONFIG.STORAGE_KEYS.HISTORY, []);
this.history.storage.save(CONFIG.STORAGE_KEYS.FAVORITES, []);
this.history.storage.save(CONFIG.STORAGE_KEYS.TEMPLATE_STATS, {});
Utils.showToast('All data cleared', 'success');
this.showSettings();
}
});
}
getMostUsedTemplateName() {
const mostUsed = this.history.getMostUsed(1);
if (mostUsed.length === 0) return 'None';
const [key, count] = mostUsed[0];
const [category, template] = key.split(':');
return `${template} (${count} uses)`;
}
showCategories() {
const content = document.getElementById('lpb-main-content');
const html = Object.entries(CATEGORIES).map(([key, cat]) => `
<div class="lpb-category-card" data-category="${key}">
<div class="lpb-category-name">${cat.name}</div>
<div class="lpb-category-desc">${cat.description}</div>
</div>
`).join('');
content.innerHTML = `<div class="lpb-category-grid">${html}</div>`;
content.querySelectorAll('.lpb-category-card').forEach(card => {
card.addEventListener('click', () => {
this.showTemplates(card.dataset.category);
});
});
}
showTemplates(categoryKey) {
const content = document.getElementById('lpb-main-content');
const category = CATEGORIES[categoryKey];
this.currentCategory = categoryKey;
const html = Object.entries(category.templates).map(([key, template]) => {
const usageCount = this.history.getUsageCount(categoryKey, key);
return `
<div class="lpb-template-card" data-template="${key}">
${usageCount > 0 ? `<div class="lpb-usage-badge">${usageCount} uses</div>` : ''}
<div class="lpb-template-name">${template.name}</div>
</div>
`;
}).join('');
content.innerHTML = `
<button class="lpb-back-btn" id="lpb-back-to-categories">← Back to Categories</button>
<div class="lpb-template-grid">${html}</div>
`;
document.getElementById('lpb-back-to-categories').addEventListener('click', () => {
this.currentCategory = null;
this.showCategories();
});
content.querySelectorAll('.lpb-template-card').forEach(card => {
card.addEventListener('click', () => {
this.openTemplate(categoryKey, card.dataset.template);
});
});
}
openTemplate(categoryKey, templateKey) {
const template = CATEGORIES[categoryKey].templates[templateKey];
this.currentTemplate = { categoryKey, templateKey, ...template };
this.currentData = {};
this.validationErrors = [];
document.getElementById('lpb-form-title').textContent = template.name;
this.renderForm(template.fields);
this.hideModal('main');
this.showModal('form');
this.updatePreview();
}
renderForm(fields) {
const container = document.getElementById('lpb-form-content');
container.innerHTML = '';
fields.forEach(field => {
const fieldEl = this.createField(field);
container.appendChild(fieldEl);
});
const preview = document.createElement('div');
preview.className = 'lpb-preview';
preview.innerHTML = `
<div class="lpb-preview-header">
<div class="lpb-preview-title">Generated Prompt</div>
<div class="lpb-preview-stats">
<div class="lpb-char-count" id="lpb-char-count"></div>
<button class="lpb-btn lpb-btn-small" id="lpb-copy-btn">Copy</button>
</div>
</div>
<div class="lpb-preview-content" id="lpb-preview-text">Fill out the form to generate prompt...</div>
`;
container.appendChild(preview);
const actions = document.createElement('div');
actions.className = 'lpb-actions';
actions.innerHTML = `
<button class="lpb-btn lpb-btn-secondary" id="lpb-back-to-templates">Back</button>
<button class="lpb-btn" id="lpb-insert-btn">Insert Prompt</button>
`;
container.appendChild(actions);
container.querySelectorAll('input, select, textarea').forEach(input => {
input.addEventListener('input', () => this.updatePreview());
input.addEventListener('change', () => {
this.updateConditionals();
this.updatePreview();
});
if (input.tagName === 'TEXTAREA' || (input.tagName === 'INPUT' && input.type === 'text')) {
this.fileAutocomplete.attach(input);
}
});
document.getElementById('lpb-back-to-templates').addEventListener('click', () => {
this.hideModal('form');
this.showModal('main');
this.showTemplates(this.currentTemplate.categoryKey);
});
document.getElementById('lpb-insert-btn').addEventListener('click', () => {
this.insertPrompt();
});
document.getElementById('lpb-copy-btn').addEventListener('click', () => {
const text = document.getElementById('lpb-preview-text').textContent;
navigator.clipboard.writeText(text);
Utils.showToast('Copied to clipboard', 'success');
});
this.updateConditionals();
}
createField(field) {
const wrapper = document.createElement('div');
wrapper.className = 'lpb-form-field';
wrapper.dataset.fieldId = field.id;
if (field.show_if) {
wrapper.dataset.showIf = field.show_if.field;
wrapper.dataset.showValue = field.show_if.value;
}
const label = document.createElement('label');
label.className = 'lpb-label';
label.innerHTML = Utils.escapeHtml(field.label) + (field.required ? '<span class="lpb-required">*</span>' : '');
wrapper.appendChild(label);
let input;
if (field.type === 'text' || field.type === 'number') {
input = document.createElement('input');
input.type = field.type;
input.className = 'lpb-input';
input.value = field.default || '';
input.placeholder = field.placeholder || '';
input.dataset.field = field.id;
input.dataset.required = field.required || false;
} else if (field.type === 'textarea') {
input = document.createElement('textarea');
input.className = 'lpb-textarea';
input.value = field.default || '';
input.placeholder = field.placeholder || '';
input.dataset.field = field.id;
input.dataset.required = field.required || false;
} else if (field.type === 'select') {
input = document.createElement('select');
input.className = 'lpb-select';
input.dataset.field = field.id;
input.dataset.required = field.required || false;
field.options.forEach(opt => {
const option = document.createElement('option');
option.value = opt;
option.textContent = opt;
if (opt === field.default) option.selected = true;
input.appendChild(option);
});
} else if (field.type === 'radio') {
input = document.createElement('div');
input.className = 'lpb-radio-group';
input.dataset.field = field.id;
input.dataset.required = field.required || false;
field.options.forEach((opt, i) => {
const item = document.createElement('div');
item.className = 'lpb-radio-item';
const radio = document.createElement('input');
radio.type = 'radio';
radio.name = field.id;
radio.value = opt;
radio.id = `${field.id}_${i}`;
if (opt === field.default) radio.checked = true;
const lbl = document.createElement('label');
lbl.htmlFor = radio.id;
lbl.textContent = opt;
item.appendChild(radio);
item.appendChild(lbl);
input.appendChild(item);
});
} else if (field.type === 'checkboxes') {
input = document.createElement('div');
input.className = 'lpb-checkbox-group';
input.dataset.field = field.id;
input.dataset.required = field.required || false;
field.options.forEach((opt, i) => {
const item = document.createElement('div');
item.className = 'lpb-checkbox-item';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = opt;
checkbox.id = `${field.id}_${i}`;
checkbox.dataset.parent = field.id;
const lbl = document.createElement('label');
lbl.htmlFor = checkbox.id;
lbl.textContent = opt;
item.appendChild(checkbox);
item.appendChild(lbl);
input.appendChild(item);
});
} else if (field.type === 'list') {
input = this.createListField(field.id, field.placeholder);
input.dataset.required = field.required || false;
}
wrapper.appendChild(input);
if (field.help) {
const help = document.createElement('div');
help.className = 'lpb-help-text';
help.textContent = field.help;
wrapper.appendChild(help);
}
return wrapper;
}
createListField(fieldId, placeholder) {
const wrapper = document.createElement('div');
wrapper.className = 'lpb-list-wrapper';
wrapper.dataset.field = fieldId;
const addRow = () => {
const row = document.createElement('div');
row.className = 'lpb-list-row';
const input = document.createElement('input');
input.type = 'text';
input.className = 'lpb-list-input';
input.placeholder = placeholder || '';
input.addEventListener('input', () => this.updatePreview());
this.fileAutocomplete.attach(input);
const removeBtn = document.createElement('button');
removeBtn.className = 'lpb-list-remove';
removeBtn.textContent = 'Remove';
removeBtn.onclick = () => {
row.remove();
this.updatePreview();
};
row.appendChild(input);
row.appendChild(removeBtn);
wrapper.insertBefore(row, addBtn);
};
const addBtn = document.createElement('button');
addBtn.className = 'lpb-list-add';
addBtn.textContent = 'Add Item';
addBtn.onclick = addRow;
wrapper.appendChild(addBtn);
addRow();
return wrapper;
}
updateConditionals() {
document.querySelectorAll('[data-show-if]').forEach(field => {
const targetField = field.dataset.showIf;
const targetValue = field.dataset.showValue;
const control = document.querySelector(`[data-field="${targetField}"]`);
let currentValue;
if (control.tagName === 'SELECT') {
currentValue = control.value;
} else if (control.classList.contains('lpb-radio-group')) {
const checked = control.querySelector('input:checked');
currentValue = checked ? checked.value : '';
}
if (currentValue === targetValue) {
field.classList.remove('lpb-field-hidden');
} else {
field.classList.add('lpb-field-hidden');
}
});
}
collectData() {
const data = {};
document.querySelectorAll('[data-field]').forEach(field => {
const fieldId = field.dataset.field;
if (field.tagName === 'INPUT' || field.tagName === 'TEXTAREA' || field.tagName === 'SELECT') {
data[fieldId] = field.value;
} else if (field.classList.contains('lpb-radio-group')) {
const checked = field.querySelector('input:checked');
data[fieldId] = checked ? checked.value : '';
} else if (field.classList.contains('lpb-checkbox-group')) {
const checked = Array.from(field.querySelectorAll('input:checked'));
data[fieldId] = checked.map(c => c.value);
} else if (field.classList.contains('lpb-list-wrapper')) {
const items = Array.from(field.querySelectorAll('.lpb-list-input'))
.map(i => i.value.trim())
.filter(v => v);
data[fieldId] = items;
}
});
return data;
}
validateForm(fields) {
const data = this.collectData();
const errors = [];
document.querySelectorAll('.lpb-input, .lpb-select, .lpb-textarea').forEach(el => {
el.classList.remove('lpb-error');
});
fields.forEach(field => {
const fieldEl = document.querySelector(`[data-field-id="${field.id}"]`);
if (fieldEl && fieldEl.classList.contains('lpb-field-hidden')) {
return;
}
if (field.required) {
const value = data[field.id];
let isEmpty = false;
if (Array.isArray(value)) {
isEmpty = value.length === 0;
} else if (typeof value === 'string') {
isEmpty = value.trim() === '';
} else {
isEmpty = !value;
}
if (isEmpty) {
errors.push(field.label);
const input = document.querySelector(`[data-field="${field.id}"]`);
if (input) input.classList.add('lpb-error');
}
}
});
if (errors.length > 0) {
Utils.showToast(`Please fill required fields: ${errors.join(', ')}`, 'error', 5000);
return false;
}
return true;
}
updatePreview() {
clearTimeout(this.previewDebounceTimer);
this.previewDebounceTimer = setTimeout(() => {
this.currentData = this.collectData();
const prompt = this.currentTemplate.generate(this.currentData);
const previewEl = document.getElementById('lpb-preview-text');
if (previewEl) {
previewEl.textContent = prompt;
const charCount = prompt.length;
const wordCount = prompt.split(/\s+/).filter(Boolean).length;
const countEl = document.getElementById('lpb-char-count');
if (countEl) {
countEl.textContent = `${charCount} chars • ${wordCount} words`;
}
}
}, CONFIG.PREVIEW_DEBOUNCE);
}
insertPrompt() {
if (!this.validateForm(this.currentTemplate.fields)) {
return;
}
const prompt = document.getElementById('lpb-preview-text').textContent;
this.history.add(
prompt,
this.currentTemplate.categoryKey,
this.currentTemplate.name,
this.currentData
);
this.insertPromptText(prompt);
}
insertPromptText(prompt) {
const input = this.findInput();
if (!input) {
Utils.showToast('Could not find input field', 'error');
navigator.clipboard.writeText(prompt);
Utils.showToast('Copied to clipboard instead', 'info');
return;
}
try {
input.click();
input.focus();
setTimeout(() => {
try {
const nativeTextareaSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
const nativeInputSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
if (input.tagName === 'TEXTAREA' && nativeTextareaSetter) {
nativeTextareaSetter.call(input, prompt);
} else if (input.tagName === 'INPUT' && nativeInputSetter) {
nativeInputSetter.call(input, prompt);
} else {
input.value = prompt;
}
const events = ['input', 'change', 'keyup', 'keydown', 'keypress'];
events.forEach(eventType => {
const event = new Event(eventType, { bubbles: true, cancelable: true });
input.dispatchEvent(event);
});
const inputEvent = new InputEvent('input', {
bubbles: true,
cancelable: true,
data: prompt
});
input.dispatchEvent(inputEvent);
setTimeout(() => {
if (input.setSelectionRange) {
input.setSelectionRange(prompt.length, prompt.length);
}
input.focus();
if (this.settings.get('autoSubmit')) {
setTimeout(() => {
this.autoSubmitPrompt();
}, this.settings.get('autoSubmitDelay'));
}
}, 50);
this.hideModal('form');
this.hideModal('main');
Utils.showToast('Prompt inserted successfully', 'success');
} catch (e) {
console.error('Insert error:', e);
Utils.showToast('Error inserting prompt - copying to clipboard', 'error');
navigator.clipboard.writeText(prompt);
}
}, 100);
} catch (error) {
console.error('Failed to insert prompt:', error);
Utils.showToast('Failed to insert. Copied to clipboard.', 'error');
navigator.clipboard.writeText(prompt);
}
}
autoSubmitPrompt() {
const submitBtn = this.findSubmitButton();
if (submitBtn) {
submitBtn.click();
Utils.showToast('Prompt auto-submitted!', 'success');
} else {
Utils.log('Submit button not found');
}
}
findSubmitButton() {
const selectors = [
'button[type="submit"]',
'button[class*="send" i]',
'button[class*="submit" i]',
'button[aria-label*="send" i]',
'button:has(svg[class*="send"])'
];
for (const selector of selectors) {
const btn = document.querySelector(selector);
if (btn && btn.offsetParent !== null) {
return btn;
}
}
const buttons = Array.from(document.querySelectorAll('button'));
return buttons.find(btn =>
btn.textContent.toLowerCase().includes('send') ||
btn.textContent.toLowerCase().includes('submit') ||
btn.innerHTML.includes('send')
);
}
findInput() {
const selectors = [
'textarea[placeholder*="prompt" i]',
'textarea[placeholder*="message" i]',
'textarea[placeholder*="type" i]',
'textarea',
'input[type="text"]',
'[contenteditable="true"]'
];
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element && element.offsetParent !== null) {
return element;
}
}
return null;
}
showHistory() {
const content = document.getElementById('lpb-main-content');
const searchHTML = `
<div class="lpb-search-wrapper">
<input type="text" id="lpb-history-search" class="lpb-input lpb-search-input"
placeholder="Search history..." />
</div>
<div id="lpb-history-results"></div>
`;
content.innerHTML = searchHTML;
const renderResults = (items) => {
const resultsDiv = document.getElementById('lpb-history-results');
if (!resultsDiv) return;
if (items.length === 0) {
resultsDiv.innerHTML = '<div class="lpb-empty">No items found</div>';
return;
}
this.renderHistoryItems(resultsDiv, items);
};
renderResults(this.history.getAll());
document.getElementById('lpb-history-search')?.addEventListener('input', (e) => {
const query = e.target.value.toLowerCase();
const filtered = this.history.getAll().filter(item =>
item.prompt.toLowerCase().includes(query) ||
item.template.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query) ||
(item.customName && item.customName.toLowerCase().includes(query))
);
renderResults(filtered);
});
}
showFavorites() {
const content = document.getElementById('lpb-main-content');
const items = this.history.getFavorites();
if (items.length === 0) {
content.innerHTML = '<div class="lpb-empty">No favorites yet</div>';
return;
}
const container = document.createElement('div');
this.renderHistoryItems(container, items);
content.innerHTML = '';
content.appendChild(container);
}
renderHistoryItems(container, items) {
const html = items.map(item => {
const isFav = this.history.isFavorite(item.id);
const date = new Date(item.timestamp).toLocaleString();
const displayName = item.customName || item.template;
const truncatedPrompt = item.prompt.substring(0, 100);
const needsTruncation = item.prompt.length > 100;
return `
<div class="lpb-history-item">
<div class="lpb-history-top">
<div class="lpb-history-info">
${item.customName
? `<span class="lpb-custom-name">${Utils.escapeHtml(item.customName)}</span>`
: `<span class="lpb-badge">${Utils.escapeHtml(item.template)}</span>`
}
<span class="lpb-timestamp">${date}</span>
</div>
<button class="lpb-fav-btn" data-id="${item.id}" data-action="fav">${isFav ? '★' : '☆'}</button>
</div>
<div class="lpb-history-preview" data-id="${item.id}" data-action="expand">
<div class="lpb-history-preview-text truncated" data-full="${Utils.escapeHtml(item.prompt)}">
${Utils.escapeHtml(truncatedPrompt)}${needsTruncation ? '...' : ''}
</div>
${needsTruncation ? '<span class="lpb-expand-icon">▼</span>' : ''}
</div>
<div class="lpb-history-actions">
<button class="lpb-btn lpb-btn-small" data-id="${item.id}" data-action="use">Use</button>
<button class="lpb-btn lpb-btn-small lpb-btn-secondary" data-id="${item.id}" data-action="edit">Rename</button>
<button class="lpb-btn lpb-btn-small lpb-btn-secondary" data-id="${item.id}" data-action="copy">Copy</button>
<button class="lpb-btn lpb-btn-small lpb-btn-secondary" data-id="${item.id}" data-action="delete">Delete</button>
</div>
</div>
`;
}).join('');
container.innerHTML = html;
// Handle expand/collapse
container.querySelectorAll('.lpb-history-preview').forEach(preview => {
preview.addEventListener('click', (e) => {
const textEl = preview.querySelector('.lpb-history-preview-text');
const iconEl = preview.querySelector('.lpb-expand-icon');
if (!textEl || !iconEl) return;
const isExpanded = textEl.classList.contains('expanded');
if (isExpanded) {
// Collapse
const truncated = textEl.dataset.full.substring(0, 100);
textEl.textContent = truncated + '...';
textEl.classList.remove('expanded');
textEl.classList.add('truncated');
iconEl.textContent = '▼';
} else {
// Expand
textEl.textContent = textEl.dataset.full;
textEl.classList.remove('truncated');
textEl.classList.add('expanded');
iconEl.textContent = '▲';
}
});
});
// Handle other actions
container.querySelectorAll('[data-action]:not([data-action="expand"])').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation(); // Prevent triggering expand when clicking action buttons
const id = e.target.dataset.id;
const action = e.target.dataset.action;
this.handleHistoryAction(id, action);
});
});
}
handleHistoryAction(id, action) {
const item = this.history.items.find(i => i.id === id);
if (!item) return;
if (action === 'use') {
this.insertPromptText(item.prompt);
} else if (action === 'copy') {
navigator.clipboard.writeText(item.prompt);
Utils.showToast('Copied to clipboard', 'success');
} else if (action === 'delete') {
if (confirm('Delete this prompt from history?')) {
this.history.remove(id);
const activeTab = document.querySelector('.lpb-tab.active').dataset.tab;
if (activeTab === 'history') this.showHistory();
else if (activeTab === 'favorites') this.showFavorites();
Utils.showToast('Prompt deleted', 'info');
}
} else if (action === 'fav') {
this.history.toggleFavorite(id);
const activeTab = document.querySelector('.lpb-tab.active').dataset.tab;
if (activeTab === 'history') this.showHistory();
else if (activeTab === 'favorites') this.showFavorites();
} else if (action === 'edit') {
this.showEditModal(id, item.customName || item.template);
}
}
showEditModal(id, currentName) {
const modal = document.getElementById('lpb-edit-modal');
const input = document.getElementById('lpb-edit-name-input');
input.value = currentName;
modal.classList.add('active');
input.focus();
input.select();
const saveBtn = document.getElementById('lpb-edit-save');
const newSaveBtn = saveBtn.cloneNode(true);
saveBtn.parentNode.replaceChild(newSaveBtn, saveBtn);
newSaveBtn.addEventListener('click', () => {
const newName = input.value.trim();
if (newName) {
this.history.rename(id, newName);
modal.classList.remove('active');
const activeTab = document.querySelector('.lpb-tab.active').dataset.tab;
if (activeTab === 'history') this.showHistory();
else if (activeTab === 'favorites') this.showFavorites();
Utils.showToast('Prompt renamed', 'success');
}
});
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
newSaveBtn.click();
}
});
}
}
Utils.log('Initializing Lemonade Prompt Builder v8.8.3.1');
new UI();
Utils.log('Prompt Builder ready! Press Ctrl+Shift+P to open, type @ to mention files');
})();