Complete prompt builder for Lemonade AI with advanced features
当前为
// ==UserScript==
// @name Lemonade Prompt Builder
// @namespace http://tampermonkey.net/
// @version 8.5
// @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 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'
}
};
const 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.`
}
}
}
};
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;
}
@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 FileScanner {
constructor() {
this.files = [];
this.scanInterval = null;
}
start() {
this.scan();
this.scanInterval = setInterval(() => this.scan(), 2000);
}
stop() {
if (this.scanInterval) {
clearInterval(this.scanInterval);
}
}
scan() {
const files = [];
const selectors = [
'[data-file-name]',
'[class*="file"]',
'div.flex.items-center.gap-2.px-3.py-2.cursor-pointer',
'button[class*="file"]',
'div[role="button"]'
];
const potentialFiles = new Set();
selectors.forEach(selector => {
document.querySelectorAll(selector).forEach(el => {
const text = el.textContent.trim();
const fileName = el.getAttribute('data-file-name') || text;
if (fileName &&
fileName.length > 0 &&
fileName.length < 100 &&
!fileName.includes('\n') &&
(fileName.includes('.') || fileName.match(/^[A-Z][a-z]+$/))) {
const isScript = fileName.endsWith('.lua') ||
fileName.endsWith('.luau') ||
fileName.match(/Script$/i) ||
fileName.match(/^[A-Z][a-z]+$/);
if (isScript) {
let path = '';
let current = el.parentElement;
const pathParts = [];
for (let i = 0; i < 5 && current; i++) {
const pathText = current.textContent;
if (pathText && pathText !== text && pathText.length < 50) {
const match = pathText.match(/([A-Z][a-z]+(?:Service|Storage|Scripts)?)/);
if (match && !pathParts.includes(match[1])) {
pathParts.unshift(match[1]);
}
}
current = current.parentElement;
}
path = pathParts.join(' > ');
potentialFiles.add(JSON.stringify({
name: fileName,
path: path,
fullPath: path ? `${path} > ${fileName}` : fileName
}));
}
}
});
});
this.files = Array.from(potentialFiles).map(f => JSON.parse(f));
}
getFiles() {
return this.files;
}
search(query) {
if (!query) return this.files;
const lowerQuery = query.toLowerCase();
return this.files.filter(f =>
f.name.toLowerCase().includes(lowerQuery) ||
f.path.toLowerCase().includes(lowerQuery)
);
}
}
class FileAutocomplete {
constructor(fileScanner) {
this.fileScanner = fileScanner;
this.activeInput = null;
this.dropdown = null;
this.selectedIndex = 0;
this.currentFiles = [];
this.atPosition = -1;
this.createDropdown();
}
createDropdown() {
this.dropdown = document.createElement('div');
this.dropdown.className = 'lpb-file-autocomplete';
document.body.appendChild(this.dropdown);
}
attach(input) {
input.addEventListener('input', (e) => this.handleInput(e));
input.addEventListener('keydown', (e) => this.handleKeydown(e));
input.addEventListener('blur', () => {
setTimeout(() => this.hide(), 200);
});
}
handleInput(e) {
const input = e.target;
const value = input.value;
const cursorPos = input.selectionStart;
const atIndex = value.lastIndexOf('@', cursorPos - 1);
if (atIndex !== -1) {
const afterAt = value.substring(atIndex + 1, cursorPos);
const spaceAfterAt = afterAt.indexOf(' ');
if (spaceAfterAt === -1 || spaceAfterAt > afterAt.length) {
this.atPosition = atIndex;
this.show(input, afterAt);
return;
}
}
this.hide();
}
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') {
this.hide();
}
}
show(input, query) {
this.activeInput = input;
this.currentFiles = this.fileScanner.search(query);
this.selectedIndex = 0;
if (this.currentFiles.length === 0) {
this.hide();
return;
}
this.dropdown.innerHTML = this.currentFiles.map((file, index) => `
<div class="lpb-file-item ${index === 0 ? 'selected' : ''}" data-index="${index}">
<div class="lpb-file-icon">📄</div>
<div class="lpb-file-name">${Utils.escapeHtml(file.name)}</div>
${file.path ? `<div class="lpb-file-path">${Utils.escapeHtml(file.path)}</div>` : ''}
</div>
`).join('');
this.dropdown.querySelectorAll('.lpb-file-item').forEach(item => {
item.addEventListener('click', () => {
const index = parseInt(item.dataset.index);
this.selectFile(this.currentFiles[index]);
});
});
const rect = input.getBoundingClientRect();
const atPos = this.getCaretCoordinates(input);
this.dropdown.style.left = `${rect.left + atPos.left}px`;
this.dropdown.style.top = `${rect.top + atPos.top + 20}px`;
this.dropdown.classList.add('active');
}
hide() {
this.dropdown.classList.remove('active');
this.activeInput = null;
this.atPosition = -1;
}
updateSelection() {
this.dropdown.querySelectorAll('.lpb-file-item').forEach((item, index) => {
item.classList.toggle('selected', index === this.selectedIndex);
});
const selected = this.dropdown.querySelector('.lpb-file-item.selected');
if (selected) {
selected.scrollIntoView({ block: 'nearest' });
}
}
selectFile(file) {
if (!this.activeInput || this.atPosition === -1) return;
const input = this.activeInput;
const value = input.value;
const cursorPos = input.selectionStart;
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;
input.value = newValue;
input.setSelectionRange(newCursorPos, newCursorPos);
const event = new Event('input', { bubbles: true });
input.dispatchEvent(event);
this.hide();
}
getCaretCoordinates(element) {
const properties = [
'direction', 'boxSizing', 'width', 'height', 'overflowX',
'overflowY', 'borderTopWidth', 'borderRightWidth',
'borderBottomWidth', 'borderLeftWidth', 'paddingTop',
'paddingRight', 'paddingBottom', 'paddingLeft',
'fontStyle', 'fontVariant', 'fontWeight', 'fontStretch',
'fontSize', 'fontSizeAdjust', 'lineHeight', 'fontFamily',
'textAlign', 'textTransform', 'textIndent', 'textDecoration',
'letterSpacing', 'wordSpacing'
];
const div = document.createElement('div');
const style = getComputedStyle(element);
div.style.position = 'absolute';
div.style.visibility = 'hidden';
div.style.whiteSpace = 'pre-wrap';
div.style.wordWrap = 'break-word';
properties.forEach(prop => {
div.style[prop] = style[prop];
});
div.textContent = element.value.substring(0, element.selectionStart);
const span = document.createElement('span');
span.textContent = element.value.substring(element.selectionStart) || '.';
div.appendChild(span);
document.body.appendChild(div);
const coordinates = {
top: span.offsetTop,
left: span.offsetLeft
};
document.body.removeChild(div);
return coordinates;
}
}
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.fileScanner = new FileScanner();
this.fileAutocomplete = new FileAutocomplete(this.fileScanner);
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();
this.fileScanner.start();
Utils.showToast('File scanner active - type @ in text fields to reference scripts', '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;
this.hideModal(modal);
if (modal === 'main') {
this.saveCurrentView();
}
});
});
document.querySelectorAll('.lpb-modal').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.remove('active');
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') {
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');
}
showSettings() {
const content = document.getElementById('lpb-main-content');
const fileCount = this.fileScanner.getFiles().length;
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 (${fileCount} files detected)</p>
</div>
<button class="lpb-btn lpb-btn-small" id="lpb-refresh-files">Refresh</button>
</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>
<p style="margin: 8px 0;"> Files available: <strong style="color: #2196F3;">${fileCount}</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.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
</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-refresh-files')?.addEventListener('click', () => {
this.fileScanner.scan();
Utils.showToast(`Refreshed - ${this.fileScanner.getFiles().length} files found`, 'success');
this.showSettings();
});
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.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();
}
});
}
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.5');
new UI();
Utils.log('Prompt Builder ready! Press Ctrl+Shift+P to open, type @ to mention files');
})();