Skip to content

Instantly share code, notes, and snippets.

@hsingh23
Created August 6, 2025 05:48
Show Gist options
  • Save hsingh23/13ff99906f3b384005701116968fcd17 to your computer and use it in GitHub Desktop.
Save hsingh23/13ff99906f3b384005701116968fcd17 to your computer and use it in GitHub Desktop.
Higgsfield Playlist Automation tampermonkey
// ==UserScript==
// @name Higgsfield Playlist Automation
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description Automated playlist execution for Higgsfield.ai with throttling and queue management
// @author Auto-Generated
// @match https://higgsfield.ai/create/video*
// @require https://unpkg.com/[email protected]/dist/dexie.js
// @icon https://www.google.com/s2/favicons?sz=64&domain=higgsfield.ai
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// Configuration
const CONFIG = {
THROTTLE_MS: 2000,
MAX_IN_FLIGHT: 4,
MAX_RETRIES: 5,
DB_NAME: 'hfPromptDB',
DB_VERSION: 2,
DEBUG: true,
STORAGE_BROADCAST_KEY: 'hf_runner_active',
UI_Z_INDEX: 2147483647,
ENHANCEMENT_PREFIX: 'photorealistic, shot on Arri Alexa, 8k: ',
HASH_TIMEOUT_HOURS: 12
};
// Debug logging
const debug = (...args) => {
if (CONFIG.DEBUG) {
console.debug('[HF-Playlist]', ...args);
}
};
// Hash utility for duplicate detection
async function generatePromptHash(prompt) {
const encoder = new TextEncoder();
const data = encoder.encode(prompt.toLowerCase().trim());
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
// Check if prompt was recently submitted
async function isPromptDuplicate(prompt) {
try {
// Skip check if database isn't available
if (!db || !db.prompt_hashes) {
return false;
}
const hash = await generatePromptHash(prompt);
const cutoffTime = Date.now() - (CONFIG.HASH_TIMEOUT_HOURS * 60 * 60 * 1000);
// Clean up old hashes (with limit to prevent blocking)
await db.prompt_hashes.where('timestamp').below(cutoffTime).limit(100).delete();
// Check if hash exists
const existing = await db.prompt_hashes.where('hash').equals(hash).first();
return existing !== undefined;
} catch (error) {
debug('Error checking prompt duplicate:', error);
return false; // If error, allow submission to be safe
}
}
// Store prompt hash
async function storePromptHash(prompt) {
try {
const hash = await generatePromptHash(prompt);
await db.prompt_hashes.add({
hash,
timestamp: Date.now()
});
debug('Stored prompt hash:', hash.substring(0, 8) + '...');
} catch (error) {
debug('Error storing prompt hash:', error);
}
}
// Database setup
let db;
async function initDatabase() {
try {
db = new Dexie(CONFIG.DB_NAME);
// Version 1 - Original schema
db.version(1).stores({
playlists: '++id, name, prompts, lastIndexSent, created, updated',
corrupt_playlists: '++id, name, data, error, created'
});
// Version 2 - Added prompt hashes
db.version(2).stores({
playlists: '++id, name, prompts, lastIndexSent, created, updated',
corrupt_playlists: '++id, name, data, error, created',
prompt_hashes: '++id, hash, timestamp'
});
await db.open();
debug('Database initialized, version:', db.verno);
// Clean up old hashes on startup
const cutoffTime = Date.now() - (CONFIG.HASH_TIMEOUT_HOURS * 60 * 60 * 1000);
await db.prompt_hashes?.where('timestamp').below(cutoffTime).delete();
} catch (error) {
debug('Database error:', error);
showToast('Database initialization failed: ' + error.message, 'error');
throw error;
}
}
// Playlist management
class PlaylistManager {
constructor() {
this.activePlaylists = new Map(); // id -> playlist data
this.runningState = 'stopped'; // 'running', 'paused', 'stopped'
this.inFlightPrompts = new Map();
this.retryQueue = new Map();
this.dispatchTimer = null;
this.lastDispatchTime = 0;
this.roundRobinIndex = 0;
}
async createPlaylist(name, promptsText) {
try {
const prompts = promptsText.split('~').map(p => p.trim()).filter(p => p);
const uniqueName = await this.generateUniqueName(name);
const playlist = {
name: uniqueName,
prompts,
lastIndexSent: 0,
created: new Date(),
updated: new Date()
};
const id = await db.playlists.add(playlist);
debug('Created playlist:', uniqueName, 'with', prompts.length, 'prompts');
return { ...playlist, id };
} catch (error) {
debug('Error creating playlist:', error);
throw error;
}
}
async generateUniqueName(baseName) {
const existing = await db.playlists.where('name').startsWithIgnoreCase(baseName).toArray();
if (!existing.some(p => p.name === baseName)) {
return baseName;
}
let counter = 2;
let uniqueName;
do {
uniqueName = `${baseName} (${counter})`;
counter++;
} while (existing.some(p => p.name === uniqueName));
return uniqueName;
}
async getAllPlaylists() {
try {
return await db.playlists.orderBy('updated').reverse().toArray();
} catch (error) {
debug('Error fetching playlists:', error);
return [];
}
}
async updatePlaylist(id, updates) {
try {
await db.playlists.update(id, { ...updates, updated: new Date() });
debug('Updated playlist:', id);
// If this is an active playlist, refresh its data
if (this.activePlaylists.has(id)) {
const updatedPlaylist = await db.playlists.get(id);
if (updatedPlaylist) {
this.activePlaylists.set(id, updatedPlaylist);
debug('Refreshed active playlist data:', id);
}
}
} catch (error) {
debug('Error updating playlist:', error);
throw error;
}
}
async deletePlaylist(id) {
try {
await db.playlists.delete(id);
debug('Deleted playlist:', id);
} catch (error) {
debug('Error deleting playlist:', error);
throw error;
}
}
async startPlaylist(playlistId) {
// Check for other tabs running
if (localStorage.getItem(CONFIG.STORAGE_BROADCAST_KEY) && this.runningState === 'stopped') {
showToast('Playlist runner active in another tab', 'warning');
return false;
}
try {
const playlist = await db.playlists.get(playlistId);
if (!playlist || playlist.prompts.length === 0) {
showToast('Playlist is empty or not found', 'error');
return false;
}
// Check if already running
if (this.activePlaylists.has(playlistId)) {
showToast('Playlist is already running', 'warning');
return false;
}
// Add to active playlists
this.activePlaylists.set(playlistId, playlist);
// Start the system if not already running
if (this.runningState === 'stopped') {
this.runningState = 'running';
localStorage.setItem(CONFIG.STORAGE_BROADCAST_KEY, 'true');
this.startDispatchLoop();
}
showToast(`Started playlist: ${playlist.name}`, 'success');
return true;
} catch (error) {
debug('Error starting playlist:', error);
showToast('Failed to start playlist', 'error');
return false;
}
}
pausePlaylist() {
if (this.runningState === 'running') {
this.runningState = 'paused';
this.stopDispatchLoop();
showToast('All playlists paused', 'info');
}
}
stopPlaylist(playlistId = null) {
if (playlistId && this.activePlaylists.has(playlistId)) {
// Stop specific playlist
const playlist = this.activePlaylists.get(playlistId);
this.activePlaylists.delete(playlistId);
// Clean up related in-flight and retry entries
for (const [key, data] of this.inFlightPrompts.entries()) {
if (key.toString().startsWith(`${playlistId}-`)) {
this.inFlightPrompts.delete(key);
}
}
for (const [key, data] of this.retryQueue.entries()) {
if (key.toString().startsWith(`${playlistId}-`)) {
this.retryQueue.delete(key);
}
}
showToast(`Stopped playlist: ${playlist.name}`, 'info');
// If no more active playlists, stop the system
if (this.activePlaylists.size === 0) {
this.stopAllPlaylists();
}
} else {
// Stop all playlists
this.stopAllPlaylists();
}
}
stopAllPlaylists() {
this.runningState = 'stopped';
this.activePlaylists.clear();
this.inFlightPrompts.clear();
this.retryQueue.clear();
this.stopDispatchLoop();
this.roundRobinIndex = 0;
localStorage.removeItem(CONFIG.STORAGE_BROADCAST_KEY);
showToast('All playlists stopped', 'info');
}
resetProgress(playlistId = null) {
if (playlistId && this.activePlaylists.has(playlistId)) {
const playlist = this.activePlaylists.get(playlistId);
playlist.lastIndexSent = 0;
this.updatePlaylist(playlistId, { lastIndexSent: 0 });
// Clean up related entries
for (const [key, data] of this.inFlightPrompts.entries()) {
if (key.toString().startsWith(`${playlistId}-`)) {
this.inFlightPrompts.delete(key);
}
}
for (const [key, data] of this.retryQueue.entries()) {
if (key.toString().startsWith(`${playlistId}-`)) {
this.retryQueue.delete(key);
}
}
showToast(`Progress reset for: ${playlist.name}`, 'info');
} else {
// Reset all active playlists
for (const [id, playlist] of this.activePlaylists.entries()) {
playlist.lastIndexSent = 0;
this.updatePlaylist(id, { lastIndexSent: 0 });
}
this.inFlightPrompts.clear();
this.retryQueue.clear();
showToast('Progress reset for all playlists', 'info');
}
}
startDispatchLoop() {
if (this.dispatchTimer) return;
const loop = () => {
if (this.runningState === 'running') {
this.tryDispatchNext();
this.dispatchTimer = setTimeout(loop, 100);
}
};
this.dispatchTimer = setTimeout(loop, 100);
}
stopDispatchLoop() {
if (this.dispatchTimer) {
clearTimeout(this.dispatchTimer);
this.dispatchTimer = null;
}
}
async tryDispatchNext() {
if (this.activePlaylists.size === 0 || this.runningState !== 'running') return;
// Check if we can dispatch more
if (this.inFlightPrompts.size >= CONFIG.MAX_IN_FLIGHT) return;
// Throttle check
const now = Date.now();
if (now - this.lastDispatchTime < CONFIG.THROTTLE_MS) return;
// Check Higgsfield UI state
if (!this.canSubmitPrompt()) return;
// Process retry queue first
for (const [promptKey, retryData] of this.retryQueue) {
if (retryData.nextRetry <= now && this.inFlightPrompts.size < CONFIG.MAX_IN_FLIGHT) {
await this.dispatchPrompt(promptKey, retryData.prompt, retryData.attempts + 1, retryData.playlistId);
this.retryQueue.delete(promptKey);
this.lastDispatchTime = now;
return;
}
}
// Round-robin dispatch from active playlists
const activePlaylistIds = Array.from(this.activePlaylists.keys());
if (activePlaylistIds.length === 0) return;
let attempts = 0;
while (attempts < activePlaylistIds.length) {
// Ensure roundRobinIndex is within bounds
this.roundRobinIndex = this.roundRobinIndex % activePlaylistIds.length;
const currentId = activePlaylistIds[this.roundRobinIndex];
const playlist = this.activePlaylists.get(currentId);
if (playlist && playlist.lastIndexSent < playlist.prompts.length) {
const promptIndex = playlist.lastIndexSent;
const prompt = playlist.prompts[promptIndex];
const promptKey = `${currentId}-${promptIndex}`;
await this.dispatchPrompt(promptKey, prompt, 0, currentId);
playlist.lastIndexSent = promptIndex + 1;
await this.updatePlaylist(currentId, { lastIndexSent: promptIndex + 1 });
this.lastDispatchTime = now;
// Move to next playlist for next dispatch
this.roundRobinIndex = (this.roundRobinIndex + 1) % activePlaylistIds.length;
return;
}
// This playlist is done, move to next
this.roundRobinIndex = (this.roundRobinIndex + 1) % activePlaylistIds.length;
attempts++;
}
// Check if all playlists are complete
if (this.inFlightPrompts.size === 0 && this.retryQueue.size === 0) {
const allComplete = Array.from(this.activePlaylists.values()).every(playlist =>
playlist.lastIndexSent >= playlist.prompts.length
);
if (allComplete) {
const completedNames = Array.from(this.activePlaylists.values()).map(p => p.name).join(', ');
this.stopAllPlaylists();
showToast(`All playlists completed: ${completedNames}`, 'success');
}
}
}
canSubmitPrompt() {
try {
const preloadGradients = document.querySelectorAll('.video-media-preload-gradient');
const submitButton = document.querySelector('button[type="submit"]');
const gradientCheck = preloadGradients.length <= 3;
const buttonCheck = submitButton &&
(submitButton.innerText.trim() === 'Generate\nUnlimited' ||
/Generate\s+Unlimited/i.test(submitButton.innerText.trim()));
return gradientCheck && buttonCheck;
} catch (error) {
debug('Error checking UI state:', error);
showToast('Higgsfield layout changed', 'error');
return false;
}
}
async dispatchPrompt(promptKey, prompt, attempts, playlistId) {
debug('Dispatching prompt', promptKey, 'attempt', attempts + 1);
// Check for duplicates
const isDuplicate = await isPromptDuplicate(prompt);
if (isDuplicate) {
debug('Skipping duplicate prompt:', prompt.substring(0, 50) + '...');
showToast('Skipped duplicate prompt', 'info');
// Mark as completed immediately to continue with next prompt
this.handlePromptCompletion(promptKey, true);
return;
}
this.inFlightPrompts.set(promptKey, {
prompt,
attempts,
startTime: Date.now(),
playlistId
});
try {
await this.submitPromptWithEnhancement(prompt);
// Store hash after successful submission
await storePromptHash(prompt);
// Set timeout for completion detection
setTimeout(() => {
if (this.inFlightPrompts.has(promptKey)) {
this.handlePromptCompletion(promptKey, false);
}
}, 30000); // 30 second timeout
} catch (error) {
debug('Error dispatching prompt:', error);
this.handlePromptCompletion(promptKey, false);
}
}
async submitPromptWithEnhancement(promptText) {
// Get the UI manager instance to check enhancement setting
const uiManager = window.hfUIManager;
const shouldEnhance = uiManager && uiManager.enhancePrompts;
let finalPrompt = promptText;
if (shouldEnhance) {
finalPrompt = CONFIG.ENHANCEMENT_PREFIX + promptText;
debug('Enhanced prompt:', finalPrompt.substring(0, 80) + '...');
}
// Call the actual submission logic directly
return new Promise((resolve, reject) => {
try {
// Try multiple selectors for the prompt textarea
const selectors = [
'textarea[id*="prompt"]',
'textarea[placeholder*="prompt" i]',
'textarea[name*="prompt" i]',
'textarea[data-testid*="prompt"]',
'textarea[aria-label*="prompt" i]',
'textarea'
];
let textarea = null;
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element && element.offsetParent !== null) { // Check if visible
textarea = element;
break;
}
}
const submitButton = document.querySelector('button[type="submit"]');
if (!textarea || !submitButton) {
reject(new Error('Could not find prompt input or submit button'));
return;
}
// Focus the textarea first
textarea.focus();
// Set value using React/Vue compatible method
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
nativeInputValueSetter.call(textarea, finalPrompt);
// Trigger events that React/Vue apps listen for
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
const keyupEvent = new KeyboardEvent('keyup', { bubbles: true, cancelable: true });
textarea.dispatchEvent(inputEvent);
textarea.dispatchEvent(changeEvent);
textarea.dispatchEvent(keyupEvent);
// Verify the value was set
debug('Textarea value after setting:', textarea.value);
// Wait a bit for the app to process the input
setTimeout(() => {
submitButton.click();
debug('Submitted prompt:', finalPrompt.substring(0, 50) + '...');
resolve();
}, 500);
} catch (error) {
debug('Error submitting prompt:', error);
reject(error);
}
});
}
submitPrompt(promptText) {
try {
// Try multiple selectors for the prompt textarea
const selectors = [
'textarea[id*="prompt"]',
'textarea[placeholder*="prompt" i]',
'textarea[name*="prompt" i]',
'textarea[data-testid*="prompt"]',
'textarea[aria-label*="prompt" i]',
'textarea'
];
let textarea = null;
for (const selector of selectors) {
const element = document.querySelector(selector);
if (element && element.offsetParent !== null) { // Check if visible
textarea = element;
break;
}
}
const submitButton = document.querySelector('button[type="submit"]');
if (!textarea || !submitButton) {
throw new Error('Could not find prompt input or submit button');
}
// Focus the textarea first
textarea.focus();
// Set value using React/Vue compatible method
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
nativeInputValueSetter.call(textarea, promptText);
// Trigger events that React/Vue apps listen for
const inputEvent = new Event('input', { bubbles: true, cancelable: true });
const changeEvent = new Event('change', { bubbles: true, cancelable: true });
const keyupEvent = new KeyboardEvent('keyup', { bubbles: true, cancelable: true });
textarea.dispatchEvent(inputEvent);
textarea.dispatchEvent(changeEvent);
textarea.dispatchEvent(keyupEvent);
// Verify the value was set
debug('Textarea value after setting:', textarea.value);
// Wait a bit for the app to process the input
setTimeout(() => {
submitButton.click();
debug('Submitted prompt:', promptText.substring(0, 50) + '...');
}, 500);
} catch (error) {
debug('Error submitting prompt:', error);
throw error;
}
}
handlePromptCompletion(promptKey, success) {
const promptData = this.inFlightPrompts.get(promptKey);
if (!promptData) return;
this.inFlightPrompts.delete(promptKey);
if (!success && promptData.attempts < CONFIG.MAX_RETRIES) {
// Add to retry queue with exponential backoff
const delay = Math.min(1000 * Math.pow(2, promptData.attempts), 60000);
this.retryQueue.set(promptKey, {
prompt: promptData.prompt,
attempts: promptData.attempts,
nextRetry: Date.now() + delay,
playlistId: promptData.playlistId
});
debug('Queued prompt for retry:', promptKey, 'delay:', delay);
} else if (!success) {
debug('Prompt failed permanently:', promptKey);
const [playlistId, promptIndex] = promptKey.split('-');
showToast(`Prompt ${parseInt(promptIndex) + 1} failed after ${CONFIG.MAX_RETRIES} attempts`, 'error');
} else {
debug('Prompt completed successfully:', promptKey);
}
}
getRunningStatus() {
if (this.activePlaylists.size === 0) return null;
const playlists = [];
let totalPrompts = 0;
let totalCompleted = 0;
for (const [id, playlist] of this.activePlaylists.entries()) {
const completed = Math.max(0, playlist.lastIndexSent - this.getInFlightCountForPlaylist(id));
const progress = playlist.prompts.length > 0 ? (completed / playlist.prompts.length) * 100 : 0;
const onDeck = [];
for (let i = playlist.lastIndexSent; i < Math.min(playlist.lastIndexSent + 4, playlist.prompts.length); i++) {
onDeck.push({
index: i + 1,
prompt: playlist.prompts[i].substring(0, 30) + '...'
});
}
playlists.push({
id,
name: playlist.name,
progress: Math.round(progress),
completed,
total: playlist.prompts.length,
onDeck
});
totalPrompts += playlist.prompts.length;
totalCompleted += completed;
}
const overallProgress = totalPrompts > 0 ? (totalCompleted / totalPrompts) * 100 : 0;
return {
state: this.runningState,
overallProgress: Math.round(overallProgress),
totalCompleted,
totalPrompts,
playlists
};
}
getInFlightCountForPlaylist(playlistId) {
let count = 0;
for (const [key, data] of this.inFlightPrompts.entries()) {
if (key.toString().startsWith(`${playlistId}-`)) {
count++;
}
}
return count;
}
isPlaylistRunning(playlistId) {
return this.activePlaylists.has(playlistId);
}
}
// UI Management
class UIManager {
constructor(playlistManager) {
this.playlistManager = playlistManager;
this.shadowRoot = null;
this.isExpanded = false;
this.currentTab = 'playlists';
this.isDragging = false;
this.dragOffset = { x: 0, y: 0 };
this.position = { x: window.innerWidth - 50, y: 20 };
this.enhancePrompts = false;
}
init() {
this.createShadowRoot();
this.createFAB();
this.setupEventListeners();
this.setupDOMObserver();
debug('UI initialized');
}
createShadowRoot() {
const container = document.createElement('div');
container.id = 'hf-playlist-container';
container.style.cssText = `
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
pointer-events: none !important;
z-index: ${CONFIG.UI_Z_INDEX} !important;
`;
document.body.appendChild(container);
this.shadowRoot = container.attachShadow({ mode: 'closed' });
this.shadowRoot.innerHTML = this.getStyles();
this.container = container;
}
createFAB() {
const fab = document.createElement('div');
fab.className = 'hf-fab';
fab.innerHTML = '🚀';
fab.style.left = this.position.x + 'px';
fab.style.top = this.position.y + 'px';
this.shadowRoot.appendChild(fab);
this.fab = fab;
}
setupEventListeners() {
// FAB events
this.fab.addEventListener('mousedown', this.handleMouseDown.bind(this));
this.fab.addEventListener('click', this.handleFABClick.bind(this));
this.fab.addEventListener('dblclick', this.toggleMinimize.bind(this));
// Global events
document.addEventListener('mousemove', this.handleMouseMove.bind(this));
document.addEventListener('mouseup', this.handleMouseUp.bind(this));
window.addEventListener('resize', this.handleResize.bind(this));
window.addEventListener('beforeunload', this.cleanup.bind(this));
// Network events
window.addEventListener('online', () => {
if (this.playlistManager.runningState === 'paused') {
this.playlistManager.runningState = 'running';
this.playlistManager.startDispatchLoop();
showToast('Connection restored, resuming playlist', 'success');
}
});
window.addEventListener('offline', () => {
if (this.playlistManager.runningState === 'running') {
this.playlistManager.pausePlaylist();
showToast('Connection lost, pausing playlist', 'warning');
}
});
}
setupDOMObserver() {
// Watch for DOM changes that might remove our container
const observer = new MutationObserver(() => {
if (!document.contains(this.container)) {
debug('Container removed, re-inserting...');
document.body.appendChild(this.container);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
this.domObserver = observer;
}
handleMouseDown(e) {
if (e.detail === 1) { // Single click
this.isDragging = true;
this.dragOffset.x = e.clientX - this.position.x;
this.dragOffset.y = e.clientY - this.position.y;
e.preventDefault();
}
}
handleMouseMove(e) {
if (this.isDragging) {
this.position.x = e.clientX - this.dragOffset.x;
this.position.y = e.clientY - this.dragOffset.y;
// Keep within viewport
this.position.x = Math.max(0, Math.min(window.innerWidth - 40, this.position.x));
this.position.y = Math.max(0, Math.min(window.innerHeight - 40, this.position.y));
this.fab.style.left = this.position.x + 'px';
this.fab.style.top = this.position.y + 'px';
}
}
handleMouseUp() {
this.isDragging = false;
}
handleFABClick() {
if (!this.isDragging) {
this.togglePanel();
}
}
toggleMinimize() {
// TODO: Implement minimize functionality
debug('Toggle minimize');
}
handleResize() {
// Ensure FAB stays in viewport
this.position.x = Math.max(0, Math.min(window.innerWidth - 40, this.position.x));
this.position.y = Math.max(0, Math.min(window.innerHeight - 40, this.position.y));
this.fab.style.left = this.position.x + 'px';
this.fab.style.top = this.position.y + 'px';
}
togglePanel() {
this.isExpanded = !this.isExpanded;
if (this.isExpanded) {
this.createPanel();
} else {
this.removePanel();
}
}
async createPanel() {
if (this.shadowRoot.querySelector('.hf-panel')) return;
const panel = document.createElement('div');
panel.className = 'hf-panel';
panel.innerHTML = await this.getPanelHTML();
this.shadowRoot.appendChild(panel);
this.setupPanelEvents();
// Ensure the active tab is visible immediately
this.switchTabs();
this.updateUI();
}
removePanel() {
const panel = this.shadowRoot.querySelector('.hf-panel');
if (panel) {
panel.remove();
}
}
async getPanelHTML() {
const playlistsActive = this.currentTab === 'playlists' ? 'active' : '';
const runningActive = this.currentTab === 'running' ? 'active' : '';
const playlistsChecked = this.currentTab === 'playlists' ? 'checked' : '';
const runningChecked = this.currentTab === 'running' ? 'checked' : '';
return `
<div class="hf-panel-header">
<div class="hf-tabs">
<input type="radio" id="tab-playlists" name="tabs" ${playlistsChecked}>
<label for="tab-playlists">Playlists</label>
<input type="radio" id="tab-running" name="tabs" ${runningChecked}>
<label for="tab-running">Running</label>
</div>
<button class="hf-close-btn">×</button>
</div>
<div class="hf-panel-content">
<div class="hf-tab-content ${playlistsActive}" id="playlists-tab">
<div class="hf-search">
<input type="text" placeholder="Search playlists..." id="playlist-search">
</div>
<div class="hf-enhancement-toggle">
<label class="hf-checkbox-label">
<input type="checkbox" id="enhance-prompts" ${this.enhancePrompts ? 'checked' : ''}>
<span class="hf-checkbox-text">✨ Enhance prompts</span>
</label>
</div>
<div class="hf-playlist-list" id="playlist-list"></div>
<button class="hf-btn hf-btn-primary" id="new-playlist-btn">+ New Playlist</button>
</div>
<div class="hf-tab-content ${runningActive}" id="running-tab">
<div class="hf-running-status" id="running-status"></div>
</div>
</div>
`;
}
setupPanelEvents() {
// Tab switching
this.shadowRoot.querySelectorAll('input[name="tabs"]').forEach(tab => {
tab.addEventListener('change', (e) => {
this.currentTab = e.target.id.replace('tab-', '');
this.switchTabs();
this.updateUI();
});
});
// Close button
this.shadowRoot.querySelector('.hf-close-btn').addEventListener('click', () => {
this.togglePanel();
});
// New playlist button
const newPlaylistBtn = this.shadowRoot.querySelector('#new-playlist-btn');
debug('New playlist button found:', !!newPlaylistBtn);
if (newPlaylistBtn) {
newPlaylistBtn.addEventListener('click', () => {
debug('New playlist button clicked');
this.showNewPlaylistModal();
});
}
// Search
this.shadowRoot.querySelector('#playlist-search').addEventListener('input', (e) => {
this.filterPlaylists(e.target.value);
});
// Enhancement toggle
const enhanceToggle = this.shadowRoot.querySelector('#enhance-prompts');
if (enhanceToggle) {
enhanceToggle.addEventListener('change', (e) => {
this.enhancePrompts = e.target.checked;
debug('Prompt enhancement:', this.enhancePrompts ? 'enabled' : 'disabled');
showToast(
this.enhancePrompts ? 'Prompt enhancement enabled (affects pending prompts only)' : 'Prompt enhancement disabled',
'info'
);
// Note: This only affects future prompts, doesn't restart current queue
});
}
}
switchTabs() {
debug('Switching to tab:', this.currentTab);
const allTabs = this.shadowRoot.querySelectorAll('.hf-tab-content');
debug('Found tabs:', allTabs.length);
allTabs.forEach(tab => {
tab.classList.remove('active');
debug('Removed active from:', tab.id);
});
const activeTab = this.shadowRoot.querySelector(`#${this.currentTab}-tab`);
debug('Active tab element:', activeTab?.id);
if (activeTab) {
activeTab.classList.add('active');
debug('Added active to:', activeTab.id);
}
}
async updateUI() {
if (!this.isExpanded) return;
if (this.currentTab === 'playlists') {
await this.updatePlaylistsTab();
} else if (this.currentTab === 'running') {
this.updateRunningTab();
}
}
async updatePlaylistsTab() {
const listContainer = this.shadowRoot.querySelector('#playlist-list');
if (!listContainer) return;
try {
const playlists = await this.playlistManager.getAllPlaylists();
listContainer.innerHTML = playlists.map(playlist => {
const isRunning = this.playlistManager.isPlaylistRunning(playlist.id);
const runningClass = isRunning ? 'hf-playlist-running' : '';
const statusIcon = isRunning ? '🟢' : '';
const buttonIcon = isRunning ? '⏹' : '▶️';
const buttonClass = isRunning ? 'hf-stop-btn' : 'hf-start-btn';
return `
<div class="hf-playlist-item ${runningClass}" data-id="${playlist.id}">
<div class="hf-playlist-info">
<span class="hf-playlist-name">${statusIcon} ${playlist.name}</span>
<span class="hf-playlist-meta">(${playlist.prompts.length}) • ${playlist.lastIndexSent}/${playlist.prompts.length}</span>
</div>
<div class="hf-playlist-actions">
<button class="hf-btn hf-btn-small ${buttonClass}" data-id="${playlist.id}">${buttonIcon}</button>
<button class="hf-btn hf-btn-small hf-menu-btn" data-id="${playlist.id}">⋯</button>
</div>
</div>
`;
}).join('');
// Add event listeners
listContainer.querySelectorAll('.hf-start-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = parseInt(e.target.dataset.id);
this.playlistManager.startPlaylist(id);
setTimeout(() => this.updatePlaylistsTab(), 100); // Refresh to show new state
});
});
listContainer.querySelectorAll('.hf-stop-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = parseInt(e.target.dataset.id);
this.playlistManager.stopPlaylist(id);
setTimeout(() => this.updatePlaylistsTab(), 100); // Refresh to show new state
});
});
listContainer.querySelectorAll('.hf-menu-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = parseInt(e.target.dataset.id);
this.showPlaylistMenu(id);
});
});
} catch (error) {
debug('Error updating playlists tab:', error);
listContainer.innerHTML = '<div class="hf-error">Error loading playlists</div>';
}
}
updateRunningTab() {
const statusContainer = this.shadowRoot.querySelector('#running-status');
if (!statusContainer) return;
const status = this.playlistManager.getRunningStatus();
if (!status) {
statusContainer.innerHTML = '<div class="hf-no-running">No playlists running</div>';
return;
}
const playlistsHtml = status.playlists.map(playlist => `
<div class="hf-running-playlist">
<div class="hf-running-header">
<h3>▸ ${playlist.name}</h3>
<div class="hf-progress">
<div class="hf-progress-bar" style="width: ${playlist.progress}%"></div>
<span>${playlist.completed} / ${playlist.total}</span>
</div>
</div>
<div class="hf-running-queues">
<div class="hf-queue">
<h4>⏳ On-Deck</h4>
${playlist.onDeck.length > 0 ?
playlist.onDeck.map(item => `<div>#${item.index} "${item.prompt}"</div>`).join('') :
'<div class="hf-empty-queue">All prompts dispatched</div>'
}
</div>
</div>
<div class="hf-playlist-controls">
<button class="hf-btn hf-btn-small hf-stop-playlist-btn" data-id="${playlist.id}">⏹</button>
<button class="hf-btn hf-btn-small hf-reset-playlist-btn" data-id="${playlist.id}">🔄</button>
</div>
</div>
`).join('');
statusContainer.innerHTML = `
<div class="hf-overall-status">
<h3>Overall Progress</h3>
<div class="hf-progress">
<div class="hf-progress-bar" style="width: ${status.overallProgress}%"></div>
<span>${status.totalCompleted} / ${status.totalPrompts}</span>
</div>
</div>
<div class="hf-playlists-container">
${playlistsHtml}
</div>
<div class="hf-global-controls">
<button class="hf-btn ${status.state === 'running' ? 'hf-pause-btn' : 'hf-play-btn'}" id="play-pause-btn">
${status.state === 'running' ? '⏸' : '▶️'}
</button>
<button class="hf-btn hf-stop-btn" id="stop-all-btn">⏹ All</button>
<button class="hf-btn hf-reset-btn" id="reset-all-btn">🔄 All</button>
</div>
`;
// Add global control event listeners
const playPauseBtn = statusContainer.querySelector('#play-pause-btn');
const stopAllBtn = statusContainer.querySelector('#stop-all-btn');
const resetAllBtn = statusContainer.querySelector('#reset-all-btn');
playPauseBtn?.addEventListener('click', () => {
if (status.state === 'running') {
this.playlistManager.pausePlaylist();
} else {
this.playlistManager.runningState = 'running';
this.playlistManager.startDispatchLoop();
}
setTimeout(() => this.updateRunningTab(), 100);
});
stopAllBtn?.addEventListener('click', () => {
this.playlistManager.stopAllPlaylists();
setTimeout(() => this.updateRunningTab(), 100);
});
resetAllBtn?.addEventListener('click', () => {
this.playlistManager.resetProgress();
setTimeout(() => this.updateRunningTab(), 100);
});
// Add individual playlist control event listeners
statusContainer.querySelectorAll('.hf-stop-playlist-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = parseInt(e.target.dataset.id);
this.playlistManager.stopPlaylist(id);
setTimeout(() => this.updateRunningTab(), 100);
});
});
statusContainer.querySelectorAll('.hf-reset-playlist-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
const id = parseInt(e.target.dataset.id);
this.playlistManager.resetProgress(id);
setTimeout(() => this.updateRunningTab(), 100);
});
});
}
filterPlaylists(searchTerm) {
const items = this.shadowRoot.querySelectorAll('.hf-playlist-item');
items.forEach(item => {
const name = item.querySelector('.hf-playlist-name').textContent.toLowerCase();
const matches = name.includes(searchTerm.toLowerCase());
item.style.display = matches ? 'flex' : 'none';
});
}
showNewPlaylistModal() {
debug('Opening new playlist modal');
this.showModal('New Playlist', `
<div class="hf-form-group">
<label>Name:</label>
<input type="text" id="playlist-name" placeholder="Enter playlist name">
</div>
<div class="hf-form-group">
<label>Prompts (separated by ~):</label>
<textarea id="playlist-prompts" rows="10" placeholder="Prompt 1 ~ Prompt 2 ~ Prompt 3"></textarea>
</div>
`, async (modal) => {
const name = modal.querySelector('#playlist-name').value.trim();
const prompts = modal.querySelector('#playlist-prompts').value.trim();
if (!name || !prompts) {
showToast('Please fill in all fields', 'error');
return false;
}
try {
await this.playlistManager.createPlaylist(name, prompts);
showToast('Playlist created successfully', 'success');
this.updateUI();
return true;
} catch (error) {
showToast('Failed to create playlist', 'error');
return false;
}
});
}
async showPlaylistMenu(playlistId) {
debug('Show playlist menu for:', playlistId);
try {
const playlist = await db.playlists.get(playlistId);
if (!playlist) return;
const menu = document.createElement('div');
menu.className = 'hf-context-menu';
menu.innerHTML = `
<div class="hf-context-menu-item" data-action="rename">✏️ Rename</div>
<div class="hf-context-menu-item" data-action="replace">🔄 Replace</div>
<div class="hf-context-menu-item" data-action="append">➕ Append</div>
<div class="hf-context-menu-item" data-action="export">📤 Export</div>
<div class="hf-context-menu-item" data-action="delete">🗑️ Delete</div>
`;
// Position menu near the button
const button = this.shadowRoot.querySelector(`[data-id="${playlistId}"].hf-menu-btn`);
const rect = button.getBoundingClientRect();
menu.style.cssText = `
position: absolute;
top: ${rect.bottom + 5}px;
left: ${rect.left - 100}px;
z-index: ${CONFIG.UI_Z_INDEX + 1};
`;
this.shadowRoot.appendChild(menu);
// Add event listeners
menu.addEventListener('click', async (e) => {
const action = e.target.dataset.action;
if (action) {
await this.handlePlaylistAction(playlistId, action, playlist);
menu.remove();
}
});
// Close menu when clicking outside
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
menu.remove();
document.removeEventListener('click', closeMenu);
}
};
setTimeout(() => {
document.addEventListener('click', closeMenu);
}, 100);
} catch (error) {
debug('Error showing playlist menu:', error);
}
}
async handlePlaylistAction(playlistId, action, playlist) {
debug('Playlist action:', action, 'for playlist:', playlistId);
switch (action) {
case 'rename':
this.showRenameModal(playlistId, playlist.name);
break;
case 'replace':
this.showReplaceModal(playlistId, playlist);
break;
case 'append':
this.showAppendModal(playlistId, playlist);
break;
case 'export':
this.exportPlaylist(playlist);
break;
case 'delete':
if (confirm(`Delete playlist "${playlist.name}"?`)) {
await this.playlistManager.deletePlaylist(playlistId);
showToast('Playlist deleted', 'success');
this.updateUI();
}
break;
}
}
showRenameModal(playlistId, currentName) {
this.showModal('Rename Playlist', `
<div class="hf-form-group">
<label>Name:</label>
<input type="text" id="playlist-name" value="${currentName}" placeholder="Enter playlist name">
</div>
`, async (modal) => {
const name = modal.querySelector('#playlist-name').value.trim();
if (!name) {
showToast('Please enter a name', 'error');
return false;
}
try {
const uniqueName = await this.playlistManager.generateUniqueName(name);
await this.playlistManager.updatePlaylist(playlistId, { name: uniqueName });
showToast('Playlist renamed', 'success');
this.updateUI();
return true;
} catch (error) {
showToast('Failed to rename playlist', 'error');
return false;
}
});
}
showReplaceModal(playlistId, playlist) {
this.showModal('Replace Playlist', `
<div class="hf-form-group">
<label>Prompts (separated by ~):</label>
<textarea id="playlist-prompts" rows="10" placeholder="Prompt 1 ~ Prompt 2 ~ Prompt 3">${playlist.prompts.join(' ~ ')}</textarea>
</div>
`, async (modal) => {
const prompts = modal.querySelector('#playlist-prompts').value.trim();
if (!prompts) {
showToast('Please enter prompts', 'error');
return false;
}
try {
const promptsArray = prompts.split('~').map(p => p.trim()).filter(p => p);
await this.playlistManager.updatePlaylist(playlistId, {
prompts: promptsArray,
lastIndexSent: 0
});
showToast('Playlist replaced', 'success');
this.updateUI();
return true;
} catch (error) {
showToast('Failed to replace playlist', 'error');
return false;
}
});
}
showAppendModal(playlistId, playlist) {
this.showModal('Append to Playlist', `
<div class="hf-form-group">
<label>Additional Prompts (separated by ~):</label>
<textarea id="playlist-prompts" rows="6" placeholder="New prompt 1 ~ New prompt 2"></textarea>
</div>
`, async (modal) => {
const prompts = modal.querySelector('#playlist-prompts').value.trim();
if (!prompts) {
showToast('Please enter prompts to append', 'error');
return false;
}
try {
const newPrompts = prompts.split('~').map(p => p.trim()).filter(p => p);
const updatedPrompts = [...playlist.prompts, ...newPrompts];
await this.playlistManager.updatePlaylist(playlistId, { prompts: updatedPrompts });
showToast(`Added ${newPrompts.length} prompts`, 'success');
// Update UI for both tabs
await this.updateUI();
// If this playlist is currently running, refresh the active playlist data
if (this.playlistManager.activePlaylists.has(playlistId)) {
const updatedPlaylist = await db.playlists.get(playlistId);
if (updatedPlaylist) {
this.playlistManager.activePlaylists.set(playlistId, updatedPlaylist);
}
if (this.currentTab === 'running') {
this.updateRunningTab();
}
}
return true;
} catch (error) {
showToast('Failed to append prompts', 'error');
return false;
}
});
}
exportPlaylist(playlist) {
const exportData = {
name: playlist.name,
prompts: playlist.prompts,
created: playlist.created,
updated: playlist.updated
};
const jsonString = JSON.stringify(exportData, null, 2);
if (navigator.clipboard) {
navigator.clipboard.writeText(jsonString).then(() => {
showToast('Playlist copied to clipboard', 'success');
}).catch(() => {
this.fallbackCopyToClipboard(jsonString);
});
} else {
this.fallbackCopyToClipboard(jsonString);
}
}
fallbackCopyToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
document.body.appendChild(textArea);
textArea.select();
try {
document.execCommand('copy');
showToast('Playlist copied to clipboard', 'success');
} catch (err) {
showToast('Failed to copy to clipboard', 'error');
}
document.body.removeChild(textArea);
}
showModal(title, content, onSave) {
debug('Creating modal:', title);
const modal = document.createElement('div');
modal.className = 'hf-modal-overlay';
modal.innerHTML = `
<div class="hf-modal">
<div class="hf-modal-header">
<h3>${title}</h3>
<button class="hf-modal-close">×</button>
</div>
<div class="hf-modal-content">${content}</div>
<div class="hf-modal-footer">
<button class="hf-btn hf-btn-secondary hf-modal-cancel">Cancel</button>
<button class="hf-btn hf-btn-primary hf-modal-save">Save</button>
</div>
</div>
`;
this.shadowRoot.appendChild(modal);
// Event listeners
const closeModal = () => modal.remove();
modal.querySelector('.hf-modal-close').addEventListener('click', closeModal);
modal.querySelector('.hf-modal-cancel').addEventListener('click', closeModal);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModal();
});
modal.querySelector('.hf-modal-save').addEventListener('click', async () => {
if (onSave) {
const shouldClose = await onSave(modal);
if (shouldClose) closeModal();
} else {
closeModal();
}
});
// Focus first input
const firstInput = modal.querySelector('input, textarea');
if (firstInput) firstInput.focus();
}
cleanup() {
this.playlistManager.stopPlaylist();
if (this.domObserver) {
this.domObserver.disconnect();
}
debug('UI cleanup completed');
}
getStyles() {
return `
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.hf-fab {
position: fixed !important;
width: 28px;
height: 28px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
border-radius: 50%;
display: flex !important;
align-items: center;
justify-content: center;
cursor: pointer !important;
user-select: none;
z-index: ${CONFIG.UI_Z_INDEX} !important;
font-size: 12px;
box-shadow: 0 4px 20px rgba(0,0,0,0.4) !important;
transition: transform 0.2s;
pointer-events: auto !important;
opacity: 1 !important;
visibility: visible !important;
border: 2px solid rgba(255,255,255,0.3) !important;
}
.hf-fab:hover {
transform: scale(1.1);
}
.hf-panel {
position: fixed;
top: 60px;
right: 12px;
width: 320px;
max-height: 70vh;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
z-index: ${CONFIG.UI_Z_INDEX - 1} !important;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.2);
pointer-events: auto !important;
color: #333 !important;
}
.hf-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
border-bottom: 1px solid rgba(0,0,0,0.1);
background: rgba(255,255,255,0.5);
}
.hf-tabs {
display: flex;
gap: 8px;
}
.hf-tabs input[type="radio"] {
display: none;
}
.hf-tabs label {
padding: 4px 12px;
border-radius: 16px;
cursor: pointer;
font-size: 12px;
background: rgba(0,0,0,0.1);
transition: all 0.2s;
color: #333 !important;
}
.hf-tabs input[type="radio"]:checked + label {
background: #667eea;
color: white !important;
}
.hf-close-btn {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
color: #333 !important;
}
.hf-close-btn:hover {
background: rgba(0,0,0,0.1);
}
.hf-panel-content {
padding: 12px;
max-height: calc(70vh - 60px);
overflow-y: auto;
}
.hf-tab-content {
display: none;
}
.hf-tab-content.active {
display: block;
}
.hf-search input {
width: 100%;
padding: 8px;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 6px;
margin-bottom: 8px;
font-size: 12px;
}
.hf-enhancement-toggle {
margin-bottom: 12px;
padding: 8px;
background: rgba(102, 126, 234, 0.1);
border-radius: 6px;
border: 1px solid rgba(102, 126, 234, 0.2);
}
.hf-checkbox-label {
display: flex !important;
align-items: center !important;
gap: 8px !important;
cursor: pointer !important;
font-size: 12px !important;
color: #333 !important;
}
.hf-checkbox-label input[type="checkbox"] {
width: auto !important;
margin: 0 !important;
padding: 0 !important;
}
.hf-checkbox-text {
font-weight: 500 !important;
color: #667eea !important;
}
.hf-playlist-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
margin-bottom: 4px;
border-radius: 6px;
background: rgba(0,0,0,0.02);
transition: all 0.2s;
}
.hf-playlist-item.hf-playlist-running {
background: rgba(102, 126, 234, 0.1);
border: 1px solid rgba(102, 126, 234, 0.3);
}
.hf-playlist-info {
flex: 1;
min-width: 0;
}
.hf-playlist-name {
display: block;
font-size: 12px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hf-playlist-meta {
font-size: 10px;
color: #666;
}
.hf-playlist-actions {
display: flex;
gap: 4px;
}
.hf-btn {
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.hf-btn-small {
padding: 4px 8px;
font-size: 10px;
}
.hf-btn-primary {
background: #667eea !important;
color: white !important;
padding: 8px 16px;
width: 100%;
margin-top: 12px;
}
.hf-btn-secondary {
background: rgba(0,0,0,0.1);
color: #333 !important;
padding: 8px 16px;
}
.hf-btn:hover {
opacity: 0.8;
transform: translateY(-1px);
}
.hf-running-header h3 {
font-size: 14px;
margin-bottom: 8px;
}
.hf-progress {
position: relative;
height: 20px;
background: rgba(0,0,0,0.1);
border-radius: 10px;
overflow: hidden;
margin-bottom: 12px;
}
.hf-progress-bar {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
transition: width 0.3s;
}
.hf-progress span {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 10px;
font-weight: bold;
color: #333;
}
.hf-running-queues {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-bottom: 12px;
}
.hf-queue h4 {
font-size: 10px;
margin-bottom: 4px;
color: #666;
}
.hf-queue > div {
font-size: 9px;
padding: 2px 4px;
background: rgba(0,0,0,0.05);
border-radius: 3px;
margin-bottom: 2px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.hf-overall-status {
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.hf-overall-status h3 {
font-size: 14px;
margin-bottom: 8px;
color: #333;
}
.hf-playlists-container {
max-height: 300px;
overflow-y: auto;
margin-bottom: 16px;
}
.hf-running-playlist {
margin-bottom: 16px;
padding: 12px;
background: rgba(0,0,0,0.02);
border-radius: 8px;
border: 1px solid rgba(0,0,0,0.1);
}
.hf-playlist-controls {
display: flex;
gap: 4px;
justify-content: flex-end;
margin-top: 8px;
}
.hf-global-controls {
display: flex;
gap: 8px;
justify-content: center;
padding-top: 12px;
border-top: 1px solid rgba(0,0,0,0.1);
}
.hf-global-controls .hf-btn {
padding: 8px 12px;
background: rgba(0,0,0,0.1);
}
.hf-empty-queue {
font-size: 9px;
color: #999;
font-style: italic;
padding: 2px 4px;
}
.hf-no-running {
text-align: center;
color: #666;
font-size: 12px;
padding: 20px;
}
.hf-error {
color: #e74c3c;
font-size: 12px;
text-align: center;
padding: 20px;
}
.hf-modal-overlay {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
background: rgba(0,0,0,0.5) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
z-index: ${CONFIG.UI_Z_INDEX + 10} !important;
pointer-events: auto !important;
}
.hf-modal {
background: white !important;
border-radius: 12px !important;
width: 90% !important;
max-width: 400px !important;
max-height: 80vh !important;
overflow: hidden !important;
box-shadow: 0 10px 40px rgba(0,0,0,0.3) !important;
pointer-events: auto !important;
position: relative !important;
color: #333 !important;
}
.hf-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid rgba(0,0,0,0.1);
background: #f8f9fa;
}
.hf-modal-header h3 {
font-size: 16px;
margin: 0;
color: #333 !important;
}
.hf-modal-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
padding: 4px;
border-radius: 4px;
color: #333 !important;
}
.hf-modal-content {
padding: 16px;
max-height: 50vh;
overflow-y: auto;
}
.hf-modal-footer {
display: flex;
gap: 8px;
padding: 16px;
border-top: 1px solid rgba(0,0,0,0.1);
background: #f8f9fa;
justify-content: flex-end;
}
.hf-form-group {
margin-bottom: 16px;
}
.hf-form-group label {
display: block;
margin-bottom: 4px;
font-size: 12px;
font-weight: 500;
color: #333 !important;
}
.hf-form-group input,
.hf-form-group textarea {
width: 100%;
padding: 8px;
border: 1px solid rgba(0,0,0,0.2);
border-radius: 6px;
font-size: 12px;
font-family: inherit;
resize: vertical;
color: #333 !important;
background: white !important;
}
.hf-toast {
position: fixed;
top: 80px;
right: 12px;
padding: 12px 16px;
border-radius: 8px;
color: white;
font-size: 12px;
z-index: ${CONFIG.UI_Z_INDEX + 5};
animation: hf-toast-in 0.3s ease-out;
max-width: 300px;
word-wrap: break-word;
pointer-events: auto !important;
}
.hf-toast.success { background: #27ae60; }
.hf-toast.error { background: #e74c3c; }
.hf-toast.warning { background: #f39c12; }
.hf-toast.info { background: #3498db; }
.hf-context-menu {
background: white !important;
border-radius: 8px !important;
box-shadow: 0 4px 20px rgba(0,0,0,0.2) !important;
border: 1px solid rgba(0,0,0,0.1) !important;
overflow: hidden !important;
min-width: 140px !important;
pointer-events: auto !important;
}
.hf-context-menu-item {
padding: 8px 12px !important;
cursor: pointer !important;
font-size: 12px !important;
color: #333 !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
transition: background-color 0.2s !important;
}
.hf-context-menu-item:hover {
background-color: rgba(102, 126, 234, 0.1) !important;
}
.hf-context-menu-item:last-child {
color: #e74c3c !important;
}
@keyframes hf-toast-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
`;
}
}
// Toast notifications
function showToast(message, type = 'info') {
const container = document.querySelector('#hf-playlist-container');
if (!container?.shadowRoot) return;
const toast = document.createElement('div');
toast.className = `hf-toast ${type}`;
toast.textContent = message;
container.shadowRoot.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
debug('Toast:', type, message);
}
// Initialize the application
async function init() {
try {
debug('Initializing Higgsfield Playlist Automation...');
await initDatabase();
const playlistManager = new PlaylistManager();
const uiManager = new UIManager(playlistManager);
// Set global reference for enhancement checking
window.hfUIManager = uiManager;
uiManager.init();
// Update UI periodically when running
setInterval(() => {
if (uiManager.isExpanded) {
if (uiManager.currentTab === 'running') {
uiManager.updateRunningTab();
} else if (uiManager.currentTab === 'playlists') {
// Refresh playlist list to show running indicators
uiManager.updatePlaylistsTab();
}
}
}, 1000);
debug('Initialization complete');
} catch (error) {
console.error('Failed to initialize Higgsfield Playlist Automation:', error);
}
}
// Start the application when DOM is ready
function delayedInit() {
// Wait a bit for the SPA to finish rendering
setTimeout(init, 2000);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', delayedInit);
} else {
delayedInit();
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment